# ================================================================ # services/forecasting/app/schemas/forecasts.py # ================================================================ """ Forecast schemas for request/response validation """ from pydantic import BaseModel, Field, validator from datetime import datetime, date from typing import Optional, List, Dict, Any from enum import Enum from uuid import UUID class BusinessType(str, Enum): INDIVIDUAL = "individual" CENTRAL_WORKSHOP = "central_workshop" class ForecastRequest(BaseModel): """Request schema for generating forecasts""" inventory_product_id: str = Field(..., description="Inventory product UUID reference") # product_name: str = Field(..., description="Product name") # DEPRECATED - use inventory_product_id forecast_date: date = Field(..., description="Starting date for forecast") forecast_days: int = Field(1, ge=1, le=30, description="Number of days to forecast") location: str = Field(..., description="Location identifier") # Optional parameters - internally handled confidence_level: float = Field(0.8, ge=0.5, le=0.95, description="Confidence level") @validator('inventory_product_id') def validate_inventory_product_id(cls, v): """Validate that inventory_product_id is a valid UUID""" try: UUID(v) except (ValueError, AttributeError): raise ValueError(f"inventory_product_id must be a valid UUID, got: {v}") return v @validator('forecast_date') def validate_forecast_date(cls, v): if v < date.today(): raise ValueError("Forecast date cannot be in the past") return v class BatchForecastRequest(BaseModel): """Request schema for batch forecasting""" tenant_id: Optional[str] = None # Optional, can be from path parameter batch_name: str = Field(..., description="Batch name for tracking") inventory_product_ids: List[str] = Field(..., description="List of inventory product IDs") forecast_days: int = Field(7, ge=1, le=30, description="Number of days to forecast") @validator('tenant_id') def validate_tenant_id(cls, v): """Validate that tenant_id is a valid UUID if provided""" if v is not None: try: UUID(v) except (ValueError, AttributeError): raise ValueError(f"tenant_id must be a valid UUID, got: {v}") return v @validator('inventory_product_ids') def validate_inventory_product_ids(cls, v): """Validate that all inventory_product_ids are valid UUIDs""" for product_id in v: try: UUID(product_id) except (ValueError, AttributeError): raise ValueError(f"All inventory_product_ids must be valid UUIDs, got invalid: {product_id}") return v class ForecastResponse(BaseModel): """Response schema for forecast results""" id: str tenant_id: str inventory_product_id: str # Reference to inventory service # product_name: str # Can be fetched from inventory service if needed for display location: str forecast_date: datetime # Predictions predicted_demand: float confidence_lower: float confidence_upper: float confidence_level: float # Model info model_id: str model_version: str algorithm: str # Context business_type: str is_holiday: bool is_weekend: bool day_of_week: int # External factors weather_temperature: Optional[float] weather_precipitation: Optional[float] weather_description: Optional[str] traffic_volume: Optional[int] # Metadata created_at: datetime processing_time_ms: Optional[int] features_used: Optional[Dict[str, Any]] class BatchForecastResponse(BaseModel): """Response schema for batch forecast requests""" id: str tenant_id: str batch_name: str status: str total_products: int completed_products: int failed_products: int # Timing requested_at: datetime completed_at: Optional[datetime] processing_time_ms: Optional[int] # Results forecasts: Optional[List[ForecastResponse]] error_message: Optional[str] class MultiDayForecastResponse(BaseModel): """Response schema for multi-day forecast results""" tenant_id: str = Field(..., description="Tenant ID") inventory_product_id: str = Field(..., description="Inventory product ID") forecast_start_date: date = Field(..., description="Start date of forecast period") forecast_days: int = Field(..., description="Number of forecasted days") forecasts: List[ForecastResponse] = Field(..., description="Daily forecasts") total_predicted_demand: float = Field(..., description="Total demand across all days") average_confidence_level: float = Field(..., description="Average confidence across all days") processing_time_ms: int = Field(..., description="Total processing time") # ================================================================ # SCENARIO SIMULATION SCHEMAS - PROFESSIONAL/ENTERPRISE ONLY # ================================================================ class ScenarioType(str, Enum): """Types of scenarios available for simulation""" WEATHER = "weather" # Weather impact (heatwave, cold snap, rain, etc.) COMPETITION = "competition" # New competitor opening nearby EVENT = "event" # Local event (festival, sports, concert, etc.) PRICING = "pricing" # Price changes PROMOTION = "promotion" # Promotional campaigns HOLIDAY = "holiday" # Holiday periods SUPPLY_DISRUPTION = "supply_disruption" # Supply chain issues CUSTOM = "custom" # Custom user-defined scenario class WeatherScenario(BaseModel): """Weather scenario parameters""" temperature_change: Optional[float] = Field(None, ge=-30, le=30, description="Temperature change in °C") precipitation_change: Optional[float] = Field(None, ge=0, le=100, description="Precipitation change in mm") weather_type: Optional[str] = Field(None, description="Weather type (heatwave, cold_snap, rainy, etc.)") class CompetitionScenario(BaseModel): """Competition scenario parameters""" new_competitors: int = Field(1, ge=1, le=10, description="Number of new competitors") distance_km: float = Field(0.5, ge=0.1, le=10, description="Distance from location in km") estimated_market_share_loss: float = Field(0.1, ge=0, le=0.5, description="Estimated market share loss (0-50%)") class EventScenario(BaseModel): """Event scenario parameters""" event_type: str = Field(..., description="Type of event (festival, sports, concert, etc.)") expected_attendance: int = Field(..., ge=0, description="Expected attendance") distance_km: float = Field(0.5, ge=0, le=50, description="Distance from location in km") duration_days: int = Field(1, ge=1, le=30, description="Duration in days") class PricingScenario(BaseModel): """Pricing scenario parameters""" price_change_percent: float = Field(..., ge=-50, le=100, description="Price change percentage") affected_products: Optional[List[str]] = Field(None, description="List of affected product IDs") class PromotionScenario(BaseModel): """Promotion scenario parameters""" discount_percent: float = Field(..., ge=0, le=75, description="Discount percentage") promotion_type: str = Field(..., description="Type of promotion (bogo, discount, bundle, etc.)") expected_traffic_increase: float = Field(0.2, ge=0, le=2, description="Expected traffic increase (0-200%)") class ScenarioSimulationRequest(BaseModel): """Request schema for scenario simulation - PROFESSIONAL/ENTERPRISE ONLY""" scenario_name: str = Field(..., min_length=3, max_length=200, description="Name for this scenario") scenario_type: ScenarioType = Field(..., description="Type of scenario to simulate") inventory_product_ids: List[str] = Field(..., min_items=1, description="Products to simulate") start_date: date = Field(..., description="Simulation start date") duration_days: int = Field(7, ge=1, le=30, description="Simulation duration in days") # Scenario-specific parameters (one should be provided based on scenario_type) weather_params: Optional[WeatherScenario] = None competition_params: Optional[CompetitionScenario] = None event_params: Optional[EventScenario] = None pricing_params: Optional[PricingScenario] = None promotion_params: Optional[PromotionScenario] = None # Custom scenario parameters custom_multipliers: Optional[Dict[str, float]] = Field( None, description="Custom multipliers for baseline forecast (e.g., {'demand': 1.2, 'traffic': 0.8})" ) # Comparison settings include_baseline: bool = Field(True, description="Include baseline forecast for comparison") @validator('start_date') def validate_start_date(cls, v): if v < date.today(): raise ValueError("Simulation start date cannot be in the past") return v class ScenarioImpact(BaseModel): """Impact of scenario on a specific product""" inventory_product_id: str baseline_demand: float simulated_demand: float demand_change_percent: float confidence_range: tuple[float, float] impact_factors: Dict[str, Any] # Breakdown of what drove the change class ScenarioSimulationResponse(BaseModel): """Response schema for scenario simulation""" id: str = Field(..., description="Simulation ID") tenant_id: str scenario_name: str scenario_type: ScenarioType # Simulation parameters start_date: date end_date: date duration_days: int # Results baseline_forecasts: Optional[List[ForecastResponse]] = Field( None, description="Baseline forecasts (if requested)" ) scenario_forecasts: List[ForecastResponse] = Field(..., description="Forecasts with scenario applied") # Impact summary total_baseline_demand: float total_scenario_demand: float overall_impact_percent: float product_impacts: List[ScenarioImpact] # Insights and recommendations insights: List[str] = Field(..., description="AI-generated insights about the scenario") recommendations: List[str] = Field(..., description="Actionable recommendations") risk_level: str = Field(..., description="Risk level: low, medium, high") # Metadata created_at: datetime processing_time_ms: int class Config: json_schema_extra = { "example": { "id": "scenario_123", "tenant_id": "tenant_456", "scenario_name": "Summer Heatwave Impact", "scenario_type": "weather", "overall_impact_percent": 15.5, "insights": [ "Cold beverages expected to increase by 45%", "Bread products may decrease by 8% due to reduced appetite", "Ice cream demand projected to surge by 120%" ], "recommendations": [ "Increase cold beverage inventory by 40%", "Reduce bread production by 10%", "Stock additional ice cream varieties" ], "risk_level": "medium" } } class ScenarioComparisonRequest(BaseModel): """Request to compare multiple scenarios""" scenario_ids: List[str] = Field(..., min_items=2, max_items=5, description="Scenario IDs to compare") class ScenarioComparisonResponse(BaseModel): """Response comparing multiple scenarios""" scenarios: List[ScenarioSimulationResponse] comparison_matrix: Dict[str, Dict[str, Any]] best_case_scenario_id: str worst_case_scenario_id: str recommended_action: str