diff --git a/domain/modelling/scoring/overlay_applicator.py b/domain/modelling/scoring/overlay_applicator.py index 73934779..11d6e562 100644 --- a/domain/modelling/scoring/overlay_applicator.py +++ b/domain/modelling/scoring/overlay_applicator.py @@ -90,6 +90,16 @@ def _fold_heating(epc: EpcPropertyData, overlay: HeatingOverlay) -> None: value = getattr(overlay, field_name) if value is not None: setattr(main, field_name, value) + # `main_heating_index_number` (PCDB-resolved, e.g. a heat pump) and + # `sap_main_heating_code` (Table 4a-resolved, e.g. storage heaters) are + # mutually-exclusive efficiency anchors: a whole-system replacement to one + # must clear the other, else a stale code from the old system wins the + # calculator's dispatch (e.g. a gas-boiler code 104 left beside a heat-pump + # index makes hot water use boiler efficiency, not the HP SCOP). + if overlay.main_heating_index_number is not None: + main.sap_main_heating_code = None + elif overlay.sap_main_heating_code is not None: + main.main_heating_index_number = None for field_name in _SAP_HEATING_FIELDS: value = getattr(overlay, field_name) if value is not None: diff --git a/tests/domain/modelling/fixtures/ashp_001431_after.pdf b/tests/domain/modelling/fixtures/ashp_001431_after.pdf new file mode 100644 index 00000000..538a8ed2 Binary files /dev/null and b/tests/domain/modelling/fixtures/ashp_001431_after.pdf differ diff --git a/tests/domain/modelling/fixtures/ashp_from_gas_boiler_001431_before.pdf b/tests/domain/modelling/fixtures/ashp_from_gas_boiler_001431_before.pdf new file mode 100644 index 00000000..f67db819 Binary files /dev/null and b/tests/domain/modelling/fixtures/ashp_from_gas_boiler_001431_before.pdf differ diff --git a/tests/domain/modelling/test_elmhurst_cascade_pins.py b/tests/domain/modelling/test_elmhurst_cascade_pins.py index 7410d969..50c7f7cd 100644 --- a/tests/domain/modelling/test_elmhurst_cascade_pins.py +++ b/tests/domain/modelling/test_elmhurst_cascade_pins.py @@ -620,3 +620,24 @@ def test_hhr_storage_overlay_reproduces_the_relodged_after_from_no_system() -> N # Act / Assert _assert_overlay_reproduces_after(before, after, option.overlay) + + +def test_ashp_overlay_reproduces_the_relodged_after_from_a_gas_boiler() -> None: + # Arrange — a typical mains-gas combi house re-lodged as an air-source heat + # pump (fuel 26 -> 30, SAP code 104 -> PCDB index 101413 + category 4, + # control 2106 -> 2210), off mains gas, gaining a heat-pump cylinder + # (ADR-0024). + before: EpcPropertyData = parse_recommendation_summary( + "ashp_from_gas_boiler_001431_before.pdf" + ) + after: EpcPropertyData = parse_recommendation_summary( + "ashp_001431_after.pdf" + ) + recommendation: Recommendation | None = recommend_heating(before, _AnyProduct()) + assert recommendation is not None + option = next( + o for o in recommendation.options if o.measure_type == "air_source_heat_pump" + ) + + # Act / Assert + _assert_overlay_reproduces_after(before, after, option.overlay) diff --git a/tests/domain/modelling/test_overlay_applicator.py b/tests/domain/modelling/test_overlay_applicator.py index 16228ec5..75d2c901 100644 --- a/tests/domain/modelling/test_overlay_applicator.py +++ b/tests/domain/modelling/test_overlay_applicator.py @@ -327,11 +327,54 @@ def test_baseline_heating_is_not_mutated_by_a_heating_overlay() -> None: assert baseline.sap_energy_source.mains_gas == original_mains_gas +def test_heating_index_overlay_clears_a_stale_sap_main_heating_code() -> None: + # Arrange — 000490's gas combi lodges a Table 4a code; an ASHP bundle sets a + # PCDB index instead. The two are mutually-exclusive efficiency anchors, so + # the stale code must be cleared or it wins the calculator's dispatch. + baseline: EpcPropertyData = build_epc() + baseline.sap_heating.main_heating_details[0].sap_main_heating_code = 104 + + # Act + result: EpcPropertyData = apply_simulations( + baseline, + [ + EpcSimulation( + heating=HeatingOverlay( + main_heating_index_number=101413, main_heating_category=4 + ) + ) + ], + ) + + # Assert — the index is set and the old SAP code is gone. + main = result.sap_heating.main_heating_details[0] + assert main.main_heating_index_number == 101413 + assert main.sap_main_heating_code is None + + +def test_heating_sap_code_overlay_clears_a_stale_index() -> None: + # Arrange — a dwelling with a PCDB-indexed system; an HHR storage bundle sets + # a Table 4a code instead, so the stale index must be cleared. + baseline: EpcPropertyData = build_epc() + baseline.sap_heating.main_heating_details[0].main_heating_index_number = 8262 + + # Act + result: EpcPropertyData = apply_simulations( + baseline, + [EpcSimulation(heating=HeatingOverlay(sap_main_heating_code=409))], + ) + + # Assert + main = result.sap_heating.main_heating_details[0] + assert main.sap_main_heating_code == 409 + assert main.main_heating_index_number is None + + def test_baseline_lighting_is_not_mutated_by_a_lighting_overlay() -> None: # Arrange — 000490 lodges 8 low-energy-unknown bulbs, 0 LED. baseline: EpcPropertyData = build_epc() original_led: int = baseline.led_fixed_lighting_bulbs_count - original_lel: int = baseline.low_energy_fixed_lighting_bulbs_count + original_lel: int | None = baseline.low_energy_fixed_lighting_bulbs_count # Act — fold an all-LED overlay (led = the 8 total). _: EpcPropertyData = apply_simulations(