Skip to main content

Crate grid_billing

Crate grid_billing 

Source
Expand description

Role-neutral German grid settlement calculation engine.

Covers all DSO/TSO-side INVOIC documents:

  • NNE (Netznutzungsentgelt) — PID 31002 (NN-Rechnung, Strom + Gas); Abschlag 31001
  • MMM (Mehr-/Mindermengensaldo) — PIDs 31005/31006 (aggregiert Gas 31007/31008)
  • MSB (Messstellenbetrieb) — PID 31009

§Calculation flow

Input → validation → Settlement Engine → SettlementResult → into_rechnung() (service layer)

SettlementResult is the canonical output. The service layer (netzbilanzd, invoicd) converts it to BO4E Rechnung. This keeps grid-billing publishable to crates.io without pulling in the internal rubo4e crate.

§Explainability

Every SettlementPosition carries a CalculationTrace with:

  • input values (quantity, unit price)
  • gross intermediate result before rounding
  • applicable LegalReferences (e.g. StromNEV §17, KAV §2)
  • the TariffSource justifying each rate

§No float money

Quantities, rates, and factors are rust_decimal::Decimal; every EUR result is range-checked through the billing::EuroAmount newtype (Amount<5>) before it leaves the crate.

§Example

use grid_billing::{
    ArbeitspreisModell, InvoiceDocument, KaKundengruppe, Konzessionsabgabe, MengePreis,
    NneInput, SettlementPeriod, settle_nne,
};
use rust_decimal::Decimal;
use time::macros::date;

fn d(s: &str) -> Decimal { Decimal::from_str_exact(s).unwrap() }

// The engine is given what was supplied and at what rates — no invoice
// number, no issue date, no Prüfidentifikator.
let settlement = settle_nne(&NneInput {
    malo_id: "51238696780".into(),
    nb_mp_id: "9900357000004".into(),
    lf_mp_id: "9900012345678".into(),
    period: SettlementPeriod::new(date!(2025-01-01), date!(2025-01-31))?,
    // One value, not twelve loose fields: flat rate, §14a Modul 1, Modul 2
    // HT/NT and Modul 3 are mutually exclusive by construction.
    arbeitspreis: ArbeitspreisModell::Einheitlich(MengePreis {
        menge_kwh: d("1500"),
        preis_ct_per_kwh: d("3.5"),
    }),
    leistungspreis: None,
    letztverbrauchergruppe: Default::default(),
    sect19_umlage_ct_per_kwh: None,
    offshore_umlage_ct_per_kwh: None,
    kwkg_umlage_ct_per_kwh: None,
    grundpreis: None,
    // Recorded so an auditor can check the rate came from the right sheet.
    netzebene: Some(grid_billing::netzebene::Netzebene::Niederspannung),
    sect19: None,
    gas_kapazitaet: None,
    jahreshoechstleistung_kw: None,
    jahresarbeit_kwh: Some(d("18000")),
    // Rate and customer group travel together, so the KAV §2 Höchstbetrag is
    // always checked.
    konzessionsabgabe: Some(Konzessionsabgabe {
        satz_ct_per_kwh: d("0.11"),
        klasse: KaKundengruppe::Sondervertragskunde,
    }),
    tariff_sheet_id: Some("Preisblatt-NNE-2025-Q1".into()),
    sparte: grid_billing::Sparte::Strom,
})?;

// Every position explains itself:
for pos in &settlement.positions {
    println!("{}: {}", pos.text, pos.trace.explanation);
}
for r in settlement.all_legal_refs() {
    println!("  → {r}");
}

// Presenting it as an invoice is a separate step, and the only place
// document identity enters.
let document = InvoiceDocument {
    settlement,
    pid: 31002,
    rechnungsnummer: "NNE-2025-001".into(),
    correction_of: None,
    invoice_date: date!(2025-02-15),
    due_date: date!(2025-03-15),
};
for (number, pos) in document.numbered_positions() {
    println!("{number}. {}", pos.text);
}

Re-exports§

pub use billing::correct;
pub use billing::reverse;
pub use billing::settle_gas_awh;
pub use billing::settle_mmm;
pub use billing::settle_msb;
pub use billing::settle_nne;
pub use error::BillingError;
pub use redispatch::RedispatchVerguetung;
pub use redispatch::RedispatchVerguetungInput;
pub use redispatch::RedispatchVerguetungsart;
pub use redispatch::bilarem_finanzielle_korrektur;
pub use redispatch::eeg_entgangene_einnahmen;
pub use redispatch::redispatch_verguetung;
pub use regulatory::EntgeltRegime;
pub use regulatory::NetzzugangRegime;
pub use regulatory::RegulatoryRegime;
pub use regulatory::SECT19_ABS3_LETZTER_TAG;
pub use regulatory::SECT19_ABS3_UEBERGANG_ENDE;
pub use types::ArbeitspreisModell;
pub use types::AwhPositionInput;
pub use types::BillingPositionKind;
pub use types::CalculationTrace;
pub use types::GasAwhInput;
pub use types::GemeindeGroesse;
pub use types::Grundpreis;
pub use types::InvoiceDocument;
pub use types::KaKundengruppe;
pub use types::Konzessionsabgabe;
pub use types::LegalReference;
pub use types::Leistungspreis;
pub use types::MengePreis;
pub use types::MmmInput;
pub use types::MsbInput;
pub use types::NneInput;
pub use types::PriceReference;
pub use types::PriceStep;
pub use types::QuantityUnit;
pub use types::Reduktionsfaktor;
pub use types::Sect14aModul3Interval;
pub use types::Sect14aModule;
pub use types::SettlementPeriod;
pub use types::SettlementPosition;
pub use types::SettlementResult;
pub use types::SettlementStatus;
pub use types::SettlementType;
pub use types::SettlementWarning;
pub use types::Sparte;
pub use types::SpotPriceFormula;
pub use types::TariffCalculationMethod;
pub use types::TariffSource;
pub use types::ValidationResult;
pub use types::WarningSeverity;
pub use types::validate_gas_awh_input;
pub use types::validate_mmm_input;
pub use types::validate_msb_input;

Modules§

billing
NNE, MMM, and MSB settlement calculation logic.
error
Error types for grid-billing.
gas
Gas network structure — Druckstufen and Kapazitätsprodukte, §15 GasNEV.
msbg
Preisobergrenzen für den Messstellenbetrieb — §30 MsbG.
netzebene
Netzebenen und Entgeltsystematik — §17 StromNEV.
redispatch
§13a EnWG Redispatch 2.0 compensation (angemessene Vergütung).
regulatory
Which rules were in force for a delivery period.
sect18
Entgelte für dezentrale Erzeugung — §18 StromNEV, under Abschmelzung.
sect19
Individuelle Netzentgelte — §19 Abs. 2 StromNEV.
types
Input/output types for the settlement calculation engine.
umlagen
Netzseitige Umlagen — the levies the ÜNB collect through the Netzentgelt.