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>
161 lines
5.4 KiB
Python
161 lines
5.4 KiB
Python
"""Run one property through the full First Run pipeline with no database.
|
|
|
|
The interactive inspection entrypoint: hand it an `EpcPropertyData` (e.g.
|
|
`EpcPropertyDataMapper.from_api_response(json)`), and it wires the whole
|
|
`AraFirstRunPipeline` (Ingestion -> Baseline -> Modelling) against in-memory
|
|
fakes — no Postgres, no network — runs it, prints the sense-check table, and
|
|
returns the `Plan` for further poking.
|
|
|
|
Dev tooling, not deployed: it reuses the in-memory test fakes, so run it from a
|
|
REPL at the worktree root::
|
|
|
|
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
|
from harness.console import run_one
|
|
plan = run_one(EpcPropertyDataMapper.from_api_response(my_api_json), goal_band="C")
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
|
from domain.geospatial.coordinates import Coordinates
|
|
from domain.modelling.plan import Plan
|
|
from domain.modelling.scenario import Scenario
|
|
from domain.property.property import Property, PropertyIdentity
|
|
from domain.property_baseline.rebaseliner import StubRebaseliner
|
|
from domain.sap10_calculator.calculator import Sap10Calculator
|
|
from harness.plan_table import format_plan_table
|
|
from orchestration.ara_first_run_pipeline import AraFirstRunPipeline
|
|
from orchestration.ingestion_orchestrator import IngestionOrchestrator
|
|
from orchestration.modelling_orchestrator import ModellingOrchestrator
|
|
from orchestration.property_baseline_orchestrator import PropertyBaselineOrchestrator
|
|
from repositories.fuel_rates.fuel_rates_static_file_repository import (
|
|
FuelRatesStaticFileRepository,
|
|
)
|
|
from repositories.geospatial.geospatial_repository import GeospatialRepository
|
|
from repositories.product.product_json_repository import ProductJsonRepository
|
|
from tests.orchestration.fakes import (
|
|
FakeEpcRepo,
|
|
FakePlanRepository,
|
|
FakePropertyRepo,
|
|
FakeScenarioRepository,
|
|
FakeUnitOfWork,
|
|
)
|
|
|
|
DEFAULT_CATALOGUE = Path(__file__).resolve().parent / "sample_catalogue.json"
|
|
|
|
_PROPERTY_ID = 1
|
|
_SCENARIO_ID = 7
|
|
_PORTFOLIO_ID = 1
|
|
|
|
|
|
@dataclass
|
|
class _Command:
|
|
portfolio_id: int
|
|
property_ids: list[int]
|
|
scenario_ids: list[int]
|
|
|
|
|
|
class _FetcherReturning:
|
|
def __init__(self, epc: EpcPropertyData) -> None:
|
|
self._epc = epc
|
|
|
|
def get_by_uprn(self, uprn: int) -> Optional[EpcPropertyData]:
|
|
return self._epc
|
|
|
|
|
|
class _NoCoordinates(GeospatialRepository):
|
|
def coordinates_for(self, uprn: int) -> Optional[Coordinates]:
|
|
return None # skip the solar leg
|
|
|
|
|
|
class _UnusedSolarFetcher:
|
|
def get_building_insights(
|
|
self, longitude: float, latitude: float
|
|
) -> dict[str, Any]: # pragma: no cover
|
|
return {}
|
|
|
|
|
|
def run_one(
|
|
epc: EpcPropertyData,
|
|
*,
|
|
goal_band: str = "C",
|
|
catalogue_path: Path = DEFAULT_CATALOGUE,
|
|
current_market_value: Optional[float] = None,
|
|
print_table: bool = True,
|
|
) -> Plan:
|
|
"""Run ``epc`` through the full First Run pipeline with no database and
|
|
return its Plan for the default Increasing-EPC Scenario targeting
|
|
``goal_band``. Prints the sense-check table unless ``print_table`` is False.
|
|
|
|
Pass ``current_market_value`` (a Property Valuation) to value the Plan's
|
|
Valuation Uplift in £ — otherwise the uplift is percentage-only (ADR-0018).
|
|
``epc`` must carry lodged recorded-performance + the RHI block (a real lodged
|
|
EPC does) so the Baseline stage can run."""
|
|
epc_repo = FakeEpcRepo()
|
|
plan_repo = FakePlanRepository()
|
|
property_repo = FakePropertyRepo(
|
|
{
|
|
_PROPERTY_ID: Property(
|
|
identity=PropertyIdentity(
|
|
portfolio_id=_PORTFOLIO_ID,
|
|
postcode="A0 0AA",
|
|
address="1 Some Street",
|
|
uprn=12345,
|
|
),
|
|
current_market_value=current_market_value,
|
|
)
|
|
},
|
|
epc_repo=epc_repo,
|
|
)
|
|
unit = FakeUnitOfWork(
|
|
property=property_repo,
|
|
epc=epc_repo,
|
|
scenario=FakeScenarioRepository(
|
|
{
|
|
_SCENARIO_ID: Scenario(
|
|
id=_SCENARIO_ID,
|
|
goal="Increasing EPC",
|
|
goal_value=goal_band,
|
|
budget=None,
|
|
is_default=True,
|
|
)
|
|
}
|
|
),
|
|
product=ProductJsonRepository(catalogue_path),
|
|
plan=plan_repo,
|
|
)
|
|
|
|
pipeline = AraFirstRunPipeline(
|
|
ingestion=IngestionOrchestrator(
|
|
unit_of_work=lambda: unit,
|
|
epc_fetcher=_FetcherReturning(epc),
|
|
geospatial_repo=_NoCoordinates(),
|
|
solar_fetcher=_UnusedSolarFetcher(),
|
|
),
|
|
baseline=PropertyBaselineOrchestrator(
|
|
unit_of_work=lambda: unit,
|
|
rebaseliner=StubRebaseliner(),
|
|
fuel_rates=FuelRatesStaticFileRepository(),
|
|
),
|
|
modelling=ModellingOrchestrator(
|
|
unit_of_work=lambda: unit,
|
|
calculator=Sap10Calculator(),
|
|
fuel_rates=FuelRatesStaticFileRepository(),
|
|
),
|
|
)
|
|
pipeline.run(
|
|
_Command(
|
|
portfolio_id=_PORTFOLIO_ID,
|
|
property_ids=[_PROPERTY_ID],
|
|
scenario_ids=[_SCENARIO_ID],
|
|
)
|
|
)
|
|
|
|
plan = plan_repo.saved[(_PROPERTY_ID, _SCENARIO_ID)]
|
|
if print_table:
|
|
print("\n" + format_plan_table(plan))
|
|
return plan
|