""" Enhanced Forecast API Endpoints with Repository Pattern Updated to use repository pattern with dependency injection and improved error handling """ import structlog from fastapi import APIRouter, Depends, HTTPException, status, Query, Path, Request from typing import List, Optional from datetime import date, datetime import uuid from app.services.forecasting_service import EnhancedForecastingService from app.schemas.forecasts import ( ForecastRequest, ForecastResponse, BatchForecastRequest, BatchForecastResponse, AlertResponse ) from shared.auth.decorators import ( get_current_user_dep, get_current_tenant_id_dep, require_admin_role ) from shared.database.base import create_database_manager from shared.monitoring.decorators import track_execution_time from shared.monitoring.metrics import get_metrics_collector from app.core.config import settings logger = structlog.get_logger() router = APIRouter(tags=["enhanced-forecasts"]) def get_enhanced_forecasting_service(): """Dependency injection for EnhancedForecastingService""" database_manager = create_database_manager(settings.DATABASE_URL, "forecasting-service") return EnhancedForecastingService(database_manager) @router.post("/tenants/{tenant_id}/forecasts/single", response_model=ForecastResponse) @track_execution_time("enhanced_single_forecast_duration_seconds", "forecasting-service") async def create_enhanced_single_forecast( request: ForecastRequest, tenant_id: str = Path(..., description="Tenant ID"), request_obj: Request = None, current_tenant: str = Depends(get_current_tenant_id_dep), enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) ): """Generate a single product forecast using enhanced repository pattern""" metrics = get_metrics_collector(request_obj) try: # Enhanced tenant validation if tenant_id != current_tenant: if metrics: metrics.increment_counter("enhanced_forecast_access_denied_total") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to tenant resources" ) logger.info("Generating enhanced single forecast", tenant_id=tenant_id, product_name=request.product_name, forecast_date=request.forecast_date.isoformat()) # Record metrics if metrics: metrics.increment_counter("enhanced_single_forecasts_total") # Generate forecast using enhanced service forecast = await enhanced_forecasting_service.generate_forecast( tenant_id=tenant_id, request=request ) if metrics: metrics.increment_counter("enhanced_single_forecasts_success_total") logger.info("Enhanced single forecast generated successfully", tenant_id=tenant_id, forecast_id=forecast.id) return forecast except ValueError as e: if metrics: metrics.increment_counter("enhanced_forecast_validation_errors_total") logger.error("Enhanced forecast validation error", error=str(e), tenant_id=tenant_id) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: if metrics: metrics.increment_counter("enhanced_single_forecasts_errors_total") logger.error("Enhanced single forecast generation failed", error=str(e), tenant_id=tenant_id) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Enhanced forecast generation failed" ) @router.post("/tenants/{tenant_id}/forecasts/batch", response_model=BatchForecastResponse) @track_execution_time("enhanced_batch_forecast_duration_seconds", "forecasting-service") async def create_enhanced_batch_forecast( request: BatchForecastRequest, tenant_id: str = Path(..., description="Tenant ID"), request_obj: Request = None, current_tenant: str = Depends(get_current_tenant_id_dep), enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) ): """Generate batch forecasts using enhanced repository pattern""" metrics = get_metrics_collector(request_obj) try: # Enhanced tenant validation if tenant_id != current_tenant: if metrics: metrics.increment_counter("enhanced_batch_forecast_access_denied_total") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to tenant resources" ) logger.info("Generating enhanced batch forecasts", tenant_id=tenant_id, products_count=len(request.products), forecast_dates_count=len(request.forecast_dates)) # Record metrics if metrics: metrics.increment_counter("enhanced_batch_forecasts_total") metrics.histogram("enhanced_batch_forecast_products_count", len(request.products)) # Generate batch forecasts using enhanced service batch_result = await enhanced_forecasting_service.generate_batch_forecasts( tenant_id=tenant_id, request=request ) if metrics: metrics.increment_counter("enhanced_batch_forecasts_success_total") logger.info("Enhanced batch forecasts generated successfully", tenant_id=tenant_id, batch_id=batch_result.get("batch_id"), forecasts_generated=len(batch_result.get("forecasts", []))) return BatchForecastResponse(**batch_result) except ValueError as e: if metrics: metrics.increment_counter("enhanced_batch_forecast_validation_errors_total") logger.error("Enhanced batch forecast validation error", error=str(e), tenant_id=tenant_id) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: if metrics: metrics.increment_counter("enhanced_batch_forecasts_errors_total") logger.error("Enhanced batch forecast generation failed", error=str(e), tenant_id=tenant_id) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Enhanced batch forecast generation failed" ) @router.get("/tenants/{tenant_id}/forecasts") @track_execution_time("enhanced_get_forecasts_duration_seconds", "forecasting-service") async def get_enhanced_tenant_forecasts( tenant_id: str = Path(..., description="Tenant ID"), product_name: Optional[str] = Query(None, description="Filter by product name"), start_date: Optional[date] = Query(None, description="Start date filter"), end_date: Optional[date] = Query(None, description="End date filter"), skip: int = Query(0, description="Number of records to skip"), limit: int = Query(100, description="Number of records to return"), request_obj: Request = None, current_tenant: str = Depends(get_current_tenant_id_dep), enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) ): """Get tenant forecasts with enhanced filtering using repository pattern""" metrics = get_metrics_collector(request_obj) try: # Enhanced tenant validation if tenant_id != current_tenant: if metrics: metrics.increment_counter("enhanced_get_forecasts_access_denied_total") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to tenant resources" ) # Record metrics if metrics: metrics.increment_counter("enhanced_get_forecasts_total") # Get forecasts using enhanced service forecasts = await enhanced_forecasting_service.get_tenant_forecasts( tenant_id=tenant_id, product_name=product_name, start_date=start_date, end_date=end_date, skip=skip, limit=limit ) if metrics: metrics.increment_counter("enhanced_get_forecasts_success_total") return { "tenant_id": tenant_id, "forecasts": forecasts, "total_returned": len(forecasts), "filters": { "product_name": product_name, "start_date": start_date.isoformat() if start_date else None, "end_date": end_date.isoformat() if end_date else None }, "pagination": { "skip": skip, "limit": limit }, "enhanced_features": True, "repository_integration": True } except Exception as e: if metrics: metrics.increment_counter("enhanced_get_forecasts_errors_total") logger.error("Failed to get enhanced tenant forecasts", tenant_id=tenant_id, error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get tenant forecasts" ) @router.get("/tenants/{tenant_id}/forecasts/{forecast_id}") @track_execution_time("enhanced_get_forecast_duration_seconds", "forecasting-service") async def get_enhanced_forecast_by_id( tenant_id: str = Path(..., description="Tenant ID"), forecast_id: str = Path(..., description="Forecast ID"), request_obj: Request = None, current_tenant: str = Depends(get_current_tenant_id_dep), enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) ): """Get specific forecast by ID using enhanced repository pattern""" metrics = get_metrics_collector(request_obj) try: # Enhanced tenant validation if tenant_id != current_tenant: if metrics: metrics.increment_counter("enhanced_get_forecast_access_denied_total") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to tenant resources" ) # Record metrics if metrics: metrics.increment_counter("enhanced_get_forecast_by_id_total") # Get forecast using enhanced service forecast = await enhanced_forecasting_service.get_forecast_by_id(forecast_id) if not forecast: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Forecast not found" ) if metrics: metrics.increment_counter("enhanced_get_forecast_by_id_success_total") return { **forecast, "enhanced_features": True, "repository_integration": True } except HTTPException: raise except Exception as e: if metrics: metrics.increment_counter("enhanced_get_forecast_by_id_errors_total") logger.error("Failed to get enhanced forecast by ID", forecast_id=forecast_id, error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get forecast" ) @router.delete("/tenants/{tenant_id}/forecasts/{forecast_id}") @track_execution_time("enhanced_delete_forecast_duration_seconds", "forecasting-service") async def delete_enhanced_forecast( tenant_id: str = Path(..., description="Tenant ID"), forecast_id: str = Path(..., description="Forecast ID"), request_obj: Request = None, current_tenant: str = Depends(get_current_tenant_id_dep), enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) ): """Delete forecast using enhanced repository pattern""" metrics = get_metrics_collector(request_obj) try: # Enhanced tenant validation if tenant_id != current_tenant: if metrics: metrics.increment_counter("enhanced_delete_forecast_access_denied_total") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to tenant resources" ) # Record metrics if metrics: metrics.increment_counter("enhanced_delete_forecast_total") # Delete forecast using enhanced service deleted = await enhanced_forecasting_service.delete_forecast(forecast_id) if not deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Forecast not found" ) if metrics: metrics.increment_counter("enhanced_delete_forecast_success_total") logger.info("Enhanced forecast deleted successfully", forecast_id=forecast_id, tenant_id=tenant_id) return { "message": "Forecast deleted successfully", "forecast_id": forecast_id, "enhanced_features": True, "repository_integration": True } except HTTPException: raise except Exception as e: if metrics: metrics.increment_counter("enhanced_delete_forecast_errors_total") logger.error("Failed to delete enhanced forecast", forecast_id=forecast_id, error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete forecast" ) @router.get("/tenants/{tenant_id}/forecasts/alerts") @track_execution_time("enhanced_get_alerts_duration_seconds", "forecasting-service") async def get_enhanced_forecast_alerts( tenant_id: str = Path(..., description="Tenant ID"), active_only: bool = Query(True, description="Return only active alerts"), skip: int = Query(0, description="Number of records to skip"), limit: int = Query(50, description="Number of records to return"), request_obj: Request = None, current_tenant: str = Depends(get_current_tenant_id_dep), enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) ): """Get forecast alerts using enhanced repository pattern""" metrics = get_metrics_collector(request_obj) try: # Enhanced tenant validation if tenant_id != current_tenant: if metrics: metrics.increment_counter("enhanced_get_alerts_access_denied_total") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to tenant resources" ) # Record metrics if metrics: metrics.increment_counter("enhanced_get_alerts_total") # Get alerts using enhanced service alerts = await enhanced_forecasting_service.get_tenant_alerts( tenant_id=tenant_id, active_only=active_only, skip=skip, limit=limit ) if metrics: metrics.increment_counter("enhanced_get_alerts_success_total") return { "tenant_id": tenant_id, "alerts": alerts, "total_returned": len(alerts), "active_only": active_only, "pagination": { "skip": skip, "limit": limit }, "enhanced_features": True, "repository_integration": True } except Exception as e: if metrics: metrics.increment_counter("enhanced_get_alerts_errors_total") logger.error("Failed to get enhanced forecast alerts", tenant_id=tenant_id, error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get forecast alerts" ) @router.get("/tenants/{tenant_id}/forecasts/statistics") @track_execution_time("enhanced_forecast_statistics_duration_seconds", "forecasting-service") async def get_enhanced_forecast_statistics( tenant_id: str = Path(..., description="Tenant ID"), request_obj: Request = None, current_tenant: str = Depends(get_current_tenant_id_dep), enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) ): """Get comprehensive forecast statistics using enhanced repository pattern""" metrics = get_metrics_collector(request_obj) try: # Enhanced tenant validation if tenant_id != current_tenant: if metrics: metrics.increment_counter("enhanced_forecast_statistics_access_denied_total") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to tenant resources" ) # Record metrics if metrics: metrics.increment_counter("enhanced_forecast_statistics_total") # Get statistics using enhanced service statistics = await enhanced_forecasting_service.get_tenant_forecast_statistics(tenant_id) if statistics.get("error"): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=statistics["error"] ) if metrics: metrics.increment_counter("enhanced_forecast_statistics_success_total") return { **statistics, "enhanced_features": True, "repository_integration": True } except HTTPException: raise except Exception as e: if metrics: metrics.increment_counter("enhanced_forecast_statistics_errors_total") logger.error("Failed to get enhanced forecast statistics", tenant_id=tenant_id, error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get forecast statistics" ) @router.get("/health") async def enhanced_health_check(): """Enhanced health check endpoint for the forecasting service""" return { "status": "healthy", "service": "enhanced-forecasting-service", "version": "2.0.0", "features": [ "repository-pattern", "dependency-injection", "enhanced-error-handling", "metrics-tracking", "transactional-operations", "batch-processing" ], "timestamp": datetime.now().isoformat() }