Model/tests/repositories/plan/test_plan_postgres_repository.py
2026-06-23 15:01:34 +00:00

208 lines
7.7 KiB
Python

"""Behaviour of the Postgres-backed PlanRepository: persisting a Plan and its
Plan Measures to the live ``plan`` / ``recommendation`` tables (ADR-0017).
The Plan is the parent; each selected Plan Measure is a ``recommendation`` row
linked by the new ``plan_id`` FK. A re-run INSERTs a fresh Plan and keeps the
prior one as history (no cascade delete); when the new Plan is the default it
demotes prior default Plans for the (property, scenario) to ``is_default=False``
(ADR-0012). CO₂ is stored in tonnes (calculator kg ÷ 1000) to match the live
column contract.
"""
from __future__ import annotations
from sqlalchemy import Engine
from sqlmodel import Session, col, select
from datatypes.epc.domain.epc import Epc
from domain.modelling.measure_type import MeasureType
from domain.modelling.scoring.package_scorer import Score
from domain.modelling.plan import Plan, PlanMeasure
from domain.modelling.recommendation import Cost
from domain.modelling.scoring.scoring import MeasureImpact
from infrastructure.postgres.modelling import PlanModel, RecommendationModel
from repositories.plan.plan_postgres_repository import PlanPostgresRepository
def _plan() -> Plan:
measures: tuple[PlanMeasure, ...] = (
PlanMeasure(
measure_type=MeasureType.CAVITY_WALL_INSULATION,
description="Cavity wall insulation",
cost=Cost(total=1000.0, contingency_rate=0.10),
impact=MeasureImpact(
sap_points=8.0,
co2_savings_kg_per_yr=500.0,
energy_savings_kwh_per_yr=2000.0,
),
kwh_savings=1500.0,
energy_cost_savings=300.0,
),
)
return Plan(
measures=measures,
baseline=Score(
sap_continuous=40.0,
co2_kg_per_yr=4000.0,
primary_energy_kwh_per_yr=20000.0,
),
post_retrofit=Score(
sap_continuous=70.0,
co2_kg_per_yr=3500.0,
primary_energy_kwh_per_yr=18000.0,
),
)
def test_save_persists_plan_and_its_measures_with_tonnes_and_band(
db_engine: Engine,
) -> None:
# Act
with Session(db_engine) as session:
plan_id: int = PlanPostgresRepository(session).save(
_plan(), property_id=10, scenario_id=7, portfolio_id=1, is_default=True
)
session.commit()
# Assert
with Session(db_engine) as session:
plan_row = session.get(PlanModel, plan_id)
rec_rows = session.exec(
select(RecommendationModel).where(
col(RecommendationModel.plan_id) == plan_id
)
).all()
assert plan_row is not None
assert plan_row.property_id == 10
assert plan_row.scenario_id == 7
assert plan_row.portfolio_id == 1
assert plan_row.is_default is True
assert plan_row.post_sap_points is not None
assert plan_row.post_co2_emissions is not None
assert plan_row.co2_savings is not None
assert plan_row.cost_of_works is not None
assert plan_row.contingency_cost is not None
assert abs(plan_row.post_sap_points - 70.0) <= 1e-9
assert plan_row.post_epc_rating is Epc.C # SAP 70 → band C
assert abs(plan_row.post_co2_emissions - 3.5) <= 1e-9 # tonnes
assert abs(plan_row.co2_savings - 0.5) <= 1e-9 # (4000-3500)/1000
assert abs(plan_row.cost_of_works - 1000.0) <= 1e-9
assert abs(plan_row.contingency_cost - 100.0) <= 1e-9 # 1000 * 0.10
assert len(rec_rows) == 1
rec = rec_rows[0]
assert rec.estimated_cost is not None
assert rec.sap_points is not None
assert rec.co2_equivalent_savings is not None
assert rec.type == "cavity_wall_insulation"
assert rec.measure_type == "cavity_wall_insulation"
assert rec.description == "Cavity wall insulation"
assert abs(rec.estimated_cost - 1000.0) <= 1e-9
assert abs(rec.sap_points - 8.0) <= 1e-9
assert abs(rec.co2_equivalent_savings - 0.5) <= 1e-9 # tonnes
assert rec.kwh_savings is not None
assert rec.energy_cost_savings is not None
assert abs(rec.kwh_savings - 1500.0) <= 1e-9 # delivered kWh saved/yr
assert abs(rec.energy_cost_savings - 300.0) <= 1e-9 # £/yr saved
assert rec.default is True
assert rec.already_installed is False
def test_save_persists_null_per_measure_savings_when_unbilled(
db_engine: Engine,
) -> None:
# Arrange — a Plan Measure whose per-measure bills were never derived.
measure = PlanMeasure(
measure_type=MeasureType.LOFT_INSULATION,
description="Loft insulation",
cost=Cost(total=500.0, contingency_rate=0.20),
impact=MeasureImpact(
sap_points=3.0, co2_savings_kg_per_yr=200.0, energy_savings_kwh_per_yr=800.0
),
)
plan = Plan(
measures=(measure,),
baseline=Score(
sap_continuous=40.0, co2_kg_per_yr=4000.0, primary_energy_kwh_per_yr=20000.0
),
post_retrofit=Score(
sap_continuous=45.0, co2_kg_per_yr=3800.0, primary_energy_kwh_per_yr=19000.0
),
)
# Act
with Session(db_engine) as session:
plan_id: int = PlanPostgresRepository(session).save(
plan, property_id=11, scenario_id=7, portfolio_id=1, is_default=True
)
session.commit()
# Assert — the savings columns persist as NULL (ADR-0014 amendment)
with Session(db_engine) as session:
rec_rows = session.exec(
select(RecommendationModel).where(col(RecommendationModel.plan_id) == plan_id)
).all()
assert len(rec_rows) == 1
assert rec_rows[0].kwh_savings is None
assert rec_rows[0].energy_cost_savings is None
def test_rerun_keeps_history_and_demotes_the_prior_default_plan(
db_engine: Engine,
) -> None:
# Arrange — first (default) run
with Session(db_engine) as session:
first_id = PlanPostgresRepository(session).save(
_plan(), property_id=10, scenario_id=7, portfolio_id=1, is_default=True
)
session.commit()
# Act — re-run the same (property, scenario) as the default
with Session(db_engine) as session:
second_id = PlanPostgresRepository(session).save(
_plan(), property_id=10, scenario_id=7, portfolio_id=1, is_default=True
)
session.commit()
# Assert — the prior Plan is kept as history (no delete), and only the new
# Plan is the default; exactly one Plan for the pair stays is_default=True.
with Session(db_engine) as session:
plan_rows = session.exec(
select(PlanModel).where(col(PlanModel.property_id) == 10)
).all()
by_id = {p.id: p for p in plan_rows}
assert len(plan_rows) == 2
assert first_id != second_id
assert by_id[first_id].is_default is False # demoted
assert by_id[second_id].is_default is True # the current default
assert sum(1 for p in plan_rows if p.is_default) == 1
def test_rerun_as_non_default_does_not_demote_the_prior_default(
db_engine: Engine,
) -> None:
# Arrange — a default Plan exists
with Session(db_engine) as session:
first_id = PlanPostgresRepository(session).save(
_plan(), property_id=12, scenario_id=7, portfolio_id=1, is_default=True
)
session.commit()
# Act — re-run as NON-default (e.g. a non-default scenario); no demotion runs
with Session(db_engine) as session:
PlanPostgresRepository(session).save(
_plan(), property_id=12, scenario_id=7, portfolio_id=1, is_default=False
)
session.commit()
# Assert — the prior default is untouched (we only demote when saving a default)
with Session(db_engine) as session:
plan_rows = session.exec(
select(PlanModel).where(col(PlanModel.property_id) == 12)
).all()
by_id = {p.id: p for p in plan_rows}
assert len(plan_rows) == 2
assert by_id[first_id].is_default is True