Model/domain/fuel_rates/fuel_rates.py
Khalim Conn-Kowlessar 14b45a1b3e feat(fuel-rates): FuelRates snapshot + repository foundation (ADR-0014)
Slice 1 of Bill Derivation — the reference-data foundation that later slices
price the calculator's per-end-use kWh against:

- Fuel enum (canonical billing fuels; the join key between the calculator's
  SAP-code fuels and the rates snapshot). COAL + HEAT_NETWORK are members with
  no national rate.
- FuelRates value object: unit_rate_p_per_kwh / standing_charge_p_per_day /
  seg_export_p_per_kwh; raises UnpricedFuel on a fuel it has no rate for rather
  than billing at a wrong default.
- FuelRatesRepository port (ADR-0011 Repo-reads-stored-reference-data) +
  StaticFileFuelRatesRepository reading a committed JSON snapshot.
- Snapshot fuel_rates_2026_q2.json: GB national, Apr-Jun 2026 Ofgem cap
  (gas/electricity) + DESNZ/NEP May 2026 (off-gas). Carries the full researched
  data; the value object exposes single-rate fuels this slice. Off-peak
  (day/night), house coal and heat network raise UnpricedFuel until later slices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 09:29:07 +00:00

46 lines
1.4 KiB
Python

from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from domain.fuel_rates.fuel import Fuel, UnpricedFuel
@dataclass(frozen=True)
class FuelRate:
"""One fuel's current tariff: unit price + daily standing charge.
Off-gas fuels (oil / LPG / solid / wood) carry a ``0.0`` standing charge —
they are delivered, not metered, so there is no daily charge.
"""
unit_rate_p_per_kwh: float
standing_charge_p_per_day: float
@dataclass(frozen=True)
class FuelRates:
"""A current Fuel Rates snapshot — the rate per billing Fuel plus the SEG
export credit (ADR-0014). ``period`` records which window it is for, since
a committed snapshot goes stale on the Ofgem-cap (quarterly) cadence.
Pricing a fuel the snapshot does not carry raises ``UnpricedFuel`` rather
than defaulting — see [[reference-unmapped-sap-code]] for the same strict
discipline on the calculator side.
"""
period: str
seg_export_p_per_kwh: float
rates: Mapping[Fuel, FuelRate]
def unit_rate_p_per_kwh(self, fuel: Fuel) -> float:
return self._rate(fuel).unit_rate_p_per_kwh
def standing_charge_p_per_day(self, fuel: Fuel) -> float:
return self._rate(fuel).standing_charge_p_per_day
def _rate(self, fuel: Fuel) -> FuelRate:
rate = self.rates.get(fuel)
if rate is None:
raise UnpricedFuel(fuel)
return rate