Files
bakery-ia/services/inventory/app/api/ingredients.py
2025-08-13 17:39:35 +02:00

208 lines
6.8 KiB
Python

# 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, 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
from shared.auth.tenant_access import verify_tenant_access_dep
router = APIRouter(prefix="/ingredients", 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("/", response_model=IngredientResponse)
async def create_ingredient(
ingredient_data: IngredientCreate,
tenant_id: UUID = Depends(verify_tenant_access_dep),
user_id: UUID = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db)
):
"""Create a new ingredient"""
try:
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("/{ingredient_id}", response_model=IngredientResponse)
async def get_ingredient(
ingredient_id: UUID,
tenant_id: UUID = Depends(verify_tenant_access_dep),
db: AsyncSession = Depends(get_db)
):
"""Get ingredient by ID"""
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("/{ingredient_id}", response_model=IngredientResponse)
async def update_ingredient(
ingredient_id: UUID,
ingredient_data: IngredientUpdate,
tenant_id: UUID = Depends(verify_tenant_access_dep),
db: AsyncSession = Depends(get_db)
):
"""Update ingredient"""
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("/", response_model=List[IngredientResponse])
async def list_ingredients(
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"),
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"),
tenant_id: UUID = Depends(verify_tenant_access_dep),
db: AsyncSession = Depends(get_db)
):
"""List ingredients with filtering"""
try:
service = InventoryService()
# Build filters
filters = {}
if category:
filters['category'] = category
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("/{ingredient_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_ingredient(
ingredient_id: UUID,
tenant_id: UUID = Depends(verify_tenant_access_dep),
db: AsyncSession = Depends(get_db)
):
"""Soft delete ingredient (mark as inactive)"""
try:
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("/{ingredient_id}/stock", response_model=List[dict])
async def get_ingredient_stock(
ingredient_id: UUID,
include_unavailable: bool = Query(False, description="Include unavailable stock"),
tenant_id: UUID = Depends(verify_tenant_access_dep),
db: AsyncSession = Depends(get_db)
):
"""Get stock entries for an ingredient"""
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"
)