diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index e17b43033..cb118b083 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -596,9 +596,13 @@ class EpcPropertyDataMapper: heated_rooms_count=schema.heated_room_count, wet_rooms_count=0, extensions_count=schema.extensions_count, - open_chimneys_count=0, + # Both lodged by the schema and read by the 17.1/18.0/20.0 paths. + # open_chimneys_count was hardcoded 0 (understating infiltration); + # percent_draughtproofed was omitted entirely. + open_chimneys_count=schema.open_fireplaces_count, insulated_door_count=schema.insulated_door_count, draughtproofed_door_count=None, + percent_draughtproofed=schema.percent_draughtproofed, led_fixed_lighting_bulbs_count=0, cfl_fixed_lighting_bulbs_count=0, incandescent_fixed_lighting_bulbs_count=0, @@ -728,6 +732,19 @@ class EpcPropertyDataMapper: if getattr(bp, "glazed_perimeter", None) is None ], sap_conservatory=_api_sap_conservatory(schema.sap_building_parts), + # 17.0 lodges `mechanical_ventilation` + `built_form` but passed no + # sap_ventilation, so the §2 cascade fell back to NATURAL and its + # default sheltered_sides=2. Mirror the 19.0/17.1 path exactly (same + # gov code lists — verified in-set across the corpus, strict-coverage + # raises on any divergence). For the current corpus the score-mover + # is sheltered_sides from built_form (every 17.0 cert lodges + # mechanical_ventilation=0 → NATURAL). + sap_ventilation=SapVentilation( + sheltered_sides=_api_sheltered_sides(schema.built_form), + mechanical_ventilation_kind=_api_mechanical_ventilation_kind( + schema.mechanical_ventilation + ), + ), ) @staticmethod @@ -1540,9 +1557,13 @@ class EpcPropertyDataMapper: heated_rooms_count=schema.heated_room_count, wet_rooms_count=0, extensions_count=schema.extensions_count, - open_chimneys_count=0, + # Both lodged by the schema and read by the 17.1/18.0/20.0 paths. + # open_chimneys_count was hardcoded 0 (understating infiltration); + # percent_draughtproofed was omitted entirely. + open_chimneys_count=schema.open_fireplaces_count, insulated_door_count=schema.insulated_door_count, draughtproofed_door_count=None, + percent_draughtproofed=schema.percent_draughtproofed, led_fixed_lighting_bulbs_count=0, cfl_fixed_lighting_bulbs_count=0, incandescent_fixed_lighting_bulbs_count=0, diff --git a/tests/datatypes/epc/domain/test_mapper_ventilation_parity.py b/tests/datatypes/epc/domain/test_mapper_ventilation_parity.py new file mode 100644 index 000000000..fb31fdfea --- /dev/null +++ b/tests/datatypes/epc/domain/test_mapper_ventilation_parity.py @@ -0,0 +1,67 @@ +"""Regression: the degraded RdSAP paths (17.0, 19.0) must carry the lodged +ventilation fields, like the reference-complete 17.1/18.0/20.0 paths. + +PRD #1385 (mapper normalization). Both paths dropped ventilation lodgements the +schema carries and the SAP §2 cascade reads: + + * `open_fireplaces_count` → `open_chimneys_count` — was hardcoded 0 on both, + understating infiltration. + * `percent_draughtproofed` — was omitted on both. + * 17.0 additionally passed no `sap_ventilation` block at all, so the cascade + fell back to NATURAL + its default `sheltered_sides=2`. Restored from the + lodged `mechanical_ventilation` + `built_form` (same gov code lists the + 19.0/17.1 paths already use; strict-coverage raises on any divergence). 19.0 + already carried `sap_ventilation`, so only its two counts are restored here. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from datatypes.epc.domain.mapper import EpcPropertyDataMapper + +# 17.0 cert: open_fireplaces=1, percent_draughtproofed=100, built_form=2 +# (Semi-Detached → sheltered_sides=1, i.e. NOT the cascade default of 2), +# mechanical_ventilation=0 (natural → kind None). +_FIXTURE_17_0 = Path("tests/fixtures/epc_prediction/BD24JG/cert-f326c2524ab3.json") +# 19.0 cert: open_fireplaces=1, percent_draughtproofed=100. +_FIXTURE_19_0 = Path("tests/fixtures/epc_prediction/RM143YU/cert-720d5771bfd7.json") + + +def _load(path: Path, schema_type: str) -> dict[str, Any]: + data: dict[str, Any] = json.loads(path.read_text()) + assert data["schema_type"] == schema_type + return data + + +def test_17_0_path_carries_ventilation_lodgements() -> None: + epc = EpcPropertyDataMapper.from_api_response( + _load(_FIXTURE_17_0, "RdSAP-Schema-17.0") + ) + + # Counts that were hardcoded 0 / omitted. + assert epc.open_chimneys_count == 1 + assert epc.percent_draughtproofed == 100 + + # sap_ventilation block was absent entirely — now built from built_form + + # mechanical_ventilation. built_form 2 (Semi-Detached) → sheltered_sides 1, + # which differs from the cascade's default of 2 (the real score-mover here). + assert epc.sap_ventilation is not None + assert epc.sap_ventilation.sheltered_sides == 1 + # mechanical_ventilation 0 is natural → no mechanical kind. + assert epc.sap_ventilation.mechanical_ventilation_kind is None + + +def test_19_0_path_carries_ventilation_counts() -> None: + epc = EpcPropertyDataMapper.from_api_response( + _load(_FIXTURE_19_0, "RdSAP-Schema-19.0") + ) + + # The two counts 19.0 previously dropped. + assert epc.open_chimneys_count == 1 + assert epc.percent_draughtproofed == 100 + # 19.0 already carried sap_ventilation; confirm it still resolves built_form. + assert epc.sap_ventilation is not None + assert epc.sap_ventilation.sheltered_sides == 1