"""Behaviour of the Plan / PlanMeasure domain types: the per-Property output of one Scenario's modelling run. A Plan carries its selected Plan Measures (the Optimised Package) plus the baseline/post-retrofit Scores, and derives the persisted headline figures (cost aggregates, CO₂ saving, post-retrofit band). Single-phase, flat post-retrofit figures (ADR-0005 / ADR-0017). """ from __future__ import annotations from datatypes.epc.domain.epc import Epc from domain.billing.bill import Bill, BillSection, BillSectionCost 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 def _bill(*, heating_kwh: float, total_gbp: float) -> Bill: return Bill( sections={ BillSection.HEATING: BillSectionCost(kwh=heating_kwh, cost_gbp=total_gbp) }, standing_charges_gbp=0.0, seg_credit_gbp=0.0, total_gbp=total_gbp, ) def _measure(measure_type: str, total: float, rate: float) -> PlanMeasure: return PlanMeasure( measure_type=measure_type, description=measure_type.replace("_", " "), cost=Cost(total=total, contingency_rate=rate), impact=MeasureImpact( sap_points=1.0, co2_savings_kg_per_yr=1.0, energy_savings_kwh_per_yr=1.0 ), ) def test_plan_aggregates_cost_and_savings_and_bands_the_post_sap() -> None: # Arrange measures: tuple[PlanMeasure, ...] = ( _measure("cavity_wall_insulation", total=1000.0, rate=0.10), _measure("loft_insulation", total=500.0, rate=0.20), ) baseline = Score( sap_continuous=40.0, co2_kg_per_yr=4000.0, primary_energy_kwh_per_yr=20000.0 ) post = Score( sap_continuous=70.4, co2_kg_per_yr=3600.0, primary_energy_kwh_per_yr=18000.0 ) plan = Plan(measures=measures, baseline=baseline, post_retrofit=post) # Act / Assert assert abs(plan.cost_of_works - 1500.0) <= 1e-9 assert abs(plan.contingency_cost - 200.0) <= 1e-9 # 1000*0.10 + 500*0.20 assert abs(plan.co2_savings_kg_per_yr - 400.0) <= 1e-9 # baseline - post assert abs(plan.post_sap_continuous - 70.4) <= 1e-9 assert plan.post_epc_rating is Epc.C # round(70.4) = 70 → band C (69–80) def test_plan_derives_post_bill_and_savings_from_the_baseline_and_post_bills() -> None: # Arrange — a Plan whose baseline and post-package Bills have been derived. baseline = Score( sap_continuous=40.0, co2_kg_per_yr=4000.0, primary_energy_kwh_per_yr=20000.0 ) post = Score( sap_continuous=70.0, co2_kg_per_yr=3600.0, primary_energy_kwh_per_yr=18000.0 ) plan = Plan( measures=(), baseline=baseline, post_retrofit=post, baseline_bill=_bill(heating_kwh=10000.0, total_gbp=2000.0), post_bill=_bill(heating_kwh=6000.0, total_gbp=1400.0), ) # Act / Assert — plan-level energy/bill figures (ADR-0014 amendment). assert plan.post_energy_bill == 1400.0 assert plan.energy_bill_savings == 600.0 # 2000 − 1400 assert plan.post_energy_consumption == 6000.0 # Σ post section kWh assert plan.energy_consumption_savings == 4000.0 # 10000 − 6000 def test_plan_energy_figures_are_none_without_bills() -> None: # Arrange — a Plan with no bills derived (the figures persist as NULL). score = Score( sap_continuous=55.0, co2_kg_per_yr=3000.0, primary_energy_kwh_per_yr=15000.0 ) plan = Plan(measures=(), baseline=score, post_retrofit=score) # Act / Assert assert plan.post_energy_bill is None assert plan.energy_bill_savings is None assert plan.post_energy_consumption is None assert plan.energy_consumption_savings is None