Parse 20.0.0 conservatory building parts so all 1000 certs map 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-10 15:21:00 +00:00
parent eb5bb89612
commit 12ff15e55b
2 changed files with 52 additions and 12 deletions

View file

@ -1365,3 +1365,37 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
# Assert
assert result.sap_heating.number_baths == expected_baths
assert result.sap_heating.mixer_shower_count == expected_mixers
def test_conservatory_building_part_maps_without_missing_required_field(
self,
) -> None:
# Arrange — ADR-0027: 17/1000 certs lodge a conservatory-shaped
# sap_building_part carrying only {double_glazed, floor_area,
# glazed_perimeter, room_height} — NOT the wall/roof/floor construction
# fields. The placeholder schema declared identifier (and the
# construction fields) required, so all 17 failed to parse. Following
# the 21.0.1 precedent, every SapBuildingPart field is Optional and the
# conservatory's effect is carried separately by conservatory_type, so
# the all-None part flows through harmlessly.
corpus = _load_20_0_0_corpus()
if not corpus:
pytest.skip("no RdSAP-Schema-20.0.0 corpus harvested")
cert = next(
(
c
for c in corpus
if any(
"identifier" not in part
for part in c.get("sap_building_parts", [])
)
),
None,
)
if cert is None:
pytest.skip("no corpus cert with a conservatory building part")
# Act
result = EpcPropertyDataMapper.from_api_response(cert)
# Assert
assert isinstance(result, EpcPropertyData)

View file

@ -128,18 +128,24 @@ class SapAlternativeWall:
@dataclass
class SapBuildingPart:
identifier: str
wall_dry_lined: str
floor_heat_loss: int
roof_construction: int
wall_construction: int
building_part_number: int
sap_floor_dimensions: List[SapFloorDimension]
wall_insulation_type: int
construction_age_band: str
party_wall_construction: Union[int, str]
wall_thickness_measured: str
roof_insulation_location: Union[int, str]
# 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.
identifier: Optional[str] = None
wall_dry_lined: Optional[str] = None
floor_heat_loss: Optional[int] = None
roof_construction: Optional[int] = None
wall_construction: Optional[int] = None
building_part_number: Optional[int] = None
sap_floor_dimensions: Optional[List[SapFloorDimension]] = None
wall_insulation_type: Optional[int] = None
construction_age_band: Optional[str] = None
party_wall_construction: Optional[Union[int, str]] = None
wall_thickness_measured: Optional[str] = None
roof_insulation_location: Optional[Union[int, str]] = None
# ADR-0027: absent on 254/1506 building parts (flat-roof / no-loft) → optional.
roof_insulation_thickness: Optional[Union[str, int]] = None
sap_room_in_roof: Optional[SapRoomInRoof] = None