from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass from typing import Optional 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 OffPeakRate: """An Off-Peak Meter's dual-rate tariff — a cheaper ``night`` (low) rate and a dearer ``day`` (high) rate, plus the daily standing charge (ADR-0014). Off-peak electricity has no single unit rate, so it does not live in the single-rate ``FuelRates.rates`` map; each end use blends day/night by its own High-Rate Fraction (a calculator output).""" day_p_per_kwh: float night_p_per_kwh: float standing_charge_p_per_day: float def blended_p_per_kwh(self, high_rate_fraction: float) -> float: """Effective p/kWh for an end use billing ``high_rate_fraction`` of its kWh at the day (high) rate and the remainder at the night (low) rate.""" return ( high_rate_fraction * self.day_p_per_kwh + (1.0 - high_rate_fraction) * self.night_p_per_kwh ) @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] off_peak: Optional[OffPeakRate] = None def off_peak_blended_p_per_kwh(self, high_rate_fraction: float) -> float: """Blended day/night p/kWh for an Off-Peak Meter end use at the given High-Rate Fraction. Raises ``UnpricedFuel`` when the snapshot carries no off-peak entry.""" if self.off_peak is None: raise UnpricedFuel(Fuel.ELECTRICITY_OFF_PEAK) return self.off_peak.blended_p_per_kwh(high_rate_fraction) 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: if fuel is Fuel.ELECTRICITY_OFF_PEAK: if self.off_peak is None: raise UnpricedFuel(fuel) return self.off_peak.standing_charge_p_per_day 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