mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
feat: thread physical-state-change signal into rebaselining 🟩
Add Property.physical_state_changed (true on Site Notes / Landlord Overrides / Prediction — trigger (b)/(c)) and pass it from the PropertyBaselineOrchestrator into the Rebaseliner. So an overridden or predicted SAP>=10.2 property now stores calc(effective) as its Effective baseline instead of echoing the lodged headline — closing the "81 in the DB" divergence between the displayed baseline and the modelled plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f7cfbf446
commit
4d5504fa10
4 changed files with 76 additions and 2 deletions
|
|
@ -84,6 +84,19 @@ class Property:
|
|||
"no source path to model from"
|
||||
)
|
||||
|
||||
@property
|
||||
def physical_state_changed(self) -> bool:
|
||||
"""True when the Effective EPC was assembled from something other than a
|
||||
pristine lodged cert — Site Notes superseded it, Landlord Overrides were
|
||||
folded on, or it was synthesised by EPC Prediction (Rebaselining trigger
|
||||
(b)/(c), CONTEXT.md). The Rebaseliner uses this to adopt the calculator's
|
||||
output over the accredited lodged figure even at SAP >= 10.2. A pristine
|
||||
lodged EPC with no overrides returns False."""
|
||||
path = self.source_path
|
||||
if path == "site_notes" or path == "predicted":
|
||||
return True
|
||||
return bool(self.landlord_overrides)
|
||||
|
||||
@property
|
||||
def effective_epc(self) -> EpcPropertyData:
|
||||
"""The EpcPropertyData the modelling pipeline scores against.
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ class PropertyBaselineOrchestrator:
|
|||
effective_epc = prop.effective_epc
|
||||
lodged = lodged_performance(effective_epc)
|
||||
rebaselined = self._rebaseliner.rebaseline(
|
||||
property_id, effective_epc, lodged
|
||||
property_id,
|
||||
effective_epc,
|
||||
lodged,
|
||||
physical_state_changed=prop.physical_state_changed,
|
||||
)
|
||||
# No SapResult (the stub path) means no scored picture to price,
|
||||
# so the bill stays None.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
"""`Property.physical_state_changed` — the Rebaselining trigger (b)/(c) signal.
|
||||
|
||||
True when the Effective EPC was assembled from something other than a pristine
|
||||
lodged cert: Landlord Overrides folded on, Site Notes superseding the cert, or an
|
||||
EPC-Prediction synthesis. The PropertyBaselineOrchestrator threads it into the
|
||||
Rebaseliner so an overridden / predicted SAP >= 10.2 picture rebaselines off the
|
||||
calculator instead of echoing the (now-stale) accredited lodged figure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.epc.property_overlays.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()
|
||||
)
|
||||
return EpcPropertyDataMapper.from_api_response(raw)
|
||||
|
||||
|
||||
def _identity() -> PropertyIdentity:
|
||||
return PropertyIdentity(
|
||||
portfolio_id=1, postcode="A0 0AA", address="1 Some Street", uprn=12345
|
||||
)
|
||||
|
||||
|
||||
def test_pristine_lodged_cert_with_no_overrides_is_unchanged() -> None:
|
||||
prop = Property(identity=_identity(), epc=_epc())
|
||||
assert prop.physical_state_changed is False
|
||||
|
||||
|
||||
def test_lodged_cert_with_landlord_overrides_is_changed() -> None:
|
||||
overlay = wall_overlay_for("Solid brick, with internal insulation", 0)
|
||||
assert overlay is not None
|
||||
prop = Property(identity=_identity(), epc=_epc(), landlord_overrides=[overlay])
|
||||
assert prop.physical_state_changed is True
|
||||
|
||||
|
||||
def test_predicted_property_is_changed() -> None:
|
||||
# A neighbour-synthesised picture is assembled, not a real lodged cert, so it
|
||||
# rebaselines off the calculator (trigger (c)).
|
||||
prop = Property(identity=_identity(), epc=None, predicted_epc=_epc())
|
||||
assert prop.physical_state_changed is True
|
||||
|
|
@ -158,7 +158,12 @@ class _ScoringRebaseliner(Rebaseliner):
|
|||
self._result = result
|
||||
|
||||
def rebaseline(
|
||||
self, property_id: int, effective_epc: EpcPropertyData, lodged: Performance
|
||||
self,
|
||||
property_id: int,
|
||||
effective_epc: EpcPropertyData,
|
||||
lodged: Performance,
|
||||
*,
|
||||
physical_state_changed: bool = False,
|
||||
) -> RebaselineResult:
|
||||
return RebaselineResult(
|
||||
effective=lodged, reason="none", sap_result=self._result
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue