Model/infrastructure/postgres/property_table.py
Khalim Conn-Kowlessar 07190fc332 Legacy property columns no longer resolve type or built form 🟩
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 <noreply@anthropic.com>
2026-07-07 12:52:11 +00:00

63 lines
3 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import ClassVar, Optional
from sqlalchemy import Column
from sqlalchemy import Enum as SAEnum
from sqlmodel import Field, SQLModel
# Mirror of the FE-owned `creation_status` pgEnum (property.ts:
# propertyCreationStatusEnum = {LOADING, READY, ERROR}). A single SAEnum instance
# so test-schema create_all emits one CREATE TYPE; prod owns the type via Drizzle.
property_creation_status_sa_enum = SAEnum(
"LOADING", "READY", "ERROR", name="creation_status"
)
class PropertyRow(SQLModel, table=True):
"""Mirror of the FE-owned ``property`` table.
The schema and migrations for ``property`` are owned by the front-end Next.js
repo (``src/app/db/schema/property.ts``); this declares the identity columns
the modelling backend reads, plus the subset the ``bulk_upload_finaliser``
Lambda **inserts** at Finalise (ADR-0013). It is no longer read-only — the
finaliser is the one backend caller that inserts. Columns not declared here
are still owned by FE migrations and don't ripple into us.
"""
__tablename__: ClassVar[str] = "property" # pyright: ignore[reportIncompatibleVariableOverride]
# bigserial in the FE schema — DB-assigned on insert, so Optional/None on the
# way in and always populated on the way out.
id: Optional[int] = Field(default=None, primary_key=True)
portfolio_id: int
# Nullable in the FE schema. The finaliser writes `matched ?? user-inputted`,
# which is absent for fully-unmatched rows.
postcode: Optional[str] = Field(default=None)
address: Optional[str] = Field(default=None)
uprn: Optional[int] = Field(default=None)
landlord_property_id: Optional[str] = Field(default=None)
# Insertable columns the finaliser writes (ADR-0013). All nullable in the FE
# schema except `creation_status` (NOT NULL); the finaliser always sets it to
# 'READY', so a nullable mirror is safe — the real column enforces NOT NULL.
creation_status: Optional[str] = Field(
default=None,
sa_column=Column(property_creation_status_sa_enum, nullable=True),
)
user_inputted_address: Optional[str] = Field(default=None)
user_inputted_postcode: Optional[str] = Field(default=None)
lexiscore: Optional[float] = Field(default=None)
# FE-owned columns the modelling pipeline now WRITES to record a run: the old
# engine set `has_recommendations` (engine.py); we mirror that, and bump
# `updated_at` so a run is datable (a first-run under the new process is
# `updated_at >= 2026-06-01`, the cutoff the old pipeline predates).
has_recommendations: Optional[bool] = Field(default=None)
updated_at: Optional[datetime] = Field(default=None)
# 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)