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, # Valuation Uplift £ forms (NULL when no Property Valuation is known; # the percentage is not persisted on the live plan columns — ADR-0018). valuation_increase_lower_bound=plan.valuation.lower_value, valuation_increase_upper_bound=plan.valuation.upper_value, valuation_increase_average=plan.valuation.average_value, valuation_post_retrofit=plan.valuation.post_retrofit_value, valuation_increase=plan.valuation.average_value, )