from __future__ import annotations from datetime import datetime from typing import ClassVar, Optional from sqlalchemy import Column, TIMESTAMP from sqlalchemy import Enum as SAEnum from sqlalchemy.sql import func from sqlmodel import Field, SQLModel from domain.modelling.portfolio_goal import PortfolioGoal from domain.modelling.scenario import Scenario class ScenarioModel(SQLModel, table=True): """The single SQLModel definition of the live ``scenario`` table (ADR-0017 amendment). Full legacy column parity; ``goal`` is the ``PortfolioGoal`` enum (legacy planning branches on it, so it must stay an enum — the stored string is the enum *value*, e.g. ``"Increasing EPC"``). Only ``goal`` / ``goal_value`` are required; everything else is nullable (mirror convention — the live NOT-NULLs are owned by the Drizzle schema), so the Modelling stage can construct the thin slice it uses while the legacy writers still supply the full row. """ __tablename__: ClassVar[str] = "scenario" # pyright: ignore[reportIncompatibleVariableOverride] id: Optional[int] = Field(default=None, primary_key=True) name: Optional[str] = Field(default=None) created_at: Optional[datetime] = Field( default=None, sa_column=Column(TIMESTAMP, nullable=False, server_default=func.now()), ) budget: Optional[float] = Field(default=None) portfolio_id: Optional[int] = Field(default=None) housing_type: Optional[str] = Field(default=None) goal: PortfolioGoal = Field( sa_column=Column( SAEnum( PortfolioGoal, values_callable=lambda cls: [m.value for m in cls], # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType, reportUnknownVariableType] name="goal", ), nullable=False, ) ) goal_value: str trigger_file_path: Optional[str] = Field(default=None) already_installed_file_path: Optional[str] = Field(default=None) patches_file_path: Optional[str] = Field(default=None) non_invasive_recommendations_file_path: Optional[str] = Field(default=None) exclusions: Optional[str] = Field(default=None) multi_plan: bool = False is_default: bool = False # Portfolio-level aggregates stored against the Scenario. cost: Optional[float] = Field(default=None) contingency: Optional[float] = Field(default=None) funding: Optional[float] = Field(default=None) total_work_hours: Optional[float] = Field(default=None) energy_savings: Optional[float] = Field(default=None) co2_equivalent_savings: Optional[float] = Field(default=None) energy_cost_savings: Optional[float] = Field(default=None) epc_breakdown_pre_retrofit: Optional[str] = Field(default=None) epc_breakdown_post_retrofit: Optional[str] = Field(default=None) number_of_properties: Optional[int] = Field(default=None) n_units_to_retrofit: Optional[int] = Field(default=None) co2_per_unit_pre_retrofit: Optional[str] = Field(default=None) co2_per_unit_post_retrofit: Optional[str] = Field(default=None) energy_bill_per_unit_pre_retrofit: Optional[str] = Field(default=None) energy_bill_per_unit_post_retrofit: Optional[str] = Field(default=None) energy_consumption_per_unit_pre_retrofit: Optional[str] = Field(default=None) energy_consumption_per_unit_post_retrofit: Optional[str] = Field(default=None) valuation_improvement_per_unit: Optional[str] = Field(default=None) cost_per_unit: Optional[str] = Field(default=None) cost_per_co2_saved: Optional[str] = Field(default=None) cost_per_sap_point: Optional[str] = Field(default=None) valuation_return_on_investment: Optional[str] = Field(default=None) property_valuation_increase: Optional[float] = Field(default=None) labour_days: Optional[float] = Field(default=None) def to_domain(self) -> Scenario: if self.id is None: raise ValueError("scenario row has no id") return Scenario( id=self.id, goal=self.goal.value, goal_value=self.goal_value, budget=self.budget, is_default=self.is_default, )