# services/inventory/app/api/analytics.py """ Analytics API endpoints for Inventory Service Following standardized URL structure: /api/v1/tenants/{tenant_id}/inventory/analytics/{operation} Requires: Professional or Enterprise subscription tier """ from datetime import datetime, timedelta 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 shared.auth.decorators import get_current_user_dep from shared.auth.access_control import analytics_tier_required from app.core.database import get_db from app.services.inventory_service import InventoryService from app.services.dashboard_service import DashboardService from app.services.food_safety_service import FoodSafetyService from app.schemas.dashboard import ( InventoryAnalytics, BusinessModelInsights, ) from shared.routing import RouteBuilder logger = structlog.get_logger() # Create route builder for consistent URL structure route_builder = RouteBuilder('inventory') router = APIRouter(tags=["inventory-analytics"]) # ===== Dependency Injection ===== async def get_dashboard_service(db: AsyncSession = Depends(get_db)) -> DashboardService: """Get dashboard service with dependencies""" return DashboardService( inventory_service=InventoryService(), food_safety_service=FoodSafetyService() ) # ===== ANALYTICS ENDPOINTS (Professional/Enterprise Only) ===== @router.get( route_builder.build_analytics_route("inventory-insights"), response_model=InventoryAnalytics ) @analytics_tier_required async def get_inventory_analytics( tenant_id: UUID = Path(...), days_back: int = Query(30, ge=1, le=365, description="Number of days to analyze"), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """ Get advanced inventory analytics (Professional/Enterprise only) Provides: - Stock turnover rates - Inventory valuation trends - ABC analysis - Stockout risk predictions - Seasonal patterns """ try: analytics = await dashboard_service.get_inventory_analytics(db, tenant_id, days_back) logger.info("Inventory analytics retrieved", tenant_id=str(tenant_id), days_analyzed=days_back, user_id=current_user.get('user_id')) return analytics except Exception as e: logger.error("Error getting inventory analytics", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve inventory analytics" ) @router.get( route_builder.build_analytics_route("business-model"), response_model=BusinessModelInsights ) @analytics_tier_required async def get_business_model_insights( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """ Get business model insights based on inventory patterns (Professional/Enterprise only) Analyzes inventory patterns to provide insights on: - Detected business model (retail, wholesale, production, etc.) - Product mix recommendations - Inventory optimization suggestions """ try: insights = await dashboard_service.get_business_model_insights(db, tenant_id) logger.info("Business model insights retrieved", tenant_id=str(tenant_id), detected_model=insights.detected_model, user_id=current_user.get('user_id')) return insights except Exception as e: logger.error("Error getting business model insights", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve business model insights" ) @router.get( route_builder.build_analytics_route("turnover-rate"), response_model=dict ) @analytics_tier_required async def get_inventory_turnover_rate( tenant_id: UUID = Path(...), start_date: Optional[datetime] = Query(None, description="Start date for analysis"), end_date: Optional[datetime] = Query(None, description="End date for analysis"), category: Optional[str] = Query(None, description="Filter by category"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Calculate inventory turnover rate (Professional/Enterprise only) Metrics: - Overall turnover rate - By category - By product - Trend analysis """ try: service = InventoryService() # Set default dates if not provided if not end_date: end_date = datetime.now() if not start_date: start_date = end_date - timedelta(days=90) # Calculate turnover metrics turnover_data = await service.calculate_turnover_rate( tenant_id, start_date, end_date, category ) logger.info("Turnover rate calculated", tenant_id=str(tenant_id), category=category, user_id=current_user.get('user_id')) return turnover_data except Exception as e: logger.error("Error calculating turnover rate", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to calculate turnover rate" ) @router.get( route_builder.build_analytics_route("abc-analysis"), response_model=dict ) @analytics_tier_required async def get_abc_analysis( tenant_id: UUID = Path(...), days_back: int = Query(90, ge=30, le=365, description="Days to analyze"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Perform ABC analysis on inventory (Professional/Enterprise only) Categorizes inventory items by: - A: High-value items requiring tight control - B: Moderate-value items with moderate control - C: Low-value items with simple control """ try: service = InventoryService() abc_analysis = await service.perform_abc_analysis(tenant_id, days_back) logger.info("ABC analysis completed", tenant_id=str(tenant_id), days_analyzed=days_back, user_id=current_user.get('user_id')) return abc_analysis except Exception as e: logger.error("Error performing ABC analysis", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to perform ABC analysis" ) @router.get( route_builder.build_analytics_route("stockout-predictions"), response_model=dict ) @analytics_tier_required async def get_stockout_predictions( tenant_id: UUID = Path(...), forecast_days: int = Query(30, ge=7, le=90, description="Days to forecast"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Predict potential stockouts (Professional/Enterprise only) Provides: - Items at risk of stockout - Predicted stockout dates - Recommended reorder quantities - Lead time considerations """ try: service = InventoryService() predictions = await service.predict_stockouts(tenant_id, forecast_days) logger.info("Stockout predictions generated", tenant_id=str(tenant_id), forecast_days=forecast_days, at_risk_items=len(predictions.get('items_at_risk', [])), user_id=current_user.get('user_id')) return predictions except Exception as e: logger.error("Error predicting stockouts", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to predict stockouts" ) @router.get( route_builder.build_analytics_route("waste-analysis"), response_model=dict ) @analytics_tier_required async def get_waste_analysis( tenant_id: UUID = Path(...), start_date: Optional[datetime] = Query(None, description="Start date"), end_date: Optional[datetime] = Query(None, description="End date"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Analyze inventory waste and expiration (Professional/Enterprise only) Metrics: - Total waste value - Waste by category - Expiration patterns - Optimization recommendations """ try: service = InventoryService() # Set default dates if not end_date: end_date = datetime.now() if not start_date: start_date = end_date - timedelta(days=30) waste_analysis = await service.analyze_waste(tenant_id, start_date, end_date) logger.info("Waste analysis completed", tenant_id=str(tenant_id), total_waste_value=waste_analysis.get('total_waste_value', 0), user_id=current_user.get('user_id')) return waste_analysis except Exception as e: logger.error("Error analyzing waste", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to analyze waste" )