From 28b72fc67c682b5b4d6c0e90d2c6a23afb96edbe Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 1 Jul 2026 12:52:33 +0000 Subject: [PATCH] Normalize RdSAP hot-water demand (WWHRS rooms) on the degraded 17.0/19.0 paths PRD #1385, shower/WWHRS family. The 17.0 and 19.0 API mappers left sap_heating.number_baths / mixer_shower_count as None, so the water-heating demand (cert_to_inputs) fell back to defaults instead of the lodged bath/shower room counts. Derive them from schema.sap_heating.instantaneous_wwhrs, mirroring the 17.1/18.0/20.0 paths: number_baths = rooms_with_bath_and_or_shower + rooms_with_bath_and_mixer_shower mixer_shower_count = rooms_with_mixer_shower_no_bath + rooms_with_bath_and_mixer_shower Guarded on instantaneous_wwhrs is not None (Optional in the schema), like 17.1. Counts, same meaning across the reduced-field family. Verification: new test 2 passed; test_mapper_corpus 6002 passed; SAP-accuracy / RealCertExpectation regressions pass; pyright unchanged (39 -> 39, pre-existing). Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/mapper.py | 32 +++++++++++++ .../epc/domain/test_mapper_wwhrs_parity.py | 47 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 tests/datatypes/epc/domain/test_mapper_wwhrs_parity.py diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index c4a33a514..08bcb1da8 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -620,6 +620,22 @@ class EpcPropertyDataMapper: ), sap_heating=SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), + # ADR-0028: derive HW-demand counts from the lodged bath/shower + # ROOM counts, mirroring the 17.1/18.0/20.0 paths. These were + # dropped (left None), so the water-heating demand fell back to + # defaults instead of the lodged bath/mixer-shower counts. + number_baths=( + schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_or_shower + + schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_mixer_shower + if schema.sap_heating.instantaneous_wwhrs is not None + else None + ), + mixer_shower_count=( + schema.sap_heating.instantaneous_wwhrs.rooms_with_mixer_shower_no_bath + + schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_mixer_shower + if schema.sap_heating.instantaneous_wwhrs is not None + else None + ), main_heating_details=[ MainHeatingDetail( has_fghrs=d.has_fghrs == "Y", @@ -1602,6 +1618,22 @@ class EpcPropertyDataMapper: ), sap_heating=SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), + # ADR-0028: derive HW-demand counts from the lodged bath/shower + # ROOM counts, mirroring the 17.1/18.0/20.0 paths. These were + # dropped (left None), so the water-heating demand fell back to + # defaults instead of the lodged bath/mixer-shower counts. + number_baths=( + schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_or_shower + + schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_mixer_shower + if schema.sap_heating.instantaneous_wwhrs is not None + else None + ), + mixer_shower_count=( + schema.sap_heating.instantaneous_wwhrs.rooms_with_mixer_shower_no_bath + + schema.sap_heating.instantaneous_wwhrs.rooms_with_bath_and_mixer_shower + if schema.sap_heating.instantaneous_wwhrs is not None + else None + ), main_heating_details=[ MainHeatingDetail( has_fghrs=d.has_fghrs == "Y", diff --git a/tests/datatypes/epc/domain/test_mapper_wwhrs_parity.py b/tests/datatypes/epc/domain/test_mapper_wwhrs_parity.py new file mode 100644 index 000000000..4d9519cd0 --- /dev/null +++ b/tests/datatypes/epc/domain/test_mapper_wwhrs_parity.py @@ -0,0 +1,47 @@ +"""Regression: the degraded RdSAP paths (17.0, 19.0) must derive the hot-water +bath/shower demand counts from the lodged WWHRS room counts, like the +reference-complete 17.1/18.0/20.0 paths. + +PRD #1385 (mapper normalization), shower/WWHRS family. Both paths left +`sap_heating.number_baths` / `mixer_shower_count` as None, so the water-heating +demand (`cert_to_inputs`) fell back to defaults. The reference derivation: + number_baths = rooms_with_bath_and_or_shower + rooms_with_bath_and_mixer_shower + mixer_shower_count = rooms_with_mixer_shower_no_bath + rooms_with_bath_and_mixer_shower +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from datatypes.epc.domain.mapper import EpcPropertyDataMapper + +# 17.0: rooms bath_or_shower=4, mixer_no_bath=0, bath_and_mixer=4 +# → number_baths 8, mixer_shower_count 4. +_FIXTURE_17_0 = Path("tests/fixtures/epc_prediction/W104HL/cert-f96b9b6f471b.json") +# 19.0: rooms bath_or_shower=2, mixer_no_bath=1, bath_and_mixer=1 +# → number_baths 3, mixer_shower_count 2. +_FIXTURE_19_0 = Path("tests/fixtures/epc_prediction/LE113PX/cert-ba628e4f9cbf.json") + + +def _load(path: Path, schema_type: str) -> dict[str, Any]: + data: dict[str, Any] = json.loads(path.read_text()) + assert data["schema_type"] == schema_type + return data + + +def test_17_0_derives_hot_water_demand_from_wwhrs_rooms() -> None: + epc = EpcPropertyDataMapper.from_api_response( + _load(_FIXTURE_17_0, "RdSAP-Schema-17.0") + ) + assert epc.sap_heating.number_baths == 8 + assert epc.sap_heating.mixer_shower_count == 4 + + +def test_19_0_derives_hot_water_demand_from_wwhrs_rooms() -> None: + epc = EpcPropertyDataMapper.from_api_response( + _load(_FIXTURE_19_0, "RdSAP-Schema-19.0") + ) + assert epc.sap_heating.number_baths == 3 + assert epc.sap_heating.mixer_shower_count == 2