Model/tests/domain/modelling/test_products.py
Khalim Conn-Kowlessar cf7c2e017d feat(modelling): ASHP decommission cost by existing system
Slice 2 of ADR-0025 costing. _decommission maps the existing system to its
Southern Housing line: gas/oil flat 720, LPG 960 (tank+fuel removal),
electric-storage 570/840 by property-size band. Unmapped systems raise for
now -- the no-system/electric-other/other fallbacks land in the next slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 20:36:12 +00:00

65 lines
2.8 KiB
Python

"""Behaviour of `Products.ashp_bundle_cost` — the composite, per-dwelling ASHP
bundle cost (ADR-0025). Pure catalogue math: given a typed `AshpCostInputs` it
selects and sums the applicable Southern Housing rate lines (decommission +
heat pump + cylinder + distribution) into a `Cost`, carrying the separate ASHP
contingency. No EpcPropertyData / calculator — the dwelling interpretation that
produces the inputs lives in the modelling layer.
Costs are pinned against the real Southern Housing Group rate sheet, so the
totals are exact (delta <= 1e-9), mirroring the cascade-pin philosophy.
"""
from domain.modelling.products import (
AshpCostInputs,
AshpExistingSystem,
Products,
)
from domain.modelling.recommendation import Cost
def test_ashp_bundle_cost_composes_an_electric_storage_full_distribution_dwelling() -> None:
# Arrange — a small electric-storage dwelling: no reusable wet system, so a
# full new wet distribution is priced. 4 kW design heat loss (smallest pump
# band), 7 radiators.
products = Products()
inputs = AshpCostInputs(
existing_system=AshpExistingSystem.ELECTRIC_STORAGE,
is_small_property=True,
design_heat_loss_kw=4.0,
radiator_count=7,
has_reusable_wet_system=False,
)
# Act
cost: Cost = products.ashp_bundle_cost(inputs)
# Assert — decommission 570 + pump 9720 + cylinder 2382.60 + distribution
# (7 rads) 3618 = 16290.60, with the separate 25% ASHP contingency.
assert abs(cost.total - 16290.60) <= 1e-9
assert abs(cost.contingency_rate - 0.25) <= 1e-9
def _large_no_reuse(system: AshpExistingSystem) -> AshpCostInputs:
"""A large dwelling, 8 kW band, 8 radiators, no reusable wet system — so the
only thing varying with ``system`` is the decommission line."""
return AshpCostInputs(
existing_system=system,
is_small_property=False,
design_heat_loss_kw=8.0,
radiator_count=8,
has_reusable_wet_system=False,
)
def test_decommission_cost_varies_by_existing_system() -> None:
# Arrange — common: pump (8 kW) 9840 + cylinder 2382.60 + distribution (8
# rads) 4152 = 16374.60; only decommission differs by system.
products = Products()
common = 16374.60
# Act / Assert — gas and oil are flat 720; LPG 960; electric-storage large
# 840 (small 570 is pinned by the tracer above).
assert abs(products.ashp_bundle_cost(_large_no_reuse(AshpExistingSystem.GAS)).total - (common + 720.0)) <= 1e-9
assert abs(products.ashp_bundle_cost(_large_no_reuse(AshpExistingSystem.OIL)).total - (common + 720.0)) <= 1e-9
assert abs(products.ashp_bundle_cost(_large_no_reuse(AshpExistingSystem.LPG)).total - (common + 960.0)) <= 1e-9
assert abs(products.ashp_bundle_cost(_large_no_reuse(AshpExistingSystem.ELECTRIC_STORAGE)).total - (common + 840.0)) <= 1e-9