mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Merge pull request #1452 from Hestia-Homes/fix/roof-construction-another-premises-above
Map API roof_construction code 9 ("another premises above") to None
This commit is contained in:
commit
6172f5d30d
2 changed files with 60 additions and 28 deletions
|
|
@ -4423,28 +4423,47 @@ _API_FLOOR_CONSTRUCTION_TO_STR: Dict[int, Optional[str]] = {
|
|||
# enum (1=Flat, 3=Pitched no-access, 5=Vaulted, etc.) is mapped as
|
||||
# best-effort against SAP10 nomenclature.
|
||||
#
|
||||
# Codes 6 and 7 → None. This field is read ONLY for the sloping-ceiling
|
||||
# inclined factor; the base roof U-value comes from the global
|
||||
# roofs[].description, so a non-sloping code carries no information the
|
||||
# cascade consumes here, and None correctly avoids the cos(30°) false-
|
||||
# trigger:
|
||||
# Code 6 → None. This field is read ONLY for the sloping-ceiling inclined
|
||||
# factor; the base roof U-value comes from the global roofs[].description,
|
||||
# so a non-sloping code carries no information the cascade consumes here:
|
||||
# 6 = "Thatched, with additional insulation" — its U is set by the
|
||||
# global description; not a sloping ceiling.
|
||||
# 7 = "(same dwelling above)" / "(another dwelling above)" — an
|
||||
# internal ceiling with no roof heat loss (the roof-side analogue
|
||||
# of floor_construction code 0). Heat loss is governed by the
|
||||
# roof_heat_loss / description path, not this field.
|
||||
# Empirically inert: roof W/K is identical whether 6/7 map to None or to
|
||||
# an explicit pitched string across all code-6/7 certs in the 2026
|
||||
# sample (were raising UnmappedApiCode, blocking the cert).
|
||||
# global description; not a sloping ceiling. None correctly avoids
|
||||
# the cos(30°) false-trigger.
|
||||
#
|
||||
# Codes 7 and 9 → "(another dwelling above)", NOT None. RdSAP 10
|
||||
# Specification (10-06-2025) §5.2.5 (p.31): "If a dwelling or part of a
|
||||
# dwelling has commercial premises above record as another dwelling
|
||||
# above" — i.e. premises-above (code 9) gets the SAME treatment as
|
||||
# dwelling-above (code 7), not the semi-exposed "partially heated space"
|
||||
# treatment used for premises *below* (see `_API_FLOOR_HEAT_LOSS_ABOVE_
|
||||
# PARTIALLY_HEATED`) — the spec's above/below split is asymmetric. Per
|
||||
# spec p.~55: "There is no heat loss through the roof of a building part
|
||||
# that has the same dwelling above or another dwelling above" — zero
|
||||
# heat loss, mirrored by the landlord-override system's independent
|
||||
# `roof_party_ceiling_guard` (`domain/epc/property_overrides/`), which
|
||||
# already resolves this exact "(another premises above)" marker to a
|
||||
# ~0-heat-loss RoofType citing the same spec line.
|
||||
#
|
||||
# `heat_transmission.py`'s per-part suppression (`part_roof_is_party =
|
||||
# "another dwelling above" in roof_type`) only fires when this field
|
||||
# CONTAINS that literal substring — it existed already for the Elmhurst
|
||||
# path (which sets it via `_strip_code(roof.roof_type)`) but the API path
|
||||
# previously mapped 7 (and, before this fix, 9) to None, so the
|
||||
# suppression never fired for ANY API-derived cert: `has_exposed_roof`
|
||||
# (the dwelling_type-label-only fallback) was the sole gate, and it has
|
||||
# no way to know about a per-part party-ceiling lodgement. Surfaced by
|
||||
# live cert uprn 10013320122 ("44 Regent House", code 9); code 7 has the
|
||||
# same latent gap (11 building parts in the RdSAP-21.0.1 corpus), fixed
|
||||
# here too rather than left half-fixed.
|
||||
_API_ROOF_CONSTRUCTION_TO_STR: Dict[int, Optional[str]] = {
|
||||
1: "Flat",
|
||||
3: "Pitched (slates/tiles), no access to loft",
|
||||
4: "Pitched (slates/tiles), access to loft",
|
||||
5: "Pitched (vaulted ceiling)",
|
||||
6: None,
|
||||
7: None,
|
||||
7: "(another dwelling above)",
|
||||
8: "Pitched, sloping ceiling",
|
||||
9: "(another dwelling above)",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1339,18 +1339,18 @@ class TestDefaultMissingPostTown:
|
|||
|
||||
class TestApiRoofConstructionCode:
|
||||
"""`_api_roof_construction_str` maps the GOV.UK API integer
|
||||
roof_construction code to the string the cascade reads ONLY for the
|
||||
"sloping ceiling" cos(30°) inclined-surface factor (Slice 89). Codes
|
||||
6 and 7 are neither sloping ceilings nor base-U drivers (the roof
|
||||
U-value comes from the global roofs[].description), so both map to
|
||||
None: code 6 = "Thatched" (its U is set by the description, not this
|
||||
field) and code 7 = "(same/another dwelling above)" — an internal
|
||||
ceiling with no roof heat loss, the roof-side analogue of
|
||||
floor_construction code 0. Empirically inert: roof W/K is identical
|
||||
whether 6/7 map to None or to an explicit pitched string across all
|
||||
code-6/7 certs in the 2026 sample."""
|
||||
roof_construction code to the string `heat_transmission.py` reads for
|
||||
two purposes: the "sloping ceiling" cos(30°) inclined-surface factor
|
||||
(Slice 89), and — for codes 7/9 — the "another dwelling above"
|
||||
substring that suppresses that building part's roof entirely (zero
|
||||
heat loss). Code 6 = "Thatched" is neither: its U-value comes from
|
||||
the global roofs[].description, not this field, so it maps to None
|
||||
(avoids the cos(30°) false-trigger). Codes 7 and 9 both map to
|
||||
"(another dwelling above)" per RdSAP 10 Specification (10-06-2025)
|
||||
§5.2.5: commercial premises above (code 9) is recorded identically to
|
||||
another dwelling above (code 7) — both zero heat loss."""
|
||||
|
||||
def test_code_7_same_dwelling_above_maps_to_none(self) -> None:
|
||||
def test_code_7_same_dwelling_above_suppresses_roof_heat_loss(self) -> None:
|
||||
# Arrange
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_roof_construction_str,
|
||||
|
|
@ -1359,9 +1359,22 @@ class TestApiRoofConstructionCode:
|
|||
# Act
|
||||
result = _api_roof_construction_str(7)
|
||||
|
||||
# Assert — None: no sloping-ceiling signal (avoids the cos(30°)
|
||||
# false-trigger); the internal ceiling has no roof heat loss.
|
||||
assert result is None
|
||||
# Assert — the party-ceiling marker, so heat_transmission.py's
|
||||
# per-part "another dwelling above" suppression fires.
|
||||
assert result == "(another dwelling above)"
|
||||
|
||||
def test_code_9_another_premises_above_suppresses_roof_heat_loss(self) -> None:
|
||||
# Arrange — RdSAP 10 Spec §5.2.5: commercial premises above is
|
||||
# recorded as "another dwelling above", same as code 7.
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_api_roof_construction_str,
|
||||
) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
# Act
|
||||
result = _api_roof_construction_str(9)
|
||||
|
||||
# Assert
|
||||
assert result == "(another dwelling above)"
|
||||
|
||||
def test_code_6_thatched_maps_to_none(self) -> None:
|
||||
# Arrange
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue