From 23245a085c811b3233e4c00fe9b7fee1fcf5a890 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 27 Jun 2026 07:06:04 +0000 Subject: [PATCH] =?UTF-8?q?test(elmhurst):=20party-ceiling=20BP=20window?= =?UTF-8?q?=20is=20vertical,=20not=20a=20rooflight=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _is_elmhurst_roof_window mis-routes a vertical window (Location "External wall") to sap_roof_windows when the building part's roof is a PARTY ceiling (A "Another dwelling above" / NR "Non-residential space above"). A party ceiling has no external roof, so it cannot host a rooflight. Surfaced by simulated case 53 (cert 000565 re-keyed as a mid-floor electric- storage flat, roof "A Another dwelling above"): its External-wall window (U 2.00) routed to sap_roof_windows -> window area not deducted from the wall (wall over-counted ~7 W/K) + priced as a roof window -> our SAP 74.0 vs Elmhurst worksheet 75. Elmhurst lodges it Type "Window", Location "External wall". Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_mapper_roof_window_classification.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/datatypes/epc/domain/test_mapper_roof_window_classification.py diff --git a/tests/datatypes/epc/domain/test_mapper_roof_window_classification.py b/tests/datatypes/epc/domain/test_mapper_roof_window_classification.py new file mode 100644 index 00000000..fc0c8a85 --- /dev/null +++ b/tests/datatypes/epc/domain/test_mapper_roof_window_classification.py @@ -0,0 +1,89 @@ +"""Mapper boundary: Elmhurst §11 window vs roof window classification. + +`_is_elmhurst_roof_window` must NOT route a vertical window (lodged Location +"External wall") to `sap_roof_windows` when the building part's roof is a +PARTY ceiling — "A Another dwelling above" / "NR Non-residential space above" +/ "S Same dwelling above". A party ceiling has no external roof structure, so +it cannot host a rooflight (RdSAP 10 §3.7.1: a rooflight is glazing in a roof; +party codes carry no heat-loss roof per `_ELMHURST_PARTY_ROOF_CODES`). + +Surfaced by "simulated case 53" (cert 000565 re-keyed as a mid-floor electric- +storage flat, roof "A Another dwelling above"): its one External-wall window +(U 2.00) was mis-routed to `sap_roof_windows`, so the window area was never +deducted from the wall (wall over-counted ~7 W/K) and it was priced as a roof +window — our SAP 74.0 vs Elmhurst worksheet 75. Elmhurst's own entry lodges it +Type "Window", Location "External wall". +""" + +from datatypes.epc.surveys.elmhurst_site_notes import ( + RoofDetails, + Window as ElmhurstWindow, +) +from datatypes.epc.surveys.elmhurst_site_notes import ElmhurstSiteNotes +from datatypes.epc.domain.mapper import ( + _is_elmhurst_roof_window, # pyright: ignore[reportPrivateUsage] +) + + +def _external_wall_window(*, glazing: str, u_value: float) -> ElmhurstWindow: + return ElmhurstWindow( + width_m=15.14, + height_m=1.0, + area_m2=15.14, + glazing_type=glazing, + frame_factor=0.7, + building_part="Main", + location="External wall", + orientation="North", + data_source="Manufacturer", + u_value=u_value, + g_value=0.72, + draught_proofed=True, + permanent_shutters="None", + frame_type="PVC", + ) + + +def _survey_with_main_roof(roof_type: str) -> ElmhurstSiteNotes: + """Partial `ElmhurstSiteNotes` carrying only what the roof-window + classifier reads: `roof`, `room_in_roof`, `extensions`.""" + survey = object.__new__(ElmhurstSiteNotes) + survey.roof = RoofDetails( + roof_type=roof_type, insulation="", u_value_known=False + ) + survey.room_in_roof = None + survey.extensions = [] + return survey + + +def test_external_wall_window_on_party_ceiling_is_vertical() -> None: + # Arrange — mid-floor flat: Main BP roof is "Another dwelling above" + # (party ceiling, no external roof). Window lodged on External wall. + window = _external_wall_window( + glazing="Double between 2002 and 2021", u_value=2.0 + ) + survey = _survey_with_main_roof("A Another dwelling above") + + # Act / Assert — vertical, not a rooflight. + assert _is_elmhurst_roof_window(window, survey) is False + + +def test_external_wall_window_under_non_residential_space_is_vertical() -> None: + window = _external_wall_window( + glazing="Double between 2002 and 2021", u_value=2.0 + ) + survey = _survey_with_main_roof("NR Non-residential space above") + + assert _is_elmhurst_roof_window(window, survey) is False + + +def test_roof_of_room_location_still_classified_as_rooflight() -> None: + # Guard: an explicit "Roof of Room" location stays a rooflight + # (unaffected by the party-ceiling fix). + window = _external_wall_window( + glazing="Double between 2002 and 2021", u_value=2.0 + ) + window.location = "Roof of Room" + survey = _survey_with_main_roof("A Another dwelling above") + + assert _is_elmhurst_roof_window(window, survey) is True