""" Enterprise Inventory API Endpoints APIs for enterprise-level inventory management across outlets """ from fastapi import APIRouter, Depends, HTTPException, Query from typing import List, Optional from datetime import date from pydantic import BaseModel, Field import structlog from app.services.enterprise_inventory_service import EnterpriseInventoryService from shared.auth.tenant_access import verify_tenant_permission_dep from shared.clients import get_inventory_client, get_tenant_client from app.core.config import settings logger = structlog.get_logger() router = APIRouter() # Pydantic models for request/response class InventoryCoverageResponse(BaseModel): outlet_id: str = Field(..., description="Outlet tenant ID") outlet_name: str = Field(..., description="Outlet name") overall_coverage: float = Field(..., description="Overall inventory coverage percentage (0-100)") critical_items_count: int = Field(..., description="Number of items at critical stock levels") high_risk_items_count: int = Field(..., description="Number of items at high risk of stockout") medium_risk_items_count: int = Field(..., description="Number of items at medium risk") low_risk_items_count: int = Field(..., description="Number of items at low risk") fulfillment_rate: float = Field(..., description="Order fulfillment rate percentage (0-100)") last_updated: str = Field(..., description="Last inventory update timestamp") status: str = Field(..., description="Overall status: normal, warning, critical") class ProductCoverageDetail(BaseModel): product_id: str = Field(..., description="Product ID") product_name: str = Field(..., description="Product name") current_stock: int = Field(..., description="Current stock quantity") safety_stock: int = Field(..., description="Safety stock threshold") coverage_percentage: float = Field(..., description="Coverage percentage (current/safety)") risk_level: str = Field(..., description="Risk level: critical, high, medium, low") days_until_stockout: Optional[int] = Field(None, description="Estimated days until stockout") class OutletInventoryDetailResponse(BaseModel): outlet_id: str = Field(..., description="Outlet tenant ID") outlet_name: str = Field(..., description="Outlet name") overall_coverage: float = Field(..., description="Overall inventory coverage percentage") products: List[ProductCoverageDetail] = Field(..., description="Product-level inventory details") last_updated: str = Field(..., description="Last update timestamp") class NetworkInventorySummary(BaseModel): total_outlets: int = Field(..., description="Total number of outlets") average_coverage: float = Field(..., description="Network average inventory coverage") average_fulfillment_rate: float = Field(..., description="Network average fulfillment rate") critical_outlets: int = Field(..., description="Number of outlets with critical status") warning_outlets: int = Field(..., description="Number of outlets with warning status") normal_outlets: int = Field(..., description="Number of outlets with normal status") total_critical_items: int = Field(..., description="Total critical items across network") network_health_score: float = Field(..., description="Overall network health score (0-100)") class InventoryAlert(BaseModel): alert_id: str = Field(..., description="Alert ID") outlet_id: str = Field(..., description="Outlet ID") outlet_name: str = Field(..., description="Outlet name") product_id: Optional[str] = Field(None, description="Product ID if applicable") product_name: Optional[str] = Field(None, description="Product name if applicable") alert_type: str = Field(..., description="Type of alert: stockout_risk, low_coverage, etc.") severity: str = Field(..., description="Severity: critical, high, medium, low") current_coverage: float = Field(..., description="Current inventory coverage percentage") threshold: float = Field(..., description="Threshold that triggered alert") timestamp: str = Field(..., description="Alert timestamp") message: str = Field(..., description="Alert message") async def get_enterprise_inventory_service() -> "EnterpriseInventoryService": """Dependency injection for EnterpriseInventoryService""" inventory_client = get_inventory_client(settings, "inventory-service") tenant_client = get_tenant_client(settings, "inventory-service") return EnterpriseInventoryService( inventory_client=inventory_client, tenant_client=tenant_client ) @router.get("/tenants/{parent_id}/outlets/inventory-coverage", response_model=List[InventoryCoverageResponse], summary="Get inventory coverage for all outlets in network") async def get_outlet_inventory_coverage( parent_id: str, min_coverage: Optional[float] = Query(None, description="Filter outlets with coverage below this threshold"), risk_level: Optional[str] = Query(None, description="Filter by risk level: critical, high, medium, low"), enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Get inventory coverage metrics for all child outlets in a parent tenant's network This endpoint provides a comprehensive view of inventory health across all outlets, enabling enterprise managers to identify stockout risks and prioritize inventory transfers. """ try: # Verify this is a parent tenant tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id) if tenant_info.get('tenant_type') != 'parent': raise HTTPException( status_code=403, detail="Only parent tenants can access outlet inventory coverage" ) # Get all child outlets for this parent child_outlets = await enterprise_inventory_service.get_child_outlets(parent_id) if not child_outlets: return [] # Get inventory coverage for each outlet coverage_data = [] for outlet in child_outlets: outlet_id = outlet['id'] # Get inventory coverage data coverage = await enterprise_inventory_service.get_inventory_coverage(outlet_id) if coverage: # Apply filters if specified if min_coverage is not None and coverage['overall_coverage'] >= min_coverage: continue if risk_level is not None and coverage.get('status') != risk_level: continue coverage_data.append(coverage) # Sort by coverage (lowest first) to prioritize critical outlets coverage_data.sort(key=lambda x: x['overall_coverage']) return coverage_data except Exception as e: logger.error("Failed to get outlet inventory coverage", error=str(e)) raise HTTPException(status_code=500, detail=f"Failed to get inventory coverage: {str(e)}") @router.get("/tenants/{parent_id}/outlets/inventory-summary", response_model=NetworkInventorySummary, summary="Get network-wide inventory summary") async def get_network_inventory_summary( parent_id: str, enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Get aggregated inventory summary across the entire network Provides key metrics for network health monitoring and decision making. """ try: # Verify this is a parent tenant tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id) if tenant_info.get('tenant_type') != 'parent': raise HTTPException( status_code=403, detail="Only parent tenants can access network inventory summary" ) return await enterprise_inventory_service.get_network_inventory_summary(parent_id) except Exception as e: logger.error("Failed to get network inventory summary", error=str(e)) raise HTTPException(status_code=500, detail=f"Failed to get inventory summary: {str(e)}") @router.get("/tenants/{parent_id}/outlets/{outlet_id}/inventory-details", response_model=OutletInventoryDetailResponse, summary="Get detailed inventory for specific outlet") async def get_outlet_inventory_details( parent_id: str, outlet_id: str, product_id: Optional[str] = Query(None, description="Filter by specific product ID"), risk_level: Optional[str] = Query(None, description="Filter products by risk level"), enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Get detailed product-level inventory data for a specific outlet Enables drill-down analysis of inventory issues at the product level. """ try: # Verify parent-child relationship await enterprise_inventory_service.verify_parent_child_relationship(parent_id, outlet_id) return await enterprise_inventory_service.get_outlet_inventory_details(outlet_id, product_id, risk_level) except Exception as e: logger.error("Failed to get outlet inventory details", error=str(e)) raise HTTPException(status_code=500, detail=f"Failed to get inventory details: {str(e)}") @router.get("/tenants/{parent_id}/inventory-alerts", response_model=List[InventoryAlert], summary="Get real-time inventory alerts across network") async def get_network_inventory_alerts( parent_id: str, severity: Optional[str] = Query(None, description="Filter by severity: critical, high, medium, low"), alert_type: Optional[str] = Query(None, description="Filter by alert type"), limit: int = Query(50, description="Maximum number of alerts to return"), enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Get real-time inventory alerts across all outlets Provides actionable alerts for inventory management and stockout prevention. """ try: # Verify this is a parent tenant tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id) if tenant_info.get('tenant_type') != 'parent': raise HTTPException( status_code=403, detail="Only parent tenants can access network inventory alerts" ) alerts = await enterprise_inventory_service.get_inventory_alerts(parent_id) # Apply filters if severity: alerts = [alert for alert in alerts if alert.get('severity') == severity] if alert_type: alerts = [alert for alert in alerts if alert.get('alert_type') == alert_type] # Sort by severity (critical first) and timestamp (newest first) severity_order = {'critical': 1, 'high': 2, 'medium': 3, 'low': 4} alerts.sort(key=lambda x: (severity_order.get(x.get('severity', 'low'), 5), -int(x.get('timestamp', 0)))) return alerts[:limit] except Exception as e: logger.error("Failed to get inventory alerts", error=str(e)) raise HTTPException(status_code=500, detail=f"Failed to get inventory alerts: {str(e)}") @router.post("/tenants/{parent_id}/inventory-transfers/recommend", summary="Get inventory transfer recommendations") async def get_inventory_transfer_recommendations( parent_id: str, urgency: str = Query("medium", description="Urgency level: low, medium, high, critical"), enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Get AI-powered inventory transfer recommendations Analyzes inventory levels across outlets and suggests optimal transfers to prevent stockouts and balance inventory. """ try: # Verify this is a parent tenant tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id) if tenant_info.get('tenant_type') != 'parent': raise HTTPException( status_code=403, detail="Only parent tenants can request transfer recommendations" ) recommendations = await enterprise_inventory_service.get_transfer_recommendations(parent_id, urgency) return { 'success': True, 'recommendations': recommendations, 'message': f'Generated {len(recommendations)} transfer recommendations' } except Exception as e: logger.error("Failed to get transfer recommendations", error=str(e)) raise HTTPException(status_code=500, detail=f"Failed to get recommendations: {str(e)}") @router.get("/tenants/{parent_id}/inventory/coverage-trends", summary="Get inventory coverage trends over time") async def get_inventory_coverage_trends( parent_id: str, days: int = Query(30, description="Number of days to analyze"), enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Get historical inventory coverage trends Enables analysis of inventory performance over time. """ try: # Verify this is a parent tenant tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id) if tenant_info.get('tenant_type') != 'parent': raise HTTPException( status_code=403, detail="Only parent tenants can access coverage trends" ) trends = await enterprise_inventory_service.get_coverage_trends(parent_id, days) return { 'success': True, 'trends': trends, 'period': f'Last {days} days' } except Exception as e: logger.error("Failed to get coverage trends", error=str(e)) raise HTTPException(status_code=500, detail=f"Failed to get coverage trends: {str(e)}")