Files
bakery-ia/services/inventory/app/api/stock.py
2025-09-16 12:21:15 +02:00

330 lines
11 KiB
Python

# services/inventory/app/api/stock.py
"""
API endpoints for stock 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
import structlog
from app.core.database import get_db
from app.services.inventory_service import InventoryService
from app.schemas.inventory import (
StockCreate,
StockUpdate,
StockResponse,
StockMovementCreate,
StockMovementResponse,
StockFilter
)
from shared.auth.decorators import get_current_user_dep
logger = structlog.get_logger()
router = APIRouter(tags=["stock"])
# 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:
# Handle service tokens that don't have UUID user_ids
if current_user.get('type') == 'service':
return None
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User ID not found in context"
)
try:
return UUID(user_id)
except (ValueError, TypeError):
return None
@router.post("/tenants/{tenant_id}/stock", response_model=StockResponse)
async def add_stock(
stock_data: StockCreate,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Add new stock entry"""
try:
# Extract user ID - handle service tokens
user_id = get_current_user_id(current_user)
service = InventoryService()
stock = await service.add_stock(stock_data, tenant_id, user_id)
return stock
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 add stock"
)
@router.post("/tenants/{tenant_id}/stock/consume")
async def consume_stock(
tenant_id: UUID = Path(..., description="Tenant ID"),
ingredient_id: UUID = Query(..., description="Ingredient ID to consume"),
quantity: float = Query(..., gt=0, description="Quantity to consume"),
reference_number: Optional[str] = Query(None, description="Reference number (e.g., production order)"),
notes: Optional[str] = Query(None, description="Additional notes"),
fifo: bool = Query(True, description="Use FIFO (First In, First Out) method"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Consume stock for production"""
try:
# Extract user ID - handle service tokens
user_id = get_current_user_id(current_user)
service = InventoryService()
consumed_items = await service.consume_stock(
ingredient_id, quantity, tenant_id, user_id, reference_number, notes, fifo
)
return {
"ingredient_id": str(ingredient_id),
"total_quantity_consumed": quantity,
"consumed_items": consumed_items,
"method": "FIFO" if fifo else "LIFO"
}
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 consume stock"
)
@router.get("/tenants/{tenant_id}/stock/expiring", response_model=List[dict])
async def get_expiring_stock(
tenant_id: UUID = Path(..., description="Tenant ID"),
days_ahead: int = Query(7, ge=1, le=365, description="Days ahead to check for expiring items"),
db: AsyncSession = Depends(get_db)
):
"""Get stock items expiring within specified days"""
try:
service = InventoryService()
expiring_items = await service.check_expiration_alerts(tenant_id, days_ahead)
return expiring_items
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get expiring stock"
)
@router.get("/tenants/{tenant_id}/stock/low-stock", response_model=List[dict])
async def get_low_stock(
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get ingredients with low stock levels"""
try:
service = InventoryService()
low_stock_items = await service.check_low_stock_alerts(tenant_id)
return low_stock_items
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get low stock items"
)
@router.get("/tenants/{tenant_id}/stock/summary", response_model=dict)
async def get_stock_summary(
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get stock summary for dashboard"""
try:
service = InventoryService()
summary = await service.get_inventory_summary(tenant_id)
return summary.dict()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get stock summary"
)
@router.get("/tenants/{tenant_id}/stock", response_model=List[StockResponse])
async def get_stock(
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"),
ingredient_id: Optional[UUID] = Query(None, description="Filter by ingredient"),
available_only: bool = Query(True, description="Show only available stock"),
db: AsyncSession = Depends(get_db)
):
"""Get stock entries with filtering"""
try:
service = InventoryService()
stock_entries = await service.get_stock(
tenant_id, skip, limit, ingredient_id, available_only
)
return stock_entries
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get stock entries"
)
@router.get("/tenants/{tenant_id}/stock/movements", response_model=List[StockMovementResponse])
async def get_stock_movements(
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"),
ingredient_id: Optional[UUID] = Query(None, description="Filter by ingredient"),
movement_type: Optional[str] = Query(None, description="Filter by movement type"),
db: AsyncSession = Depends(get_db)
):
"""Get stock movements with filtering"""
logger.info("🌐 API endpoint reached!",
tenant_id=tenant_id,
ingredient_id=ingredient_id,
skip=skip,
limit=limit)
try:
service = InventoryService()
movements = await service.get_stock_movements(
tenant_id, skip, limit, ingredient_id, movement_type
)
logger.info("📈 Returning movements", count=len(movements))
return movements
except Exception as e:
logger.error("❌ Failed to get stock movements", error=str(e), tenant_id=tenant_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get stock movements"
)
@router.get("/tenants/{tenant_id}/stock/{stock_id}", response_model=StockResponse)
async def get_stock_entry(
stock_id: UUID = Path(..., description="Stock entry ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get specific stock entry"""
try:
service = InventoryService()
stock = await service.get_stock_entry(stock_id, tenant_id)
if not stock:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Stock entry not found"
)
return stock
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get stock entry"
)
@router.put("/tenants/{tenant_id}/stock/{stock_id}", response_model=StockResponse)
async def update_stock(
stock_data: StockUpdate,
stock_id: UUID = Path(..., description="Stock entry ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Update stock entry"""
try:
service = InventoryService()
stock = await service.update_stock(stock_id, stock_data, tenant_id)
if not stock:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Stock entry not found"
)
return stock
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 stock entry"
)
@router.delete("/tenants/{tenant_id}/stock/{stock_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_stock(
stock_id: UUID = Path(..., description="Stock entry ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Delete stock entry (mark as unavailable)"""
try:
service = InventoryService()
deleted = await service.delete_stock(stock_id, tenant_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Stock entry 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 stock entry"
)
@router.post("/tenants/{tenant_id}/stock/movements", response_model=StockMovementResponse)
async def create_stock_movement(
movement_data: StockMovementCreate,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create stock movement record"""
try:
# Extract user ID - handle service tokens
user_id = get_current_user_id(current_user)
service = InventoryService()
movement = await service.create_stock_movement(movement_data, tenant_id, user_id)
return movement
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 stock movement"
)