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