diff --git a/tests/domain/epc/test_boiler_efficiency_band_overlay.py b/tests/domain/epc/test_boiler_efficiency_band_overlay.py new file mode 100644 index 000000000..bc4dd7b72 --- /dev/null +++ b/tests/domain/epc/test_boiler_efficiency_band_overlay.py @@ -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