"""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 replaces (delete the Plan for the (property, scenario) → cascade its recommendations → insert fresh), so the batch write is idempotent (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_save_is_idempotent_on_rerun_for_the_same_property_and_scenario( db_engine: Engine, ) -> None: # Arrange — first run with Session(db_engine) as session: 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) with Session(db_engine) as session: PlanPostgresRepository(session).save( _plan(), property_id=10, scenario_id=7, portfolio_id=1, is_default=True ) session.commit() # Assert — replaced, not duplicated (cascade removed the old measures) with Session(db_engine) as session: plan_rows = session.exec( select(PlanModel).where(col(PlanModel.property_id) == 10) ).all() rec_rows = session.exec( select(RecommendationModel).where(col(RecommendationModel.property_id) == 10) ).all() assert len(plan_rows) == 1 assert len(rec_rows) == 1