Files
bakery-ia/services/forecasting/app/api/forecasts.py

503 lines
19 KiB
Python
Raw Normal View History

2025-07-21 19:48:56 +02:00
"""
2025-08-08 09:08:41 +02:00
Enhanced Forecast API Endpoints with Repository Pattern
Updated to use repository pattern with dependency injection and improved error handling
2025-07-21 19:48:56 +02:00
"""
import structlog
2025-08-08 09:08:41 +02:00
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path, Request
2025-07-21 19:48:56 +02:00
from typing import List, Optional
2025-08-02 09:41:50 +02:00
from datetime import date, datetime
import uuid
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
from app.services.forecasting_service import EnhancedForecastingService
2025-07-21 19:48:56 +02:00
from app.schemas.forecasts import (
ForecastRequest, ForecastResponse, BatchForecastRequest,
BatchForecastResponse, AlertResponse
)
2025-08-08 09:08:41 +02:00
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
2025-07-21 19:48:56 +02:00
logger = structlog.get_logger()
2025-08-08 09:08:41 +02:00
router = APIRouter(tags=["enhanced-forecasts"])
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
def get_enhanced_forecasting_service():
"""Dependency injection for EnhancedForecastingService"""
database_manager = create_database_manager(settings.DATABASE_URL, "forecasting-service")
return EnhancedForecastingService(database_manager)
2025-07-21 19:48:56 +02:00
2025-07-29 13:02:42 +02:00
@router.post("/tenants/{tenant_id}/forecasts/single", response_model=ForecastResponse)
2025-08-08 09:08:41 +02:00
@track_execution_time("enhanced_single_forecast_duration_seconds", "forecasting-service")
async def create_enhanced_single_forecast(
2025-07-21 19:48:56 +02:00
request: ForecastRequest,
2025-08-08 09:08:41 +02:00
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)
2025-07-21 19:48:56 +02:00
):
2025-08-08 09:08:41 +02:00
"""Generate a single product forecast using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
2025-07-21 19:48:56 +02:00
try:
2025-08-08 09:08:41 +02:00
# 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,
2025-08-14 16:47:34 +02:00
inventory_product_id=request.inventory_product_id,
2025-08-08 09:08:41 +02:00
forecast_date=request.forecast_date.isoformat())
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
# Record metrics
if metrics:
metrics.increment_counter("enhanced_single_forecasts_total")
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
# Generate forecast using enhanced service
forecast = await enhanced_forecasting_service.generate_forecast(
2025-07-29 17:50:01 +02:00
tenant_id=tenant_id,
2025-08-08 09:08:41 +02:00
request=request
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
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
2025-07-21 19:48:56 +02:00
except ValueError as e:
2025-08-08 09:08:41 +02:00
if metrics:
metrics.increment_counter("enhanced_forecast_validation_errors_total")
logger.error("Enhanced forecast validation error",
error=str(e),
tenant_id=tenant_id)
2025-07-21 19:48:56 +02:00
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
2025-08-08 09:08:41 +02:00
if metrics:
metrics.increment_counter("enhanced_single_forecasts_errors_total")
logger.error("Enhanced single forecast generation failed",
error=str(e),
tenant_id=tenant_id)
2025-07-21 19:48:56 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
2025-08-08 09:08:41 +02:00
detail="Enhanced forecast generation failed"
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
2025-07-29 13:02:42 +02:00
@router.post("/tenants/{tenant_id}/forecasts/batch", response_model=BatchForecastResponse)
2025-08-08 09:08:41 +02:00
@track_execution_time("enhanced_batch_forecast_duration_seconds", "forecasting-service")
async def create_enhanced_batch_forecast(
2025-07-21 19:48:56 +02:00
request: BatchForecastRequest,
2025-07-29 13:02:42 +02:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-08-08 09:08:41 +02:00
request_obj: Request = None,
current_tenant: str = Depends(get_current_tenant_id_dep),
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
2025-07-21 19:48:56 +02:00
):
2025-08-08 09:08:41 +02:00
"""Generate batch forecasts using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
2025-07-21 19:48:56 +02:00
try:
2025-08-08 09:08:41 +02:00
# Enhanced tenant validation
if tenant_id != current_tenant:
if metrics:
metrics.increment_counter("enhanced_batch_forecast_access_denied_total")
2025-07-21 19:48:56 +02:00
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
2025-08-08 09:08:41 +02:00
detail="Access denied to tenant resources"
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
logger.info("Generating enhanced batch forecasts",
tenant_id=tenant_id,
2025-08-14 16:47:34 +02:00
products_count=len(request.inventory_product_ids),
forecast_dates_count=request.forecast_days)
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
# Record metrics
if metrics:
metrics.increment_counter("enhanced_batch_forecasts_total")
2025-08-14 16:47:34 +02:00
metrics.histogram("enhanced_batch_forecast_products_count", len(request.inventory_product_ids))
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
# Generate batch forecasts using enhanced service
batch_result = await enhanced_forecasting_service.generate_batch_forecasts(
tenant_id=tenant_id,
request=request
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
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)
2025-07-21 19:48:56 +02:00
except ValueError as e:
2025-08-08 09:08:41 +02:00
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)
2025-07-21 19:48:56 +02:00
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
2025-08-08 09:08:41 +02:00
if metrics:
metrics.increment_counter("enhanced_batch_forecasts_errors_total")
logger.error("Enhanced batch forecast generation failed",
error=str(e),
tenant_id=tenant_id)
2025-07-21 19:48:56 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
2025-08-08 09:08:41 +02:00
detail="Enhanced batch forecast generation failed"
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
@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"),
2025-08-14 16:47:34 +02:00
inventory_product_id: Optional[str] = Query(None, description="Filter by inventory product ID"),
2025-08-08 09:08:41 +02:00
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)
2025-07-21 19:48:56 +02:00
):
2025-08-08 09:08:41 +02:00
"""Get tenant forecasts with enhanced filtering using repository pattern"""
metrics = get_metrics_collector(request_obj)
2025-07-21 19:48:56 +02:00
try:
2025-08-08 09:08:41 +02:00
# 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")
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
# Get forecasts using enhanced service
forecasts = await enhanced_forecasting_service.get_tenant_forecasts(
2025-07-21 19:48:56 +02:00
tenant_id=tenant_id,
2025-08-14 16:47:34 +02:00
inventory_product_id=inventory_product_id,
2025-07-21 19:48:56 +02:00
start_date=start_date,
end_date=end_date,
2025-08-08 09:08:41 +02:00
skip=skip,
limit=limit
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
if metrics:
metrics.increment_counter("enhanced_get_forecasts_success_total")
return {
"tenant_id": tenant_id,
"forecasts": forecasts,
"total_returned": len(forecasts),
"filters": {
2025-08-14 16:47:34 +02:00
"inventory_product_id": inventory_product_id,
2025-08-08 09:08:41 +02:00
"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
}
2025-07-21 19:48:56 +02:00
except Exception as e:
2025-08-08 09:08:41 +02:00
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))
2025-07-21 19:48:56 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
2025-08-08 09:08:41 +02:00
detail="Failed to get tenant forecasts"
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
2025-08-15 23:11:53 +02:00
@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"
)
2025-08-08 09:08:41 +02:00
@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(
2025-07-29 13:02:42 +02:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-08-08 09:08:41 +02:00
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)
2025-07-21 19:48:56 +02:00
):
2025-08-08 09:08:41 +02:00
"""Get specific forecast by ID using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
2025-07-21 19:48:56 +02:00
try:
2025-08-08 09:08:41 +02:00
# 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"
)
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
# 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)
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
if not forecast:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Forecast not found"
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
if metrics:
metrics.increment_counter("enhanced_get_forecast_by_id_success_total")
return {
**forecast,
"enhanced_features": True,
"repository_integration": True
}
except HTTPException:
raise
2025-07-21 19:48:56 +02:00
except Exception as e:
2025-08-08 09:08:41 +02:00
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))
2025-07-21 19:48:56 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
2025-08-08 09:08:41 +02:00
detail="Failed to get forecast"
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
@router.delete("/tenants/{tenant_id}/forecasts/{forecast_id}")
@track_execution_time("enhanced_delete_forecast_duration_seconds", "forecasting-service")
async def delete_enhanced_forecast(
2025-07-29 13:02:42 +02:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-08-08 09:08:41 +02:00
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)
2025-07-21 19:48:56 +02:00
):
2025-08-08 09:08:41 +02:00
"""Delete forecast using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
2025-07-21 19:48:56 +02:00
try:
2025-08-08 09:08:41 +02:00
# 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"
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
# 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:
2025-07-21 19:48:56 +02:00
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
2025-08-08 09:08:41 +02:00
detail="Forecast not found"
2025-07-21 19:48:56 +02:00
)
2025-08-08 09:08:41 +02:00
if metrics:
metrics.increment_counter("enhanced_delete_forecast_success_total")
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
logger.info("Enhanced forecast deleted successfully",
forecast_id=forecast_id,
tenant_id=tenant_id)
2025-07-21 19:48:56 +02:00
2025-08-08 09:08:41 +02:00
return {
"message": "Forecast deleted successfully",
"forecast_id": forecast_id,
"enhanced_features": True,
"repository_integration": True
}
2025-07-21 19:48:56 +02:00
except HTTPException:
raise
except Exception as e:
2025-08-08 09:08:41 +02:00
if metrics:
metrics.increment_counter("enhanced_delete_forecast_errors_total")
logger.error("Failed to delete enhanced forecast",
forecast_id=forecast_id,
error=str(e))
2025-07-21 19:48:56 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
2025-08-08 09:08:41 +02:00
detail="Failed to delete forecast"
2025-08-02 09:41:50 +02:00
)
2025-08-02 17:09:53 +02:00
2025-08-08 09:08:41 +02:00
@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)
2025-08-02 17:09:53 +02:00
):
2025-08-08 09:08:41 +02:00
"""Get comprehensive forecast statistics using enhanced repository pattern"""
metrics = get_metrics_collector(request_obj)
2025-08-02 17:09:53 +02:00
try:
2025-08-08 09:08:41 +02:00
# 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"
)
2025-08-02 17:09:53 +02:00
2025-08-08 09:08:41 +02:00
# Record metrics
if metrics:
metrics.increment_counter("enhanced_forecast_statistics_total")
2025-08-02 17:09:53 +02:00
2025-08-08 09:08:41 +02:00
# Get statistics using enhanced service
statistics = await enhanced_forecasting_service.get_tenant_forecast_statistics(tenant_id)
2025-08-02 17:09:53 +02:00
2025-08-08 09:08:41 +02:00
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")
2025-08-02 17:09:53 +02:00
return {
2025-08-08 09:08:41 +02:00
**statistics,
"enhanced_features": True,
"repository_integration": True
2025-08-02 17:09:53 +02:00
}
2025-08-08 09:08:41 +02:00
except HTTPException:
raise
2025-08-02 17:09:53 +02:00
except Exception as e:
2025-08-08 09:08:41 +02:00
if metrics:
metrics.increment_counter("enhanced_forecast_statistics_errors_total")
logger.error("Failed to get enhanced forecast statistics",
tenant_id=tenant_id,
2025-08-02 17:09:53 +02:00
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
2025-08-08 09:08:41 +02:00
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()
}