cater for goal_value being NULL 🟥

This commit is contained in:
Daniel Roth 2026-02-12 15:40:03 +00:00
parent 70fd417c4a
commit 080000123f
6 changed files with 54 additions and 37 deletions

View file

@ -32,7 +32,7 @@ class PortfolioStatus(enum.Enum):
NEEDS_REVIEW = "needs review" NEEDS_REVIEW = "needs review"
class PortfolioGoal(enum.Enum): class PortfolioGoal(enum.Enum): # TODO: Move to domain?
VALUATION_IMPROVEMENT = "Valuation Improvement" VALUATION_IMPROVEMENT = "Valuation Improvement"
INCREASING_EPC = "Increasing EPC" INCREASING_EPC = "Increasing EPC"
REDUCING_CO2_EMISSIONS = "Reducing CO2 emissions" REDUCING_CO2_EMISSIONS = "Reducing CO2 emissions"

View file

@ -13,7 +13,7 @@ from sqlalchemy.orm import declarative_base, Mapped, mapped_column
from sqlalchemy.sql import func from sqlalchemy.sql import func
from datetime import datetime 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.materials import Material
from backend.app.db.models.portfolio import Epc from backend.app.db.models.portfolio import Epc
from datatypes.enums import QuantityUnits from datatypes.enums import QuantityUnits
@ -152,8 +152,8 @@ class ScenarioModel(Base):
BigInteger, ForeignKey(Portfolio.id), nullable=False BigInteger, ForeignKey(Portfolio.id), nullable=False
) )
housing_type: Mapped[str] = mapped_column(String, nullable=False) housing_type: Mapped[str] = mapped_column(String, nullable=False)
goal: Mapped[str] = mapped_column(String, nullable=False) goal: Mapped[PortfolioGoal] = mapped_column(Enum(PortfolioGoal), nullable=False)
goal_value: Mapped[str] = mapped_column(String, nullable=False) goal_value: Mapped[Optional[str]] = mapped_column(String, nullable=False)
trigger_file_path: 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) already_installed_file_path: Mapped[Optional[str]] = mapped_column(String)
patches_file_path: Mapped[Optional[str]] = mapped_column(String) patches_file_path: Mapped[Optional[str]] = mapped_column(String)

View file

@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import replace from dataclasses import replace
from typing import Optional from typing import Optional
from backend.app.db.models.portfolio import PortfolioGoal
from backend.app.db.models.recommendations import PlanModel from backend.app.db.models.recommendations import PlanModel
from backend.app.domain.classes.scenario import Scenario from backend.app.domain.classes.scenario import Scenario
from backend.app.domain.records.plan_record import PlanRecord from backend.app.domain.records.plan_record import PlanRecord
@ -48,5 +49,14 @@ class Plan:
def is_compliant(self) -> bool: def is_compliant(self) -> bool:
raise NotImplementedError 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: def set_default(self, value: bool) -> None:
self.record = replace(self.record, is_default=value) self.record = replace(self.record, is_default=value)

View file

@ -9,7 +9,7 @@ from backend.app.domain.records.scenario_record import ScenarioRecord
class Scenario: class Scenario:
def __init__(self, record: ScenarioRecord, id: Optional[int] = None): def __init__(self, record: ScenarioRecord, id: Optional[int] = None):
self.id = id self.id = id
self._record = record self.record = record
@classmethod @classmethod
def from_sqlalchemy(cls, scenario_model: ScenarioModel) -> Scenario: def from_sqlalchemy(cls, scenario_model: ScenarioModel) -> Scenario:
@ -55,4 +55,4 @@ class Scenario:
return cls(record, scenario_model.id) return cls(record, scenario_model.id)
def set_default(self, value: bool) -> None: def set_default(self, value: bool) -> None:
self._record = replace(self._record, is_default=value) self.record = replace(self.record, is_default=value)

View file

@ -2,14 +2,15 @@ from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from backend.app.db.models.portfolio import PortfolioGoal
@dataclass(frozen=True) @dataclass(frozen=True)
class ScenarioRecord: class ScenarioRecord:
name: str name: str
created_at: datetime created_at: datetime
housing_type: str housing_type: str
goal: str goal: PortfolioGoal
goal_value: str
trigger_file_path: str trigger_file_path: str
multi_plan: bool multi_plan: bool
is_default: bool is_default: bool
@ -19,6 +20,7 @@ class ScenarioRecord:
non_invasive_recommendations_file_path: Optional[str] = None non_invasive_recommendations_file_path: Optional[str] = None
exclusions: Optional[str] = None exclusions: Optional[str] = None
goal_value: Optional[str] = None
cost: Optional[float] = None cost: Optional[float] = None
contingency: Optional[float] = None contingency: Optional[float] = None
funding: Optional[float] = None funding: Optional[float] = None

View file

@ -1,4 +1,4 @@
from typing import Callable from typing import Callable, Optional
import pytest import pytest
from datetime import datetime 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.classes.scenario import Scenario
from backend.app.domain.records.plan_record import PlanRecord from backend.app.domain.records.plan_record import PlanRecord
from backend.app.domain.records.scenario_record import ScenarioRecord 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 @pytest.fixture
@ -14,28 +14,17 @@ def created_at_datetime() -> datetime:
return datetime.now() 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 @pytest.fixture
def plan_factory( def plan_factory(
epc_c_scenario: "Scenario", created_at_datetime: datetime created_at_datetime: datetime,
) -> Callable[[int, "Epc"], "Plan"]: ) -> Callable[[int, "Epc", "Scenario"], "Plan"]:
# returns a function to create plans with different attributes """
def _create_plan(post_sap_points: int, post_epc_rating: "Epc") -> "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( plan_record = PlanRecord(
property_id=1, property_id=1,
portfolio_id=1, portfolio_id=1,
@ -44,27 +33,43 @@ def plan_factory(
post_sap_points=post_sap_points, post_sap_points=post_sap_points,
post_epc_rating=post_epc_rating, 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 return _create_plan
@pytest.mark.parametrize( @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), ("EPC C", "C", 75, Epc.C, True),
(100, Epc.A, True), ("EPC A", "A", 100, Epc.A, True),
(60, Epc.D, False), ("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( 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_sap_points: int,
post_epc_rating: "Epc", post_epc_rating: "Epc",
expected_compliance: bool, expected_compliance: bool,
) -> None: ) -> None:
# arrange # 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 # act
actual_compliance: bool = plan.is_compliant actual_compliance: bool = plan.is_compliant