# services/inventory/app/api/ingredients.py """ Base CRUD operations for inventory ingredients resources Following standardized URL structure: /api/v1/tenants/{tenant_id}/inventory/{resource} """ 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, StockResponse, StockCreate, StockUpdate, ) from shared.auth.decorators import get_current_user_dep from shared.auth.access_control import require_user_role, admin_role_required, owner_role_required from shared.routing import RouteBuilder # Create route builder for consistent URL structure route_builder = RouteBuilder('inventory') 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) # ===== INGREDIENTS ENDPOINTS ===== @router.post( route_builder.build_base_route("ingredients"), response_model=IngredientResponse, status_code=status.HTTP_201_CREATED ) @require_user_role(['admin', 'owner']) async def create_ingredient( ingredient_data: IngredientCreate, tenant_id: UUID = Path(..., description="Tenant ID"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Create a new ingredient (Admin/Manager only)""" try: # Extract user ID - handle service tokens raw_user_id = current_user.get('user_id') if current_user.get('type') == 'service': 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( route_builder.build_base_route("ingredients/count"), response_model=dict ) async def count_ingredients( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get count of ingredients for a tenant (All users)""" try: service = InventoryService() count = await service.count_ingredients_by_tenant(tenant_id) return { "tenant_id": str(tenant_id), "ingredient_count": count } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to count ingredients: {str(e)}" ) @router.get( route_builder.build_resource_detail_route("ingredients", "ingredient_id"), response_model=IngredientResponse ) async def get_ingredient( ingredient_id: UUID, tenant_id: UUID = Path(..., description="Tenant ID"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get ingredient by ID (All users)""" try: 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( route_builder.build_resource_detail_route("ingredients", "ingredient_id"), response_model=IngredientResponse ) @require_user_role(['admin', 'owner', 'member']) async def update_ingredient( ingredient_id: UUID, ingredient_data: IngredientUpdate, tenant_id: UUID = Path(..., description="Tenant ID"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Update ingredient (Admin/Manager/User)""" try: 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( route_builder.build_base_route("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"), 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_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """List ingredients with filtering (All users)""" try: 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( route_builder.build_resource_detail_route("ingredients", "ingredient_id"), status_code=status.HTTP_204_NO_CONTENT ) @admin_role_required async def soft_delete_ingredient( ingredient_id: UUID, tenant_id: UUID = Path(..., description="Tenant ID"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Soft delete ingredient - mark as inactive (Admin only)""" try: service = InventoryService() result = await service.soft_delete_ingredient(ingredient_id, tenant_id) return None except ValueError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e) ) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to soft delete ingredient" ) @router.delete( route_builder.build_nested_resource_route("ingredients", "ingredient_id", "hard"), response_model=dict ) @admin_role_required async def hard_delete_ingredient( ingredient_id: UUID, tenant_id: UUID = Path(..., description="Tenant ID"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Hard delete ingredient and all associated data (Admin only)""" try: service = InventoryService() deletion_summary = await service.hard_delete_ingredient(ingredient_id, tenant_id) return deletion_summary except ValueError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e) ) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to hard delete ingredient" ) @router.get( route_builder.build_nested_resource_route("ingredients", "ingredient_id", "stock"), response_model=List[StockResponse] ) 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_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get stock entries for an ingredient (All users)""" try: 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" )