mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Map non-separated conservatory on the gov-API RdSAP-19.0 path 🟩
Mirror the merged 21.0.1 fix (d501535c): declare the four glazed conservatory
fields on the 19.0 SapBuildingPart so from_dict stops dropping them, exclude the
glazed BP from the fabric loop, and carry it as EpcPropertyData.sap_conservatory
(§6.1). Fixes cert 718138 (conservatory_type=4) mis-scoring as a fabric part and
failing to persist on the NOT-NULL construction_age_band.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f6ec96fbcd
commit
56add97bd9
3 changed files with 89 additions and 0 deletions
|
|
@ -1568,8 +1568,14 @@ class EpcPropertyDataMapper:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
# RdSAP 10 §6.1 — exclude the glazed conservatory BP from the
|
||||||
|
# fabric loop; it is carried as `sap_conservatory` below and
|
||||||
|
# billed by the §6.1 cascade (window/rooflight/floor), not as
|
||||||
|
# a dwelling building part. Mirrors the 21.0.1 path.
|
||||||
for bp in schema.sap_building_parts
|
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
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -2433,3 +2433,76 @@ class TestNonSeparatedConservatoryApiMirror:
|
||||||
# Assert — §6.2: disregarded; no conservatory geometry.
|
# Assert — §6.2: disregarded; no conservatory geometry.
|
||||||
assert epc.sap_conservatory is None
|
assert epc.sap_conservatory is None
|
||||||
assert conservatory_geometry(epc) is None
|
assert conservatory_geometry(epc) is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestNonSeparatedConservatoryApiMirror19_0:
|
||||||
|
"""RdSAP 10 §6.1 — the same glazed-BP conservatory the 21.0.1 mapper
|
||||||
|
handles is also lodged on RdSAP-Schema-19.0 (repro cert 718138). The four
|
||||||
|
glazed fields were undeclared on the 19.0 `SapBuildingPart`, so `from_dict`
|
||||||
|
dropped them: the conservatory mapped to a fabric-less building part that
|
||||||
|
mis-scored and could not persist (NOT-NULL `construction_age_band`). The
|
||||||
|
19.0 mapper now mirrors the 21.0.1 split.
|
||||||
|
|
||||||
|
Unlike 21.0.1, the 19.0 mapper keeps the **lodged** `total_floor_area`
|
||||||
|
scalar (a real 19.0 cert already lodges the non-separated conservatory in
|
||||||
|
it — 718138: dwelling 140.23 + conservatory 9.71 = 149.94 → lodged 150), so
|
||||||
|
appending a conservatory does NOT change `total_floor_area_m2`. The §6.1
|
||||||
|
floor-area fold reaches the score through the calculator's dimensions, not
|
||||||
|
through the scalar."""
|
||||||
|
|
||||||
|
def test_from_api_response_splits_out_conservatory_building_part(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
# Arrange — the 19.0 dwelling plus a non-separated double-glazed
|
||||||
|
# conservatory glazed BP.
|
||||||
|
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("19_0.json")).sap_building_parts
|
||||||
|
)
|
||||||
|
|
||||||
|
cert = load("19_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.
|
||||||
|
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
|
||||||
|
# §6.1 fold is active (surfaces derived by the shared cascade).
|
||||||
|
assert conservatory_geometry(epc) is not None
|
||||||
|
|
||||||
|
def test_separated_conservatory_lodges_no_glazed_building_part(self) -> None:
|
||||||
|
# Arrange — a separated conservatory (type 2/3) lodges NO glazed BP;
|
||||||
|
# the dwelling is unchanged.
|
||||||
|
from domain.sap10_calculator.worksheet.conservatory import (
|
||||||
|
conservatory_geometry,
|
||||||
|
)
|
||||||
|
|
||||||
|
cert = load("19_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
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,16 @@ class SapBuildingPart:
|
||||||
wall_insulation_thickness: Optional[str] = None
|
wall_insulation_thickness: Optional[str] = None
|
||||||
floor_insulation_thickness: Optional[str] = None
|
floor_insulation_thickness: Optional[str] = None
|
||||||
flat_roof_insulation_thickness: Optional[Union[str, int]] = None
|
flat_roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||||
|
# RdSAP 10 §6.1 — a NON-SEPARATED conservatory (`conservatory_type == 4`) is
|
||||||
|
# lodged by the gov API as a glazed "building part" carrying ONLY these four
|
||||||
|
# fields (no fabric, no floor dimensions). Previously undeclared → dropped by
|
||||||
|
# `from_dict`, so the conservatory was silently lost on the 19.0 path. The
|
||||||
|
# mapper splits this BP out into `EpcPropertyData.sap_conservatory`. Mirrors
|
||||||
|
# the 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
|
@dataclass
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue