148 lines
4.8 KiB
Python
148 lines
4.8 KiB
Python
|
|
# ================================================================
|
||
|
|
# services/forecasting/app/jobs/daily_validation.py
|
||
|
|
# ================================================================
|
||
|
|
"""
|
||
|
|
Daily Validation Job
|
||
|
|
|
||
|
|
Scheduled job to validate previous day's forecasts against actual sales.
|
||
|
|
This job is called by the orchestrator as part of the daily workflow.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Dict, Any, Optional
|
||
|
|
from datetime import datetime, timedelta, timezone
|
||
|
|
import structlog
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
from app.services.validation_service import ValidationService
|
||
|
|
from app.core.database import database_manager
|
||
|
|
|
||
|
|
logger = structlog.get_logger()
|
||
|
|
|
||
|
|
|
||
|
|
async def daily_validation_job(
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
orchestration_run_id: Optional[uuid.UUID] = None
|
||
|
|
) -> Dict[str, Any]:
|
||
|
|
"""
|
||
|
|
Validate yesterday's forecasts against actual sales
|
||
|
|
|
||
|
|
This function is designed to be called by the orchestrator as part of
|
||
|
|
the daily workflow (Step 5: validate_previous_forecasts).
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tenant_id: Tenant identifier
|
||
|
|
orchestration_run_id: Optional orchestration run ID for tracking
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary with validation results
|
||
|
|
"""
|
||
|
|
async with database_manager.get_session() as db:
|
||
|
|
try:
|
||
|
|
logger.info(
|
||
|
|
"Starting daily validation job",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
orchestration_run_id=orchestration_run_id
|
||
|
|
)
|
||
|
|
|
||
|
|
validation_service = ValidationService(db)
|
||
|
|
|
||
|
|
# Validate yesterday's forecasts
|
||
|
|
result = await validation_service.validate_yesterday(
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
orchestration_run_id=orchestration_run_id,
|
||
|
|
triggered_by="orchestrator"
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Daily validation job completed",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
validation_run_id=result.get("validation_run_id"),
|
||
|
|
forecasts_evaluated=result.get("forecasts_evaluated"),
|
||
|
|
forecasts_with_actuals=result.get("forecasts_with_actuals"),
|
||
|
|
overall_mape=result.get("overall_metrics", {}).get("mape")
|
||
|
|
)
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Daily validation job failed",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
orchestration_run_id=orchestration_run_id,
|
||
|
|
error=str(e),
|
||
|
|
error_type=type(e).__name__
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"status": "failed",
|
||
|
|
"error": str(e),
|
||
|
|
"tenant_id": str(tenant_id),
|
||
|
|
"orchestration_run_id": str(orchestration_run_id) if orchestration_run_id else None
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def validate_date_range_job(
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
start_date: datetime,
|
||
|
|
end_date: datetime,
|
||
|
|
orchestration_run_id: Optional[uuid.UUID] = None
|
||
|
|
) -> Dict[str, Any]:
|
||
|
|
"""
|
||
|
|
Validate forecasts for a specific date range
|
||
|
|
|
||
|
|
Useful for backfilling validation metrics when historical data is uploaded.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tenant_id: Tenant identifier
|
||
|
|
start_date: Start of validation period
|
||
|
|
end_date: End of validation period
|
||
|
|
orchestration_run_id: Optional orchestration run ID for tracking
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary with validation results
|
||
|
|
"""
|
||
|
|
async with database_manager.get_session() as db:
|
||
|
|
try:
|
||
|
|
logger.info(
|
||
|
|
"Starting date range validation job",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
start_date=start_date.isoformat(),
|
||
|
|
end_date=end_date.isoformat(),
|
||
|
|
orchestration_run_id=orchestration_run_id
|
||
|
|
)
|
||
|
|
|
||
|
|
validation_service = ValidationService(db)
|
||
|
|
|
||
|
|
result = await validation_service.validate_date_range(
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
start_date=start_date,
|
||
|
|
end_date=end_date,
|
||
|
|
orchestration_run_id=orchestration_run_id,
|
||
|
|
triggered_by="scheduled"
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Date range validation job completed",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
validation_run_id=result.get("validation_run_id"),
|
||
|
|
forecasts_evaluated=result.get("forecasts_evaluated"),
|
||
|
|
forecasts_with_actuals=result.get("forecasts_with_actuals")
|
||
|
|
)
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Date range validation job failed",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
start_date=start_date.isoformat(),
|
||
|
|
end_date=end_date.isoformat(),
|
||
|
|
error=str(e),
|
||
|
|
error_type=type(e).__name__
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"status": "failed",
|
||
|
|
"error": str(e),
|
||
|
|
"tenant_id": str(tenant_id),
|
||
|
|
"orchestration_run_id": str(orchestration_run_id) if orchestration_run_id else None
|
||
|
|
}
|