mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Fold landlord overrides onto the lodged EPC to form the Effective EPC 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
db1e283b07
commit
6eaf7456c2
2 changed files with 98 additions and 6 deletions
|
|
@ -1,10 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional, Sequence
|
||||||
|
|
||||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
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
|
from domain.property.site_notes import SiteNotes
|
||||||
|
|
||||||
SourcePath = Literal["site_notes", "epc_with_overlay", "predicted"]
|
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
|
# 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.
|
# neither a lodged EPC nor Site Notes; a real source always wins.
|
||||||
predicted_epc: Optional[EpcPropertyData] = None
|
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
|
# The current open-market value (a Property Valuation) — externally sourced
|
||||||
# and mostly absent; feeds the Plan's Valuation Uplift £ forms (ADR-0018).
|
# and mostly absent; feeds the Plan's Valuation Uplift £ forms (ADR-0018).
|
||||||
current_market_value: Optional[float] = None
|
current_market_value: Optional[float] = None
|
||||||
|
|
@ -78,10 +85,11 @@ class Property:
|
||||||
def effective_epc(self) -> EpcPropertyData:
|
def effective_epc(self) -> EpcPropertyData:
|
||||||
"""The EpcPropertyData the modelling pipeline scores against.
|
"""The EpcPropertyData the modelling pipeline scores against.
|
||||||
|
|
||||||
Path 1: the Site Notes' surveyed data. Path 2: the public EPC (Landlord
|
Path 1: the Site Notes' surveyed data. Path 2: the public EPC with any
|
||||||
Overrides overlay is a later slice — returned as-is for now). Path 3: a
|
Landlord Overrides folded on as Simulation Overlays (ADR-0032) — returned
|
||||||
neighbour-synthesised EPC (EPC Prediction gap-fill, ADR-0031), used only
|
as-is when there are none. Path 3: a neighbour-synthesised EPC (EPC
|
||||||
when neither real source is present.
|
Prediction gap-fill, ADR-0031), used only when neither real source is
|
||||||
|
present.
|
||||||
"""
|
"""
|
||||||
if self.source_path == "site_notes":
|
if self.source_path == "site_notes":
|
||||||
assert self.site_notes is not None
|
assert self.site_notes is not None
|
||||||
|
|
@ -90,4 +98,6 @@ class Property:
|
||||||
assert self.predicted_epc is not None
|
assert self.predicted_epc is not None
|
||||||
return self.predicted_epc
|
return self.predicted_epc
|
||||||
assert self.epc is not None
|
assert self.epc is not None
|
||||||
|
if self.landlord_overrides:
|
||||||
|
return apply_simulations(self.epc, self.landlord_overrides)
|
||||||
return self.epc
|
return self.epc
|
||||||
|
|
|
||||||
82
tests/domain/property/test_property_landlord_overlay.py
Normal file
82
tests/domain/property/test_property_landlord_overlay.py
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Reference in a new issue