Files
bakery-ia/services/suppliers/app/api/performance.py
2025-08-21 20:28:14 +02:00

599 lines
22 KiB
Python

# ================================================================
# services/suppliers/app/api/performance.py
# ================================================================
"""
Supplier Performance Tracking API endpoints
"""
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, get_current_tenant_id_dep
from app.core.database import get_db
from app.services.performance_service import PerformanceTrackingService, AlertService
from app.services.dashboard_service import DashboardService
from app.schemas.performance import (
PerformanceMetric, PerformanceMetricCreate, PerformanceMetricUpdate,
Alert, AlertCreate, AlertUpdate, Scorecard, ScorecardCreate, ScorecardUpdate,
PerformanceDashboardSummary, SupplierPerformanceInsights, PerformanceAnalytics,
BusinessModelInsights, AlertSummary, DashboardFilter, AlertFilter,
PerformanceReportRequest, ExportDataResponse
)
from app.models.performance import PerformancePeriod, PerformanceMetricType, AlertType, AlertSeverity
logger = structlog.get_logger()
router = APIRouter(prefix="/performance", tags=["performance"])
# ===== 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()
# ===== Performance Metrics Endpoints =====
@router.post("/tenants/{tenant_id}/suppliers/{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_tenant: str = Depends(get_current_tenant_id_dep),
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:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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("/tenants/{tenant_id}/suppliers/{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_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get performance metrics for a supplier"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# TODO: Implement get_supplier_performance_metrics in service
# For now, return empty list
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 Endpoints =====
@router.post("/tenants/{tenant_id}/alerts/evaluate", response_model=List[Alert])
async def evaluate_performance_alerts(
tenant_id: UUID = Path(...),
supplier_id: Optional[UUID] = Query(None, description="Specific supplier to evaluate"),
current_tenant: str = Depends(get_current_tenant_id_dep),
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:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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("/tenants/{tenant_id}/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_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get supplier alerts with filtering"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# TODO: Implement get_supplier_alerts in service
# For now, return empty list
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.patch("/tenants/{tenant_id}/alerts/{alert_id}", response_model=Alert)
async def update_alert(
alert_update: AlertUpdate,
tenant_id: UUID = Path(...),
alert_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update an alert (acknowledge, resolve, etc.)"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# TODO: Implement update_alert in service
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Alert update not yet implemented"
)
except Exception as e:
logger.error("Error updating alert",
tenant_id=str(tenant_id),
alert_id=str(alert_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update alert"
)
# ===== Dashboard Endpoints =====
@router.get("/tenants/{tenant_id}/dashboard/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_tenant: str = Depends(get_current_tenant_id_dep),
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:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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("/tenants/{tenant_id}/suppliers/{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_tenant: str = Depends(get_current_tenant_id_dep),
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:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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("/tenants/{tenant_id}/analytics", response_model=PerformanceAnalytics)
async def get_performance_analytics(
tenant_id: UUID = Path(...),
period_days: int = Query(90, ge=1, le=365),
current_tenant: str = Depends(get_current_tenant_id_dep),
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:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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("/tenants/{tenant_id}/business-model", response_model=BusinessModelInsights)
async def get_business_model_insights(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
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:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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"
)
@router.get("/tenants/{tenant_id}/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_tenant: str = Depends(get_current_tenant_id_dep),
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:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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"
)
# ===== Export and Reporting Endpoints =====
@router.post("/tenants/{tenant_id}/reports/generate", response_model=ExportDataResponse)
async def generate_performance_report(
report_request: PerformanceReportRequest,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Generate a performance report"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
# 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("/tenants/{tenant_id}/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_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Export performance data"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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 Endpoints =====
@router.get("/tenants/{tenant_id}/config")
async def get_performance_config(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep)
):
"""Get performance tracking configuration"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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("/tenants/{tenant_id}/health")
async def get_performance_health(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep)
):
"""Get performance service health status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant data"
)
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"
)