Map oil and solid-fuel boilers to their SAP codes and companion set 🟥

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-24 09:14:33 +00:00
parent 55ab5bfb80
commit 04774a8888
2 changed files with 119 additions and 0 deletions

View file

@ -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"

View file

@ -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