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//!     // A′ has no EnFG 1-GWh boundary, so the year to date does not place it.
61//!     enfg_jahresvorverbrauch_kwh: None,
62//!     sect19_umlage_ct_per_kwh: None,
63//!     offshore_umlage_ct_per_kwh: None,
64//!     kwkg_umlage_ct_per_kwh: None,
65//!     grundpreis: None,
66//!     // Recorded so an auditor can check the rate came from the right sheet.
67//!     netzebene: Some(grid_billing::netzebene::Netzebene::Niederspannung),
68//!     sect19: None,
69//!     gas_kapazitaet: None,
70//!     jahreshoechstleistung_kw: None,
71//!     jahresarbeit_kwh: Some(d("18000")),
72//!     // Rate and customer group travel together, so the KAV §2 Höchstbetrag is
73//!     // always checked.
74//!     konzessionsabgabe: Some(Konzessionsabgabe {
75//!         satz_ct_per_kwh: d("0.11"),
76//!         klasse: KaKundengruppe::Sondervertragskunde,
77//!         // § 2 Abs. 7 classification facts and the Abs. 4 Grenzpreisvergleich,
78//!         // where the settlement holds them.
79//!         niederspannung: None,
80//!         grenzpreis: None,
81//!     }),
82//!     tariff_sheet_id: Some("Preisblatt-NNE-2025-Q1".into()),
83//!     sparte: grid_billing::Sparte::Strom,
84//! })?;
85//!
86//! // Every position explains itself:
87//! for pos in &settlement.positions {
88//!     println!("{}: {}", pos.text, pos.trace.explanation);
89//! }
90//! for r in settlement.all_legal_refs() {
91//!     println!("  → {r}");
92//! }
93//!
94//! // Presenting it as an invoice is a separate step, and the only place
95//! // document identity enters.
96//! let document = InvoiceDocument {
97//!     settlement,
98//!     pid: 31002,
99//!     rechnungsnummer: "NNE-2025-001".into(),
100//!     correction_of: None,
101//!     invoice_date: date!(2025-02-15),
102//!     due_date: date!(2025-03-15),
103//!     // Cadence is a document fact (`IMD+7081`), and Abschläge are deducted
104//!     // from what is owed rather than from what was supplied.
105//!     cadence: Some(grid_billing::Rechnungscharakter::Monatsrechnung),
106//!     abschlaege: Vec::new(),
107//! };
108//! for (number, pos) in document.numbered_positions() {
109//!     println!("{number}. {}", pos.text);
110//! }
111//! # Ok::<(), grid_billing::BillingError>(())
112//! ```
113#![deny(unsafe_code)]
114#![warn(missing_docs)]
115
116pub mod billing;
117/// BO4E bridge (feature `bo4e`): [`crate::InvoiceDocument`] → `rubo4e` Rechnung.
118#[cfg(feature = "bo4e")]
119pub mod bo4e;
120pub mod error;
121pub mod gas;
122pub mod msbg;
123pub mod netzebene;
124pub mod redispatch;
125pub mod regulatory;
126pub mod rounding;
127pub mod sect18;
128pub mod sect19;
129pub mod types;
130pub mod umlagen;
131pub mod umsatzsteuer;
132
133/// A monetary amount in euro at 10⁻⁵-EUR resolution.
134///
135/// `billing` 0.12 dropped its own `EuroAmount` alias — the engine is
136/// currency-agnostic and the name asserted a currency the type does not carry.
137/// Netznutzungsentgelte are euro-denominated by statute, so the alias is correct
138/// here; it just belongs to the domain crate rather than the engine.
139pub type EuroAmount = ::billing::Amount<5>;
140
141pub use billing::{
142    correct, reverse, settle_abschlag, settle_gas_awh, settle_mmm, settle_msb, settle_nne,
143};
144pub use error::BillingError;
145pub use gas::GasKapazitaet;
146pub use redispatch::{
147    AusfallarbeitBasis, RedispatchVerguetung, RedispatchVerguetungInput, RedispatchVerguetungsart,
148    bilarem_finanzielle_korrektur, eeg_entgangene_einnahmen, redispatch_verguetung,
149};
150pub use regulatory::{
151    // Which rules were in force for the delivery period — resolved once, at
152    // the edge, and recorded on every SettlementResult.
153    EntgeltRegime,
154    NetzzugangRegime,
155    RegulatoryRegime,
156    SECT19_ABS3_LETZTER_TAG,
157    SECT19_ABS3_UEBERGANG_ENDE,
158};
159pub use rounding::RoundMoney;
160pub use types::{
161    // Abschlagsrechnung — a payment on account, and its later deduction.
162    AbschlagGrundlage,
163    AbschlagInput,
164    Abschlagsverrechnung,
165    // The settlement — what is owed and why.
166    ArbeitspreisModell,
167    // AWH positions
168    AwhPositionInput,
169    // BDEW Artikelnummer bridge — maps SettlementPosition.kind → BdewArtikelnummer in service layer
170    BillingPositionKind,
171    // Reactive energy and the terms its excess is charged on.
172    Blindarbeit,
173    // Domain types for explainability + audit
174    CalculationTrace,
175    // Input types
176    GasAwhInput,
177    GemeindeGroesse,
178    Grundpreis,
179    // Presenting a settlement as an invoice: numbers, dates, Prüfidentifikator.
180    InvoiceDocument,
181    Jahresanteil,
182    KaKundengruppe,
183    Konzessionsabgabe,
184    KorrekturGrund,
185    LegalReference,
186    Leistungspreis,
187    // Which of the two §17 StromNEV Leistungspreissysteme a price sheet states.
188    LeistungspreisSystem,
189    MengePreis,
190    MmmInput,
191    MsbEmpfaengerRolle,
192    MsbInput,
193    MsbRechnungsempfaenger,
194    NneInput,
195    // The pricing formula behind a rate, as a value rather than a document.
196    PriceReference,
197    PriceStep,
198    QuantityUnit,
199    Rechnungscharakter,
200    Reduktionsfaktor,
201    // §14a module type (replaces module: u8 for type safety)
202    Sect14aModule,
203    SettlementPeriod,
204    SettlementPosition,
205    SettlementResult,
206    SettlementStatus,
207    SettlementType,
208    SettlementWarning,
209    Sparte,
210    SpotPriceFormula,
211    SpotpreisInterval,
212    TariffCalculationMethod,
213    TariffSource,
214    ValidationResult,
215    WarningSeverity,
216    // Validation functions
217    validate_gas_awh_input,
218    validate_mmm_input,
219    validate_msb_input,
220};
221pub use umsatzsteuer::{
222    Leistungsart, Steuerausweis, TaxCategory, Wiederverkaeuferstatus, regelsatz_prozent,
223    steuerausweis,
224};