Initial commit - production deployment
This commit is contained in:
7
services/recipes/app/repositories/__init__.py
Normal file
7
services/recipes/app/repositories/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# services/recipes/app/repositories/__init__.py
|
||||
|
||||
from .recipe_repository import RecipeRepository
|
||||
|
||||
__all__ = [
|
||||
"RecipeRepository"
|
||||
]
|
||||
270
services/recipes/app/repositories/recipe_repository.py
Normal file
270
services/recipes/app/repositories/recipe_repository.py
Normal file
@@ -0,0 +1,270 @@
|
||||
# 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": float(recipe.yield_quantity),
|
||||
"yield_unit": recipe.yield_unit.value if hasattr(recipe.yield_unit, 'value') else recipe.yield_unit,
|
||||
"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_check_configuration": recipe.quality_check_configuration,
|
||||
"serves_count": recipe.serves_count,
|
||||
"nutritional_info": recipe.nutritional_info,
|
||||
"allergen_info": recipe.allergen_info,
|
||||
"dietary_tags": recipe.dietary_tags,
|
||||
"batch_size_multiplier": float(recipe.batch_size_multiplier),
|
||||
"minimum_batch_size": float(recipe.minimum_batch_size) if recipe.minimum_batch_size else None,
|
||||
"maximum_batch_size": float(recipe.maximum_batch_size) if recipe.maximum_batch_size else None,
|
||||
"optimal_production_temperature": float(recipe.optimal_production_temperature) if recipe.optimal_production_temperature else None,
|
||||
"optimal_humidity": float(recipe.optimal_humidity) if recipe.optimal_humidity else None,
|
||||
"status": recipe.status.value if hasattr(recipe.status, 'value') else 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,
|
||||
"created_by": str(recipe.created_by) if recipe.created_by else None,
|
||||
"updated_by": str(recipe.updated_by) if hasattr(recipe, 'updated_by') and recipe.updated_by else None,
|
||||
"ingredients": [
|
||||
{
|
||||
"id": str(ingredient.id),
|
||||
"tenant_id": str(ingredient.tenant_id),
|
||||
"recipe_id": str(ingredient.recipe_id),
|
||||
"ingredient_id": str(ingredient.ingredient_id),
|
||||
"quantity": float(ingredient.quantity),
|
||||
"unit": ingredient.unit.value if hasattr(ingredient.unit, 'value') else ingredient.unit,
|
||||
"quantity_in_base_unit": float(ingredient.quantity_in_base_unit) if ingredient.quantity_in_base_unit else None,
|
||||
"alternative_quantity": float(ingredient.alternative_quantity) if ingredient.alternative_quantity else None,
|
||||
"alternative_unit": ingredient.alternative_unit.value if hasattr(ingredient.alternative_unit, 'value') and ingredient.alternative_unit else None,
|
||||
"preparation_method": ingredient.preparation_method,
|
||||
"ingredient_notes": ingredient.ingredient_notes,
|
||||
"is_optional": ingredient.is_optional,
|
||||
"ingredient_order": ingredient.ingredient_order,
|
||||
"ingredient_group": ingredient.ingredient_group,
|
||||
"substitution_options": ingredient.substitution_options,
|
||||
"substitution_ratio": float(ingredient.substitution_ratio) if ingredient.substitution_ratio else None,
|
||||
"unit_cost": float(ingredient.unit_cost) if hasattr(ingredient, 'unit_cost') and ingredient.unit_cost else None,
|
||||
"total_cost": float(ingredient.total_cost) if hasattr(ingredient, 'total_cost') and ingredient.total_cost else None,
|
||||
"cost_updated_at": ingredient.cost_updated_at.isoformat() if hasattr(ingredient, 'cost_updated_at') and ingredient.cost_updated_at else None
|
||||
}
|
||||
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": float(recipe.yield_quantity),
|
||||
"yield_unit": recipe.yield_unit.value if hasattr(recipe.yield_unit, 'value') else recipe.yield_unit,
|
||||
"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_check_configuration": recipe.quality_check_configuration,
|
||||
"serves_count": recipe.serves_count,
|
||||
"nutritional_info": recipe.nutritional_info,
|
||||
"allergen_info": recipe.allergen_info,
|
||||
"dietary_tags": recipe.dietary_tags,
|
||||
"batch_size_multiplier": float(recipe.batch_size_multiplier),
|
||||
"minimum_batch_size": float(recipe.minimum_batch_size) if recipe.minimum_batch_size else None,
|
||||
"maximum_batch_size": float(recipe.maximum_batch_size) if recipe.maximum_batch_size else None,
|
||||
"optimal_production_temperature": float(recipe.optimal_production_temperature) if recipe.optimal_production_temperature else None,
|
||||
"optimal_humidity": float(recipe.optimal_humidity) if recipe.optimal_humidity else None,
|
||||
"status": recipe.status.value if hasattr(recipe.status, 'value') else 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,
|
||||
"created_by": str(recipe.created_by) if recipe.created_by else None,
|
||||
"updated_by": str(recipe.updated_by) if hasattr(recipe, 'updated_by') and recipe.updated_by else None,
|
||||
"ingredients": [] # For list view, don't load ingredients to improve performance
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user