Model/repositories/property/property_override_repository.py
Jun-te Kim daa1cd7967 feat(address2uprn): withhold ambiguous cross-row UPRN matches (ADR-0057)
Phase 1 of confirming UPRNs before finalise. address2uprn matched each
row independently, so one UPRN could be the best match for two distinct
addresses (a coarse EPC record absorbing several real addresses, e.g.
flats in a block). Those distinct addresses were then silently merged by
the property identity insert, and collided in property_overrides.

resolve_group_ambiguity() withholds a UPRN claimed by >=2 distinct
normalised addresses within a postcode group (keeps genuine same-address
re-listings), and the handler now emits an address2uprn_status column
(matched | ambiguous_duplicate | unmatched | invalid_postcode | error).
Withheld rows drop to a null UPRN but keep their lexiscore for triage on
the (upcoming) confirmation page.

Also adds the ADR-0057 backstop dedup in property_overrides upsert_all so
the ON CONFLICT statement can never double-touch a row.

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

42 lines
1.5 KiB
Python

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass(frozen=True)
class PropertyOverrideInsert:
"""One ``property_overrides`` row written at Finalise (ADR-0006).
``override_value`` is the resolved enum snapshot (a denormalised text copy
from ``landlord_*_overrides``); ``original_spreadsheet_description`` is the
raw cell entry it resolved from (un-normalized).
"""
property_id: int
portfolio_id: int
building_part: int
override_component: str
override_value: str
original_spreadsheet_description: str
class PropertyOverrideRepository(ABC):
"""Writes the per-Property fact layer (``property_overrides``).
A distinct aggregate from ``property``: own table, own write semantics —
re-run = recalculate (upsert), in contrast to ``property``'s
insert-do-nothing. One repository per aggregate (ADR-0006).
"""
@abstractmethod
def upsert_all(self, rows: list[PropertyOverrideInsert]) -> int:
"""Upsert each row on ``(property_id, override_component, building_part)``,
refreshing ``override_value`` + ``original_spreadsheet_description`` +
``updated_at`` on conflict (recalculate-on-rerun). Returns the number of
rows affected; an empty list is a no-op returning 0.
Rows sharing a conflict key within one call are collapsed to the last
occurrence (last-write-wins) before writing, so a single batch never
upserts the same target row twice (ADR-0057 backstop)."""
...