import enum from typing import Iterable, List, NamedTuple, Optional, Type from sqlalchemy import ( Column, BigInteger, String, Float, Boolean, TIMESTAMP, ForeignKey, Enum, ) from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.sql import func from datetime import datetime from backend.app.db.base import Base from backend.app.db.models.portfolio import Portfolio, PortfolioGoal, PropertyModel from backend.app.db.models.materials import Material from datatypes.enums import QuantityUnits from datatypes.epc.domain.epc import Epc def portfolio_goal_values(enum_cls: Type[PortfolioGoal]) -> List[str]: return [e.value for e in enum_cls] class Recommendation(Base): __tablename__ = "recommendation" id = Column(BigInteger, primary_key=True, autoincrement=True) property_id = Column(BigInteger, ForeignKey(PropertyModel.id), nullable=False) created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) type = Column(String, nullable=False) measure_type = Column(String) description = Column(String, nullable=False) estimated_cost = Column(Float) default = Column(Boolean, nullable=False) starting_u_value = Column(Float) new_u_value = Column(Float) sap_points = Column(Float) heat_demand = Column(Float) kwh_savings = Column(Float) co2_equivalent_savings = Column(Float) energy_savings = Column(Float) energy_cost_savings = Column(Float) property_valuation_increase = Column(Float) rental_yield_increase = Column(Float) total_work_hours = Column(Float) labour_days = Column(Float) already_installed = Column(Boolean, nullable=False, default=False) class RecommendationMaterials(Base): __tablename__ = "recommendation_materials" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) recommendation_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("recommendation.id"), nullable=False, ) material_id: Mapped[int] = mapped_column( BigInteger, ForeignKey(Material.id), nullable=False, ) created_at: Mapped[datetime] = mapped_column( TIMESTAMP, nullable=False, server_default=func.now(), ) depth: Mapped[float] = mapped_column( Float, nullable=False, ) quantity: Mapped[float] = mapped_column( Float, nullable=False, ) quantity_unit: Mapped[QuantityUnits] = mapped_column( Enum(QuantityUnits, values_callable=lambda x: [e.value for e in x]), nullable=False, ) estimated_cost: Mapped[float] = mapped_column( Float, nullable=False, ) class PlanTypeEnum(enum.Enum): # TODO: move this to domain? 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(Base): __tablename__ = "plan" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) name: Mapped[Optional[str]] = mapped_column(String, nullable=True, default="") portfolio_id: Mapped[int] = mapped_column( BigInteger, ForeignKey(Portfolio.id), nullable=False ) property_id: Mapped[int] = mapped_column( BigInteger, ForeignKey(PropertyModel.id), nullable=False ) scenario_id: Mapped[Optional[int]] = mapped_column( BigInteger, ForeignKey("scenario.id") ) created_at: Mapped[datetime] = mapped_column( # type: ignore TIMESTAMP, nullable=False, server_default=func.now() ) is_default: Mapped[bool] = mapped_column(Boolean, nullable=False) valuation_increase_lower_bound: Mapped[Optional[float]] = mapped_column(Float) valuation_increase_upper_bound: Mapped[Optional[float]] = mapped_column(Float) valuation_increase_average: Mapped[Optional[float]] = mapped_column(Float) plan_type: Mapped[Optional[PlanTypeEnum]] = mapped_column( Enum( PlanTypeEnum, name="plan_type", values_callable=lambda e: [m.value for m in e], create_type=False, ), nullable=True, ) post_sap_points: Mapped[Optional[float]] = mapped_column(Float) post_epc_rating: Mapped[Optional[Epc]] = mapped_column(Enum(Epc)) post_co2_emissions: Mapped[Optional[float]] = mapped_column(Float) co2_savings: Mapped[Optional[float]] = mapped_column(Float) post_energy_bill: Mapped[Optional[float]] = mapped_column(Float) energy_bill_savings: Mapped[Optional[float]] = mapped_column(Float) post_energy_consumption: Mapped[Optional[float]] = mapped_column(Float) energy_consumption_savings: Mapped[Optional[float]] = mapped_column(Float) valuation_post_retrofit: Mapped[Optional[float]] = mapped_column(Float) valuation_increase: Mapped[Optional[float]] = mapped_column(Float) # Financial metrics, excluding funding cost_of_works: Mapped[Optional[float]] = mapped_column(Float) contingency_cost: Mapped[Optional[float]] = mapped_column(Float) class PlanRecommendations(Base): __tablename__ = "plan_recommendations" id = Column(BigInteger, primary_key=True, autoincrement=True) plan_id = Column(BigInteger, ForeignKey("plan.id"), nullable=False) recommendation_id = Column( BigInteger, ForeignKey("recommendation.id"), nullable=False ) class ScenarioModel(Base): __tablename__ = "scenario" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String, nullable=False) created_at: Mapped[datetime] = mapped_column( TIMESTAMP, nullable=False, server_default=func.now() ) budget: Mapped[Optional[float]] = mapped_column(Float) portfolio_id: Mapped[int] = mapped_column( BigInteger, ForeignKey(Portfolio.id), nullable=False ) housing_type: Mapped[str] = mapped_column(String, nullable=False) goal: Mapped[PortfolioGoal] = mapped_column( Enum(PortfolioGoal, values_callable=portfolio_goal_values, name="goal"), nullable=False, ) goal_value: Mapped[str] = mapped_column(String, nullable=False) trigger_file_path: Mapped[str] = mapped_column(String, nullable=False) already_installed_file_path: Mapped[Optional[str]] = mapped_column(String) patches_file_path: Mapped[Optional[str]] = mapped_column(String) non_invasive_recommendations_file_path: Mapped[Optional[str]] = mapped_column( String ) exclusions: Mapped[Optional[str]] = mapped_column(String) multi_plan: Mapped[bool] = mapped_column(Boolean, default=False) is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # Add in the fields we need, which were previously sitting at the portfolio level cost: Mapped[Optional[float]] = mapped_column(Float) contingency: Mapped[Optional[float]] = mapped_column(Float) funding: Mapped[Optional[float]] = mapped_column(Float) total_work_hours: Mapped[Optional[float]] = mapped_column(Float) energy_savings: Mapped[Optional[float]] = mapped_column(Float) co2_equivalent_savings: Mapped[Optional[float]] = mapped_column(Float) energy_cost_savings: Mapped[Optional[float]] = mapped_column(Float) epc_breakdown_pre_retrofit: Mapped[Optional[str]] = mapped_column(String) epc_breakdown_post_retrofit: Mapped[Optional[str]] = mapped_column(String) number_of_properties: Mapped[Optional[int]] = mapped_column(BigInteger) n_units_to_retrofit: Mapped[Optional[int]] = mapped_column(BigInteger) co2_per_unit_pre_retrofit: Mapped[Optional[str]] = mapped_column(String) co2_per_unit_post_retrofit: Mapped[Optional[str]] = mapped_column(String) energy_bill_per_unit_pre_retrofit: Mapped[Optional[str]] = mapped_column(String) energy_bill_per_unit_post_retrofit: Mapped[Optional[str]] = mapped_column(String) energy_consumption_per_unit_pre_retrofit: Mapped[Optional[str]] = mapped_column( String ) energy_consumption_per_unit_post_retrofit: Mapped[Optional[str]] = mapped_column( String ) valuation_improvement_per_unit: Mapped[Optional[str]] = mapped_column(String) cost_per_unit: Mapped[Optional[str]] = mapped_column(String) cost_per_co2_saved: Mapped[Optional[str]] = mapped_column(String) cost_per_sap_point: Mapped[Optional[str]] = mapped_column(String) valuation_return_on_investment: Mapped[Optional[str]] = mapped_column(String) property_valuation_increase: Mapped[Optional[float]] = mapped_column(Float) labour_days: Mapped[Optional[float]] = mapped_column(Float) class MeasureType(enum.Enum): air_source_heat_pump = "air_source_heat_pump" boiler_upgrade = "boiler_upgrade" high_heat_retention_storage_heaters = "high_heat_retention_storage_heaters" secondary_heating = "secondary_heating" roomstat_programmer_trvs = "roomstat_programmer_trvs" time_temperature_zone_control = "time_temperature_zone_control" cylinder_thermostat = "cylinder_thermostat" cavity_wall_insulation = "cavity_wall_insulation" extension_cavity_wall_insulation = "extension_cavity_wall_insulation" external_wall_insulation = "external_wall_insulation" internal_wall_insulation = "internal_wall_insulation" loft_insulation = "loft_insulation" flat_roof_insulation = "flat_roof_insulation" room_roof_insulation = "room_roof_insulation" solid_floor_insulation = "solid_floor_insulation" suspended_floor_insulation = "suspended_floor_insulation" double_glazing = "double_glazing" secondary_glazing = "secondary_glazing" draught_proofing = "draught_proofing" mechanical_ventilation = "mechanical_ventilation" low_energy_lighting = "low_energy_lighting" solar_pv = "solar_pv" hot_water_tank_insulation = "hot_water_tank_insulation" sealing_open_fireplace = "sealing_open_fireplace" class InstalledMeasure(Base): __tablename__ = "installed_measure" id = Column(BigInteger, primary_key=True, autoincrement=True) uprn = Column(BigInteger, nullable=False) measure_type = Column( Enum( MeasureType, name="measure_type", values_callable=lambda e: [m.value for m in e], create_type=False, # <-- critical ), nullable=False, ) installed_at = Column(TIMESTAMP) sap_points = Column(Float) carbon_savings = Column(Float) kwh_savings = Column(Float) bill_savings = Column(Float) heat_demand_savings = Column(Float) source = Column(String) is_active = Column(Boolean, nullable=False, default=True) def enum_values(e: Iterable[PlanTypeEnum]) -> list[str]: return [m.value for m in e] class PlanPersistence(NamedTuple): plan: PlanModel scenario: ScenarioModel