diff --git a/domain/epc/property_overrides/main_heating_system_type.py b/domain/epc/property_overrides/main_heating_system_type.py index f8426fdcf..98d07a6e4 100644 --- a/domain/epc/property_overrides/main_heating_system_type.py +++ b/domain/epc/property_overrides/main_heating_system_type.py @@ -19,6 +19,15 @@ class MainHeatingSystemType(Enum): GAS_COMBI = "Gas boiler, combi" GAS_REGULAR = "Gas boiler, regular" GAS_CPSU = "Gas CPSU" + # Oil / solid-fuel wet boilers off the fuel-agnostic gas default (ADR-0067). + # A fuel-blind "Boiler … Regular/Combi" description on a non-gas main_fuel was + # funnelled onto the gas archetype; these give it a home, with the fuel column + # (`resolve_boiler_archetype`) re-pointing at resolution. Solid fuel is a single + # conservative member (SAP 151, 0.60); sub-fuels deferred. Lock-step with + # assessment-model#455 / migration 0279 (ADR-0002). + OIL_BOILER_REGULAR = "Oil boiler, regular" + OIL_BOILER_COMBI = "Oil boiler, combi" + SOLID_FUEL_BOILER = "Solid fuel boiler" ELECTRIC_STORAGE_OLD = "Electric storage heaters, old" ELECTRIC_STORAGE_SLIMLINE = "Electric storage heaters, slimline" ELECTRIC_STORAGE_CONVECTOR = "Electric storage heaters, convector" diff --git a/tests/domain/epc/test_main_heating_system_overlay.py b/tests/domain/epc/test_main_heating_system_overlay.py index dc50dd2d5..40be0a988 100644 --- a/tests/domain/epc/test_main_heating_system_overlay.py +++ b/tests/domain/epc/test_main_heating_system_overlay.py @@ -817,6 +817,116 @@ def test_every_resolvable_main_heating_value_decodes( assert simulation is not None +@pytest.mark.parametrize( + ("main_heating_value", "code"), + [ + # Oil / solid-fuel wet boilers off the fuel-agnostic gas default (ADR-0067). + # Modern/condensing codes (A-G deferred, like gas): SAP Table 4b 127 + # condensing oil regular, 130 condensing oil combi; Table 4a 151 manual-feed + # independent solid-fuel boiler (the conservative single member). + ("Oil boiler, regular", 127), + ("Oil boiler, combi", 130), + ("Solid fuel boiler", 151), + ], +) +def test_oil_and_solid_fuel_boilers_decode_to_their_boiler_codes( + main_heating_value: str, code: int +) -> None: + # Act + simulation = main_heating_overlay_for(main_heating_value, 0) + + # Assert — its own boiler code, not the mains-gas default. + assert simulation is not None + assert simulation.heating is not None + assert simulation.heating.sap_main_heating_code == code + + +@pytest.mark.parametrize( + ("main_heating_value", "fuel"), + [ + ("Oil boiler, regular", 28), # heating oil + ("Oil boiler, combi", 28), + ("Solid fuel boiler", 33), # house coal — the conservative solid default + ], +) +def test_oil_and_solid_fuel_boilers_drag_their_natural_fuel( + main_heating_value: str, fuel: int +) -> None: + # The archetype implies a fuel so a system-only override is self-coherent; the + # property's `main_fuel` override still wins by last-wins composition (ADR-0041). + + # Act + simulation = main_heating_overlay_for(main_heating_value, 0) + + # Assert + assert simulation is not None + assert simulation.heating is not None + assert simulation.heating.main_fuel_type == fuel + + +@pytest.mark.parametrize( + "main_heating_value", ["Oil boiler, regular", "Oil boiler, combi", "Solid fuel boiler"] +) +def test_fuel_boilers_drag_the_gas_pattern_boiler_companions( + main_heating_value: str, +) -> None: + # Modelled on the gas boiler (ADR-0067): SAP Table 4a category 2, full modern + # controls (Table 4e 2106), a single-rate meter — but NOT a forced gas + # connection (an oil/solid boiler needs no gas main, so it inherits the lodged + # EPC: `gas_connection_available` stays unset). + + # Act + simulation = main_heating_overlay_for(main_heating_value, 0) + + # Assert + assert simulation is not None + assert simulation.heating is not None + assert simulation.heating.main_heating_category == 2 + assert simulation.heating.main_heating_control == 2106 + assert simulation.heating.meter_type == "Single" + assert simulation.heating.gas_connection_available is None + + +@pytest.mark.parametrize( + ("main_heating_value", "has_cylinder"), + [ + ("Oil boiler, regular", True), + ("Oil boiler, combi", False), # a combi heats hot water instantaneously + ("Solid fuel boiler", True), # no solid-fuel combi exists + ], +) +def test_fuel_boiler_cylinder_follows_combi( + main_heating_value: str, has_cylinder: bool +) -> None: + # Act + simulation = main_heating_overlay_for(main_heating_value, 0) + + # Assert — hot water from the main system, with a cylinder unless a combi. + assert simulation is not None + assert simulation.heating is not None + assert simulation.heating.has_hot_water_cylinder is has_cylinder + + +@pytest.mark.parametrize( + ("main_heating_value", "fan_flue"), + [ + ("Oil boiler, regular", True), # modern condensing oil: room-sealed/fan flue + ("Oil boiler, combi", True), + ("Solid fuel boiler", False), # solid fuel: conventional (non-fanned) flue + ], +) +def test_fuel_boiler_fan_flue_follows_fuel( + main_heating_value: str, fan_flue: bool +) -> None: + # Act + simulation = main_heating_overlay_for(main_heating_value, 0) + + # Assert + assert simulation is not None + assert simulation.heating is not None + assert simulation.heating.fan_flue_present is fan_flue + + def test_solid_fuel_room_heater_decodes_to_the_closed_room_heater_code() -> None: # A landlord-named solid-fuel room heater (e.g. a closed stove) is a # recognised archetype, not a gas wet system — it must decode to its SAP