Files
bakery-ia/services/recipes/app/api/recipe_operations.py

190 lines
6.1 KiB
Python
Raw Normal View History

2025-10-06 15:27:01 +02:00
# services/recipes/app/api/recipe_operations.py
"""
Recipe Operations API - Business operations and complex workflows
"""
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from sqlalchemy.ext.asyncio import AsyncSession
from uuid import UUID
import logging
from ..core.database import get_db
from ..services.recipe_service import RecipeService
from ..schemas.recipes import (
RecipeResponse,
RecipeDuplicateRequest,
RecipeFeasibilityResponse,
RecipeStatisticsResponse,
)
from shared.routing import RouteBuilder, RouteCategory
from shared.auth.access_control import require_user_role
2025-10-07 07:15:07 +02:00
from shared.auth.decorators import get_current_user_dep
2025-10-06 15:27:01 +02:00
route_builder = RouteBuilder('recipes')
logger = logging.getLogger(__name__)
router = APIRouter(tags=["recipe-operations"])
def get_user_id(x_user_id: str = Header(...)) -> UUID:
"""Extract user ID from header"""
try:
return UUID(x_user_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid user ID format")
@router.post(
route_builder.build_custom_route(RouteCategory.BASE, ["{recipe_id}", "duplicate"]),
response_model=RecipeResponse
)
@require_user_role(['admin', 'owner', 'member'])
async def duplicate_recipe(
tenant_id: UUID,
recipe_id: UUID,
duplicate_data: RecipeDuplicateRequest,
user_id: UUID = Depends(get_user_id),
db: AsyncSession = Depends(get_db)
):
"""Create a duplicate of an existing recipe"""
try:
recipe_service = RecipeService(db)
existing_recipe = recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
result = await recipe_service.duplicate_recipe(
recipe_id,
duplicate_data.new_name,
user_id
)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return RecipeResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error duplicating recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post(
route_builder.build_custom_route(RouteCategory.BASE, ["{recipe_id}", "activate"]),
response_model=RecipeResponse
)
@require_user_role(['admin', 'owner', 'member'])
async def activate_recipe(
tenant_id: UUID,
recipe_id: UUID,
user_id: UUID = Depends(get_user_id),
db: AsyncSession = Depends(get_db)
):
"""Activate a recipe for production"""
try:
recipe_service = RecipeService(db)
existing_recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
result = await recipe_service.activate_recipe(recipe_id, user_id)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return RecipeResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error activating recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get(
route_builder.build_custom_route(RouteCategory.BASE, ["{recipe_id}", "feasibility"]),
response_model=RecipeFeasibilityResponse
)
async def check_recipe_feasibility(
tenant_id: UUID,
recipe_id: UUID,
batch_multiplier: float = Query(1.0, gt=0),
db: AsyncSession = Depends(get_db)
):
"""Check if recipe can be produced with current inventory"""
try:
recipe_service = RecipeService(db)
existing_recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
result = await recipe_service.check_recipe_feasibility(recipe_id, batch_multiplier)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return RecipeFeasibilityResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error checking recipe feasibility {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get(
route_builder.build_dashboard_route("statistics"),
response_model=RecipeStatisticsResponse
)
2025-10-07 07:15:07 +02:00
@require_user_role(['viewer', 'member', 'admin', 'owner'])
2025-10-06 15:27:01 +02:00
async def get_recipe_statistics(
tenant_id: UUID,
2025-10-07 07:15:07 +02:00
current_user: dict = Depends(get_current_user_dep),
2025-10-06 15:27:01 +02:00
db: AsyncSession = Depends(get_db)
):
"""Get recipe statistics for dashboard"""
try:
recipe_service = RecipeService(db)
stats = await recipe_service.get_recipe_statistics(tenant_id)
return RecipeStatisticsResponse(**stats)
except Exception as e:
logger.error(f"Error getting recipe statistics: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get(
route_builder.build_custom_route(RouteCategory.BASE, ["categories", "list"])
)
async def get_recipe_categories(
tenant_id: UUID,
db: AsyncSession = Depends(get_db)
):
"""Get list of recipe categories used by tenant"""
try:
recipe_service = RecipeService(db)
recipes = await recipe_service.search_recipes(tenant_id, limit=1000)
categories = list(set(recipe["category"] for recipe in recipes if recipe["category"]))
categories.sort()
return {"categories": categories}
except Exception as e:
logger.error(f"Error getting recipe categories: {e}")
raise HTTPException(status_code=500, detail="Internal server error")