Merge pull request #1386 from Hestia-Homes/fix/rdsap-mapper-ventilation-parity

Normalize RdSAP ventilation mapping on the degraded 17.0/19.0 paths
This commit is contained in:
Jun-te Kim 2026-07-01 13:26:25 +01:00 committed by GitHub
commit cf4c56df2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 90 additions and 2 deletions

View file

@ -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,
@ -741,6 +745,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
@ -1553,9 +1570,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,

View file

@ -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