From c3fd9a68722cccf5b7477dfc87f39e3c91cdca4b Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 15 Jun 2026 13:05:38 +0000 Subject: [PATCH] =?UTF-8?q?Map=20full-SAP=20cert=20identity=20and=20scalar?= =?UTF-8?q?=20fields=20to=20EpcPropertyData=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/mapper.py | 58 +++++++++++++++++++ .../epc/domain/tests/test_from_sap_schema.py | 28 +++++++++ datatypes/epc/schema/sap_schema_17_1.py | 12 ++++ 3 files changed, 98 insertions(+) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 2c762a9e..902b05a4 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -43,6 +43,7 @@ from datatypes.epc.schema.rdsap_schema_17_1 import ( RdSapSchema17_1, EnergyElement as EnergyElement_17_1, ) +from datatypes.epc.schema.sap_schema_17_1 import SapSchema17_1 from datatypes.epc.schema.rdsap_schema_18_0 import ( RdSapSchema18_0, EnergyElement as EnergyElement_18_0, @@ -619,6 +620,63 @@ class EpcPropertyDataMapper: ], ) + @staticmethod + def from_sap_schema_17_1(schema: SapSchema17_1) -> EpcPropertyData: + # Built incrementally via TDD — see scripts/hyde/mapping_decisions.md. + # Tracer slice: identity + scalars are mapped; the load-bearing + # collections (walls/floors/roofs, sap_windows, sap_building_parts, + # door/room counts) are left empty here and filled by later slices + # (D1 perimeter, D2 openings, D3 living-area, D4 fabric-U). + return EpcPropertyData( + uprn=schema.uprn, + dwelling_type=( + schema.dwelling_type + if isinstance(schema.dwelling_type, str) + else schema.dwelling_type.value + ), + inspection_date=date.fromisoformat(schema.inspection_date), + tenure=str(schema.tenure), + transaction_type=str(schema.transaction_type), + address_line_1=schema.address_line_1, + postcode=schema.postcode, + post_town=schema.post_town, + total_floor_area_m2=float(schema.total_floor_area), + has_hot_water_cylinder=schema.has_hot_water_cylinder == "true", + has_fixed_air_conditioning=schema.has_fixed_air_conditioning == "true", + solar_water_heating=False, + door_count=0, + wet_rooms_count=0, + extensions_count=0, + heated_rooms_count=0, + open_chimneys_count=0, + habitable_rooms_count=0, + insulated_door_count=0, + cfl_fixed_lighting_bulbs_count=0, + led_fixed_lighting_bulbs_count=0, + incandescent_fixed_lighting_bulbs_count=0, + roofs=[], + walls=[], + floors=[], + main_heating=[], + sap_windows=[], + sap_building_parts=[], + sap_heating=SapHeating( + instantaneous_wwhrs=InstantaneousWwhrs(), + main_heating_details=[], + has_fixed_air_conditioning=schema.has_fixed_air_conditioning == "true", + ), + sap_energy_source=SapEnergySource( + mains_gas=False, + meter_type="", + pv_battery_count=0, + wind_turbines_count=0, + gas_smart_meter_present=False, + is_dwelling_export_capable=False, + wind_turbines_terrain_type="", + electricity_smart_meter_present=False, + ), + ) + @staticmethod def from_rdsap_schema_17_1(schema: RdSapSchema17_1) -> EpcPropertyData: es = schema.sap_energy_source diff --git a/datatypes/epc/domain/tests/test_from_sap_schema.py b/datatypes/epc/domain/tests/test_from_sap_schema.py index 980aa95a..715040c1 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema.py @@ -16,6 +16,8 @@ from typing import Any, Dict import pytest +from datatypes.epc.domain.epc_property_data import EpcPropertyData +from datatypes.epc.domain.mapper import EpcPropertyDataMapper from datatypes.epc.schema.sap_schema_17_1 import SapSchema17_1 from datatypes.epc.schema.tests.helpers import from_dict @@ -47,3 +49,29 @@ class TestSapSchema17_1Parsing: # Assert assert schema.uprn == 10092973954 assert schema.total_floor_area == 68 + + +class TestFromSapSchema17_1Tracer: + """Slice 2: the mapper produces a valid EpcPropertyData from a full-SAP cert.""" + + @pytest.mark.parametrize("fixture", _ALL_FIXTURES) + def test_produces_epc_property_data(self, fixture: str) -> None: + # Arrange + schema = from_dict(SapSchema17_1, load(fixture)) + + # Act + result = EpcPropertyDataMapper.from_sap_schema_17_1(schema) + + # Assert + assert isinstance(result, EpcPropertyData) + + def test_maps_sample_uprn_and_floor_area(self) -> None: + # Arrange + schema = from_dict(SapSchema17_1, load("sap_17_1.json")) + + # Act + result = EpcPropertyDataMapper.from_sap_schema_17_1(schema) + + # Assert + assert result.uprn == 10092973954 + assert result.total_floor_area_m2 == 68.0 diff --git a/datatypes/epc/schema/sap_schema_17_1.py b/datatypes/epc/schema/sap_schema_17_1.py index 4dc5e25f..3b8995cf 100644 --- a/datatypes/epc/schema/sap_schema_17_1.py +++ b/datatypes/epc/schema/sap_schema_17_1.py @@ -12,9 +12,21 @@ raises only on a missing *required* field. from dataclasses import dataclass from typing import Union +from .common import DescriptionV1 + @dataclass class SapSchema17_1: uprn: int schema_type: str total_floor_area: Union[int, float] + # full SAP lodges dwelling_type as a localised object OR a plain string. + dwelling_type: Union[str, DescriptionV1] + tenure: Union[str, int] + transaction_type: int + address_line_1: str + postcode: str + post_town: str + inspection_date: str + has_hot_water_cylinder: str + has_fixed_air_conditioning: str