From c0b4fd1e6a9d2da1b3007565c9925132c3014821 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Fri, 10 Jul 2026 11:58:07 +0000 Subject: [PATCH] Fix has_hot_water_cylinder derivation to lodge the string convention, not a bool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior commit derived a Python bool (`cylinder_size != 1`), but `from_rdsap_schema_17_1` reads the field via `schema.has_hot_water_cylinder == "true"` — every real 15.0/16.x fixture lodges the lowercase string "true"/"false", not a JSON boolean. A bare bool compares False against that string check either way, so the derivation silently mapped every cylinder-present cert to has_hot_water_cylinder=False too. Caught by the requested true-branch test (mutating sap_16_2.json, which lodges cylinder_size=2/has_hot_water_cylinder="true", to omit the field) — it failed under the original bool-typed fix. Now emits "true"/"false" strings to match the lodged convention. Co-Authored-By: Claude Sonnet 5 --- datatypes/epc/domain/mapper.py | 14 ++++++++++---- .../epc/domain/tests/test_from_sap_schema_15_0.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) 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