mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
The Plan derives its Valuation Uplift (ADR-0018) from its baseline -> post band jump and works+contingency cost, given one external input — the Property's current market value (a Property Valuation, mostly absent). `Plan.valuation` / `Plan.baseline_epc_rating` are derived like the other headline figures; `PlanModel.from_domain` maps the £ forms to the live plan.valuation_* columns (NULL when no value — the percentage is not persisted on those columns). `Property.current_market_value` is the new optional source; the orchestrator threads it onto the Plan. `run_one` takes a `current_market_value` so the harness can value the uplift, and the sense-check table shows the average % (always) plus the £ forms when known. Sourcing the current market value (upload / default) remains deferred (ADR-0018); it is None throughout until that lands, so the columns stay NULL at scale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
"""A Plan derives its Valuation Uplift from its band jump (ADR-0018).
|
|
|
|
The uplift is plan-conditional — it needs the Plan's baseline -> post band jump
|
|
and its cost — so the Plan derives it, given one external input: the Property's
|
|
current market value (mostly absent, so the £ forms are usually None)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from domain.modelling.plan import Plan
|
|
from domain.modelling.scoring.package_scorer import Score
|
|
from infrastructure.postgres.modelling import PlanModel
|
|
|
|
|
|
def _plan(*, current_market_value: float | None) -> Plan:
|
|
# Baseline SAP 57.4 rounds to band D; post 70.0 rounds to band C.
|
|
baseline = Score(
|
|
sap_continuous=57.4, co2_kg_per_yr=3000.0, primary_energy_kwh_per_yr=300.0
|
|
)
|
|
post = Score(
|
|
sap_continuous=70.0, co2_kg_per_yr=2100.0, primary_energy_kwh_per_yr=240.0
|
|
)
|
|
return Plan(
|
|
measures=(),
|
|
baseline=baseline,
|
|
post_retrofit=post,
|
|
current_market_value=current_market_value,
|
|
)
|
|
|
|
|
|
def test_plan_derives_pound_uplift_from_a_current_market_value() -> None:
|
|
# Arrange — a £200k property modelled D -> C.
|
|
plan: Plan = _plan(current_market_value=200_000.0)
|
|
|
|
# Act
|
|
uplift = plan.valuation
|
|
|
|
# Assert — D->C average 2.5% of £200k = £5,000 uplift, £205,000 post-retrofit.
|
|
assert uplift.average_value is not None
|
|
assert abs(uplift.average_value - 5_000.0) <= 1e-6
|
|
assert uplift.post_retrofit_value is not None
|
|
assert abs(uplift.post_retrofit_value - 205_000.0) <= 1e-6
|
|
|
|
|
|
def test_plan_model_persists_the_valuation_pound_forms() -> None:
|
|
# Arrange
|
|
plan: Plan = _plan(current_market_value=200_000.0)
|
|
|
|
# Act
|
|
model: PlanModel = PlanModel.from_domain(
|
|
plan, property_id=1, scenario_id=7, portfolio_id=1, is_default=True
|
|
)
|
|
|
|
# Assert
|
|
assert model.valuation_increase_lower_bound is not None
|
|
assert abs(model.valuation_increase_lower_bound - 4_000.0) <= 1e-6
|
|
assert model.valuation_increase_average is not None
|
|
assert abs(model.valuation_increase_average - 5_000.0) <= 1e-6
|
|
assert model.valuation_post_retrofit is not None
|
|
assert abs(model.valuation_post_retrofit - 205_000.0) <= 1e-6
|
|
|
|
|
|
def test_plan_model_leaves_valuation_null_without_a_market_value() -> None:
|
|
# Arrange — no current market value (the common case at scale).
|
|
plan: Plan = _plan(current_market_value=None)
|
|
|
|
# Act
|
|
model: PlanModel = PlanModel.from_domain(
|
|
plan, property_id=1, scenario_id=7, portfolio_id=1, is_default=True
|
|
)
|
|
|
|
# Assert — the percentage is still derivable, but the £ columns stay NULL.
|
|
assert model.valuation_increase_average is None
|
|
assert model.valuation_post_retrofit is None
|
|
assert abs(plan.valuation.average_pct - 0.025) <= 1e-9
|