From 864ba8dc1b009ef13ec95ee42661a9f2d047b520 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 16 Jun 2026 15:18:44 +0000 Subject: [PATCH] =?UTF-8?q?Resolve=20a=20Property's=20prediction=20attribu?= =?UTF-8?q?tes=20from=20landlord=20overrides=20in=20gov-code=20space=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- ...ide_backed_prediction_attributes_reader.py | 55 ++++++++++++ ...ide_backed_prediction_attributes_reader.py | 86 +++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 repositories/property/override_backed_prediction_attributes_reader.py create mode 100644 tests/repositories/property/test_override_backed_prediction_attributes_reader.py diff --git a/repositories/property/override_backed_prediction_attributes_reader.py b/repositories/property/override_backed_prediction_attributes_reader.py new file mode 100644 index 00000000..5befd3b3 --- /dev/null +++ b/repositories/property/override_backed_prediction_attributes_reader.py @@ -0,0 +1,55 @@ +"""The real ``PredictionTargetAttributesReader`` — landlord-overrides-backed. + +Composes the faithful ``PropertyOverridesReader`` with the value→code mapping: +reads the Property's main-building (building_part 0) ``property_type`` / +``built_form_type`` overrides and translates them into the gov-EPC code space the +cohort filter compares against (ADR-0031). An unresolvable ``property_type`` +becomes None, which gates the Property out of prediction downstream +(``build_prediction_target``). Wall/roof overrides are left to the later +``epc_with_overlay`` slice — this reader conditions cohort selection only. +""" + +from __future__ import annotations + +from typing import Optional + +from domain.epc.override_code_mapping import ( + built_form_to_code, + property_type_to_code, +) +from domain.epc_prediction.prediction_target import PredictionTargetAttributes +from repositories.property.prediction_target_attributes_reader import ( + PredictionTargetAttributesReader, +) +from repositories.property.property_overrides_reader import PropertyOverridesReader + +_MAIN_BUILDING = 0 +_PROPERTY_TYPE_COMPONENT = "property_type" +_BUILT_FORM_COMPONENT = "built_form_type" + + +class OverrideBackedPredictionAttributesReader(PredictionTargetAttributesReader): + def __init__(self, overrides_reader: PropertyOverridesReader) -> None: + self._overrides_reader = overrides_reader + + def attributes_for(self, property_id: int) -> PredictionTargetAttributes: + overrides = self._overrides_reader.overrides_for(property_id) + + property_type_value = overrides.value(_PROPERTY_TYPE_COMPONENT, _MAIN_BUILDING) + built_form_value = overrides.value(_BUILT_FORM_COMPONENT, _MAIN_BUILDING) + + property_type: Optional[str] = ( + property_type_to_code(property_type_value) + if property_type_value is not None + else None + ) + built_form: Optional[str] = ( + built_form_to_code(built_form_value) + if built_form_value is not None + else None + ) + + return PredictionTargetAttributes( + property_type=property_type, + built_form=built_form, + ) diff --git a/tests/repositories/property/test_override_backed_prediction_attributes_reader.py b/tests/repositories/property/test_override_backed_prediction_attributes_reader.py new file mode 100644 index 00000000..4aad84d0 --- /dev/null +++ b/tests/repositories/property/test_override_backed_prediction_attributes_reader.py @@ -0,0 +1,86 @@ +"""The landlord-overrides-backed PredictionTargetAttributesReader (ADR-0031). + +Unit-level: a fake ``PropertyOverridesReader`` supplies value-space snapshots so +these tests pin the composition — main-building selection, value→code mapping, +and the gate (unresolvable property_type → None) — without a database. +""" + +from __future__ import annotations + +from repositories.property.override_backed_prediction_attributes_reader import ( + OverrideBackedPredictionAttributesReader, +) +from repositories.property.property_overrides_reader import ( + PropertyOverridesReader, + ResolvedPropertyOverride, + ResolvedPropertyOverrides, +) + + +class _FakeOverridesReader(PropertyOverridesReader): + def __init__(self, *rows: ResolvedPropertyOverride) -> None: + self._snapshot = ResolvedPropertyOverrides(rows=rows) + + def overrides_for(self, property_id: int) -> ResolvedPropertyOverrides: + return self._snapshot + + +def test_main_building_property_type_is_mapped_to_its_gov_code() -> None: + # Arrange + reader = OverrideBackedPredictionAttributesReader( + _FakeOverridesReader( + ResolvedPropertyOverride("property_type", 0, "House"), + ) + ) + + # Act + attributes = reader.attributes_for(1) + + # Assert + assert attributes.property_type == "0" + + +def test_built_form_is_mapped_and_only_the_main_building_is_read() -> None: + # Arrange — main building is a House/Detached; an extension (part 1) carries a + # different property type that must not be read. + reader = OverrideBackedPredictionAttributesReader( + _FakeOverridesReader( + ResolvedPropertyOverride("property_type", 0, "House"), + ResolvedPropertyOverride("built_form_type", 0, "Detached"), + ResolvedPropertyOverride("property_type", 1, "Flat"), + ) + ) + + # Act + attributes = reader.attributes_for(1) + + # Assert — built_form mapped to its code; the part-1 "Flat" is ignored. + assert attributes.property_type == "0" + assert attributes.built_form == "1" + + +def test_unresolvable_property_type_gates_the_property_out() -> None: + # Arrange — the landlord override resolved only to "Unknown". + reader = OverrideBackedPredictionAttributesReader( + _FakeOverridesReader( + ResolvedPropertyOverride("property_type", 0, "Unknown"), + ) + ) + + # Act + attributes = reader.attributes_for(1) + + # Assert — None property_type makes build_prediction_target skip the Property. + assert attributes.property_type is None + + +def test_property_with_no_overrides_yields_no_attributes() -> None: + # Arrange — nothing resolved for the Property. + reader = OverrideBackedPredictionAttributesReader(_FakeOverridesReader()) + + # Act + attributes = reader.attributes_for(1) + + # Assert + assert attributes.property_type is None + assert attributes.built_form is None