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

220 lines
6.6 KiB
Python
Raw Normal View History

# services/recipes/app/api/recipes.py
"""
2025-10-06 15:27:01 +02:00
Recipes API - Atomic CRUD operations on Recipe model
"""
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from uuid import UUID
import logging
from ..core.database import get_db
from ..services.recipe_service import RecipeService
from ..schemas.recipes import (
RecipeCreate,
RecipeUpdate,
RecipeResponse,
)
2025-10-06 15:27:01 +02:00
from shared.routing import RouteBuilder, RouteCategory
from shared.auth.access_control import require_user_role
2025-10-06 15:27:01 +02:00
route_builder = RouteBuilder('recipes')
logger = logging.getLogger(__name__)
2025-10-06 15:27:01 +02:00
router = APIRouter(tags=["recipes"])
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")
2025-10-06 15:27:01 +02:00
@router.post(
route_builder.build_custom_route(RouteCategory.BASE, []),
response_model=RecipeResponse
)
@require_user_role(['admin', 'owner', 'member'])
async def create_recipe(
tenant_id: UUID,
recipe_data: RecipeCreate,
user_id: UUID = Depends(get_user_id),
db: AsyncSession = Depends(get_db)
):
"""Create a new recipe"""
try:
recipe_service = RecipeService(db)
2025-10-06 15:27:01 +02:00
recipe_dict = recipe_data.dict(exclude={"ingredients"})
recipe_dict["tenant_id"] = tenant_id
2025-10-06 15:27:01 +02:00
ingredients_list = [ing.dict() for ing in recipe_data.ingredients]
2025-10-06 15:27:01 +02:00
result = await recipe_service.create_recipe(
recipe_dict,
ingredients_list,
user_id
)
2025-10-06 15:27:01 +02:00
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
2025-10-06 15:27:01 +02:00
return RecipeResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
2025-10-06 15:27:01 +02:00
logger.error(f"Error creating recipe: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_custom_route(RouteCategory.BASE, []),
response_model=List[RecipeResponse]
)
async def search_recipes(
tenant_id: UUID,
search_term: Optional[str] = Query(None),
status: Optional[str] = Query(None),
category: Optional[str] = Query(None),
is_seasonal: Optional[bool] = Query(None),
is_signature: Optional[bool] = Query(None),
difficulty_level: Optional[int] = Query(None, ge=1, le=5),
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db)
):
"""Search recipes with filters"""
try:
recipe_service = RecipeService(db)
recipes = await recipe_service.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
)
2025-10-06 15:27:01 +02:00
return [RecipeResponse(**recipe) for recipe in recipes]
2025-10-06 15:27:01 +02:00
except Exception as e:
logger.error(f"Error searching recipes: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_custom_route(RouteCategory.BASE, ["{recipe_id}"]),
response_model=RecipeResponse
)
async def get_recipe(
tenant_id: UUID,
recipe_id: UUID,
db: AsyncSession = Depends(get_db)
):
2025-10-06 15:27:01 +02:00
"""Get recipe by ID with ingredients"""
try:
recipe_service = RecipeService(db)
2025-10-06 15:27:01 +02:00
recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
2025-10-06 15:27:01 +02:00
if recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
2025-10-06 15:27:01 +02:00
return RecipeResponse(**recipe)
except HTTPException:
raise
except Exception as e:
2025-10-06 15:27:01 +02:00
logger.error(f"Error getting recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
2025-10-06 15:27:01 +02:00
@router.put(
route_builder.build_custom_route(RouteCategory.BASE, ["{recipe_id}"]),
response_model=RecipeResponse
)
@require_user_role(['admin', 'owner', 'member'])
async def update_recipe(
tenant_id: UUID,
recipe_id: UUID,
2025-10-06 15:27:01 +02:00
recipe_data: RecipeUpdate,
user_id: UUID = Depends(get_user_id),
db: AsyncSession = Depends(get_db)
):
2025-10-06 15:27:01 +02:00
"""Update an existing recipe"""
try:
recipe_service = RecipeService(db)
2025-10-06 15:27:01 +02:00
existing_recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
2025-10-06 15:27:01 +02:00
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
2025-10-06 15:27:01 +02:00
recipe_dict = recipe_data.dict(exclude={"ingredients"}, exclude_unset=True)
2025-10-06 15:27:01 +02:00
ingredients_list = None
if recipe_data.ingredients is not None:
ingredients_list = [ing.dict() for ing in recipe_data.ingredients]
2025-09-24 21:54:49 +02:00
2025-10-06 15:27:01 +02:00
result = await recipe_service.update_recipe(
recipe_id,
recipe_dict,
ingredients_list,
user_id
)
2025-09-24 21:54:49 +02:00
2025-10-06 15:27:01 +02:00
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
2025-09-24 21:54:49 +02:00
2025-10-06 15:27:01 +02:00
return RecipeResponse(**result["data"])
2025-09-24 21:54:49 +02:00
except HTTPException:
raise
except Exception as e:
2025-10-06 15:27:01 +02:00
logger.error(f"Error updating recipe {recipe_id}: {e}")
2025-09-24 21:54:49 +02:00
raise HTTPException(status_code=500, detail="Internal server error")
2025-10-06 15:27:01 +02:00
@router.delete(
route_builder.build_custom_route(RouteCategory.BASE, ["{recipe_id}"])
)
@require_user_role(['admin', 'owner'])
async def delete_recipe(
2025-09-24 21:54:49 +02:00
tenant_id: UUID,
recipe_id: UUID,
db: AsyncSession = Depends(get_db)
):
2025-10-06 15:27:01 +02:00
"""Delete a recipe"""
2025-09-24 21:54:49 +02:00
try:
recipe_service = RecipeService(db)
2025-10-06 15:27:01 +02:00
existing_recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
2025-09-24 21:54:49 +02:00
raise HTTPException(status_code=404, detail="Recipe not found")
2025-10-06 15:27:01 +02:00
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
2025-09-24 21:54:49 +02:00
2025-10-06 15:27:01 +02:00
success = await recipe_service.delete_recipe(recipe_id)
if not success:
2025-09-24 21:54:49 +02:00
raise HTTPException(status_code=404, detail="Recipe not found")
2025-10-06 15:27:01 +02:00
return {"message": "Recipe deleted successfully"}
2025-09-24 21:54:49 +02:00
except HTTPException:
raise
except Exception as e:
2025-10-06 15:27:01 +02:00
logger.error(f"Error deleting recipe {recipe_id}: {e}")
2025-09-24 21:54:49 +02:00
raise HTTPException(status_code=500, detail="Internal server error")