# services/inventory/app/api/batch.py """ Inventory Batch API - Batch operations for enterprise dashboards Phase 2 optimization: Eliminate N+1 query patterns by fetching inventory data for multiple tenants in a single request. """ from fastapi import APIRouter, Depends, HTTPException, Body from typing import List, Dict, Any from uuid import UUID from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession import structlog import asyncio from app.core.database import get_db from app.services.dashboard_service import DashboardService from app.services.inventory_service import InventoryService from shared.auth.decorators import get_current_user_dep router = APIRouter(tags=["inventory-batch"]) logger = structlog.get_logger() class InventorySummaryBatchRequest(BaseModel): """Request model for batch inventory summary""" tenant_ids: List[str] = Field(..., description="List of tenant IDs", max_length=100) class InventorySummary(BaseModel): """Inventory summary for a single tenant""" tenant_id: str total_value: float out_of_stock_count: int low_stock_count: int adequate_stock_count: int total_ingredients: int @router.post("/batch/inventory-summary", response_model=Dict[str, InventorySummary]) async def get_inventory_summary_batch( request: InventorySummaryBatchRequest = Body(...), current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Get inventory summary for multiple tenants in a single request. Optimized for enterprise dashboards to eliminate N+1 query patterns. Fetches inventory data for all tenants in parallel. Args: request: Batch request with tenant IDs Returns: Dictionary mapping tenant_id -> inventory summary Example: POST /api/v1/inventory/batch/inventory-summary { "tenant_ids": ["tenant-1", "tenant-2", "tenant-3"] } Response: { "tenant-1": {"tenant_id": "tenant-1", "total_value": 15000, ...}, "tenant-2": {"tenant_id": "tenant-2", "total_value": 12000, ...}, "tenant-3": {"tenant_id": "tenant-3", "total_value": 18000, ...} } """ try: if len(request.tenant_ids) > 100: raise HTTPException( status_code=400, detail="Maximum 100 tenant IDs allowed per batch request" ) if not request.tenant_ids: return {} logger.info( "Batch fetching inventory summaries", tenant_count=len(request.tenant_ids) ) async def fetch_tenant_inventory(tenant_id: str) -> tuple[str, InventorySummary]: """Fetch inventory summary for a single tenant""" try: tenant_uuid = UUID(tenant_id) dashboard_service = DashboardService( inventory_service=InventoryService(), food_safety_service=None ) overview = await dashboard_service.get_inventory_overview(db, tenant_uuid) return tenant_id, InventorySummary( tenant_id=tenant_id, total_value=float(overview.get('total_value', 0)), out_of_stock_count=int(overview.get('out_of_stock_count', 0)), low_stock_count=int(overview.get('low_stock_count', 0)), adequate_stock_count=int(overview.get('adequate_stock_count', 0)), total_ingredients=int(overview.get('total_ingredients', 0)) ) except Exception as e: logger.warning( "Failed to fetch inventory for tenant in batch", tenant_id=tenant_id, error=str(e) ) return tenant_id, InventorySummary( tenant_id=tenant_id, total_value=0.0, out_of_stock_count=0, low_stock_count=0, adequate_stock_count=0, total_ingredients=0 ) # Fetch all tenant inventory data in parallel tasks = [fetch_tenant_inventory(tid) for tid in request.tenant_ids] results = await asyncio.gather(*tasks, return_exceptions=True) # Build result dictionary result_dict = {} for result in results: if isinstance(result, Exception): logger.error("Exception in batch inventory fetch", error=str(result)) continue tenant_id, summary = result result_dict[tenant_id] = summary logger.info( "Batch inventory summaries retrieved", requested_count=len(request.tenant_ids), successful_count=len(result_dict) ) return result_dict except HTTPException: raise except Exception as e: logger.error("Error in batch inventory summary", error=str(e), exc_info=True) raise HTTPException( status_code=500, detail=f"Failed to fetch batch inventory summaries: {str(e)}" )