Price an off-peak meter line day/night by its high-rate fraction 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-24 17:24:50 +00:00
parent 1acfc08fce
commit dc55d3b899
3 changed files with 46 additions and 5 deletions

View file

@ -28,11 +28,17 @@ class BillSection(Enum):
@dataclass(frozen=True)
class EnergyLine:
"""One section's delivered energy on one fuel. A section may have more than
one line (e.g. gas main heating + electric secondary heating)."""
one line (e.g. gas main heating + electric secondary heating).
``high_rate_fraction`` is the calculator's High-Rate Fraction for this end
use the share of its kWh billed at the day (high) rate on an Off-Peak
Meter set only on ``ELECTRICITY_OFF_PEAK`` lines; ``None`` for single-rate
fuels, which bill at their flat unit rate."""
section: BillSection
fuel: Fuel
kwh: float
high_rate_fraction: Optional[float] = None
@dataclass(frozen=True)

View file

@ -10,6 +10,7 @@ from domain.billing.bill import (
BillSection,
BillSectionCost,
EnergyBreakdown,
EnergyLine,
)
_DAYS_PER_YEAR: Final[float] = 365.0
@ -36,9 +37,7 @@ class BillDerivation:
fuels_used: set[Fuel] = set()
for line in breakdown.lines:
section_kwh[line.section] += line.kwh
section_cost_p[line.section] += (
line.kwh * self._rates.unit_rate_p_per_kwh(line.fuel)
)
section_cost_p[line.section] += line.kwh * self._unit_rate_p_per_kwh(line)
if line.kwh > 0:
fuels_used.add(line.fuel)
@ -69,3 +68,15 @@ class BillDerivation:
seg_credit_gbp=seg_credit_gbp,
total_gbp=total_gbp,
)
def _unit_rate_p_per_kwh(self, line: EnergyLine) -> float:
"""Price one line's fuel (p/kWh). An Off-Peak Meter line blends day/night
by its High-Rate Fraction; every other fuel bills at its flat unit rate."""
if line.fuel is Fuel.ELECTRICITY_OFF_PEAK:
if line.high_rate_fraction is None:
raise ValueError(
f"{line.section.value} bills on an off-peak meter but carries "
"no high-rate fraction; cannot split day/night"
)
return self._rates.off_peak_blended_p_per_kwh(line.high_rate_fraction)
return self._rates.unit_rate_p_per_kwh(line.fuel)

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import pytest
from domain.fuel_rates.fuel import Fuel, UnpricedFuel
from domain.fuel_rates.fuel_rates import FuelRate, FuelRates
from domain.fuel_rates.fuel_rates import FuelRate, FuelRates, OffPeakRate
from domain.billing.bill import BillSection, EnergyBreakdown, EnergyLine
from domain.billing.bill_derivation import BillDerivation
@ -17,9 +17,33 @@ def _rates() -> FuelRates:
Fuel.ELECTRICITY: FuelRate(unit_rate_p_per_kwh=24.67, standing_charge_p_per_day=57.21),
Fuel.OIL: FuelRate(unit_rate_p_per_kwh=9.16, standing_charge_p_per_day=0.0),
},
off_peak=OffPeakRate(
day_p_per_kwh=29.73, night_p_per_kwh=13.89, standing_charge_p_per_day=56.99
),
)
def test_an_all_night_off_peak_heating_line_bills_at_the_night_rate() -> None:
# Arrange — 10,000 kWh of electric storage heating on an Off-Peak Meter,
# charged wholly overnight (high-rate fraction 0.0).
breakdown = EnergyBreakdown(
lines=[
EnergyLine(
section=BillSection.HEATING,
fuel=Fuel.ELECTRICITY_OFF_PEAK,
kwh=10000.0,
high_rate_fraction=0.0,
)
]
)
# Act
bill = BillDerivation(_rates()).derive(breakdown)
# Assert — every kWh at the night rate: 10000 × 13.89p = £1389.
assert bill.sections[BillSection.HEATING].cost_gbp == pytest.approx(1389.0)
def test_derive_prices_a_single_gas_heating_line_with_its_standing_charge() -> None:
# Arrange — 10,000 kWh of mains-gas heating.
breakdown = EnergyBreakdown(