mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Merge pull request #1308 from Hestia-Homes/fix/conservatory-age-band-sibling-mappers
Map non-separated conservatory on the 17.0/17.1/18.0/20.0.0 gov paths 🟩
This commit is contained in:
commit
1a37caa44d
6 changed files with 159 additions and 3 deletions
|
|
@ -711,8 +711,13 @@ class EpcPropertyDataMapper:
|
|||
roof_insulation_thickness=bp.roof_insulation_thickness,
|
||||
sap_room_in_roof=None,
|
||||
)
|
||||
# RdSAP 10 §6.1 — exclude the glazed conservatory BP from the
|
||||
# fabric loop; carried as `sap_conservatory` below. Mirrors the
|
||||
# 19.0 / 21.0.1 path.
|
||||
for bp in schema.sap_building_parts
|
||||
if getattr(bp, "glazed_perimeter", None) is None
|
||||
],
|
||||
sap_conservatory=_api_sap_conservatory(schema.sap_building_parts),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -1148,8 +1153,13 @@ class EpcPropertyDataMapper:
|
|||
roof_insulation_thickness=bp.roof_insulation_thickness,
|
||||
sap_room_in_roof=None,
|
||||
)
|
||||
# RdSAP 10 §6.1 — exclude the glazed conservatory BP from the
|
||||
# fabric loop; carried as `sap_conservatory` below. Mirrors the
|
||||
# 19.0 / 21.0.1 path.
|
||||
for bp in schema.sap_building_parts
|
||||
if getattr(bp, "glazed_perimeter", None) is None
|
||||
],
|
||||
sap_conservatory=_api_sap_conservatory(schema.sap_building_parts),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -1371,8 +1381,13 @@ class EpcPropertyDataMapper:
|
|||
else None
|
||||
),
|
||||
)
|
||||
# RdSAP 10 §6.1 — exclude the glazed conservatory BP from the
|
||||
# fabric loop; carried as `sap_conservatory` below. Mirrors the
|
||||
# 19.0 / 21.0.1 path.
|
||||
for bp in schema.sap_building_parts
|
||||
if getattr(bp, "glazed_perimeter", None) is None
|
||||
],
|
||||
sap_conservatory=_api_sap_conservatory(schema.sap_building_parts),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -1826,8 +1841,13 @@ class EpcPropertyDataMapper:
|
|||
else None
|
||||
),
|
||||
)
|
||||
# RdSAP 10 §6.1 — exclude the glazed conservatory BP from the
|
||||
# fabric loop; carried as `sap_conservatory` below. Mirrors the
|
||||
# 19.0 / 21.0.1 path.
|
||||
for bp in schema.sap_building_parts
|
||||
if getattr(bp, "glazed_perimeter", None) is None
|
||||
],
|
||||
sap_conservatory=_api_sap_conservatory(schema.sap_building_parts),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -2533,3 +2533,106 @@ class TestNonSeparatedConservatoryApiMirror19_0:
|
|||
# Assert — §6.2: disregarded; no conservatory geometry.
|
||||
assert epc.sap_conservatory is None
|
||||
assert conservatory_geometry(epc) is None
|
||||
|
||||
|
||||
class TestNonSeparatedConservatoryApiMirror20_0_0:
|
||||
"""RdSAP 10 §6.1 — the same glazed-BP conservatory, on RdSAP-Schema-20.0.0.
|
||||
Surfaced by the modelling_e2e triage: 5 type-4 certs (e.g. property 725213)
|
||||
failed `NotNullViolation: construction_age_band` because the 20.0.0 schema
|
||||
declared none of the four glazed fields, so the conservatory mapped to a
|
||||
null-age fabric part. Mirrors the 19.0 / 21.0.1 split."""
|
||||
|
||||
def test_from_api_response_splits_out_conservatory_building_part(
|
||||
self,
|
||||
) -> None:
|
||||
from datatypes.epc.domain.epc_property_data import SapConservatory
|
||||
from domain.sap10_calculator.worksheet.conservatory import (
|
||||
conservatory_geometry,
|
||||
)
|
||||
|
||||
dwelling_part_count = len(
|
||||
EpcPropertyDataMapper.from_api_response(load("20_0_0.json")).sap_building_parts
|
||||
)
|
||||
|
||||
cert = load("20_0_0.json")
|
||||
cert["conservatory_type"] = 4
|
||||
cert["sap_building_parts"].append(
|
||||
{
|
||||
"floor_area": 12.0,
|
||||
"room_height": 1,
|
||||
"double_glazed": "Y",
|
||||
"glazed_perimeter": 9.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
||||
# Assert — conservatory split out; the glazed BP is NOT a fabric part
|
||||
# (so it never hits the NOT-NULL construction_age_band on persist).
|
||||
assert epc.sap_conservatory == SapConservatory(
|
||||
floor_area_m2=12.0,
|
||||
glazed_perimeter_m=9.0,
|
||||
double_glazed=True,
|
||||
thermally_separated=False,
|
||||
room_height_storeys=1.0,
|
||||
)
|
||||
assert len(epc.sap_building_parts) == dwelling_part_count
|
||||
assert all(
|
||||
bp.construction_age_band is not None for bp in epc.sap_building_parts
|
||||
)
|
||||
assert conservatory_geometry(epc) is not None
|
||||
|
||||
def test_separated_conservatory_lodges_no_glazed_building_part(self) -> None:
|
||||
from domain.sap10_calculator.worksheet.conservatory import (
|
||||
conservatory_geometry,
|
||||
)
|
||||
|
||||
cert = load("20_0_0.json")
|
||||
cert["conservatory_type"] = 2
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
||||
# Assert — §6.2: disregarded; no conservatory geometry.
|
||||
assert epc.sap_conservatory is None
|
||||
assert conservatory_geometry(epc) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture", ["17_0.json", "17_1.json", "18_0.json"])
|
||||
def test_gov_mappers_split_non_separated_conservatory(fixture: str) -> None:
|
||||
"""RdSAP 10 §6.1 — every gov mapper splits a non-separated conservatory
|
||||
(conservatory_type=4) glazed BP out of the fabric parts and into
|
||||
`sap_conservatory`, so it never persists as a null-age fabric part (the
|
||||
modelling_e2e `construction_age_band` NOT-NULL triage). 19.0 / 20.0.0 /
|
||||
21.0.1 are covered by their own classes; this pins the rest.
|
||||
|
||||
21.0.0 is intentionally excluded: unlike every other gov schema it never
|
||||
adopted the ADR-0027 "every SapBuildingPart field is Optional" treatment, so
|
||||
a sparse conservatory part fails `from_dict` on its required fields. Porting
|
||||
it needs that broader strictness change — tracked as a follow-up; no 21.0.0
|
||||
conservatory cert surfaced in the triage."""
|
||||
from datatypes.epc.domain.epc_property_data import SapConservatory
|
||||
|
||||
base_parts = len(
|
||||
EpcPropertyDataMapper.from_api_response(load(fixture)).sap_building_parts
|
||||
)
|
||||
cert = load(fixture)
|
||||
cert["conservatory_type"] = 4
|
||||
cert["sap_building_parts"].append(
|
||||
{"floor_area": 12.0, "room_height": 1, "double_glazed": "Y", "glazed_perimeter": 9.0}
|
||||
)
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
||||
# Assert — split out, not a (null-age) fabric part.
|
||||
assert epc.sap_conservatory == SapConservatory(
|
||||
floor_area_m2=12.0,
|
||||
glazed_perimeter_m=9.0,
|
||||
double_glazed=True,
|
||||
thermally_separated=False,
|
||||
room_height_storeys=1.0,
|
||||
)
|
||||
assert len(epc.sap_building_parts) == base_parts
|
||||
assert all(bp.construction_age_band is not None for bp in epc.sap_building_parts)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,14 @@ class SapBuildingPart:
|
|||
wall_insulation_thickness: Optional[str] = None
|
||||
floor_insulation_thickness: Optional[str] = None
|
||||
flat_roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||
# RdSAP 10 §6.1 — non-separated conservatory glazed-BP fields
|
||||
# ({double_glazed, floor_area, glazed_perimeter, room_height}); declared so
|
||||
# `from_dict` keeps them and the mapper splits the BP into
|
||||
# `EpcPropertyData.sap_conservatory`. Mirrors the 19.0 / 21.0.1 schema.
|
||||
floor_area: Optional[Union[Measurement, int, float]] = None
|
||||
room_height: Optional[Union[Measurement, int, float]] = None
|
||||
double_glazed: Optional[str] = None
|
||||
glazed_perimeter: Optional[Union[Measurement, int, float]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -106,6 +106,14 @@ class SapBuildingPart:
|
|||
# Can be a thickness string (e.g. "100mm") or 0 for uninsulated flat roofs
|
||||
roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||
wall_insulation_thickness: Optional[str] = None
|
||||
# RdSAP 10 §6.1 — non-separated conservatory glazed-BP fields
|
||||
# ({double_glazed, floor_area, glazed_perimeter, room_height}); declared so
|
||||
# `from_dict` keeps them and the mapper splits the BP into
|
||||
# `EpcPropertyData.sap_conservatory`. Mirrors the 19.0 / 21.0.1 schema.
|
||||
floor_area: Optional[Union[Measurement, int, float]] = None
|
||||
room_height: Optional[Union[Measurement, int, float]] = None
|
||||
double_glazed: Optional[str] = None
|
||||
glazed_perimeter: Optional[Union[Measurement, int, float]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -121,6 +121,14 @@ class SapBuildingPart:
|
|||
wall_insulation_thickness: Optional[str] = None
|
||||
floor_insulation_thickness: Optional[str] = None
|
||||
flat_roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||
# RdSAP 10 §6.1 — non-separated conservatory glazed-BP fields
|
||||
# ({double_glazed, floor_area, glazed_perimeter, room_height}); declared so
|
||||
# `from_dict` keeps them and the mapper splits the BP into
|
||||
# `EpcPropertyData.sap_conservatory`. Mirrors the 19.0 / 21.0.1 schema.
|
||||
floor_area: Optional[Union[Measurement, int, float]] = None
|
||||
room_height: Optional[Union[Measurement, int, float]] = None
|
||||
double_glazed: Optional[str] = None
|
||||
glazed_perimeter: Optional[Union[Measurement, int, float]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -131,9 +131,12 @@ class SapBuildingPart:
|
|||
# ADR-0027: 17/1000 certs lodge a CONSERVATORY-shaped building part carrying
|
||||
# only {double_glazed, floor_area, glazed_perimeter, room_height} — none of
|
||||
# the wall/roof/floor construction fields below. Following the 21.0.1
|
||||
# precedent every field is Optional, so a conservatory part parses to an
|
||||
# all-None SapBuildingPart; its thermal effect is carried separately by the
|
||||
# cert-level conservatory_type, so the empty part flows through harmlessly.
|
||||
# precedent every field is Optional. RdSAP 10 §6.1: the four glazed fields
|
||||
# are declared so `from_dict` keeps them and the mapper can split this BP out
|
||||
# into `EpcPropertyData.sap_conservatory` (a non-separated conservatory,
|
||||
# `conservatory_type == 4`). Previously undeclared → dropped → the part
|
||||
# mapped to an all-None fabric part that mis-scored AND violated the
|
||||
# NOT-NULL `construction_age_band` column on persist (modelling_e2e triage).
|
||||
identifier: Optional[str] = None
|
||||
wall_dry_lined: Optional[str] = None
|
||||
floor_heat_loss: Optional[int] = None
|
||||
|
|
@ -153,6 +156,12 @@ class SapBuildingPart:
|
|||
wall_insulation_thickness: Optional[str] = None
|
||||
floor_insulation_thickness: Optional[str] = None
|
||||
flat_roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||
# RdSAP 10 §6.1 — non-separated conservatory glazed-BP fields (see the
|
||||
# class docstring). Mirrors the 19.0 / 21.0.1 schema declaration.
|
||||
floor_area: Optional[Union[Measurement, int, float]] = None
|
||||
room_height: Optional[Union[Measurement, int, float]] = None
|
||||
double_glazed: Optional[str] = None
|
||||
glazed_perimeter: Optional[Union[Measurement, int, float]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue