# 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 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 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/{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" ) @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""" try: service = InventoryService() movements = await service.get_stock_movements( tenant_id, skip, limit, ingredient_id, movement_type ) return movements except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get stock movements" )