Skip to main content

invoic_checker/
lib.rs

1//! Automated INVOIC plausibility and tariff validation, over BO4E.
2//!
3//! An invoice recipient — an LF, an NB or an ESA — receives INVOIC messages
4//! (PIDs 31001–31011) from NB/GNB/MSB/BIKO counterparties for grid fees (NNE),
5//! meter charges, Leistungen des Preisblatts B and Mehr-/Mindermengen (MMM)
6//! settlement. This library runs automated business-rule checks over BO4E
7//! [`Rechnung`][rubo4e::current::Rechnung] objects — the industry-standard German
8//! energy domain model — and produces a [`CheckReport`] that drives the REMADV /
9//! dispute workflow in `invoicd`.
10//!
11//! # Boundary with `mako-pruefung`
12//!
13//! `mako-pruefung` decides **published BDEW Antwortcodes** for the wire and
14//! knows nothing but Prüfschritte. This crate decides mako's own [`Finding`]s
15//! for the operator queue and the § 147 AO receipt, over BO4E, for **every**
16//! INVOIC PID — including those with no Entscheidungsbaum. The dependency runs
17//! one way: this crate maps BO4E onto [`mako_pruefung::rechnung`]'s Prüfschritte
18//! and calls the walk, which holds no BO4E or money type. See the README for
19//! why they are not one crate.
20//!
21//! Where a plausibility check asks the same question as a Prüfschritt —
22//! position arithmetic against `A20`, the document total against `A24`, the tax
23//! breakdown against `A22`/`A23` — **both paths read the same tolerance**:
24//! Summen-level `total_tolerance_ppm`, position-level
25//! `arithmetic_tolerance_ppm`. Two knobs for one question would let the engine
26//! record a `TotalMismatch` Dispute while the walk dispatched a Zahlungsavis.
27//!
28//! ```text
29//! EDIFACT INVOIC segments
30//!   → [makod adapter: anti-corruption layer]
31//!   → BO4E Rechnung            — industry-standard domain model, stored in events
32//!   → InvoicCheckEngine::check — pure business rules, no EDIFACT dependency
33//!   → CheckReport { Ok | Warn | Dispute }
34//!       → REMADV auto-dispatch or dispute workflow
35//! ```
36//!
37//! # Design principles
38//!
39//! - **Format-agnostic**: zero dependency on `edifact-rs`. Operates solely on
40//!   the BO4E domain model. EDIFACT → BO4E translation belongs in the `makod`
41//!   transport adapter (anti-corruption layer).
42//! - **Pure library** — no I/O, no async, no Tokio dependency.
43//! - **Trait-injected stores** — [`PreisblattStore`] is injected by the caller
44//!   (e.g. `invoicd` injects an in-memory store seeded from `marktd`'s price-sheet API).
45//! - **No floating-point money** — all amounts are [`EuroAmount`] (`i64` ×10⁻⁵ EUR).
46//!
47//! # Monetary precision
48//!
49//! [`EuroAmount`] stores values as `i64` in units of 10⁻⁵ EUR (1/100 000 EUR):
50//! - `EuroAmount(100_000)` = 1.00000 EUR
51//! - `EuroAmount(3_456)`   = 0.03456 EUR (typical NNE unit price per kWh)
52//!
53//! This gives five decimal places — sufficient for all BDEW INVOIC precision
54//! requirements (NNE unit prices: typically 4 decimal places).
55//!
56//! # Example
57//!
58//! ```rust,no_run
59//! use invoic_checker::{
60//!     check::{CheckConfig, CheckOutcome, InvoicCheckEngine},
61//!     tariff::InMemoryPreisblattStore,
62//!     amount::EuroAmount,
63//! };
64//! use rubo4e::current::{PreisblattNetznutzung, Rechnung};
65//!
66//! let preisblatt_store = InMemoryPreisblattStore::default();
67//!
68//! // A `Rechnung` that states no Umsatzsteuer is disputed: §14 Abs. 4 Nr. 8
69//! // UStG makes the rate and the amount mandatory, and without them the
70//! // recipient has no Vorsteuerabzug.
71//! let rechnung = Rechnung::default();
72//! let report = InvoicCheckEngine::check(
73//!     31001,
74//!     "9900357000004",
75//!     &rechnung,
76//!     &preisblatt_store,
77//!     &CheckConfig::default(),
78//! );
79//! assert_eq!(report.outcome, CheckOutcome::Dispute);
80//! ```
81#![deny(unsafe_code)]
82
83pub mod amount;
84pub mod check;
85pub mod error;
86pub mod rechnung;
87pub mod tariff;
88
89// ── Convenient re-exports ─────────────────────────────────────────────────────
90
91pub use amount::EuroAmount;
92pub use check::{
93    CheckConfig, CheckOutcome, CheckReport, Finding, FindingKind, InvoicCheckEngine, is_stornierung,
94};
95pub use error::CheckError;
96pub use rechnung::{
97    EmpfaengerFakten, StornoEmpfaengerFakten, antwort_auf_erneute_rechnung, antwort_auf_rechnung,
98    antwort_auf_stornorechnung,
99};
100pub use tariff::{InMemoryPreisblattStore, PreisblattStore};