From c6c07c2c51cee18465fc55c675e2516b81f18561 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 29 Jun 2026 22:50:34 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Full-SAP=2018-hour=20off-peak=20tariff=20(c?= =?UTF-8?q?ode=205)=20must=20resolve=20to=20EIGHTEEN=5FHOUR=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full-SAP energy_tariff code space includes 5=off-peak-18-hour, but _sap_17_1_meter_type only maps 1-4, dropping code 5 to "" → STANDARD. Real SAP-19.1.0/19.2.0 certs lodge tariff 5, so the 18-hour off-peak split is silently lost. Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/tests/test_from_sap_schema.py | 1 + 1 file changed, 1 insertion(+) diff --git a/datatypes/epc/domain/tests/test_from_sap_schema.py b/datatypes/epc/domain/tests/test_from_sap_schema.py index 7a682c8d7..ec9981009 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema.py @@ -181,6 +181,7 @@ class TestFullSapElectricityTariffTranslation: (2, "SEVEN_HOUR"), # off-peak 7-hour → Economy 7 (3, "SEVEN_HOUR"), # off-peak 10-hour → dual (§12 resolves 7/10hr) (4, "TWENTY_FOUR_HOUR"), # 24-hour tariff (e.g. property 709874 — unchanged) + (5, "EIGHTEEN_HOUR"), # off-peak 18-hour — was dropped to "" → STANDARD (None, "STANDARD"), # absent/ND → unknown → standard ], ) From 7866b4a34c907208b5c382a1b5a12b308f58c8d3 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 29 Jun 2026 22:55:40 +0000 Subject: [PATCH 2/3] =?UTF-8?q?Translate=20full-SAP=2018-hour=20off-peak?= =?UTF-8?q?=20tariff=20(code=205)=20to=20EIGHTEEN=5FHOUR=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add code 5 → "off-peak 18 hour" to _SAP_TARIFF_TO_RDSAP_METER_TYPE so the full-SAP meter translation covers the whole energy_tariff code space, not just 1-4. Table 12a resolves the word alias to Tariff.EIGHTEEN_HOUR. Real SAP-19.1.0/19.2.0 sample certs lodge tariff 5 and now resolve to the 18-hour off-peak split instead of defaulting to STANDARD (under/over-rated). The same fix covers 17.0/17.1/18.0.0/19.0.0 — all route through the full-SAP mapper via the assessment_type=SAP gate. Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/mapper.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index ec27d6fcb..e89062610 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -2966,8 +2966,9 @@ def _sap_17_1_meter_type(electricity_tariff: Optional[int]) -> str: The two code spaces *differ* (epc_codes.csv `energy_tariff` vs `_METER_INT_TO_TARIFF`): full-SAP 1=standard / 2=off-peak-7hr / 3=off-peak- - 10hr / 4=24-hour, whereas RdSAP meter 1=dual-7hr / 2=single / 3=unknown / - 4=24-hour. Passing the full-SAP code straight through (the prior bug) read a + 10hr / 4=24-hour / 5=off-peak-18hr, whereas RdSAP meter 1=dual-7hr / + 2=single / 3=unknown / 4=24-hour. Passing the full-SAP code straight + through (the prior bug) read a standard-tariff cert as Economy 7 (over-rated) and an Economy-7 cert as single (under-rated). Map onto the RdSAP word aliases so the resolved tariff is correct; absent/ND → "" (the unknown→standard sentinel).""" @@ -2985,6 +2986,7 @@ _SAP_TARIFF_TO_RDSAP_METER_TYPE: dict[int, str] = { 2: "dual", # off-peak 7 hour (Economy 7) 3: "dual", # off-peak 10 hour (§12 dispatch resolves 7/10hr) 4: "dual (24 hour)", # 24-hour tariff + 5: "off-peak 18 hour", # off-peak 18 hour (Table 12a EIGHTEEN_HOUR) } From 450ecfb9b4aea3915b0dcb1fbfb3f3aafdc3f39d Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 29 Jun 2026 22:58:35 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Lock=20full-SAP=2016.x=20tariff=20translati?= =?UTF-8?q?on=20via=20the=20assessment=5Ftype=20gate=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirming regression test for the prime-lead suspicion (SAP-16.x meter over-rating). A full-SAP-shaped 16.x cert lodges the SAP energy_tariff code space; the assessment_type=SAP gate routes it to the full-SAP mapper, which translates the code (1=standard → STANDARD) rather than raw-passing it to the RdSAP normaliser (where 1="dual" → SEVEN_HOUR, the over-rating bug). No fix needed — the gate already prevents the drop; this pins that behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../epc/domain/tests/test_from_sap_schema.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/datatypes/epc/domain/tests/test_from_sap_schema.py b/datatypes/epc/domain/tests/test_from_sap_schema.py index ec9981009..957cc4a44 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema.py @@ -870,6 +870,27 @@ class TestFullSapSchema16xRouting: assert epc.door_count == 1 assert epc.dwelling_type == "Detached house" + def test_full_sap_16_x_electricity_tariff_is_translated_not_passed_through( + self, + ) -> None: + # A full-SAP 16.x cert lodges the SAP `energy_tariff` code space, not the + # RdSAP `meter_type` one. Routing it through the full-SAP mapper (via the + # assessment_type=SAP gate) must apply `_sap_17_1_meter_type`: tariff 1 = + # standard → STANDARD. Were it raw-passed to the RdSAP normaliser, code 1 + # = "dual" → SEVEN_HOUR (the prime-lead over-rating bug this guards). + from domain.sap10_calculator.tables.table_12a import ( + Tariff, + tariff_from_meter_type, + ) + + data = load("sap_16_0_full.json") # electricity_tariff = 1 (standard) + + epc = EpcPropertyDataMapper.from_api_response(data) + + assert tariff_from_meter_type(epc.sap_energy_source.meter_type) is ( + Tariff.STANDARD + ) + def test_reduced_16_x_cert_unaffected_by_full_sap_routing(self) -> None: # Arrange — a reduced 16.2 cert (assessment_type RdSAP) must stay on the # RdSAP path, keeping its top-level property_type.