# services/inventory/app/repositories/ingredient_repository.py """ Ingredient Repository using Repository Pattern """ from typing import List, Optional, Dict, Any from uuid import UUID from datetime import datetime from sqlalchemy import select, func, and_, or_, desc, asc from sqlalchemy.ext.asyncio import AsyncSession import structlog from app.models.inventory import Ingredient, Stock from app.schemas.inventory import IngredientCreate, IngredientUpdate from shared.database.repository import BaseRepository logger = structlog.get_logger() class IngredientRepository(BaseRepository[Ingredient, IngredientCreate, IngredientUpdate]): """Repository for ingredient operations""" def __init__(self, session: AsyncSession): super().__init__(Ingredient, session) async def create_ingredient(self, ingredient_data: IngredientCreate, tenant_id: UUID) -> Ingredient: """Create a new ingredient""" try: # Prepare data create_data = ingredient_data.model_dump() create_data['tenant_id'] = tenant_id # Create record record = await self.create(create_data) logger.info( "Created ingredient", ingredient_id=record.id, name=record.name, category=record.category.value if record.category else None, tenant_id=tenant_id ) return record except Exception as e: logger.error("Failed to create ingredient", error=str(e), tenant_id=tenant_id) raise async def get_ingredients_by_tenant( self, tenant_id: UUID, skip: int = 0, limit: int = 100, filters: Optional[Dict[str, Any]] = None ) -> List[Ingredient]: """Get ingredients for a tenant with filtering""" try: query_filters = {'tenant_id': tenant_id} if filters: if filters.get('category'): query_filters['category'] = filters['category'] if filters.get('is_active') is not None: query_filters['is_active'] = filters['is_active'] if filters.get('is_perishable') is not None: query_filters['is_perishable'] = filters['is_perishable'] ingredients = await self.get_multi( skip=skip, limit=limit, filters=query_filters, order_by='name' ) return ingredients except Exception as e: logger.error("Failed to get ingredients", error=str(e), tenant_id=tenant_id) raise async def search_ingredients( self, tenant_id: UUID, search_term: str, skip: int = 0, limit: int = 50 ) -> List[Ingredient]: """Search ingredients by name, sku, or barcode""" try: # Add tenant filter to search query = select(self.model).where( and_( self.model.tenant_id == tenant_id, or_( self.model.name.ilike(f"%{search_term}%"), self.model.sku.ilike(f"%{search_term}%"), self.model.barcode.ilike(f"%{search_term}%"), self.model.brand.ilike(f"%{search_term}%") ) ) ).offset(skip).limit(limit) result = await self.session.execute(query) return result.scalars().all() except Exception as e: logger.error("Failed to search ingredients", error=str(e), tenant_id=tenant_id) raise async def get_low_stock_ingredients(self, tenant_id: UUID) -> List[Dict[str, Any]]: """Get ingredients with low stock levels""" try: # Query ingredients with their current stock levels query = select( Ingredient, func.coalesce(func.sum(Stock.available_quantity), 0).label('current_stock') ).outerjoin( Stock, and_( Stock.ingredient_id == Ingredient.id, Stock.is_available == True ) ).where( Ingredient.tenant_id == tenant_id ).group_by(Ingredient.id).having( func.coalesce(func.sum(Stock.available_quantity), 0) <= Ingredient.low_stock_threshold ) result = await self.session.execute(query) results = [] for ingredient, current_stock in result: results.append({ 'ingredient': ingredient, 'current_stock': float(current_stock) if current_stock else 0.0, 'threshold': ingredient.low_stock_threshold, 'needs_reorder': current_stock <= ingredient.reorder_point if current_stock else True }) return results except Exception as e: logger.error("Failed to get low stock ingredients", error=str(e), tenant_id=tenant_id) raise async def get_ingredients_needing_reorder(self, tenant_id: UUID) -> List[Dict[str, Any]]: """Get ingredients that need reordering""" try: query = select( Ingredient, func.coalesce(func.sum(Stock.available_quantity), 0).label('current_stock') ).outerjoin( Stock, and_( Stock.ingredient_id == Ingredient.id, Stock.is_available == True ) ).where( and_( Ingredient.tenant_id == tenant_id, Ingredient.is_active == True ) ).group_by(Ingredient.id).having( func.coalesce(func.sum(Stock.available_quantity), 0) <= Ingredient.reorder_point ) result = await self.session.execute(query) results = [] for ingredient, current_stock in result: results.append({ 'ingredient': ingredient, 'current_stock': float(current_stock) if current_stock else 0.0, 'reorder_point': ingredient.reorder_point, 'reorder_quantity': ingredient.reorder_quantity }) return results except Exception as e: logger.error("Failed to get ingredients needing reorder", error=str(e), tenant_id=tenant_id) raise async def get_by_sku(self, tenant_id: UUID, sku: str) -> Optional[Ingredient]: """Get ingredient by SKU""" try: result = await self.session.execute( select(self.model).where( and_( self.model.tenant_id == tenant_id, self.model.sku == sku ) ) ) return result.scalar_one_or_none() except Exception as e: logger.error("Failed to get ingredient by SKU", error=str(e), sku=sku, tenant_id=tenant_id) raise async def get_by_barcode(self, tenant_id: UUID, barcode: str) -> Optional[Ingredient]: """Get ingredient by barcode""" try: result = await self.session.execute( select(self.model).where( and_( self.model.tenant_id == tenant_id, self.model.barcode == barcode ) ) ) return result.scalar_one_or_none() except Exception as e: logger.error("Failed to get ingredient by barcode", error=str(e), barcode=barcode, tenant_id=tenant_id) raise async def update_last_purchase_price(self, ingredient_id: UUID, price: float) -> Optional[Ingredient]: """Update the last purchase price for an ingredient""" try: update_data = {'last_purchase_price': price} return await self.update(ingredient_id, update_data) except Exception as e: logger.error("Failed to update last purchase price", error=str(e), ingredient_id=ingredient_id) raise async def get_ingredients_by_category(self, tenant_id: UUID, category: str) -> List[Ingredient]: """Get all ingredients in a specific category""" try: result = await self.session.execute( select(self.model).where( and_( self.model.tenant_id == tenant_id, self.model.category == category, self.model.is_active == True ) ).order_by(self.model.name) ) return result.scalars().all() except Exception as e: logger.error("Failed to get ingredients by category", error=str(e), category=category, tenant_id=tenant_id) raise