Model/tests/domain/modelling/test_floor_recommendation.py
Khalim Conn-Kowlessar 84ec6da032 refactor(modelling): group domain/modelling into generators/scoring/optimisation
domain/modelling/ had grown to 15 flat modules. Group the behavioural ones into
subpackages — generators/ (wall/roof/floor Recommendation Generators), scoring/
(overlay applicator, package scorer, role-1/3 scoring), optimisation/ (optimiser
+ measure dependency) — and leave the shared value-object vocabulary
(recommendation, plan, scenario, product, contingencies, simulation) flat at the
top, since it is imported everywhere. Pure move + import-path rewrite across 89
import sites; no behaviour change. 136 pass, pyright strict clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:48:36 +00:00

97 lines
3.2 KiB
Python

"""Behaviour of the floor Recommendation Generator: detecting an uninsulated
ground floor and its construction (suspended vs solid), emitting the matching
single insulation Option with overlay + priced Cost. A floor is one
construction, so this is a single-Option Recommendation (like cavity walls).
"""
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
SapBuildingPart,
)
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.floor_recommendation import recommend_floor_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=25.0, contingency_rate=0.20
)
def _main(epc: EpcPropertyData) -> SapBuildingPart:
return next(
p for p in epc.sap_building_parts if p.identifier is BuildingPartIdentifier.MAIN
)
def test_uninsulated_suspended_floor_yields_suspended_insulation() -> None:
# Arrange — 000490 MAIN: "Suspended timber", "As built" (uninsulated)
baseline: EpcPropertyData = build_epc()
# Act
recommendation: Recommendation | None = recommend_floor_insulation(
baseline, _StubProducts()
)
# Assert
assert recommendation is not None
assert recommendation.surface == "Ground floor"
assert len(recommendation.options) == 1
option = recommendation.options[0]
assert option.measure_type == "suspended_floor_insulation"
simulated: EpcPropertyData = apply_simulations(baseline, [option.overlay])
assert _main(simulated).floor_insulation_thickness == 100
def test_uninsulated_solid_floor_yields_solid_insulation() -> None:
# Arrange
baseline: EpcPropertyData = build_epc()
_main(baseline).floor_construction_type = "Solid"
# Act
recommendation: Recommendation | None = recommend_floor_insulation(
baseline, _StubProducts()
)
# Assert
assert recommendation is not None
assert recommendation.options[0].measure_type == "solid_floor_insulation"
def test_already_insulated_floor_yields_no_recommendation() -> None:
# Arrange
baseline: EpcPropertyData = build_epc()
_main(baseline).floor_insulation_thickness = "100"
# Act
recommendation: Recommendation | None = recommend_floor_insulation(
baseline, _StubProducts()
)
# Assert
assert recommendation is None
def test_floor_option_carries_cost_from_ground_floor_area_and_product() -> None:
# Arrange — 000490 MAIN ground floor area 14.85 m^2
baseline: EpcPropertyData = build_epc()
# Act
recommendation: Recommendation | None = recommend_floor_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 * 25.0) <= 0.01
assert abs(cost.contingency_rate - 0.20) <= 1e-9