Persist no Lodged Performance for a predicted Property 🟩

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) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-30 22:10:57 +00:00
parent 5d63118d5a
commit da805bce21
5 changed files with 34 additions and 9 deletions

View file

@ -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
)

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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
)