# ================================================================ # 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 }