From 9860f06864fdb0487fa6aefa5d01c732b4cf2b13 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 6 Jun 2026 20:42:14 +0000 Subject: [PATCH] feat(modelling): ASHP decommission fallbacks for off-sheet systems Slice 4 of ADR-0025 costing. ASHP is offered to any house regardless of fuel, so _decommission now prices a fallback instead of raising: no system -> 0, electric room/panel heaters -> electric-storage line, anything else -> gas line (representative default). Never blocks ASHP eligibility. Co-Authored-By: Claude Opus 4.8 --- domain/modelling/products.py | 10 ++++++++- tests/domain/modelling/test_products.py | 27 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/domain/modelling/products.py b/domain/modelling/products.py index 132e3896..e60f986a 100644 --- a/domain/modelling/products.py +++ b/domain/modelling/products.py @@ -136,7 +136,15 @@ class Products: return _DECOMMISSION_OIL if inputs.existing_system is AshpExistingSystem.LPG: return _DECOMMISSION_LPG - raise ValueError(f"no decommission rate for {inputs.existing_system}") + # Systems off the rate sheet: ASHP is still offered (ADR-0025), so price + # a fallback rather than raise. Nothing to remove for no system; electric + # room/panel heaters are comparable work to storage heaters; anything + # else takes the gas wet-system line as a representative default. + if inputs.existing_system is AshpExistingSystem.NONE: + return 0.0 + if inputs.existing_system is AshpExistingSystem.ELECTRIC_OTHER: + return _DECOMMISSION_ELECTRIC_STORAGE_SMALL if inputs.is_small_property else _DECOMMISSION_ELECTRIC_STORAGE_LARGE + return _DECOMMISSION_GAS def _distribution(self, inputs: AshpCostInputs) -> float: radiators: int = max(_MIN_RADIATORS, min(_MAX_RADIATORS, inputs.radiator_count)) diff --git a/tests/domain/modelling/test_products.py b/tests/domain/modelling/test_products.py index 6db7c458..e4953a3e 100644 --- a/tests/domain/modelling/test_products.py +++ b/tests/domain/modelling/test_products.py @@ -84,3 +84,30 @@ def test_reusable_wet_system_prices_a_flush_plus_half_the_distribution() -> None # Assert — decommission 720 + pump 9840 + cylinder 2382.60 + distribution # (flush 168 + 0.5 x 4152 = 2244) = 15186.60. assert abs(cost.total - 15186.60) <= 1e-9 + + +def _small_no_reuse(system: AshpExistingSystem) -> AshpCostInputs: + """A small dwelling, 4 kW band, 7 radiators, no reusable wet system — pump + 9720 + cylinder 2382.60 + distribution (7) 3618 = 15720.60 common base.""" + return AshpCostInputs( + existing_system=system, + is_small_property=True, + design_heat_loss_kw=4.0, + radiator_count=7, + has_reusable_wet_system=False, + ) + + +def test_decommission_falls_back_for_systems_not_on_the_rate_sheet() -> None: + # Arrange — the rate sheet covers gas/oil/LPG/electric-storage, but ASHP is + # offered to any house regardless of fuel (ADR-0025): no system costs nothing + # to remove; electric room/panel heaters use the electric-storage line; any + # other system defaults to the gas line — never a raise (that would wrongly + # block ASHP eligibility). + products = Products() + base = 15720.60 + + # Act / Assert + assert abs(products.ashp_bundle_cost(_small_no_reuse(AshpExistingSystem.NONE)).total - (base + 0.0)) <= 1e-9 + assert abs(products.ashp_bundle_cost(_small_no_reuse(AshpExistingSystem.ELECTRIC_OTHER)).total - (base + 570.0)) <= 1e-9 + assert abs(products.ashp_bundle_cost(_small_no_reuse(AshpExistingSystem.OTHER)).total - (base + 720.0)) <= 1e-9