diff --git a/repositories/postgres_unit_of_work.py b/repositories/postgres_unit_of_work.py index a828d8c6..8a66146c 100644 --- a/repositories/postgres_unit_of_work.py +++ b/repositories/postgres_unit_of_work.py @@ -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) diff --git a/tests/repositories/test_unit_of_work.py b/tests/repositories/test_unit_of_work.py index 5008d4c6..3cc13d6c 100644 --- a/tests/repositories/test_unit_of_work.py +++ b/tests/repositories/test_unit_of_work.py @@ -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))