Files
bakery-ia/services/recipes/app/services/recipe_service.py
2025-09-24 21:54:49 +02:00

385 lines
14 KiB
Python

# services/recipes/app/services/recipe_service.py
"""
Service layer for recipe management operations
"""
import logging
from typing import List, Optional, Dict, Any
from uuid import UUID
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from ..repositories.recipe_repository import RecipeRepository
from ..schemas.recipes import RecipeCreate, RecipeUpdate
logger = logging.getLogger(__name__)
class RecipeService:
"""Async service for recipe management operations"""
def __init__(self, session: AsyncSession):
self.session = session
self.recipe_repo = RecipeRepository(session)
async def get_recipe_with_ingredients(self, recipe_id: UUID) -> Optional[Dict[str, Any]]:
"""Get recipe by ID with ingredients"""
try:
return await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
except Exception as e:
logger.error(f"Error getting recipe {recipe_id}: {e}")
return None
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 filters"""
try:
return await self.recipe_repo.search_recipes(
tenant_id=tenant_id,
search_term=search_term,
status=status,
category=category,
is_seasonal=is_seasonal,
is_signature=is_signature,
difficulty_level=difficulty_level,
limit=limit,
offset=offset
)
except Exception as e:
logger.error(f"Error searching recipes: {e}")
return []
async def get_recipe_statistics(self, tenant_id: UUID) -> Dict[str, Any]:
"""Get recipe statistics for dashboard"""
try:
return await self.recipe_repo.get_recipe_statistics(tenant_id)
except Exception as e:
logger.error(f"Error getting recipe statistics: {e}")
return {"total_recipes": 0, "active_recipes": 0, "signature_recipes": 0, "seasonal_recipes": 0}
async def create_recipe(
self,
recipe_data: Dict[str, Any],
ingredients_data: List[Dict[str, Any]],
created_by: UUID
) -> Dict[str, Any]:
"""Create a new recipe with ingredients"""
try:
# Add metadata
recipe_data["created_by"] = created_by
recipe_data["created_at"] = datetime.utcnow()
recipe_data["updated_at"] = datetime.utcnow()
# Use the shared repository's create method
recipe_create = RecipeCreate(**recipe_data)
recipe = await self.recipe_repo.create(recipe_create)
# Get the created recipe with ingredients (if the repository supports it)
result = await self.recipe_repo.get_recipe_with_ingredients(recipe.id)
return {
"success": True,
"data": result
}
except Exception as e:
logger.error(f"Error creating recipe: {e}")
await self.session.rollback()
return {
"success": False,
"error": str(e)
}
async def update_recipe(
self,
recipe_id: UUID,
recipe_data: Dict[str, Any],
ingredients_data: Optional[List[Dict[str, Any]]] = None,
updated_by: UUID = None
) -> Dict[str, Any]:
"""Update an existing recipe"""
try:
# Check if recipe exists
existing_recipe = await self.recipe_repo.get_by_id(recipe_id)
if not existing_recipe:
return {
"success": False,
"error": "Recipe not found"
}
# Add metadata
if updated_by:
recipe_data["updated_by"] = updated_by
recipe_data["updated_at"] = datetime.utcnow()
# Use the shared repository's update method
recipe_update = RecipeUpdate(**recipe_data)
updated_recipe = await self.recipe_repo.update(recipe_id, recipe_update)
# Get the updated recipe with ingredients
result = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
return {
"success": True,
"data": result
}
except Exception as e:
logger.error(f"Error updating recipe {recipe_id}: {e}")
await self.session.rollback()
return {
"success": False,
"error": str(e)
}
async def delete_recipe(self, recipe_id: UUID) -> bool:
"""Delete a recipe"""
try:
return await self.recipe_repo.delete(recipe_id)
except Exception as e:
logger.error(f"Error deleting recipe {recipe_id}: {e}")
return False
async def check_recipe_feasibility(self, recipe_id: UUID, batch_multiplier: float = 1.0) -> Dict[str, Any]:
"""Check if recipe can be produced with current inventory"""
try:
recipe = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
if not recipe:
return {
"success": False,
"error": "Recipe not found"
}
# Simplified feasibility check - can be enhanced later with inventory service integration
return {
"success": True,
"data": {
"recipe_id": str(recipe_id),
"recipe_name": recipe["name"],
"batch_multiplier": batch_multiplier,
"feasible": True,
"missing_ingredients": [],
"insufficient_ingredients": []
}
}
except Exception as e:
logger.error(f"Error checking recipe feasibility {recipe_id}: {e}")
return {
"success": False,
"error": str(e)
}
async def duplicate_recipe(
self,
recipe_id: UUID,
new_name: str,
created_by: UUID
) -> Dict[str, Any]:
"""Create a duplicate of an existing recipe"""
try:
# Get original recipe
original_recipe = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
if not original_recipe:
return {
"success": False,
"error": "Recipe not found"
}
# Create new recipe data
new_recipe_data = original_recipe.copy()
new_recipe_data["name"] = new_name
# Remove fields that should be auto-generated
new_recipe_data.pop("id", None)
new_recipe_data.pop("created_at", None)
new_recipe_data.pop("updated_at", None)
# Handle ingredients
ingredients = new_recipe_data.pop("ingredients", [])
# Create the duplicate
result = await self.create_recipe(new_recipe_data, ingredients, created_by)
return result
except Exception as e:
logger.error(f"Error duplicating recipe {recipe_id}: {e}")
await self.session.rollback()
return {
"success": False,
"error": str(e)
}
async def activate_recipe(self, recipe_id: UUID, activated_by: UUID) -> Dict[str, Any]:
"""Activate a recipe for production"""
try:
# Check if recipe exists
recipe = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
if not recipe:
return {
"success": False,
"error": "Recipe not found"
}
if not recipe.get("ingredients"):
return {
"success": False,
"error": "Recipe must have at least one ingredient"
}
# Update recipe status
update_data = {
"status": "active",
"updated_by": activated_by,
"updated_at": datetime.utcnow()
}
recipe_update = RecipeUpdate(**update_data)
await self.recipe_repo.update(recipe_id, recipe_update)
# Get the updated recipe
result = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
return {
"success": True,
"data": result
}
except Exception as e:
logger.error(f"Error activating recipe {recipe_id}: {e}")
return {
"success": False,
"error": str(e)
}
# Quality Configuration Methods
async def update_recipe_quality_configuration(
self,
tenant_id: UUID,
recipe_id: UUID,
quality_config_update: Dict[str, Any],
user_id: UUID
) -> Dict[str, Any]:
"""Update quality configuration for a recipe"""
try:
# Get current recipe
recipe = await self.recipe_repo.get_recipe(tenant_id, recipe_id)
if not recipe:
raise ValueError("Recipe not found")
# Get existing quality configuration or create default
current_config = recipe.get("quality_check_configuration", {
"stages": {},
"overall_quality_threshold": 7.0,
"critical_stage_blocking": True,
"auto_create_quality_checks": True,
"quality_manager_approval_required": False
})
# Merge with updates
if "stages" in quality_config_update:
current_config["stages"].update(quality_config_update["stages"])
for key in ["overall_quality_threshold", "critical_stage_blocking",
"auto_create_quality_checks", "quality_manager_approval_required"]:
if key in quality_config_update:
current_config[key] = quality_config_update[key]
# Update recipe with new configuration
recipe_update = RecipeUpdate(quality_check_configuration=current_config)
await self.recipe_repo.update_recipe(tenant_id, recipe_id, recipe_update, user_id)
# Return updated recipe
updated_recipe = await self.recipe_repo.get_recipe(tenant_id, recipe_id)
return updated_recipe
except Exception as e:
logger.error(f"Error updating recipe quality configuration: {e}")
raise
async def add_quality_templates_to_stage(
self,
tenant_id: UUID,
recipe_id: UUID,
stage: str,
template_ids: List[UUID],
user_id: UUID
):
"""Add quality templates to a specific recipe stage"""
try:
# Get current recipe
recipe = await self.recipe_repo.get_recipe(tenant_id, recipe_id)
if not recipe:
raise ValueError("Recipe not found")
# Get existing quality configuration
quality_config = recipe.get("quality_check_configuration", {"stages": {}})
# Initialize stage if it doesn't exist
if stage not in quality_config["stages"]:
quality_config["stages"][stage] = {
"template_ids": [],
"required_checks": [],
"optional_checks": [],
"blocking_on_failure": True,
"min_quality_score": None
}
# Add template IDs (avoid duplicates)
stage_config = quality_config["stages"][stage]
existing_ids = set(stage_config.get("template_ids", []))
new_ids = [str(tid) for tid in template_ids if str(tid) not in existing_ids]
stage_config["template_ids"].extend(new_ids)
# Update recipe
recipe_update = RecipeUpdate(quality_check_configuration=quality_config)
await self.recipe_repo.update_recipe(tenant_id, recipe_id, recipe_update, user_id)
except Exception as e:
logger.error(f"Error adding quality templates to stage: {e}")
raise
async def remove_quality_template_from_stage(
self,
tenant_id: UUID,
recipe_id: UUID,
stage: str,
template_id: UUID,
user_id: UUID
):
"""Remove a quality template from a specific recipe stage"""
try:
# Get current recipe
recipe = await self.recipe_repo.get_recipe(tenant_id, recipe_id)
if not recipe:
raise ValueError("Recipe not found")
# Get existing quality configuration
quality_config = recipe.get("quality_check_configuration", {"stages": {}})
# Remove template ID from stage
if stage in quality_config["stages"]:
stage_config = quality_config["stages"][stage]
template_ids = stage_config.get("template_ids", [])
template_ids = [tid for tid in template_ids if str(tid) != str(template_id)]
stage_config["template_ids"] = template_ids
# Update recipe
recipe_update = RecipeUpdate(quality_check_configuration=quality_config)
await self.recipe_repo.update_recipe(tenant_id, recipe_id, recipe_update, user_id)
except Exception as e:
logger.error(f"Error removing quality template from stage: {e}")
raise