From db95205fc53f85890b0d8318cb08f7f82a5cb187 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Fri, 19 Jun 2026 13:36:34 +0000 Subject: [PATCH] =?UTF-8?q?Classify=20the=20landlord=20Glazing=20column=20?= =?UTF-8?q?into=20a=20glazing=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/glazing_type.py | 24 +++++++ .../landlord_glazing_override_table.py | 69 +++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 domain/epc/glazing_type.py create mode 100644 infrastructure/postgres/landlord_glazing_override_table.py diff --git a/applications/landlord_description_overrides/handler.py b/applications/landlord_description_overrides/handler.py index 7be4dba1..16dbb000 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.glazing_type import GlazingType from domain.epc.main_fuel_type import MainFuelType from domain.epc.property_type import PropertyType from domain.epc.roof_type import RoofType @@ -25,6 +26,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_glazing_override_table import ( + LandlordGlazingOverrideRow, +) from infrastructure.postgres.landlord_main_fuel_override_table import ( LandlordMainFuelOverrideRow, ) @@ -116,6 +120,16 @@ def _build_columns( session, LandlordMainFuelOverrideRow ), ), + "glazing": lambda src: ClassifiableColumn( + name="glazing", + source_column=src, + classifier=ChatGptColumnClassifier( + chat_gpt, GlazingType, GlazingType.UNKNOWN + ), + repo=LandlordOverridesRepository[GlazingType]( + session, LandlordGlazingOverrideRow + ), + ), } columns: list[ClassifiableColumn[Any]] = [] diff --git a/domain/epc/glazing_type.py b/domain/epc/glazing_type.py new file mode 100644 index 00000000..ee3ad267 --- /dev/null +++ b/domain/epc/glazing_type.py @@ -0,0 +1,24 @@ +from enum import Enum + + +class GlazingType(Enum): + """A landlord-supplied glazing description, as resolved by the + landlord-description-overrides context. + + Each member's value is the canonical glazing description (type + era) that + the glazing Simulation Overlay + (``domain/epc/property_overlays/glazing_overlay.py``) decomposes into the + SAP10 ``glazing_type`` code the calculator's Table-24 cascade reads — so the + member values here MUST stay in lock-step with that overlay's + ``_GLAZING_CODES`` keys. The era matters: double-glazing pre-2002 and + 2002-onward resolve to different codes (and U-values). ``UNKNOWN`` covers + values the classifier cannot resolve, and any glazing not yet given a + verified overlay code (it leaves the lodged cert's glazing untouched). + """ + + SINGLE = "Single glazing" + DOUBLE_POST_2002 = "Double glazing, 2002 or later" + DOUBLE_PRE_2002 = "Double glazing, pre-2002" + TRIPLE_PRE_2002 = "Triple glazing, pre-2002" + TRIPLE_POST_2002 = "Triple glazing, 2002 or later" + UNKNOWN = "Unknown" diff --git a/infrastructure/postgres/landlord_glazing_override_table.py b/infrastructure/postgres/landlord_glazing_override_table.py new file mode 100644 index 00000000..f42a48d2 --- /dev/null +++ b/infrastructure/postgres/landlord_glazing_override_table.py @@ -0,0 +1,69 @@ +"""SQLModel mirror of the ``landlord_glazing_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 ``glazing`` 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.glazing_type import GlazingType +from infrastructure.postgres.landlord_override_enums import override_source_sa_enum + + +class LandlordGlazingOverrideRow(SQLModel, table=True): + __tablename__: ClassVar[str] = "landlord_glazing_overrides" # pyright: ignore[reportIncompatibleVariableOverride] + __table_args__: ClassVar[tuple[UniqueConstraint, ...]] = ( # pyright: ignore[reportIncompatibleVariableOverride] + UniqueConstraint( + "portfolio_id", + "description", + name="landlord_glazing_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: GlazingType = Field( + sa_column=Column( + SAEnum( + GlazingType, + name="glazing", + 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, + )