From dc55d3b8994de7d598e726fd565ecf6d71f00fc5 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 24 Jun 2026 17:24:50 +0000 Subject: [PATCH] =?UTF-8?q?Price=20an=20off-peak=20meter=20line=20day/nigh?= =?UTF-8?q?t=20by=20its=20high-rate=20fraction=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- domain/billing/bill.py | 8 +++++- domain/billing/bill_derivation.py | 17 ++++++++++--- tests/domain/billing/test_bill_derivation.py | 26 +++++++++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/domain/billing/bill.py b/domain/billing/bill.py index 5aff24cf..104bece2 100644 --- a/domain/billing/bill.py +++ b/domain/billing/bill.py @@ -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) diff --git a/domain/billing/bill_derivation.py b/domain/billing/bill_derivation.py index c1a09c64..a73b7d4e 100644 --- a/domain/billing/bill_derivation.py +++ b/domain/billing/bill_derivation.py @@ -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) diff --git a/tests/domain/billing/test_bill_derivation.py b/tests/domain/billing/test_bill_derivation.py index cce045ee..14efea71 100644 --- a/tests/domain/billing/test_bill_derivation.py +++ b/tests/domain/billing/test_bill_derivation.py @@ -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(