# ================================================================ # services/forecasting/app/models/validation_run.py # ================================================================ """ Validation run models for tracking forecast validation executions """ from sqlalchemy import Column, String, Integer, Float, DateTime, Text, JSON, Index from sqlalchemy.dialects.postgresql import UUID from datetime import datetime, timezone import uuid from shared.database.base import Base class ValidationRun(Base): """Track forecast validation execution runs""" __tablename__ = "validation_runs" __table_args__ = ( Index('ix_validation_runs_tenant_created', 'tenant_id', 'started_at'), Index('ix_validation_runs_status', 'status', 'started_at'), Index('ix_validation_runs_orchestration', 'orchestration_run_id'), ) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Link to orchestration run (if triggered by orchestrator) orchestration_run_id = Column(UUID(as_uuid=True), nullable=True) # Validation period validation_start_date = Column(DateTime(timezone=True), nullable=False) validation_end_date = Column(DateTime(timezone=True), nullable=False) # Execution metadata started_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) completed_at = Column(DateTime(timezone=True), nullable=True) duration_seconds = Column(Float, nullable=True) # Status and results status = Column(String(50), default="pending") # pending, running, completed, failed # Validation statistics total_forecasts_evaluated = Column(Integer, default=0) forecasts_with_actuals = Column(Integer, default=0) forecasts_without_actuals = Column(Integer, default=0) # Accuracy metrics summary (across all validated forecasts) overall_mae = Column(Float, nullable=True) overall_mape = Column(Float, nullable=True) overall_rmse = Column(Float, nullable=True) overall_r2_score = Column(Float, nullable=True) overall_accuracy_percentage = Column(Float, nullable=True) # Additional statistics total_predicted_demand = Column(Float, default=0.0) total_actual_demand = Column(Float, default=0.0) # Breakdown by product/location (JSON) metrics_by_product = Column(JSON, nullable=True) # {product_id: {mae, mape, ...}} metrics_by_location = Column(JSON, nullable=True) # {location: {mae, mape, ...}} # Performance metrics created count metrics_records_created = Column(Integer, default=0) # Error tracking error_message = Column(Text, nullable=True) error_details = Column(JSON, nullable=True) # Execution context triggered_by = Column(String(100), default="manual") # manual, orchestrator, scheduled execution_mode = Column(String(50), default="batch") # batch, single_day, real_time def __repr__(self): return ( f"" ) def to_dict(self): """Convert to dictionary for API responses""" return { 'id': str(self.id), 'tenant_id': str(self.tenant_id), 'orchestration_run_id': str(self.orchestration_run_id) if self.orchestration_run_id else None, 'validation_start_date': self.validation_start_date.isoformat() if self.validation_start_date else None, 'validation_end_date': self.validation_end_date.isoformat() if self.validation_end_date else None, 'started_at': self.started_at.isoformat() if self.started_at else None, 'completed_at': self.completed_at.isoformat() if self.completed_at else None, 'duration_seconds': self.duration_seconds, 'status': self.status, 'total_forecasts_evaluated': self.total_forecasts_evaluated, 'forecasts_with_actuals': self.forecasts_with_actuals, 'forecasts_without_actuals': self.forecasts_without_actuals, 'overall_mae': self.overall_mae, 'overall_mape': self.overall_mape, 'overall_rmse': self.overall_rmse, 'overall_r2_score': self.overall_r2_score, 'overall_accuracy_percentage': self.overall_accuracy_percentage, 'total_predicted_demand': self.total_predicted_demand, 'total_actual_demand': self.total_actual_demand, 'metrics_by_product': self.metrics_by_product, 'metrics_by_location': self.metrics_by_location, 'metrics_records_created': self.metrics_records_created, 'error_message': self.error_message, 'error_details': self.error_details, 'triggered_by': self.triggered_by, 'execution_mode': self.execution_mode, }