"""Behaviour of the heating Recommendation Generator: detecting a dwelling whose heating system should be replaced and emitting one "Heating & Hot Water" Recommendation of competing whole-system bundles (ADR-0024). This slice covers the high-heat-retention storage (HHRSH) bundle; ASHP and boiler bundles land in later slices. Detection + pricing only; impact is produced by scoring (ADR-0016). """ from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.modelling.generators.heating_recommendation import recommend_heating from domain.modelling.product import Product from domain.modelling.recommendation import Recommendation from domain.modelling.simulation import HeatingOverlay from repositories.product.product_repository import ProductRepository from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import ( build_epc, ) # Electricity main-fuel code (Elmhurst → SAP10) and the Table 4a SAP code an # existing (non-HHR) electric storage system lodges. _ELECTRICITY = 30 _OLD_STORAGE_SAP_CODE = 402 def _electric_storage_baseline() -> EpcPropertyData: """A 000490 dwelling re-cast as an existing (non-HHR) electric storage system: electric main fuel, Table 4a code 402.""" epc: EpcPropertyData = build_epc() main = epc.sap_heating.main_heating_details[0] main.main_fuel_type = _ELECTRICITY main.sap_main_heating_code = _OLD_STORAGE_SAP_CODE return epc class _StubProducts(ProductRepository): """In-memory ProductRepository returning a fixed HHRSH product cost.""" def get(self, measure_type: str) -> Product: return Product( measure_type=measure_type, unit_cost_per_m2=3500.0, contingency_rate=0.26 ) def test_electric_storage_dwelling_yields_an_hhr_storage_bundle() -> None: # Arrange — an existing electric storage system (not HHR). baseline: EpcPropertyData = _electric_storage_baseline() # Act recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) # Assert — one "Heating & Hot Water" Recommendation carrying the HHRSH # bundle, whose overlay is the absolute HHR end-state. assert recommendation is not None assert recommendation.surface == "Heating & Hot Water" options = {o.measure_type: o for o in recommendation.options} assert "high_heat_retention_storage_heaters" in options assert options["high_heat_retention_storage_heaters"].overlay.heating == HeatingOverlay( main_fuel_type=30, sap_main_heating_code=409, main_heating_control=2404, water_heating_code=903, water_heating_fuel=30, cylinder_size=2, cylinder_insulation_type=1, cylinder_insulation_thickness_mm=120, cylinder_thermostat="Y", has_hot_water_cylinder=True, meter_type="Dual", ) def test_on_gas_boiler_dwelling_yields_no_hhr_storage_bundle() -> None: # Arrange — 000490 is a mains-gas combi (fuel 26, mains_gas True). HHR # storage suits off-gas / electric dwellings, not an on-gas gas boiler. baseline: EpcPropertyData = build_epc() # Act recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) # Assert — no HHRSH bundle (no other bundle is built in this slice → None). if recommendation is not None: assert "high_heat_retention_storage_heaters" not in { o.measure_type for o in recommendation.options } def test_already_hhr_storage_dwelling_yields_no_hhr_bundle() -> None: # Arrange — an electric dwelling already on HHR storage (Table 4a code 409) # must not be told to install HHR storage again (ASHP may still be offered as # a better end-state — it is a house here). baseline: EpcPropertyData = _electric_storage_baseline() baseline.sap_heating.main_heating_details[0].sap_main_heating_code = 409 # Act recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) # Assert — no HHRSH bundle. if recommendation is not None: assert "high_heat_retention_storage_heaters" not in { o.measure_type for o in recommendation.options } def test_existing_heat_pump_dwelling_yields_no_hhr_storage_bundle() -> None: # Arrange — an electric dwelling already on a heat pump (category 4) is never # downgraded to storage heaters. baseline: EpcPropertyData = _electric_storage_baseline() baseline.sap_heating.main_heating_details[0].main_heating_category = 4 # Act / Assert assert recommend_heating(baseline, _StubProducts()) is None def test_hhr_storage_bundle_carries_the_product_cost_and_contingency() -> None: # Arrange baseline: EpcPropertyData = _electric_storage_baseline() # Act recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) # Assert — the bundle's fully-loaded cost + contingency come from the product. assert recommendation is not None option = next( o for o in recommendation.options if o.measure_type == "high_heat_retention_storage_heaters" ) assert option.cost is not None assert abs(option.cost.total - 3500.0) <= 1e-9 assert abs(option.cost.contingency_rate - 0.26) <= 1e-9 def _gas_boiler_house() -> EpcPropertyData: """A 000490 mains-gas combi dwelling, explicitly a House — ASHP-eligible.""" epc: EpcPropertyData = build_epc() epc.property_type = "House" return epc def test_gas_boiler_house_yields_an_ashp_bundle() -> None: # Arrange — a mains-gas house; ASHP is offered for any non-flat house/ # bungalow regardless of current system or efficiency (ADR-0024). baseline: EpcPropertyData = _gas_boiler_house() # Act recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) # Assert — the ASHP bundle carries the absolute heat-pump end-state. assert recommendation is not None options = {o.measure_type: o for o in recommendation.options} assert "air_source_heat_pump" in options assert options["air_source_heat_pump"].overlay.heating == HeatingOverlay( main_fuel_type=30, main_heating_control=2210, main_heating_index_number=101413, main_heating_category=4, water_heating_code=901, water_heating_fuel=30, cylinder_size=4, cylinder_insulation_type=1, cylinder_insulation_thickness_mm=50, cylinder_thermostat="Y", has_hot_water_cylinder=True, meter_type="Single", mains_gas=False, ) def test_listed_building_yields_no_ashp_bundle() -> None: # Arrange — a listed building protects the fabric; an external ASHP unit is # not auto-offered (ADR-0024). The dwelling is on gas, so HHRSH is also out. baseline: EpcPropertyData = _gas_boiler_house() # Act recommendation: Recommendation | None = recommend_heating( baseline, _StubProducts(), PlanningRestrictions(is_listed=True) ) # Assert assert recommendation is None def test_conservation_area_still_yields_an_ashp_bundle() -> None: # Arrange — unlike glazing, a conservation area does NOT exclude ASHP; it is # offered with a planning caveat (ADR-0024, research-grounded). baseline: EpcPropertyData = _gas_boiler_house() # Act recommendation: Recommendation | None = recommend_heating( baseline, _StubProducts(), PlanningRestrictions(in_conservation_area=True) ) # Assert assert recommendation is not None assert "air_source_heat_pump" in {o.measure_type for o in recommendation.options} def test_flat_yields_no_ashp_bundle() -> None: # Arrange — flats are not auto-offered an individual ASHP (siting/lease/ # MCS-020 need a survey — ADR-0024). baseline: EpcPropertyData = _gas_boiler_house() baseline.property_type = "Flat" # Act recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) # Assert — no ASHP (and the gas flat is not HHRSH-eligible either → None). if recommendation is not None: assert "air_source_heat_pump" not in { o.measure_type for o in recommendation.options } def test_existing_heat_pump_yields_no_ashp_bundle() -> None: # Arrange — a dwelling already on a heat pump (category 4) is not re-offered # an ASHP. baseline: EpcPropertyData = _gas_boiler_house() baseline.sap_heating.main_heating_details[0].main_heating_category = 4 # Act recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) # Assert if recommendation is not None: assert "air_source_heat_pump" not in { o.measure_type for o in recommendation.options }