mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Merge pull request #1286 from Hestia-Homes/fix/predicted-baseline-rhi-fallback
Fall back to computed kWh for predicted-EPC baselines (fix 16 e2e failures)
This commit is contained in:
commit
eecea8301e
2 changed files with 103 additions and 14 deletions
|
|
@ -1,12 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
EpcPropertyData,
|
||||
RenewableHeatIncentive,
|
||||
)
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.billing.bill import EnergyBreakdown
|
||||
from domain.sap10_calculator.calculator import SapResult
|
||||
from domain.billing.bill_derivation import BillDerivation
|
||||
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
|
||||
from domain.property_baseline.performance import lodged_performance
|
||||
|
|
@ -64,24 +63,40 @@ class PropertyBaselineOrchestrator:
|
|||
if rebaselined.sap_result is not None
|
||||
else None
|
||||
)
|
||||
rhi = _require_rhi(effective_epc)
|
||||
space_heating_kwh, water_heating_kwh = _baseline_kwh(
|
||||
effective_epc, rebaselined.sap_result
|
||||
)
|
||||
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,
|
||||
space_heating_kwh=space_heating_kwh,
|
||||
water_heating_kwh=water_heating_kwh,
|
||||
bill=bill,
|
||||
)
|
||||
uow.property_baseline.save(baseline, property_id)
|
||||
uow.commit()
|
||||
|
||||
|
||||
def _require_rhi(epc: EpcPropertyData) -> RenewableHeatIncentive:
|
||||
def _baseline_kwh(
|
||||
epc: EpcPropertyData, sap_result: Optional[SapResult]
|
||||
) -> tuple[float, float]:
|
||||
"""Baseline space- and water-heating kWh for the Property.
|
||||
|
||||
Lodged certs carry these in the EPC's RHI block (the ADR-0007 ML target).
|
||||
Predicted EPCs (ADR-0031) deep-copy a neighbour template that may lack an RHI
|
||||
block, so fall back to the property's OWN computed figures from the scored
|
||||
SapResult — a better baseline than a neighbour's lodged numbers anyway. When
|
||||
neither source exists (no RHI and the rebaseliner scored nothing), there is
|
||||
genuinely no demand to record, so fail noisily.
|
||||
"""
|
||||
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
|
||||
if rhi is not None:
|
||||
return rhi.space_heating_kwh, rhi.water_heating_kwh
|
||||
if sap_result is not None:
|
||||
return sap_result.space_heating_kwh_per_yr, sap_result.hot_water_kwh_per_yr
|
||||
raise ValueError(
|
||||
"Effective EPC has no renewable_heat_incentive and the rebaseliner "
|
||||
"produced no SapResult; cannot establish baseline space-heating / "
|
||||
"hot-water kWh"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -165,6 +165,80 @@ class _ScoringRebaseliner(Rebaseliner):
|
|||
)
|
||||
|
||||
|
||||
def _property_without_rhi(*, sap_version: float = 10.2) -> Property:
|
||||
"""A predicted-style property whose EPC carries no lodged RHI block (ADR-0031
|
||||
predicted EPCs deep-copy a template that may lack RHI)."""
|
||||
epc = object.__new__(EpcPropertyData)
|
||||
epc.energy_rating_current = 72
|
||||
epc.current_energy_efficiency_band = Epc.C
|
||||
epc.co2_emissions_current = 1.8
|
||||
epc.energy_consumption_current = 180
|
||||
epc.sap_version = sap_version
|
||||
epc.renewable_heat_incentive = None
|
||||
return Property(
|
||||
identity=PropertyIdentity(
|
||||
portfolio_id=1, postcode="A0 0AA", address="1 Some Street", uprn=123
|
||||
),
|
||||
epc=epc,
|
||||
)
|
||||
|
||||
|
||||
def _sap_result_with_heating(*, space_kwh: float, water_kwh: float) -> SapResult:
|
||||
result = _sap_result_with_lighting()
|
||||
object.__setattr__(result, "space_heating_kwh_per_yr", space_kwh)
|
||||
object.__setattr__(result, "hot_water_kwh_per_yr", water_kwh)
|
||||
# hot-water kWh must carry a fuel code for Bill Derivation (1 = mains gas).
|
||||
object.__setattr__(result, "hot_water_fuel_code", 1)
|
||||
return result
|
||||
|
||||
|
||||
def test_run_falls_back_to_computed_kwh_when_epc_has_no_rhi() -> None:
|
||||
# Arrange — a predicted property with no lodged RHI, scored by a rebaseliner
|
||||
# that returns a SapResult carrying the property's own computed energy.
|
||||
property_baseline_repo = FakePropertyBaselineRepo()
|
||||
uow = FakeUnitOfWork(
|
||||
property=FakePropertyRepo({10: _property_without_rhi()}),
|
||||
property_baseline=property_baseline_repo,
|
||||
)
|
||||
orchestrator = PropertyBaselineOrchestrator(
|
||||
unit_of_work=lambda: uow,
|
||||
rebaseliner=_ScoringRebaseliner(
|
||||
_sap_result_with_heating(space_kwh=4200.0, water_kwh=1600.0)
|
||||
),
|
||||
fuel_rates=FuelRatesStaticFileRepository(),
|
||||
)
|
||||
|
||||
# Act
|
||||
orchestrator.run([10])
|
||||
|
||||
# Assert — baseline kWh come from the computed SapResult, not a (missing) RHI.
|
||||
(baseline, _) = property_baseline_repo.saved[0]
|
||||
assert baseline.space_heating_kwh == 4200.0
|
||||
assert baseline.water_heating_kwh == 1600.0
|
||||
assert uow.commits == 1
|
||||
|
||||
|
||||
def test_run_raises_when_no_rhi_and_no_scored_result() -> None:
|
||||
# Arrange — no lodged RHI AND the stub rebaseliner scores nothing: there is
|
||||
# genuinely no source for baseline kWh, so the batch must fail noisily.
|
||||
property_baseline_repo = FakePropertyBaselineRepo()
|
||||
uow = FakeUnitOfWork(
|
||||
property=FakePropertyRepo({10: _property_without_rhi()}),
|
||||
property_baseline=property_baseline_repo,
|
||||
)
|
||||
orchestrator = PropertyBaselineOrchestrator(
|
||||
unit_of_work=lambda: uow,
|
||||
rebaseliner=StubRebaseliner(),
|
||||
fuel_rates=FuelRatesStaticFileRepository(),
|
||||
)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(ValueError, match="renewable_heat_incentive"):
|
||||
orchestrator.run([10])
|
||||
assert property_baseline_repo.saved == []
|
||||
assert uow.commits == 0
|
||||
|
||||
|
||||
def test_run_derives_and_persists_a_bill_when_the_rebaseliner_scores() -> None:
|
||||
# Arrange — a rebaseliner that hands back a SapResult with lighting energy,
|
||||
# so the orchestrator prices it into a Bill at the committed snapshot.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue