# services/inventory/app/api/ingredients.py """ API endpoints for ingredient management """ from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Path, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.services.inventory_service import InventoryService from app.schemas.inventory import ( IngredientCreate, IngredientUpdate, IngredientResponse, InventoryFilter, PaginatedResponse ) from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep router = APIRouter(tags=["ingredients"]) # Helper function to extract user ID from user object def get_current_user_id(current_user: dict = Depends(get_current_user_dep)) -> UUID: """Extract user ID from current user context""" user_id = current_user.get('user_id') if not user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User ID not found in context" ) return UUID(user_id) @router.post("/tenants/{tenant_id}/ingredients", response_model=IngredientResponse) async def create_ingredient( ingredient_data: IngredientCreate, tenant_id: UUID = Path(..., description="Tenant ID"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Create a new ingredient""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") # Extract user ID - handle service tokens that don't have UUID user_ids raw_user_id = current_user.get('user_id') if current_user.get('type') == 'service': # For service tokens, user_id might not be a UUID, so set to None user_id = None else: try: user_id = UUID(raw_user_id) except (ValueError, TypeError): user_id = None service = InventoryService() ingredient = await service.create_ingredient(ingredient_data, tenant_id, user_id) return ingredient except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create ingredient" ) @router.get("/tenants/{tenant_id}/ingredients/{ingredient_id}", response_model=IngredientResponse) async def get_ingredient( ingredient_id: UUID, tenant_id: UUID = Path(..., description="Tenant ID"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get ingredient by ID""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") service = InventoryService() ingredient = await service.get_ingredient(ingredient_id, tenant_id) if not ingredient: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Ingredient not found" ) return ingredient except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get ingredient" ) @router.put("/tenants/{tenant_id}/ingredients/{ingredient_id}", response_model=IngredientResponse) async def update_ingredient( ingredient_id: UUID, ingredient_data: IngredientUpdate, tenant_id: UUID = Path(..., description="Tenant ID"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Update ingredient""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") service = InventoryService() ingredient = await service.update_ingredient(ingredient_id, ingredient_data, tenant_id) if not ingredient: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Ingredient not found" ) return ingredient except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update ingredient" ) @router.get("/tenants/{tenant_id}/ingredients", response_model=List[IngredientResponse]) async def list_ingredients( tenant_id: UUID = Path(..., description="Tenant ID"), skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(100, ge=1, le=1000, description="Number of records to return"), category: Optional[str] = Query(None, description="Filter by category"), product_type: Optional[str] = Query(None, description="Filter by product type (ingredient or finished_product)"), is_active: Optional[bool] = Query(None, description="Filter by active status"), is_low_stock: Optional[bool] = Query(None, description="Filter by low stock status"), needs_reorder: Optional[bool] = Query(None, description="Filter by reorder needed"), search: Optional[str] = Query(None, description="Search in name, SKU, or barcode"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """List ingredients with filtering""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") service = InventoryService() # Build filters filters = {} if category: filters['category'] = category if product_type: filters['product_type'] = product_type if is_active is not None: filters['is_active'] = is_active if is_low_stock is not None: filters['is_low_stock'] = is_low_stock if needs_reorder is not None: filters['needs_reorder'] = needs_reorder if search: filters['search'] = search ingredients = await service.get_ingredients(tenant_id, skip, limit, filters) return ingredients except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to list ingredients" ) @router.delete("/tenants/{tenant_id}/ingredients/{ingredient_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_ingredient( ingredient_id: UUID, tenant_id: UUID = Path(..., description="Tenant ID"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Soft delete ingredient (mark as inactive)""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") service = InventoryService() ingredient = await service.update_ingredient( ingredient_id, {"is_active": False}, tenant_id ) if not ingredient: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Ingredient not found" ) return None except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete ingredient" ) @router.get("/tenants/{tenant_id}/ingredients/{ingredient_id}/stock", response_model=List[dict]) async def get_ingredient_stock( ingredient_id: UUID, tenant_id: UUID = Path(..., description="Tenant ID"), include_unavailable: bool = Query(False, description="Include unavailable stock"), current_tenant: str = Depends(get_current_tenant_id_dep), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get stock entries for an ingredient""" try: # Verify tenant access if str(tenant_id) != current_tenant: raise HTTPException(status_code=403, detail="Access denied to this tenant") service = InventoryService() stock_entries = await service.get_stock_by_ingredient( ingredient_id, tenant_id, include_unavailable ) return stock_entries except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get ingredient stock" )