From f34a6269f7ae6a06de67171106cd5958aa547140 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 13 Feb 2026 09:39:25 +0000 Subject: [PATCH] Move updating of is_default to domain rather than database layer --- .../db/functions/recommendations_functions.py | 6 +- backend/app/domain/classes/plan.py | 78 ++++++++++++++++++- backend/categorisation/processor.py | 16 +++- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/backend/app/db/functions/recommendations_functions.py b/backend/app/db/functions/recommendations_functions.py index 2f85cbec..2fdb6142 100644 --- a/backend/app/db/functions/recommendations_functions.py +++ b/backend/app/db/functions/recommendations_functions.py @@ -632,12 +632,12 @@ def get_scenario(scenario_id: int) -> Optional[ScenarioModel]: return session_any.exec(stmt).scalar_one_or_none() -def set_plan_default(plan_id: int, is_default: bool) -> bool: +def update_plan(plan_model: PlanModel, scenario_model: ScenarioModel) -> bool: with db_read_session() as session: stmt = ( update(PlanModel) - .where(PlanModel.id == plan_id) - .values(is_default=is_default) + .where(PlanModel.id == plan_model.id) + .values(**plan_model.model_dump(exclude={"id"}, exclude_unset=True)) ) result = session.exec(stmt) session.commit() diff --git a/backend/app/domain/classes/plan.py b/backend/app/domain/classes/plan.py index e1215178..2b1d3026 100644 --- a/backend/app/domain/classes/plan.py +++ b/backend/app/domain/classes/plan.py @@ -2,8 +2,10 @@ from __future__ import annotations from dataclasses import replace from typing import Optional +from sqlalchemy import Tuple + from backend.app.db.models.portfolio import PortfolioGoal -from backend.app.db.models.recommendations import PlanModel +from backend.app.db.models.recommendations import PlanModel, 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 @@ -56,8 +58,82 @@ class Plan: case _: raise NotImplementedError + def to_sqlalchemy(self) -> Tuple[PlanModel, ScenarioModel]: + 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, + ) + + return Tuple(plan_model, scenario_model) # TODO: create a type for this + 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 diff --git a/backend/categorisation/processor.py b/backend/categorisation/processor.py index 55a1a1c6..9c1bb8f0 100644 --- a/backend/categorisation/processor.py +++ b/backend/categorisation/processor.py @@ -1,11 +1,15 @@ from collections import defaultdict -from typing import List +from typing import List, cast + +from sqlalchemy import Tuple from backend.app.db.functions.recommendations_functions import ( get_plans_by_portfolio_id, get_scenario, set_plan_default, + update_plan, ) +from backend.app.db.models.recommendations import PlanModel, ScenarioModel from backend.app.domain.classes.plan import Plan from backend.categorisation.categorisation_logic import CategorisationLogic from utils.logger import setup_logger @@ -58,7 +62,11 @@ def _update_default_flags(plans: List[Plan], cheapest_plan: Plan) -> None: if plan.id is None: raise ValueError("Cannot update Plan with missing ID") - set_plan_default( - plan.id, - plan.id == cheapest_plan.id, + plan.set_default(plan.id == cheapest_plan.id) + + plan_model, scenario_model = cast( + tuple[PlanModel, ScenarioModel], + plan.to_sqlalchemy(), ) + + update_plan(plan_model, scenario_model)