mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
226 lines
7.5 KiB
Python
226 lines
7.5 KiB
Python
"""The Scenario Export sheet shape (ADR-0065) — pure layout of one scenario
|
|
sheet: measure costs pivoted onto a frozen column contract, savings summed per
|
|
Property, and the Plan's post-works figures."""
|
|
|
|
from typing import Optional, Sequence
|
|
|
|
import pytest
|
|
|
|
from domain.scenario_export.scenario_sheet import (
|
|
MEASURE_COLUMNS,
|
|
ExportMeasure,
|
|
PropertyScenarioData,
|
|
shape_scenario_sheet,
|
|
)
|
|
|
|
|
|
def _measure(
|
|
*,
|
|
measure_type: str = "loft_insulation",
|
|
estimated_cost: Optional[float] = 1000.0,
|
|
sap_points: Optional[float] = None,
|
|
co2_equivalent_savings: Optional[float] = None,
|
|
kwh_savings: Optional[float] = None,
|
|
energy_cost_savings: Optional[float] = None,
|
|
includes_battery: bool = False,
|
|
) -> ExportMeasure:
|
|
return ExportMeasure(
|
|
measure_type=measure_type,
|
|
estimated_cost=estimated_cost,
|
|
sap_points=sap_points,
|
|
co2_equivalent_savings=co2_equivalent_savings,
|
|
kwh_savings=kwh_savings,
|
|
energy_cost_savings=energy_cost_savings,
|
|
includes_battery=includes_battery,
|
|
)
|
|
|
|
|
|
def _property(
|
|
*,
|
|
property_id: int = 1,
|
|
landlord_property_id: Optional[str] = "LP1",
|
|
measures: Sequence[ExportMeasure] = (),
|
|
**fields: object,
|
|
) -> PropertyScenarioData:
|
|
return PropertyScenarioData(
|
|
property_id=property_id,
|
|
landlord_property_id=landlord_property_id,
|
|
measures=measures,
|
|
**fields, # pyright: ignore[reportArgumentType]
|
|
)
|
|
|
|
|
|
def test_a_measures_cost_lands_in_its_own_column_with_the_property_identity() -> None:
|
|
# arrange — one Property with one loft-insulation measure priced at £1200.
|
|
prop = _property(
|
|
property_id=42,
|
|
landlord_property_id="LP42",
|
|
measures=[_measure(measure_type="loft_insulation", estimated_cost=1200.0)],
|
|
)
|
|
|
|
# act
|
|
sheet = shape_scenario_sheet([prop])
|
|
|
|
# assert — one row carrying the Property identity and the cost in the
|
|
# loft_insulation column.
|
|
assert len(sheet.rows) == 1
|
|
row = sheet.rows[0]
|
|
assert row["property_id"] == 42
|
|
assert row["landlord_property_id"] == "LP42"
|
|
assert row["loft_insulation"] == 1200.0
|
|
|
|
|
|
def test_every_measure_column_is_present_and_blank_where_the_property_lacks_it() -> None:
|
|
# arrange — a Property with a single measure. Every other measure column
|
|
# must still appear on the sheet (the frozen contract, ADR-0065), blank for
|
|
# this Property, so all scenario sheets share one column set.
|
|
prop = _property(
|
|
measures=[_measure(measure_type="cavity_wall_insulation", estimated_cost=800.0)]
|
|
)
|
|
|
|
# act
|
|
sheet = shape_scenario_sheet([prop])
|
|
|
|
# assert — the full frozen measure contract is present; the one selected
|
|
# measure carries its cost, every other measure column is blank.
|
|
assert set(MEASURE_COLUMNS).issubset(set(sheet.columns))
|
|
row = sheet.rows[0]
|
|
assert row["cavity_wall_insulation"] == 800.0
|
|
assert row["loft_insulation"] == ""
|
|
assert row["solar_pv"] == ""
|
|
|
|
|
|
def test_savings_and_sap_points_are_summed_across_a_propertys_measures() -> None:
|
|
# arrange — two measures, each pivoting to its own cost column and each
|
|
# contributing SAP points and carbon / energy / bill savings.
|
|
prop = _property(
|
|
measures=[
|
|
_measure(
|
|
measure_type="loft_insulation",
|
|
estimated_cost=1000.0,
|
|
sap_points=3.0,
|
|
co2_equivalent_savings=0.4,
|
|
kwh_savings=900.0,
|
|
energy_cost_savings=120.0,
|
|
),
|
|
_measure(
|
|
measure_type="solar_pv",
|
|
estimated_cost=5000.0,
|
|
sap_points=5.0,
|
|
co2_equivalent_savings=1.1,
|
|
kwh_savings=2500.0,
|
|
energy_cost_savings=300.0,
|
|
),
|
|
]
|
|
)
|
|
|
|
# act
|
|
sheet = shape_scenario_sheet([prop])
|
|
|
|
# assert — each cost pivots to its own column, and the savings roll up to the
|
|
# Property's totals.
|
|
row = sheet.rows[0]
|
|
assert row["loft_insulation"] == 1000.0
|
|
assert row["solar_pv"] == 5000.0
|
|
assert row["sap_points"] == 8.0
|
|
assert row["co2_equivalent_savings"] == pytest.approx(1.5)
|
|
assert row["kwh_savings"] == 3400.0
|
|
assert row["energy_cost_savings"] == 420.0
|
|
|
|
|
|
def test_total_retrofit_cost_sums_the_per_measure_costs() -> None:
|
|
# arrange — a Property with two priced measures.
|
|
prop = _property(
|
|
measures=[
|
|
_measure(measure_type="loft_insulation", estimated_cost=1200.0),
|
|
_measure(measure_type="cavity_wall_insulation", estimated_cost=800.0),
|
|
]
|
|
)
|
|
|
|
# act
|
|
sheet = shape_scenario_sheet([prop])
|
|
|
|
# assert — the package total is the sum of the measure costs.
|
|
assert sheet.rows[0]["total_retrofit_cost"] == 2000.0
|
|
|
|
|
|
def test_solar_pv_with_a_battery_pivots_to_its_own_battery_column() -> None:
|
|
# arrange — a solar_pv Option that includes a battery.
|
|
prop = _property(
|
|
measures=[
|
|
_measure(
|
|
measure_type="solar_pv", estimated_cost=6000.0, includes_battery=True
|
|
)
|
|
]
|
|
)
|
|
|
|
# act
|
|
sheet = shape_scenario_sheet([prop])
|
|
|
|
# assert — the cost lands in the battery variant column (a stable part of the
|
|
# contract), the plain solar_pv column stays blank, and the total still
|
|
# counts it.
|
|
assert "solar_pv_with_battery" in sheet.columns
|
|
row = sheet.rows[0]
|
|
assert row["solar_pv_with_battery"] == 6000.0
|
|
assert row["solar_pv"] == ""
|
|
assert row["total_retrofit_cost"] == 6000.0
|
|
|
|
|
|
def test_plan_post_works_figures_come_from_the_plan_row_blank_when_absent() -> None:
|
|
# arrange — a Property whose default Plan carries the SAP calculator's
|
|
# post-works figures; the contingency figure happens to be absent.
|
|
prop = _property(
|
|
post_sap_points=78.0,
|
|
post_epc_rating="C",
|
|
cost_of_works=4200.0,
|
|
co2_savings=1.8,
|
|
energy_bill_savings=350.0,
|
|
energy_consumption_savings=4100.0,
|
|
valuation_increase=15000.0,
|
|
)
|
|
|
|
# act
|
|
sheet = shape_scenario_sheet([prop])
|
|
|
|
# assert — the persisted Plan figures pass through directly (no recompute,
|
|
# ADR-0065); an absent figure is blank rather than zero.
|
|
row = sheet.rows[0]
|
|
assert row["post_sap_points"] == 78.0
|
|
assert row["post_epc_rating"] == "C"
|
|
assert row["cost_of_works"] == 4200.0
|
|
assert row["valuation_increase"] == 15000.0
|
|
assert row["contingency_cost"] == ""
|
|
|
|
|
|
def test_identity_and_effective_epc_descriptive_fields_pass_through() -> None:
|
|
# arrange — a Property with identity and (already override-resolved)
|
|
# Effective-EPC descriptive fields; the floor description is absent.
|
|
prop = _property(
|
|
property_id=7,
|
|
landlord_property_id="LP7",
|
|
uprn="100023",
|
|
address="12 Oak Street",
|
|
postcode="AB1 2CD",
|
|
property_type="House",
|
|
walls="Cavity wall, as built, insulated",
|
|
roof="Pitched, 250mm loft insulation",
|
|
current_epc_rating="D",
|
|
current_sap_points=58.0,
|
|
)
|
|
|
|
# act
|
|
sheet = shape_scenario_sheet([prop])
|
|
|
|
# assert — identity and descriptive fields appear as supplied; an unsupplied
|
|
# descriptive field is blank.
|
|
row = sheet.rows[0]
|
|
assert row["uprn"] == "100023"
|
|
assert row["address"] == "12 Oak Street"
|
|
assert row["postcode"] == "AB1 2CD"
|
|
assert row["property_type"] == "House"
|
|
assert row["walls"] == "Cavity wall, as built, insulated"
|
|
assert row["roof"] == "Pitched, 250mm loft insulation"
|
|
assert row["current_epc_rating"] == "D"
|
|
assert row["current_sap_points"] == 58.0
|
|
assert row["floor"] == ""
|