diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 1658383f..7fe948dc 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -735,7 +735,11 @@ class EpcPropertyDataMapper: postcode=schema.postcode, post_town=schema.post_town, total_floor_area_m2=float(schema.total_floor_area), - has_hot_water_cylinder=schema.has_hot_water_cylinder == "true", + has_hot_water_cylinder=( + schema.has_hot_water_cylinder + or schema.sap_heating.has_hot_water_cylinder + ) + == "true", has_fixed_air_conditioning=schema.has_fixed_air_conditioning == "true", solar_water_heating=False, extensions_count=0, diff --git a/datatypes/epc/domain/tests/test_from_sap_schema.py b/datatypes/epc/domain/tests/test_from_sap_schema.py index 4cc08213..c8f6716a 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema.py @@ -77,6 +77,39 @@ class TestFromSapSchema17_1Tracer: assert result.total_floor_area_m2 == 68.0 +class TestFullSapHasHotWaterCylinderFallback: + """Some full-SAP certs (e.g. SAP-Schema-17.0 cert 8265-7433-3220-9736-7902) + omit the top-level `has_hot_water_cylinder` and lodge it only under + `sap_heating`. The required top-level field made `from_dict` raise; it is now + optional and the mapper falls back to the nested value.""" + + def test_top_level_omitted_falls_back_to_sap_heating(self) -> None: + # Arrange — drop the top-level flag, keep the nested one ("true"). + data = load("sap_17_1.json") + data.pop("has_hot_water_cylinder", None) + data["sap_heating"]["has_hot_water_cylinder"] = "true" + + # Act + schema = from_dict(SapSchema17_1, data) + result = EpcPropertyDataMapper.from_sap_schema_17_1(schema) + + # Assert — resolved from sap_heating, not crashed + assert result.has_hot_water_cylinder is True + + def test_top_level_omitted_nested_false_resolves_false(self) -> None: + # Arrange + data = load("sap_17_1.json") + data.pop("has_hot_water_cylinder", None) + data["sap_heating"]["has_hot_water_cylinder"] = "false" + + # Act + schema = from_dict(SapSchema17_1, data) + result = EpcPropertyDataMapper.from_sap_schema_17_1(schema) + + # Assert + assert result.has_hot_water_cylinder is False + + class TestFromSapSchema17_1FabricDescriptions: """Slice 3 (D4): the measured-U fabric descriptions flow through to epc.walls/floors/roofs so the engine's u_wall/u_floor/u_roof can parse diff --git a/datatypes/epc/schema/sap_schema_17_1.py b/datatypes/epc/schema/sap_schema_17_1.py index 195e4f53..b84d3270 100644 --- a/datatypes/epc/schema/sap_schema_17_1.py +++ b/datatypes/epc/schema/sap_schema_17_1.py @@ -184,7 +184,6 @@ class SapSchema17_1: postcode: str post_town: str inspection_date: str - has_hot_water_cylinder: str has_fixed_air_conditioning: str roofs: List[EnergyElement] walls: List[EnergyElement] @@ -197,3 +196,6 @@ class SapSchema17_1: # measured living-room area (m²); the engine consumes it via a back-solved # habitable_rooms_count (Table 27). Optional — 100% present in the corpus. living_area: Optional[Union[int, float]] = None + # Some 17.0 full-SAP certs omit the top-level flag and lodge it only under + # sap_heating; the mapper falls back to sap_heating.has_hot_water_cylinder. + has_hot_water_cylinder: Optional[str] = None