mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
feat(epc): persist renewable_heat_incentive — full round-trip equality (#1137)
Add epc_renewable_heat_incentive table (space_heating_kwh, water_heating_kwh + the three insulation-impact kWh fields), wired into EpcPostgresRepository save/get. This is the P0 gap: RenewableHeatIncentive carries the baseline space-heating/hot-water kWh that EPC Energy Derivation consumes. The round-trip test now asserts full deep-equality (dropped the renewable_heat_incentive exclusion) and passes for RdSAP 21.0.0 + 21.0.1. DB migration for the new table documented in docs/migrations/epc-property-round-trip-fidelity.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5f0a3b8f65
commit
311d1e751a
4 changed files with 61 additions and 11 deletions
|
|
@ -44,11 +44,14 @@ and **still require the matching DB migration** wherever the physical tables liv
|
||||||
`ventilation_has_draught_lobby`, `ventilation_air_permeability_ap4_m3_h_m2`,
|
`ventilation_has_draught_lobby`, `ventilation_air_permeability_ap4_m3_h_m2`,
|
||||||
`ventilation_mechanical_ventilation_kind`; `epc_building_part`: `roof_construction_type`,
|
`ventilation_mechanical_ventilation_kind`; `epc_building_part`: `roof_construction_type`,
|
||||||
`curtain_wall_age`.
|
`curtain_wall_age`.
|
||||||
|
- **§2.1 `epc_renewable_heat_incentive` table** (#1137) — now created on the SQLModel and wired
|
||||||
|
into save/get; the round-trip test asserts **full deep-equality** (no exclusion). DB migration
|
||||||
|
still required.
|
||||||
|
|
||||||
**Still open (follow-up issues):** §2.1 `epc_renewable_heat_incentive` (P0, #1137 — excluded from the
|
**Still open (follow-up issues):** the remaining §2 structural tables (room-in-roof detail, PV
|
||||||
slice-1 assertion via `dataclasses.replace(..., renewable_heat_incentive=None)` until landed), and
|
arrays, roof windows) + §3 nested-wall fields (`SapAlternativeWall.u_value`/`wall_thickness_mm`) +
|
||||||
the remaining §2 structural tables (room-in-roof detail, PV arrays, roof windows) + §3 nested-wall
|
`SapFloorDimension` exposed-floor flags — none populated in the 21.0.0/21.0.1 fixtures, so latent
|
||||||
fields (`SapAlternativeWall.u_value`/`wall_thickness_mm`) + `SapFloorDimension` exposed-floor flags.
|
until a richer fixture exercises them.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
EpcPropertyData,
|
EpcPropertyData,
|
||||||
EnergyElement,
|
EnergyElement,
|
||||||
MainHeatingDetail,
|
MainHeatingDetail,
|
||||||
|
RenewableHeatIncentive,
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
SapFloorDimension,
|
SapFloorDimension,
|
||||||
SapFlatDetails,
|
SapFlatDetails,
|
||||||
|
|
@ -413,6 +414,34 @@ class EpcPropertyEnergyPerformanceModel(SQLModel, table=True):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EpcRenewableHeatIncentiveModel(SQLModel, table=True):
|
||||||
|
__tablename__: ClassVar[str] = "epc_renewable_heat_incentive" # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
epc_property_id: int = Field(
|
||||||
|
foreign_key="epc_property.id", nullable=False, unique=True
|
||||||
|
)
|
||||||
|
|
||||||
|
space_heating_kwh: float
|
||||||
|
water_heating_kwh: float
|
||||||
|
impact_of_loft_insulation_kwh: Optional[float] = Field(default=None)
|
||||||
|
impact_of_cavity_insulation_kwh: Optional[float] = Field(default=None)
|
||||||
|
impact_of_solid_wall_insulation_kwh: Optional[float] = Field(default=None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_domain(
|
||||||
|
cls, rhi: RenewableHeatIncentive, epc_property_id: int
|
||||||
|
) -> EpcRenewableHeatIncentiveModel:
|
||||||
|
return cls(
|
||||||
|
epc_property_id=epc_property_id,
|
||||||
|
space_heating_kwh=rhi.space_heating_kwh,
|
||||||
|
water_heating_kwh=rhi.water_heating_kwh,
|
||||||
|
impact_of_loft_insulation_kwh=rhi.impact_of_loft_insulation_kwh,
|
||||||
|
impact_of_cavity_insulation_kwh=rhi.impact_of_cavity_insulation_kwh,
|
||||||
|
impact_of_solid_wall_insulation_kwh=rhi.impact_of_solid_wall_insulation_kwh,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EpcFlatDetailsModel(SQLModel, table=True):
|
class EpcFlatDetailsModel(SQLModel, table=True):
|
||||||
__tablename__: ClassVar[str] = "epc_flat_details" # pyright: ignore[reportIncompatibleVariableOverride]
|
__tablename__: ClassVar[str] = "epc_flat_details" # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
PhotovoltaicSupplyNoneOrNoDetails,
|
PhotovoltaicSupplyNoneOrNoDetails,
|
||||||
PvBatteries,
|
PvBatteries,
|
||||||
PvBattery,
|
PvBattery,
|
||||||
|
RenewableHeatIncentive,
|
||||||
SapAlternativeWall,
|
SapAlternativeWall,
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
SapEnergySource,
|
SapEnergySource,
|
||||||
|
|
@ -40,6 +41,7 @@ from infrastructure.postgres.epc_property_table import (
|
||||||
EpcMainHeatingDetailModel,
|
EpcMainHeatingDetailModel,
|
||||||
EpcPropertyEnergyPerformanceModel,
|
EpcPropertyEnergyPerformanceModel,
|
||||||
EpcPropertyModel,
|
EpcPropertyModel,
|
||||||
|
EpcRenewableHeatIncentiveModel,
|
||||||
EpcWindowModel,
|
EpcWindowModel,
|
||||||
)
|
)
|
||||||
from repositories.epc.epc_repository import EpcRepository
|
from repositories.epc.epc_repository import EpcRepository
|
||||||
|
|
@ -124,6 +126,12 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
self._session.add(
|
self._session.add(
|
||||||
EpcFlatDetailsModel.from_domain(data.sap_flat_details, epc_property_id)
|
EpcFlatDetailsModel.from_domain(data.sap_flat_details, epc_property_id)
|
||||||
)
|
)
|
||||||
|
if data.renewable_heat_incentive is not None:
|
||||||
|
self._session.add(
|
||||||
|
EpcRenewableHeatIncentiveModel.from_domain(
|
||||||
|
data.renewable_heat_incentive, epc_property_id
|
||||||
|
)
|
||||||
|
)
|
||||||
return epc_property_id
|
return epc_property_id
|
||||||
|
|
||||||
def get(self, epc_property_id: int) -> EpcPropertyData:
|
def get(self, epc_property_id: int) -> EpcPropertyData:
|
||||||
|
|
@ -161,6 +169,11 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
EpcFlatDetailsModel.epc_property_id == epc_property_id
|
EpcFlatDetailsModel.epc_property_id == epc_property_id
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
|
rhi_row = self._session.exec(
|
||||||
|
select(EpcRenewableHeatIncentiveModel).where(
|
||||||
|
EpcRenewableHeatIncentiveModel.epc_property_id == epc_property_id
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
def _elements(element_type: str) -> list[EnergyElement]:
|
def _elements(element_type: str) -> list[EnergyElement]:
|
||||||
return [self._to_energy_element(e) for e in elements if e.element_type == element_type]
|
return [self._to_energy_element(e) for e in elements if e.element_type == element_type]
|
||||||
|
|
@ -308,6 +321,17 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
waste_water_heat_recovery=p.waste_water_heat_recovery,
|
waste_water_heat_recovery=p.waste_water_heat_recovery,
|
||||||
hydro=p.hydro,
|
hydro=p.hydro,
|
||||||
photovoltaic_array=p.photovoltaic_array,
|
photovoltaic_array=p.photovoltaic_array,
|
||||||
|
renewable_heat_incentive=(
|
||||||
|
RenewableHeatIncentive(
|
||||||
|
space_heating_kwh=rhi_row.space_heating_kwh,
|
||||||
|
water_heating_kwh=rhi_row.water_heating_kwh,
|
||||||
|
impact_of_loft_insulation_kwh=rhi_row.impact_of_loft_insulation_kwh,
|
||||||
|
impact_of_cavity_insulation_kwh=rhi_row.impact_of_cavity_insulation_kwh,
|
||||||
|
impact_of_solid_wall_insulation_kwh=rhi_row.impact_of_solid_wall_insulation_kwh,
|
||||||
|
)
|
||||||
|
if rhi_row is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
mechanical_vent_duct_insulation_level=p.mechanical_vent_duct_insulation_level,
|
mechanical_vent_duct_insulation_level=p.mechanical_vent_duct_insulation_level,
|
||||||
addendum=(
|
addendum=(
|
||||||
Addendum(
|
Addendum(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ inside First Run, rather than be deferred to a later Refresh.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
@ -48,9 +47,4 @@ def test_epc_property_data_round_trips(schema_dir: str, db_engine: Engine) -> No
|
||||||
reloaded = EpcPostgresRepository(session).get(epc_property_id)
|
reloaded = EpcPostgresRepository(session).get(epc_property_id)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
# Slice 1 pins round-trip fidelity over the persisted projection. The only
|
assert reloaded == original
|
||||||
# field not yet stored is `renewable_heat_incentive` (the P0 structural gap
|
|
||||||
# tracked in #1137 — a new table); exclude it here and drop this `replace`
|
|
||||||
# once that table lands.
|
|
||||||
projected = dataclasses.replace(original, renewable_heat_incentive=None)
|
|
||||||
assert reloaded == projected
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue