From 07190fc332aad025e83b06152b48888b1ee400d8 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 7 Jul 2026 12:52:11 +0000 Subject: [PATCH] =?UTF-8?q?Legacy=20property=20columns=20no=20longer=20res?= =?UTF-8?q?olve=20type=20or=20built=20form=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0056 amended: override and EPC own type/form facts; a property with neither is Unknown whatever the legacy columns say. Mirror columns removed. Co-Authored-By: Claude Fable 5 --- backend/app/modelling/property_filters.py | 25 ++++++------------- ...-a-shared-contract-with-the-app-preview.md | 12 ++++++--- infrastructure/postgres/property_table.py | 9 +++---- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/backend/app/modelling/property_filters.py b/backend/app/modelling/property_filters.py index 0c0934fb0..44d8f19eb 100644 --- a/backend/app/modelling/property_filters.py +++ b/backend/app/modelling/property_filters.py @@ -52,12 +52,12 @@ class _ComponentResolution: """How one filterable component (property_type / built_form) resolves at each ADR-0056 precedence level: which override row names it, how its RdSAP numeric codes map (text labels pass through as-is), and which columns hold - it on the EPC and the legacy property row.""" + it on the EPC. The legacy property.property_type / built_form columns are + NOT consulted (ADR-0056 amendment — overrides own those facts).""" override_component: str codes: dict[str, str] epc_columns: tuple[str, ...] - legacy_column: str _PROPERTY_TYPE = _ComponentResolution( @@ -71,7 +71,6 @@ _PROPERTY_TYPE = _ComponentResolution( }, # A cert without the property_type column falls back to its dwelling_type. epc_columns=("property_type", "dwelling_type"), - legacy_column="property_type", ) _BUILT_FORM = _ComponentResolution( @@ -85,7 +84,6 @@ _BUILT_FORM = _ComponentResolution( "6": "Enclosed Mid-Terrace", }, epc_columns=("built_form",), - legacy_column="built_form", ) @@ -94,12 +92,12 @@ def resolve_filtered_property_ids( ) -> list[FilteredProperty]: """Resolve *filters* against *portfolio_id* per ADR-0056: base set is the portfolio's rows with ``marked_for_deletion = false``; property_type / - built_form resolve override → lodged EPC → predicted EPC → legacy columns - → "Unknown". Filters AND-combine; an absent key is unconstrained.""" + built_form resolve override → lodged EPC → predicted EPC → "Unknown". + Filters AND-combine; an absent key is unconstrained.""" # IS NOT TRUE rather than = false: the FE schema defaults the flag, but a # NULL must never silently exclude a property. base_sql = ( - "SELECT id, postcode, property_type, built_form FROM property" + "SELECT id, postcode FROM property" " WHERE portfolio_id = :portfolio_id" " AND marked_for_deletion IS NOT TRUE" ) @@ -109,14 +107,7 @@ def resolve_filtered_property_ids( params["postcodes"] = filters.postcodes base_sql += " ORDER BY id" candidates = [ - _Candidate( - property_id=int(row[0]), - postcode=row[1], - legacy_values={ - _PROPERTY_TYPE.legacy_column: row[2], - _BUILT_FORM.legacy_column: row[3], - }, - ) + _Candidate(property_id=int(row[0]), postcode=row[1]) for row in session.connection().execute(text(base_sql), params) ] @@ -142,7 +133,6 @@ def resolve_filtered_property_ids( class _Candidate: property_id: int postcode: Optional[str] - legacy_values: dict[str, Optional[str]] def _resolved_component_values( @@ -150,7 +140,7 @@ def _resolved_component_values( ) -> dict[int, str]: """Each candidate's effective value for *component* per the ADR-0056 precedence: override (building_part 0) → lodged EPC → predicted EPC → - legacy column → "Unknown".""" + "Unknown".""" property_ids = [c.property_id for c in candidates] override_rows = session.connection().execute( @@ -186,7 +176,6 @@ def _resolved_component_values( by_override.get(c.property_id) or by_epc_source["lodged"].get(c.property_id) or by_epc_source["predicted"].get(c.property_id) - or c.legacy_values.get(component.legacy_column) or _UNKNOWN ) for c in candidates diff --git a/docs/adr/0056-modelling-run-filter-resolution-is-a-shared-contract-with-the-app-preview.md b/docs/adr/0056-modelling-run-filter-resolution-is-a-shared-contract-with-the-app-preview.md index 2dd8f56e8..9e13a5732 100644 --- a/docs/adr/0056-modelling-run-filter-resolution-is-a-shared-contract-with-the-app-preview.md +++ b/docs/adr/0056-modelling-run-filter-resolution-is-a-shared-contract-with-the-app-preview.md @@ -40,9 +40,7 @@ lands in both codebases and amends this ADR. built_form: 1=Detached, 2=Semi-Detached, 3=End-Terrace, 4=Mid-Terrace, 5=Enclosed End-Terrace, 6=Enclosed Mid-Terrace. Text passes through as-is; - 3. else the legacy `property.property_type` / `property.built_form` - columns; - 4. else **"Unknown"** — a selectable filter value meaning exactly this + 3. else **"Unknown"** — a selectable filter value meaning exactly this bucket (no resolvable value at any level). 4. An absent filter key is unconstrained; present keys combine with **AND**. @@ -57,3 +55,11 @@ lands in both codebases and amends this ADR. - Drift risk is accepted and mitigated by this document; a cross-repo contract test (same fixture set, both implementations) is the natural follow-on if drift ever bites. + +## Amendment (2026-07-08): the legacy property columns are not consulted + +The original rule fell back to `property.property_type` / `property.built_form` +before "Unknown". Those columns are legacy: the override layer owns +user-supplied type/form facts, so a property with no override and no EPC is +**"Unknown"** whatever the legacy columns say. The app's preview must apply +the same amendment. diff --git a/infrastructure/postgres/property_table.py b/infrastructure/postgres/property_table.py index c3cf497c2..e62ac958e 100644 --- a/infrastructure/postgres/property_table.py +++ b/infrastructure/postgres/property_table.py @@ -57,10 +57,7 @@ class PropertyRow(SQLModel, table=True): has_recommendations: Optional[bool] = Field(default=None) updated_at: Optional[datetime] = Field(default=None) - # Columns the Modelling Run filter resolution reads (ADR-0056): the base-set - # exclusion flag and the legacy type/form columns (the last resort before - # "Unknown" in the resolution precedence). NOT NULL / defaulted in the FE - # schema; nullable mirrors are safe — prod enforces the real constraints. + # The Modelling Run filter resolution's base-set exclusion flag (ADR-0056). + # The legacy property_type / built_form columns are deliberately NOT + # mirrored: overrides and EPCs own those facts (ADR-0056 amendment). marked_for_deletion: Optional[bool] = Field(default=None) - property_type: Optional[str] = Field(default=None) - built_form: Optional[str] = Field(default=None)