Pivot a Property's measure cost onto its scenario-sheet column 🟥

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-13 23:08:26 +00:00
parent 01e8b8c289
commit 1a0e87e593
4 changed files with 178 additions and 0 deletions

View file

View file

@ -0,0 +1,111 @@
"""The Scenario Export sheet shape (ADR-0065).
Pure logic: given the persisted, override-resolved data for the Properties in one
Scenario, lay out the wide export sheet property identity + Effective-EPC
descriptive fields, each measure's cost pivoted onto its own column against a
frozen column contract, savings summed per Property, and the Plan's post-works
figures. No I/O: the repository reads the rows and the infrastructure layer
renders the workbook; this module only shapes one sheet's rows.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Optional, Sequence
# The measure cost columns always present on every sheet, in a stable order, so
# every scenario sheet in the workbook shares one column contract regardless of
# which measures a given selection happens to contain (ADR-0065). A Property that
# lacks a measure carries a blank in that column, never a missing column.
MEASURE_COLUMNS: tuple[str, ...] = (
"suspended_floor_insulation",
"solid_floor_insulation",
"external_wall_insulation",
"internal_wall_insulation",
"cavity_wall_insulation",
"loft_insulation",
"flat_roof_insulation",
"room_roof_insulation",
"secondary_glazing",
"double_glazing",
"solar_pv",
"high_heat_retention_storage_heaters",
"air_source_heat_pump",
"boiler_upgrade",
"gas_boiler_upgrade",
"roomstat_programmer_trvs",
"time_temperature_zone_control",
"low_energy_lighting",
"mechanical_ventilation",
"system_tune_up",
"system_tune_up_zoned",
)
@dataclass(frozen=True)
class ExportMeasure:
"""One selected measure on a Property's default Plan for the Scenario: the
measure type (the pivot column), its installed cost, and the SAP / carbon /
energy / bill savings attributed to it. ``includes_battery`` distinguishes a
solar-with-battery Option so it pivots to its own column (ADR-0065)."""
measure_type: str
estimated_cost: Optional[float] = None
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
@dataclass(frozen=True)
class PropertyScenarioData:
"""One Property's persisted data for one Scenario: identity, the
Effective-EPC descriptive fields (already override-resolved by the
repository), the default Plan's post-works figures, and the selected
measures. The input row the shaper turns into one wide sheet row."""
property_id: int
landlord_property_id: Optional[str] = None
uprn: Optional[str] = None
address: Optional[str] = None
postcode: Optional[str] = None
property_type: Optional[str] = None
walls: Optional[str] = None
roof: Optional[str] = None
floor: Optional[str] = None
windows: Optional[str] = None
heating: Optional[str] = None
hot_water: Optional[str] = None
lighting: Optional[str] = None
total_floor_area: Optional[float] = None
number_of_rooms: Optional[int] = None
lodgement_date: Optional[str] = None
is_expired: Optional[bool] = None
current_epc_rating: Optional[str] = None
current_sap_points: Optional[float] = None
post_sap_points: Optional[float] = None
post_epc_rating: Optional[str] = None
cost_of_works: Optional[float] = None
contingency_cost: Optional[float] = None
co2_savings: Optional[float] = None
energy_bill_savings: Optional[float] = None
energy_consumption_savings: Optional[float] = None
valuation_increase: Optional[float] = None
measures: Sequence[ExportMeasure] = ()
@dataclass(frozen=True)
class ExportSheet:
"""One laid-out scenario sheet: the ordered column contract and the rows,
each a column-keyed mapping ready for the workbook renderer."""
columns: tuple[str, ...]
rows: tuple[dict[str, Any], ...]
def shape_scenario_sheet(
properties: Sequence[PropertyScenarioData],
) -> ExportSheet:
"""Shape one Scenario's Properties into a wide export sheet (ADR-0065)."""
raise NotImplementedError

View file

View file

@ -0,0 +1,67 @@
"""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
from domain.scenario_export.scenario_sheet import (
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