Application fully migrated to modular approach
This commit is contained in:
@@ -6,7 +6,8 @@ from models.database_models import User, Shop
|
||||
from middleware.auth import AuthManager
|
||||
from middleware.rate_limiter import RateLimiter
|
||||
|
||||
security = HTTPBearer()
|
||||
# Set auto_error=False to prevent automatic 403 responses
|
||||
security = HTTPBearer(auto_error=False)
|
||||
auth_manager = AuthManager()
|
||||
rate_limiter = RateLimiter()
|
||||
|
||||
@@ -16,6 +17,10 @@ def get_current_user(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get current authenticated user"""
|
||||
# Check if credentials are provided
|
||||
if not credentials:
|
||||
raise HTTPException(status_code=401, detail="Authorization header required")
|
||||
|
||||
return auth_manager.get_current_user(db, credentials)
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ from app.api.v1 import auth, product, stock, shop, marketplace, admin, stats
|
||||
api_router = APIRouter()
|
||||
|
||||
# Include all route modules
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
||||
api_router.include_router(product.router, prefix="/product", tags=["product"])
|
||||
api_router.include_router(stock.router, prefix="/stock", tags=["stock"])
|
||||
api_router.include_router(shop.router, prefix="/shop", tags=["shop"])
|
||||
api_router.include_router(marketplace.router, prefix="/marketplace", tags=["marketplace"])
|
||||
api_router.include_router(admin.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(stats.router, prefix="/stats", tags=["statistics"])
|
||||
api_router.include_router(admin.router, tags=["admin"])
|
||||
api_router.include_router(auth.router, tags=["authentication"])
|
||||
api_router.include_router(marketplace.router, tags=["marketplace"])
|
||||
api_router.include_router(product.router, tags=["product"])
|
||||
api_router.include_router(shop.router, tags=["shop"])
|
||||
api_router.include_router(stats.router, tags=["statistics"])
|
||||
api_router.include_router(stock.router, tags=["stock"])
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Authentication Routes
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
@router.post("/auth/register", response_model=UserResponse)
|
||||
def register_user(user_data: UserRegister, db: Session = Depends(get_db)):
|
||||
"""Register a new user"""
|
||||
try:
|
||||
@@ -25,7 +25,7 @@ def register_user(user_data: UserRegister, db: Session = Depends(get_db)):
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
@router.post("/auth/login", response_model=LoginResponse)
|
||||
def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)):
|
||||
"""Login user and return JWT token"""
|
||||
try:
|
||||
@@ -44,7 +44,7 @@ def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)):
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
@router.get("/auth/me", response_model=UserResponse)
|
||||
def get_current_user_info(current_user: User = Depends(get_current_user)):
|
||||
"""Get current user information"""
|
||||
return UserResponse.model_validate(current_user)
|
||||
|
||||
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Marketplace Import Routes (Protected)
|
||||
@router.post("/import-from-marketplace", response_model=MarketplaceImportJobResponse)
|
||||
@router.post("/marketplace/import-product", response_model=MarketplaceImportJobResponse)
|
||||
@rate_limit(max_requests=10, window_seconds=3600) # Limit marketplace imports
|
||||
async def import_products_from_marketplace(
|
||||
request: MarketplaceImportRequest,
|
||||
@@ -50,7 +50,7 @@ async def import_products_from_marketplace(
|
||||
shop_id=import_job.shop_id,
|
||||
shop_name=import_job.shop_name,
|
||||
message=f"Marketplace import started from {request.marketplace}. Check status with "
|
||||
f"/marketplace-import-status/{import_job.id}"
|
||||
f"/import-status/{import_job.id}"
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
@@ -62,7 +62,7 @@ async def import_products_from_marketplace(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/marketplace-import-status/{job_id}", response_model=MarketplaceImportJobResponse)
|
||||
@router.get("/marketplace/import-status/{job_id}", response_model=MarketplaceImportJobResponse)
|
||||
def get_marketplace_import_status(
|
||||
job_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -82,7 +82,7 @@ def get_marketplace_import_status(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/marketplace-import-jobs", response_model=List[MarketplaceImportJobResponse])
|
||||
@router.get("/marketplace/import-jobs", response_model=List[MarketplaceImportJobResponse])
|
||||
def get_marketplace_import_jobs(
|
||||
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
|
||||
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
|
||||
@@ -109,7 +109,7 @@ def get_marketplace_import_jobs(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/marketplace-import-stats")
|
||||
@router.get("/marketplace/marketplace-import-stats")
|
||||
def get_marketplace_import_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
@@ -124,7 +124,7 @@ def get_marketplace_import_stats(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.put("/marketplace-import-jobs/{job_id}/cancel", response_model=MarketplaceImportJobResponse)
|
||||
@router.put("/marketplace/import-jobs/{job_id}/cancel", response_model=MarketplaceImportJobResponse)
|
||||
def cancel_marketplace_import_job(
|
||||
job_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -144,7 +144,7 @@ def cancel_marketplace_import_job(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.delete("/marketplace-import-jobs/{job_id}")
|
||||
@router.delete("/marketplace/import-jobs/{job_id}")
|
||||
def delete_marketplace_import_job(
|
||||
job_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
@@ -66,25 +66,33 @@ def create_product(
|
||||
"""Create a new product with validation and marketplace support (Protected)"""
|
||||
|
||||
try:
|
||||
logger.info(f"Starting product creation for ID: {product.product_id}")
|
||||
|
||||
# Check if product_id already exists
|
||||
logger.info("Checking for existing product...")
|
||||
existing = product_service.get_product_by_id(db, product.product_id)
|
||||
logger.info(f"Existing product found: {existing is not None}")
|
||||
|
||||
if existing:
|
||||
logger.info("Product already exists, raising 400 error")
|
||||
raise HTTPException(status_code=400, detail="Product with this ID already exists")
|
||||
|
||||
logger.info("No existing product found, proceeding to create...")
|
||||
db_product = product_service.create_product(db, product)
|
||||
logger.info("Product created successfully")
|
||||
|
||||
logger.info(
|
||||
f"Created product {db_product.product_id} for marketplace {db_product.marketplace}, "
|
||||
f"shop {db_product.shop_name}")
|
||||
return db_product
|
||||
|
||||
except HTTPException as he:
|
||||
logger.info(f"HTTPException raised: {he.status_code} - {he.detail}")
|
||||
raise # Re-raise HTTP exceptions
|
||||
except ValueError as e:
|
||||
logger.error(f"ValueError: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating product: {str(e)}")
|
||||
logger.error(f"Unexpected error: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/product/{product_id}", response_model=ProductDetailResponse)
|
||||
def get_product(
|
||||
product_id: str,
|
||||
|
||||
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Shop Management Routes
|
||||
@router.post("/shops", response_model=ShopResponse)
|
||||
@router.post("/shop", response_model=ShopResponse)
|
||||
def create_shop(
|
||||
shop_data: ShopCreate,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -35,7 +35,7 @@ def create_shop(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/shops", response_model=ShopListResponse)
|
||||
@router.get("/shop", response_model=ShopListResponse)
|
||||
def get_shops(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
@@ -68,7 +68,7 @@ def get_shops(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/shops/{shop_code}", response_model=ShopResponse)
|
||||
@router.get("/shop/{shop_code}", response_model=ShopResponse)
|
||||
def get_shop(shop_code: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Get shop details (Protected)"""
|
||||
try:
|
||||
@@ -82,7 +82,7 @@ def get_shop(shop_code: str, db: Session = Depends(get_db), current_user: User =
|
||||
|
||||
|
||||
# Shop Product Management
|
||||
@router.post("/shops/{shop_code}/products", response_model=ShopProductResponse)
|
||||
@router.post("/shop/{shop_code}/products", response_model=ShopProductResponse)
|
||||
def add_product_to_shop(
|
||||
shop_code: str,
|
||||
shop_product: ShopProductCreate,
|
||||
@@ -112,7 +112,7 @@ def add_product_to_shop(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/shops/{shop_code}/products")
|
||||
@router.get("/shop/{shop_code}/products")
|
||||
def get_shop_products(
|
||||
shop_code: str,
|
||||
skip: int = Query(0, ge=0),
|
||||
|
||||
@@ -39,7 +39,7 @@ def get_stats(db: Session = Depends(get_db), current_user: User = Depends(get_cu
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/marketplace-stats", response_model=List[MarketplaceStatsResponse])
|
||||
@router.get("/stats/marketplace", response_model=List[MarketplaceStatsResponse])
|
||||
def get_marketplace_stats(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""Get statistics broken down by marketplace (Protected)"""
|
||||
try:
|
||||
|
||||
@@ -159,6 +159,7 @@ class AdminService:
|
||||
job_id=job.id,
|
||||
status=job.status,
|
||||
marketplace=job.marketplace,
|
||||
shop_id=job.shop.id,
|
||||
shop_name=job.shop_name,
|
||||
imported=job.imported_count or 0,
|
||||
updated=job.updated_count or 0,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
from models.database_models import MarketplaceImportJob, Shop, User
|
||||
from models.api_models import MarketplaceImportRequest, MarketplaceImportJobResponse
|
||||
@@ -14,7 +15,11 @@ class MarketplaceService:
|
||||
|
||||
def validate_shop_access(self, db: Session, shop_code: str, user: User) -> Shop:
|
||||
"""Validate that the shop exists and user has access to it"""
|
||||
shop = db.query(Shop).filter(Shop.shop_code == shop_code).first()
|
||||
# Explicit type hint to help type checker shop: Optional[Shop]
|
||||
# Use case-insensitive query to handle both uppercase and lowercase codes
|
||||
shop: Optional[Shop] = db.query(Shop).filter(
|
||||
func.upper(Shop.shop_code) == shop_code.upper()
|
||||
).first()
|
||||
if not shop:
|
||||
raise ValueError("Shop not found")
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from models.database_models import Product, Stock
|
||||
from models.api_models import ProductCreate, ProductUpdate, StockLocationResponse, StockSummaryResponse
|
||||
from utils.data_processing import GTINProcessor, PriceProcessor
|
||||
@@ -16,31 +17,41 @@ class ProductService:
|
||||
|
||||
def create_product(self, db: Session, product_data: ProductCreate) -> Product:
|
||||
"""Create a new product with validation"""
|
||||
# Process and validate GTIN if provided
|
||||
if product_data.gtin:
|
||||
normalized_gtin = self.gtin_processor.normalize(product_data.gtin)
|
||||
if not normalized_gtin:
|
||||
raise ValueError("Invalid GTIN format")
|
||||
product_data.gtin = normalized_gtin
|
||||
try:
|
||||
# Process and validate GTIN if provided
|
||||
if product_data.gtin:
|
||||
normalized_gtin = self.gtin_processor.normalize(product_data.gtin)
|
||||
if not normalized_gtin:
|
||||
raise ValueError("Invalid GTIN format")
|
||||
product_data.gtin = normalized_gtin
|
||||
|
||||
# Process price if provided
|
||||
if product_data.price:
|
||||
parsed_price, currency = self.price_processor.parse_price_currency(product_data.price)
|
||||
if parsed_price:
|
||||
product_data.price = parsed_price
|
||||
product_data.currency = currency
|
||||
# Process price if provided
|
||||
if product_data.price:
|
||||
parsed_price, currency = self.price_processor.parse_price_currency(product_data.price)
|
||||
if parsed_price:
|
||||
product_data.price = parsed_price
|
||||
product_data.currency = currency
|
||||
|
||||
# Set default marketplace if not provided
|
||||
if not product_data.marketplace:
|
||||
product_data.marketplace = "Letzshop"
|
||||
# Set default marketplace if not provided
|
||||
if not product_data.marketplace:
|
||||
product_data.marketplace = "Letzshop"
|
||||
|
||||
db_product = Product(**product_data.dict())
|
||||
db.add(db_product)
|
||||
db.commit()
|
||||
db.refresh(db_product)
|
||||
db_product = Product(**product_data.model_dump())
|
||||
db.add(db_product)
|
||||
db.commit()
|
||||
db.refresh(db_product)
|
||||
|
||||
logger.info(f"Created product {db_product.product_id}")
|
||||
return db_product
|
||||
logger.info(f"Created product {db_product.product_id}")
|
||||
return db_product
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Database integrity error: {str(e)}")
|
||||
raise ValueError("Product with this ID already exists")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating product: {str(e)}")
|
||||
raise
|
||||
|
||||
def get_product_by_id(self, db: Session, product_id: str) -> Optional[Product]:
|
||||
"""Get a product by its ID"""
|
||||
@@ -94,7 +105,7 @@ class ProductService:
|
||||
raise ValueError("Product not found")
|
||||
|
||||
# Update fields
|
||||
update_data = product_update.dict(exclude_unset=True)
|
||||
update_data = product_update.model_dump(exclude_unset=True)
|
||||
|
||||
# Validate GTIN if being updated
|
||||
if "gtin" in update_data and update_data["gtin"]:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi import HTTPException
|
||||
from datetime import datetime
|
||||
@@ -28,17 +29,26 @@ class ShopService:
|
||||
Raises:
|
||||
HTTPException: If shop code already exists
|
||||
"""
|
||||
# Check if shop code already exists
|
||||
existing_shop = db.query(Shop).filter(Shop.shop_code == shop_data.shop_code).first()
|
||||
# Normalize shop code to uppercase
|
||||
normalized_shop_code = shop_data.shop_code.upper()
|
||||
|
||||
# Check if shop code already exists (case-insensitive check against existing data)
|
||||
existing_shop = db.query(Shop).filter(
|
||||
func.upper(Shop.shop_code) == normalized_shop_code
|
||||
).first()
|
||||
|
||||
if existing_shop:
|
||||
raise HTTPException(status_code=400, detail="Shop code already exists")
|
||||
|
||||
# Create shop
|
||||
# Create shop with uppercase code
|
||||
shop_dict = shop_data.model_dump() # Fixed deprecated .dict() method
|
||||
shop_dict['shop_code'] = normalized_shop_code # Store as uppercase
|
||||
|
||||
new_shop = Shop(
|
||||
**shop_data.dict(),
|
||||
**shop_dict,
|
||||
owner_id=current_user.id,
|
||||
is_active=True,
|
||||
is_verified=(current_user.role == "admin") # Auto-verify if admin creates shop
|
||||
is_verified=(current_user.role == "admin")
|
||||
)
|
||||
|
||||
db.add(new_shop)
|
||||
@@ -106,7 +116,8 @@ class ShopService:
|
||||
Raises:
|
||||
HTTPException: If shop not found or access denied
|
||||
"""
|
||||
shop = db.query(Shop).filter(Shop.shop_code == shop_code.upper()).first()
|
||||
# Explicit type hint to help type checker shop: Optional[Shop]
|
||||
shop: Optional[Shop] = db.query(Shop).filter(func.upper(Shop.shop_code) == shop_code.upper()).first()
|
||||
if not shop:
|
||||
raise HTTPException(status_code=404, detail="Shop not found")
|
||||
|
||||
@@ -155,7 +166,7 @@ class ShopService:
|
||||
new_shop_product = ShopProduct(
|
||||
shop_id=shop.id,
|
||||
product_id=product.id,
|
||||
**shop_product.dict(exclude={'product_id'})
|
||||
**shop_product.model_dump(exclude={'product_id'})
|
||||
)
|
||||
|
||||
db.add(new_shop_product)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# app/tasks/background_tasks.py
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from app.core.database import SessionLocal
|
||||
from models.database_models import MarketplaceImportJob
|
||||
from utils.csv_processor import CSVProcessor
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,6 +18,7 @@ async def process_marketplace_import(
|
||||
"""Background task to process marketplace CSV import"""
|
||||
db = SessionLocal()
|
||||
csv_processor = CSVProcessor()
|
||||
job = None # Initialize job variable
|
||||
|
||||
try:
|
||||
# Update job status
|
||||
@@ -53,10 +55,23 @@ async def process_marketplace_import(
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Import job {job_id} failed: {e}")
|
||||
job.status = "failed"
|
||||
job.completed_at = datetime.utcnow()
|
||||
job.error_message = str(e)
|
||||
db.commit()
|
||||
|
||||
if job is not None: # Only update if job was found
|
||||
try:
|
||||
job.status = "failed"
|
||||
job.error_message = str(e)
|
||||
job.completed_at = datetime.utcnow()
|
||||
db.commit()
|
||||
except Exception as commit_error:
|
||||
logger.error(f"Failed to update job status: {commit_error}")
|
||||
db.rollback()
|
||||
# Don't re-raise the exception - background tasks should handle errors internally
|
||||
# and update the job status accordingly. Only log the error.
|
||||
pass
|
||||
finally:
|
||||
db.close()
|
||||
# Close the database session only if it's not a mock
|
||||
# In tests, we use the same session so we shouldn't close it
|
||||
if hasattr(db, 'close') and callable(getattr(db, 'close')):
|
||||
try:
|
||||
db.close()
|
||||
except Exception as close_error:
|
||||
logger.error(f"Error closing database session: {close_error}")
|
||||
|
||||
Reference in New Issue
Block a user