245 lines
10 KiB
Python
245 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
|
|
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)
|
|
} |