From dbfe9820a5a06a8d1b33d2457098096cef6e1491 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Fri, 19 Jun 2026 12:44:56 +0000 Subject: [PATCH] =?UTF-8?q?Classify=20the=20landlord=20Main=20Fuel=20colum?= =?UTF-8?q?n=20into=20a=20fuel=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/main_fuel_type.py | 23 +++++++ .../landlord_main_fuel_override_table.py | 69 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 domain/epc/main_fuel_type.py create mode 100644 infrastructure/postgres/landlord_main_fuel_override_table.py diff --git a/applications/landlord_description_overrides/handler.py b/applications/landlord_description_overrides/handler.py index 5e689a45..7be4dba1 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.main_fuel_type import MainFuelType from domain.epc.property_type import PropertyType from domain.epc.roof_type import RoofType from domain.epc.wall_type import WallType @@ -24,6 +25,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_main_fuel_override_table import ( + LandlordMainFuelOverrideRow, +) from infrastructure.postgres.landlord_property_type_override_table import ( LandlordPropertyTypeOverrideRow, ) @@ -102,6 +106,16 @@ def _build_columns( session, LandlordRoofTypeOverrideRow ), ), + "main_fuel": lambda src: ClassifiableColumn( + name="main_fuel", + source_column=src, + classifier=ChatGptColumnClassifier( + chat_gpt, MainFuelType, MainFuelType.UNKNOWN + ), + repo=LandlordOverridesRepository[MainFuelType]( + session, LandlordMainFuelOverrideRow + ), + ), } columns: list[ClassifiableColumn[Any]] = [] diff --git a/domain/epc/main_fuel_type.py b/domain/epc/main_fuel_type.py new file mode 100644 index 00000000..0ad575c3 --- /dev/null +++ b/domain/epc/main_fuel_type.py @@ -0,0 +1,23 @@ +from enum import Enum + + +class MainFuelType(Enum): + """A landlord-supplied main-fuel description, as resolved by the + landlord-description-overrides context. + + Each member's value is the canonical fuel description that the main-fuel + Simulation Overlay (``domain/epc/property_overlays/main_fuel_overlay.py``) + decomposes into the RdSAP ``main_fuel`` int code the calculator reads — so + the member values here MUST stay in lock-step with that overlay's + ``_FUEL_CODES`` keys. ``UNKNOWN`` covers values the classifier cannot + resolve, and also any fuel not yet given a verified overlay code (it leaves + the lodged cert's fuel untouched rather than guessing). + """ + + MAINS_GAS = "mains gas" + MAINS_GAS_COMMUNITY = "mains gas (community)" + ELECTRICITY = "electricity" + LPG_BULK = "LPG (bulk)" + OIL = "oil" + HOUSE_COAL = "house coal" + UNKNOWN = "Unknown" diff --git a/infrastructure/postgres/landlord_main_fuel_override_table.py b/infrastructure/postgres/landlord_main_fuel_override_table.py new file mode 100644 index 00000000..c046453b --- /dev/null +++ b/infrastructure/postgres/landlord_main_fuel_override_table.py @@ -0,0 +1,69 @@ +"""SQLModel mirror of the ``landlord_main_fuel_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 ``main_fuel`` 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.main_fuel_type import MainFuelType +from infrastructure.postgres.landlord_override_enums import override_source_sa_enum + + +class LandlordMainFuelOverrideRow(SQLModel, table=True): + __tablename__: ClassVar[str] = "landlord_main_fuel_overrides" # pyright: ignore[reportIncompatibleVariableOverride] + __table_args__: ClassVar[tuple[UniqueConstraint, ...]] = ( # pyright: ignore[reportIncompatibleVariableOverride] + UniqueConstraint( + "portfolio_id", + "description", + name="landlord_main_fuel_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: MainFuelType = Field( + sa_column=Column( + SAEnum( + MainFuelType, + name="main_fuel", + 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, + )