mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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>
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from typing import Final
|
|
|
|
from domain.fuel_rates.fuel import Fuel
|
|
from domain.fuel_rates.fuel_rates import FuelRates
|
|
from domain.property_baseline.bill import (
|
|
Bill,
|
|
BillSection,
|
|
BillSectionCost,
|
|
EnergyBreakdown,
|
|
)
|
|
|
|
_DAYS_PER_YEAR: Final[float] = 365.0
|
|
_PENCE_PER_POUND: Final[float] = 100.0
|
|
|
|
|
|
class BillDerivation:
|
|
"""Derives a Property's annual energy Bill by pricing a delivered-energy
|
|
breakdown at current Fuel Rates (ADR-0014).
|
|
|
|
Each end-use line is billed at its fuel's unit rate; **standing charges are
|
|
added once per distinct fuel used** (a meter, not an end use — off-gas fuels
|
|
carry a 0 standing charge so they contribute nothing); the SEG export credit
|
|
is subtracted. Deterministic (ADR-0006). Raises ``UnpricedFuel`` (via
|
|
``FuelRates``) on a fuel the snapshot does not price.
|
|
"""
|
|
|
|
def __init__(self, fuel_rates: FuelRates) -> None:
|
|
self._rates = fuel_rates
|
|
|
|
def derive(self, breakdown: EnergyBreakdown) -> Bill:
|
|
section_kwh: defaultdict[BillSection, float] = defaultdict(float)
|
|
section_cost_p: defaultdict[BillSection, float] = defaultdict(float)
|
|
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)
|
|
)
|
|
if line.kwh > 0:
|
|
fuels_used.add(line.fuel)
|
|
|
|
sections = {
|
|
section: BillSectionCost(
|
|
kwh=section_kwh[section], cost_gbp=section_cost_p[section] / _PENCE_PER_POUND
|
|
)
|
|
for section in section_kwh
|
|
}
|
|
standing_charges_gbp = (
|
|
sum(
|
|
(self._rates.standing_charge_p_per_day(fuel) * _DAYS_PER_YEAR for fuel in fuels_used),
|
|
0.0,
|
|
)
|
|
/ _PENCE_PER_POUND
|
|
)
|
|
seg_credit_gbp = (
|
|
breakdown.exported_kwh * self._rates.seg_export_p_per_kwh / _PENCE_PER_POUND
|
|
)
|
|
total_gbp = (
|
|
sum((section.cost_gbp for section in sections.values()), 0.0)
|
|
+ standing_charges_gbp
|
|
- seg_credit_gbp
|
|
)
|
|
return Bill(
|
|
sections=sections,
|
|
standing_charges_gbp=standing_charges_gbp,
|
|
seg_credit_gbp=seg_credit_gbp,
|
|
total_gbp=total_gbp,
|
|
)
|