fix(repositories): fold landlord overrides when re-hydrating a Property via the UoW

PostgresUnitOfWork built its PropertyPostgresRepository without an overrides
reader, so a Property re-hydrated through the unit silently dropped its
Landlord Overrides (ADR-0032). The Baseline orchestrator runs through the UoW,
so it scored the bare lodged EPC while the Plan modelled the override-folded
Effective EPC — the two diverged (e.g. baseline effective 71/C vs plan
baseline 62/D), producing "already at band C yet recommends reaching C".

Wire PropertyOverridesPostgresReader into the unit's property repo (uow-
independent committed reference data, read via the same session factory) so
every re-hydration folds overrides, matching the live modelling path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-24 08:34:30 +00:00
parent e80be44fd1
commit 7a970ea393
2 changed files with 76 additions and 1 deletions

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

@ -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))