Model/domain/property_baseline/bill.py
Khalim Conn-Kowlessar 8ae3b56f41 feat(baseline): BillDerivation prices an energy breakdown at Fuel Rates (ADR-0014)
Slice 2 of Bill Derivation. BillDerivation(fuel_rates).derive(breakdown) takes a
delivered-energy breakdown (per-section EnergyLine(section, fuel, kwh) +
exported_kwh) and produces a Bill: per-section kWh + cost, standing charges,
SEG credit, and total.

- Each end-use line billed at its fuel's unit rate.
- Standing charge added ONCE per distinct fuel used (a meter, not an end use);
  off-gas fuels carry 0 so contribute nothing — no metered/unmetered special case.
- SEG export credit subtracted.
- Deterministic (ADR-0006); raises UnpricedFuel (via FuelRates) on an unpriced
  fuel (e.g. heat network) rather than billing at a wrong default.

Pure domain — no calculator dependency; the SapResult->EnergyBreakdown adapter
is slice 3.

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

58 lines
1.5 KiB
Python

from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from enum import Enum
from domain.fuel_rates.fuel import Fuel
class BillSection(Enum):
"""A user-meaningful slice of the annual energy bill — the calculator's raw
end uses folded into the sections the UI shows (ADR-0014)."""
HEATING = "HEATING"
HOT_WATER = "HOT_WATER"
LIGHTING = "LIGHTING"
APPLIANCES = "APPLIANCES"
COOKING = "COOKING"
PUMPS_FANS = "PUMPS_FANS"
@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)."""
section: BillSection
fuel: Fuel
kwh: float
@dataclass(frozen=True)
class EnergyBreakdown:
"""A Property's delivered energy per end use, the input to Bill Derivation —
produced from SAP10 Calculation in a later slice. ``exported_kwh`` is PV
generation exported to the grid, credited at the SEG rate."""
lines: Sequence[EnergyLine]
exported_kwh: float = 0.0
@dataclass(frozen=True)
class BillSectionCost:
"""One section's rolled-up delivered kWh and annual cost (£)."""
kwh: float
cost_gbp: float
@dataclass(frozen=True)
class Bill:
"""A Property's annual energy bill, composed per section plus the per-meter
standing charges and the SEG export credit, and the total (ADR-0014)."""
sections: Mapping[BillSection, BillSectionCost]
standing_charges_gbp: float
seg_credit_gbp: float
total_gbp: float