Model/tests/domain/modelling/test_wall_recommendation.py
Khalim Conn-Kowlessar bb2c0068ff feat(modelling): price the cavity Option from area x Product — closes #1155
recommend_cavity_wall now takes a ProductRepository and prices the Measure
Option: Cost(total = gross_heat_loss_wall_area(MAIN) x product.unit_cost_per_m2,
contingency_rate = product.contingency_rate). Detection is unchanged and runs
before pricing, so ineligible walls still return None without a catalogue hit.

Completes #1155 — the cavity-wall Recommendation Generator now detects an
uninsulated main cavity wall and emits a priced Option carrying the filled-
cavity overlay. Four behaviour tests (detection x3 + fully-loaded cost).
pyright strict clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:35:52 +00:00

89 lines
3.2 KiB
Python

"""Behaviour of the wall Recommendation Generator: detecting a treatable
wall and emitting a Recommendation whose Measure Option carries the
Simulation Overlay for the intervention. See CONTEXT.md / ADR-0016.
"""
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
SapBuildingPart,
)
from domain.modelling.overlay_applicator import apply_simulations
from domain.modelling.product import Product
from domain.modelling.recommendation import Recommendation
from domain.modelling.wall_recommendation import recommend_cavity_wall
from repositories.product.product_repository import ProductRepository
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
build_epc,
)
class _StubProducts(ProductRepository):
"""In-memory ProductRepository returning a fixed cavity Product."""
def get(self, measure_type: str) -> Product:
return Product(
measure_type=measure_type, unit_cost_per_m2=18.5, 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_main_cavity_wall_yields_a_cavity_fill_recommendation() -> None:
# Arrange
baseline: EpcPropertyData = build_epc() # MAIN: cavity (4), uninsulated (4)
# Act
recommendation: Recommendation | None = recommend_cavity_wall(baseline, _StubProducts())
# Assert
assert recommendation is not None
assert recommendation.surface == "Main wall"
assert len(recommendation.options) == 1
option = recommendation.options[0]
assert option.measure_type == "cavity_wall_insulation"
simulated: EpcPropertyData = apply_simulations(baseline, [option.overlay])
assert _part(simulated, BuildingPartIdentifier.MAIN).wall_insulation_type == 2
def test_already_insulated_main_wall_yields_no_recommendation() -> None:
# Arrange
baseline: EpcPropertyData = build_epc()
_part(baseline, BuildingPartIdentifier.MAIN).wall_insulation_type = 2 # filled
# Act
recommendation: Recommendation | None = recommend_cavity_wall(baseline, _StubProducts())
# Assert
assert recommendation is None
def test_cavity_option_carries_fully_loaded_cost_from_area_and_product() -> None:
# Arrange
baseline: EpcPropertyData = build_epc() # MAIN gross heat-loss area 45.93 m^2
products = _StubProducts() # cavity 18.5 GBP/m^2, contingency 0.10
# Act
recommendation: Recommendation | None = recommend_cavity_wall(baseline, products)
# Assert
assert recommendation is not None
cost = recommendation.options[0].cost
assert cost is not None
assert abs(cost.total - 45.93 * 18.5) <= 0.01
assert abs(cost.contingency_rate - 0.10) <= 1e-9
def test_non_cavity_main_wall_yields_no_recommendation() -> None:
# Arrange
baseline: EpcPropertyData = build_epc()
main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN)
main.wall_construction = 2 # solid (not cavity); still uninsulated
# Act
recommendation: Recommendation | None = recommend_cavity_wall(baseline, _StubProducts())
# Assert
assert recommendation is None