Hydrate landlord-override overlays onto the Property from property_overrides 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-16 17:29:31 +00:00
parent 6eaf7456c2
commit 4c038ae8dc
3 changed files with 155 additions and 0 deletions

View file

@ -0,0 +1,27 @@
"""Map a Property's resolved Landlord Overrides to Simulation Overlays (ADR-0032).
The boundary between the faithful `property_overrides` read model
(`ResolvedPropertyOverrides`, value-space) and the domain overlay surface
(`EpcSimulation`). Lives in `repositories/` because it consumes a repository
type `domain/` never imports `repositories/`. Per-component and partial: an
override produces an overlay only where a component mapping exists and resolves
(the tracer covers `wall_type`); everything else is left to the lodged EPC.
"""
from __future__ import annotations
from domain.epc.wall_type_overlay import wall_overlay_for
from domain.modelling.simulation import EpcSimulation
from repositories.property.property_overrides_reader import ResolvedPropertyOverrides
_WALL_TYPE = "wall_type"
def overlays_from(overrides: ResolvedPropertyOverrides) -> list[EpcSimulation]:
overlays: list[EpcSimulation] = []
for row in overrides.rows:
if row.override_component == _WALL_TYPE:
overlay = wall_overlay_for(row.override_value, row.building_part)
if overlay is not None:
overlays.append(overlay)
return overlays

View file

@ -8,10 +8,13 @@ from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlmodel import Session, col, select
from domain.geospatial.planning_restrictions import PlanningRestrictions
from domain.modelling.simulation import EpcSimulation
from domain.property.properties import Properties
from domain.property.property import Property, PropertyIdentity
from infrastructure.postgres.property_table import PropertyRow
from repositories.epc.epc_repository import EpcRepository
from repositories.property.landlord_override_overlays import overlays_from
from repositories.property.property_overrides_reader import PropertyOverridesReader
from repositories.property.property_repository import (
PropertyIdentityInsert,
PropertyRepository,
@ -36,10 +39,12 @@ class PropertyPostgresRepository(PropertyRepository):
session: Session,
epc_repo: Optional[EpcRepository] = None,
spatial_repo: Optional[SpatialRepository] = None,
overrides_reader: Optional[PropertyOverridesReader] = None,
) -> None:
self._session = session
self._epc_repo = epc_repo
self._spatial_repo = spatial_repo
self._overrides_reader = overrides_reader
# ``__table__`` is injected at runtime on table=True classes but the stubs
# don't expose it; pin to ``Table`` so the dialect insert is typed.
self._table: Table = cast(Table, getattr(PropertyRow, "__table__"))
@ -52,6 +57,13 @@ class PropertyPostgresRepository(PropertyRepository):
)
return self._epc_repo
def _landlord_overrides(self, property_id: int) -> list[EpcSimulation]:
"""The Property's Landlord Overrides as Simulation Overlays — empty when
no reader is wired (the overlay stays off) or the Property has none."""
if self._overrides_reader is None:
return []
return overlays_from(self._overrides_reader.overrides_for(property_id))
def get(self, property_id: int) -> Property:
row = self._session.get(PropertyRow, property_id)
if row is None:
@ -73,6 +85,7 @@ class PropertyPostgresRepository(PropertyRepository):
identity=identity,
epc=self._epc().get_for_property(property_id),
predicted_epc=self._epc().get_predicted_for_property(property_id),
landlord_overrides=self._landlord_overrides(property_id),
planning_restrictions=_restrictions_of(row.uprn, restrictions),
)
@ -104,6 +117,7 @@ class PropertyPostgresRepository(PropertyRepository):
),
epc=epcs.get(property_id),
predicted_epc=predicted_epcs.get(property_id),
landlord_overrides=self._landlord_overrides(property_id),
planning_restrictions=_restrictions_of(row.uprn, restrictions),
)
)

View file

@ -0,0 +1,114 @@
"""PropertyPostgresRepository hydrates Landlord Overrides as overlays (ADR-0032).
Real Postgres end-to-end for the read side: a lodged EPC and a `wall_type`
override row are persisted, and the reloaded Property's Effective EPC reflects
the override folded onto the lodged wall proving reader overlay aggregate.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from sqlalchemy import Engine
from sqlmodel import Session
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
)
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
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.property.property_overrides_postgres_reader import (
PropertyOverridesPostgresReader,
)
from repositories.property.property_postgres_repository import (
PropertyPostgresRepository,
)
_JSON_SAMPLES = Path(__file__).resolve().parents[3] / "backend/epc_api/json_samples"
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 test_reloaded_property_effective_epc_reflects_the_wall_override(
db_engine: Engine,
) -> None:
# Arrange — a Property with a lodged EPC (cavity main wall) and a solid-brick
# / internal-insulation wall override.
with Session(db_engine) as session:
row = PropertyRow(portfolio_id=1, postcode="A0 0AA", address="1 St", uprn=1)
session.add(row)
session.commit()
property_id = row.id
assert property_id is not None
EpcPostgresRepository(session).save(_epc(), property_id=property_id)
session.add(
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",
)
)
session.commit()
# Act — reload through the real repository with the overrides reader wired.
with Session(db_engine) as session:
repo = PropertyPostgresRepository(
session,
EpcPostgresRepository(session),
overrides_reader=PropertyOverridesPostgresReader(lambda: Session(db_engine)),
)
prop = repo.get(property_id)
main = next(
part
for part in prop.effective_epc.sap_building_parts
if part.identifier is BuildingPartIdentifier.MAIN
)
# Assert — the lodged cavity wall (4) is overlaid to solid brick (3) / internal (3).
assert main.wall_construction == 3
assert main.wall_insulation_type == 3
def test_property_without_overrides_keeps_its_lodged_wall(db_engine: Engine) -> None:
# Arrange — a Property with a lodged EPC but no override rows.
with Session(db_engine) as session:
row = PropertyRow(portfolio_id=1, postcode="A0 0AA", address="2 St", uprn=2)
session.add(row)
session.commit()
property_id = row.id
assert property_id is not None
EpcPostgresRepository(session).save(_epc(), property_id=property_id)
session.commit()
# Act
with Session(db_engine) as session:
repo = PropertyPostgresRepository(
session,
EpcPostgresRepository(session),
overrides_reader=PropertyOverridesPostgresReader(lambda: Session(db_engine)),
)
prop = repo.get(property_id)
main = next(
part
for part in prop.effective_epc.sap_building_parts
if part.identifier is BuildingPartIdentifier.MAIN
)
# Assert — the lodged cavity wall (4) is untouched.
assert main.wall_construction == 4