mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(elmhurst): raise on unmapped fuel-fired secondary room-heater code
The Elmhurst Summary lodges only the secondary heating SAP code (Table 4a Category 10), never its fuel. `_elmhurst_secondary_fuel_from_sap_code` mapped the gas block (601-613 → mains gas) and solid block (631-634 → house coal) to their modal defaults, but returned None for any OTHER Category-10 code — and None makes the cascade SILENTLY bill the secondary as electricity (13.19 p/kWh). For a fuel-fired heater (e.g. 621-625 liquid-fuel oil/bioethanol) that is a large, invisible mis-price. Per the UnmappedElmhurstLabel strict-raise pattern (mirrors the wall_type / glazing label raises), a fuel-fired Category-10 code (601-699) outside the mapped gas/solid blocks now RAISES instead of guessing. Electric room heaters (691-699) keep returning None — electricity IS their fuel. The gas block 601-613 still resolves to the modal default mains gas: the Summary cannot distinguish mains gas from LPG/biogas, so an LPG or biogas live-effect fire (worksheet "simulated case 37" used biogas at 7.60 p/kWh vs our 3.48 p/kWh mains-gas default, a +7 SAP gap) is not recoverable from the Summary export — that is a data-availability limit, not a guess we can fix here. This commit closes the genuinely-silent-wrong path; the gas sub-fuel remains the documented modal default. Worksheet harness 47/47, 0 raised. 3 AAA tests, pyright net-zero, regression clean, corpus gauge unchanged (Elmhurst-path only; the API path lodges the secondary fuel explicitly). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ac77624d67
commit
9830ea2110
2 changed files with 71 additions and 1 deletions
|
|
@ -5347,7 +5347,7 @@ def _elmhurst_secondary_fuel_from_sap_code(
|
|||
column B for LPG. Cohort default is mains gas
|
||||
(`_ELMHURST_MAIN_FUEL_TO_SAP10["Mains gas"] = 26`).
|
||||
621-625: Liquid fuel room heaters (oil / bioethanol). Cohort
|
||||
not yet exercised; deferred until a fixture surfaces.
|
||||
not yet exercised — NOT mapped, so they RAISE (see below).
|
||||
631-634: Solid fuel room heaters (open fire, closed room
|
||||
heater with/without boiler). House coal is the modal
|
||||
default per Table 12 secondary rate (3.67 p/kWh).
|
||||
|
|
@ -5358,6 +5358,21 @@ def _elmhurst_secondary_fuel_from_sap_code(
|
|||
in grate") path — pre-slice the cascade defaulted to electricity
|
||||
at 13.19 p/kWh, over-charging secondary by ~£340/yr and pushing
|
||||
SAP -15.81 below the worksheet's 63.87.
|
||||
|
||||
RAISE-don't-guess: a Category-10 room-heater code (601-699) that is
|
||||
fuel-fired but NOT in the mapped gas/solid blocks (e.g. 621-625 liquid
|
||||
fuel) used to return None → the cascade silently billed it as
|
||||
electricity (13.19 p/kWh), a large mis-price for an oil/LPG heater.
|
||||
Per the `UnmappedElmhurstLabel` strict-raise pattern these now raise
|
||||
so the gap surfaces instead of producing a wrong SAP. Electric room
|
||||
heaters (691-699) keep returning None — electricity IS their fuel.
|
||||
|
||||
NOTE the gas block 601-613 still resolves to the MODAL default mains
|
||||
gas: the Summary lodges only the SAP code, never the gas sub-fuel, so
|
||||
an LPG or biogas live-effect fire (worksheet "simulated case 37" used
|
||||
biogas at 7.60 p/kWh) is indistinguishable here from mains gas — the
|
||||
cohort default is correct for the common case and the rarer sub-fuels
|
||||
are not recoverable from the Summary export.
|
||||
"""
|
||||
if sap_code is None:
|
||||
return None
|
||||
|
|
@ -5365,6 +5380,12 @@ def _elmhurst_secondary_fuel_from_sap_code(
|
|||
return 26 # Mains gas, matching `_ELMHURST_MAIN_FUEL_TO_SAP10`
|
||||
if 631 <= sap_code <= 634:
|
||||
return 11 # House coal (Coal in `_ELMHURST_MAIN_FUEL_TO_SAP10`)
|
||||
if 691 <= sap_code <= 699:
|
||||
return None # Electric room heaters → cascade electricity default
|
||||
if 601 <= sap_code <= 699:
|
||||
# Fuel-fired Category-10 room heater we do not yet map — raise
|
||||
# rather than let the cascade silently bill it as electricity.
|
||||
raise UnmappedElmhurstLabel("secondary_heating.sap_code", str(sap_code))
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -713,3 +713,52 @@ class TestUnmeasurableWallThickness:
|
|||
|
||||
def test_wall_thickness_mm_is_none(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_building_parts[0].wall_thickness_mm is None
|
||||
|
||||
|
||||
class TestElmhurstSecondaryFuelFromSapCode:
|
||||
"""`_elmhurst_secondary_fuel_from_sap_code` must RAISE on an unmapped
|
||||
fuel-fired Table 4a Category-10 room-heater code rather than return
|
||||
None and let the cascade silently bill it as electricity (13.19
|
||||
p/kWh). The Summary lodges only the secondary SAP code, not its fuel.
|
||||
"""
|
||||
|
||||
def test_gas_room_heater_resolves_to_mains_gas_modal_default(self) -> None:
|
||||
# Arrange — SAP code 605 (flush-fitting live-effect gas fire), in
|
||||
# the 601-613 gas block. The Summary cannot distinguish mains gas
|
||||
# from LPG/biogas, so the modal default is mains gas (26).
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_elmhurst_secondary_fuel_from_sap_code, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
# Act
|
||||
fuel = _elmhurst_secondary_fuel_from_sap_code(605)
|
||||
|
||||
# Assert
|
||||
assert fuel == 26
|
||||
|
||||
def test_electric_room_heater_returns_none_for_electricity_default(self) -> None:
|
||||
# Arrange — SAP code 693 (electric room heater); electricity IS its
|
||||
# fuel, so None (→ cascade electricity default) is correct, NOT a
|
||||
# silent mis-fuel.
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_elmhurst_secondary_fuel_from_sap_code, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
# Act
|
||||
fuel = _elmhurst_secondary_fuel_from_sap_code(693)
|
||||
|
||||
# Assert
|
||||
assert fuel is None
|
||||
|
||||
def test_unmapped_liquid_fuel_room_heater_raises(self) -> None:
|
||||
# Arrange — SAP code 621 (liquid-fuel room heater) is fuel-fired but
|
||||
# not in the mapped gas/solid blocks; returning None would silently
|
||||
# bill it as electricity. It must raise instead.
|
||||
from datatypes.epc.domain.mapper import (
|
||||
UnmappedElmhurstLabel,
|
||||
_elmhurst_secondary_fuel_from_sap_code, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(UnmappedElmhurstLabel):
|
||||
_elmhurst_secondary_fuel_from_sap_code(621)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue