mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
refactor(modelling): consolidate scenario + installed_measure into the subpackage
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>
This commit is contained in:
parent
2fbd7147b7
commit
c18968ba3c
8 changed files with 225 additions and 215 deletions
|
|
@ -1,168 +1,41 @@
|
|||
"""Re-export shim + remaining legacy models (ADR-0017 amendment).
|
||||
"""Re-export shim (ADR-0017 amendment).
|
||||
|
||||
`plan`, `recommendation`, `recommendation_materials` and the retiring
|
||||
`plan_recommendations` moved to `infrastructure/postgres/modelling/` as single
|
||||
SQLModel definitions (the `epc_property` pattern). This module re-exports them
|
||||
under their legacy names so the dying `backend/` callers keep working; new code
|
||||
imports from `infrastructure.postgres.modelling` directly. `ScenarioModel` and
|
||||
`InstalledMeasure` are not yet migrated and stay here for now.
|
||||
The Modelling-stage persistence models — `plan`, `recommendation`,
|
||||
`recommendation_materials`, `scenario`, `installed_measure` — moved to
|
||||
`infrastructure/postgres/modelling/` as single SQLModel definitions (the
|
||||
`epc_property` pattern). This module re-exports them under their legacy names so
|
||||
the dying `backend/` callers keep working; new code imports from
|
||||
`infrastructure.postgres.modelling` directly. The `plan_recommendations` m2m is
|
||||
retired — measures link to their Plan via `recommendation.plan_id`.
|
||||
"""
|
||||
|
||||
import enum
|
||||
from typing import Iterable, List, NamedTuple, Optional, Type
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
String,
|
||||
Float,
|
||||
Boolean,
|
||||
TIMESTAMP,
|
||||
ForeignKey,
|
||||
Column,
|
||||
Enum,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
from datetime import datetime
|
||||
|
||||
from backend.app.db.base import Base
|
||||
from backend.app.db.models.portfolio import Portfolio, PortfolioGoal, PropertyModel
|
||||
from typing import NamedTuple
|
||||
|
||||
from infrastructure.postgres.modelling import (
|
||||
InstalledMeasureModel,
|
||||
PlanModel,
|
||||
PlanType,
|
||||
RecommendationMaterialModel,
|
||||
RecommendationModel,
|
||||
ScenarioModel,
|
||||
)
|
||||
|
||||
# Legacy names → the single SQLModel definitions now in
|
||||
# `infrastructure/postgres/modelling/`. The `plan_recommendations` m2m is
|
||||
# retired (ADR-0017 amendment) — measures link to their Plan via
|
||||
# `recommendation.plan_id`.
|
||||
# `infrastructure/postgres/modelling/`.
|
||||
Recommendation = RecommendationModel
|
||||
RecommendationMaterials = RecommendationMaterialModel
|
||||
PlanTypeEnum = PlanType
|
||||
InstalledMeasure = InstalledMeasureModel
|
||||
|
||||
|
||||
def portfolio_goal_values(enum_cls: Type[PortfolioGoal]) -> List[str]:
|
||||
return [e.value for e in enum_cls]
|
||||
|
||||
|
||||
class ScenarioModel(Base):
|
||||
__tablename__ = "scenario"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP, nullable=False, server_default=func.now()
|
||||
)
|
||||
budget: Mapped[Optional[float]] = mapped_column(Float)
|
||||
portfolio_id: Mapped[int] = mapped_column(
|
||||
BigInteger, ForeignKey(Portfolio.id), nullable=False
|
||||
)
|
||||
housing_type: Mapped[str] = mapped_column(String, nullable=False)
|
||||
goal: Mapped[PortfolioGoal] = mapped_column(
|
||||
Enum(PortfolioGoal, values_callable=portfolio_goal_values, name="goal"),
|
||||
nullable=False,
|
||||
)
|
||||
goal_value: Mapped[str] = mapped_column(String, nullable=False)
|
||||
trigger_file_path: Mapped[str] = mapped_column(String, nullable=False)
|
||||
already_installed_file_path: Mapped[Optional[str]] = mapped_column(String)
|
||||
patches_file_path: Mapped[Optional[str]] = mapped_column(String)
|
||||
non_invasive_recommendations_file_path: Mapped[Optional[str]] = mapped_column(
|
||||
String
|
||||
)
|
||||
exclusions: Mapped[Optional[str]] = mapped_column(String)
|
||||
multi_plan: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Add in the fields we need, which were previously sitting at the portfolio level
|
||||
cost: Mapped[Optional[float]] = mapped_column(Float)
|
||||
contingency: Mapped[Optional[float]] = mapped_column(Float)
|
||||
funding: Mapped[Optional[float]] = mapped_column(Float)
|
||||
total_work_hours: Mapped[Optional[float]] = mapped_column(Float)
|
||||
energy_savings: Mapped[Optional[float]] = mapped_column(Float)
|
||||
co2_equivalent_savings: Mapped[Optional[float]] = mapped_column(Float)
|
||||
energy_cost_savings: Mapped[Optional[float]] = mapped_column(Float)
|
||||
epc_breakdown_pre_retrofit: Mapped[Optional[str]] = mapped_column(String)
|
||||
epc_breakdown_post_retrofit: Mapped[Optional[str]] = mapped_column(String)
|
||||
number_of_properties: Mapped[Optional[int]] = mapped_column(BigInteger)
|
||||
n_units_to_retrofit: Mapped[Optional[int]] = mapped_column(BigInteger)
|
||||
co2_per_unit_pre_retrofit: Mapped[Optional[str]] = mapped_column(String)
|
||||
co2_per_unit_post_retrofit: Mapped[Optional[str]] = mapped_column(String)
|
||||
energy_bill_per_unit_pre_retrofit: Mapped[Optional[str]] = mapped_column(String)
|
||||
energy_bill_per_unit_post_retrofit: Mapped[Optional[str]] = mapped_column(String)
|
||||
energy_consumption_per_unit_pre_retrofit: Mapped[Optional[str]] = mapped_column(
|
||||
String
|
||||
)
|
||||
energy_consumption_per_unit_post_retrofit: Mapped[Optional[str]] = mapped_column(
|
||||
String
|
||||
)
|
||||
valuation_improvement_per_unit: Mapped[Optional[str]] = mapped_column(String)
|
||||
cost_per_unit: Mapped[Optional[str]] = mapped_column(String)
|
||||
cost_per_co2_saved: Mapped[Optional[str]] = mapped_column(String)
|
||||
cost_per_sap_point: Mapped[Optional[str]] = mapped_column(String)
|
||||
valuation_return_on_investment: Mapped[Optional[str]] = mapped_column(String)
|
||||
property_valuation_increase: Mapped[Optional[float]] = mapped_column(Float)
|
||||
labour_days: Mapped[Optional[float]] = mapped_column(Float)
|
||||
|
||||
|
||||
class MeasureType(enum.Enum):
|
||||
air_source_heat_pump = "air_source_heat_pump"
|
||||
boiler_upgrade = "boiler_upgrade"
|
||||
high_heat_retention_storage_heaters = "high_heat_retention_storage_heaters"
|
||||
secondary_heating = "secondary_heating"
|
||||
|
||||
roomstat_programmer_trvs = "roomstat_programmer_trvs"
|
||||
time_temperature_zone_control = "time_temperature_zone_control"
|
||||
cylinder_thermostat = "cylinder_thermostat"
|
||||
|
||||
cavity_wall_insulation = "cavity_wall_insulation"
|
||||
extension_cavity_wall_insulation = "extension_cavity_wall_insulation"
|
||||
external_wall_insulation = "external_wall_insulation"
|
||||
internal_wall_insulation = "internal_wall_insulation"
|
||||
loft_insulation = "loft_insulation"
|
||||
flat_roof_insulation = "flat_roof_insulation"
|
||||
room_roof_insulation = "room_roof_insulation"
|
||||
solid_floor_insulation = "solid_floor_insulation"
|
||||
suspended_floor_insulation = "suspended_floor_insulation"
|
||||
|
||||
double_glazing = "double_glazing"
|
||||
secondary_glazing = "secondary_glazing"
|
||||
draught_proofing = "draught_proofing"
|
||||
|
||||
mechanical_ventilation = "mechanical_ventilation"
|
||||
low_energy_lighting = "low_energy_lighting"
|
||||
solar_pv = "solar_pv"
|
||||
hot_water_tank_insulation = "hot_water_tank_insulation"
|
||||
sealing_open_fireplace = "sealing_open_fireplace"
|
||||
|
||||
|
||||
class InstalledMeasure(Base):
|
||||
__tablename__ = "installed_measure"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
uprn = Column(BigInteger, nullable=False)
|
||||
measure_type = Column(
|
||||
Enum(
|
||||
MeasureType,
|
||||
name="measure_type",
|
||||
values_callable=lambda e: [m.value for m in e],
|
||||
create_type=False, # <-- critical
|
||||
),
|
||||
nullable=False,
|
||||
)
|
||||
installed_at = Column(TIMESTAMP)
|
||||
sap_points = Column(Float)
|
||||
carbon_savings = Column(Float)
|
||||
kwh_savings = Column(Float)
|
||||
bill_savings = Column(Float)
|
||||
heat_demand_savings = Column(Float)
|
||||
source = Column(String)
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
|
||||
def enum_values(e: Iterable[PlanType]) -> list[str]:
|
||||
return [m.value for m in e]
|
||||
__all__ = [
|
||||
"PlanModel",
|
||||
"ScenarioModel",
|
||||
"Recommendation",
|
||||
"RecommendationMaterials",
|
||||
"InstalledMeasure",
|
||||
"PlanTypeEnum",
|
||||
"PlanPersistence",
|
||||
]
|
||||
|
||||
|
||||
class PlanPersistence(NamedTuple):
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
(ADR-0017 amendment).
|
||||
|
||||
One canonical SQLModel per physical table — `plan`, `recommendation`,
|
||||
`recommendation_materials` — replacing the legacy SQLAlchemy `Base` models in
|
||||
`backend/app/db/models/recommendations.py` (now a re-export shim, the
|
||||
`epc_property` pattern). `recommendation` carries `plan_id`; the
|
||||
`plan_recommendations` m2m is retired.
|
||||
`recommendation_materials`, `scenario`, `installed_measure` — replacing the
|
||||
legacy SQLAlchemy `Base` models in `backend/app/db/models/recommendations.py`
|
||||
(now a re-export shim, the `epc_property` pattern). `recommendation` carries
|
||||
`plan_id`; the `plan_recommendations` m2m is retired.
|
||||
"""
|
||||
|
||||
from infrastructure.postgres.modelling.plan_table import PlanModel, PlanType
|
||||
|
|
@ -13,10 +13,18 @@ from infrastructure.postgres.modelling.recommendation_table import (
|
|||
RecommendationMaterialModel,
|
||||
RecommendationModel,
|
||||
)
|
||||
from infrastructure.postgres.modelling.scenario_table import ScenarioModel
|
||||
from infrastructure.postgres.modelling.installed_measure_table import (
|
||||
InstalledMeasureModel,
|
||||
MeasureType,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PlanModel",
|
||||
"PlanType",
|
||||
"RecommendationModel",
|
||||
"RecommendationMaterialModel",
|
||||
"ScenarioModel",
|
||||
"InstalledMeasureModel",
|
||||
"MeasureType",
|
||||
]
|
||||
|
|
|
|||
73
infrastructure/postgres/modelling/installed_measure_table.py
Normal file
73
infrastructure/postgres/modelling/installed_measure_table.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from datetime import datetime
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
from sqlalchemy import Column, TIMESTAMP
|
||||
from sqlalchemy import Enum as SAEnum
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class MeasureType(enum.Enum):
|
||||
air_source_heat_pump = "air_source_heat_pump"
|
||||
boiler_upgrade = "boiler_upgrade"
|
||||
high_heat_retention_storage_heaters = "high_heat_retention_storage_heaters"
|
||||
secondary_heating = "secondary_heating"
|
||||
|
||||
roomstat_programmer_trvs = "roomstat_programmer_trvs"
|
||||
time_temperature_zone_control = "time_temperature_zone_control"
|
||||
cylinder_thermostat = "cylinder_thermostat"
|
||||
|
||||
cavity_wall_insulation = "cavity_wall_insulation"
|
||||
extension_cavity_wall_insulation = "extension_cavity_wall_insulation"
|
||||
external_wall_insulation = "external_wall_insulation"
|
||||
internal_wall_insulation = "internal_wall_insulation"
|
||||
loft_insulation = "loft_insulation"
|
||||
flat_roof_insulation = "flat_roof_insulation"
|
||||
room_roof_insulation = "room_roof_insulation"
|
||||
solid_floor_insulation = "solid_floor_insulation"
|
||||
suspended_floor_insulation = "suspended_floor_insulation"
|
||||
|
||||
double_glazing = "double_glazing"
|
||||
secondary_glazing = "secondary_glazing"
|
||||
draught_proofing = "draught_proofing"
|
||||
|
||||
mechanical_ventilation = "mechanical_ventilation"
|
||||
low_energy_lighting = "low_energy_lighting"
|
||||
solar_pv = "solar_pv"
|
||||
hot_water_tank_insulation = "hot_water_tank_insulation"
|
||||
sealing_open_fireplace = "sealing_open_fireplace"
|
||||
|
||||
|
||||
class InstalledMeasureModel(SQLModel, table=True):
|
||||
"""The single SQLModel definition of the live ``installed_measure`` table
|
||||
(ADR-0017 amendment). ``measure_type`` is the ``MeasureType`` Postgres enum;
|
||||
the remaining NOT-NULLs are relaxed to nullable (mirror convention — the
|
||||
live constraints are owned by the Drizzle schema)."""
|
||||
|
||||
__tablename__: ClassVar[str] = "installed_measure" # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
uprn: Optional[int] = Field(default=None, index=True)
|
||||
measure_type: MeasureType = Field(
|
||||
sa_column=Column(
|
||||
SAEnum(
|
||||
MeasureType,
|
||||
name="measure_type",
|
||||
values_callable=lambda cls: [m.value for m in cls], # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType, reportUnknownVariableType]
|
||||
create_type=False,
|
||||
),
|
||||
nullable=False,
|
||||
)
|
||||
)
|
||||
installed_at: Optional[datetime] = Field(
|
||||
default=None, sa_column=Column(TIMESTAMP, nullable=True)
|
||||
)
|
||||
sap_points: Optional[float] = Field(default=None)
|
||||
carbon_savings: Optional[float] = Field(default=None)
|
||||
kwh_savings: Optional[float] = Field(default=None)
|
||||
bill_savings: Optional[float] = Field(default=None)
|
||||
heat_demand_savings: Optional[float] = Field(default=None)
|
||||
source: Optional[str] = Field(default=None)
|
||||
is_active: bool = True
|
||||
92
infrastructure/postgres/modelling/scenario_table.py
Normal file
92
infrastructure/postgres/modelling/scenario_table.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
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,
|
||||
)
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
from domain.modelling.scenario import Scenario
|
||||
|
||||
|
||||
class ScenarioRow(SQLModel, table=True):
|
||||
"""SQLModel mirror of the live ``scenario`` table (ADR-0017).
|
||||
|
||||
Declares only the columns the Modelling stage reads — the legacy
|
||||
file-path columns (`trigger_file_path`, `exclusions`, …) and the
|
||||
portfolio-level aggregates are left to the legacy SQLAlchemy model
|
||||
(`backend/app/db/models/recommendations.py::ScenarioModel`), which still
|
||||
owns the live reads. The physical table is the shared contract; this
|
||||
mirror is read-only from the rebuild's side.
|
||||
|
||||
`goal` is a Postgres enum in production; mapped here as its string value
|
||||
(the Modelling stage does not yet branch on it — #1160).
|
||||
"""
|
||||
|
||||
__tablename__: ClassVar[str] = "scenario" # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
goal: str
|
||||
goal_value: str
|
||||
budget: Optional[float] = Field(default=None)
|
||||
is_default: bool = Field(default=False)
|
||||
|
||||
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,
|
||||
goal_value=self.goal_value,
|
||||
budget=self.budget,
|
||||
is_default=self.is_default,
|
||||
)
|
||||
|
|
@ -3,12 +3,12 @@ from __future__ import annotations
|
|||
from sqlmodel import Session, col, select
|
||||
|
||||
from domain.modelling.scenario import Scenario
|
||||
from infrastructure.postgres.scenario_table import ScenarioRow
|
||||
from infrastructure.postgres.modelling import ScenarioModel
|
||||
from repositories.scenario.scenario_repository import ScenarioRepository
|
||||
|
||||
|
||||
class ScenarioPostgresRepository(ScenarioRepository):
|
||||
"""Reads the live ``scenario`` table (via the ``ScenarioRow`` mirror) and
|
||||
"""Reads the live ``scenario`` table (via the ``ScenarioModel`` mirror) and
|
||||
maps each row to the thin domain ``Scenario`` the Modelling stage uses
|
||||
(ADR-0017). The legacy file-path / aggregate columns are not read."""
|
||||
|
||||
|
|
@ -17,9 +17,9 @@ class ScenarioPostgresRepository(ScenarioRepository):
|
|||
|
||||
def get_many(self, scenario_ids: list[int]) -> list[Scenario]:
|
||||
rows = self._session.exec(
|
||||
select(ScenarioRow).where(col(ScenarioRow.id).in_(scenario_ids))
|
||||
select(ScenarioModel).where(col(ScenarioModel.id).in_(scenario_ids))
|
||||
).all()
|
||||
by_id: dict[int, ScenarioRow] = {
|
||||
by_id: dict[int, ScenarioModel] = {
|
||||
row.id: row for row in rows if row.id is not None
|
||||
}
|
||||
scenarios: list[Scenario] = []
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
|||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.property_baseline.rebaseliner import StubRebaseliner
|
||||
from domain.sap10_calculator.calculator import Sap10Calculator
|
||||
from infrastructure.postgres.scenario_table import ScenarioRow
|
||||
from domain.modelling.portfolio_goal import PortfolioGoal
|
||||
from infrastructure.postgres.modelling import ScenarioModel
|
||||
from domain.geospatial.coordinates import Coordinates
|
||||
from infrastructure.postgres.property_baseline_performance_table import (
|
||||
PropertyBaselinePerformanceModel,
|
||||
|
|
@ -110,8 +111,8 @@ def test_first_run_baselines_through_repos_and_is_idempotent_on_rerun(
|
|||
# Modelling now runs for real: it reads scenario 7 (the command's
|
||||
# scenario_ids) through the repo, so the row must exist.
|
||||
session.add(
|
||||
ScenarioRow(
|
||||
id=7, goal="Increasing EPC", goal_value="C", is_default=True
|
||||
ScenarioModel(
|
||||
id=7, goal=PortfolioGoal.INCREASING_EPC, goal_value="C", is_default=True
|
||||
)
|
||||
)
|
||||
# The sample EPC's solid floor is uninsulated, so the floor generator
|
||||
|
|
@ -214,8 +215,8 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan(
|
|||
)
|
||||
)
|
||||
session.add(
|
||||
ScenarioRow(
|
||||
id=7, goal="Increasing EPC", goal_value="C", is_default=True
|
||||
ScenarioModel(
|
||||
id=7, goal=PortfolioGoal.INCREASING_EPC, goal_value="C", is_default=True
|
||||
)
|
||||
)
|
||||
session.add_all(
|
||||
|
|
@ -356,8 +357,8 @@ def test_modelling_recommends_nothing_when_already_at_the_target_band(
|
|||
)
|
||||
)
|
||||
session.add(
|
||||
ScenarioRow(
|
||||
id=8, goal="Increasing EPC", goal_value="D", is_default=True
|
||||
ScenarioModel(
|
||||
id=8, goal=PortfolioGoal.INCREASING_EPC, goal_value="D", is_default=True
|
||||
)
|
||||
)
|
||||
# The fabric Generators + the ventilation dependency builder still run
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ import pytest
|
|||
from sqlalchemy import Engine
|
||||
from sqlmodel import Session
|
||||
|
||||
from domain.modelling.portfolio_goal import PortfolioGoal
|
||||
from domain.modelling.scenario import Scenario
|
||||
from infrastructure.postgres.scenario_table import ScenarioRow
|
||||
from infrastructure.postgres.modelling import ScenarioModel
|
||||
from repositories.scenario.scenario_postgres_repository import (
|
||||
ScenarioPostgresRepository,
|
||||
)
|
||||
|
|
@ -27,18 +28,18 @@ def test_get_many_maps_live_scenario_rows_to_domain_in_input_order(
|
|||
# Arrange
|
||||
with Session(db_engine) as session:
|
||||
session.add(
|
||||
ScenarioRow(
|
||||
ScenarioModel(
|
||||
id=7,
|
||||
goal="INCREASING_EPC",
|
||||
goal=PortfolioGoal.INCREASING_EPC,
|
||||
goal_value="C",
|
||||
budget=15000.0,
|
||||
is_default=True,
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
ScenarioRow(
|
||||
ScenarioModel(
|
||||
id=9,
|
||||
goal="INCREASING_EPC",
|
||||
goal=PortfolioGoal.INCREASING_EPC,
|
||||
goal_value="B",
|
||||
budget=None,
|
||||
is_default=False,
|
||||
|
|
@ -52,14 +53,14 @@ def test_get_many_maps_live_scenario_rows_to_domain_in_input_order(
|
|||
[9, 7]
|
||||
)
|
||||
|
||||
# Assert
|
||||
# Assert — to_domain maps the PortfolioGoal enum to its value string
|
||||
assert [s.id for s in scenarios] == [9, 7] # input order preserved
|
||||
assert scenarios[0] == Scenario(
|
||||
id=9, goal="INCREASING_EPC", goal_value="B", budget=None, is_default=False
|
||||
id=9, goal="Increasing EPC", goal_value="B", budget=None, is_default=False
|
||||
)
|
||||
assert scenarios[1] == Scenario(
|
||||
id=7,
|
||||
goal="INCREASING_EPC",
|
||||
goal="Increasing EPC",
|
||||
goal_value="C",
|
||||
budget=15000.0,
|
||||
is_default=True,
|
||||
|
|
@ -70,8 +71,11 @@ def test_get_many_raises_when_a_scenario_id_is_missing(db_engine: Engine) -> Non
|
|||
# Arrange
|
||||
with Session(db_engine) as session:
|
||||
session.add(
|
||||
ScenarioRow(
|
||||
id=7, goal="INCREASING_EPC", goal_value="C", is_default=True
|
||||
ScenarioModel(
|
||||
id=7,
|
||||
goal=PortfolioGoal.INCREASING_EPC,
|
||||
goal_value="C",
|
||||
is_default=True,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue