diff --git a/backend/documents_parser/elmhurst_extractor.py b/backend/documents_parser/elmhurst_extractor.py index 87075f59..fa5dadc0 100644 --- a/backend/documents_parser/elmhurst_extractor.py +++ b/backend/documents_parser/elmhurst_extractor.py @@ -340,6 +340,12 @@ class ElmhurstSiteNotesExtractor: dry_lined=self._local_bool( lines, f"Alternative Wall {n} Dry-lining" ), + # RdSAP 10 Table 4 (p.22): a sheltered alt sub-area + # (adjacent to an unheated buffer, e.g. a flat corridor + # wall) adds R=0.5 m²K/W → U = 1/(1/U + 0.5). + sheltered=self._local_bool( + lines, f"Alternative Wall {n} Sheltered Wall" + ), )) return result 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 25c0b63f..fbc1c6f9 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -45,8 +45,10 @@ from datatypes.epc.domain.mapper import ( _elmhurst_dwelling_type, # pyright: ignore[reportPrivateUsage] _elmhurst_glazing_type_code, # pyright: ignore[reportPrivateUsage] _elmhurst_immersion_type_code, # pyright: ignore[reportPrivateUsage] + _map_elmhurst_alternative_wall, # pyright: ignore[reportPrivateUsage] ) from datatypes.epc.surveys.elmhurst_site_notes import ( + AlternativeWall as ElmhurstAlternativeWall, FloorDetails as ElmhurstFloorDetails, RoofDetails as ElmhurstRoofDetails, ) @@ -1545,6 +1547,35 @@ def test_summary_mapper_raises_on_unmapped_cylinder_insulation_label() -> None: assert excinfo.value.value == "Polyester wool" +def test_map_elmhurst_alternative_wall_carries_sheltered_flag() -> None: + # Arrange — Elmhurst Summary §7 lodges "Alternative Wall N Sheltered + # Wall: Yes" for a sub-area adjacent to an unheated buffer (e.g. a flat's + # corridor wall). RdSAP 10 Table 4 (p.22) gives a sheltered wall an added + # R=0.5 m²K/W → U=1/(1/U+0.5). The cascade applies this via + # SapAlternativeWall.is_sheltered (the API path already wires it); the + # Elmhurst path must surface it too. Surfaced by simulated case 34 (cert + # 001431 flat: 6.02 m² corridor wall billed at full U=1.50 instead of + # the sheltered 0.86 → +3.85 W/K, -1.61 SAP). + sheltered = ElmhurstAlternativeWall( + area_m2=12.5, wall_type="CA Cavity", insulation="A As Built", + thickness_unknown=False, thickness_mm=250, u_value_known=False, + dry_lined=False, sheltered=True, + ) + plain = ElmhurstAlternativeWall( + area_m2=12.5, wall_type="CA Cavity", insulation="A As Built", + thickness_unknown=False, thickness_mm=250, u_value_known=False, + dry_lined=False, sheltered=False, + ) + + # Act + mapped_sheltered = _map_elmhurst_alternative_wall(sheltered) + mapped_plain = _map_elmhurst_alternative_wall(plain) + + # Assert + assert mapped_sheltered.is_sheltered is True + assert mapped_plain.is_sheltered is False + + def test_elmhurst_dwelling_type_single_storey_flat_with_exposed_roof_is_top_floor() -> None: # Arrange — a single-storey flat exposed BOTH top (external pitched # roof, access to loft) AND bottom (floor over partially-heated space, diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 985760ab..b47db127 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3864,6 +3864,11 @@ def _map_elmhurst_alternative_wall( wall_insulation_thickness=None, wall_thickness_mm=measured_thickness_mm, is_basement=_elmhurst_wall_is_basement(a.wall_type), + # Summary §7 "Alternative Wall N Sheltered Wall: Yes" → RdSAP 10 + # Table 4 (p.22) sheltered U = 1/(1/U + 0.5), applied by the + # cascade's `_alt_wall_w_per_k`. Mirror of the API path's + # `sheltered_wall == "Y"` wiring. + is_sheltered=a.sheltered, ) diff --git a/datatypes/epc/surveys/elmhurst_site_notes.py b/datatypes/epc/surveys/elmhurst_site_notes.py index 16464766..3d5b2b21 100644 --- a/datatypes/epc/surveys/elmhurst_site_notes.py +++ b/datatypes/epc/surveys/elmhurst_site_notes.py @@ -71,6 +71,11 @@ class AlternativeWall: thickness_mm: Optional[int] u_value_known: bool dry_lined: bool = False + # Summary §7 "Alternative Wall N Sheltered Wall: Yes/No". RdSAP 10 + # Table 4 (p.22): a sheltered sub-area (adjacent to an unheated buffer + # such as a flat's corridor) carries an added R=0.5 m²K/W → U = + # 1/(1/U + 0.5). Drives SapAlternativeWall.is_sheltered. + sheltered: bool = False @dataclass