test(elmhurst): party-ceiling BP window is vertical, not a rooflight 🟥

_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) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-27 07:06:04 +00:00
parent 6303343575
commit 23245a085c

View file

@ -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