From 7c88e22424a1f4d93c6a6f9c5d56578438e45c3d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 12 Feb 2026 12:31:37 +0000 Subject: [PATCH] Define Plan and Scenario domain classes --- backend/app/db/models/portfolio.py | 151 ++++++++++++++++------- backend/app/db/models/recommendations.py | 4 +- backend/domain/plan.py | 30 +++++ backend/domain/scenario.py | 46 +++++++ 4 files changed, 186 insertions(+), 45 deletions(-) create mode 100644 backend/domain/plan.py create mode 100644 backend/domain/scenario.py diff --git a/backend/app/db/models/portfolio.py b/backend/app/db/models/portfolio.py index d151bdc4..54de8dcc 100644 --- a/backend/app/db/models/portfolio.py +++ b/backend/app/db/models/portfolio.py @@ -1,7 +1,17 @@ import enum import pytz import datetime -from sqlalchemy import Column, Integer, Text, Boolean, Float, DateTime, Enum, ForeignKey, CheckConstraint +from sqlalchemy import ( + Column, + Integer, + Text, + Boolean, + Float, + DateTime, + Enum, + ForeignKey, + CheckConstraint, +) from sqlalchemy.ext.declarative import declarative_base from backend.app.db.models.users import UserModel # noqa from backend.app.db.models.materials import MaterialType @@ -31,23 +41,43 @@ class PortfolioGoal(enum.Enum): class Portfolio(Base): - __tablename__ = 'portfolio' + __tablename__ = "portfolio" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(Text, nullable=False) budget = Column(Float) - status = Column(Enum(PortfolioStatus, values_callable=lambda x: [e.value for e in x]), nullable=False) - goal = Column(Enum(PortfolioGoal, values_callable=lambda x: [e.value for e in x]), nullable=False) + status = Column( + Enum(PortfolioStatus, values_callable=lambda x: [e.value for e in x]), + nullable=False, + ) + goal = Column( + Enum(PortfolioGoal, values_callable=lambda x: [e.value for e in x]), + nullable=False, + ) cost = Column(Float) number_of_properties = Column(Integer) - co2_equivalent_savings = Column(Float) # Unit is always tonnes so we don't need to store the unit - energy_savings = Column(Float) # Unit is always kWh so we don't need to store the unit - energy_cost_savings = Column(Float) # Unit is always £ so we don't need to store the unit for the moment - property_valuation_increase = Column(Float) # Unit is always £ so we don't need to store the unit for the moment - rental_yield_increase = Column(Float) # Unit is always £ so we don't need to store the unit for the moment + co2_equivalent_savings = Column( + Float + ) # Unit is always tonnes so we don't need to store the unit + energy_savings = Column( + Float + ) # Unit is always kWh so we don't need to store the unit + energy_cost_savings = Column( + Float + ) # Unit is always £ so we don't need to store the unit for the moment + property_valuation_increase = Column( + Float + ) # Unit is always £ so we don't need to store the unit for the moment + rental_yield_increase = Column( + Float + ) # Unit is always £ so we don't need to store the unit for the moment total_work_hours = Column(Float) labour_days = Column(Float) - created_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) - updated_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) + created_at = Column( + DateTime, nullable=False, default=datetime.datetime.now(pytz.utc) + ) + updated_at = Column( + DateTime, nullable=False, default=datetime.datetime.now(pytz.utc) + ) # Aggregations for summary epc_breakdown_pre_retrofit = Column(Text) epc_breakdown_post_retrofit = Column(Text) @@ -71,7 +101,7 @@ class PropertyCreationStatus(enum.Enum): ERROR = "ERROR" -class Epc(enum.Enum): +class Epc(enum.Enum): # TODO: Move to domain? A = "A" B = "B" C = "C" @@ -82,20 +112,27 @@ class Epc(enum.Enum): class PropertyModel(Base): - __tablename__ = 'property' + __tablename__ = "property" id = Column(Integer, primary_key=True, autoincrement=True) - portfolio_id = Column(Integer, ForeignKey('portfolio.id'), nullable=False) + portfolio_id = Column(Integer, ForeignKey("portfolio.id"), nullable=False) creation_status = Column(Enum(PropertyCreationStatus), nullable=False) uprn = Column(Integer) landlord_property_id = Column(Text) building_reference_number = Column(Integer) - status = Column(Enum(PortfolioStatus, values_callable=lambda x: [e.value for e in x]), nullable=False) + status = Column( + Enum(PortfolioStatus, values_callable=lambda x: [e.value for e in x]), + nullable=False, + ) address = Column(Text) postcode = Column(Text) has_pre_condition_report = Column(Boolean) has_recommendations = Column(Boolean) - created_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) - updated_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) + created_at = Column( + DateTime, nullable=False, default=datetime.datetime.now(pytz.utc) + ) + updated_at = Column( + DateTime, nullable=False, default=datetime.datetime.now(pytz.utc) + ) property_type = Column(Text) built_form = Column(Text) local_authority = Column(Text) @@ -127,7 +164,7 @@ rating_lookup = { "Average": FeatureRating.AVERAGE, "Poor": FeatureRating.POOR, "Very Poor": FeatureRating.VERY_POOR, - "N/A": FeatureRating.NA + "N/A": FeatureRating.NA, } @@ -136,32 +173,45 @@ def get_feature_rating_from_string(rating_str: str): class PropertyDetailsEpcModel(Base): - __tablename__ = 'property_details_epc' + __tablename__ = "property_details_epc" id = Column(Integer, primary_key=True, autoincrement=True) - property_id = Column(Integer, ForeignKey('property.id'), nullable=False) - portfolio_id = Column(Integer, ForeignKey('portfolio.id'), nullable=False) + property_id = Column(Integer, ForeignKey("property.id"), nullable=False) + portfolio_id = Column(Integer, ForeignKey("portfolio.id"), nullable=False) full_address = Column(Text) lodgement_date = Column(DateTime) is_expired = Column(Boolean) total_floor_area = Column(Float) walls = Column(Text) - walls_rating = Column(Integer, CheckConstraint('walls_rating>=1 AND walls_rating<=5')) + walls_rating = Column( + Integer, CheckConstraint("walls_rating>=1 AND walls_rating<=5") + ) roof = Column(Text) - roof_rating = Column(Integer, CheckConstraint('roof_rating>=1 AND roof_rating<=5')) + roof_rating = Column(Integer, CheckConstraint("roof_rating>=1 AND roof_rating<=5")) floor = Column(Text) - floor_rating = Column(Integer, CheckConstraint('floor_rating>=1 AND floor_rating<=5')) + floor_rating = Column( + Integer, CheckConstraint("floor_rating>=1 AND floor_rating<=5") + ) windows = Column(Text) - windows_rating = Column(Integer, CheckConstraint('windows_rating>=1 AND windows_rating<=5')) + windows_rating = Column( + Integer, CheckConstraint("windows_rating>=1 AND windows_rating<=5") + ) heating = Column(Text) - heating_rating = Column(Integer, CheckConstraint('heating_rating>=1 AND heating_rating<=5')) + heating_rating = Column( + Integer, CheckConstraint("heating_rating>=1 AND heating_rating<=5") + ) heating_controls = Column(Text) heating_controls_rating = Column( - Integer, CheckConstraint('heating_controls_rating>=1 AND heating_controls_rating<=5') + Integer, + CheckConstraint("heating_controls_rating>=1 AND heating_controls_rating<=5"), ) hot_water = Column(Text) - hot_water_rating = Column(Integer, CheckConstraint('hot_water_rating>=1 AND hot_water_rating<=5')) + hot_water_rating = Column( + Integer, CheckConstraint("hot_water_rating>=1 AND hot_water_rating<=5") + ) lighting = Column(Text) - lighting_rating = Column(Integer, CheckConstraint('lighting_rating>=1 AND lighting_rating<=5')) + lighting_rating = Column( + Integer, CheckConstraint("lighting_rating>=1 AND lighting_rating<=5") + ) mainfuel = Column(Text) ventilation = Column(Text) solar_pv = Column(Text) @@ -219,7 +269,7 @@ class PropertyDetailsSpatial(Base): class PropertyDetailsMeter(Base): - __tablename__ = 'property_details_meter' + __tablename__ = "property_details_meter" id = Column(Integer, primary_key=True, autoincrement=True) uprn = Column(Integer, nullable=False) energy_supplier = Column(Text) @@ -230,11 +280,13 @@ class PropertyDetailsMeter(Base): class PropertyTargetsModel(Base): - __tablename__ = 'property_targets' + __tablename__ = "property_targets" id = Column(Integer, primary_key=True, autoincrement=True) - property_id = Column(Integer, ForeignKey('property.id'), nullable=False) - portfolio_id = Column(Integer, ForeignKey('portfolio.id'), nullable=False) - created_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) + property_id = Column(Integer, ForeignKey("property.id"), nullable=False) + portfolio_id = Column(Integer, ForeignKey("portfolio.id"), nullable=False) + created_at = Column( + DateTime, nullable=False, default=datetime.datetime.now(pytz.utc) + ) epc = Column(Enum(Epc)) heat_demand = Column(Text) @@ -242,23 +294,36 @@ class PropertyTargetsModel(Base): class PortfolioUsers(Base): __tablename__ = "portfolioUsers" id = Column(Integer, primary_key=True, autoincrement=True) - user_id = Column(Integer, ForeignKey('user.id'), nullable=False) - portfolioId = Column(Integer, ForeignKey('portfolio.id'), nullable=False) + user_id = Column(Integer, ForeignKey("user.id"), nullable=False) + portfolioId = Column(Integer, ForeignKey("portfolio.id"), nullable=False) role = Column(Text, nullable=False) - created_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) - updated_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) + created_at = Column( + DateTime, nullable=False, default=datetime.datetime.now(pytz.utc) + ) + updated_at = Column( + DateTime, nullable=False, default=datetime.datetime.now(pytz.utc) + ) class PropertyInstalledMeasures(Base): """ This model keeps a record of the installed measures for each property, at the UPRN level """ - __tablename__ = 'property_installed_measures' + + __tablename__ = "property_installed_measures" id = Column(Integer, primary_key=True, autoincrement=True) uprn = Column(Integer, nullable=False) measure_type = Column( - Enum(MaterialType, values_callable=lambda x: [e.value for e in x], create_constraint=False), - nullable=False + Enum( + MaterialType, + values_callable=lambda x: [e.value for e in x], + create_constraint=False, + ), + nullable=False, + ) + created_at = Column( + DateTime, nullable=False, default=datetime.datetime.now(pytz.utc) + ) + installed_at = Column( + DateTime, nullable=False, default=datetime.datetime.now(pytz.utc) ) - created_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) - installed_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) diff --git a/backend/app/db/models/recommendations.py b/backend/app/db/models/recommendations.py index 759c088e..356c0fd7 100644 --- a/backend/app/db/models/recommendations.py +++ b/backend/app/db/models/recommendations.py @@ -66,7 +66,7 @@ class RecommendationMaterials(Base): estimated_cost = Column(Float, nullable=False) -class PlanTypeEnum(enum.Enum): +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" @@ -93,7 +93,7 @@ class PlanModel(Base): BigInteger, ForeignKey("scenario.id") ) - created_at: Mapped = mapped_column( # type: ignore + created_at: Mapped[datetime] = mapped_column( # type: ignore TIMESTAMP, nullable=False, server_default=func.now() ) diff --git a/backend/domain/plan.py b/backend/domain/plan.py new file mode 100644 index 00000000..b14213c1 --- /dev/null +++ b/backend/domain/plan.py @@ -0,0 +1,30 @@ +from datetime import datetime +from typing import Optional + +from backend.app.db.models.portfolio import Epc +from backend.app.db.models.recommendations import PlanTypeEnum +from backend.domain.scenario import Scenario + + +class Plan: + property_id: int + portfolio_id: int + scenario: Scenario + created_at: datetime + is_default: bool + + valuation_increase_lower_bound: Optional[float] = None + valuation_increase_upper_bound: Optional[float] = None + valuation_increase_average: Optional[float] = None + plan_type: Optional[PlanTypeEnum] = None + post_sap_points: Optional[float] = None + post_epc_rating: Optional[Epc] = None + post_co2_emissions: Optional[float] = None + co2_savings: Optional[float] = None + post_energy_bill: Optional[float] = None + post_energy_consumption: Optional[float] = None + energy_consumption_savings: Optional[float] = None + valuation_post_retrofit: Optional[float] = None + valuation_increase: Optional[float] = None + cost_of_works: Optional[float] = None + contingency_cost: Optional[float] = None diff --git a/backend/domain/scenario.py b/backend/domain/scenario.py new file mode 100644 index 00000000..4a15fc09 --- /dev/null +++ b/backend/domain/scenario.py @@ -0,0 +1,46 @@ +from datetime import datetime +from typing import Optional + + +class Scenario: + name: str + created_at: datetime + housing_type: str + goal: str # TODO: make enum + goal_value: str + trigger_file_path: str + multi_plan: bool + is_default: bool # TODO: isn't this Plan-level? + + budget: Optional[float] = None + already_installed_file_path: Optional[str] = None + patches_file_path: Optional[str] = None + non_invasive_recommendations_file_path: Optional[str] = None + exclusions: Optional[str] = None + + # Previously portfolio-level fields + # TODO: are these needed scenario-level? + cost: Optional[float] = None + contingency: Optional[float] = None + funding: Optional[float] = None + total_work_hours: Optional[float] = None + energy_savings: Optional[float] = None + co2_equivalent_savings: Optional[float] = None + energy_cost_savings: Optional[float] = None + epc_breakdown_pre_retrofit: Optional[int] = None + epc_breakdown_post_retrofit: Optional[int] = None + number_of_properties: Optional[int] = None + n_units_to_retrofit: Optional[int] = None + co2_per_unit_pre_retrofit: Optional[str] = None + co2_per_unit_post_retrofit: Optional[str] = None + energy_bill_per_unit_pre_retrofit: Optional[str] = None + energy_bill_per_unit_post_retrofit: Optional[str] = None + energy_consumption_per_unit_pre_retrofit: Optional[str] = None + energy_consumption_per_unit_post_retrofit: Optional[str] = None + valuation_improvement_per_unit: Optional[str] = None + cost_per_unit: Optional[str] = None + cost_per_co2_saved: Optional[str] = None + cost_per_sap_point: Optional[str] = None + valuation_return_on_ivestment: Optional[str] = None + property_valuation_increase: Optional[float] = None + labour_days: Optional[float] = None