From 18065ae01001c4097d975609370b236a2a548202 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 14 Jul 2026 17:44:38 +0000 Subject: [PATCH] Set roof_construction_type and country_code on the PasHub site-notes path roof_construction_type was left None in from_site_notes, so the calculator's "flat"/"sloping ceiling" substring routing in heat_transmission couldn't detect flat roofs and every roof (flat or pitched) fell through the pitched default. country_code was never set on this path either, so the England/Wales/Scotland/NI U-value cascade (u_floor, u_basement_floor, u_door) had no country to key off. Sets roof_construction_type from the surveyed roof_space construction_type (main building + extensions), and country_code="ENG" for this all-English cohort, matching the existing from_elmhurst_site_notes path. Fixes #1566 Co-Authored-By: Claude Sonnet 5 --- datatypes/epc/domain/mapper.py | 11 ++++ .../epc/domain/tests/test_from_site_notes.py | 58 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 3e56e8cae..737b1a94f 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -343,6 +343,13 @@ class EpcPropertyDataMapper: inspection_date=general.inspection_date, tenure=general.tenure, transaction_type=general.transaction_type, + # PAS Hub site notes don't lodge a country field, but the + # cascade reads `country_code` to pick the England (vs Welsh / + # Scottish / NI) U-value tables. Set 'ENG' explicitly, matching + # the Elmhurst path (`from_elmhurst_site_notes`) and this + # cohort's postcodes; a general fix should read the surveyed + # country once PAS Hub lodges one. + country_code="ENG", roofs=[], walls=[], floors=[], @@ -5985,6 +5992,7 @@ def _map_main_building_part( ), sap_floor_dimensions=_map_floor_dimensions(measurements.main_building.floors), wall_thickness_mm=main.wall_thickness_mm, + roof_construction_type=roof.construction_type or None, roof_insulation_location=roof_location, roof_insulation_thickness=roof_thickness, floor_type=floor.floor_type, @@ -6015,6 +6023,9 @@ def _map_extension_building_part( ), sap_floor_dimensions=_map_floor_dimensions(ext_m.floors), wall_thickness_mm=ext_c.wall_thickness_mm, + roof_construction_type=( + roof.construction_type or None if roof is not None else None + ), roof_insulation_location=roof_location, roof_insulation_thickness=roof_thickness, ) diff --git a/datatypes/epc/domain/tests/test_from_site_notes.py b/datatypes/epc/domain/tests/test_from_site_notes.py index be334e293..6412659da 100644 --- a/datatypes/epc/domain/tests/test_from_site_notes.py +++ b/datatypes/epc/domain/tests/test_from_site_notes.py @@ -34,6 +34,62 @@ def load(filename: str) -> Dict[str, Any]: return json.load(f) # type: ignore[no-any-return] +class TestRoofConstructionTypeCoding: + """`from_site_notes` must set `roof_construction_type` from the surveyed + `roof_space.*.construction_type` label, not leave it `None`. The + calculator's `heat_transmission` reads this field for its "flat" / + "sloping ceiling" substring routing (flat-roof U path vs the pitched- + roof cos(30°) inclined-surface factor); leaving it `None` silently + routes every roof — flat or pitched — through the pitched default + (Guinness cohort, issue #1566). + """ + + @pytest.mark.parametrize( + "label, expected", + [ + ("Pitched roof (Slates or tiles), Access to loft", "Pitched roof (Slates or tiles), Access to loft"), + ("Flat", "Flat"), # routes to the flat-roof U path (heat_transmission "flat" substring) + ("", None), # no lodging + ], + ) + def test_main_building_label_passes_through( + self, label: str, expected: Optional[str] + ) -> None: + data = load("pashub_rdsap_site_notes_example1.json") + data["roof_space"]["main_building"]["construction_type"] = label + survey = from_dict(PasHubRdSapSiteNotes, data) + result = EpcPropertyDataMapper.from_site_notes(survey) + assert result.sap_building_parts[0].roof_construction_type == expected + + def test_extension_label_passes_through(self) -> None: + data = load("pashub_rdsap_site_notes_example2.json") + data["roof_space"]["extensions"][0]["construction_type"] = "Flat" + survey = from_dict(PasHubRdSapSiteNotes, data) + result = EpcPropertyDataMapper.from_site_notes(survey) + extension_part = next( + bp + for bp in result.sap_building_parts + if bp.identifier != BuildingPartIdentifier.MAIN + ) + assert extension_part.roof_construction_type == "Flat" + + +class TestCountryCode: + """`from_site_notes` must set `country_code` so the cascade's England / + Wales / Scotland / NI U-value tables (`u_floor`, `u_basement_floor`, + `u_door`) resolve correctly. PAS Hub site notes don't lodge a country + field; default to 'ENG' for this all-English cohort (Guinness cohort, + issue #1566), matching the Elmhurst site-notes path. + """ + + def test_country_code_is_england(self) -> None: + survey = from_dict( + PasHubRdSapSiteNotes, load("pashub_rdsap_site_notes_example1.json") + ) + result = EpcPropertyDataMapper.from_site_notes(survey) + assert result.country_code == "ENG" + + class TestPartyWallConstructionCoding: """`from_site_notes` must int-code the PAS Hub `party_wall_construction_type` label to the SAP10 code `u_party_wall` consumes (RdSAP 10 Table 15), not copy @@ -517,6 +573,7 @@ class TestFromSiteNotesExample1: status=None, tenure="Rented Social", transaction_type="None of the Above", + country_code="ENG", # Elements (not available from site notes) roofs=[], walls=[], @@ -644,6 +701,7 @@ class TestFromSiteNotesExample1: ), ], wall_thickness_mm=280, + roof_construction_type="Pitched roof (Slates or tiles), Access to loft", roof_insulation_location="Joists", roof_insulation_thickness=100, floor_type="Ground Floor",