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};