mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Move the scenario and installed_measure tables into
infrastructure/postgres/modelling/ as full-parity SQLModel definitions
(ScenarioModel, InstalledMeasureModel + MeasureType), completing the cluster
consolidation. backend/app/db/models/recommendations.py is now a pure
re-export shim.
ScenarioModel.goal is the PortfolioGoal enum (legacy planning branches on it),
sourced from domain/modelling/portfolio_goal.py; the repo's to_domain maps it to
its value string, so domain Scenario.goal is now the value ("Increasing EPC")
consistent with the orchestrator's check — fixing the latent name-vs-value
inconsistency the old str column masked (the scenario repo test stored the enum
*name*). Parity columns are nullable (mirror convention; live NOT-NULLs owned by
Drizzle).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
4.1 KiB
Python
92 lines
4.1 KiB
Python
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,
|
|
)
|