From 6eaf7456c2210cdfc84a68702541e2bb898844d5 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 16 Jun 2026 17:18:31 +0000 Subject: [PATCH] =?UTF-8?q?Fold=20landlord=20overrides=20onto=20the=20lodg?= =?UTF-8?q?ed=20EPC=20to=20form=20the=20Effective=20EPC=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- domain/property/property.py | 22 +++-- .../test_property_landlord_overlay.py | 82 +++++++++++++++++++ 2 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 tests/domain/property/test_property_landlord_overlay.py diff --git a/domain/property/property.py b/domain/property/property.py index 70acd711..d4652d99 100644 --- a/domain/property/property.py +++ b/domain/property/property.py @@ -1,10 +1,12 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Literal, Optional +from dataclasses import dataclass, field +from typing import Literal, Optional, Sequence from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.geospatial.planning_restrictions import PlanningRestrictions +from domain.modelling.scoring.overlay_applicator import apply_simulations +from domain.modelling.simulation import EpcSimulation from domain.property.site_notes import SiteNotes SourcePath = Literal["site_notes", "epc_with_overlay", "predicted"] @@ -43,6 +45,11 @@ class Property: # structural). Used as the Effective EPC only as a last resort — when there is # neither a lodged EPC nor Site Notes; a real source always wins. predicted_epc: Optional[EpcPropertyData] = None + # Resolved Landlord Overrides as Simulation Overlays, folded onto the lodged + # EPC to form the Effective EPC (ADR-0032). Empty when the Property has no + # overrides — the EPC is then returned unchanged. Only applied on the + # `epc_with_overlay` path; never when Site Notes are the source. + landlord_overrides: Sequence[EpcSimulation] = field(default_factory=tuple) # The current open-market value (a Property Valuation) — externally sourced # and mostly absent; feeds the Plan's Valuation Uplift £ forms (ADR-0018). current_market_value: Optional[float] = None @@ -78,10 +85,11 @@ class Property: def effective_epc(self) -> EpcPropertyData: """The EpcPropertyData the modelling pipeline scores against. - Path 1: the Site Notes' surveyed data. Path 2: the public EPC (Landlord - Overrides overlay is a later slice — returned as-is for now). Path 3: a - neighbour-synthesised EPC (EPC Prediction gap-fill, ADR-0031), used only - when neither real source is present. + Path 1: the Site Notes' surveyed data. Path 2: the public EPC with any + Landlord Overrides folded on as Simulation Overlays (ADR-0032) — returned + as-is when there are none. Path 3: a neighbour-synthesised EPC (EPC + Prediction gap-fill, ADR-0031), used only when neither real source is + present. """ if self.source_path == "site_notes": assert self.site_notes is not None @@ -90,4 +98,6 @@ class Property: assert self.predicted_epc is not None return self.predicted_epc assert self.epc is not None + if self.landlord_overrides: + return apply_simulations(self.epc, self.landlord_overrides) return self.epc diff --git a/tests/domain/property/test_property_landlord_overlay.py b/tests/domain/property/test_property_landlord_overlay.py new file mode 100644 index 00000000..d4006b1b --- /dev/null +++ b/tests/domain/property/test_property_landlord_overlay.py @@ -0,0 +1,82 @@ +"""Effective EPC on the `epc_with_overlay` path folds Landlord Overrides (ADR-0032). + +When a Property has a lodged EPC and resolved Landlord Overrides (as Simulation +Overlays), the Effective EPC is the lodged EPC with those overlays applied — so +the calculator scores what the landlord knows beyond the cert. With no overrides +the lodged EPC is returned unchanged. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from datatypes.epc.domain.epc_property_data import ( + BuildingPartIdentifier, + EpcPropertyData, +) +from domain.epc.wall_type_overlay import wall_overlay_for +from domain.property.property import Property, PropertyIdentity + +_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() + ) + from datatypes.epc.domain.mapper import EpcPropertyDataMapper + + return EpcPropertyDataMapper.from_api_response(raw) + + +def _identity() -> PropertyIdentity: + return PropertyIdentity( + portfolio_id=1, postcode="A0 0AA", address="1 Some Street", uprn=12345 + ) + + +def _main_wall(epc: EpcPropertyData) -> Any: + return next( + part + for part in epc.sap_building_parts + if part.identifier is BuildingPartIdentifier.MAIN + ) + + +def test_effective_epc_folds_the_wall_override_onto_the_main_part() -> None: + # Arrange — a Property with a lodged EPC and a solid-brick/internal-insulation + # wall override. + overlay = wall_overlay_for("Solid brick, with internal insulation", 0) + assert overlay is not None + prop = Property(identity=_identity(), epc=_epc(), landlord_overrides=[overlay]) + + # Act + main = _main_wall(prop.effective_epc) + + # Assert — the override's codes are present on the main wall. + assert main.wall_construction == 3 + assert main.wall_insulation_type == 3 + + +def test_effective_epc_is_the_lodged_epc_when_there_are_no_overrides() -> None: + # Arrange — a Property with an EPC and no Landlord Overrides. + prop = Property(identity=_identity(), epc=_epc()) + + # Act + effective = prop.effective_epc + + # Assert — the lodged EPC is returned untouched (same object, no fold). + assert effective is prop.epc + + +def test_baseline_wall_is_unchanged_when_no_override_applies() -> None: + # Arrange — the lodged main wall is cavity (construction 4). + prop = Property(identity=_identity(), epc=_epc()) + + # Act + main = _main_wall(prop.effective_epc) + + # Assert + assert main.wall_construction == 4