mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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>
81 lines
2.8 KiB
Python
81 lines
2.8 KiB
Python
"""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,
|
|
)
|
|
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_loft_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_loft_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_loft_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_loft_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
|