503 lines
19 KiB
Python
503 lines
19 KiB
Python
"""
|
|
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,
|
|
inventory_product_id=request.inventory_product_id,
|
|
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.inventory_product_ids),
|
|
forecast_dates_count=request.forecast_days)
|
|
|
|
# Record metrics
|
|
if metrics:
|
|
metrics.increment_counter("enhanced_batch_forecasts_total")
|
|
metrics.histogram("enhanced_batch_forecast_products_count", len(request.inventory_product_ids))
|
|
|
|
# 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"),
|
|
inventory_product_id: Optional[str] = Query(None, description="Filter by inventory product ID"),
|
|
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,
|
|
inventory_product_id=inventory_product_id,
|
|
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": {
|
|
"inventory_product_id": inventory_product_id,
|
|
"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/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/{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/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()
|
|
} |