Model/infrastructure/postgres/modelling/recommendation_table.py
Khalim Conn-Kowlessar 01c2c3910e refactor(modelling): rename the cluster SQLModel classes …Row → …Model
Standardise the modelling persistence classes on the …Model suffix (PlanModel,
RecommendationModel, RecommendationMaterialModel) — matching the epc_property
precedent and the legacy names the rest of backend/ already imports, so the
shim's plan re-export becomes literal (no alias) and the eventual shim deletion
needs zero renames. The …Row→…Model sweep for the non-cluster tables
(Property/Task/Material/…) waits until their live legacy …Model counterparts
are retired, to avoid reintroducing dual-definition collisions. No behaviour
change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 22:42:21 +00:00

120 lines
4.7 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import ClassVar, Optional
from sqlalchemy import BigInteger, Column, ForeignKey, TIMESTAMP
from sqlalchemy import Enum as SAEnum
from sqlalchemy.sql import func
from sqlmodel import Field, SQLModel
from datatypes.enums import QuantityUnits
from domain.modelling.plan import PlanMeasure
# Calculator metrics are in kg CO₂/yr; the live ``recommendation`` column is
# tonnes (legacy ``emissions_kg / 1000``). Convert on the way in.
_KG_PER_TONNE = 1000.0
class RecommendationModel(SQLModel, table=True):
"""The single SQLModel definition of the live ``recommendation`` table
(ADR-0017 amendment) — one row per persisted Plan Measure.
Carries full legacy column parity (the readers iterate the columns / sum
them) **plus** ``plan_id``, the FK that links a measure to its Plan and
replaces the retired ``plan_recommendations`` m2m. Out-of-cluster columns
(``property_id``) are plain indexed ints, not FK constraints, matching the
mirror convention so ``SQLModel.metadata.create_all`` needs no foreign
table to exist (the live FKs are owned by the Drizzle schema).
"""
__tablename__: ClassVar[str] = "recommendation" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
property_id: int = Field(index=True)
plan_id: Optional[int] = Field(
default=None,
sa_column=Column(
BigInteger,
ForeignKey("plan.id", ondelete="CASCADE"),
nullable=True,
index=True,
),
)
created_at: Optional[datetime] = Field(
default=None,
sa_column=Column(TIMESTAMP, nullable=False, server_default=func.now()),
)
type: str
measure_type: Optional[str] = Field(default=None)
description: str
estimated_cost: Optional[float] = Field(default=None)
starting_u_value: Optional[float] = Field(default=None)
new_u_value: Optional[float] = Field(default=None)
sap_points: Optional[float] = Field(default=None)
heat_demand: Optional[float] = Field(default=None)
kwh_savings: Optional[float] = Field(default=None) # delivered kWh/yr
co2_equivalent_savings: Optional[float] = Field(default=None) # tonnes/yr
energy_savings: Optional[float] = Field(default=None)
energy_cost_savings: Optional[float] = Field(default=None) # £/yr
property_valuation_increase: Optional[float] = Field(default=None)
rental_yield_increase: Optional[float] = Field(default=None)
total_work_hours: Optional[float] = Field(default=None)
labour_days: Optional[float] = Field(default=None)
default: bool = True
already_installed: bool = False
@classmethod
def from_domain(
cls, measure: PlanMeasure, *, property_id: int, plan_id: int
) -> "RecommendationModel":
return cls(
property_id=property_id,
plan_id=plan_id,
type=measure.measure_type,
measure_type=measure.measure_type,
description=measure.description,
estimated_cost=measure.cost.total,
sap_points=measure.impact.sap_points,
co2_equivalent_savings=(
measure.impact.co2_savings_kg_per_yr / _KG_PER_TONNE
),
kwh_savings=measure.kwh_savings,
energy_cost_savings=measure.energy_cost_savings,
default=True,
already_installed=False,
)
class RecommendationMaterialModel(SQLModel, table=True):
"""The live ``recommendation_materials`` table — one row per material used
by a Recommendation. ``recommendation_id`` is an intra-cluster FK;
``material_id`` is a plain int (out-of-cluster, mirror convention)."""
__tablename__: ClassVar[str] = "recommendation_materials" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
recommendation_id: int = Field(
sa_column=Column(
BigInteger, ForeignKey("recommendation.id"), nullable=False
)
)
material_id: int = Field(index=True)
created_at: Optional[datetime] = Field(
default=None,
sa_column=Column(TIMESTAMP, nullable=False, server_default=func.now()),
)
depth: Optional[float] = Field(default=None)
quantity: Optional[float] = Field(default=None)
quantity_unit: Optional[QuantityUnits] = Field(
default=None,
sa_column=Column(
SAEnum(
QuantityUnits,
values_callable=lambda cls: [m.value for m in cls], # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType, reportUnknownVariableType]
),
nullable=True,
),
)
estimated_cost: Optional[float] = Field(default=None)