diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 017f3ac19..b02455c8e 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -4000,19 +4000,25 @@ def _normalize_sap_schema_16_x(data: Dict[str, Any]) -> Dict[str, Any]: # Some 16.x/15.0 certs (e.g. cert 9568-3034-6211-6089-5960, UPRN # 100010349400, SAP-Schema-15.0) lodge `sap_heating.cylinder_size` but - # omit the top-level `has_hot_water_cylinder` boolean entirely — + # omit the top-level `has_hot_water_cylinder` field entirely — # RdSapSchema17_1 requires both as independent fields. Unlike # `multiple_glazed_proportion` above, this is NOT a guess: RdSAP 10 # §10.5's `cylinder_size` code 1 means "no cylinder" by definition # (see `_CYLINDER_SIZE_CODE_TO_LITRES` / cert_to_inputs.py:5902), the # exact same fact `has_hot_water_cylinder` encodes — confirmed 1:1 # across every real 15.0/16.x fixture that lodges both fields - # (cylinder_size == 1 <-> has_hot_water_cylinder == false, every - # other code <-> true). Derive rather than leave the cert unmappable. + # (cylinder_size == 1 <-> has_hot_water_cylinder == "false", every + # other code <-> "true"). Derive rather than leave the cert + # unmappable. Lodged as the lowercase string "true"/"false" (RdSAP + # convention every real fixture uses — `from_rdsap_schema_17_1` + # reads it via `schema.has_hot_water_cylinder == "true"`, a bare + # Python bool would silently compare False either way). if "has_hot_water_cylinder" not in d: cylinder_size = heating.get("cylinder_size") if isinstance(cylinder_size, int): - d["has_hot_water_cylinder"] = cylinder_size != 1 + d["has_hot_water_cylinder"] = ( + "true" if cylinder_size != 1 else "false" + ) for bp in _dicts(d.get("sap_building_parts")): for fd in _dicts(bp.get("sap_floor_dimensions")): diff --git a/datatypes/epc/domain/tests/test_from_sap_schema_15_0.py b/datatypes/epc/domain/tests/test_from_sap_schema_15_0.py index a95345344..82be846d8 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema_15_0.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema_15_0.py @@ -107,3 +107,18 @@ class TestFromSapSchema15_0: assert epc.has_hot_water_cylinder is False assert epc.sap_heating.cylinder_size == 1 + + def test_derives_has_hot_water_cylinder_true_from_a_sized_cylinder( + self, + ) -> None: + # Mirror of the above for the other branch: a lodged size code other + # than 1 (here 2 = Normal/110L, per sap_16_2.json) means a cylinder + # IS present, so an omitted `has_hot_water_cylinder` must derive + # True — not just default-False by omission. + data = load("sap_16_2.json") + assert data["sap_heating"]["cylinder_size"] == 2 + del data["has_hot_water_cylinder"] + + epc = EpcPropertyDataMapper.from_api_response(data) + + assert epc.has_hot_water_cylinder is True