diff --git a/tests/domain/modelling/test_elmhurst_cascade_pins.py b/tests/domain/modelling/test_elmhurst_cascade_pins.py index c89c2aa5..0e6832b7 100644 --- a/tests/domain/modelling/test_elmhurst_cascade_pins.py +++ b/tests/domain/modelling/test_elmhurst_cascade_pins.py @@ -14,6 +14,7 @@ is a named generator/overlay/calculator gap to fix, never a tolerance to widen from __future__ import annotations +import copy from dataclasses import replace from typing import Final @@ -46,6 +47,9 @@ from domain.modelling.generators.solid_wall_recommendation import ( from domain.modelling.generators.glazing_recommendation import recommend_glazing from domain.modelling.generators.lighting_recommendation import recommend_lighting from domain.modelling.generators.heating_recommendation import recommend_heating +from domain.modelling.generators.secondary_heating_recommendation import ( + recommend_secondary_heating_removal, +) from domain.modelling.scoring.overlay_applicator import apply_simulations from domain.modelling.recommendation import MeasureOption from domain.sap10_calculator.calculator import Sap10Calculator, SapResult @@ -53,6 +57,17 @@ from repositories.product.product_repository import ProductRepository from tests.domain.modelling._elmhurst_recommendation import ( parse_recommendation_summary, ) +from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_001431 import ( + build_epc as build_001431_epc, +) + +# RdSAP §A.2.2 forces a secondary system for electric-storage mains; SAP code +# 402 (slimline storage) is in that set. Code 104 (a gas combi boiler) is not. +_ELECTRIC_STORAGE_MAIN_CODE: Final[int] = 402 +_STANDARD_ELECTRICITY_FUEL: Final[int] = 30 +# SAP 10.2 Table 4a code 691 — electric panel/convector/radiant heaters, the +# fixed secondary the user's example cert lodges. +_SECONDARY_ELECTRIC_PANEL_CODE: Final[int] = 691 # Pin tolerance: the Summary PDFs are deterministic test vectors, so the # overlay must reproduce the re-lodged cert exactly. Matches the worksheet @@ -1122,3 +1137,58 @@ def test_solar_before_baselines() -> None: # Act / Assert — currently raises MissingMainFuelType. Sap10Calculator().calculate(before) + + +# --- Secondary Heating Removal (ADR-0028) ---------------------------------- +# The user's Elmhurst before/after Summary for this measure (cert 001431, +# electric-storage main + secondary 691) cannot be parsed — that PDF export +# trips the documented 001431 Summary window-extraction bug. So these pins use +# the worksheet-pinned `build_epc()` (a validated real-001431 representation, +# the repo's sanctioned 001431 baseline) with the secondary configuration set on +# it, exercising the real generator → overlay → calculator cascade. + + +def test_secondary_removal_on_an_electric_storage_main_is_a_no_op() -> None: + # Arrange — 001431 recast to an electric-storage main (SAP code 402, fuel 30) + # with a lodged secondary (691). RdSAP §A.2.2 forces a default secondary back + # on storage mains, so removal reproduces the after at delta 0 — exactly why + # the user's before/after Summaries both print SAP F35. + before: EpcPropertyData = build_001431_epc() + main = before.sap_heating.main_heating_details[0] + main.sap_main_heating_code = _ELECTRIC_STORAGE_MAIN_CODE + main.main_fuel_type = _STANDARD_ELECTRICITY_FUEL + main.main_heating_index_number = None + before.sap_heating.secondary_heating_type = _SECONDARY_ELECTRIC_PANEL_CODE + after: EpcPropertyData = copy.deepcopy(before) + after.sap_heating.secondary_heating_type = None + after.sap_heating.secondary_fuel_type = None + recommendation: Recommendation | None = recommend_secondary_heating_removal( + before, _AnyProduct() + ) + assert recommendation is not None + + # Act / Assert — the overlay reproduces the secondary-removed cert at delta 0. + _assert_overlay_reproduces_after( + before, after, recommendation.options[0].overlay + ) + + +def test_secondary_removal_on_a_non_forced_main_raises_sap() -> None: + # Arrange — 001431's lodged gas combi (SAP code 104, NOT a forced-secondary + # main) with an added electric secondary (691). Removing it reallocates the + # Table 11 secondary fraction to the cheaper gas main, so cost-based SAP rises + # (the value path the forced-secondary example can't exercise). + before: EpcPropertyData = build_001431_epc() + before.sap_heating.secondary_heating_type = _SECONDARY_ELECTRIC_PANEL_CODE + recommendation: Recommendation | None = recommend_secondary_heating_removal( + before, _AnyProduct() + ) + assert recommendation is not None + scorer = PackageScorer(Sap10Calculator()) + + # Act + with_secondary: Score = scorer.score(before, []) + removed: Score = scorer.score(before, [recommendation.options[0].overlay]) + + # Assert — removal strictly raises SAP (delta well above the pin tolerance). + assert removed.sap_continuous - with_secondary.sap_continuous > _PIN_ABS