mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Map landlord-override property type and built form to gov EPC codes 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
90bed458f4
commit
a1ce8ece50
3 changed files with 149 additions and 0 deletions
56
domain/epc/override_code_mapping.py
Normal file
56
domain/epc/override_code_mapping.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""Map a Landlord-Override enum *value* to the gov-EPC API *code* space.
|
||||
|
||||
The `property_overrides` fact layer stores resolved overrides as enum-value
|
||||
strings ("House", "Detached"); the EPC-API cohort certs carry numeric codes
|
||||
("0", "2") — `EpcPropertyData.property_type = str(schema.property_type)`. EPC
|
||||
Prediction filters `comparable.epc.property_type == target.property_type`, so a
|
||||
target attribute sourced from overrides must be translated into the code space
|
||||
or no comparable ever matches (ADR-0031, the wiring-handover "every cohort
|
||||
comes back empty" gotcha).
|
||||
|
||||
Codes are the gov RdSAP/SAP table values in `datatypes/epc/domain/epc_codes.csv`.
|
||||
This module owns "unresolvable": a value that maps to no code returns None.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
# property_type codes (epc_codes.csv, `property_type` rows — stable across the
|
||||
# RdSAP/SAP schemas that carry each member). "Park home" exists only from
|
||||
# SAP-17.0 / RdSAP-17.0 onward; the code itself is stable where present.
|
||||
_PROPERTY_TYPE_CODES: dict[str, str] = {
|
||||
"House": "0",
|
||||
"Bungalow": "1",
|
||||
"Flat": "2",
|
||||
"Maisonette": "3",
|
||||
"Park home": "4",
|
||||
}
|
||||
|
||||
# built_form codes (epc_codes.csv, `built_form` rows). "Not Recorded" lodges as
|
||||
# the non-numeric "NR", but cohort comparables carry `str(int)` for built_form,
|
||||
# so an "NR" target could never match — and built_form is the SOFT filter, so a
|
||||
# non-match only widens the cohort. We therefore treat "Not Recorded" (and the
|
||||
# classifier "Unknown") as "no usable built-form signal" → None.
|
||||
_BUILT_FORM_CODES: dict[str, str] = {
|
||||
"Detached": "1",
|
||||
"Semi-Detached": "2",
|
||||
"End-Terrace": "3",
|
||||
"Mid-Terrace": "4",
|
||||
"Enclosed End-Terrace": "5",
|
||||
"Enclosed Mid-Terrace": "6",
|
||||
}
|
||||
|
||||
|
||||
def property_type_to_code(override_value: str) -> Optional[str]:
|
||||
"""The gov-EPC `property_type` code for a Landlord-Override value, or None
|
||||
when it has no code ("Unknown", or any unmapped value) — which gates the
|
||||
Property out of prediction, as `property_type` is the hard cohort filter."""
|
||||
return _PROPERTY_TYPE_CODES.get(override_value)
|
||||
|
||||
|
||||
def built_form_to_code(override_value: str) -> Optional[str]:
|
||||
"""The gov-EPC `built_form` code for a Landlord-Override value, or None when
|
||||
it has no usable code ("Unknown", "Not Recorded", or any unmapped value).
|
||||
built_form is the soft filter, so None simply leaves it unconditioned."""
|
||||
return _BUILT_FORM_CODES.get(override_value)
|
||||
0
tests/domain/epc/__init__.py
Normal file
0
tests/domain/epc/__init__.py
Normal file
93
tests/domain/epc/test_override_code_mapping.py
Normal file
93
tests/domain/epc/test_override_code_mapping.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""The Landlord-Override value → gov-EPC code mapping (ADR-0031 wiring).
|
||||
|
||||
`property_type` is the HARD cohort filter, so its mapping is exhaustive over
|
||||
`PropertyType` and the only one that can silently empty a cohort; `built_form`
|
||||
is the SOFT filter. Both collapse an unresolvable value to None — gating lives
|
||||
downstream, the mapping just reports "no usable code".
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from domain.epc.built_form_type import BuiltFormType
|
||||
from domain.epc.override_code_mapping import (
|
||||
built_form_to_code,
|
||||
property_type_to_code,
|
||||
)
|
||||
from domain.epc.property_type import PropertyType
|
||||
|
||||
|
||||
def test_house_maps_to_gov_code_zero() -> None:
|
||||
# Act
|
||||
code = property_type_to_code("House")
|
||||
|
||||
# Assert
|
||||
assert code == "0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("override_value", "expected_code"),
|
||||
[
|
||||
(PropertyType.HOUSE.value, "0"),
|
||||
(PropertyType.BUNGALOW.value, "1"),
|
||||
(PropertyType.FLAT.value, "2"),
|
||||
(PropertyType.MAISONETTE.value, "3"),
|
||||
(PropertyType.PARK_HOME.value, "4"),
|
||||
],
|
||||
)
|
||||
def test_each_resolvable_property_type_maps_to_its_gov_code(
|
||||
override_value: str, expected_code: str
|
||||
) -> None:
|
||||
# Act
|
||||
code = property_type_to_code(override_value)
|
||||
|
||||
# Assert
|
||||
assert code == expected_code
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"override_value",
|
||||
[PropertyType.UNKNOWN.value, "Castle", ""],
|
||||
)
|
||||
def test_unresolvable_property_type_has_no_code(override_value: str) -> None:
|
||||
# Act
|
||||
code = property_type_to_code(override_value)
|
||||
|
||||
# Assert
|
||||
assert code is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("override_value", "expected_code"),
|
||||
[
|
||||
(BuiltFormType.DETACHED.value, "1"),
|
||||
(BuiltFormType.SEMI_DETACHED.value, "2"),
|
||||
(BuiltFormType.END_TERRACE.value, "3"),
|
||||
(BuiltFormType.MID_TERRACE.value, "4"),
|
||||
(BuiltFormType.ENCLOSED_END_TERRACE.value, "5"),
|
||||
(BuiltFormType.ENCLOSED_MID_TERRACE.value, "6"),
|
||||
],
|
||||
)
|
||||
def test_each_resolvable_built_form_maps_to_its_gov_code(
|
||||
override_value: str, expected_code: str
|
||||
) -> None:
|
||||
# Act
|
||||
code = built_form_to_code(override_value)
|
||||
|
||||
# Assert
|
||||
assert code == expected_code
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"override_value",
|
||||
[BuiltFormType.UNKNOWN.value, BuiltFormType.NOT_RECORDED.value, "Castle", ""],
|
||||
)
|
||||
def test_built_form_without_usable_code_returns_none(override_value: str) -> None:
|
||||
# Act
|
||||
code: Optional[str] = built_form_to_code(override_value)
|
||||
|
||||
# Assert
|
||||
assert code is None
|
||||
Loading…
Add table
Reference in a new issue