"""Recipe router - CRUD, versioning, barcode lookup.""" from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from database import get_db from middleware.api_key import get_current_user, require_maker, require_measurement_tec from models.user import User from schemas.recipe import ( RecipeCreate, RecipeListResponse, RecipeResponse, RecipeUpdate, RecipeVersionResponse, ) from services import recipe_service router = APIRouter(prefix="/api/recipes", tags=["recipes"]) # --------------------------------------------------------------------------- # Helper: build RecipeResponse with current_version attached # --------------------------------------------------------------------------- def _recipe_to_response(recipe) -> RecipeResponse: """Convert a Recipe ORM object to RecipeResponse, injecting current_version.""" current = None for v in (recipe.versions or []): if v.is_current: current = v break resp = RecipeResponse.model_validate(recipe) if current is not None: resp.current_version = RecipeVersionResponse.model_validate(current) return resp # --------------------------------------------------------------------------- # Recipes CRUD # --------------------------------------------------------------------------- @router.post("", response_model=RecipeResponse, status_code=201) async def create_recipe( data: RecipeCreate, user: User = Depends(require_maker), db: AsyncSession = Depends(get_db), ): """Create a new recipe with its initial version (v1). Requires Maker role.""" recipe = await recipe_service.create_recipe(db, data, user) return _recipe_to_response(recipe) @router.get("", response_model=RecipeListResponse) async def list_recipes( page: int = Query(1, ge=1), per_page: int = Query(20, ge=1, le=100), search: str | None = Query(None, max_length=200), _user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """List active recipes with pagination and optional search. All authenticated users.""" result = await recipe_service.list_recipes(db, page=page, per_page=per_page, search=search) result["items"] = [_recipe_to_response(r) for r in result["items"]] return result @router.get("/code/{code}", response_model=RecipeResponse) async def get_recipe_by_code( code: str, _user: User = Depends(require_measurement_tec), db: AsyncSession = Depends(get_db), ): """Look up a recipe by its barcode/code. Requires MeasurementTec role.""" recipe = await recipe_service.get_recipe_by_code(db, code) return _recipe_to_response(recipe) @router.get("/{recipe_id}", response_model=RecipeResponse) async def get_recipe( recipe_id: int, _user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get recipe detail with current version and all tasks/subtasks.""" recipe = await recipe_service.get_recipe_detail(db, recipe_id) return _recipe_to_response(recipe) @router.put("/{recipe_id}", response_model=RecipeResponse) async def update_recipe( recipe_id: int, data: RecipeUpdate, user: User = Depends(require_maker), db: AsyncSession = Depends(get_db), ): """Update a recipe. Creates a new version only if the current one has measurements. Otherwise updates in-place. Requires Maker role. """ current = await recipe_service.get_current_version(db, recipe_id) has_measurements = await recipe_service.version_has_measurements(db, current.id) if has_measurements: # Copy-on-write: create new version to preserve measurement data await recipe_service.create_new_version(db, recipe_id, data, user) else: # No measurements yet: update in-place await recipe_service.update_current_version(db, recipe_id, data) recipe = await recipe_service.get_recipe_detail(db, recipe_id) return _recipe_to_response(recipe) @router.delete("/{recipe_id}", response_model=RecipeResponse) async def delete_recipe( recipe_id: int, user: User = Depends(require_maker), db: AsyncSession = Depends(get_db), ): """Deactivate a recipe (soft delete). Requires Maker role.""" recipe = await recipe_service.deactivate_recipe(db, recipe_id, user) return RecipeResponse.model_validate(recipe) # --------------------------------------------------------------------------- # Versions # --------------------------------------------------------------------------- @router.get("/{recipe_id}/versions", response_model=list[RecipeVersionResponse]) async def list_versions( recipe_id: int, _user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """List all versions of a recipe. Requires Maker or Metrologist role.""" versions = await recipe_service.list_versions(db, recipe_id) return [RecipeVersionResponse.model_validate(v) for v in versions] @router.get( "/{recipe_id}/versions/{version_number}", response_model=RecipeVersionResponse, ) async def get_version( recipe_id: int, version_number: int, _user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get a specific version with its tasks. Requires Maker or Metrologist role.""" version = await recipe_service.get_version_detail(db, recipe_id, version_number) return RecipeVersionResponse.model_validate(version) @router.get("/{recipe_id}/versions/{version_number}/measurement-count") async def get_measurement_count( recipe_id: int, version_number: int, _user: User = Depends(require_maker), db: AsyncSession = Depends(get_db), ): """Count measurements on a specific version. Requires Maker role. Useful to warn before creating a new version if the current one has measurements. """ count = await recipe_service.get_measurement_count(db, recipe_id, version_number) return {"recipe_id": recipe_id, "version_number": version_number, "measurement_count": count}