diff --git a/backend/app/db/models/portfolio.py b/backend/app/db/models/portfolio.py index 54de8dcc..f6a99a97 100644 --- a/backend/app/db/models/portfolio.py +++ b/backend/app/db/models/portfolio.py @@ -32,7 +32,7 @@ class PortfolioStatus(enum.Enum): NEEDS_REVIEW = "needs review" -class PortfolioGoal(enum.Enum): +class PortfolioGoal(enum.Enum): # TODO: Move to domain? VALUATION_IMPROVEMENT = "Valuation Improvement" INCREASING_EPC = "Increasing EPC" REDUCING_CO2_EMISSIONS = "Reducing CO2 emissions" diff --git a/backend/app/db/models/recommendations.py b/backend/app/db/models/recommendations.py index 356c0fd7..82032d35 100644 --- a/backend/app/db/models/recommendations.py +++ b/backend/app/db/models/recommendations.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import declarative_base, Mapped, mapped_column from sqlalchemy.sql import func from datetime import datetime -from backend.app.db.models.portfolio import Portfolio, PropertyModel +from backend.app.db.models.portfolio import Portfolio, PortfolioGoal, PropertyModel from backend.app.db.models.materials import Material from backend.app.db.models.portfolio import Epc from datatypes.enums import QuantityUnits @@ -152,8 +152,8 @@ class ScenarioModel(Base): BigInteger, ForeignKey(Portfolio.id), nullable=False ) housing_type: Mapped[str] = mapped_column(String, nullable=False) - goal: Mapped[str] = mapped_column(String, nullable=False) - goal_value: Mapped[str] = mapped_column(String, nullable=False) + goal: Mapped[PortfolioGoal] = mapped_column(Enum(PortfolioGoal), nullable=False) + goal_value: Mapped[Optional[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) diff --git a/backend/app/domain/classes/plan.py b/backend/app/domain/classes/plan.py index 76aba958..b44543a6 100644 --- a/backend/app/domain/classes/plan.py +++ b/backend/app/domain/classes/plan.py @@ -2,6 +2,7 @@ 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 from backend.app.domain.classes.scenario import Scenario from backend.app.domain.records.plan_record import PlanRecord @@ -48,5 +49,14 @@ class Plan: def is_compliant(self) -> bool: raise NotImplementedError + goal: PortfolioGoal = self.scenario.record.goal + goal_value: str = self.scenario.record.goal_value + + match goal: + case PortfolioGoal.INCREASING_EPC: + return True + case _: + raise NotImplementedError + def set_default(self, value: bool) -> None: self.record = replace(self.record, is_default=value) diff --git a/backend/app/domain/classes/scenario.py b/backend/app/domain/classes/scenario.py index 657ca1ef..3c22657e 100644 --- a/backend/app/domain/classes/scenario.py +++ b/backend/app/domain/classes/scenario.py @@ -9,7 +9,7 @@ from backend.app.domain.records.scenario_record import ScenarioRecord class Scenario: def __init__(self, record: ScenarioRecord, id: Optional[int] = None): self.id = id - self._record = record + self.record = record @classmethod def from_sqlalchemy(cls, scenario_model: ScenarioModel) -> Scenario: @@ -55,4 +55,4 @@ class Scenario: return cls(record, scenario_model.id) def set_default(self, value: bool) -> None: - self._record = replace(self._record, is_default=value) + self.record = replace(self.record, is_default=value) diff --git a/backend/app/domain/records/scenario_record.py b/backend/app/domain/records/scenario_record.py index 09367203..48ddf0ca 100644 --- a/backend/app/domain/records/scenario_record.py +++ b/backend/app/domain/records/scenario_record.py @@ -2,14 +2,15 @@ from dataclasses import dataclass from datetime import datetime from typing import Optional +from backend.app.db.models.portfolio import PortfolioGoal + @dataclass(frozen=True) class ScenarioRecord: name: str created_at: datetime housing_type: str - goal: str - goal_value: str + goal: PortfolioGoal trigger_file_path: str multi_plan: bool is_default: bool @@ -19,6 +20,7 @@ class ScenarioRecord: non_invasive_recommendations_file_path: Optional[str] = None exclusions: Optional[str] = None + goal_value: Optional[str] = None cost: Optional[float] = None contingency: Optional[float] = None funding: Optional[float] = None diff --git a/backend/categorisation/tests/test_plan_is_compliant.py b/backend/categorisation/tests/test_plan_is_compliant.py index 41fb1b85..c0f7add0 100644 --- a/backend/categorisation/tests/test_plan_is_compliant.py +++ b/backend/categorisation/tests/test_plan_is_compliant.py @@ -1,4 +1,4 @@ -from typing import Callable +from typing import Callable, Optional import pytest from datetime import datetime @@ -6,7 +6,7 @@ from backend.app.domain.classes.plan import Plan from backend.app.domain.classes.scenario import Scenario from backend.app.domain.records.plan_record import PlanRecord from backend.app.domain.records.scenario_record import ScenarioRecord -from backend.app.db.models.portfolio import Epc +from backend.app.db.models.portfolio import Epc, PortfolioGoal @pytest.fixture @@ -14,28 +14,17 @@ def created_at_datetime() -> datetime: return datetime.now() -@pytest.fixture -def epc_c_scenario(created_at_datetime: datetime) -> "Scenario": - # arrange - scenario_record = ScenarioRecord( - name="EPC C", - created_at=created_at_datetime, - housing_type="", - goal="EPC", - goal_value="C", - trigger_file_path="", - multi_plan=False, - is_default=False, - ) - return Scenario(record=scenario_record, id=1) - - @pytest.fixture def plan_factory( - epc_c_scenario: "Scenario", created_at_datetime: datetime -) -> Callable[[int, "Epc"], "Plan"]: - # returns a function to create plans with different attributes - def _create_plan(post_sap_points: int, post_epc_rating: "Epc") -> "Plan": + created_at_datetime: datetime, +) -> Callable[[int, "Epc", "Scenario"], "Plan"]: + """ + Returns a factory function to create plans with different attributes and scenarios. + """ + + def _create_plan( + post_sap_points: int, post_epc_rating: "Epc", scenario: "Scenario" + ) -> "Plan": plan_record = PlanRecord( property_id=1, portfolio_id=1, @@ -44,27 +33,43 @@ def plan_factory( post_sap_points=post_sap_points, post_epc_rating=post_epc_rating, ) - return Plan(record=plan_record, scenario=epc_c_scenario, id=1) + return Plan(record=plan_record, scenario=scenario, id=1) return _create_plan @pytest.mark.parametrize( - "post_sap_points, post_epc_rating, expected_compliance", + "scenario_name, goal_value, post_sap_points, post_epc_rating, expected_compliance", [ - (75, Epc.C, True), - (100, Epc.A, True), - (60, Epc.D, False), + ("EPC C", "C", 75, Epc.C, True), + ("EPC A", "A", 100, Epc.A, True), + ("EPC D", "D", 60, Epc.D, False), + ("Achieve EPC B", None, 100, Epc.A, True), + ("Achieve EPC B", None, 60, Epc.D, False), ], ) def test_scenario_goal_is_epc_c( - plan_factory: Callable[[int, "Epc"], "Plan"], + plan_factory: Callable[[int, "Epc", "Scenario"], "Plan"], + scenario_name: str, + goal_value: Optional[str], post_sap_points: int, post_epc_rating: "Epc", expected_compliance: bool, ) -> None: # arrange - plan = plan_factory(post_sap_points, post_epc_rating) + scenario_record = ScenarioRecord( + name=scenario_name, + created_at=datetime.now(), + housing_type="", + goal=PortfolioGoal.INCREASING_EPC, + goal_value=goal_value, + trigger_file_path="", + multi_plan=False, + is_default=False, + ) + scenario = Scenario(record=scenario_record, id=1) + + plan = plan_factory(post_sap_points, post_epc_rating, scenario) # act actual_compliance: bool = plan.is_compliant