diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index ceff10a8..cf4ecb27 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -306,3 +306,24 @@ def test_summary_001479_ext2_floor_is_exposed_to_external_air() -> None: ext2 = epc.sap_building_parts[2] assert ext2.floor_type == "To external air" assert ext2.sap_floor_dimensions[0].is_exposed_floor is True + + +def test_summary_001479_ext2_sloping_ceiling_roof_uninsulated_for_pre_1950() -> None: + # Arrange — cert 001479 Ext2 §8 lodges "Type: PS Pitched, sloping + # ceiling" + "Insulation Thickness: As Built" + age band C (1930-49). + # Original 1930s construction had no sloping-ceiling insulation; + # worksheet §3 `External roof Ext2 … 2.30` pins U=2.30 (uninsulated + # Table 16 row 0). Pre-slice the mapper passed thickness=None through, + # routing to `u_roof`'s pitched-roof Table 18 col 1 default (0.40 for + # age C, assumes loft-joist retrofit) — wrong geometry for PS. + # Ext1's PS roof at age M leaves thickness=None (modern build, + # cascade default U=0.15 matches worksheet). + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_001479_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + + # Act + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + + # Assert + assert epc.sap_building_parts[2].roof_insulation_thickness == 0 + assert epc.sap_building_parts[1].roof_insulation_thickness is None diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index a9c7f94f..7e2b7083 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -1879,6 +1879,33 @@ def _elmhurst_wall_insulation_int(coded: str) -> Optional[int]: _UPPER_FLOOR_HEIGHT_ADD_M: float = 0.25 +# Pre-1950 age bands lodged with an "As Built" sloping-ceiling roof get +# their original-construction U-value (uninsulated) — original 1930-49 +# construction had no sloping-ceiling insulation by default. RdSAP10 +# Table 18 col 1 (the pitched-roof default behind `u_roof`) assumes +# joist insulation accessible from the loft, which doesn't apply to PS +# sloping-ceiling roofs; without an explicit thickness override the +# cascade would understate U for this geometry. Cert 001479 Ext2's +# worksheet row `External roof Ext2 … 2.22, 2.30` pins U=2.30 at this +# age + geometry combination. +_PRE_1950_AGE_CODES: Final[frozenset[str]] = frozenset({"A", "B", "C", "D"}) + + +def _resolve_sloping_ceiling_thickness( + roof: ElmhurstRoofDetails, age_code: str, +) -> Optional[int]: + """Map an Elmhurst sloping-ceiling roof (`PS Pitched, sloping ceiling`) + with no lodged thickness ("As Built") to an explicit 0 mm for pre- + 1950 age bands. Other lodgements pass through unchanged.""" + if roof.insulation_thickness_mm is not None: + return roof.insulation_thickness_mm + if not roof.roof_type.startswith("PS"): + return None + if age_code in _PRE_1950_AGE_CODES: + return 0 + return None + + def _is_floor_exposed_to_unheated_space(location: Optional[str]) -> bool: """True when the floor sits above an unheated space OR is exposed directly to external air. Both lodgements route through @@ -2118,9 +2145,10 @@ def _map_elmhurst_building_part( ] while len(alt_walls) < 2: alt_walls.append(None) + age_code = _leading_code(age_band) return SapBuildingPart( identifier=identifier, - construction_age_band=_leading_code(age_band), + construction_age_band=age_code, wall_construction=_elmhurst_wall_construction_int(walls.wall_type), wall_insulation_type=_elmhurst_wall_insulation_int(walls.insulation), wall_thickness_measured=not walls.thickness_unknown, @@ -2128,7 +2156,9 @@ def _map_elmhurst_building_part( sap_floor_dimensions=floor_dims, wall_thickness_mm=walls.thickness_mm, roof_insulation_location=_strip_code(roof.insulation), - roof_insulation_thickness=roof.insulation_thickness_mm, + roof_insulation_thickness=_resolve_sloping_ceiling_thickness( + roof, age_code, + ), floor_type=_strip_code(floor.location), floor_construction_type=_strip_code(floor.floor_type), floor_insulation_type_str=_strip_code(floor.insulation),