mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Drop lodged building parts that carry no construction age band
2 modelling_e2e properties (LODGED, e.g. uprn 100061733327 cert
3235-4627-4400-0610-7292 / uprn 100020397707 cert 8081-7022-7669-5038-0996)
failed with NotNullViolation on epc_building_part.construction_age_band.
The certs lodge a leading building part with floor geometry but no construction
descriptors — {floor_area, room_height, double_glazed, glazed_perimeter}, no age
band / identifier / wall / roof. A glazed-perimeter conservatory-shell / lodging
artifact. It maps to construction_age_band=None → violates the NOT-NULL column,
and the engine reads parts[0].construction_age_band as the primary era (so the
junk part also poisoned the calc's primary age band).
Fix: from_api_response drops building parts with no construction_age_band, but
only when banded parts remain (a fully band-less cert is left untouched). The
real Main + Extension parts survive and parts[0] becomes the real Main Dwelling.
Verified both certs now map with only their banded parts. Regression test +
mapper-corpus / accuracy gates green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f7f74ea72b
commit
ae81a81e79
2 changed files with 59 additions and 1 deletions
|
|
@ -2685,7 +2685,10 @@ class EpcPropertyDataMapper:
|
|||
|
||||
return _clear_basement_flag_when_system_built(
|
||||
_with_renewable_heat_incentive(
|
||||
_with_recorded_performance(mapped, data), data
|
||||
_with_recorded_performance(
|
||||
_drop_building_parts_without_age_band(mapped), data
|
||||
),
|
||||
data,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -2987,6 +2990,35 @@ def _optional_float(value: Any) -> Optional[float]:
|
|||
return float(value) if value is not None else None
|
||||
|
||||
|
||||
def _drop_building_parts_without_age_band(
|
||||
epc: EpcPropertyData,
|
||||
) -> EpcPropertyData:
|
||||
"""Drop building parts that carry no `construction_age_band`.
|
||||
|
||||
Some gov-API certs lodge a leading building part with floor geometry but no
|
||||
construction descriptors (no age band, identifier or wall/roof construction)
|
||||
— a glazed-perimeter "conservatory shell" / lodging artifact, e.g. cert
|
||||
3235-4627-4400-0610-7292's part 0 `{floor_area, room_height, glazed_perimeter,
|
||||
double_glazed}`. A part with no age band cannot have its RdSAP fabric U-values
|
||||
derived (the engine reads `parts[0].construction_age_band` as the primary era),
|
||||
and it violates the epc_building_part NOT-NULL column on persist.
|
||||
|
||||
Drops such parts only when at least one banded part remains, so a fully
|
||||
band-less cert (e.g. full SAP, measured-U) is left untouched. Returns the same
|
||||
object when nothing changes."""
|
||||
parts = epc.sap_building_parts
|
||||
banded = [
|
||||
p
|
||||
for p in parts
|
||||
# construction_age_band is typed str but the gov API genuinely omits it
|
||||
# on these artifact parts, so the runtime None check is real.
|
||||
if p.construction_age_band is not None # pyright: ignore[reportUnnecessaryComparison]
|
||||
]
|
||||
if banded and len(banded) != len(parts):
|
||||
return replace(epc, sap_building_parts=banded)
|
||||
return epc
|
||||
|
||||
|
||||
def _clear_basement_flag_when_system_built(
|
||||
epc: EpcPropertyData,
|
||||
) -> EpcPropertyData:
|
||||
|
|
|
|||
|
|
@ -191,6 +191,32 @@ class TestFromRdSapSchema20_0_0:
|
|||
schema = from_dict(RdSapSchema20_0_0, load("20_0_0.json"))
|
||||
return EpcPropertyDataMapper.from_rdsap_schema_20_0_0(schema)
|
||||
|
||||
def test_leading_building_part_without_age_band_is_dropped(self) -> None:
|
||||
# Some gov-API certs lodge a leading building part with floor geometry but
|
||||
# no construction descriptors (no age band / identifier / wall / roof) — a
|
||||
# conservatory-shell / lodging artifact (e.g. cert 3235-4627-4400-0610-7292
|
||||
# part 0). It maps to construction_age_band=None, which violates the
|
||||
# epc_building_part NOT-NULL column on persist (and the engine reads
|
||||
# parts[0].construction_age_band as the primary era). It must be dropped,
|
||||
# leaving the real banded parts.
|
||||
data = load("20_0_0.json")
|
||||
data["sap_building_parts"].insert(
|
||||
0,
|
||||
{
|
||||
"floor_area": 11.22,
|
||||
"room_height": 1,
|
||||
"double_glazed": "N",
|
||||
"glazed_perimeter": 10.31,
|
||||
},
|
||||
)
|
||||
|
||||
epc = EpcPropertyDataMapper.from_api_response(data)
|
||||
|
||||
assert len(epc.sap_building_parts) == 1
|
||||
assert all(
|
||||
bp.construction_age_band is not None for bp in epc.sap_building_parts
|
||||
)
|
||||
|
||||
def test_photovoltaic_supply_as_dict_list_is_mapped_not_crashed(self) -> None:
|
||||
# 20.0.0 types `photovoltaic_supply` as the wrapper only (not the 21.0.x
|
||||
# Union), so a cert lodging measured arrays as a LIST leaves the leaves as
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue