Model/tests/domain/scenario_export/test_fabric_description.py
Khalim Conn-Kowlessar 35a0c6e972 Source export fabric from structured SAP fields; fix stale-cert selection
The "latest EPC per UPRN" selection ordered by e.id DESC, but row-id does not
track recency: a gov-EPC cert re-ingested after a PasHub survey lands with a
higher e.id, so the header/perf reads silently picked the stale gov cert (wrong
TFA, lodgement, property_type, current band) on 3 of portfolio 838. Order by the
effective lodgement date (registration -> completion -> inspection) DESC NULLS
LAST, e.id DESC tiebreak.

Compose the descriptive fabric from the structured SAP fields a re-survey lodges
(no gov-EPC prose): walls/roof/floor from the main epc_building_part, heating and
hot water from epc_main_heating_detail + the water-heating codes, decoded with
the SAP engine's own code space (fabric_description.py). Structured fabric wins
where present and falls back to the gov-EPC prose otherwise; windows and lighting
stay on prose (their per-element codes do not reconstruct the dwelling phrase).

Add built_form (epc_property, code-decoded) and property_age_band (building part
construction_age_band -> RdSAP date range) columns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 19:38:12 +00:00

174 lines
6.9 KiB
Python

"""Behaviour of the structured-fabric renderers (ADR-0065): composing client
sheet text from an EPC's SAP integer codes, decoding with the SAP engine's own
code space and normalising the ragged shapes a re-survey lodges."""
from __future__ import annotations
from domain.scenario_export.fabric_description import (
age_band_description,
built_form_description,
floor_description,
heating_description,
hot_water_description,
roof_description,
wall_description,
)
class TestBuiltFormDescription:
def test_lodged_display_text_passes_through(self) -> None:
assert built_form_description("Mid-terrace") == "Mid-terrace"
assert built_form_description("Semi-Detached") == "Semi-Detached"
def test_integer_code_is_decoded(self) -> None:
assert built_form_description(4) == "Mid-terrace"
assert built_form_description("1") == "Detached"
def test_absent_or_unknown_code_renders_nothing(self) -> None:
assert built_form_description(None) is None
assert built_form_description(99) is None
class TestAgeBandDescription:
def test_rdsap_letter_decodes_to_date_range_and_band(self) -> None:
assert age_band_description("B") == "1900-1929 (band B)"
assert age_band_description("A") == "before 1900 (band A)"
assert age_band_description("M") == "2023 onwards (band M)"
def test_lowercase_and_whitespace_are_tolerated(self) -> None:
assert age_band_description(" k ") == "2007-2011 (band K)"
def test_unknown_or_absent_band_renders_nothing(self) -> None:
assert age_band_description("Unknown") is None
assert age_band_description(None) is None
assert age_band_description("Z") is None
class TestWallDescription:
def test_construction_and_insulation_codes_decode_to_text(self) -> None:
assert wall_description(4, 2) == "Cavity wall, filled cavity"
assert wall_description(3, 4) == "Solid brick wall, as built"
assert wall_description(5, 1) == "Timber frame wall, external insulation"
def test_construction_only_when_insulation_unknown(self) -> None:
assert wall_description(4, None) == "Cavity wall"
assert wall_description(4, 99) == "Cavity wall"
def test_numeric_string_codes_are_accepted(self) -> None:
# JSONB can round-trip a code as a string; it still decodes.
assert wall_description("4", "2") == "Cavity wall, filled cavity"
def test_unknown_construction_renders_nothing(self) -> None:
assert wall_description(None, 2) is None
assert wall_description(99, 2) is None
class TestRoofDescription:
def test_construction_prose_with_thickness_and_location(self) -> None:
assert (
roof_description("Pitched roof, Access to loft", "Joists", 270)
== "Pitched roof, Access to loft; 270mm insulation at joists"
)
def test_thickness_string_with_unit_is_normalised(self) -> None:
# A minority of records lodge the thickness as "225mm" rather than 225.
assert (
roof_description("Pitched", "Joists", "225mm")
== "Pitched; 225mm insulation at joists"
)
def test_non_numeric_thickness_and_coded_location_are_dropped(self) -> None:
# thickness "As built" is not a measurement; a bare int location is a
# code, not a place — with neither, only the construction survives.
assert roof_description("Pitched", 2, "As built") == "Pitched"
def test_named_location_without_a_thickness_still_notes_insulation(self) -> None:
assert (
roof_description("Pitched", "Joists", "As built")
== "Pitched; insulation at joists"
)
def test_zero_thickness_reads_as_no_insulation(self) -> None:
assert roof_description("Flat roof", "Joists", 0) == "Flat roof; no insulation"
def test_unknown_location_is_omitted(self) -> None:
assert roof_description("Pitched", "Unknown", 100) == "Pitched; 100mm insulation"
def test_construction_only_when_no_insulation_detail(self) -> None:
assert roof_description("Pitched", None, None) == "Pitched"
def test_no_construction_renders_nothing(self) -> None:
assert roof_description(None, "Joists", 270) is None
class TestFloorDescription:
def test_composes_type_construction_and_insulation(self) -> None:
assert (
floor_description("Ground floor", "Suspended, timber", "As Built")
== "Ground floor, suspended, timber, insulation as built"
)
def test_normalises_casing_of_the_floor_type(self) -> None:
assert floor_description("Ground Floor", "Solid", None) == "Ground floor, solid"
def test_retrofitted_insulation_is_carried(self) -> None:
assert (
floor_description("Ground floor", "Solid", "Retro-fitted")
== "Ground floor, solid, insulation retro-fitted"
)
def test_empty_when_nothing_lodged(self) -> None:
assert floor_description(None, None, None) is None
class TestHeatingDescription:
def test_fuel_emitter_control_and_condensing(self) -> None:
assert (
heating_description(26, 1, 2106, True)
== "Mains gas, radiators (condensing); programmer, room thermostat and TRVs"
)
def test_condensing_false_or_absent_omits_the_flag(self) -> None:
assert heating_description(26, 1, 2104, None) == (
"Mains gas, radiators; programmer and room thermostat"
)
assert heating_description(26, 1, None, False) == "Mains gas, radiators"
def test_unknown_control_is_dropped(self) -> None:
assert heating_description(26, 1, 9999, True) == "Mains gas, radiators (condensing)"
def test_empty_when_no_fuel_or_emitter(self) -> None:
assert heating_description(None, None, 2106, True) is None
class TestHotWaterDescription:
def test_lodged_code_and_fuel_decode(self) -> None:
assert (
hot_water_description(901, 26, 26, False, False) == "From main system, mains gas"
)
assert (
hot_water_description(903, 29, 26, True, False)
== "Electric immersion, electricity"
)
def test_falls_back_to_main_system_when_no_code_and_no_cylinder(self) -> None:
# SAP Table 4a: a dwelling with no cylinder draws hot water from the main
# system; the main heating fuel names it.
assert (
hot_water_description(None, None, 26, False, False)
== "From main system, mains gas"
)
def test_cylinder_without_a_code(self) -> None:
assert (
hot_water_description(None, None, 29, True, False)
== "Hot water cylinder, electricity"
)
def test_solar_water_heating_is_appended(self) -> None:
assert hot_water_description(901, 26, 26, False, True) == (
"From main system, mains gas; with solar water heating"
)
def test_empty_when_nothing_lodged(self) -> None:
assert hot_water_description(None, None, None, None, False) is None