Load the off-peak day/night rate from the committed snapshot 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-24 17:20:19 +00:00
parent bda8a3f276
commit 1acfc08fce
2 changed files with 32 additions and 8 deletions

View file

@ -5,7 +5,7 @@ from pathlib import Path
from typing import Any, Optional
from domain.fuel_rates.fuel import Fuel
from domain.fuel_rates.fuel_rates import FuelRate, FuelRates
from domain.fuel_rates.fuel_rates import FuelRate, FuelRates, OffPeakRate
from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository
_DEFAULT_SNAPSHOT = Path(__file__).parent / "data" / "fuel_rates_2026_q2.json"
@ -14,10 +14,12 @@ _DEFAULT_SNAPSHOT = Path(__file__).parent / "data" / "fuel_rates_2026_q2.json"
class FuelRatesStaticFileRepository(FuelRatesRepository):
"""Reads Fuel Rates from a committed JSON snapshot (ADR-0014).
Only **single-rate** fuels (those lodging a ``unit_rate_p_per_kwh``) are
exposed. Off-peak (day/night) and the unpriced gaps (null entries house
coal, heat network) are skipped, so pricing them raises ``UnpricedFuel``.
The day/night accessor for off-peak lands in a later slice.
**Single-rate** fuels (those lodging a ``unit_rate_p_per_kwh``) populate the
``rates`` map. The **off-peak** entry a dual-rate meter lodging
``day_p_per_kwh`` / ``night_p_per_kwh`` loads as an ``OffPeakRate`` (each
end use blends day/night by its own High-Rate Fraction). The unpriced gaps
(null entries house coal, heat network) are skipped, so pricing them
raises ``UnpricedFuel``.
"""
def __init__(self, snapshot_path: Optional[Path] = None) -> None:
@ -27,11 +29,19 @@ class FuelRatesStaticFileRepository(FuelRatesRepository):
payload: dict[str, Any] = json.loads(self._snapshot_path.read_text())
fuels: dict[str, Any] = payload["fuels"]
rates: dict[Fuel, FuelRate] = {}
off_peak: Optional[OffPeakRate] = None
for name, entry in fuels.items():
if entry is None:
continue # an unpriced gap (house coal / heat network)
if name == Fuel.ELECTRICITY_OFF_PEAK.name:
off_peak = OffPeakRate(
day_p_per_kwh=float(entry["day_p_per_kwh"]),
night_p_per_kwh=float(entry["night_p_per_kwh"]),
standing_charge_p_per_day=float(entry["standing_charge_p_per_day"]),
)
continue
if "unit_rate_p_per_kwh" not in entry:
continue # off-peak day/night — priced in a later slice
continue # an unpriced gap with no single rate
rates[Fuel[name]] = FuelRate(
unit_rate_p_per_kwh=float(entry["unit_rate_p_per_kwh"]),
standing_charge_p_per_day=float(entry["standing_charge_p_per_day"]),
@ -40,4 +50,5 @@ class FuelRatesStaticFileRepository(FuelRatesRepository):
period=str(payload["period"]),
seg_export_p_per_kwh=float(payload["seg_export_p_per_kwh"]),
rates=rates,
off_peak=off_peak,
)

View file

@ -55,8 +55,21 @@ def test_dual_fuel_carries_a_derived_midpoint_rate() -> None:
assert rates.standing_charge_p_per_day(Fuel.DUAL_FUEL_MINERAL_AND_WOOD) == 0.0
def test_off_peak_remains_unpriced_pending_the_day_night_accessor() -> None:
# Arrange — off-peak still needs the day/night split a later slice adds (ADR-0014).
def test_off_peak_meter_prices_day_night_from_the_snapshot() -> None:
# Arrange — the committed snapshot's off-peak entry carries a day/night
# split (ADR-0014 2026-06-24 amendment); the repo loads it as an OffPeakRate.
rates = FuelRatesStaticFileRepository().get_current()
# Act / Assert — an all-night load bills at the night rate, an all-day load
# at the day rate, and the off-peak meter carries its own standing charge.
assert rates.off_peak_blended_p_per_kwh(high_rate_fraction=0.0) == 13.89
assert rates.off_peak_blended_p_per_kwh(high_rate_fraction=1.0) == 29.73
assert rates.standing_charge_p_per_day(Fuel.ELECTRICITY_OFF_PEAK) == 56.99
def test_off_peak_has_no_single_unit_rate() -> None:
# Arrange — off-peak has no single blended unit rate; callers must price it
# day/night via a High-Rate Fraction, so unit_rate_p_per_kwh still raises.
rates = FuelRatesStaticFileRepository().get_current()
# Act / Assert