mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
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:
parent
4d5504fa10
commit
c776341e03
1 changed files with 192 additions and 0 deletions
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue