from sqlalchemy.orm import Session from models.database_models import Stock, Product from models.api_models import StockCreate, StockAdd, StockUpdate, StockLocationResponse, StockSummaryResponse from utils.data_processing import GTINProcessor from typing import Optional, List, Tuple from datetime import datetime import logging logger = logging.getLogger(__name__) class StockService: def __init__(self): self.gtin_processor = GTINProcessor() def normalize_gtin(self, gtin_value) -> Optional[str]: """Normalize GTIN format using the GTINProcessor""" return self.gtin_processor.normalize(gtin_value) def set_stock(self, db: Session, stock_data: StockCreate) -> Stock: """Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)""" normalized_gtin = self.normalize_gtin(stock_data.gtin) if not normalized_gtin: raise ValueError("Invalid GTIN format") location = stock_data.location.strip().upper() # Check if stock entry already exists for this GTIN and location existing_stock = db.query(Stock).filter( Stock.gtin == normalized_gtin, Stock.location == location ).first() if existing_stock: # Update existing stock (SET to exact quantity) old_quantity = existing_stock.quantity existing_stock.quantity = stock_data.quantity existing_stock.updated_at = datetime.utcnow() db.commit() db.refresh(existing_stock) logger.info( f"Updated stock for GTIN {normalized_gtin} at {location}: {old_quantity} → {stock_data.quantity}") return existing_stock else: # Create new stock entry new_stock = Stock( gtin=normalized_gtin, location=location, quantity=stock_data.quantity ) db.add(new_stock) db.commit() db.refresh(new_stock) logger.info(f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}") return new_stock def add_stock(self, db: Session, stock_data: StockAdd) -> Stock: """Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)""" normalized_gtin = self.normalize_gtin(stock_data.gtin) if not normalized_gtin: raise ValueError("Invalid GTIN format") location = stock_data.location.strip().upper() # Check if stock entry already exists for this GTIN and location existing_stock = db.query(Stock).filter( Stock.gtin == normalized_gtin, Stock.location == location ).first() if existing_stock: # Add to existing stock old_quantity = existing_stock.quantity existing_stock.quantity += stock_data.quantity existing_stock.updated_at = datetime.utcnow() db.commit() db.refresh(existing_stock) logger.info( f"Added stock for GTIN {normalized_gtin} at {location}: {old_quantity} + {stock_data.quantity} = {existing_stock.quantity}") return existing_stock else: # Create new stock entry with the quantity new_stock = Stock( gtin=normalized_gtin, location=location, quantity=stock_data.quantity ) db.add(new_stock) db.commit() db.refresh(new_stock) logger.info(f"Created new stock for GTIN {normalized_gtin} at {location}: {stock_data.quantity}") return new_stock def remove_stock(self, db: Session, stock_data: StockAdd) -> Stock: """Remove quantity from existing stock for a GTIN at a specific location""" normalized_gtin = self.normalize_gtin(stock_data.gtin) if not normalized_gtin: raise ValueError("Invalid GTIN format") location = stock_data.location.strip().upper() # Find existing stock entry existing_stock = db.query(Stock).filter( Stock.gtin == normalized_gtin, Stock.location == location ).first() if not existing_stock: raise ValueError(f"No stock found for GTIN {normalized_gtin} at location {location}") # Check if we have enough stock to remove if existing_stock.quantity < stock_data.quantity: raise ValueError( f"Insufficient stock. Available: {existing_stock.quantity}, Requested to remove: {stock_data.quantity}") # Remove from existing stock old_quantity = existing_stock.quantity existing_stock.quantity -= stock_data.quantity existing_stock.updated_at = datetime.utcnow() db.commit() db.refresh(existing_stock) logger.info( f"Removed stock for GTIN {normalized_gtin} at {location}: {old_quantity} - {stock_data.quantity} = {existing_stock.quantity}") return existing_stock def get_stock_by_gtin(self, db: Session, gtin: str) -> StockSummaryResponse: """Get all stock locations and total quantity for a specific GTIN""" normalized_gtin = self.normalize_gtin(gtin) if not normalized_gtin: raise ValueError("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 ValueError(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 ) def get_total_stock(self, db: Session, gtin: str) -> dict: """Get total quantity in stock for a specific GTIN""" normalized_gtin = self.normalize_gtin(gtin) if not normalized_gtin: raise ValueError("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) } def get_all_stock( self, db: Session, skip: int = 0, limit: int = 100, location: Optional[str] = None, gtin: Optional[str] = None ) -> List[Stock]: """Get all stock entries with optional filtering""" query = db.query(Stock) if location: query = query.filter(Stock.location.ilike(f"%{location}%")) if gtin: normalized_gtin = self.normalize_gtin(gtin) if normalized_gtin: query = query.filter(Stock.gtin == normalized_gtin) return query.offset(skip).limit(limit).all() def update_stock(self, db: Session, stock_id: int, stock_update: StockUpdate) -> Stock: """Update stock quantity for a specific stock entry""" stock_entry = db.query(Stock).filter(Stock.id == stock_id).first() if not stock_entry: raise ValueError("Stock entry not found") stock_entry.quantity = stock_update.quantity stock_entry.updated_at = datetime.utcnow() db.commit() db.refresh(stock_entry) logger.info(f"Updated stock entry {stock_id} to quantity {stock_update.quantity}") return stock_entry def delete_stock(self, db: Session, stock_id: int) -> bool: """Delete a stock entry""" stock_entry = db.query(Stock).filter(Stock.id == stock_id).first() if not stock_entry: raise ValueError("Stock entry not found") gtin = stock_entry.gtin location = stock_entry.location db.delete(stock_entry) db.commit() logger.info(f"Deleted stock entry {stock_id} for GTIN {gtin} at {location}") return True def get_stock_by_id(self, db: Session, stock_id: int) -> Optional[Stock]: """Get a stock entry by its ID""" return db.query(Stock).filter(Stock.id == stock_id).first() # Create service instance stock_service = StockService()