from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks from sqlalchemy.orm import Session from app.core.database import get_db from app.api.deps import get_current_user from app.tasks.background_tasks import process_marketplace_import from middleware.decorators import rate_limit from models.api_models import MarketplaceImportJobResponse, MarketplaceImportRequest, StockResponse, \ StockSummaryResponse from models.database_models import User, MarketplaceImportJob, Shop from datetime import datetime import logging router = APIRouter() logger = logging.getLogger(__name__) # Stock Management Routes (Protected) @router.post("/stock", response_model=StockResponse) def set_stock(stock: StockCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): """Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)""" # Normalize GTIN def normalize_gtin(gtin_value): if not gtin_value: return None gtin_str = str(gtin_value).strip() if '.' in gtin_str: gtin_str = gtin_str.split('.')[0] gtin_clean = ''.join(filter(str.isdigit, gtin_str)) if len(gtin_clean) in [8, 12, 13, 14]: return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12) return gtin_clean if gtin_clean else None normalized_gtin = normalize_gtin(stock.gtin) if not normalized_gtin: raise HTTPException(status_code=400, detail="Invalid GTIN format") # Check if stock entry already exists for this GTIN and location existing_stock = db.query(Stock).filter( Stock.gtin == normalized_gtin, Stock.location == stock.location.strip().upper() ).first() if existing_stock: # Update existing stock (SET to exact quantity) old_quantity = existing_stock.quantity existing_stock.quantity = stock.quantity existing_stock.updated_at = datetime.utcnow() db.commit() db.refresh(existing_stock) logger.info(f"Updated stock for GTIN {normalized_gtin} at {stock.location}: {old_quantity} → {stock.quantity}") return existing_stock else: # Create new stock entry new_stock = Stock( gtin=normalized_gtin, location=stock.location.strip().upper(), quantity=stock.quantity ) db.add(new_stock) db.commit() db.refresh(new_stock) logger.info(f"Created new stock for GTIN {normalized_gtin} at {stock.location}: {stock.quantity}") return new_stock @router.post("/stock/add", response_model=StockResponse) def add_stock(stock: StockAdd, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): """Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)""" # Normalize GTIN def normalize_gtin(gtin_value): if not gtin_value: return None gtin_str = str(gtin_value).strip() if '.' in gtin_str: gtin_str = gtin_str.split('.')[0] gtin_clean = ''.join(filter(str.isdigit, gtin_str)) if len(gtin_clean) in [8, 12, 13, 14]: return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12) return gtin_clean if gtin_clean else None normalized_gtin = normalize_gtin(stock.gtin) if not normalized_gtin: raise HTTPException(status_code=400, detail="Invalid GTIN format") # Check if stock entry already exists for this GTIN and location existing_stock = db.query(Stock).filter( Stock.gtin == normalized_gtin, Stock.location == stock.location.strip().upper() ).first() if existing_stock: # Add to existing stock old_quantity = existing_stock.quantity existing_stock.quantity += stock.quantity existing_stock.updated_at = datetime.utcnow() db.commit() db.refresh(existing_stock) logger.info( f"Added stock for GTIN {normalized_gtin} at {stock.location}: {old_quantity} + {stock.quantity} = {existing_stock.quantity}") return existing_stock else: # Create new stock entry with the quantity new_stock = Stock( gtin=normalized_gtin, location=stock.location.strip().upper(), quantity=stock.quantity ) db.add(new_stock) db.commit() db.refresh(new_stock) logger.info(f"Created new stock for GTIN {normalized_gtin} at {stock.location}: {stock.quantity}") return new_stock @router.post("/stock/remove", response_model=StockResponse) def remove_stock(stock: StockAdd, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): """Remove quantity from existing stock for a GTIN at a specific location""" # Normalize GTIN def normalize_gtin(gtin_value): if not gtin_value: return None gtin_str = str(gtin_value).strip() if '.' in gtin_str: gtin_str = gtin_str.split('.')[0] gtin_clean = ''.join(filter(str.isdigit, gtin_str)) if len(gtin_clean) in [8, 12, 13, 14]: return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12) return gtin_clean if gtin_clean else None normalized_gtin = normalize_gtin(stock.gtin) if not normalized_gtin: raise HTTPException(status_code=400, detail="Invalid GTIN format") # Find existing stock entry existing_stock = db.query(Stock).filter( Stock.gtin == normalized_gtin, Stock.location == stock.location.strip().upper() ).first() if not existing_stock: raise HTTPException( status_code=404, detail=f"No stock found for GTIN {normalized_gtin} at location {stock.location}" ) # Check if we have enough stock to remove if existing_stock.quantity < stock.quantity: raise HTTPException( status_code=400, detail=f"Insufficient stock. Available: {existing_stock.quantity}, Requested to remove: {stock.quantity}" ) # Remove from existing stock old_quantity = existing_stock.quantity existing_stock.quantity -= stock.quantity existing_stock.updated_at = datetime.utcnow() db.commit() db.refresh(existing_stock) logger.info( f"Removed stock for GTIN {normalized_gtin} at {stock.location}: {old_quantity} - {stock.quantity} = {existing_stock.quantity}") return existing_stock @router.get("/stock/{gtin}", response_model=StockSummaryResponse) def get_stock_by_gtin(gtin: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): """Get all stock locations and total quantity for a specific GTIN""" # Normalize GTIN def normalize_gtin(gtin_value): if not gtin_value: return None gtin_str = str(gtin_value).strip() if '.' in gtin_str: gtin_str = gtin_str.split('.')[0] gtin_clean = ''.join(filter(str.isdigit, gtin_str)) if len(gtin_clean) in [8, 12, 13, 14]: return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12) return gtin_clean if gtin_clean else None normalized_gtin = normalize_gtin(gtin) if not normalized_gtin: raise HTTPException(status_code=400, detail="Invalid GTIN format") # Get all stock entries for this GTIN stock_entries = db.query(Stock).filter(Stock.gtin == normalized_gtin).all() if not stock_entries: raise HTTPException(status_code=404, detail=f"No stock found for GTIN: {gtin}") # Calculate total quantity and build locations list total_quantity = 0 locations = [] for entry in stock_entries: total_quantity += entry.quantity locations.append(StockLocationResponse( location=entry.location, quantity=entry.quantity )) # Try to get product title for reference product = db.query(Product).filter(Product.gtin == normalized_gtin).first() product_title = product.title if product else None return StockSummaryResponse( gtin=normalized_gtin, total_quantity=total_quantity, locations=locations, product_title=product_title ) @router.get("/stock/{gtin}/total") def get_total_stock(gtin: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): """Get total quantity in stock for a specific GTIN""" # Normalize GTIN def normalize_gtin(gtin_value): if not gtin_value: return None gtin_str = str(gtin_value).strip() if '.' in gtin_str: gtin_str = gtin_str.split('.')[0] gtin_clean = ''.join(filter(str.isdigit, gtin_str)) if len(gtin_clean) in [8, 12, 13, 14]: return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12) return gtin_clean if gtin_clean else None normalized_gtin = normalize_gtin(gtin) if not normalized_gtin: raise HTTPException(status_code=400, detail="Invalid GTIN format") # Calculate total stock total_stock = db.query(Stock).filter(Stock.gtin == normalized_gtin).all() total_quantity = sum(entry.quantity for entry in total_stock) # Get product info for context product = db.query(Product).filter(Product.gtin == normalized_gtin).first() return { "gtin": normalized_gtin, "total_quantity": total_quantity, "product_title": product.title if product else None, "locations_count": len(total_stock) } @router.get("/stock", response_model=List[StockResponse]) def get_all_stock( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), location: Optional[str] = Query(None, description="Filter by location"), gtin: Optional[str] = Query(None, description="Filter by GTIN"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get all stock entries with optional filtering""" query = db.query(Stock) if location: query = query.filter(Stock.location.ilike(f"%{location}%")) if gtin: # Normalize GTIN for search def normalize_gtin(gtin_value): if not gtin_value: return None gtin_str = str(gtin_value).strip() if '.' in gtin_str: gtin_str = gtin_str.split('.')[0] gtin_clean = ''.join(filter(str.isdigit, gtin_str)) if len(gtin_clean) in [8, 12, 13, 14]: return gtin_clean.zfill(13) if len(gtin_clean) == 13 else gtin_clean.zfill(12) return gtin_clean if gtin_clean else None normalized_gtin = normalize_gtin(gtin) if normalized_gtin: query = query.filter(Stock.gtin == normalized_gtin) stock_entries = query.offset(skip).limit(limit).all() return stock_entries @router.put("/stock/{stock_id}", response_model=StockResponse) def update_stock(stock_id: int, stock_update: StockUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): """Update stock quantity for a specific stock entry""" stock_entry = db.query(Stock).filter(Stock.id == stock_id).first() if not stock_entry: raise HTTPException(status_code=404, detail="Stock entry not found") stock_entry.quantity = stock_update.quantity stock_entry.updated_at = datetime.utcnow() db.commit() db.refresh(stock_entry) return stock_entry @router.delete("/stock/{stock_id}") def delete_stock(stock_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): """Delete a stock entry""" stock_entry = db.query(Stock).filter(Stock.id == stock_id).first() if not stock_entry: raise HTTPException(status_code=404, detail="Stock entry not found") db.delete(stock_entry) db.commit() return {"message": "Stock entry deleted successfully"}