mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Slice 5a: the promotion. Replaces StubRebaseliner in production and collapses the
shadow runner into the rebaseliner (ADR-0013 amendment).
- CalculatorRebaseliner runs Sap10Calculator on every Property:
* sap_version < 10.2 -> Effective Performance IS the calculator output
(band via Epc.from_sap_score, CO2 kg->t, PEUI rounded), reason "pre_sap10".
* sap_version >= 10.2 -> Effective = lodged (API figures on-target), and the
calculator only logs divergence (SAP>0.5, PEUI/CO2 1%) as a validation signal.
* a calculator raise propagates -> batch aborts (ADR-0012); fix the cert at once.
- Rebaseliner.rebaseline gains property_id (for the divergence log).
- LoggingCalculatorShadow / the calculator_shadow seam removed from the
orchestrator; its divergence-comparison logic now lives in the rebaseliner.
- StubRebaseliner kept (signature updated) for orchestrator/repo unit tests.
The SapResult->EnergyBreakdown adapter + BillDerivation wiring (to populate the
bill block) follow once the appliances/cooking SapResult fields land.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
3.7 KiB
Python
87 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
|
|
from datatypes.epc.domain.epc_property_data import (
|
|
EpcPropertyData,
|
|
RenewableHeatIncentive,
|
|
)
|
|
from domain.billing.bill import EnergyBreakdown
|
|
from domain.billing.bill_derivation import BillDerivation
|
|
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
|
|
from domain.property_baseline.performance import lodged_performance
|
|
from domain.property_baseline.rebaseliner import Rebaseliner
|
|
from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository
|
|
from repositories.unit_of_work import UnitOfWork
|
|
|
|
|
|
class PropertyBaselineOrchestrator:
|
|
"""Stage 2: establish each Property's Baseline Performance and persist it.
|
|
|
|
Runs the whole batch in **one** Unit of Work and commits once (ADR-0012):
|
|
for each property it hydrates the Property via the unit's PropertyRepo,
|
|
resolves the Effective EPC, reads Lodged Performance off it, runs the
|
|
Rebaseliner to produce Effective Performance, and persists the pair plus the
|
|
deterministic kWh. Any property raising aborts the batch — the unit is left
|
|
uncommitted, so nothing persists and the subtask fails noisily.
|
|
|
|
Reads only from repos — never a Fetcher or HTTP (ADR-0003) — so it is
|
|
byte-identical whether Ingestion ran milliseconds ago (First Run) or last
|
|
week. The injected Rebaseliner is the re-score-on-override seam (ADR-0011).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
unit_of_work: Callable[[], UnitOfWork],
|
|
rebaseliner: Rebaseliner,
|
|
fuel_rates: FuelRatesRepository,
|
|
) -> None:
|
|
self._unit_of_work = unit_of_work
|
|
self._rebaseliner = rebaseliner
|
|
self._fuel_rates = fuel_rates
|
|
|
|
def run(self, property_ids: list[int]) -> None:
|
|
# The Fuel Rates snapshot is a committed static file (no DB), so read it
|
|
# once before the unit opens and reuse the BillDerivation across the
|
|
# batch — every property prices against the same snapshot.
|
|
fuel_rates = self._fuel_rates.get_current()
|
|
bill_derivation = BillDerivation(fuel_rates)
|
|
with self._unit_of_work() as uow:
|
|
properties = uow.property.get_many(property_ids)
|
|
for property_id, prop in zip(property_ids, properties, strict=True):
|
|
effective_epc = prop.effective_epc
|
|
lodged = lodged_performance(effective_epc)
|
|
rebaselined = self._rebaseliner.rebaseline(
|
|
property_id, effective_epc, lodged
|
|
)
|
|
# No SapResult (the stub path) means no scored picture to price,
|
|
# so the bill stays None.
|
|
bill = (
|
|
bill_derivation.derive(
|
|
EnergyBreakdown.from_sap_result(rebaselined.sap_result)
|
|
)
|
|
if rebaselined.sap_result is not None
|
|
else None
|
|
)
|
|
rhi = _require_rhi(effective_epc)
|
|
baseline = PropertyBaselinePerformance(
|
|
lodged=lodged,
|
|
effective=rebaselined.effective,
|
|
rebaseline_reason=rebaselined.reason,
|
|
space_heating_kwh=rhi.space_heating_kwh,
|
|
water_heating_kwh=rhi.water_heating_kwh,
|
|
bill=bill,
|
|
)
|
|
uow.property_baseline.save(baseline, property_id)
|
|
uow.commit()
|
|
|
|
|
|
def _require_rhi(epc: EpcPropertyData) -> RenewableHeatIncentive:
|
|
rhi = epc.renewable_heat_incentive
|
|
if rhi is None:
|
|
raise ValueError(
|
|
"Effective EPC is missing renewable_heat_incentive; cannot read "
|
|
"baseline space-heating / hot-water kWh"
|
|
)
|
|
return rhi
|