From da805bce21b5c6cd8f47bde45ef5b38a74b15138 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 30 Jun 2026 22:10:57 +0000 Subject: [PATCH] =?UTF-8?q?Persist=20no=20Lodged=20Performance=20for=20a?= =?UTF-8?q?=20predicted=20Property=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PropertyBaselineOrchestrator now reads no Lodged Performance off a predicted Property's neighbour-synthesised EPC: lodged is None when source_path is 'predicted', so no phantom lodged figure is manufactured. The Effective half is unchanged. Rebaseliner port + PropertyBaselinePerformance.lodged widen to Optional; the pristine-cert paths assert non-None (lodged is None only for a predicted Property, which is always physical_state_changed). Co-Authored-By: Claude Opus 4.8 (1M context) --- domain/property_baseline/calculator_rebaseliner.py | 7 ++++++- .../property_baseline_performance.py | 7 +++++-- domain/property_baseline/rebaseliner.py | 14 +++++++++++--- orchestration/property_baseline_orchestrator.py | 12 ++++++++++-- .../test_property_baseline_orchestrator.py | 3 ++- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/domain/property_baseline/calculator_rebaseliner.py b/domain/property_baseline/calculator_rebaseliner.py index 04558ab75..0b9414fdf 100644 --- a/domain/property_baseline/calculator_rebaseliner.py +++ b/domain/property_baseline/calculator_rebaseliner.py @@ -57,7 +57,7 @@ class CalculatorRebaseliner(Rebaseliner): self, property_id: int, effective_epc: "EpcPropertyData", - lodged: Performance, + lodged: Optional[Performance], *, physical_state_changed: bool = False, ) -> RebaselineResult: @@ -87,6 +87,11 @@ class CalculatorRebaseliner(Rebaseliner): reason=reason, sap_result=result, ) + # The pristine-lodged path: not pre-SAP10 and not physical-state-changed, so + # there is a real lodged cert to validate against. ``lodged is None`` only + # happens for a predicted Property, which is physical_state_changed and so + # took the branch above — it never reaches here. + assert lodged is not None self._log_divergence( property_id=property_id, sap_version=sap_version, result=result, lodged=lodged ) diff --git a/domain/property_baseline/property_baseline_performance.py b/domain/property_baseline/property_baseline_performance.py index 6fee98582..a65289bca 100644 --- a/domain/property_baseline/property_baseline_performance.py +++ b/domain/property_baseline/property_baseline_performance.py @@ -15,7 +15,10 @@ class PropertyBaselinePerformance: Holds both halves — ``lodged`` (what the gov register says) and ``effective`` (what the modelling pipeline scored against) — plus the ``rebaseline_reason`` recording *why* they differ (``"none"`` when equal). - Both halves are always populated, even when equal. + Both halves are populated for a Property with a real record of itself; the + ``lodged`` half is ``None`` for a predicted Property, which has no lodged cert + to read a Lodged Performance off — only the Effective half is established then + (ADR-0004 amendment, #1361). Carries the part of the energy block that needs no derivation: annual ``space_heating_kwh`` / ``water_heating_kwh`` read off the EPC's RHI. @@ -25,7 +28,7 @@ class PropertyBaselinePerformance: ran (the stub path produced no ``SapResult`` to price). """ - lodged: Performance + lodged: Optional[Performance] effective: Performance rebaseline_reason: RebaselineReason space_heating_kwh: float diff --git a/domain/property_baseline/rebaseliner.py b/domain/property_baseline/rebaseliner.py index 9171b4bf6..daef0045c 100644 --- a/domain/property_baseline/rebaseliner.py +++ b/domain/property_baseline/rebaseliner.py @@ -62,11 +62,15 @@ class Rebaseliner(ABC): self, property_id: int, effective_epc: EpcPropertyData, - lodged: Performance, + lodged: Optional[Performance], *, physical_state_changed: bool = False, ) -> RebaselineResult: - """Produce Effective Performance. ``physical_state_changed`` is True when + """Produce Effective Performance. ``lodged`` is ``None`` for a predicted + Property — it has no lodged cert, so there is no Lodged Performance to + compare against (which is also why ``physical_state_changed`` is always + True there: the calculator output IS the Effective Performance). + ``physical_state_changed`` is True when the Effective EPC was assembled from something other than a pristine lodged cert — Landlord Overrides, Site Notes, or EPC Prediction moved the physical picture (Rebaselining trigger (b)/(c)) — so the accredited lodged @@ -91,7 +95,7 @@ class StubRebaseliner(Rebaseliner): self, property_id: int, effective_epc: EpcPropertyData, - lodged: Performance, + lodged: Optional[Performance], *, physical_state_changed: bool = False, ) -> RebaselineResult: @@ -108,4 +112,8 @@ class StubRebaseliner(Rebaseliner): "Property needs rebaselining (physical state changed by overrides " "/ prediction); this stub does not run the calculator" ) + # Only a pristine SAP10 cert reaches here, and that always has a Lodged + # Performance — ``lodged is None`` implies a predicted Property, which is + # ``physical_state_changed`` and so raised above. + assert lodged is not None return RebaselineResult(effective=lodged, reason="none", sap_result=None) diff --git a/orchestration/property_baseline_orchestrator.py b/orchestration/property_baseline_orchestrator.py index bc77605ff..0ee523479 100644 --- a/orchestration/property_baseline_orchestrator.py +++ b/orchestration/property_baseline_orchestrator.py @@ -8,7 +8,7 @@ from domain.billing.bill import EnergyBreakdown from domain.sap10_calculator.calculator import SapResult from domain.billing.bill_derivation import BillDerivation from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance -from domain.property_baseline.performance import lodged_performance +from domain.property_baseline.performance import Performance, lodged_performance from domain.property_baseline.rebaseliner import Rebaseliner from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository from repositories.unit_of_work import UnitOfWork @@ -50,7 +50,15 @@ class PropertyBaselineOrchestrator: properties = uow.property.get_many(property_ids) for property_id, prop in zip(property_ids, properties, strict=True): effective_epc = prop.effective_epc - lodged = lodged_performance(effective_epc) + # A predicted Property has no lodged cert — its Effective EPC is a + # neighbour-synthesised picture, so its recorded performance fields + # are a different dwelling's. There is no Lodged Performance to read + # (ADR-0004 amendment, #1361); only the Effective half is established. + lodged: Optional[Performance] = ( + None + if prop.source_path == "predicted" + else lodged_performance(effective_epc) + ) rebaselined = self._rebaseliner.rebaseline( property_id, effective_epc, diff --git a/tests/orchestration/test_property_baseline_orchestrator.py b/tests/orchestration/test_property_baseline_orchestrator.py index a848ddd22..912dc9085 100644 --- a/tests/orchestration/test_property_baseline_orchestrator.py +++ b/tests/orchestration/test_property_baseline_orchestrator.py @@ -161,10 +161,11 @@ class _ScoringRebaseliner(Rebaseliner): self, property_id: int, effective_epc: EpcPropertyData, - lodged: Performance, + lodged: Optional[Performance], *, physical_state_changed: bool = False, ) -> RebaselineResult: + assert lodged is not None # this stub is only used on the real-cert path return RebaselineResult( effective=lodged, reason="none", sap_result=self._result )