diff --git a/c6_cohort.py b/c6_cohort.py new file mode 100644 index 000000000..25c18c5cd --- /dev/null +++ b/c6_cohort.py @@ -0,0 +1,41 @@ +"""Which 21.0.1-corpus certs are in #1606's C6 half-charge cohort? +Cohort = WHC in {950,951,952} AND space main is NOT a heat network. +""" +import json +from pathlib import Path +from datatypes.epc.domain.mapper import EpcPropertyDataMapper +from datatypes.epc.domain.field_mappings import is_heat_network_main +from domain.sap10_calculator.rdsap.cert_to_inputs import ( + _first_main_heating, _heat_network_standing_charge_gbp, + _WATER_HEAT_NETWORK_ONLY_CODES, +) + +path = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl") +certs = [json.loads(l) for l in path.read_text().splitlines() if l.strip()] + +hits = [] +for c in certs: + epc = EpcPropertyDataMapper.from_api_response(c) + whc = epc.sap_heating.water_heating_code if epc.sap_heating else None + if whc not in _WATER_HEAT_NETWORK_ONLY_CODES: + continue + m1 = _first_main_heating(epc) + net = is_heat_network_main(m1) + hits.append({ + "rrn": c.get("rrn") or c.get("lmk_key"), + "uprn": c.get("uprn"), + "addr": (c.get("address1") or "")[:34], + "whc": whc, + "main_code": m1.sap_main_heating_code if m1 else None, + "main_is_network": net, + "net_standing": _heat_network_standing_charge_gbp(epc, m1), + "in_C6_half_cohort": not net, + }) + +print(f"WHC 950/951/952 certs in 21.0.1 corpus: {len(hits)}\n") +for h in hits: + flag = " <== C6 HALF COHORT" if h["in_C6_half_cohort"] else "" + print(f" rrn={h['rrn']} uprn={h['uprn']}") + print(f" addr={h['addr']!r} whc={h['whc']} main={h['main_code']} " + f"network_main={h['main_is_network']} net_standing={h['net_standing']}{flag}") +print(f"\nIn C6 half cohort: {sum(h['in_C6_half_cohort'] for h in hits)}") 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 58c1150bc..fbdde3f29 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -68,6 +68,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import ( _mev_default_data_iuf, # pyright: ignore[reportPrivateUsage] _TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S, # pyright: ignore[reportPrivateUsage] dimensions_from_cert, + fuel_cost_section_from_cert, _table_12_factor_fuel_code, # pyright: ignore[reportPrivateUsage] _table_12a_system_for_main, # pyright: ignore[reportPrivateUsage] _is_electric_main, # pyright: ignore[reportPrivateUsage] @@ -9160,6 +9161,34 @@ def test_water_only_heat_network_half_charge_adds_to_the_dwellings_fuel_standing assert abs(total - 84.0) <= 1e-9 +def test_water_only_heat_network_half_charge_reaches_the_fuel_cost_worksheet() -> None: + # Arrange — the §C6 half charge must reach worksheet (251), not just the + # helper. A gas-boiler main (SAP 102, not a network) with WHC 950 keeps the + # dwelling on the STANDARD tariff, so `_fuel_cost` returns the real + # (240)..(255) result rather than the off-peak zero sentinel. + # + # Table 12 note (a): "The standing charge for gas fuels is added to space + # and water heating costs where the gas fuel is used for space heating" → + # Table 32 code 1 = £120. Plus the C6 half (£60) = £180. + from dataclasses import replace + + base = _typical_semi_detached_epc() + epc = replace( + base, + sap_heating=make_sap_heating( + main_heating_details=[_gas_boiler_detail(sap_main_heating_code=102)], + water_heating_code=950, # DHW-only heat network (boilers) + ), + ) + + # Act + fc = fuel_cost_section_from_cert(epc) + + # Assert + assert fc is not None + assert abs(fc.additional_standing_charges_gbp - 180.0) <= 1e-9 + + def test_mev_default_data_iuf_is_table_329_system_type_10_value() -> None: # Arrange / Act — SAP 10.2 Table 4g note 3 (PDF p.176) directs the # default SFP to "the appropriate in-use factor for default data from