Classify the landlord Hot Water and Heating columns into categories 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-19 14:09:05 +00:00
parent ad3b1f15a8
commit cc228b3da5
6 changed files with 233 additions and 0 deletions

View file

@ -11,8 +11,10 @@ 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.main_heating_system_type import MainHeatingSystemType
from domain.epc.property_type import PropertyType
from domain.epc.roof_type import RoofType
from domain.epc.water_heating_type import WaterHeatingType
from domain.epc.wall_type import WallType
from domain.epc.wall_type_construction_dates import (
wall_type_construction_date_prompt_hint,
@ -36,6 +38,12 @@ from infrastructure.postgres.landlord_glazing_override_table import (
from infrastructure.postgres.landlord_main_fuel_override_table import (
LandlordMainFuelOverrideRow,
)
from infrastructure.postgres.landlord_main_heating_system_override_table import (
LandlordMainHeatingSystemOverrideRow,
)
from infrastructure.postgres.landlord_water_heating_override_table import (
LandlordWaterHeatingOverrideRow,
)
from infrastructure.postgres.landlord_property_type_override_table import (
LandlordPropertyTypeOverrideRow,
)
@ -144,6 +152,26 @@ def _build_columns(
session, LandlordConstructionAgeBandOverrideRow
),
),
"water_heating": lambda src: ClassifiableColumn(
name="water_heating",
source_column=src,
classifier=ChatGptColumnClassifier(
chat_gpt, WaterHeatingType, WaterHeatingType.UNKNOWN
),
repo=LandlordOverridesRepository[WaterHeatingType](
session, LandlordWaterHeatingOverrideRow
),
),
"main_heating_system": lambda src: ClassifiableColumn(
name="main_heating_system",
source_column=src,
classifier=ChatGptColumnClassifier(
chat_gpt, MainHeatingSystemType, MainHeatingSystemType.UNKNOWN
),
repo=LandlordOverridesRepository[MainHeatingSystemType](
session, LandlordMainHeatingSystemOverrideRow
),
),
}
columns: list[ClassifiableColumn[Any]] = []

View file

@ -0,0 +1,24 @@
from enum import Enum
class MainHeatingSystemType(Enum):
"""A landlord-supplied main-heating-system description, as resolved by the
landlord-description-overrides context.
Each member's value is the canonical system archetype that the main-heating
Simulation Overlay
(``domain/epc/property_overlays/main_heating_system_overlay.py``) maps to a
representative SAP ``sap_main_heating_code`` so the member values MUST stay
in lock-step with that overlay's ``_MAIN_HEATING_CODES`` keys. The SEDBUK A-G
efficiency band the Hyde "Heating" column carries is NOT modelled yet
(deferred), so archetypes map to their modern/condensing code. ``UNKNOWN``
covers values the classifier cannot resolve and the not-yet-modelled systems
(heat pumps, community heating).
"""
GAS_COMBI = "Gas boiler, combi"
GAS_REGULAR = "Gas boiler, regular"
GAS_CPSU = "Gas CPSU"
ELECTRIC_STORAGE_FAN = "Electric storage heaters, fan"
DIRECT_ELECTRIC = "Direct-acting electric"
UNKNOWN = "Unknown"

View file

@ -0,0 +1,21 @@
from enum import Enum
class WaterHeatingType(Enum):
"""A landlord-supplied water-heating description, as resolved by the
landlord-description-overrides context.
Each member's value is the canonical "<system>, <fuel>" description that the
water-heating Simulation Overlay
(``domain/epc/property_overlays/water_heating_overlay.py``) decomposes into
the SAP ``water_heating_code`` + ``water_heating_fuel`` int codes the
calculator reads so the member values MUST stay in lock-step with that
overlay's ``_WATER_HEATING_CODES`` keys. ``UNKNOWN`` covers values the
classifier cannot resolve, and any combination not yet given verified codes
(it leaves the lodged cert's hot-water arrangement untouched).
"""
FROM_MAIN_MAINS_GAS = "From main system, mains gas"
FROM_MAIN_ELECTRICITY = "From main system, electricity"
ELECTRIC_IMMERSION = "Electric immersion, electricity"
UNKNOWN = "Unknown"

View file

@ -25,9 +25,24 @@ from infrastructure.postgres.landlord_property_type_override_table import (
from infrastructure.postgres.landlord_roof_type_override_table import (
LandlordRoofTypeOverrideRow,
)
from infrastructure.postgres.landlord_construction_age_band_override_table import (
LandlordConstructionAgeBandOverrideRow,
)
from infrastructure.postgres.landlord_glazing_override_table import (
LandlordGlazingOverrideRow,
)
from infrastructure.postgres.landlord_main_fuel_override_table import (
LandlordMainFuelOverrideRow,
)
from infrastructure.postgres.landlord_main_heating_system_override_table import (
LandlordMainHeatingSystemOverrideRow,
)
from infrastructure.postgres.landlord_wall_type_override_table import (
LandlordWallTypeOverrideRow,
)
from infrastructure.postgres.landlord_water_heating_override_table import (
LandlordWaterHeatingOverrideRow,
)
from repositories.landlord_overrides.landlord_override_reader import (
LandlordOverrideReader,
)
@ -38,6 +53,11 @@ _ROW_TYPES: dict[str, type] = {
"built_form_type": LandlordBuiltFormTypeOverrideRow,
"wall_type": LandlordWallTypeOverrideRow,
"roof_type": LandlordRoofTypeOverrideRow,
"main_fuel": LandlordMainFuelOverrideRow,
"glazing": LandlordGlazingOverrideRow,
"construction_age_band": LandlordConstructionAgeBandOverrideRow,
"water_heating": LandlordWaterHeatingOverrideRow,
"main_heating_system": LandlordMainHeatingSystemOverrideRow,
}

View file

@ -0,0 +1,71 @@
"""SQLModel mirror of the ``landlord_main_heating_system_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_heating_system`` 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_heating_system_type import MainHeatingSystemType
from infrastructure.postgres.landlord_override_enums import override_source_sa_enum
class LandlordMainHeatingSystemOverrideRow(SQLModel, table=True):
__tablename__: ClassVar[str] = "landlord_main_heating_system_overrides" # pyright: ignore[reportIncompatibleVariableOverride]
__table_args__: ClassVar[tuple[UniqueConstraint, ...]] = ( # pyright: ignore[reportIncompatibleVariableOverride]
# Shortened (drop the redundant ``_overrides``) to stay within
# PostgreSQL's 63-char identifier limit; mirrors the Drizzle name.
UniqueConstraint(
"portfolio_id",
"description",
name="landlord_main_heating_system_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: MainHeatingSystemType = Field(
sa_column=Column(
SAEnum(
MainHeatingSystemType,
name="main_heating_system",
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,
)

View file

@ -0,0 +1,69 @@
"""SQLModel mirror of the ``landlord_water_heating_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 ``water_heating`` 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.water_heating_type import WaterHeatingType
from infrastructure.postgres.landlord_override_enums import override_source_sa_enum
class LandlordWaterHeatingOverrideRow(SQLModel, table=True):
__tablename__: ClassVar[str] = "landlord_water_heating_overrides" # pyright: ignore[reportIncompatibleVariableOverride]
__table_args__: ClassVar[tuple[UniqueConstraint, ...]] = ( # pyright: ignore[reportIncompatibleVariableOverride]
UniqueConstraint(
"portfolio_id",
"description",
name="landlord_water_heating_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: WaterHeatingType = Field(
sa_column=Column(
SAEnum(
WaterHeatingType,
name="water_heating",
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,
)