Model/infrastructure/postgres/modelling/plan_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

106 lines
4 KiB
Python

from __future__ import annotations
import enum
from datetime import datetime
from typing import ClassVar, Optional
from sqlalchemy import Column, TIMESTAMP
from sqlalchemy import Enum as SAEnum
from sqlalchemy.sql import func
from sqlmodel import Field, SQLModel
from datatypes.epc.domain.epc import Epc
from domain.modelling.plan import Plan
# Calculator metrics are in kg CO₂/yr; the live ``plan`` columns are tonnes
# (legacy ``emissions_kg / 1000``). Convert on the way in.
_KG_PER_TONNE = 1000.0
class PlanType(enum.Enum):
SOLAR_ECO4 = "solar_eco4"
SOLAR_HHRSH_ECO4 = "solar_hhrsh_eco4"
EMPTY_CAVITY_ECO = "empty_cavity_eco"
PARTIAL_CAVITY_ECO = "partial_cavity_eco"
EXTRACTION_ECO = "extraction_eco"
class PlanModel(SQLModel, table=True):
"""The single SQLModel definition of the live ``plan`` table (ADR-0017
amendment). Full legacy column parity; out-of-cluster references
(``portfolio_id`` / ``property_id`` / ``scenario_id``) are plain indexed
ints, not FK constraints (mirror convention — the live FKs are owned by the
Drizzle schema)."""
__tablename__: ClassVar[str] = "plan" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
name: Optional[str] = Field(default="")
portfolio_id: int
property_id: int = Field(index=True)
scenario_id: Optional[int] = Field(default=None)
created_at: Optional[datetime] = Field(
default=None,
sa_column=Column(TIMESTAMP, nullable=False, server_default=func.now()),
)
is_default: bool = False
valuation_increase_lower_bound: Optional[float] = Field(default=None)
valuation_increase_upper_bound: Optional[float] = Field(default=None)
valuation_increase_average: Optional[float] = Field(default=None)
plan_type: Optional[PlanType] = Field(
default=None,
sa_column=Column(
SAEnum(
PlanType,
name="plan_type",
values_callable=lambda cls: [m.value for m in cls], # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType, reportUnknownVariableType]
create_type=False,
),
nullable=True,
),
)
post_sap_points: Optional[float] = Field(default=None)
post_epc_rating: Optional[Epc] = Field(
default=None,
sa_column=Column(SAEnum(Epc, name="epc"), nullable=True),
)
post_co2_emissions: Optional[float] = Field(default=None) # tonnes/yr
co2_savings: Optional[float] = Field(default=None) # tonnes/yr
post_energy_bill: Optional[float] = Field(default=None) # £/yr
energy_bill_savings: Optional[float] = Field(default=None) # £/yr
post_energy_consumption: Optional[float] = Field(default=None) # kWh/yr
energy_consumption_savings: Optional[float] = Field(default=None) # kWh/yr
valuation_post_retrofit: Optional[float] = Field(default=None)
valuation_increase: Optional[float] = Field(default=None)
cost_of_works: Optional[float] = Field(default=None)
contingency_cost: Optional[float] = Field(default=None)
@classmethod
def from_domain(
cls,
plan: Plan,
*,
property_id: int,
scenario_id: int,
portfolio_id: int,
is_default: bool,
) -> "PlanModel":
return cls(
portfolio_id=portfolio_id,
property_id=property_id,
scenario_id=scenario_id,
is_default=is_default,
post_sap_points=plan.post_sap_continuous,
post_epc_rating=plan.post_epc_rating,
post_co2_emissions=plan.post_retrofit.co2_kg_per_yr / _KG_PER_TONNE,
co2_savings=plan.co2_savings_kg_per_yr / _KG_PER_TONNE,
cost_of_works=plan.cost_of_works,
contingency_cost=plan.contingency_cost,
post_energy_bill=plan.post_energy_bill,
energy_bill_savings=plan.energy_bill_savings,
post_energy_consumption=plan.post_energy_consumption,
energy_consumption_savings=plan.energy_consumption_savings,
)