diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 482ce2723..5c785343f 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -6032,7 +6032,7 @@ def _map_sap_heating( MainHeatingDetail( has_fghrs=main.flue_gas_heat_recovery_system, main_fuel_type=_pashub_main_fuel_code(fuel_type), - heat_emitter_type=main.emitter, + heat_emitter_type=_pashub_heat_emitter_code(main.emitter), emitter_temperature=main.emitter_temperature, fan_flue_present=main.fan_assist, main_heating_control=main.controls, @@ -7085,6 +7085,18 @@ def _pashub_main_fuel_code(fuel_label: str) -> Union[int, str]: return code +def _pashub_heat_emitter_code(emitter_label: str) -> Union[int, str]: + """Resolve a PasHub surveyed main-heating emitter label to its SAP10 emitter + code at the mapper boundary (ADR-0015), reusing the Elmhurst emitter map + (`Radiators -> 1`). A blank label passes through unchanged; a non-empty label + the map does not cover strict-raises `UnmappedPasHubLabel` here rather than + resurfacing downstream as the calculator's `UnmappedSapCode: heat_emitter_type`.""" + if not emitter_label: + return emitter_label + code = _ELMHURST_HEAT_EMITTER_TO_SAP10.get(emitter_label) + if code is None: + raise UnmappedPasHubLabel("heat emitter", emitter_label) + return code # PasHub surveyed `party_wall_construction_type` label โ†’ SAP10 party-wall code # (RdSAP 10 ยง5.10 Table 15) that `u_party_wall` consumes. Same target codes as # the GOV.UK-API (`_API_PARTY_WALL_CONSTRUCTION_TO_SAP10`) and Elmhurst diff --git a/datatypes/epc/domain/tests/test_from_site_notes.py b/datatypes/epc/domain/tests/test_from_site_notes.py index 99dba4cff..843c2a96f 100644 --- a/datatypes/epc/domain/tests/test_from_site_notes.py +++ b/datatypes/epc/domain/tests/test_from_site_notes.py @@ -205,10 +205,9 @@ class TestFromSiteNotesExample1: assert result.sap_heating.main_heating_details[0].main_fuel_type == 26 def test_main_heating_emitter(self, result: EpcPropertyData) -> None: - # heating_and_hot_water.main_heating.emitter: "Radiators" - assert ( - result.sap_heating.main_heating_details[0].heat_emitter_type == "Radiators" - ) + # heating_and_hot_water.main_heating.emitter: "Radiators" is normalized + # at the mapper boundary to SAP10 emitter code 1 (matching Elmhurst). + assert result.sap_heating.main_heating_details[0].heat_emitter_type == 1 def test_main_heating_no_fghrs(self, result: EpcPropertyData) -> None: # heating_and_hot_water.main_heating.flue_gas_heat_recovery_system: false @@ -420,7 +419,7 @@ class TestFromSiteNotesExample1: MainHeatingDetail( has_fghrs=False, main_fuel_type=26, - heat_emitter_type="Radiators", + heat_emitter_type=1, emitter_temperature="Unknown", fan_flue_present=True, main_heating_control="Programmer, room thermostat and TRVs", @@ -899,3 +898,25 @@ class TestPasHubUnmappedMainFuel: # Assert assert result.sap_heating.main_heating_details[0].main_fuel_type == 30 + + +class TestPasHubUnmappedHeatEmitter: + """A PasHub survey whose surveyed main-heating emitter label the mapper does + not yet cover must strict-raise `UnmappedPasHubLabel` at the boundary, + naming the label โ€” so a coverage gap is fixed here (a one-line lookup + addition) rather than passed downstream as a raw string to resurface as the + calculator's `UnmappedSapCode: heat_emitter_type`. + """ + + def test_unrecognised_emitter_label_raises_naming_the_label(self) -> None: + # Arrange โ€” an example fixture whose emitter cell carries a label the + # Elmhurst emitter map does not cover. + from datatypes.epc.domain.mapper import UnmappedPasHubLabel + + raw = load("pashub_rdsap_site_notes_example1.json") + raw["heating_and_hot_water"]["main_heating"]["emitter"] = "Skirting heaters" + survey = from_dict(PasHubRdSapSiteNotes, raw) + + # Act / Assert + with pytest.raises(UnmappedPasHubLabel, match="Skirting heaters"): + EpcPropertyDataMapper.from_site_notes(survey)