energy-billing
Pure multi-product retail energy billing library for German markets.
energy-billing is the calculation core of billingd — the
Energy Billing Engine daemon for the Lieferant (LF) role. The library is zero I/O,
zero async, zero hardcoded regulatory rates. It answers one question:
Given a product definition, meter readings, and statutory rates — what does the customer's invoice look like?
Architecture
billingd (HTTP service)
│ tarifbd/edmd/marktd clients · HTTP endpoints
│ XRechnung 3.0 / ZUGFeRD 2.3 · PostgreSQL · CloudEvents
│
└── energy-billing (pure crate)
│
├── Product — typed enum with 12 per-category variants
│ ├── Strom(ElectricityProduct)
│ ├── Waermepumpe/Wallbox(ControllableLoadProduct) §14a
│ ├── Gas(GasProduct)
│ ├── Waerme(HeatProduct)
│ ├── Solar(SolarProduct)
│ ├── Eeg(EegProduct)
│ ├── Einspeisung(EinspeisungProduct)
│ ├── Hems/Emobility/Energiedienstleistung(…)
│ └── Sharing(SharingProduct) §42c
│
├── Quantities — all meter inputs for one billing period
├── BillingContext — period, IDs, invoice type, regulatory rates
├── BillingEngine — composes BillingProvider instances
│ ├── validate() — pre-flight regulatory check (no positions)
│ ├── bill(&self, …) — pure function → Result<Invoice, BillingError>
│ └── bill_batch(…) — portfolio billing
├── BillingProvider — one implementation per product/tax type
└── Invoice — result with positions + totals + warnings + BO4E JSON
├── warnings: Vec<BillingWarning> — regulatory compliance notices
├── has_errors() — any Error-severity warning?
└── to_rechnung_json() — BO4E JSONB for accountingd
The engine runs in passes:
Pass 0 validate_warnings() §41b iMSys guard · regulatory pre-checks
Pass 1 commodity / levy providers (ElectricityProvider, GasProvider, …)
Pass 2 tax provider (MwStProvider — sees all net positions)
Pass 3 Abschlag deductions (Final invoice reconciliation)
Pass 4 Minimum invoice top-up (B2B Mindestabnahmeverpflichtung)
Pass 5 Cancellation sign reversal (Stornorechnung — all signs negated)
Quick start
use ;
use dec;
use date;
// Deserialize directly from tarifbd JSONB using the "category" discriminator
let product: Product = from_str?;
let ctx = BillingContext ;
let quantities = Quantities ;
// Build and run — Product::build_engine() handles all category routing
let invoice = product
.build_engine
.bill?;
invoice.assert_valid;
println!;
let rechnung_json: Value = invoice.to_rechnung_json;
Product enum
Product is the typed dispatch enum that replaces the old flat TariffInput god-struct.
Each category has its own struct with only the relevant fields — no silent field confusion.
// Deserializes via #[serde(tag = "category")] from flat tarifbd JSONB:
// {"category":"STROM","arbeitspreis_ct_per_kwh":28.5} → Product::Strom(ElectricityProduct{...})
// {"category":"WAERMEPUMPE","sect14a_modul1_nne_reduktion_ct_per_kwh":1.5,...} → Product::Waermepumpe(...)
// {"category":"GAS","gas_arbeitspreis_ct_per_kwh_hs":7.5,...} → Product::Gas(GasProduct{...})
Product variant |
Category string | Provider | Key features |
|---|---|---|---|
Strom(ElectricityProduct) |
STROM |
ElectricityProvider or DynamicElectricityProvider |
SLP/RLM; HT/NT; block tariffs; §41a EPEX |
Waermepumpe(ControllableLoadProduct) |
WAERMEPUMPE |
ControllableLoadProvider |
§14a Modul 1/3 mandatory |
Wallbox(ControllableLoadProduct) |
WALLBOX |
ControllableLoadProvider |
§14a Modul 1/3 mandatory |
Gas(GasProduct) |
GAS |
GasProvider |
Brennwertkorrektur; Energiesteuer; BEHG CO₂ |
Waerme(HeatProduct) |
WAERME |
HeatProvider |
Fernwärme; auto-7% MwSt renewable |
Solar(SolarProduct) |
SOLAR |
SolarProvider |
§42b GGV; §42a Mieterstrom; 0% MwSt ≤30 kWp |
Eeg(EegProduct) |
EEG |
EegProvider |
LF-side Gutschrift; eeg feature for §51/§52 |
Einspeisung(EinspeisungProduct) |
EINSPEISUNG |
EinspeisungProvider |
Direktvermarktung Marktwert − Gebühr |
Hems(HemsProduct) |
HEMS |
HemsProvider |
Platform subscription + events |
Emobility(EmobilityProduct) |
EMOBILITY |
EmobilityProvider |
CPO/EMSP: service + kWh + session/roaming |
Energiedienstleistung(ServiceProduct) |
ENERGIEDIENSTLEISTUNG |
ServiceProvider |
Flat fee + per-event |
Sharing(SharingProduct) |
SHARING |
ElectricityProvider + EnergyShareProvider |
§42c Energiegemeinschaft credit |
ControllableLoadProduct composes ElectricityProduct (via #[serde(flatten)]) plus §14a fields — the standard electricity billing is delegated to ElectricityProvider then §14a credits are appended.
Pricing capabilities
| Feature | How |
|---|---|
| HT/NT Zweitarif | billing::TimeOfUsePricing (validated, penny-correct) |
| Block / graduated tariffs | billing::TariffSchedule::graduated() |
| Indexed prices (TTF, Phelix, NCG) | IndexedPriceConfig { base_ct, spread_ct, index_value, factor } |
| Gas indexed price | gas_indexed_price: Option<IndexedPriceConfig> in GasProduct |
| Seasonal prices | SeasonalPriceOverride by month range (wraps year boundary) |
| §41a EPEX dynamic | billing::DynamicPricing with per-interval kWh × price |
| §41b iMSys guard | Hard error when dynamic_epex=true and MeteringMode != Imsys |
| Pro-rata Grundpreis | ctx.prorate_days() clips to vertragsbeginn/vertragsende |
| Minimum invoice (B2B) | Pass 4 auto-top-up to minimum_invoice_eur_brutto |
| Discounts / bonuses | auf_abschlag_ct_per_kwh, auf_abschlag_eur_per_month, Bonus category |
| MSB pass-through | msb_gebuehr_ct_per_day (MsbG) |
| Multi-rate MwSt | Per-position applicable_tax_rate → grouped MwStProvider |
| Auto-0% MwSt solar ≤30 kWp | anlage_kwp ≤ 30 (§12 Abs. 3 UStG Solarpaket I) |
| Stromsteuer exemption | StromsteuerBefreiung typed enum (§9 Nr. 1-5 + §9a) |
| Gas RLM Leistungspreis | gas_leistungspreis_ct_per_kw_month in GasProduct |
| §42 Energiemix | EnergieQuellen struct with co2_g_per_kwh (mandatory §42 Abs. 2 Nr. 2 EnWG) |
Regulatory compliance
§41b EnWG — iMSys guard for dynamic tariffs
Dynamic tariffs (Product::Strom(p) where p.dynamic_epex = true) require an intelligent
metering system. BillingEngine::bill() rejects with BillingError::InvalidInput when
quantities.electricity.metering_mode != MeteringMode::Imsys:
// Pre-flight check: validate without generating positions
let warnings = engine.validate;
for w in &warnings
// §41b violations produce BillingWarning { code: "SECT41B_IMSYS_REQUIRED", severity: Error }
§9 StromStG — typed Stromsteuer exemption
StromsteuerBefreiung is a typed enum covering all §9 StromStG exemption grounds:
Invoice types
Meter inputs
Key regulatory fields per product
ElectricityProduct / ControllableLoadProduct
| Field | Law | Effect |
|---|---|---|
anlage_kwp |
§12 Abs. 3 UStG | Auto-0% MwSt when ≤ 30 kWp (Solarpaket I 2023) |
stromsteuer_befreiung |
§9 StromStG | Typed enum; replaces levy with exemption notice |
industrie_stromsteuer_befreiung |
§9 Nr. 4 StromStG | Legacy bool; prefer stromsteuer_befreiung |
leistungspreis_strom_ct_per_kw_month |
§41 EnWG | RLM demand charge (ct/kW/month) |
preisgarantie_bis |
§41 Abs. 1 Nr. 4 EnWG | Price guarantee expiry on invoice |
mwst_rate_override |
§12 UStG | Override 19% per product |
dynamic_epex |
§41a EnWG | EPEX spot billing (requires MeteringMode::Imsys) |
dynamic_epex_floor_ct_kwh |
§41a EnWG | Price floor for spot pass-through |
energiequellen |
§42 Abs. 2 Nr. 2 EnWG | Typed fuel mix with CO₂ label |
ControllableLoadProduct (§14a extras)
| Field | Law | Effect |
|---|---|---|
sect14a_modul1_nne_reduktion_ct_per_kwh |
§14a EnWG | Per-kWh NNE credit |
steuerungsrabatt_modul1_eur_per_kw_year |
§14a EnWG | Capacity NNE reduction |
sect14a_modul3_entschaedigung_ct_per_kwh |
§14a EnWG | Per-kWh Entschädigung |
steuerungsrabatt_modul3_eur_per_kw_year |
§14a EnWG | Capacity Entschädigung |
GasProduct
| Field | Law | Effect |
|---|---|---|
gas_energiesteuer_befreiung |
§54 EnergieStG | KWK / industrial exemption notice |
gas_leistungspreis_ct_per_kw_month |
§41 EnWG | RLM demand charge for large gas customers |
gas_indexed_price |
§41 Abs. 3 EnWG | B2B TTF/NCG indexed price (alias: indexed_price) |
Advanced operations
Tarifwechsel — mid-period price change
// Old tariff: Jan 1–14
let inv_old = old_product.build_engine.bill?;
// New tariff: Jan 15–31
let inv_new = new_product.build_engine.bill?;
// Combined January invoice
let merged = inv_old.merge;
Portfolio billing
let engine = product.build_engine;
let results: = engine.bill_batch;
Regulatory pre-flight
let engine = product.build_engine;
let warnings = engine.validate;
if invoice.has_errors
Proportional cost allocation (B2B shared buildings)
let parts = building_invoice.allocate_proportionally?;
// Guaranteed: parts[0].brutto + parts[1].brutto + parts[2].brutto == original.brutto
§41a Abs. 6 annual savings comparison
let comparison = compute;
// Rendered as Info position on the annual invoice
Optional features
= { = "…", = ["eeg"] } # full eeg-billing accuracy
= { = "…", = ["full"] } # all optional features
| Feature | Enables |
|---|---|
eeg |
EegProvider delegates to eeg_billing::calculate_settlement() for §51/§52/§36k |
Note: The
bo4e/rubo4edependency has been removed fromenergy-billing.Invoice::to_rechnung_json()produces BO4E-compatible JSON without any external dependency. For typedrubo4e::current::Rechnungoutput, convert the JSON inbillingd's service layer.
Audit trail and explainability
Every BillingPosition carries a PositionTrace with the full calculation audit:
The BillingWarning field on Invoice carries regulatory compliance notices:
// Check for dispatch-blocking violations
if invoice.has_errors
Regulatory basis
| Law | Coverage |
|---|---|
| §3 StromStG | Stromsteuer 2.05 ct/kWh; stromsteuer_for_year(year) for retroactive corrections |
| §9 StromStG | All 5 exemption grounds + §9a via typed StromsteuerBefreiung enum |
| §2 EnergieStG | Erdgassteuer 0.55 ct/kWh; energiesteuer_gas_for_year(year) (incl. 2022 0-rate) |
| §54 EnergieStG | KWK / industrial gas Energiesteuer exemption |
| BEHG §10 | CO₂-Preis H-Gas (65 EUR/t 2026) + L-Gas factor; behg_ct_per_kwh_for_year(year) |
| §10 GasGVV | Brennwertkorrektur m³ → kWh_Hs |
| §12 Abs. 2 Nr. 1 UStG | Reduced 7% MwSt for renewable Fernwärme |
| §12 Abs. 3 UStG | 0% MwSt for PV ≤ 30 kWp (Solarpaket I, since 01.01.2023) |
| §14a EnWG | Controllable loads Modul 1/3 (BK6-24-174) via ControllableLoadProvider |
| §17 Abs. 1 MessZV | Estimated reading notice on invoice |
| §40a / §40b EnWG | Mandatory ct/kWh; structured price-comparison data in JSON |
| §41 Abs. 1 EnWG | Invoice content (Zählerstand, Netzbetreiber, Preisgarantie, Energiemix) |
| §41 Abs. 1 Nr. 3 EnWG | Verbrauchshistorie (prior-year + national average) |
| §41a / §41b EnWG | §41a EPEX per-interval; §41b iMSys guard enforced as hard error |
| §42 Abs. 2 Nr. 2 EnWG | CO₂ emissions label via typed EnergieQuellen.co2_g_per_kwh |
| §42b / §42a EEG 2023 | Mieterstrom / Gemeinschaftliche Gebäudeversorgung |
| §42c EnWG | Energiegemeinschaft sharing credit via SharingProduct |
| §51 EEG 2023 | Negativpreisregel (contractual LF feature via eeg feature) |
Testing
160 tests across five suites:
| Suite | Tests | Coverage |
|---|---|---|
| Unit tests (lib) | 18 | RegulatoryRates, levy lookups, prorate_days, InvoiceType, Product enum roundtrip, StromsteuerBefreiung, tariff deserialization |
calculator_tests |
108 | All 12 categories, §14a/§41a/§41b, GGV, seasonal, indexed, prosumer, block tariffs, RLM demand charge, multi-rate MwSt, cancellation, BO4E JSON, pro-rata, Tarifwechsel, bill_batch, validate |
golden_scenarios |
11 | Golden master: SLP electricity; gas + levies; EEG Gutschrift; RLM demand charge; §54 KWK exemption; 2022 0-rate; §41b rejection; §40a ct/kWh; §41 mandatory fields; §42c sharing; §9 exemption |
proptest_invoice |
8 | Property-based: brutto == netto + mwst, cancellation sign, 0% MwSt, gas arithmetic, demand charge non-negative, StromStG year table |
| Doc tests | 15 | Inline usage examples |