from __future__ import annotations from dataclasses import replace from typing import Optional from backend.app.db.models.portfolio import PortfolioGoal from backend.app.db.models.recommendations import ( PlanModel, PlanPersistence, ScenarioModel, ) from backend.app.domain.classes.scenario import Scenario from backend.app.domain.records.plan_record import PlanRecord from backend.app.utils import sap_to_epc class Plan: def __init__( self, record: PlanRecord, scenario: Scenario, id: Optional[int] = None ): self.id: Optional[int] = id self.record: PlanRecord = record self.scenario: Scenario = scenario @classmethod def from_sqlalchemy(cls, plan_model: PlanModel, scenario: Scenario) -> Plan: if not scenario: raise ValueError(f"No Scenario associated with Plan of ID {plan_model.id}") record = PlanRecord( property_id=plan_model.property_id, portfolio_id=plan_model.portfolio_id, created_at=plan_model.created_at, is_default=plan_model.is_default, valuation_increase_lower_bound=plan_model.valuation_increase_lower_bound, valuation_increase_upper_bound=plan_model.valuation_increase_upper_bound, valuation_increase_average=plan_model.valuation_increase_average, plan_type=plan_model.plan_type, post_sap_points=plan_model.post_sap_points, post_epc_rating=plan_model.post_epc_rating, post_co2_emissions=plan_model.post_co2_emissions, co2_savings=plan_model.co2_savings, post_energy_bill=plan_model.post_energy_bill, energy_bill_savings=plan_model.energy_bill_savings, post_energy_consumption=plan_model.post_energy_consumption, energy_consumption_savings=plan_model.energy_consumption_savings, valuation_post_retrofit=plan_model.valuation_post_retrofit, valuation_increase=plan_model.valuation_increase, cost_of_works=plan_model.cost_of_works, contingency_cost=plan_model.contingency_cost, name=plan_model.name, ) return cls(record=record, scenario=scenario, id=plan_model.id) @property def is_compliant(self) -> bool: goal: PortfolioGoal = self.scenario.record.goal match goal: case PortfolioGoal.INCREASING_EPC: return self._is_compliant_epc() case _: raise NotImplementedError @property def cost(self) -> float: return ( self.record.cost_of_works if self.record.cost_of_works is not None else float("inf") ) def to_sqlalchemy(self) -> PlanPersistence: scenario_record = self.scenario.record scenario_model = ScenarioModel( id=self.scenario.id, name=scenario_record.name, created_at=scenario_record.created_at, housing_type=scenario_record.housing_type, goal=scenario_record.goal, goal_value=scenario_record.goal_value, trigger_file_path=scenario_record.trigger_file_path, multi_plan=scenario_record.multi_plan, is_default=scenario_record.is_default, budget=scenario_record.budget, already_installed_file_path=scenario_record.already_installed_file_path, patches_file_path=scenario_record.patches_file_path, non_invasive_recommendations_file_path=scenario_record.non_invasive_recommendations_file_path, exclusions=scenario_record.exclusions, cost=scenario_record.cost, contingency=scenario_record.contingency, funding=scenario_record.funding, total_work_hours=scenario_record.total_work_hours, energy_savings=scenario_record.energy_savings, co2_equivalent_savings=scenario_record.co2_equivalent_savings, energy_cost_savings=scenario_record.energy_cost_savings, epc_breakdown_pre_retrofit=scenario_record.epc_breakdown_pre_retrofit, epc_breakdown_post_retrofit=scenario_record.epc_breakdown_post_retrofit, number_of_properties=scenario_record.number_of_properties, n_units_to_retrofit=scenario_record.n_units_to_retrofit, co2_per_unit_pre_retrofit=scenario_record.co2_per_unit_pre_retrofit, co2_per_unit_post_retrofit=scenario_record.co2_per_unit_post_retrofit, energy_bill_per_unit_pre_retrofit=scenario_record.energy_bill_per_unit_pre_retrofit, energy_bill_per_unit_post_retrofit=scenario_record.energy_bill_per_unit_post_retrofit, energy_consumption_per_unit_pre_retrofit=scenario_record.energy_consumption_per_unit_pre_retrofit, energy_consumption_per_unit_post_retrofit=scenario_record.energy_consumption_per_unit_post_retrofit, valuation_improvement_per_unit=scenario_record.valuation_improvement_per_unit, cost_per_unit=scenario_record.cost_per_unit, cost_per_co2_saved=scenario_record.cost_per_co2_saved, cost_per_sap_point=scenario_record.cost_per_sap_point, valuation_return_on_investment=scenario_record.valuation_return_on_investment, property_valuation_increase=scenario_record.property_valuation_increase, labour_days=scenario_record.labour_days, ) record = self.record plan_model = PlanModel( id=self.id, property_id=record.property_id, portfolio_id=record.portfolio_id, scenario_id=self.scenario.id, created_at=record.created_at, is_default=record.is_default, valuation_increase_lower_bound=record.valuation_increase_lower_bound, valuation_increase_upper_bound=record.valuation_increase_upper_bound, valuation_increase_average=record.valuation_increase_average, plan_type=record.plan_type, post_sap_points=record.post_sap_points, post_epc_rating=record.post_epc_rating, post_co2_emissions=record.post_co2_emissions, co2_savings=record.co2_savings, post_energy_bill=record.post_energy_bill, energy_bill_savings=record.energy_bill_savings, post_energy_consumption=record.post_energy_consumption, energy_consumption_savings=record.energy_consumption_savings, valuation_post_retrofit=record.valuation_post_retrofit, valuation_increase=record.valuation_increase, cost_of_works=record.cost_of_works, contingency_cost=record.contingency_cost, name=record.name, ) return PlanPersistence(plan=plan_model, scenario=scenario_model) def set_default(self, value: bool) -> None: self.record = replace(self.record, is_default=value) self.scenario.record = replace(self.scenario.record, is_default=value) def _is_compliant_epc(self) -> bool: goal_value: str = self.scenario.record.goal_value if self.record.post_epc_rating: post_epc = self.record.post_epc_rating.value elif self.record.post_sap_points: post_epc = sap_to_epc(self.record.post_sap_points) else: return False return post_epc <= goal_value