grid-billing
Role-neutral NNE/KA/MMM/MSB grid invoice calculation library for German energy market communication (BDEW MaKo).
What this crate does
grid-billing computes BDEW INVOIC billing positions for:
- NNE (Netznutzungsentgelt) — flat-rate or §14a Modul 2 ToU (HT/NT split)
- KA (Konzessionsabgabe) — §17 StromNZV, included as separate position
- MMM (Mehr-/Mindermengensaldo) — actual vs. SLP profile deviation, credit when Mindermengen dominate
- MSB-Rechnung — metering service fee (NB → MSB, PID 31009)
All calculations are pure functions — zero I/O, zero async, no side effects.
All monetary arithmetic uses EuroAmount = i64 × 10⁻⁵ EUR — no f64 anywhere in the billing path.
Architecture: domain types, no BO4E dependency
grid-billing returns GridInvoice — a pure domain type with no dependency on rubo4e.
The service layer (netzbilanzd, invoicd) owns the into_rechnung() conversion to
rubo4e::current::Rechnung for EDIFACT serialization and invoic-checker validation.
grid-billing::calculate_nne_invoice(NneInput) → GridInvoice { positions, total_eur, pid, … }
│
▼
netzbilanzd::into_rechnung(&invoice) → Rechnung
│
▼
InvoicCheckEngine::check(pid, &rechnung, …)
│
▼
invoice_drafts (PostgreSQL) → AS4 dispatch
This separation makes grid-billing publishable to crates.io without pulling in the internal
rubo4e crate.
Who uses this library
| Consumer | Role | Use case |
|---|---|---|
netzbilanzd |
NB | Generate INVOIC 31001/31002/31005/31009/31011 to send to LF/MSB/LFG |
invoicd |
LF | §20 MessZV selbstausstellen PID 31006 — LF runs the same formula the NB would use |
This dual-role usage is intentional: under §20 MessZV selbstausgestellt invoicing, the LF runs the identical formula independently. The calculation is symmetric.
Generated invoice types
| PID | Description | Direction | billing_type in netzbilanzd |
|---|---|---|---|
| 31001 | NNE Strom | NB → LF | nne_strom |
| 31002 | MMM Strom / Gas | NB → LF · GNB → LFG | mmm_strom, mmm_gas |
| 31005 | NNE Gas | GNB → LFG | nne_gas |
| 31006 | Selbst ausgestellte NNE | LF → LF internal | invoicd only |
| 31009 | MSB-Rechnung | NB → MSB | msb_31009 |
| 31011 | AWH Sperrprozesse Gas | GNB → LFG | nne_gas_awh_31011 (PID override on GridInvoice.pid) |
Output type: GridInvoice
Design invariants
| Invariant | Detail |
|---|---|
| No floating-point money | All amounts use EuroAmount (i64 × 10⁻⁵ EUR). No f64 in billing path. |
| No rubo4e dependency | Returns GridInvoice domain types. Service layer owns into_rechnung(). |
| PID mutable | GridInvoice.pid is pub — caller sets 31005 for Gas, 31011 for AWH Gas. |
| Decimal-only tariff input | All tariff rates are rust_decimal::Decimal via from_str_exact. Never Decimal::try_from(f64). |
| Pure functions | calculate_nne_invoice, calculate_mmm_invoice, calculate_msb_invoice are synchronous with no side effects. |
Quick start
[]
= { = "0.10" }
= "1"
= "0.3"
NNE flat-rate (SLP)
use ;
use Decimal;
use date;
let invoice = calculate_nne_invoice.expect;
// invoice.total_eur == "435.15" (1500 × 0.285 + 1500 × 0.0011)
assert_eq!; // Arbeit + KA
§14a Modul 2 ToU (HT/NT split)
use ;
let invoice = calculate_nne_invoice.unwrap;
// Two separate Arbeit positions: "Netznutzung Arbeit HT (§14a Modul 2)" + NT
assert_eq!;
MMM settlement (Mehr-/Mindermengen)
use ;
let mut invoice = calculate_mmm_invoice.unwrap;
// Override PID for Gas MMM:
// invoice.pid = 31002 by default; set 31005 for Gas if needed via outer context
assert_eq!; // 100 kWh Mehrmengen × 0.04 EUR/kWh
// Mindermengen: negative total = NB owes LF
// actual = 1400, profil = 1500 → total = -2.00 EUR (credit)
MSB-Rechnung (PID 31009)
use ;
let invoice = calculate_msb_invoice.unwrap;
// 12 × 9.50 + 24.00 = 138.00 EUR
assert_eq!;
assert_eq!;
Service-layer conversion to BO4E Rechnung
The caller (netzbilanzd, invoicd) does the mapping — grid-billing stays BO4E-free:
// In netzbilanzd/src/billing.rs:
use ;
use ;
// Then validate:
let rechnung = into_rechnung;
let report = check;
Billing position table
| Position | Label | Unit | Condition |
|---|---|---|---|
| 1 | Netznutzung Arbeit |
kWh | SLP / RLM flat-rate |
| 1 + 2 | Netznutzung Arbeit HT/NT (§14a Modul 2) |
kWh each | When arbeitsmenge_ht_kwh + arbeitspreis_ht_ct_per_kwh are both set |
| next | Netznutzung Leistung |
kW | When spitzenleistung_kw + leistungspreis_eur_per_kw are both set (RLM) |
| last | Konzessionsabgabe |
kWh | When ka_satz_ct_per_kwh is set |
All amounts are rounded to 5 decimal places per position; total rounded to 2 decimal places.
See also
invoic-checker— validates the generatedRechnungin the service layernetzbilanzd— NB billing service that callsgrid-billinginvoicd— LF service usinggrid-billingfor selbstausstellen