test(exposure): mid-floor-labelled flat with a lodged exposed roof 🟥

A gov-API cert can lodge dwelling_type="Mid-floor flat" while carrying a
real exposed roof element (roof_construction != 7 "dwelling above") over a
"(another dwelling below)" floor — i.e. a top-floor flat mislabelled
mid-floor. Property 715363 (uprn 6027561) + sibling 715395 (6027563) do
exactly this; the correctly-labelled top-floor sibling 715871 (6027574),
same block + same flat roof, already computes the lodged SAP 74.

_dwelling_exposure keys roof exposure on the dwelling_type label alone, so
it drops the roof heat-loss term, under-reading space-heating demand ~32%
(calc 1833 vs lodged RHI 2694) and over-reading SAP +7 (81 vs 74).

Pins the fix: a mid-floor label + lodged exposed roof must expose the roof
(floor stays party). Also corrects the existing mid-floor fixture to lodge
the party-ceiling code 7 (the default 4 is an exposed pitched roof).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-26 20:59:31 +00:00
parent 6303343575
commit 7a74aaecb9

View file

@ -5690,7 +5690,10 @@ def test_mid_floor_flat_dwelling_type_zeroes_floor_and_roof_heat_transmission()
# Arrange — A "Mid-floor flat" has party floor (downstairs flat) and
# party ceiling (upstairs flat). The mapper must wire DwellingExposure
# to suppress both channels so the HLC matches what RdSAP-driven
# assessor software produces.
# assessor software produces. A genuine mid-floor flat lodges
# roof_construction=7 ("dwelling above") — the party-ceiling code —
# NOT an external-roof construction (the default 4 = exposed pitched
# roof, which `_cert_lodges_exposed_roof` now reads as top-floor).
epc = make_minimal_sap10_epc(
total_floor_area_m2=_TYPICAL_TFA_M2,
habitable_rooms_count=4,
@ -5698,6 +5701,7 @@ def test_mid_floor_flat_dwelling_type_zeroes_floor_and_roof_heat_transmission()
dwelling_type="Mid-floor flat",
sap_building_parts=[
make_building_part(
roof_construction=7, # "dwelling above" — party ceiling
floor_dimensions=[
make_floor_dimension(total_floor_area_m2=_TYPICAL_TFA_M2, floor=0),
],
@ -5718,6 +5722,49 @@ def test_mid_floor_flat_dwelling_type_zeroes_floor_and_roof_heat_transmission()
assert inputs.heat_transmission.walls_w_per_k > 0
def test_mid_floor_label_with_exposed_roof_element_exposes_roof() -> None:
# Arrange — gov-API certs lodge `dwelling_type` as the raw assessor
# label, which can contradict the lodged fabric. Property 715363
# (uprn 6027561) and its sibling 715395 (6027563) lodge
# dwelling_type="Mid-floor flat" yet carry a real exposed flat roof
# (roof_construction=1, "Flat", with lodged insulation) over a
# "(another dwelling below)" floor — i.e. they are TOP-floor flats
# mislabelled mid-floor. Keying roof exposure on the label alone
# dropped the roof heat-loss term, under-reading space-heating demand
# ~32% and over-reading SAP +7 (calc 81 vs lodged 74). The correctly
# labelled top-floor sibling 715871 (6027574), same block + same flat
# roof, already computes the lodged 74 — the ground truth here.
#
# The lodged roof element is the authoritative signal: an assessor
# only lodges a non-party roof_construction (anything but 7 = "dwelling
# above") when the roof is a heat-loss surface.
epc = make_minimal_sap10_epc(
total_floor_area_m2=_TYPICAL_TFA_M2,
habitable_rooms_count=4,
region_code="1",
dwelling_type="Mid-floor flat",
sap_building_parts=[
make_building_part(
roof_construction=1, # Flat — a genuine exposed roof
floor_dimensions=[
make_floor_dimension(total_floor_area_m2=_TYPICAL_TFA_M2, floor=0),
],
),
],
sap_heating=make_sap_heating(
main_heating_details=[_gas_boiler_detail(sap_main_heating_code=102)],
),
)
# Act
inputs = cert_to_inputs(epc)
# Assert — the floor stays party (dwelling below) but the lodged
# exposed roof is honoured, matching the top-floor sibling.
assert inputs.heat_transmission.floor_w_per_k == 0.0
assert inputs.heat_transmission.roof_w_per_k > 0
def test_top_floor_flat_keeps_roof_drops_floor() -> None:
# Arrange — Top-floor flat: party floor, external roof.
epc = make_minimal_sap10_epc(