Merge pull request #1293 from Hestia-Homes/feature/e2e-runs

Feature/e2e runs
This commit is contained in:
KhalimCK 2026-06-24 12:36:04 +01:00 committed by GitHub
commit 8c00b586b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 322 additions and 7 deletions

View file

@ -292,6 +292,10 @@ _Avoid_: "windows" as a Measure (name **double glazing** / **secondary glazing**
The rule fixing the single lighting Measure the **Lighting** Recommendation offers. We convert **all non-LED bulbs** (incandescent + CFL + low-energy-unknown) to **LED** — all the way to LED, not the legacy "fill to low energy", because SAP rates LED efficacy above CFL (ADR-0023). One Measure, no planning gate (lighting isn't planning-restricted). Offered only when the dwelling lodges at least one non-LED bulb; a dwelling already all-LED, or one that lodged **no** bulb counts (nothing to size against), gets no Recommendation. Unlike the fabric measures it is a **whole-dwelling** Measure — its **Simulation Overlay** writes the four top-level bulb counts directly (`led = total`, others 0), the first overlay surface that isn't a building part / window / system sub-object. Priced at a flat **average price per bulb** × the count of non-LED bulbs replaced. A free Optimiser candidate (it *improves* SAP), contrast the forced ventilation **Measure Dependency**.
_Avoid_: "low energy lighting" as the upgrade target (we go to **LED**); treating it as a forced dependency (it is a free candidate); pricing by floor area (it's per-bulb count × average)
**Coherent Heating System**:
The set of fields that together describe a dwelling's heating and must move **as a unit** whenever a heating system is *synthesised* — never a subset. It spans the main heating cluster (fuel, system, emitter, **controls**), the **hot water** it implies, the **off-peak meter** an electric-storage or CPSU system runs on, and the hot-water cylinder / solar flags. The fields are physically scattered across the `EpcPropertyData` (a gov-API schema mirror — the cluster, a loose meter on the energy source, loose top-level flags), but the *concept* is one system. Three places synthesise a heating system and each must cover the whole set with its own **fill policy**: a **Measure Option** sets a designed end-state (HHRSH / ASHP), a **Landlord Override** *completes* the partial system the landlord named with conservative assumptions for the fields they couldn't tell us (the off-peak meter is *derived* from the SAP code; an unobserved storage charge control defaults to the conservative manual one), and **EPC Prediction**'s heating donor *carries* a real neighbour's whole coherent set. Setting only a subset mis-scores: an electric-storage system left on a single-rate meter bills its overnight heat at the peak rate and collapses the SAP. **Coherence is owned by the synthesiser, never by the calculator** — a real lodged cert is scored exactly as lodged (ADR-0035).
_Avoid_: heating overlay (that is one *mechanism*, not the field set); meter as a separate concern (it is part of the system); normalising a lodged EPC (coherence is a synthesis-time obligation only)
**Heating Eligibility**:
The rule fixing which **Measure Options** the single **Heating & Hot Water** Recommendation offers (ADR-0024, expanded). The competing Options are **mutually-exclusive** (the Optimiser picks at most one) and fall in two families: **whole-system replacements**`high_heat_retention_storage_heaters`, `air_source_heat_pump` — which change main heating + **controls + fuel + meter + the implied hot water** at once (never a separate HW measure; the legacy heating-vs-HW split double-counted); and, for a dwelling keeping a serviceable wet boiler, **partial upgrades**`gas_boiler_upgrade` (a like-for-like condensing **gas** boiler: gas→gas, or non-gas→gas only where mains gas is present; combi or regular-plus-cylinder, shaped by the dwelling) and the **system tune-up** (keep the boiler; install better **controls** + fix the **cylinder**), the tune-up offered at two competing control levels: `system_tune_up` (standard, SAP code 2106) and `system_tune_up_zoned` (time-and-temperature zone control, 2110 — more SAP uplift, more cost). Each Option is a **fixed, real, contractor-installable end-state** (ASHP via a fixed PCDB heat-pump index; HHR storage via `sap_main_heating_code=409`; the gas boiler via Table 4b code 102/104; controls via 2106/2110), not a derived ideal; **Product** stays cost-only, but a partial/bundle cost is **composed per dwelling** from the components the overlay installs (ADR-0025/0027), not a flat scalar. Eligibility encodes **only physical/planning installability** — the **Optimiser owns the economics**, so it must not re-gate on cost proxies: **ASHP** → houses/bungalows that are not **listed**/**heritage** and not already a heat pump (flats excluded — individual siting needs a survey; a **conservation area** still gets the offer, unlike glazing); **HHR storage** → off-gas or currently-electric dwellings, not community-heated or already HHR; **boiler upgrade / tune-up** → an existing (non-electric) wet boiler, the gas end-state gated on a mains-gas connection, a partial control upgrade offered only when it genuinely improves the existing control (never a downgrade or no-op). Floor area, fabric, fuel, and built form are **not** gates (the legacy ASHP built-form / 120 m² rule is dropped — no authoritative basis). A free Optimiser candidate, not a forced **Measure Dependency**.
_Avoid_: separate "heating" and "hot water" recommendations (HW folds into each Option); gating ASHP on floor area / built form / fabric (eligibility is physical/planning only — the Optimiser decides cost-effectiveness); treating the whole-system replacements and the partial boiler/tune-up upgrades as **separate** Recommendations (they are mutually-exclusive Options within the one heating rec — separate recs would let the Optimiser co-select and double-charge); a standalone hot-water-only or controls-only Recommendation (controls + cylinder fold into the boiler/tune-up Option)

View file

@ -0,0 +1,64 @@
# Coherent Heating System is owned by the synthesiser, not the calculator
## Status
accepted
## Context
A dwelling's heating is one conceptual **system**, but its fields are physically
scattered across `EpcPropertyData` because that type mirrors the gov-API / RdSAP
schema: the main cluster lives on `sap_heating`, the electricity tariff on
`sap_energy_source.meter_type`, and hot-water flags (`has_hot_water_cylinder`,
`solar_water_heating`) on loose top-level fields.
Three places **synthesise** a heating system — a Measure Option (HHRSH/ASHP), a
Landlord Override, and EPC Prediction's heating donor — and each was hand-copying
a *different ad-hoc subset* of those fields. The override and the donor both
omitted `meter_type`. Property 713406 (a predicted property whose landlord
override set "Electric storage heaters") landed on the oil structural template's
single-rate meter, so its overnight storage heat billed at the peak electricity
rate: the calculator scored it **SAP 13 (G)** instead of **~50 (E)**, which then
inflated the HHRSH measure to **+45.8 SAP** and pushed the plan to band A.
## Decision
Treat **"a heating system"** as one named boundary (a **Coherent Heating
System** — see CONTEXT.md). Whoever *synthesises* a heating system owns covering
the whole boundary, with a fill policy suited to its source:
- **Measure** → set a designed end-state (already did).
- **Landlord Override***complete* the partial system the landlord named.
Companion fields are **derived from the SAP code**, not hand-attached per
archetype: the off-peak meter from the calculator's single off-peak
classification (`OFF_PEAK_IMPLYING_HEATING_CODES`, SAP §12 Rules 1-2), and —
where a field has no logical pairing — a **conservative** default (storage
charge control → manual, SAP Table 4e 2401, the lowest-SAP storage control).
So adding a heating archetype is just adding its code; coherent companions
fall out and cannot be forgotten.
- **EPC Prediction donor***carry* a real neighbour's whole coherent set
(including its `meter_type`), since the donor is already internally coherent.
**Coherence is a synthesis-time obligation, never a calculator normalisation.**
A real lodged cert is scored exactly as lodged — the calculator must not "fix" a
genuinely single-rate storage dwelling (its existing Unknown-meter inference is
a separate, spec-faithful net for certs that lodged *Unknown*, and does not fire
on an explicit meter). A contract test guards the override path: every off-peak
code the archetype map can emit must drag a `Dual` meter.
## Considered Options
- **Reify a `HeatingSystem` value object** all three produce/consume — rejected:
overlaps `HeatingOverlay` and is a large, risky restructure for the value.
- **A global coherence pass over the assembled effective EPC** — rejected: it
would corrupt real lodged certs (force storage→dual on a genuine single-rate
lodgement). Coherence must be scoped to synthesis points.
- **Set `meter_type="Unknown"` and lean on the calculator's storage inference**
— rejected as the primary mechanism: an explicit `Dual` is self-contained,
matches the measure overlay, carries display value, and does not depend on a
repo *extension* that could later be tightened toward spec.
This amends [ADR-0029](0029-epc-prediction-from-comparable-properties.md) (the
heating donor now carries the meter) and extends
[ADR-0032](0032-landlord-override-epc-overlay.md) (the override overlay derives
coherent companions).

View file

@ -7,6 +7,16 @@ archetype to a representative code and emits a whole-dwelling `HeatingOverlay`
targeting `main_heating_details[0]` (`building_part` is ignored). It composes
field-wise with the main_fuel / water_heating overlays.
**Coherent Heating System / drag-along (ADR-0035):** a landlord tells us the
*system*, not the dependent fields a coherent heating system carries its
electricity tariff (meter) and, for storage heaters, its charge control. Rather
than hand-attach those per archetype (easy to forget when a new system is
added), they are **derived from the SAP code**: the off-peak meter from the
calculator's single off-peak classification (`OFF_PEAK_IMPLYING_HEATING_CODES`,
SAP §12), and the conservative manual charge control for storage heaters. So
adding a heating archetype is just adding its code coherent companions fall
out. Synthesis owns coherence; the calculator never normalises a lodged cert.
The SEDBUK A-G efficiency band the Hyde "Heating" column carries is NOT honoured
yet (no efficiency slot on the overlay/MainHeatingDetail) -- archetypes map to
their modern/condensing Table 4b code, so an old low-rated boiler is currently
@ -20,11 +30,30 @@ from __future__ import annotations
from typing import Optional
from domain.modelling.simulation import EpcSimulation, HeatingOverlay
from domain.sap10_calculator.tables.table_12a import (
OFF_PEAK_IMPLYING_HEATING_CODES,
)
# Canonical system archetype → representative `sap_main_heating_code` (SAP Table
# 4b boiler rows / Table 4a). Codes map to the modern/condensing variant (A-G
# efficiency deferred): 102 regular condensing, 104 condensing combi, 120 CPSU,
# 404 fan storage heaters, 191 direct-acting electric boiler.
# Off-peak (Economy 7) meter. Electric storage / CPSU systems charge overnight at
# the low rate and cannot run economically on a single-rate meter; "Dual" lets
# the §12 dispatch resolve the specific tariff (storage 7-hour, CPSU 10-hour).
_OFF_PEAK_METER = "Dual"
# SAP Table 4e Group 4 storage charge-control code. Manual charge control is the
# *conservative* assumption when the landlord didn't tell us the control: its
# +0.7 C mean-internal-temperature adjustment is the largest of the storage
# controls (automatic / Celect +0.4, HHR 0), so it never over-credits an
# unobserved control. Scoped to storage *heaters* (Table 4a 401-409) — the only
# systems that take a charge control.
_MANUAL_CHARGE_CONTROL = 2401
_STORAGE_HEATER_CODES = frozenset(range(401, 410))
# Canonical system archetype → representative SAP `sap_main_heating_code`. Codes
# map to the modern/condensing variant (A-G efficiency deferred): 102 regular
# condensing, 104 condensing combi, 120 CPSU, 401-404 storage heaters, 191
# direct-acting electric. Companion fields (meter / charge control) are NOT
# listed here — they are derived from the code below, so a new archetype is just
# a code.
_MAIN_HEATING_CODES: dict[str, int] = {
"Gas boiler, combi": 104,
"Gas boiler, regular": 102,
@ -37,10 +66,29 @@ _MAIN_HEATING_CODES: dict[str, int] = {
}
def _meter_for(code: int) -> Optional[str]:
"""The coherent off-peak meter a heating code implies, or None when the
system is single-rate. Keyed off the calculator's §12 off-peak set so the
"which systems are off-peak" knowledge has one home."""
return _OFF_PEAK_METER if code in OFF_PEAK_IMPLYING_HEATING_CODES else None
def _charge_control_for(code: int) -> Optional[int]:
"""The conservative storage charge control to assume when unobserved, or
None for systems that don't take one."""
return _MANUAL_CHARGE_CONTROL if code in _STORAGE_HEATER_CODES else None
def main_heating_overlay_for(
main_heating_value: str, building_part: int
) -> Optional[EpcSimulation]:
code = _MAIN_HEATING_CODES.get(main_heating_value)
if code is None:
return None
return EpcSimulation(heating=HeatingOverlay(sap_main_heating_code=code))
return EpcSimulation(
heating=HeatingOverlay(
sap_main_heating_code=code,
meter_type=_meter_for(code),
main_heating_control=_charge_control_for(code),
)
)

View file

@ -86,13 +86,20 @@ class EpcPrediction:
(a recent cert reflects the current system). This makes the predicted
heating both representative and internally coherent, rather than whatever
the size-representative template happened to carry. No donor (no neighbour
lodges a main heating system) leaves the template's heating in place."""
lodges a main heating system) leaves the template's heating in place.
The coherent heating system spans more than `sap_heating` (ADR-0035): its
electricity tariff (`sap_energy_source.meter_type`) and hot-water flags
live on loose top-level fields. Carry the donor's whole set, not a subset
otherwise a donated storage system lands on the template's single-rate
meter and the SAP score collapses (off-peak heat billed at the peak rate)."""
donor = _heating_donor(comparables.members)
if donor is None:
return
predicted.sap_heating = copy.deepcopy(donor.epc.sap_heating)
predicted.has_hot_water_cylinder = donor.epc.has_hot_water_cylinder
predicted.solar_water_heating = donor.epc.solar_water_heating
predicted.sap_energy_source.meter_type = donor.epc.sap_energy_source.meter_type
@staticmethod
def _apply_glazing_mode(

View file

@ -275,6 +275,20 @@ _GROUND_OR_WATER_SOURCE_HEAT_PUMP_CODES: Final[frozenset[int]] = frozenset(
| {213, 216, 223, 226, 523, 526} # water source (radiator + warm-air)
)
# The heating codes whose *presence* implies an off-peak (dual) meter: electric
# CPSU (Rule 1) and storage-based electric (Rule 2). These charge overnight and
# cannot run economically on a single rate, so the §12 dispatch already infers
# off-peak for them when the meter is Unknown (see `tariff_dispatch`). Exposed so
# *synthesis* (Landlord-Override / EPC-Prediction) can pair a coherent off-peak
# meter with such a system from the SAP code alone — the single source of "which
# systems are off-peak". Rule 3 (direct-acting electric, heat pumps, room
# heaters) is deliberately NOT here: those run on demand and live on single-rate
# meters too. A "Dual" meter on any of these lets the §12 dispatch resolve the
# specific tariff (CPSU → 10-hour, storage → 7-hour).
OFF_PEAK_IMPLYING_HEATING_CODES: Final[frozenset[int]] = (
_RULE_1_CPSU_CODES | _RULE_2_STORAGE_CODES
)
def _meter_is_unknown(meter_type: object) -> bool:
"""True when the meter is the RdSAP "Unknown" sentinel (code 3 / the

View file

@ -14,6 +14,9 @@ from repositories.plan.plan_postgres_repository import PlanPostgresRepository
from repositories.product.composite_product_repository import (
catalogue_with_off_catalogue_overrides,
)
from repositories.property.property_overrides_postgres_reader import (
PropertyOverridesPostgresReader,
)
from repositories.property.property_postgres_repository import (
PropertyPostgresRepository,
)
@ -41,8 +44,14 @@ class PostgresUnitOfWork(UnitOfWork):
self._session = self._session_factory()
epc_repo = EpcPostgresRepository(self._session)
spatial_repo = SpatialPostgresRepository(self._session)
# Fold Landlord Overrides onto the Effective EPC on every re-hydration
# (ADR-0032), so what the Baseline orchestrator scores off ``uow.property``
# matches what the Plan was modelled from. The reader is uow-independent —
# ``property_overrides`` is committed reference data — so it opens its own
# short read session per call via the same session factory.
overrides_reader = PropertyOverridesPostgresReader(self._session_factory)
self.property = PropertyPostgresRepository(
self._session, epc_repo, spatial_repo
self._session, epc_repo, spatial_repo, overrides_reader
)
self.epc = epc_repo
self.solar = SolarPostgresRepository(self._session)

View file

@ -11,8 +11,12 @@ import pytest
from domain.epc.property_overrides.main_heating_system_type import MainHeatingSystemType
from domain.epc.property_overlays.main_fuel_overlay import fuel_overlay_for
from domain.epc.property_overlays.main_heating_system_overlay import (
_MAIN_HEATING_CODES,
main_heating_overlay_for,
)
from domain.sap10_calculator.tables.table_12a import (
OFF_PEAK_IMPLYING_HEATING_CODES,
)
from domain.epc.property_overlays.water_heating_overlay import (
water_heating_overlay_for,
)
@ -73,6 +77,88 @@ def test_storage_heater_subtypes_decode_to_their_codes(
assert simulation.heating.sap_main_heating_code == code
@pytest.mark.parametrize(
"main_heating_value",
[
"Electric storage heaters, old",
"Electric storage heaters, slimline",
"Electric storage heaters, convector",
"Electric storage heaters, fan",
],
)
def test_storage_heaters_carry_an_off_peak_meter(main_heating_value: str) -> None:
# Storage heaters run on an Economy 7 (off-peak) tariff by design; setting
# only the heating code while leaving a single-rate meter bills every heating
# kWh at the peak rate and collapses the score (an override-set storage
# dwelling left on an oil donor's single meter scored SAP 13 vs ~50 on
# Economy 7). The overlay carries the off-peak meter, like the HHRSH measure.
simulation = main_heating_overlay_for(main_heating_value, 0)
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.meter_type == "Dual"
@pytest.mark.parametrize(
"main_heating_value", ["Gas boiler, combi", "Direct-acting electric"]
)
def test_non_storage_heating_leaves_the_meter_untouched(
main_heating_value: str,
) -> None:
# Only storage heaters imply an off-peak tariff; gas and direct-acting
# electric (single-rate) keep whatever meter the dwelling already has.
simulation = main_heating_overlay_for(main_heating_value, 0)
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.meter_type is None
@pytest.mark.parametrize(
"main_heating_value",
[
"Electric storage heaters, old",
"Electric storage heaters, slimline",
"Electric storage heaters, convector",
"Electric storage heaters, fan",
],
)
def test_storage_heaters_drag_along_conservative_manual_charge_control(
main_heating_value: str,
) -> None:
# The landlord names the system, not its charge control. Manual charge
# control (Table 4e code 2401, +0.7 C MIT adjustment) is the lowest-SAP
# storage control, so it's the safe assumption that never over-credits.
simulation = main_heating_overlay_for(main_heating_value, 0)
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_heating_control == 2401
@pytest.mark.parametrize(
"main_heating_value", ["Gas boiler, combi", "Direct-acting electric"]
)
def test_non_storage_heating_does_not_drag_along_a_charge_control(
main_heating_value: str,
) -> None:
# Charge control is a storage-heater concept; other systems keep their own.
simulation = main_heating_overlay_for(main_heating_value, 0)
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_heating_control is None
def test_off_peak_archetypes_drag_a_dual_meter_others_do_not() -> None:
# Contract (the drag-along guard): the off-peak meter is derived from the SAP
# code via the calculator's single off-peak classification, so any heating
# archetype in the map whose code implies off-peak MUST synthesise a Dual
# meter — adding an off-peak system can never silently leave it single-rate —
# and a single-rate system must never gain one.
for value, code in _MAIN_HEATING_CODES.items():
simulation = main_heating_overlay_for(value, 0)
assert simulation is not None and simulation.heating is not None
expected = "Dual" if code in OFF_PEAK_IMPLYING_HEATING_CODES else None
assert simulation.heating.meter_type == expected, value
@pytest.mark.parametrize(
"main_heating_value",
["Unknown", "", "Air source heat pump", "Community heating"],

View file

@ -12,6 +12,7 @@ from datatypes.epc.domain.epc_property_data import (
EpcPropertyData,
MainHeatingDetail,
SapBuildingPart,
SapEnergySource,
SapFloorDimension,
SapHeating,
SapWindow,
@ -47,6 +48,7 @@ def _epc(
water_heating_code: Optional[int] = 1,
has_hot_water_cylinder: bool = True,
solar_water_heating: bool = False,
meter_type: str = "2",
) -> EpcPropertyData:
epc: EpcPropertyData = object.__new__(EpcPropertyData)
epc.property_type = "2"
@ -84,6 +86,9 @@ def _epc(
epc.sap_heating = heating
epc.has_hot_water_cylinder = has_hot_water_cylinder
epc.solar_water_heating = solar_water_heating
energy: SapEnergySource = object.__new__(SapEnergySource)
energy.meter_type = meter_type
epc.sap_energy_source = energy
return epc
@ -543,3 +548,14 @@ def test_applies_a_known_wall_override_over_the_mode() -> None:
# Assert — the known override overrides the cohort mode.
assert predicted.sap_building_parts[0].wall_construction == 2
def test_heating_donor_carries_the_donors_off_peak_meter() -> None:
# The coherent heating system spans the meter (ADR-0035): the donor's
# off-peak meter must travel with its heating cluster, replacing the
# template's single-rate meter — otherwise a donated storage system bills at
# the peak rate and the score collapses.
predicted = _epc(meter_type="2") # the structural template's single meter
donor = _epc(meter_type="Dual", main_fuel_type=29) # the cohort's heating
EpcPrediction._apply_heating_donor(predicted, _cohort(donor))
assert predicted.sap_energy_source.meter_type == "Dual"

View file

@ -65,6 +65,7 @@ def _comparable(
energy: SapEnergySource = object.__new__(SapEnergySource)
energy.photovoltaic_supply = None
energy.photovoltaic_arrays = None
energy.meter_type = "2"
epc.sap_energy_source = energy
return ComparableProperty(
epc=epc,

View file

@ -1,25 +1,45 @@
from __future__ import annotations
import json
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from sqlalchemy import Engine
from sqlmodel import Session
from datatypes.epc.domain.epc import Epc
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
)
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
from domain.property_baseline.performance import Performance
from infrastructure.postgres.property_override_table import PropertyOverrideRow
from infrastructure.postgres.property_table import PropertyRow
from repositories.epc.epc_postgres_repository import EpcPostgresRepository
from repositories.plan.plan_repository import PlanRepository
from repositories.postgres_unit_of_work import PostgresUnitOfWork
from repositories.product.product_repository import ProductRepository
from repositories.scenario.scenario_repository import ScenarioRepository
from repositories.spatial.spatial_repository import SpatialRepository
_JSON_SAMPLES = Path(__file__).resolve().parents[2] / "backend/epc_api/json_samples"
def _session_factory(db_engine: Engine) -> Callable[[], Session]:
return lambda: Session(db_engine)
def _epc() -> EpcPropertyData:
raw: dict[str, Any] = json.loads(
(_JSON_SAMPLES / "RdSAP-Schema-21.0.0" / "epc.json").read_text()
)
return EpcPropertyDataMapper.from_api_response(raw)
def _baseline() -> PropertyBaselinePerformance:
perf = Performance(
sap_score=72, epc_band=Epc.C, co2_emissions=1.8, primary_energy_intensity=180
@ -86,6 +106,52 @@ def test_unit_exposes_the_spatial_cache_repo_bound_to_its_session(
assert isinstance(uow.spatial, SpatialRepository)
def test_unit_hydrates_a_property_with_its_landlord_overrides_folded(
db_engine: Engine,
) -> None:
"""A Property re-hydrated through the unit folds its Landlord Overrides onto
the Effective EPC (ADR-0032) the same overlay the live modelling path
applies. Without this the Baseline Performance the orchestrator scores off
``uow.property`` diverges from the Plan, which does apply the overrides.
"""
# Arrange — a lodged EPC (cavity main wall, 4) plus a solid-brick / internal
# wall override, persisted through the unit.
with PostgresUnitOfWork(_session_factory(db_engine)) as uow:
row = PropertyRow(portfolio_id=1, postcode="A0 0AA", address="1 St", uprn=1)
uow._session.add(row) # pyright: ignore[reportPrivateUsage]
uow._session.flush() # pyright: ignore[reportPrivateUsage]
property_id = row.id
assert property_id is not None
EpcPostgresRepository(uow._session).save( # pyright: ignore[reportPrivateUsage]
_epc(), property_id=property_id
)
uow._session.add( # pyright: ignore[reportPrivateUsage]
PropertyOverrideRow(
property_id=property_id,
portfolio_id=1,
building_part=0,
override_component="wall_type",
override_value="Solid brick, with internal insulation",
original_spreadsheet_description="solid brick, insulated",
)
)
uow.commit()
# Act — re-hydrate through a fresh unit.
with PostgresUnitOfWork(_session_factory(db_engine)) as uow:
prop = uow.property.get(property_id)
main = next(
part
for part in prop.effective_epc.sap_building_parts
if part.identifier is BuildingPartIdentifier.MAIN
)
# Assert — the override is folded: cavity (4) → solid brick (3) / internal (3).
assert main.wall_construction == 3
assert main.wall_insulation_type == 3
def test_leaving_the_block_without_commit_persists_nothing(db_engine: Engine) -> None:
# Arrange
new_unit = lambda: PostgresUnitOfWork(_session_factory(db_engine))