mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Round-trip a null Lodged Performance through persistence 🟩
The four lodged_* columns are nullable as a unit; from_domain writes them all NULL when a predicted Property has no Lodged Performance, and to_domain reconstructs lodged=None (lodged_sap_score is the read discriminator, mirroring the bill block). The production lodged_* columns are FE-owned (Drizzle) and need a companion ALTER ... DROP NOT NULL migration before the backend writes NULL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da805bce21
commit
b9bec18f44
2 changed files with 68 additions and 15 deletions
|
|
@ -48,12 +48,16 @@ class PropertyBaselinePerformanceModel(SQLModel, table=True):
|
|||
# Postgres won't coerce to the enum. Bind the native enums explicitly
|
||||
# (``create_type=False``: the types already exist). Values stay plain
|
||||
# strings on the domain side (``Epc(...)`` / the RebaselineReason Literal).
|
||||
lodged_sap_score: int
|
||||
lodged_epc_band: str = Field(
|
||||
sa_column=Column(SAEnum(*_EPC_BANDS, name="epc", create_type=False), nullable=False)
|
||||
# The four lodged_* columns are nullable as a unit: a predicted Property has no
|
||||
# lodged cert, so it has no Lodged Performance (ADR-0004 amendment, #1361). They
|
||||
# are written all-set or all-NULL; lodged_sap_score is the read discriminator.
|
||||
lodged_sap_score: Optional[int] = None
|
||||
lodged_epc_band: Optional[str] = Field(
|
||||
default=None,
|
||||
sa_column=Column(SAEnum(*_EPC_BANDS, name="epc", create_type=False), nullable=True),
|
||||
)
|
||||
lodged_co2_emissions_t_per_yr: float
|
||||
lodged_primary_energy_intensity_kwh_per_m2_yr: int
|
||||
lodged_co2_emissions_t_per_yr: Optional[float] = None
|
||||
lodged_primary_energy_intensity_kwh_per_m2_yr: Optional[int] = None
|
||||
|
||||
effective_sap_score: int
|
||||
effective_epc_band: str = Field(
|
||||
|
|
@ -102,12 +106,17 @@ class PropertyBaselinePerformanceModel(SQLModel, table=True):
|
|||
def from_domain(
|
||||
cls, baseline: PropertyBaselinePerformance, property_id: int
|
||||
) -> "PropertyBaselinePerformanceModel":
|
||||
# A predicted Property has no Lodged Performance — the four lodged_* columns
|
||||
# are all NULL then (ADR-0004 amendment, #1361).
|
||||
lodged = baseline.lodged
|
||||
model = cls(
|
||||
property_id=property_id,
|
||||
lodged_sap_score=baseline.lodged.sap_score,
|
||||
lodged_epc_band=baseline.lodged.epc_band.value,
|
||||
lodged_co2_emissions_t_per_yr=baseline.lodged.co2_emissions,
|
||||
lodged_primary_energy_intensity_kwh_per_m2_yr=baseline.lodged.primary_energy_intensity,
|
||||
lodged_sap_score=lodged.sap_score if lodged is not None else None,
|
||||
lodged_epc_band=lodged.epc_band.value if lodged is not None else None,
|
||||
lodged_co2_emissions_t_per_yr=lodged.co2_emissions if lodged is not None else None,
|
||||
lodged_primary_energy_intensity_kwh_per_m2_yr=(
|
||||
lodged.primary_energy_intensity if lodged is not None else None
|
||||
),
|
||||
effective_sap_score=baseline.effective.sap_score,
|
||||
effective_epc_band=baseline.effective.epc_band.value,
|
||||
effective_co2_emissions_t_per_yr=baseline.effective.co2_emissions,
|
||||
|
|
@ -139,12 +148,7 @@ class PropertyBaselinePerformanceModel(SQLModel, table=True):
|
|||
|
||||
def to_domain(self) -> PropertyBaselinePerformance:
|
||||
return PropertyBaselinePerformance(
|
||||
lodged=Performance(
|
||||
sap_score=self.lodged_sap_score,
|
||||
epc_band=Epc(self.lodged_epc_band),
|
||||
co2_emissions=self.lodged_co2_emissions_t_per_yr,
|
||||
primary_energy_intensity=self.lodged_primary_energy_intensity_kwh_per_m2_yr,
|
||||
),
|
||||
lodged=self._read_lodged(),
|
||||
effective=Performance(
|
||||
sap_score=self.effective_sap_score,
|
||||
epc_band=Epc(self.effective_epc_band),
|
||||
|
|
@ -157,6 +161,23 @@ class PropertyBaselinePerformanceModel(SQLModel, table=True):
|
|||
bill=self._read_bill(),
|
||||
)
|
||||
|
||||
def _read_lodged(self) -> Optional[Performance]:
|
||||
"""The Lodged half, or None for a predicted Property whose four lodged_*
|
||||
columns are NULL (ADR-0004 amendment, #1361). They are written as a unit
|
||||
(`from_domain`), so lodged_sap_score is the discriminator and the other
|
||||
three are non-null alongside it."""
|
||||
if self.lodged_sap_score is None:
|
||||
return None
|
||||
assert self.lodged_epc_band is not None
|
||||
assert self.lodged_co2_emissions_t_per_yr is not None
|
||||
assert self.lodged_primary_energy_intensity_kwh_per_m2_yr is not None
|
||||
return Performance(
|
||||
sap_score=self.lodged_sap_score,
|
||||
epc_band=Epc(self.lodged_epc_band),
|
||||
co2_emissions=self.lodged_co2_emissions_t_per_yr,
|
||||
primary_energy_intensity=self.lodged_primary_energy_intensity_kwh_per_m2_yr,
|
||||
)
|
||||
|
||||
def _read_bill(self) -> Optional[Bill]:
|
||||
"""Reconstruct the Bill from the ``bill_*`` columns. The total is the
|
||||
not-None discriminator: a persisted bill always sets it, so its absence
|
||||
|
|
|
|||
|
|
@ -83,6 +83,38 @@ def test_resaving_baseline_for_a_property_replaces_rather_than_duplicating(
|
|||
assert loaded == rerun
|
||||
|
||||
|
||||
def _predicted_baseline() -> PropertyBaselinePerformance:
|
||||
"""A predicted Property's baseline: no Lodged Performance (no cert to read one
|
||||
off), only the Effective half established (ADR-0004 amendment, #1361)."""
|
||||
return PropertyBaselinePerformance(
|
||||
lodged=None,
|
||||
effective=Performance(
|
||||
sap_score=57, epc_band=Epc.D, co2_emissions=1.5, primary_energy_intensity=160
|
||||
),
|
||||
rebaseline_reason="physical_state_changed",
|
||||
space_heating_kwh=4200.0,
|
||||
water_heating_kwh=1600.0,
|
||||
)
|
||||
|
||||
|
||||
def test_predicted_baseline_round_trips_with_no_lodged_half(db_engine: Engine) -> None:
|
||||
# Arrange — a predicted Property has no Lodged Performance; the four lodged_*
|
||||
# columns persist as NULL and reconstruct as a None lodged half.
|
||||
baseline = _predicted_baseline()
|
||||
with Session(db_engine) as session:
|
||||
PropertyBaselinePostgresRepository(session).save(baseline, property_id=13)
|
||||
session.commit()
|
||||
|
||||
# Act
|
||||
with Session(db_engine) as session:
|
||||
loaded = PropertyBaselinePostgresRepository(session).get_for_property(13)
|
||||
|
||||
# Assert — the lodged half round-trips as None; the Effective half is intact.
|
||||
assert loaded == baseline
|
||||
assert loaded is not None
|
||||
assert loaded.lodged is None
|
||||
|
||||
|
||||
def test_get_for_property_returns_none_when_absent(db_engine: Engine) -> None:
|
||||
# Arrange / Act
|
||||
with Session(db_engine) as session:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue