test(overlay): RED — band -> (winter,summer) slot on gas/oil boiler overlay

C-and-below reuse accredited Table 4b pairs; A/B pending Elmhurst (no slot yet,
no invented numbers); non-banded boilers (solid/electric/CPSU) ignore the band.
The base code is unchanged; only the efficiency slot is set (ADR-0068).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-29 16:06:43 +00:00
parent b41c25e756
commit 7a35d177b2

View file

@ -0,0 +1,124 @@
from __future__ import annotations
from typing import Optional
import pytest
from domain.epc.property_overrides.boiler_efficiency_band import BoilerEfficiencyBand
from domain.epc.property_overlays.main_heating_system_overlay import (
band_seasonal_efficiency_pct,
main_heating_overlay_for,
)
@pytest.mark.parametrize(
("base_code", "band", "expected"),
[
# Gas regular (base 102) — C-and-below reuse the accredited Table 4b
# (winter, summer) of the code the band maps to (ADR-0068).
(102, BoilerEfficiencyBand.C, (84.0, 74.0)), # code 102
(102, BoilerEfficiencyBand.D, (80.0, 70.0)), # code 106
(102, BoilerEfficiencyBand.E, (74.0, 64.0)), # code 101
(102, BoilerEfficiencyBand.F, (70.0, 60.0)), # code 105
(102, BoilerEfficiencyBand.G, (66.0, 56.0)), # code 115
# Gas combi (base 104).
(104, BoilerEfficiencyBand.C, (84.0, 75.0)), # code 104
(104, BoilerEfficiencyBand.D, (80.0, 71.0)), # code 108
(104, BoilerEfficiencyBand.G, (66.0, 57.0)), # code 118
# Oil regular (base 127) — E/F nearest-fit to code 125 (⚠ ADR-0068).
(127, BoilerEfficiencyBand.C, (84.0, 72.0)), # code 127
(127, BoilerEfficiencyBand.D, (80.0, 68.0)), # code 126
(127, BoilerEfficiencyBand.E, (71.0, 59.0)), # code 125
(127, BoilerEfficiencyBand.G, (66.0, 54.0)), # code 124
# Oil combi (base 130) — D/E to code 129, F/G to code 128 (⚠ ADR-0068).
(130, BoilerEfficiencyBand.C, (82.0, 73.0)), # code 130
(130, BoilerEfficiencyBand.D, (77.0, 68.0)), # code 129
(130, BoilerEfficiencyBand.F, (71.0, 62.0)), # code 128
],
)
def test_c_and_below_bands_resolve_to_the_table_4b_pair(
base_code: int, band: BoilerEfficiencyBand, expected: tuple[float, float]
) -> None:
assert band_seasonal_efficiency_pct(base_code, band) == expected
@pytest.mark.parametrize("band", [BoilerEfficiencyBand.A, BoilerEfficiencyBand.B])
@pytest.mark.parametrize("base_code", [102, 104, 127, 130])
def test_a_and_b_bands_are_pending_elmhurst_validation(
base_code: int, band: BoilerEfficiencyBand
) -> None:
# A/B exceed Table 4b's 84% ceiling — they are judgement values that MUST be
# pinned against an accredited Elmhurst build before they are enabled
# (ADR-0068 gated pre-req). Until then: no slot, so an A/B boiler keeps its
# condensing code default (no invented numbers ship).
assert band_seasonal_efficiency_pct(base_code, band) is None
@pytest.mark.parametrize(
"base_code",
[
151, # Solid fuel boiler — Table 4a 75%, intrinsic (SEDBUK doesn't rate it)
191, # Electric boiler — Table 4a 100%, intrinsic
192, # Electric CPSU — intrinsic electric
120, # Gas CPSU — non-condensing, not SEDBUK-banded
],
)
@pytest.mark.parametrize("band", list(BoilerEfficiencyBand))
def test_non_banded_boilers_never_take_a_slot(
base_code: int, band: BoilerEfficiencyBand
) -> None:
assert band_seasonal_efficiency_pct(base_code, band) is None
@pytest.mark.parametrize(
("archetype", "band", "expected"),
[
("Gas boiler, regular", BoilerEfficiencyBand.D, (80.0, 70.0)),
("Gas boiler, combi", BoilerEfficiencyBand.G, (66.0, 57.0)),
("Oil boiler, regular", BoilerEfficiencyBand.E, (71.0, 59.0)),
("Oil boiler, combi", BoilerEfficiencyBand.D, (77.0, 68.0)),
],
)
def test_overlay_sets_the_band_efficiency_slot_on_gas_and_oil_boilers(
archetype: str, band: BoilerEfficiencyBand, expected: tuple[float, float]
) -> None:
simulation = main_heating_overlay_for(archetype, 0, band)
assert simulation is not None
assert simulation.heating is not None
# The base code is unchanged (it still drives combi/regular HW behaviour);
# only the efficiency slot is set from the band.
assert simulation.heating.seasonal_efficiency_override_pct == expected
@pytest.mark.parametrize(
("archetype", "band"),
[
# No band → no slot (today's behaviour: keep the condensing code default).
("Gas boiler, combi", None),
# A/B pending Elmhurst → no slot yet.
("Gas boiler, combi", BoilerEfficiencyBand.A),
# C on gas regular equals the code default — still set, but a no-op value.
# Non-banded boilers ignore the band letter entirely.
("Electric boiler", BoilerEfficiencyBand.D),
("Solid fuel boiler", BoilerEfficiencyBand.G),
("Gas CPSU", BoilerEfficiencyBand.D),
],
)
def test_overlay_sets_no_slot_when_band_absent_or_not_applicable(
archetype: str, band: Optional[BoilerEfficiencyBand]
) -> None:
simulation = main_heating_overlay_for(archetype, 0, band)
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.seasonal_efficiency_override_pct is None
def test_overlay_defaults_to_no_band_when_not_supplied() -> None:
# Back-compat: the band arg is optional; existing callers pass none.
simulation = main_heating_overlay_for("Gas boiler, combi", 0)
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.seasonal_efficiency_override_pct is None