"""Behaviour of the roof Recommendation Generator: detecting an uninsulated loft and emitting a Recommendation whose Measure Option carries the loft- insulation Simulation Overlay and a priced Cost. Mirrors the wall generator. """ from datatypes.epc.domain.epc_property_data import ( BuildingPartIdentifier, EpcPropertyData, SapBuildingPart, SapRoomInRoof, ) from domain.modelling.scoring.overlay_applicator import apply_simulations from domain.modelling.product import Product from domain.modelling.recommendation import Recommendation from domain.modelling.generators.roof_recommendation import recommend_roof_insulation from repositories.product.product_repository import ProductRepository from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import ( build_epc, ) class _StubProducts(ProductRepository): def get(self, measure_type: str) -> Product: return Product( measure_type=measure_type, unit_cost_per_m2=30.0, contingency_rate=0.10 ) def _part(epc: EpcPropertyData, identifier: BuildingPartIdentifier) -> SapBuildingPart: return next(p for p in epc.sap_building_parts if p.identifier is identifier) def test_uninsulated_loft_yields_a_loft_insulation_recommendation() -> None: # Arrange baseline: EpcPropertyData = build_epc() _part(baseline, BuildingPartIdentifier.MAIN).roof_insulation_thickness = 0 # Act recommendation: Recommendation | None = recommend_roof_insulation( baseline, _StubProducts() ) # Assert assert recommendation is not None assert recommendation.surface == "Roof" assert len(recommendation.options) == 1 option = recommendation.options[0] assert option.measure_type == "loft_insulation" simulated: EpcPropertyData = apply_simulations(baseline, [option.overlay]) assert _part(simulated, BuildingPartIdentifier.MAIN).roof_insulation_thickness == 300 def test_already_insulated_loft_yields_no_recommendation() -> None: # Arrange baseline: EpcPropertyData = build_epc() # MAIN roof already 300 mm _part(baseline, BuildingPartIdentifier.MAIN).roof_insulation_thickness = 300 # Act recommendation: Recommendation | None = recommend_roof_insulation( baseline, _StubProducts() ) # Assert assert recommendation is None def test_room_in_roof_yields_no_recommendation_pending_a_dedicated_branch() -> None: # Arrange — an uninsulated loft the fallback would otherwise top up, but the # part is a room-in-roof. The simple loft/sloping overlay can't model RR # insulation (its sloping/stud/gable surfaces carry their own U-values via # Table 17/18), so the generator must defer rather than mis-fire loft. baseline: EpcPropertyData = build_epc() main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN) main.roof_insulation_thickness = 0 main.sap_room_in_roof = SapRoomInRoof(floor_area=9.0, construction_age_band="D") # Act recommendation: Recommendation | None = recommend_roof_insulation( baseline, _StubProducts() ) # Assert assert recommendation is None def test_loft_option_carries_cost_from_roof_area_and_product() -> None: # Arrange baseline: EpcPropertyData = build_epc() # MAIN roof area 14.85 m^2 _part(baseline, BuildingPartIdentifier.MAIN).roof_insulation_thickness = 0 # Act recommendation: Recommendation | None = recommend_roof_insulation( baseline, _StubProducts() ) # Assert assert recommendation is not None cost = recommendation.options[0].cost assert cost is not None assert abs(cost.total - 14.85 * 30.0) <= 0.01 assert abs(cost.contingency_rate - 0.10) <= 1e-9