From 862a7ccc613178c27e8c31a4b44c342a09be8426 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 25 Jun 2026 12:00:00 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Coerce=20banded=20flat=5Flocation=20string?= =?UTF-8?q?=20to=20its=20floor=20integer=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../epc/domain/tests/test_from_rdsap_schema.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/datatypes/epc/domain/tests/test_from_rdsap_schema.py b/datatypes/epc/domain/tests/test_from_rdsap_schema.py index 5a2218d3..2e689f69 100644 --- a/datatypes/epc/domain/tests/test_from_rdsap_schema.py +++ b/datatypes/epc/domain/tests/test_from_rdsap_schema.py @@ -458,6 +458,23 @@ class TestFromRdSapSchema21_0_1: assert result.led_fixed_lighting_bulbs_count == 0 + def test_banded_flat_location_string_coerced_to_band_floor_int(self) -> None: + # The gov API lodges flat_location (the flat's floor number) as a plain + # int on most certs, but on high floors as a banded string "20+" (floor + # 20 or above) — e.g. cert 2481-2122-4211-1172-4722 (UPRN 6027561). + # The epc_flat_details.flat_location column is a NOT-NULL integer, so the + # raw string crashed the modelling_e2e insert (psycopg2 + # InvalidTextRepresentation, portfolio 796 run). Coerce the band to its + # floor (20); Elmhurst caps the floor number at 30. + data = load("21_0_1.json") + data["sap_flat_details"]["flat_location"] = "20+" + schema = from_dict(RdSapSchema21_0_1, data) + + result = EpcPropertyDataMapper.from_rdsap_schema_21_0_1(schema) + + assert result.sap_flat_details is not None + assert result.sap_flat_details.flat_location == 20 + def test_uprn(self, result: EpcPropertyData) -> None: assert result.uprn == 12457 From 48760971a7cd02fa35cca519fb624a93482cfd96 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 25 Jun 2026 12:01:44 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Coerce=20banded=20flat=5Flocation=20string?= =?UTF-8?q?=20to=20its=20floor=20integer=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 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 0f1b8bad..bc8c7957 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -2549,7 +2549,9 @@ class EpcPropertyDataMapper: SapFlatDetails( level=schema.sap_flat_details.level, top_storey=schema.sap_flat_details.top_storey, - flat_location=schema.sap_flat_details.flat_location, + flat_location=_flat_location_int( + schema.sap_flat_details.flat_location + ), heat_loss_corridor=schema.sap_flat_details.heat_loss_corridor, storey_count=schema.sap_flat_details.storey_count, unheated_corridor_length_m=( @@ -3111,6 +3113,17 @@ def _clear_basement_flag_when_system_built( return replace(epc, sap_building_parts=new_parts) +def _flat_location_int(value: Any) -> int: + """`sap_flat_details.flat_location` is the flat's floor number, lodged by the + gov API as a plain int on most certs but as a banded string `"20+"` (floor 20 + or above) on high floors. The `epc_flat_details.flat_location` column is a + NOT-NULL integer, so the raw band crashed the modelling_e2e insert. Coerce the + band to its floor (`"20+"` → 20); Elmhurst caps the floor number at 30.""" + if isinstance(value, int): + return value + return int(str(value).strip().rstrip("+")) + + def _measurement_value(field: Any) -> float: """SAP measurements arrive as a `Measurement` (with `.value`), a raw dict {'value': N, 'quantity': '...'} when `from_dict` didn't coerce, or a plain