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