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, )