Model/tests/domain/epc/test_main_fuel_overlay.py
Jun-te Kim a680d65188 fix(overlay): synthesise a coherent gas-boiler heating system on Landlord Override
A Landlord heating-system override was applied as a sparse patch, so the
replaced system's fields bled through. A storage flat reclassified as a gas
combi (property 728513) kept mains_gas=False, heating category 7, the 2401
storage charge control, a Dual meter and an electric-immersion cylinder — an
incoherent record that gated out the gas-boiler-upgrade Measure and made the
heating Generator read the dwelling as off-gas (offering HHRSH storage).

Extend the ADR-0035 drag-along to gas boilers (Table 4b 102/104/120): the
overlay now sets the whole coherent companion set — mains_gas, gas main fuel,
heating category 2, fanned flue, full modern controls (2106), a single-rate
meter, and hot water from the main system with the cylinder set from the boiler
type (combi → none, regular/CPSU → cylinder). The main_fuel overlay also flips
mains_gas=True for a "mains gas" fuel. Non-off-peak archetypes now drag an
explicit Single meter so a system switch never leaves a stale Dual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 12:48:33 +00:00

144 lines
4.8 KiB
Python

"""The Landlord-Override main-fuel → heating Simulation Overlay mapping.
A main-fuel value resolves to the RdSAP `main_fuel_type` int code the calculator
reads from the dwelling's primary heating system; the overlay is whole-dwelling.
"""
from __future__ import annotations
import pytest
from domain.epc.property_overrides.main_fuel_type import MainFuelType
from domain.epc.property_overlays.main_fuel_overlay import fuel_overlay_for
from domain.modelling.scoring.overlay_applicator import apply_simulations
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
build_epc,
)
def test_mains_gas_overlays_the_primary_fuel() -> None:
# Act
simulation = fuel_overlay_for("mains gas", 0)
# Assert — mains gas (not community) is RdSAP main_fuel code 26.
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_fuel_type == 26
@pytest.mark.parametrize(
("main_fuel_value", "code"),
[
("electricity", 29),
("LPG (bulk)", 27),
("oil", 28),
("house coal", 33),
],
)
def test_fuels_decode_to_their_modern_not_community_codes(
main_fuel_value: str, code: int
) -> None:
# Act
simulation = fuel_overlay_for(main_fuel_value, 0)
# Assert
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_fuel_type == code
@pytest.mark.parametrize(
("main_fuel_value", "code"),
[
("bottled LPG", 3),
("LPG special condition", 17),
("electricity (community)", 25),
("biomass (community)", 31),
("dual fuel (mineral and wood)", 10),
("smokeless coal", 15),
],
)
def test_more_fuels_decode_to_their_codes(main_fuel_value: str, code: int) -> None:
# Act
simulation = fuel_overlay_for(main_fuel_value, 0)
# Assert
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_fuel_type == code
def test_community_mains_gas_is_a_distinct_fuel_code() -> None:
# Act
simulation = fuel_overlay_for("mains gas (community)", 0)
# Assert — community mains gas is code 20, distinct from 26 (not community).
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_fuel_type == 20
def test_mains_gas_fuel_sets_the_mains_gas_connection_flag() -> None:
# A "mains gas" fuel means the dwelling has a mains-gas connection, so the
# overlay must set sap_energy_source.mains_gas too — not only the fuel code.
# Without it the effective EPC says "fuel = mains gas" yet mains_gas=False,
# which suppresses the gas-boiler-upgrade path and wrongly offers HHRSH
# storage (the off-gas path). (Property 728513.)
simulation = fuel_overlay_for("mains gas", 0)
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.mains_gas is True
@pytest.mark.parametrize(
"main_fuel_value",
["electricity", "LPG (bulk)", "bottled LPG", "oil", "house coal"],
)
def test_non_mains_gas_fuel_leaves_the_mains_gas_flag_unchanged(
main_fuel_value: str,
) -> None:
# Only an explicit "mains gas" fuel asserts a mains-gas connection. An
# electric (or LPG/oil) main fuel does NOT tell us there is no gas supply
# (the dwelling could still have a gas hob), so the flag is left None
# ("leave the baseline unchanged") rather than cleared to False.
simulation = fuel_overlay_for(main_fuel_value, 0)
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.mains_gas is None
@pytest.mark.parametrize("main_fuel_value", ["Unknown", "", "no heating or hot water"])
def test_unresolvable_fuel_produces_no_overlay(main_fuel_value: str) -> None:
# Act
simulation = fuel_overlay_for(main_fuel_value, 0)
# Assert
assert simulation is None
def test_fuel_override_remaps_the_primary_systems_fuel_on_the_epc() -> None:
# Arrange — a landlord correction that the dwelling runs on electricity.
baseline = build_epc()
overlay = fuel_overlay_for("electricity", 0)
assert overlay is not None
# Act
result = apply_simulations(baseline, [overlay])
# Assert — the calculator reads the primary fuel from main_heating_details[0].
assert result.sap_heating.main_heating_details[0].main_fuel_type == 29
@pytest.mark.parametrize(
"member", [m for m in MainFuelType if m is not MainFuelType.UNKNOWN]
)
def test_every_resolvable_fuel_value_decodes_to_a_code(member: MainFuelType) -> None:
# A classifier emits a MainFuelType value; if the overlay can't decode it the
# override silently no-ops. Every non-UNKNOWN member must resolve.
# Act
simulation = fuel_overlay_for(member.value, 0)
# Assert
assert simulation is not None