From cb6a335382985ebd2fda9df31ecc1e8e378cdcf1 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Fri, 19 Jun 2026 13:44:40 +0000 Subject: [PATCH] =?UTF-8?q?Classify=20the=20landlord=20Age=20column=20into?= =?UTF-8?q?=20a=20construction-age-band=20category=20=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) --- .../landlord_description_overrides/handler.py | 14 ++++ domain/epc/construction_age_band.py | 31 +++++++++ ...rd_construction_age_band_override_table.py | 69 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 domain/epc/construction_age_band.py create mode 100644 infrastructure/postgres/landlord_construction_age_band_override_table.py diff --git a/applications/landlord_description_overrides/handler.py b/applications/landlord_description_overrides/handler.py index 16dbb000..2f83b81f 100644 --- a/applications/landlord_description_overrides/handler.py +++ b/applications/landlord_description_overrides/handler.py @@ -8,6 +8,7 @@ from applications.landlord_description_overrides.landlord_description_overrides_ LandlordDescriptionOverridesTriggerBody, ) from domain.epc.built_form_type import BuiltFormType +from domain.epc.construction_age_band import ConstructionAgeBand from domain.epc.glazing_type import GlazingType from domain.epc.main_fuel_type import MainFuelType from domain.epc.property_type import PropertyType @@ -26,6 +27,9 @@ from infrastructure.postgres.engine import commit_scope, make_engine, make_sessi from infrastructure.postgres.landlord_built_form_type_override_table import ( LandlordBuiltFormTypeOverrideRow, ) +from infrastructure.postgres.landlord_construction_age_band_override_table import ( + LandlordConstructionAgeBandOverrideRow, +) from infrastructure.postgres.landlord_glazing_override_table import ( LandlordGlazingOverrideRow, ) @@ -130,6 +134,16 @@ def _build_columns( session, LandlordGlazingOverrideRow ), ), + "construction_age_band": lambda src: ClassifiableColumn( + name="construction_age_band", + source_column=src, + classifier=ChatGptColumnClassifier( + chat_gpt, ConstructionAgeBand, ConstructionAgeBand.UNKNOWN + ), + repo=LandlordOverridesRepository[ConstructionAgeBand]( + session, LandlordConstructionAgeBandOverrideRow + ), + ), } columns: list[ClassifiableColumn[Any]] = [] diff --git a/domain/epc/construction_age_band.py b/domain/epc/construction_age_band.py new file mode 100644 index 00000000..83f6e9a8 --- /dev/null +++ b/domain/epc/construction_age_band.py @@ -0,0 +1,31 @@ +from enum import Enum + + +class ConstructionAgeBand(Enum): + """A landlord-supplied construction age band, as resolved by the + landlord-description-overrides context. + + Each member's value is the RdSAP England-&-Wales age-band **letter code** + (A..M) the calculator's U-value cascades read from + `SapBuildingPart.construction_age_band` — the same representation the gov-EPC + API lodges. The construction-age-band Simulation Overlay + (``domain/epc/property_overlays/construction_age_band_overlay.py``) sets the + letter directly, so these values MUST stay the bare letter codes. Member + names carry the year ranges for readability. ``UNKNOWN`` covers values the + classifier cannot resolve (it leaves the lodged cert's age band untouched). + """ + + A_BEFORE_1900 = "A" + B_1900_1929 = "B" + C_1930_1949 = "C" + D_1950_1966 = "D" + E_1967_1975 = "E" + F_1976_1982 = "F" + G_1983_1990 = "G" + H_1991_1995 = "H" + I_1996_2002 = "I" + J_2003_2006 = "J" + K_2007_2011 = "K" + L_2012_2022 = "L" + M_2023_ONWARDS = "M" + UNKNOWN = "Unknown" diff --git a/infrastructure/postgres/landlord_construction_age_band_override_table.py b/infrastructure/postgres/landlord_construction_age_band_override_table.py new file mode 100644 index 00000000..598bbf56 --- /dev/null +++ b/infrastructure/postgres/landlord_construction_age_band_override_table.py @@ -0,0 +1,69 @@ +"""SQLModel mirror of the ``landlord_construction_age_band_overrides`` Drizzle table. + +The schema source of truth lives in the ``assessment-model`` TS repo +(`src/app/db/schema/landlord_overrides.ts`). The migrations are owned there; +this row class only mirrors the columns so the Python lambda can read/write. +See ADR-0003. Shape mirrors ``LandlordWallTypeOverrideRow`` -- the only +differences are the table name, the ``construction_age_band`` pgEnum on +``value``, and the unique-constraint name. +""" + +from datetime import datetime, timezone +from typing import ClassVar +from uuid import UUID, uuid4 + +from sqlalchemy import BigInteger, Column, UniqueConstraint +from sqlalchemy import Enum as SAEnum +from sqlmodel import Field, SQLModel + +from domain.epc.construction_age_band import ConstructionAgeBand +from infrastructure.postgres.landlord_override_enums import override_source_sa_enum + + +class LandlordConstructionAgeBandOverrideRow(SQLModel, table=True): + __tablename__: ClassVar[str] = "landlord_construction_age_band_overrides" # pyright: ignore[reportIncompatibleVariableOverride] + __table_args__: ClassVar[tuple[UniqueConstraint, ...]] = ( # pyright: ignore[reportIncompatibleVariableOverride] + UniqueConstraint( + "portfolio_id", + "description", + name="landlord_construction_age_band_overrides_portfolio_description_unique", + ), + ) + + id: UUID = Field(default_factory=uuid4, primary_key=True) + + # bigint to match the Drizzle ``portfolio_id`` FK; SQLModel's default int + # mapping is 32-bit Integer and would overflow once portfolio IDs exceed + # 2^31. The FK to ``portfolio.id`` is enforced by the Drizzle migration, + # not declared here -- the ``portfolio`` table is not modelled in Python. + portfolio_id: int = Field( + sa_column=Column(BigInteger, nullable=False, index=True), + ) + + description: str = Field(nullable=False) + + value: ConstructionAgeBand = Field( + sa_column=Column( + SAEnum( + ConstructionAgeBand, + name="construction_age_band", + values_callable=lambda cls: [m.value for m in cls], # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType, reportUnknownVariableType] + ), + nullable=False, + ), + ) + + # Shared SAEnum -- see ``landlord_override_enums`` for why this single + # instance is reused by every ``landlord_*_overrides`` row class. + source: str = Field( + sa_column=Column(override_source_sa_enum, nullable=False), + ) + + created_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc), + nullable=False, + ) + updated_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc), + nullable=False, + )