test(modelling): characterise the portfolio aggregation over plan_id

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 22:54:15 +00:00
parent c18968ba3c
commit 6f0dcc0455

View file

@ -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