Improve backend

This commit is contained in:
Urtzi Alfaro
2025-11-18 07:17:17 +01:00
parent d36f2ab9af
commit 5c45164c8e
61 changed files with 9846 additions and 495 deletions

View File

@@ -14,6 +14,8 @@ AuditLog = create_audit_log_model(Base)
# Import all models to register them with the Base metadata
from .forecasts import Forecast, PredictionBatch
from .predictions import ModelPerformanceMetric, PredictionCache
from .validation_run import ValidationRun
from .sales_data_update import SalesDataUpdate
# List all models for easier access
__all__ = [
@@ -21,5 +23,7 @@ __all__ = [
"PredictionBatch",
"ModelPerformanceMetric",
"PredictionCache",
"ValidationRun",
"SalesDataUpdate",
"AuditLog",
]

View File

@@ -0,0 +1,78 @@
# ================================================================
# services/forecasting/app/models/sales_data_update.py
# ================================================================
"""
Sales Data Update Tracking Model
Tracks when sales data is added or updated for past dates,
enabling automated historical validation backfill.
"""
from sqlalchemy import Column, String, Integer, DateTime, Boolean, Index, Date
from sqlalchemy.dialects.postgresql import UUID
from datetime import datetime, timezone
import uuid
from shared.database.base import Base
class SalesDataUpdate(Base):
"""Track sales data updates for historical validation"""
__tablename__ = "sales_data_updates"
__table_args__ = (
Index('ix_sales_updates_tenant_status', 'tenant_id', 'validation_status', 'created_at'),
Index('ix_sales_updates_date_range', 'tenant_id', 'update_date_start', 'update_date_end'),
Index('ix_sales_updates_validation_status', 'validation_status'),
)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Date range of sales data that was added/updated
update_date_start = Column(Date, nullable=False, index=True)
update_date_end = Column(Date, nullable=False, index=True)
# Update metadata
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
update_source = Column(String(100), nullable=True) # import, manual, pos_sync
records_affected = Column(Integer, default=0)
# Validation tracking
validation_status = Column(String(50), default="pending") # pending, processing, completed, failed
validation_run_id = Column(UUID(as_uuid=True), nullable=True)
validated_at = Column(DateTime(timezone=True), nullable=True)
validation_error = Column(String(500), nullable=True)
# Determines if this update should trigger validation
requires_validation = Column(Boolean, default=True)
# Additional context
import_job_id = Column(String(255), nullable=True) # Link to sales import job if applicable
notes = Column(String(500), nullable=True)
def __repr__(self):
return (
f"<SalesDataUpdate(id={self.id}, tenant_id={self.tenant_id}, "
f"date_range={self.update_date_start} to {self.update_date_end}, "
f"status={self.validation_status})>"
)
def to_dict(self):
"""Convert to dictionary for API responses"""
return {
'id': str(self.id),
'tenant_id': str(self.tenant_id),
'update_date_start': self.update_date_start.isoformat() if self.update_date_start else None,
'update_date_end': self.update_date_end.isoformat() if self.update_date_end else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'update_source': self.update_source,
'records_affected': self.records_affected,
'validation_status': self.validation_status,
'validation_run_id': str(self.validation_run_id) if self.validation_run_id else None,
'validated_at': self.validated_at.isoformat() if self.validated_at else None,
'validation_error': self.validation_error,
'requires_validation': self.requires_validation,
'import_job_id': self.import_job_id,
'notes': self.notes
}

View File

@@ -0,0 +1,110 @@
# ================================================================
# 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"<ValidationRun(id={self.id}, tenant_id={self.tenant_id}, "
f"status={self.status}, forecasts_evaluated={self.total_forecasts_evaluated})>"
)
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,
}