Files
bakery-ia/services/recipes/app/repositories/recipe_repository.py
2025-10-12 18:47:33 +02:00

248 lines
10 KiB
Python

# 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, RecipeStatus
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 == RecipeStatus.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)
)
category_data = category_result.all()
# Convert to list of dicts for the schema
category_breakdown = [
{"category": category or "Uncategorized", "count": count}
for category, count in category_data
]
return {
"total_recipes": total_recipes,
"active_recipes": active_recipes,
"signature_recipes": signature_recipes,
"seasonal_recipes": seasonal_recipes,
"category_breakdown": category_breakdown
}