# services/recipes/app/repositories/recipe_repository.py """ Async recipe repository for database operations """ from typing import List, Optional, Dict, Any from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_, or_ from sqlalchemy.orm import selectinload from uuid import UUID from datetime import datetime import structlog from shared.database.repository import BaseRepository from ..models.recipes import Recipe, RecipeIngredient from ..schemas.recipes import RecipeCreate, RecipeUpdate logger = structlog.get_logger() class RecipeRepository(BaseRepository[Recipe, RecipeCreate, RecipeUpdate]): """Async repository for recipe operations""" def __init__(self, session: AsyncSession): super().__init__(Recipe, session) async def get_recipe_with_ingredients(self, recipe_id: UUID) -> Optional[Dict[str, Any]]: """Get recipe with ingredients loaded""" result = await self.session.execute( select(Recipe) .options(selectinload(Recipe.ingredients)) .where(Recipe.id == recipe_id) ) recipe = result.scalar_one_or_none() if not recipe: return None return { "id": str(recipe.id), "tenant_id": str(recipe.tenant_id), "name": recipe.name, "recipe_code": recipe.recipe_code, "version": recipe.version, "finished_product_id": str(recipe.finished_product_id), "description": recipe.description, "category": recipe.category, "cuisine_type": recipe.cuisine_type, "difficulty_level": recipe.difficulty_level, "yield_quantity": recipe.yield_quantity, "yield_unit": recipe.yield_unit.value if recipe.yield_unit else None, "prep_time_minutes": recipe.prep_time_minutes, "cook_time_minutes": recipe.cook_time_minutes, "total_time_minutes": recipe.total_time_minutes, "rest_time_minutes": recipe.rest_time_minutes, "estimated_cost_per_unit": float(recipe.estimated_cost_per_unit) if recipe.estimated_cost_per_unit else None, "last_calculated_cost": float(recipe.last_calculated_cost) if recipe.last_calculated_cost else None, "cost_calculation_date": recipe.cost_calculation_date.isoformat() if recipe.cost_calculation_date else None, "target_margin_percentage": recipe.target_margin_percentage, "suggested_selling_price": float(recipe.suggested_selling_price) if recipe.suggested_selling_price else None, "instructions": recipe.instructions, "preparation_notes": recipe.preparation_notes, "storage_instructions": recipe.storage_instructions, "quality_standards": recipe.quality_standards, "serves_count": recipe.serves_count, "nutritional_info": recipe.nutritional_info, "allergen_info": recipe.allergen_info, "dietary_tags": recipe.dietary_tags, "batch_size_multiplier": recipe.batch_size_multiplier, "minimum_batch_size": recipe.minimum_batch_size, "maximum_batch_size": recipe.maximum_batch_size, "status": recipe.status, "is_seasonal": recipe.is_seasonal, "season_start_month": recipe.season_start_month, "season_end_month": recipe.season_end_month, "is_signature_item": recipe.is_signature_item, "created_at": recipe.created_at.isoformat() if recipe.created_at else None, "updated_at": recipe.updated_at.isoformat() if recipe.updated_at else None, "ingredients": [ { "id": str(ingredient.id), "ingredient_id": str(ingredient.ingredient_id), "quantity": float(ingredient.quantity), "unit": ingredient.unit, "preparation_method": ingredient.preparation_method, "notes": ingredient.notes } for ingredient in recipe.ingredients ] if hasattr(recipe, 'ingredients') else [] } async def search_recipes( self, tenant_id: UUID, search_term: Optional[str] = None, status: Optional[str] = None, category: Optional[str] = None, is_seasonal: Optional[bool] = None, is_signature: Optional[bool] = None, difficulty_level: Optional[int] = None, limit: int = 100, offset: int = 0 ) -> List[Dict[str, Any]]: """Search recipes with multiple filters""" query = select(Recipe).where(Recipe.tenant_id == tenant_id) # Text search if search_term: query = query.where( or_( Recipe.name.ilike(f"%{search_term}%"), Recipe.description.ilike(f"%{search_term}%") ) ) # Status filter if status: query = query.where(Recipe.status == status) # Category filter if category: query = query.where(Recipe.category == category) # Seasonal filter if is_seasonal is not None: query = query.where(Recipe.is_seasonal == is_seasonal) # Signature filter if is_signature is not None: query = query.where(Recipe.is_signature_item == is_signature) # Difficulty filter if difficulty_level is not None: query = query.where(Recipe.difficulty_level == difficulty_level) # Apply ordering and pagination query = query.order_by(Recipe.name).limit(limit).offset(offset) result = await self.session.execute(query) recipes = result.scalars().all() return [ { "id": str(recipe.id), "tenant_id": str(recipe.tenant_id), "name": recipe.name, "recipe_code": recipe.recipe_code, "version": recipe.version, "finished_product_id": str(recipe.finished_product_id), "description": recipe.description, "category": recipe.category, "cuisine_type": recipe.cuisine_type, "difficulty_level": recipe.difficulty_level, "yield_quantity": recipe.yield_quantity, "yield_unit": recipe.yield_unit.value if recipe.yield_unit else None, "prep_time_minutes": recipe.prep_time_minutes, "cook_time_minutes": recipe.cook_time_minutes, "total_time_minutes": recipe.total_time_minutes, "rest_time_minutes": recipe.rest_time_minutes, "estimated_cost_per_unit": float(recipe.estimated_cost_per_unit) if recipe.estimated_cost_per_unit else None, "last_calculated_cost": float(recipe.last_calculated_cost) if recipe.last_calculated_cost else None, "cost_calculation_date": recipe.cost_calculation_date.isoformat() if recipe.cost_calculation_date else None, "target_margin_percentage": recipe.target_margin_percentage, "suggested_selling_price": float(recipe.suggested_selling_price) if recipe.suggested_selling_price else None, "instructions": recipe.instructions, "preparation_notes": recipe.preparation_notes, "storage_instructions": recipe.storage_instructions, "quality_standards": recipe.quality_standards, "serves_count": recipe.serves_count, "nutritional_info": recipe.nutritional_info, "allergen_info": recipe.allergen_info, "dietary_tags": recipe.dietary_tags, "batch_size_multiplier": recipe.batch_size_multiplier, "minimum_batch_size": recipe.minimum_batch_size, "maximum_batch_size": recipe.maximum_batch_size, "status": recipe.status, "is_seasonal": recipe.is_seasonal, "season_start_month": recipe.season_start_month, "season_end_month": recipe.season_end_month, "is_signature_item": recipe.is_signature_item, "created_at": recipe.created_at.isoformat() if recipe.created_at else None, "updated_at": recipe.updated_at.isoformat() if recipe.updated_at else None } for recipe in recipes ] async def get_recipe_statistics(self, tenant_id: UUID) -> Dict[str, Any]: """Get recipe statistics for dashboard""" # Total recipes total_result = await self.session.execute( select(func.count(Recipe.id)).where(Recipe.tenant_id == tenant_id) ) total_recipes = total_result.scalar() or 0 # Active recipes active_result = await self.session.execute( select(func.count(Recipe.id)).where( and_( Recipe.tenant_id == tenant_id, Recipe.status == "active" ) ) ) active_recipes = active_result.scalar() or 0 # Signature recipes signature_result = await self.session.execute( select(func.count(Recipe.id)).where( and_( Recipe.tenant_id == tenant_id, Recipe.is_signature_item == True ) ) ) signature_recipes = signature_result.scalar() or 0 # Seasonal recipes seasonal_result = await self.session.execute( select(func.count(Recipe.id)).where( and_( Recipe.tenant_id == tenant_id, Recipe.is_seasonal == True ) ) ) seasonal_recipes = seasonal_result.scalar() or 0 # Category breakdown category_result = await self.session.execute( select(Recipe.category, func.count(Recipe.id)) .where(Recipe.tenant_id == tenant_id) .group_by(Recipe.category) ) categories = dict(category_result.all()) return { "total_recipes": total_recipes, "active_recipes": active_recipes, "signature_recipes": signature_recipes, "seasonal_recipes": seasonal_recipes, "draft_recipes": total_recipes - active_recipes, "categories": categories, "average_difficulty": 3.0, # Could calculate from actual data "total_categories": len(categories) }