test: portfolio-796 override-rebaseline regression (the 7 properties) 🟩

Pin the hit-list properties: each property's real Landlord Override set still
resolves to overlays and trips physical_state_changed, and the rebaseliner
adopts the calculator output (reason physical_state_changed/both) rather than
echoing the lodged headline — so the displayed baseline and the modelled plan
agree. Scores a real cert through the live Sap10Calculator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-26 19:22:32 +00:00
parent 4d5504fa10
commit c776341e03

View file

@ -0,0 +1,192 @@
"""Regression: portfolio-796 properties whose Landlord Overrides moved them off
their lodged headline must rebaseline off the calculator (not echo the lodged
figure).
These seven properties (a hit-list Khalim raised) each displayed an Effective
baseline equal to their *lodged accredited* SAP band B/C while the modelling
plan modelled the override-applied picture (often a different band), producing an
incoherent "already B, post-works C/D" plan. The cause: the rebaseliner kept the
lodged figure for any SAP >= 10.2 cert, ignoring the overrides (and, for the
predicted ones, the synthetic headline). The fix: when a physical-state trigger
fired Landlord Overrides or Prediction the calculator output IS the Effective
baseline, so the displayed baseline and the plan agree.
This test pins, per property, that (1) its real override set still resolves to
overlays and trips `physical_state_changed`, and (2) the rebaseliner adopts the
calculator output (reason physical_state_changed / both), not the lodged figure.
A representative end-to-end case scores a real cert through the live calculator.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
import pytest
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.property.property import Property, PropertyIdentity
from domain.property_baseline.calculator_rebaseliner import CalculatorRebaseliner
from domain.property_baseline.performance import lodged_performance
from domain.sap10_calculator.calculator import Sap10Calculator
from repositories.property.landlord_override_overlays import overlays_from
from repositories.property.property_overrides_reader import (
ResolvedPropertyOverride,
ResolvedPropertyOverrides,
)
_JSON_SAMPLES = Path(__file__).resolve().parents[3] / "backend/epc_api/json_samples"
def _cert() -> EpcPropertyData:
"""A real, scorable lodged cert used as the base picture (and, for the
predicted cases, as a stand-in synthesised EPC)."""
raw: dict[str, Any] = json.loads(
(_JSON_SAMPLES / "RdSAP-Schema-21.0.0" / "epc.json").read_text()
)
return EpcPropertyDataMapper.from_api_response(raw)
def _overrides(*pairs: tuple[str, str]) -> ResolvedPropertyOverrides:
return ResolvedPropertyOverrides(
rows=tuple(
ResolvedPropertyOverride(
override_component=component, building_part=0, override_value=value
)
for component, value in pairs
)
)
@dataclass(frozen=True)
class _Case:
pid: int
source: Literal["lodged", "predicted"]
overrides: ResolvedPropertyOverrides
# The seven properties' real Landlord Override sets (as stored in
# property_overrides), captured verbatim from portfolio 796.
_CASES: tuple[_Case, ...] = (
_Case(725634, "lodged", _overrides(
("main_heating_system", "Electric storage heaters, slimline"),
("wall_type", "Cavity wall, as built, no insulation (assumed)"),
("main_fuel", "electricity"),
("water_heating", "Electric immersion, electricity"),
("glazing", "Double glazing, pre-2002"),
("property_type", "House"),
("built_form_type", "Mid-Terrace"),
("construction_age_band", "E"),
)),
_Case(721534, "lodged", _overrides(
("main_heating_system", "Gas boiler, combi"),
("wall_type", "System built, as built, no insulation (assumed)"),
("main_fuel", "mains gas"),
("water_heating", "From main system, mains gas"),
("glazing", "Double glazing, 2002 or later"),
("property_type", "Flat"),
("built_form_type", "Mid-Terrace"),
("construction_age_band", "K"),
)),
_Case(721985, "predicted", _overrides(
("main_heating_system", "Gas boiler, combi"),
("wall_type", "Solid brick, as built, no insulation (assumed)"),
("main_fuel", "mains gas"),
("water_heating", "From main system, mains gas"),
("glazing", "Double glazing, 2002 or later"),
("property_type", "Maisonette"),
("built_form_type", "Semi-Detached"),
("construction_age_band", "B"),
)),
_Case(721993, "predicted", _overrides(
("main_heating_system", "Gas boiler, combi"),
("wall_type", "Solid brick, as built, no insulation (assumed)"),
("main_fuel", "mains gas"),
("water_heating", "From main system, mains gas"),
("glazing", "Single glazing"),
("property_type", "Maisonette"),
("built_form_type", "Semi-Detached"),
("construction_age_band", "B"),
)),
_Case(735096, "predicted", _overrides(
("main_heating_system", "Gas boiler, regular"),
("wall_type", "Cavity wall, as built, insulated (assumed)"),
("main_fuel", "electricity"),
("water_heating", "Electric immersion, electricity"),
("glazing", "Double glazing, 2002 or later"),
("property_type", "Flat"),
("built_form_type", "Enclosed Mid-Terrace"),
("construction_age_band", "K"),
)),
_Case(735220, "predicted", _overrides(
("main_heating_system", "Gas boiler, regular"),
("wall_type", "Cavity wall, as built, insulated (assumed)"),
("main_fuel", "electricity"),
("water_heating", "Electric immersion, electricity"),
("glazing", "Double glazing, 2002 or later"),
("property_type", "Flat"),
("built_form_type", "Enclosed Mid-Terrace"),
("construction_age_band", "J"),
)),
_Case(739060, "predicted", _overrides(
("main_heating_system", "Gas CPSU"),
("wall_type", "System built, as built, no insulation (assumed)"),
("main_fuel", "mains gas (community)"),
("water_heating", "From main system, mains gas"),
("glazing", "Double glazing, pre-2002"),
("property_type", "Flat"),
("built_form_type", "Mid-Terrace"),
("construction_age_band", "K"),
)),
)
def _property_for(case: _Case) -> Property:
overlays = overlays_from(case.overrides)
identity = PropertyIdentity(
portfolio_id=796, postcode="A0 0AA", address="", uprn=case.pid
)
if case.source == "lodged":
return Property(identity=identity, epc=_cert(), landlord_overrides=overlays)
return Property(
identity=identity, epc=None, predicted_epc=_cert(), landlord_overrides=overlays
)
@pytest.mark.parametrize("case", _CASES, ids=lambda c: str(c.pid))
def test_override_set_resolves_and_trips_physical_state_changed(case: _Case) -> None:
# The stored override values must still resolve to overlays (a renamed enum
# value would silently stop overriding), and the resulting Property must trip
# the Rebaselining trigger (b)/(c) — the signal that stops the baseline
# echoing the lodged headline.
overlays = overlays_from(case.overrides)
assert overlays, f"property {case.pid}: no override resolved to an overlay"
assert _property_for(case).physical_state_changed is True
@pytest.mark.parametrize("case", _CASES, ids=lambda c: str(c.pid))
def test_property_rebaselines_off_the_calculator_not_the_lodged_headline(
case: _Case,
) -> None:
# The load-bearing guarantee: with a physical-state change, the Effective
# baseline is the calculator's scoring of EPC + overrides — so the displayed
# baseline and the modelled plan derive from the same picture and agree.
prop = _property_for(case)
effective_epc = prop.effective_epc
lodged = lodged_performance(effective_epc)
rebaseliner = CalculatorRebaseliner(Sap10Calculator())
result = rebaseliner.rebaseline(
case.pid,
effective_epc,
lodged,
physical_state_changed=prop.physical_state_changed,
)
assert result.reason in ("physical_state_changed", "both")
assert result.sap_result is not None
# Effective is the calculator's output, not the lodged accredited headline.
assert result.effective.sap_score == result.sap_result.sap_score