From 6f0dcc0455f013c0013fd6a56d5f48bec7c350ab Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 3 Jun 2026 22:54:15 +0000 Subject: [PATCH] test(modelling): characterise the portfolio aggregation over plan_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin the FE-facing aggregate_portfolio_recommendations (previously untested): it sums a Scenario's default Recommendations onto the Scenario row, joining Recommendation → Plan on recommendation.plan_id. Locks the m2m→plan_id read cut for the FE-critical path, now testable thanks to the full-parity ScenarioModel. Co-Authored-By: Claude Opus 4.8 --- .../tests/test_portfolio_functions.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 backend/app/db/functions/tests/test_portfolio_functions.py diff --git a/backend/app/db/functions/tests/test_portfolio_functions.py b/backend/app/db/functions/tests/test_portfolio_functions.py new file mode 100644 index 00000000..957f6663 --- /dev/null +++ b/backend/app/db/functions/tests/test_portfolio_functions.py @@ -0,0 +1,96 @@ +"""Characterisation of the FE-facing portfolio aggregation +(`aggregate_portfolio_recommendations`): it sums a Scenario's **default** +Recommendations and writes the totals onto the Scenario row. + +This pins the `recommendation.plan_id` linkage the m2m retirement introduced +(ADR-0017 amendment): the aggregation joins Recommendation → Plan on +`recommendation.plan_id`, so only measures carrying the right `plan_id` (and +`default = True`) are summed. +""" + +from __future__ import annotations + +from sqlmodel import Session + +from backend.app.db.functions.portfolio_functions import ( + aggregate_portfolio_recommendations, +) +from backend.app.db.models.recommendations import ( + PlanModel, + Recommendation, + ScenarioModel, +) +from domain.modelling.portfolio_goal import PortfolioGoal + + +def _rec( + *, plan_id: int, default: bool, cost: float, kwh: float, gbp: float, co2: float +) -> Recommendation: + return Recommendation( + property_id=10, + plan_id=plan_id, + type="cavity_wall_insulation", + measure_type="cavity_wall_insulation", + description="Cavity wall insulation", + estimated_cost=cost, + kwh_savings=kwh, + energy_cost_savings=gbp, + co2_equivalent_savings=co2, + total_work_hours=4.0, + default=default, + already_installed=False, + ) + + +def test_aggregation_sums_default_measures_linked_by_plan_id( + db_session: Session, +) -> None: + # Arrange — one Scenario + Plan, two default measures (summed) plus a + # non-default one (excluded), all linked by recommendation.plan_id. + db_session.add( + ScenarioModel( + id=7, + portfolio_id=1, + goal=PortfolioGoal.INCREASING_EPC, + goal_value="C", + is_default=True, + ) + ) + db_session.add( + PlanModel(id=100, portfolio_id=1, property_id=10, scenario_id=7, is_default=True) + ) + db_session.add_all( + [ + _rec(plan_id=100, default=True, cost=1000.0, kwh=500.0, gbp=120.0, co2=0.5), + _rec(plan_id=100, default=True, cost=500.0, kwh=300.0, gbp=80.0, co2=0.2), + # excluded: not default + _rec(plan_id=100, default=False, cost=9.0, kwh=9.0, gbp=9.0, co2=9.0), + ] + ) + db_session.commit() + + # Act + aggregate_portfolio_recommendations( + db_session, + portfolio_id=1, + scenario_id=7, + total_valuation_increase=2500.0, + labour_days=3.0, + aggregated_data={}, + ) + db_session.commit() + + # Assert — the default measures' sums land on the Scenario row + scenario = db_session.query(ScenarioModel).filter_by(id=7).one() + assert scenario.cost is not None + assert abs(scenario.cost - 1500.0) <= 1e-9 # 1000 + 500 + assert scenario.energy_savings is not None + assert abs(scenario.energy_savings - 800.0) <= 1e-9 # Σ kwh_savings + assert scenario.energy_cost_savings is not None + assert abs(scenario.energy_cost_savings - 200.0) <= 1e-9 # 120 + 80 + assert scenario.co2_equivalent_savings is not None + assert abs(scenario.co2_equivalent_savings - 0.7) <= 1e-9 # 0.5 + 0.2 + assert scenario.total_work_hours is not None + assert abs(scenario.total_work_hours - 8.0) <= 1e-9 # 4 + 4 + assert scenario.property_valuation_increase == 2500.0 + assert scenario.labour_days == 3.0