mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
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>
This commit is contained in:
parent
015ea0a293
commit
a680d65188
5 changed files with 281 additions and 34 deletions
|
|
@ -44,7 +44,57 @@ A real lodged cert is scored exactly as lodged — the calculator must not "fix"
|
|||
genuinely single-rate storage dwelling (its existing Unknown-meter inference is
|
||||
a separate, spec-faithful net for certs that lodged *Unknown*, and does not fire
|
||||
on an explicit meter). A contract test guards the override path: every off-peak
|
||||
code the archetype map can emit must drag a `Dual` meter.
|
||||
code the archetype map can emit must drag a `Dual` meter, and every other code an
|
||||
explicit `Single` meter — a system switch can never leave the previous system's
|
||||
meter in place.
|
||||
|
||||
### Amendment: the gas-boiler companion set (property 728513)
|
||||
|
||||
The storage-heater drag-along above covered electric systems; a landlord-named
|
||||
**gas boiler** was still synthesised as a sparse patch — only the SAP code (its
|
||||
meter/charge-control companions are `None` for a boiler). The replaced system's
|
||||
fields therefore bled through: an electric-storage flat reclassified as a gas
|
||||
combi kept `mains_gas=False`, heating category 7, the 2401 storage charge
|
||||
control, a `Dual` meter and an electric-immersion cylinder. That incoherent
|
||||
record gated out the gas-boiler-upgrade Measure and made the heating Generator
|
||||
read the dwelling as off-gas (offering HHRSH storage to a gas property).
|
||||
|
||||
A gas-boiler archetype (Table 4b 102 regular / 104 combi / 120 CPSU) now drags
|
||||
its **whole** coherent set, code-derived like the storage companions:
|
||||
|
||||
- **`mains_gas=True` + `main_fuel_type=26`** — a gas boiler implies a mains-gas
|
||||
connection and gas main fuel (so a heating-system-only override is
|
||||
self-coherent, not dependent on a companion `main_fuel` override). The
|
||||
`main_fuel` overlay independently sets `mains_gas=True` for a "mains gas" fuel;
|
||||
the two agree (both 26 / True) so composition stays order-independent. A
|
||||
non-gas `main_fuel` override (electricity/LPG/oil) leaves `mains_gas` **None**
|
||||
(unchanged) — an electric main heating does not prove there is no gas hob.
|
||||
- **`main_heating_category=2`** (gas boiler) and **`fan_flue_present=True`** — a
|
||||
modern room-sealed condensing boiler.
|
||||
- **`main_heating_control=2106`** (programmer + room thermostat + TRVs) — the
|
||||
landlord names the boiler, not its controls. Unlike the storage charge control
|
||||
(where the *conservative* lowest-SAP default is right because the control is
|
||||
genuinely unknown), a gas boiler installed under modern Building Regs must
|
||||
carry compliant controls, and the overlay already assumes the modern condensing
|
||||
efficiency for the boiler — so **assuming modern controls is the coherent,
|
||||
consistent default** (the alternative, a conservative "no controls" 2101, was
|
||||
considered and rejected as inconsistent with the assumed-modern boiler). The
|
||||
controls tune-up Measure still adds zone control on top.
|
||||
- **Hot water from the main system** (`water_heating_code=901`,
|
||||
`water_heating_fuel=26`) with the cylinder set from the boiler type — a combi
|
||||
(104) has **no** cylinder (`has_hot_water_cylinder=False`), a regular boiler /
|
||||
CPSU keeps one. (Mirrors Elmhurst: the cylinder choice is offered for a regular
|
||||
condensing boiler and greyed out for a combi.)
|
||||
- **`meter_type="Single"`** — a boiler is single-rate; the explicit Single meter
|
||||
overwrites any inherited storage `Dual` (the symmetric mirror of storage→Dual).
|
||||
|
||||
A `mains_gas` flag was added to the `main_fuel` overlay. The companion-fuel and
|
||||
heating-system overlays now share the `mains_gas` / `main_fuel_type` fields
|
||||
rather than being strictly disjoint — accepted because they only ever agree for
|
||||
the mains-gas archetypes. The archetype map holds only mains-gas boilers, so a
|
||||
gas-boiler override always implies mains gas; a *contradictory* fuel override
|
||||
(gas boiler + a non-gas fuel) is out-of-scope incoherent landlord input (a
|
||||
finaliser-level validation, not an overlay concern).
|
||||
|
||||
## Considered Options
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,19 @@ _FUEL_CODES: dict[str, int] = {
|
|||
"biomass (community)": 31,
|
||||
}
|
||||
|
||||
# A "mains gas" main fuel asserts the dwelling has a mains-gas connection, so the
|
||||
# overlay must also flip `sap_energy_source.mains_gas` — not just the fuel code.
|
||||
# Without it the effective EPC says "fuel = mains gas" while `mains_gas` stays
|
||||
# False, which (a) gates out the gas-boiler-upgrade Measure and (b) makes the
|
||||
# heating Generator read the dwelling as off-gas and wrongly offer HHRSH storage
|
||||
# (property 728513). Only the **private** mains-gas connection (code 26) sets it;
|
||||
# community mains gas (code 20) is a heat network, not a gas-grid connection.
|
||||
# Non-gas fuels leave the flag None ("unchanged") rather than clearing it: an
|
||||
# electric (or LPG/oil) main fuel does not tell us there is no gas supply (a
|
||||
# dwelling can heat electrically yet still have a gas hob), so only electrifying
|
||||
# Measures clear `mains_gas`, never a Landlord fuel correction.
|
||||
_MAINS_GAS_FUEL_VALUES: frozenset[str] = frozenset({"mains gas"})
|
||||
|
||||
|
||||
def fuel_overlay_for(
|
||||
main_fuel_value: str, building_part: int
|
||||
|
|
@ -38,4 +51,7 @@ def fuel_overlay_for(
|
|||
code = _FUEL_CODES.get(main_fuel_value)
|
||||
if code is None:
|
||||
return None
|
||||
return EpcSimulation(heating=HeatingOverlay(main_fuel_type=code))
|
||||
mains_gas = True if main_fuel_value in _MAINS_GAS_FUEL_VALUES else None
|
||||
return EpcSimulation(
|
||||
heating=HeatingOverlay(main_fuel_type=code, mains_gas=mains_gas)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ from domain.sap10_calculator.tables.table_12a import (
|
|||
# the low rate and cannot run economically on a single-rate meter; "Dual" lets
|
||||
# the §12 dispatch resolve the specific tariff (storage 7-hour, CPSU 10-hour).
|
||||
_OFF_PEAK_METER = "Dual"
|
||||
# Single-rate meter (SAP 10.2 Table 12a code 2 → STANDARD tariff). Every non-off-
|
||||
# peak archetype synthesises this *explicitly* rather than leaving the meter
|
||||
# untouched: switching OFF storage heaters must not let the dwelling's old "Dual"
|
||||
# meter bleed through and bill the new gas/direct-acting system on an Economy-7
|
||||
# split (the mirror of the storage→Dual drag, ADR-0035).
|
||||
_SINGLE_RATE_METER = "Single"
|
||||
|
||||
# SAP Table 4e Group 4 storage charge-control code. Manual charge control is the
|
||||
# *conservative* assumption when the landlord didn't tell us the control: its
|
||||
|
|
@ -48,12 +54,35 @@ _OFF_PEAK_METER = "Dual"
|
|||
_MANUAL_CHARGE_CONTROL = 2401
|
||||
_STORAGE_HEATER_CODES = frozenset(range(401, 410))
|
||||
|
||||
# SAP Table 4c full boiler-control code: programmer + room thermostat + TRVs. The
|
||||
# landlord names the boiler, not its controls — but a gas boiler installed under
|
||||
# modern Building Regs must carry compliant controls, and this overlay already
|
||||
# assumes the modern condensing efficiency for the boiler (A-G deferred), so
|
||||
# assuming modern controls is the *coherent* default (decided with Khalim). It
|
||||
# also overwrites any stale storage charge control (2401) the dwelling carried
|
||||
# before the switch. (Conservative-vs-modern trade-off documented in ADR-0035.)
|
||||
_FULL_BOILER_CONTROL = 2106
|
||||
|
||||
# Gas-boiler archetypes (Table 4b 102 regular / 104 combi / 120 CPSU) and the
|
||||
# subset that heats hot water instantaneously (a combi has no cylinder; a regular
|
||||
# boiler and a CPSU heat a cylinder). A gas boiler implies a mains-gas connection
|
||||
# + a gas main fuel + a gas-boiler heating category (Table 4a cat 2) + a fanned
|
||||
# room-sealed flue — all set so a heating-system-only override is self-coherent.
|
||||
_GAS_BOILER_CODES = frozenset({102, 104, 120})
|
||||
_COMBI_CODES = frozenset({104})
|
||||
_GAS_BOILER_CATEGORY = 2
|
||||
_MAINS_GAS_FUEL = 26
|
||||
# SAP Table 4a "from the main system" water-heating code — a gas boiler heats hot
|
||||
# water from itself, so the override routes water heating to the main system on
|
||||
# mains gas (clearing a storage dwelling's old electric-immersion arrangement).
|
||||
_FROM_MAIN_WATER_HEATING_CODE = 901
|
||||
|
||||
# Canonical system archetype → representative SAP `sap_main_heating_code`. Codes
|
||||
# map to the modern/condensing variant (A-G efficiency deferred): 102 regular
|
||||
# condensing, 104 condensing combi, 120 CPSU, 401-404 storage heaters, 191
|
||||
# direct-acting electric. Companion fields (meter / charge control) are NOT
|
||||
# listed here — they are derived from the code below, so a new archetype is just
|
||||
# a code.
|
||||
# direct-acting electric. Companion fields (meter / control / fuel / hot water)
|
||||
# are NOT listed here — they are derived from the code below, so a new archetype
|
||||
# is just a code (ADR-0035 drag-along).
|
||||
_MAIN_HEATING_CODES: dict[str, int] = {
|
||||
"Gas boiler, combi": 104,
|
||||
"Gas boiler, regular": 102,
|
||||
|
|
@ -66,17 +95,42 @@ _MAIN_HEATING_CODES: dict[str, int] = {
|
|||
}
|
||||
|
||||
|
||||
def _meter_for(code: int) -> Optional[str]:
|
||||
"""The coherent off-peak meter a heating code implies, or None when the
|
||||
system is single-rate. Keyed off the calculator's §12 off-peak set so the
|
||||
"which systems are off-peak" knowledge has one home."""
|
||||
return _OFF_PEAK_METER if code in OFF_PEAK_IMPLYING_HEATING_CODES else None
|
||||
def _meter_for(code: int) -> str:
|
||||
"""The coherent meter a heating code implies: an off-peak ("Dual") meter for
|
||||
the calculator's §12 off-peak systems, an explicit single-rate ("Single")
|
||||
meter for every other system. Always set — never left to bleed."""
|
||||
return _OFF_PEAK_METER if code in OFF_PEAK_IMPLYING_HEATING_CODES else _SINGLE_RATE_METER
|
||||
|
||||
|
||||
def _charge_control_for(code: int) -> Optional[int]:
|
||||
"""The conservative storage charge control to assume when unobserved, or
|
||||
None for systems that don't take one."""
|
||||
return _MANUAL_CHARGE_CONTROL if code in _STORAGE_HEATER_CODES else None
|
||||
def _control_for(code: int) -> Optional[int]:
|
||||
"""The control to assume when the landlord named only the system: a
|
||||
conservative manual charge control for storage heaters, full modern controls
|
||||
for a gas boiler, None for systems that take neither (direct-acting electric).
|
||||
Overwrites a stale control inherited from the system being replaced."""
|
||||
if code in _STORAGE_HEATER_CODES:
|
||||
return _MANUAL_CHARGE_CONTROL
|
||||
if code in _GAS_BOILER_CODES:
|
||||
return _FULL_BOILER_CONTROL
|
||||
return None
|
||||
|
||||
|
||||
def _gas_boiler_overlay(code: int) -> HeatingOverlay:
|
||||
"""The coherent gas-boiler companion set: a mains-gas connection + gas main
|
||||
fuel, the gas-boiler heating category, a fanned room-sealed flue, full modern
|
||||
controls, a single-rate meter, and a hot-water arrangement drawn from the
|
||||
main system (a combi has no cylinder; a regular boiler / CPSU keeps one)."""
|
||||
return HeatingOverlay(
|
||||
sap_main_heating_code=code,
|
||||
main_heating_category=_GAS_BOILER_CATEGORY,
|
||||
main_fuel_type=_MAINS_GAS_FUEL,
|
||||
mains_gas=True,
|
||||
main_heating_control=_FULL_BOILER_CONTROL,
|
||||
fan_flue_present=True,
|
||||
meter_type=_SINGLE_RATE_METER,
|
||||
water_heating_code=_FROM_MAIN_WATER_HEATING_CODE,
|
||||
water_heating_fuel=_MAINS_GAS_FUEL,
|
||||
has_hot_water_cylinder=code not in _COMBI_CODES,
|
||||
)
|
||||
|
||||
|
||||
def main_heating_overlay_for(
|
||||
|
|
@ -85,10 +139,12 @@ def main_heating_overlay_for(
|
|||
code = _MAIN_HEATING_CODES.get(main_heating_value)
|
||||
if code is None:
|
||||
return None
|
||||
if code in _GAS_BOILER_CODES:
|
||||
return EpcSimulation(heating=_gas_boiler_overlay(code))
|
||||
return EpcSimulation(
|
||||
heating=HeatingOverlay(
|
||||
sap_main_heating_code=code,
|
||||
meter_type=_meter_for(code),
|
||||
main_heating_control=_charge_control_for(code),
|
||||
main_heating_control=_control_for(code),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,35 @@ def test_community_mains_gas_is_a_distinct_fuel_code() -> 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
|
||||
|
|
|
|||
|
|
@ -101,15 +101,18 @@ def test_storage_heaters_carry_an_off_peak_meter(main_heating_value: str) -> Non
|
|||
@pytest.mark.parametrize(
|
||||
"main_heating_value", ["Gas boiler, combi", "Direct-acting electric"]
|
||||
)
|
||||
def test_non_storage_heating_leaves_the_meter_untouched(
|
||||
def test_non_storage_heating_resets_to_a_single_rate_meter(
|
||||
main_heating_value: str,
|
||||
) -> None:
|
||||
# Only storage heaters imply an off-peak tariff; gas and direct-acting
|
||||
# electric (single-rate) keep whatever meter the dwelling already has.
|
||||
# A single-rate system must drag a Single meter, not be left untouched: when
|
||||
# the landlord switches OFF storage heaters the dwelling's old off-peak
|
||||
# ("Dual") meter would otherwise bleed through and bill the new gas/direct-
|
||||
# acting system on an Economy-7 split. Coherence is symmetric — off-peak
|
||||
# codes synthesise Dual, every other code synthesises Single (ADR-0035).
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.meter_type is None
|
||||
assert simulation.heating.meter_type == "Single"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -133,29 +136,89 @@ def test_storage_heaters_drag_along_conservative_manual_charge_control(
|
|||
assert simulation.heating.main_heating_control == 2401
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value", ["Gas boiler, combi", "Direct-acting electric"]
|
||||
)
|
||||
def test_non_storage_heating_does_not_drag_along_a_charge_control(
|
||||
main_heating_value: str,
|
||||
) -> None:
|
||||
# Charge control is a storage-heater concept; other systems keep their own.
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
def test_direct_acting_electric_does_not_drag_along_a_control() -> None:
|
||||
# Charge control is a storage concept and full boiler controls are a wet-
|
||||
# system concept; direct-acting electric panel heaters take neither, so the
|
||||
# overlay leaves the control untouched.
|
||||
simulation = main_heating_overlay_for("Direct-acting electric", 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.main_heating_control is None
|
||||
|
||||
|
||||
def test_off_peak_archetypes_drag_a_dual_meter_others_do_not() -> None:
|
||||
# Contract (the drag-along guard): the off-peak meter is derived from the SAP
|
||||
# code via the calculator's single off-peak classification, so any heating
|
||||
# archetype in the map whose code implies off-peak MUST synthesise a Dual
|
||||
# meter — adding an off-peak system can never silently leave it single-rate —
|
||||
# and a single-rate system must never gain one.
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value", ["Gas boiler, combi", "Gas boiler, regular", "Gas CPSU"]
|
||||
)
|
||||
def test_gas_boiler_assumes_full_modern_controls(main_heating_value: str) -> None:
|
||||
# The landlord names the boiler, not its controls. A gas boiler installed
|
||||
# under modern Building Regs carries programmer + room thermostat + TRVs
|
||||
# (SAP Table 4c code 2106), and the overlay already assumes the modern
|
||||
# condensing efficiency for the boiler — so assuming modern controls is the
|
||||
# coherent, consistent default (Khalim). It also overwrites any stale storage
|
||||
# charge control (2401) the dwelling carried before the switch.
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.main_heating_control == 2106
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value", ["Gas boiler, combi", "Gas boiler, regular", "Gas CPSU"]
|
||||
)
|
||||
def test_gas_boiler_drags_a_coherent_mains_gas_system(main_heating_value: str) -> None:
|
||||
# A gas boiler implies a mains-gas connection and a gas main fuel, a gas-
|
||||
# boiler heating category (Table 4a cat 2), and a fanned room-sealed flue
|
||||
# (a modern condensing boiler). Setting these on the system overlay makes a
|
||||
# heating-system-only override self-coherent — it does not depend on a
|
||||
# separate main_fuel override also being present.
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
heating = simulation.heating
|
||||
assert heating.mains_gas is True
|
||||
assert heating.main_fuel_type == 26
|
||||
assert heating.main_heating_category == 2
|
||||
assert heating.fan_flue_present is True
|
||||
|
||||
|
||||
def test_gas_combi_has_no_hot_water_cylinder() -> None:
|
||||
# A combi heats hot water instantaneously — there is no cylinder. Elmhurst
|
||||
# greys out the cylinder choice for a combi (Khalim). The overlay clears the
|
||||
# cylinder + routes hot water from the main system so a storage dwelling's
|
||||
# old electric-immersion cylinder doesn't bleed through.
|
||||
simulation = main_heating_overlay_for("Gas boiler, combi", 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.has_hot_water_cylinder is False
|
||||
assert simulation.heating.water_heating_code == 901
|
||||
assert simulation.heating.water_heating_fuel == 26
|
||||
|
||||
|
||||
@pytest.mark.parametrize("main_heating_value", ["Gas boiler, regular", "Gas CPSU"])
|
||||
def test_regular_gas_boiler_keeps_a_hot_water_cylinder(
|
||||
main_heating_value: str,
|
||||
) -> None:
|
||||
# A regular boiler (and a gas CPSU) heats a hot-water cylinder from the main
|
||||
# system. Elmhurst lets you pick a cylinder for a regular condensing boiler
|
||||
# (Khalim).
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.has_hot_water_cylinder is True
|
||||
assert simulation.heating.water_heating_code == 901
|
||||
assert simulation.heating.water_heating_fuel == 26
|
||||
|
||||
|
||||
def test_off_peak_archetypes_drag_dual_others_drag_single() -> None:
|
||||
# Contract (the drag-along guard): the meter is derived from the SAP code via
|
||||
# the calculator's single off-peak classification, so any archetype whose
|
||||
# code implies off-peak MUST synthesise a Dual meter and every other code
|
||||
# MUST synthesise a Single meter — a system switch can never silently leave
|
||||
# the previous system's meter in place.
|
||||
for value, code in _MAIN_HEATING_CODES.items():
|
||||
simulation = main_heating_overlay_for(value, 0)
|
||||
assert simulation is not None and simulation.heating is not None
|
||||
expected = "Dual" if code in OFF_PEAK_IMPLYING_HEATING_CODES else None
|
||||
expected = "Dual" if code in OFF_PEAK_IMPLYING_HEATING_CODES else "Single"
|
||||
assert simulation.heating.meter_type == expected, value
|
||||
|
||||
|
||||
|
|
@ -189,6 +252,39 @@ def test_main_heating_override_remaps_the_primary_system_code() -> None:
|
|||
assert result.sap_heating.main_heating_details[0].sap_main_heating_code == 102
|
||||
|
||||
|
||||
def test_gas_boiler_override_onto_a_storage_baseline_leaves_no_stale_fields() -> None:
|
||||
# Regression for property 728513: a landlord reclassified an electric-storage
|
||||
# flat as a gas combi via the main_fuel + water_heating + main_heating_system
|
||||
# overrides. Before the coherent drag-along the override set only the heating
|
||||
# code + fuel and left the storage system's fields in place — mains_gas False,
|
||||
# category 7, the 2401 charge control, a Dual meter, an electric-immersion
|
||||
# cylinder — an incoherent record that gated out the gas-boiler path and made
|
||||
# the engine read the dwelling as off-gas (offering HHRSH). The override must
|
||||
# now overwrite ALL of them, regardless of the (undefined) overlay order.
|
||||
baseline = build_epc()
|
||||
overlays = [
|
||||
fuel_overlay_for("mains gas", 0),
|
||||
water_heating_overlay_for("From main system, mains gas", 0),
|
||||
main_heating_overlay_for("Gas boiler, combi", 0),
|
||||
]
|
||||
assert all(o is not None for o in overlays)
|
||||
|
||||
# Act
|
||||
result = apply_simulations(baseline, [o for o in overlays if o is not None])
|
||||
|
||||
# Assert — a fully coherent mains-gas combi, no electric-storage residue.
|
||||
main = result.sap_heating.main_heating_details[0]
|
||||
assert main.sap_main_heating_code == 104
|
||||
assert main.main_fuel_type == 26
|
||||
assert main.main_heating_category == 2
|
||||
assert main.main_heating_control == 2106
|
||||
assert result.sap_energy_source.mains_gas is True
|
||||
assert result.sap_energy_source.meter_type == "Single"
|
||||
assert result.has_hot_water_cylinder is False
|
||||
assert result.sap_heating.water_heating_code == 901
|
||||
assert result.sap_heating.water_heating_fuel == 26
|
||||
|
||||
|
||||
def test_the_three_heating_overrides_compose_without_conflict() -> None:
|
||||
# Arrange — main_fuel, water_heating and main_heating_system all fold onto one
|
||||
# HeatingOverlay surface but set DISJOINT fields, so they compose (the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue