224 lines
7.6 KiB
Python
224 lines
7.6 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, 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
|
|
|
|
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_user: dict = Depends(get_current_user_dep),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Create a new ingredient"""
|
|
try:
|
|
# 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"),
|
|
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("/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"),
|
|
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("/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"),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""List ingredients with filtering"""
|
|
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("/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"),
|
|
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("/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"),
|
|
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"
|
|
) |