# services/suppliers/app/api/analytics.py """ Supplier Analytics API endpoints (ANALYTICS) Consolidates performance metrics, delivery stats, and all analytics operations """ from datetime import datetime, timedelta from typing import List, Optional, Dict, Any from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Path, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session import structlog from shared.auth.decorators import get_current_user_dep from shared.auth.access_control import require_user_role, analytics_tier_required from shared.routing import RouteBuilder from app.core.database import get_db from app.services.performance_service import PerformanceTrackingService, AlertService from app.services.dashboard_service import DashboardService from app.services.delivery_service import DeliveryService from app.schemas.performance import ( PerformanceMetric, Alert, PerformanceDashboardSummary, SupplierPerformanceInsights, PerformanceAnalytics, BusinessModelInsights, AlertSummary, PerformanceReportRequest, ExportDataResponse ) from app.schemas.suppliers import DeliveryPerformanceStats, DeliverySummaryStats from app.models.performance import PerformancePeriod, PerformanceMetricType, AlertType, AlertSeverity logger = structlog.get_logger() # Create route builder for consistent URL structure route_builder = RouteBuilder('suppliers') router = APIRouter(tags=["analytics"]) # ===== Dependency Injection ===== async def get_performance_service() -> PerformanceTrackingService: """Get performance tracking service""" return PerformanceTrackingService() async def get_alert_service() -> AlertService: """Get alert service""" return AlertService() async def get_dashboard_service() -> DashboardService: """Get dashboard service""" return DashboardService() # ===== Delivery Analytics ===== @router.get( route_builder.build_analytics_route("deliveries/performance-stats"), response_model=DeliveryPerformanceStats ) async def get_delivery_performance_stats( tenant_id: UUID = Path(...), days_back: int = Query(30, ge=1, le=365, description="Number of days to analyze"), supplier_id: Optional[UUID] = Query(None, description="Filter by supplier ID"), current_user: Dict[str, Any] = Depends(get_current_user_dep), db: Session = Depends(get_db) ): """Get delivery performance statistics""" try: service = DeliveryService(db) stats = await service.get_delivery_performance_stats( tenant_id=current_user.tenant_id, days_back=days_back, supplier_id=supplier_id ) return DeliveryPerformanceStats(**stats) except Exception as e: logger.error("Error getting delivery performance stats", error=str(e)) raise HTTPException(status_code=500, detail="Failed to retrieve delivery performance statistics") @router.get( route_builder.build_analytics_route("deliveries/summary-stats"), response_model=DeliverySummaryStats ) async def get_delivery_summary_stats( tenant_id: UUID = Path(...), current_user: Dict[str, Any] = Depends(get_current_user_dep), db: Session = Depends(get_db) ): """Get delivery summary statistics for dashboard""" try: service = DeliveryService(db) stats = await service.get_upcoming_deliveries_summary(current_user.tenant_id) return DeliverySummaryStats(**stats) except Exception as e: logger.error("Error getting delivery summary stats", error=str(e)) raise HTTPException(status_code=500, detail="Failed to retrieve delivery summary statistics") # ===== Performance Metrics ===== @router.post( route_builder.build_analytics_route("performance/{supplier_id}/calculate"), response_model=PerformanceMetric ) async def calculate_supplier_performance( tenant_id: UUID = Path(...), supplier_id: UUID = Path(...), period: PerformancePeriod = Query(...), period_start: datetime = Query(...), period_end: datetime = Query(...), current_user: dict = Depends(get_current_user_dep), performance_service: PerformanceTrackingService = Depends(get_performance_service), db: AsyncSession = Depends(get_db) ): """Calculate performance metrics for a supplier""" try: metric = await performance_service.calculate_supplier_performance( db, supplier_id, tenant_id, period, period_start, period_end ) if not metric: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Unable to calculate performance metrics" ) logger.info("Performance metrics calculated", tenant_id=str(tenant_id), supplier_id=str(supplier_id), period=period.value) return metric except Exception as e: logger.error("Error calculating performance metrics", tenant_id=str(tenant_id), supplier_id=str(supplier_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to calculate performance metrics" ) @router.get( route_builder.build_analytics_route("performance/{supplier_id}/metrics"), response_model=List[PerformanceMetric] ) async def get_supplier_performance_metrics( tenant_id: UUID = Path(...), supplier_id: UUID = Path(...), metric_type: Optional[PerformanceMetricType] = Query(None), period: Optional[PerformancePeriod] = Query(None), date_from: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None), limit: int = Query(50, ge=1, le=500), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get performance metrics for a supplier""" try: from app.models.performance import SupplierPerformanceMetric from sqlalchemy import select, and_, desc # Build query for performance metrics query = select(SupplierPerformanceMetric).where( and_( SupplierPerformanceMetric.supplier_id == supplier_id, SupplierPerformanceMetric.tenant_id == tenant_id ) ) # Apply filters if metric_type: query = query.where(SupplierPerformanceMetric.metric_type == metric_type) if date_from: query = query.where(SupplierPerformanceMetric.calculated_at >= date_from) if date_to: query = query.where(SupplierPerformanceMetric.calculated_at <= date_to) # Order by most recent and apply limit query = query.order_by(desc(SupplierPerformanceMetric.calculated_at)).limit(limit) result = await db.execute(query) metrics = result.scalars().all() logger.info("Retrieved performance metrics", tenant_id=str(tenant_id), supplier_id=str(supplier_id), count=len(metrics)) return metrics except Exception as e: logger.error("Error getting performance metrics", tenant_id=str(tenant_id), supplier_id=str(supplier_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve performance metrics" ) # ===== Alert Management ===== @router.post( route_builder.build_analytics_route("performance/alerts/evaluate"), response_model=List[Alert] ) @require_user_role(['admin', 'owner']) async def evaluate_performance_alerts( tenant_id: UUID = Path(...), supplier_id: Optional[UUID] = Query(None, description="Specific supplier to evaluate"), current_user: dict = Depends(get_current_user_dep), alert_service: AlertService = Depends(get_alert_service), db: AsyncSession = Depends(get_db) ): """Evaluate and create performance-based alerts""" try: alerts = await alert_service.evaluate_performance_alerts(db, tenant_id, supplier_id) logger.info("Performance alerts evaluated", tenant_id=str(tenant_id), alerts_created=len(alerts)) return alerts except Exception as e: logger.error("Error evaluating performance alerts", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to evaluate performance alerts" ) @router.get( route_builder.build_analytics_route("performance/alerts"), response_model=List[Alert] ) async def get_supplier_alerts( tenant_id: UUID = Path(...), supplier_id: Optional[UUID] = Query(None), alert_type: Optional[AlertType] = Query(None), severity: Optional[AlertSeverity] = Query(None), date_from: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None), limit: int = Query(50, ge=1, le=500), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get supplier alerts with filtering""" try: from app.models.performance import SupplierAlert from sqlalchemy import select, and_, desc # Build query for alerts query = select(SupplierAlert).where( SupplierAlert.tenant_id == tenant_id ) # Apply filters if supplier_id: query = query.where(SupplierAlert.supplier_id == supplier_id) if alert_type: query = query.where(SupplierAlert.alert_type == alert_type) if severity: query = query.where(SupplierAlert.severity == severity) if date_from: query = query.where(SupplierAlert.created_at >= date_from) if date_to: query = query.where(SupplierAlert.created_at <= date_to) # Order by most recent and apply limit query = query.order_by(desc(SupplierAlert.created_at)).limit(limit) result = await db.execute(query) alerts = result.scalars().all() logger.info("Retrieved supplier alerts", tenant_id=str(tenant_id), count=len(alerts)) return alerts except Exception as e: logger.error("Error getting supplier alerts", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve supplier alerts" ) @router.get( route_builder.build_analytics_route("performance/alerts/summary"), response_model=List[AlertSummary] ) async def get_alert_summary( tenant_id: UUID = Path(...), date_from: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get alert summary by type and severity""" try: summary = await dashboard_service.get_alert_summary(db, tenant_id, date_from, date_to) return summary except Exception as e: logger.error("Error getting alert summary", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve alert summary" ) # ===== Dashboard Analytics ===== @router.get( route_builder.build_dashboard_route("performance/summary"), response_model=PerformanceDashboardSummary ) async def get_performance_dashboard_summary( tenant_id: UUID = Path(...), date_from: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get comprehensive performance dashboard summary""" try: summary = await dashboard_service.get_performance_dashboard_summary( db, tenant_id, date_from, date_to ) logger.info("Performance dashboard summary retrieved", tenant_id=str(tenant_id)) return summary except Exception as e: logger.error("Error getting dashboard summary", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve dashboard summary" ) @router.get( route_builder.build_analytics_route("performance/{supplier_id}/insights"), response_model=SupplierPerformanceInsights ) async def get_supplier_performance_insights( tenant_id: UUID = Path(...), supplier_id: UUID = Path(...), days_back: int = Query(30, ge=1, le=365), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get detailed performance insights for a specific supplier""" try: insights = await dashboard_service.get_supplier_performance_insights( db, tenant_id, supplier_id, days_back ) logger.info("Supplier performance insights retrieved", tenant_id=str(tenant_id), supplier_id=str(supplier_id)) return insights except Exception as e: logger.error("Error getting supplier insights", tenant_id=str(tenant_id), supplier_id=str(supplier_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve supplier insights" ) @router.get( route_builder.build_analytics_route("performance/performance"), response_model=PerformanceAnalytics ) @analytics_tier_required async def get_performance_analytics( tenant_id: UUID = Path(...), period_days: int = Query(90, ge=1, le=365), current_user: dict = Depends(get_current_user_dep), dashboard_service: DashboardService = Depends(get_dashboard_service), db: AsyncSession = Depends(get_db) ): """Get advanced performance analytics""" try: analytics = await dashboard_service.get_performance_analytics( db, tenant_id, period_days ) logger.info("Performance analytics retrieved", tenant_id=str(tenant_id), period_days=period_days) return analytics except Exception as e: logger.error("Error getting performance analytics", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve performance analytics" ) @router.get( route_builder.build_analytics_route("performance/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 detection and insights""" 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) 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" ) # ===== Export and Reporting ===== @router.post( route_builder.build_analytics_route("performance/reports/generate"), response_model=ExportDataResponse ) @require_user_role(['admin', 'owner']) async def generate_performance_report( report_request: PerformanceReportRequest, tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Generate a performance report""" try: # TODO: Implement report generation raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Report generation not yet implemented" ) except Exception as e: logger.error("Error generating performance report", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to generate performance report" ) @router.get( route_builder.build_analytics_route("performance/export") ) async def export_performance_data( tenant_id: UUID = Path(...), format: str = Query("json", description="Export format: json, csv, excel"), date_from: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None), supplier_ids: Optional[List[UUID]] = Query(None), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Export performance data""" try: if format.lower() not in ["json", "csv", "excel"]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Unsupported export format. Use: json, csv, excel" ) # TODO: Implement data export raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Data export not yet implemented" ) except Exception as e: logger.error("Error exporting performance data", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to export performance data" ) # ===== Configuration and Health ===== @router.get( route_builder.build_analytics_route("performance/config") ) async def get_performance_config( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep) ): """Get performance tracking configuration""" try: from app.core.config import settings config = { "performance_tracking": { "enabled": settings.PERFORMANCE_TRACKING_ENABLED, "calculation_interval_minutes": settings.PERFORMANCE_CALCULATION_INTERVAL_MINUTES, "cache_ttl_seconds": settings.PERFORMANCE_CACHE_TTL }, "thresholds": { "excellent_delivery_rate": settings.EXCELLENT_DELIVERY_RATE, "good_delivery_rate": settings.GOOD_DELIVERY_RATE, "acceptable_delivery_rate": settings.ACCEPTABLE_DELIVERY_RATE, "poor_delivery_rate": settings.POOR_DELIVERY_RATE, "excellent_quality_rate": settings.EXCELLENT_QUALITY_RATE, "good_quality_rate": settings.GOOD_QUALITY_RATE, "acceptable_quality_rate": settings.ACCEPTABLE_QUALITY_RATE, "poor_quality_rate": settings.POOR_QUALITY_RATE }, "alerts": { "enabled": settings.ALERTS_ENABLED, "evaluation_interval_minutes": settings.ALERT_EVALUATION_INTERVAL_MINUTES, "retention_days": settings.ALERT_RETENTION_DAYS, "critical_delivery_delay_hours": settings.CRITICAL_DELIVERY_DELAY_HOURS, "critical_quality_rejection_rate": settings.CRITICAL_QUALITY_REJECTION_RATE }, "dashboard": { "cache_ttl_seconds": settings.DASHBOARD_CACHE_TTL, "refresh_interval_seconds": settings.DASHBOARD_REFRESH_INTERVAL, "default_analytics_period_days": settings.DEFAULT_ANALYTICS_PERIOD_DAYS }, "business_model": { "detection_enabled": settings.ENABLE_BUSINESS_MODEL_DETECTION, "central_bakery_threshold": settings.CENTRAL_BAKERY_THRESHOLD_SUPPLIERS, "individual_bakery_threshold": settings.INDIVIDUAL_BAKERY_THRESHOLD_SUPPLIERS } } return config except Exception as e: logger.error("Error getting performance config", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve performance configuration" ) @router.get( route_builder.build_analytics_route("performance/health") ) async def get_performance_health( tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep) ): """Get performance service health status""" try: return { "service": "suppliers-performance", "status": "healthy", "timestamp": datetime.now().isoformat(), "tenant_id": str(tenant_id), "features": { "performance_tracking": "enabled", "alerts": "enabled", "dashboard_analytics": "enabled", "business_model_detection": "enabled" } } except Exception as e: logger.error("Error getting performance health", tenant_id=str(tenant_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get performance health status" )