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) — PIDs 31001, 31005, 31006, 31011
5//! - **MMM** (Mehr-/Mindermengensaldo) — PID 31002
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//! All amounts use `rust_decimal::Decimal` and the `billing::EuroAmount` newtype.
29//!
30//! ## Example
31//!
32//! ```rust,no_run
33//! use grid_billing::{
34//!     ArbeitspreisModell, InvoiceDocument, KaKundengruppe, Konzessionsabgabe, MengePreis,
35//!     NneInput, SettlementPeriod, settle_nne,
36//! };
37//! use rust_decimal::Decimal;
38//! use time::macros::date;
39//!
40//! fn d(s: &str) -> Decimal { Decimal::from_str_exact(s).unwrap() }
41//!
42//! // The engine is given what was supplied and at what rates — no invoice
43//! // number, no issue date, no Prüfidentifikator.
44//! let settlement = settle_nne(&NneInput {
45//!     malo_id: "51238696780".into(),
46//!     nb_mp_id: "9900357000004".into(),
47//!     lf_mp_id: "9900012345678".into(),
48//!     period: SettlementPeriod::new(date!(2025-01-01), date!(2025-01-31))?,
49//!     // One value, not twelve loose fields: flat rate, §14a Modul 1, Modul 2
50//!     // HT/NT and Modul 3 are mutually exclusive by construction.
51//!     arbeitspreis: ArbeitspreisModell::Einheitlich(MengePreis {
52//!         menge_kwh: d("1500"),
53//!         preis_ct_per_kwh: d("3.5"),
54//!     }),
55//!     leistungspreis: None,
56//!     letztverbrauchergruppe: Default::default(),
57//!     sect19_umlage_ct_per_kwh: None,
58//!     offshore_umlage_ct_per_kwh: None,
59//!     kwkg_umlage_ct_per_kwh: None,
60//!     grundpreis: None,
61//!     // Recorded so an auditor can check the rate came from the right sheet.
62//!     netzebene: Some(grid_billing::netzebene::Netzebene::Niederspannung),
63//!     sect19: None,
64//!     gas_kapazitaet: None,
65//!     jahreshoechstleistung_kw: None,
66//!     jahresarbeit_kwh: Some(d("18000")),
67//!     // Rate and customer group travel together, so the KAV §2 Höchstbetrag is
68//!     // always checked.
69//!     konzessionsabgabe: Some(Konzessionsabgabe {
70//!         satz_ct_per_kwh: d("0.11"),
71//!         klasse: KaKundengruppe::Sondervertragskunde,
72//!     }),
73//!     tariff_sheet_id: Some("Preisblatt-NNE-2025-Q1".into()),
74//!     sparte: grid_billing::Sparte::Strom,
75//! })?;
76//!
77//! // Every position explains itself:
78//! for pos in &settlement.positions {
79//!     println!("{}: {}", pos.text, pos.trace.explanation);
80//! }
81//! for r in settlement.all_legal_refs() {
82//!     println!("  → {r}");
83//! }
84//!
85//! // Presenting it as an invoice is a separate step, and the only place
86//! // document identity enters.
87//! let document = InvoiceDocument {
88//!     settlement,
89//!     pid: 31001,
90//!     rechnungsnummer: "NNE-2025-001".into(),
91//!     correction_of: None,
92//!     invoice_date: date!(2025-02-15),
93//!     due_date: date!(2025-03-15),
94//! };
95//! for (number, pos) in document.numbered_positions() {
96//!     println!("{number}. {}", pos.text);
97//! }
98//! # Ok::<(), grid_billing::BillingError>(())
99//! ```
100#![deny(unsafe_code)]
101#![warn(missing_docs)]
102
103pub mod billing;
104pub mod error;
105pub mod gas;
106pub mod msbg;
107pub mod netzebene;
108pub mod regulatory;
109pub mod sect18;
110pub mod sect19;
111pub mod types;
112pub mod umlagen;
113
114pub use billing::{correct, reverse, settle_gas_awh, settle_mmm, settle_msb, settle_nne};
115pub use error::BillingError;
116pub use types::{
117    // The settlement — what is owed and why.
118    ArbeitspreisModell,
119    // AWH positions
120    AwhPositionInput,
121    // BDEW Artikelnummer bridge — maps SettlementPosition.kind → BdewArtikelnummer in service layer
122    BillingPositionKind,
123    // Domain types for explainability + audit
124    CalculationTrace,
125    // Input types
126    GasAwhInput,
127    GemeindeGroesse,
128    Grundpreis,
129    // Presenting a settlement as an invoice: numbers, dates, Prüfidentifikator.
130    InvoiceDocument,
131    KaKundengruppe,
132    Konzessionsabgabe,
133    LegalReference,
134    Leistungspreis,
135    MengePreis,
136    MmmInput,
137    MsbInput,
138    NneInput,
139    // The pricing formula behind a rate, as a value rather than a document.
140    PriceReference,
141    PriceStep,
142    QuantityUnit,
143    Reduktionsfaktor,
144    Sect14aModul3Interval,
145    // §14a module type (replaces module: u8 for type safety)
146    Sect14aModule,
147    SettlementPeriod,
148    SettlementPosition,
149    SettlementResult,
150    SettlementStatus,
151    SettlementType,
152    SettlementWarning,
153    Sparte,
154    SpotPriceFormula,
155    TariffCalculationMethod,
156    TariffSource,
157    ValidationResult,
158    WarningSeverity,
159    // Validation functions
160    validate_gas_awh_input,
161    validate_mmm_input,
162    validate_msb_input,
163};