Skip to main content

grid_billing/
lib.rs

1//! Role-neutral **German grid settlement calculation** engine.
2//!
3//! Covers all DSO/TSO-side INVOIC documents:
4//! - **NNE** (Netznutzungsentgelt) — PID 31002 (NN-Rechnung, Strom + Gas)
5//! - **MMM** (Mehr-/Mindermengensaldo) — PIDs 31005/31006 (aggregiert Gas 31007/31008)
6//! - **MSB** (Messstellenbetrieb) — PID 31009
7//!
8//! ## Calculation flow
9//!
10//! ```text
11//! Input → validation → Settlement Engine → SettlementResult → into_rechnung() (service layer)
12//! ```
13//!
14//! [`SettlementResult`] is the canonical output. The service layer (`netzbilanzd`,
15//! `invoicd`) converts it to BO4E `Rechnung`. This keeps `grid-billing`
16//! publishable to crates.io without pulling in the internal `rubo4e` crate.
17//!
18//! ## Explainability
19//!
20//! Every [`SettlementPosition`] carries a [`CalculationTrace`] with:
21//! - input values (quantity, unit price)
22//! - gross intermediate result before rounding
23//! - applicable [`LegalReference`]s (e.g. `StromNEV §17`, `KAV §2`)
24//! - the [`TariffSource`] justifying each rate
25//!
26//! ## No float money
27//!
28//! Quantities, rates, and factors are `rust_decimal::Decimal`; every EUR
29//! result is range-checked through the `crate::EuroAmount` newtype
30//! (`Amount<5>`) before it leaves the crate.
31//!
32//! ## Example
33//!
34//! ```rust,no_run
35//! use grid_billing::{
36//!     ArbeitspreisModell, InvoiceDocument, KaKundengruppe, Konzessionsabgabe, MengePreis,
37//!     NneInput, SettlementPeriod, settle_nne,
38//! };
39//! use rust_decimal::Decimal;
40//! use time::macros::date;
41//!
42//! fn d(s: &str) -> Decimal { Decimal::from_str_exact(s).unwrap() }
43//!
44//! // The engine is given what was supplied and at what rates — no invoice
45//! // number, no issue date, no Prüfidentifikator.
46//! let settlement = settle_nne(&NneInput {
47//!     blindarbeit: None,
48//!     malo_id: "51238696012".into(),
49//!     nb_mp_id: "9900357000004".into(),
50//!     lf_mp_id: "9900012345678".into(),
51//!     period: SettlementPeriod::new(date!(2025-01-01), date!(2025-01-31))?,
52//!     // One value, not twelve loose fields: flat rate and the three §14a
53//!     // modules are mutually exclusive by construction.
54//!     arbeitspreis: ArbeitspreisModell::Einheitlich(MengePreis {
55//!         menge_kwh: d("1500"),
56//!         preis_ct_per_kwh: d("3.5"),
57//!     }),
58//!     leistungspreis: None,
59//!     letztverbrauchergruppe: Default::default(),
60//!     sect19_umlage_ct_per_kwh: None,
61//!     offshore_umlage_ct_per_kwh: None,
62//!     kwkg_umlage_ct_per_kwh: None,
63//!     grundpreis: None,
64//!     // Recorded so an auditor can check the rate came from the right sheet.
65//!     netzebene: Some(grid_billing::netzebene::Netzebene::Niederspannung),
66//!     sect19: None,
67//!     gas_kapazitaet: None,
68//!     jahreshoechstleistung_kw: None,
69//!     jahresarbeit_kwh: Some(d("18000")),
70//!     // Rate and customer group travel together, so the KAV §2 Höchstbetrag is
71//!     // always checked.
72//!     konzessionsabgabe: Some(Konzessionsabgabe {
73//!         satz_ct_per_kwh: d("0.11"),
74//!         klasse: KaKundengruppe::Sondervertragskunde,
75//!     }),
76//!     tariff_sheet_id: Some("Preisblatt-NNE-2025-Q1".into()),
77//!     sparte: grid_billing::Sparte::Strom,
78//! })?;
79//!
80//! // Every position explains itself:
81//! for pos in &settlement.positions {
82//!     println!("{}: {}", pos.text, pos.trace.explanation);
83//! }
84//! for r in settlement.all_legal_refs() {
85//!     println!("  → {r}");
86//! }
87//!
88//! // Presenting it as an invoice is a separate step, and the only place
89//! // document identity enters.
90//! let document = InvoiceDocument {
91//!     settlement,
92//!     pid: 31002,
93//!     rechnungsnummer: "NNE-2025-001".into(),
94//!     correction_of: None,
95//!     invoice_date: date!(2025-02-15),
96//!     due_date: date!(2025-03-15),
97//!     // Cadence is a document fact (`IMD+7081`), and Abschläge are deducted
98//!     // from what is owed rather than from what was supplied.
99//!     cadence: Some(grid_billing::Rechnungscharakter::Monatsrechnung),
100//!     abschlaege: Vec::new(),
101//! };
102//! for (number, pos) in document.numbered_positions() {
103//!     println!("{number}. {}", pos.text);
104//! }
105//! # Ok::<(), grid_billing::BillingError>(())
106//! ```
107#![deny(unsafe_code)]
108#![warn(missing_docs)]
109
110pub mod billing;
111/// BO4E bridge (feature `bo4e`): [`crate::InvoiceDocument`] → `rubo4e` Rechnung.
112#[cfg(feature = "bo4e")]
113pub mod bo4e;
114pub mod error;
115pub mod gas;
116pub mod msbg;
117pub mod netzebene;
118pub mod redispatch;
119pub mod regulatory;
120pub mod sect18;
121pub mod sect19;
122pub mod types;
123pub mod umlagen;
124pub mod umsatzsteuer;
125
126/// A monetary amount in euro at 10⁻⁵-EUR resolution.
127///
128/// `billing` 0.12 dropped its own `EuroAmount` alias — the engine is
129/// currency-agnostic and the name asserted a currency the type does not carry.
130/// Netznutzungsentgelte are euro-denominated by statute, so the alias is correct
131/// here; it just belongs to the domain crate rather than the engine.
132pub type EuroAmount = ::billing::Amount<5>;
133
134pub use billing::{
135    correct, reverse, settle_abschlag, settle_gas_awh, settle_mmm, settle_msb, settle_nne,
136};
137pub use error::BillingError;
138pub use gas::GasKapazitaet;
139pub use redispatch::{
140    AusfallarbeitBasis, RedispatchVerguetung, RedispatchVerguetungInput, RedispatchVerguetungsart,
141    bilarem_finanzielle_korrektur, eeg_entgangene_einnahmen, redispatch_verguetung,
142};
143pub use regulatory::{
144    // Which rules were in force for the delivery period — resolved once, at
145    // the edge, and recorded on every SettlementResult.
146    EntgeltRegime,
147    NetzzugangRegime,
148    RegulatoryRegime,
149    SECT19_ABS3_LETZTER_TAG,
150    SECT19_ABS3_UEBERGANG_ENDE,
151};
152pub use types::{
153    // Abschlagsrechnung — a payment on account, and its later deduction.
154    AbschlagGrundlage,
155    AbschlagInput,
156    Abschlagsverrechnung,
157    // The settlement — what is owed and why.
158    ArbeitspreisModell,
159    // AWH positions
160    AwhPositionInput,
161    // BDEW Artikelnummer bridge — maps SettlementPosition.kind → BdewArtikelnummer in service layer
162    BillingPositionKind,
163    // Reactive energy and the terms its excess is charged on.
164    Blindarbeit,
165    // Domain types for explainability + audit
166    CalculationTrace,
167    // Input types
168    GasAwhInput,
169    GemeindeGroesse,
170    Grundpreis,
171    // Presenting a settlement as an invoice: numbers, dates, Prüfidentifikator.
172    InvoiceDocument,
173    KaKundengruppe,
174    Konzessionsabgabe,
175    KorrekturGrund,
176    LegalReference,
177    Leistungspreis,
178    MengePreis,
179    MmmInput,
180    MsbEmpfaengerRolle,
181    MsbInput,
182    MsbRechnungsempfaenger,
183    NneInput,
184    // The pricing formula behind a rate, as a value rather than a document.
185    PriceReference,
186    PriceStep,
187    QuantityUnit,
188    Rechnungscharakter,
189    Reduktionsfaktor,
190    // §14a module type (replaces module: u8 for type safety)
191    Sect14aModule,
192    SettlementPeriod,
193    SettlementPosition,
194    SettlementResult,
195    SettlementStatus,
196    SettlementType,
197    SettlementWarning,
198    Sparte,
199    SpotPriceFormula,
200    SpotpreisInterval,
201    TariffCalculationMethod,
202    TariffSource,
203    ValidationResult,
204    WarningSeverity,
205    // Validation functions
206    validate_gas_awh_input,
207    validate_mmm_input,
208    validate_msb_input,
209};
210pub use umsatzsteuer::{
211    Leistungsart, Steuerausweis, TaxCategory, Wiederverkaeuferstatus, regelsatz_prozent,
212    steuerausweis,
213};