diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index e9e72ab6c..a9f9e5cae 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -7030,6 +7030,30 @@ def _elmhurst_main_fuel_int(fuel_type: str) -> Optional[int]: return _ELMHURST_MAIN_FUEL_TO_SAP10.get(fuel_type) +class UnmappedPasHubLabel(ValueError): + """A PasHub Site-Notes survey lodged a finite-enum label that the mapper + does not yet know how to translate to the SAP10 cascade enum. + + Raised by the strict PasHub label-to-code helpers to surface mapper- + coverage gaps at the `from_site_notes` boundary (ADR-0015) instead of + passing the raw string downstream, where it resurfaces as the + calculator's deep `MissingMainFuelType` raise — or worse, is silently + mis-billed. Mirrors `UnmappedApiCode` / `UnmappedElmhurstLabel`. + + Distinguish "lodging absent" (a genuinely blank cell — the helper returns + it unchanged, correct) from "lodging present but unrecognised" (raise — + a Fuel label the lookup doesn't yet cover, needs a dict entry added). + """ + + def __init__(self, field: str, value: str) -> None: + super().__init__( + f"unmapped PasHub {field} label: {value!r}; " + f"add an entry to the corresponding mapper lookup dict" + ) + self.field = field + self.value = value + + # PasHub Site-Notes surveyed main-heating "Fuel" labels mapped to SAP10 fuel # codes (the epc-codes `main_fuel` enum the calculator's resolver routes via # its API→Table-12/32 translation). Seeded only with labels confirmed from diff --git a/datatypes/epc/domain/tests/test_from_site_notes.py b/datatypes/epc/domain/tests/test_from_site_notes.py index 81670e239..52598b720 100644 --- a/datatypes/epc/domain/tests/test_from_site_notes.py +++ b/datatypes/epc/domain/tests/test_from_site_notes.py @@ -820,3 +820,25 @@ class TestElmhurstSecondaryFuelFromSapCode: # Act / Assert with pytest.raises(UnmappedElmhurstLabel): _elmhurst_secondary_fuel_from_sap_code(620) + + +class TestPasHubUnmappedMainFuel: + """A PasHub survey whose surveyed main-heating Fuel 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 `MissingMainFuelType`, or silently mis-billed. + """ + + def test_unrecognised_fuel_label_raises_naming_the_label(self) -> None: + # Arrange — an example fixture whose Fuel cell carries a label the + # PasHub lookup does not yet know. + from datatypes.epc.domain.mapper import UnmappedPasHubLabel + + raw = load("pashub_rdsap_site_notes_example1.json") + raw["heating_and_hot_water"]["main_heating"]["fuel"] = "Anthracite" + survey = from_dict(PasHubRdSapSiteNotes, raw) + + # Act / Assert + with pytest.raises(UnmappedPasHubLabel, match="Anthracite"): + EpcPropertyDataMapper.from_site_notes(survey)