"""Valuation Uplift — the percentage-primary financial-uplift model (ADR-0018). Band-transition uplift compounded from four broker tables (MoneySupermarket, Lloyds, Knight Frank, Rightmove); always a percentage, an absolute £ only when a Property Valuation is supplied, capped so the £ uplift never exceeds 2x cost. """ from __future__ import annotations from domain.modelling.valuation import ValuationUplift, estimate_valuation_uplift def test_band_jump_yields_percentage_uplift_without_a_current_value() -> None: # Arrange / Act — D -> C, no current market value supplied. uplift: ValuationUplift = estimate_valuation_uplift("D", "C") # Assert — D->C sources: MSM 2%, Lloyds 2%, Knight Frank 3%, Rightmove 3% # → min 2%, max 3%, mean 2.5%. The £ forms stay None with no Property Valuation. assert abs(uplift.lower_pct - 0.02) <= 1e-9 assert abs(uplift.upper_pct - 0.03) <= 1e-9 assert abs(uplift.average_pct - 0.025) <= 1e-9 assert uplift.lower_value is None assert uplift.upper_value is None assert uplift.average_value is None assert uplift.post_retrofit_value is None def test_a_current_value_yields_absolute_pound_uplift() -> None: # Arrange / Act — D -> C with a £200k market value, no cost cap. uplift: ValuationUplift = estimate_valuation_uplift( "D", "C", current_value=200_000.0 ) # Assert — £ increase at each source % plus the resulting post-retrofit value. assert uplift.lower_value is not None and abs(uplift.lower_value - 4_000.0) <= 1e-6 assert uplift.upper_value is not None and abs(uplift.upper_value - 6_000.0) <= 1e-6 assert ( uplift.average_value is not None and abs(uplift.average_value - 5_000.0) <= 1e-6 ) assert ( uplift.post_retrofit_value is not None and abs(uplift.post_retrofit_value - 205_000.0) <= 1e-6 ) def test_roi_cap_rescales_the_uplift_to_twice_the_cost() -> None: # Arrange / Act — a £1m property whose raw 2.5% average (£25k) exceeds twice # a £10k retrofit, so the uplift is capped at 2x cost. uplift: ValuationUplift = estimate_valuation_uplift( "D", "C", current_value=1_000_000.0, total_cost=10_000.0 ) # Assert — average £ uplift binds to 2x cost, and the bounds rescale by the # same 0.8 scalar (0.025 -> 0.02). assert ( uplift.average_value is not None and abs(uplift.average_value - 20_000.0) <= 1e-6 ) assert abs(uplift.average_pct - 0.02) <= 1e-9 assert abs(uplift.lower_pct - 0.016) <= 1e-9 assert abs(uplift.upper_pct - 0.024) <= 1e-9 def test_multi_band_jump_compounds_steps_and_takes_a_spot_source() -> None: # Arrange / Act — F -> C: MSM/Lloyds compound three steps; Rightmove gives a # 15% whole-jump spot; Knight Frank has no F->C entry. uplift: ValuationUplift = estimate_valuation_uplift("F", "C") # Assert — min = compounded MSM (~4.05%), max = Rightmove 15%, mean of the # three covering sources. assert abs(uplift.lower_pct - 0.040502) <= 1e-6 assert abs(uplift.upper_pct - 0.15) <= 1e-9 assert abs(uplift.average_pct - (0.040502 + 0.07476992 + 0.15) / 3) <= 1e-6 def test_no_improvement_yields_zero_uplift() -> None: # Arrange / Act — same band in and out. uplift: ValuationUplift = estimate_valuation_uplift("C", "C") # Assert — every source compounds over no steps / matches no spot. assert abs(uplift.lower_pct) <= 1e-12 assert abs(uplift.upper_pct) <= 1e-12 assert abs(uplift.average_pct) <= 1e-12