diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 58a08efd..93af44e6 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -2138,6 +2138,16 @@ def _table_12a_system_for_main( main.main_heating_index_number is not None and heat_pump_record(main.main_heating_index_number) is not None ) + # Electric room heaters (RdSAP main_heating_category 10) are direct- + # acting electric → SAP 10.2 Table 12a Grid 1 (PDF p.191) "Other + # systems including direct-acting electric" row (7-hour high-rate + # fraction 1.00, 10-hour 0.50). Distinct from electric STORAGE + # heaters (category 7), which charge off-peak and correctly fall + # through to None here (→ 100% low rate). Gated on `_is_electric_main` + # so a non-electric room heater (gas / solid-fuel cat 10) is excluded; + # all callers already pre-gate on electric, this is belt-and-braces. + if main.main_heating_category == 10 and _is_electric_main(main): + return Table12aSystem.OTHER_DIRECT_ACTING_ELECTRIC # ASHP — Table 4a rows 211-217 (earlier generations) + 221-227 # (2013+) cover the air-source space. Warm-air ASHPs are 521-524. if code is not None and ( diff --git a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py index fc95112c..c0d9d685 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -2938,6 +2938,55 @@ def test_space_heating_off_peak_fallback_uses_actual_tariff_low_rate_not_e7() -> assert abs(cost_eighteen_hour - 0.0741) <= 1e-6 +def test_space_heating_electric_room_heater_off_peak_bills_at_direct_acting_high_rate() -> None: + # Arrange — an ELECTRIC room heater (RdSAP main_heating_category 10, + # e.g. SAP code 691) is direct-acting electric, so SAP 10.2 Table 12a + # Grid 1 (PDF p.191) puts it on the "Other systems including direct- + # acting electric" row: 7-hour high-rate fraction 1.00, 10-hour 0.50. + # Unlike STORAGE heaters (category 7), which charge off-peak and so + # correctly bill 100% at the low rate, a room heater runs on demand — + # mostly at the HIGH rate. `_table_12a_system_for_main` only mapped + # ASHP, so a room heater fell through to the "100% low-rate" fallback + # (5.50 p, £0.0550), under-charging space heating by ~9.79 p/kWh and + # systematically OVER-rating the cat-10 cluster (1,000-cert API sample: + # 48 certs, mean |err| 9.49, signed +5.08). The fix maps electric + # cat-10 mains to OTHER_DIRECT_ACTING_ELECTRIC. Mirror of S0380.228 + # (which fixed the same fallback for electric SECONDARY heating). + from domain.sap10_calculator.tables.table_12a import Tariff + electric_room_heater_main = MainHeatingDetail( + has_fghrs=False, + main_fuel_type=30, # standard electricity + heat_emitter_type=2, + emitter_temperature=1, + main_heating_control=2602, + main_heating_category=10, # electric room heaters + sap_main_heating_code=691, + ) + gas_room_heater_main = MainHeatingDetail( + has_fghrs=False, + main_fuel_type=1, # mains gas — NOT electric + heat_emitter_type=2, + emitter_temperature=1, + main_heating_control=2602, + main_heating_category=10, # gas room heater (also cat 10) + sap_main_heating_code=631, + ) + + # Act — 7-hour off-peak tariff. + electric_rate = _space_heating_fuel_cost_gbp_per_kwh( + electric_room_heater_main, Tariff.SEVEN_HOUR, prices=SAP_10_2_SPEC_PRICES, + ) + gas_rate = _space_heating_fuel_cost_gbp_per_kwh( + gas_room_heater_main, Tariff.SEVEN_HOUR, prices=SAP_10_2_SPEC_PRICES, + ) + + # Assert — electric room heater: 1.00 × 15.29 p = £0.1529 (high rate). + # Gas room heater is unaffected (non-electric → single Table 32 rate, + # not the off-peak electric split). + assert abs(electric_rate - 0.1529) <= 1e-6 + assert abs(gas_rate - 0.0550) > 1e-6 + + def test_heat_network_dlf_full_table_12c_age_band_coverage() -> None: # Arrange — SAP 10.2 Table 12c (page 193) heat-network Distribution # Loss Factor by dwelling age band A..M. None → K-or-newer