Skip to main content

mako_mabis/
lib.rs

1//! `mako-mabis` — MaBiS, the German electricity balance-group settlement
2//! (BNetzA **BK6-24-174 Anlage 3**).
3//!
4//! Strom only: gas balances through GaBi Gas, on the Gastag and against a
5//! Marktgebiet.
6//!
7//! # Three shapes MaBiS does not share with the other process families
8//!
9//! ## There is no Prüfmitteilung deadline
10//!
11//! A Summenzeitreihe arrives and a Prüfmitteilung goes back, so it is natural
12//! to hang a response Frist off the arrival the way GPKE and WiM do. The
13//! Festlegung says otherwise twice: Kap. 9.8.2 Nr. 1 leaves the Frist cell
14//! **empty** and says the receiving party „kann" answer, and Kap. 13.8.2 — the
15//! section a 1-Werktag deadline is usually attributed to — defines no answer at
16//! all; its two rows are the **BIKO's own** dispatch dates.
17//!
18//! What bounds a Prüfmitteilung is the clearing window of Kap. 3.10 Tabelle 2,
19//! a date range on the Bilanzierungsmonat rather than a countdown from an
20//! arrival. See [`fristen`].
21//!
22//! ## A settlement is a sequence of versions
23//!
24//! Kap. 3.8.2: versions ascend „über die gesamte BKA". One MaBiS-Zählpunkt in
25//! one Bilanzierungsmonat receives a stream of them, each checked, corrected
26//! and superseded until the window closes. The version is the
27//! **Erstellungszeitpunkt** — 17 characters in IFTSTA `SG4 RFF+AUU` and MSCONS
28//! `SG6 DTM+293` — and it is the key both ends match on.
29//!
30//! ## 55062 / 55063 / 55064 are generic codes
31//!
32//! Eleven Summenzeitreihen share them and 55064 answers all of them out of
33//! twelve different Entscheidungsbäume; six owe an answer and five do not. The
34//! discriminator is `SG10 CCI+++ZB4` / `CAV` DE 7111 plus `SG10 CCI+6`
35//! ([`zeitreihen::zeitreihe_aus_cav`], [`zp_lifecycle::ZpSerie::from_wire`]).
36//!
37//! # Workflows
38//!
39//! | Workflow | PIDs |
40//! |---|---|
41//! | `mabis-billing` | MSCONS 13003 · 13020 · 13023; IFTSTA 21000–21005 |
42//! | `mabis-profile` | MSCONS 13010–13012; ORDERS 17211 |
43//! | `mabis-clearingliste` | UTILMD 55067 · 55069 · 55070 · 55073 |
44//! | `mabis-listenabgleich` | UTILMD 55065/55066 · 55195/55196 · 55201/55202 · 55223/55224 |
45//! | `mabis-zp-lifecycle` | UTILMD 55062–55064 · 55071/55072 · 55197–55214 · 55235–55237 |
46//! | `mabis-anforderung` | ORDERS 17201–17208 · 17210; ORDRSP 19204 |
47//!
48//! # The Kapitel-17 series expire on 30.09.2026
49//!
50//! BK6-23-241 Tenorziffer 5 repeals MaBiS Anlage 1 Kapitel 17 with the end of
51//! **30.09.2026**. Kap. 17.1 and 17.3 continue as the „Anlage zur BilAReM";
52//! Kap. **17.2** (Bilanzkreismonitoring, tägliche AAÜZ — PIDs 55197/55198) and
53//! Kap. **17.3.2.1** do not. [`zeitreihen::Familie::endet_am`] and
54//! [`zp_lifecycle::ZpSerie::endet_am`] carry the date.
55//!
56//! # Architecture
57//!
58//! Each BDEW process variant is a separate [`mako_engine::workflow::Workflow`].
59//! This crate contains **only pure domain logic** — no I/O, no EDIFACT parsing,
60//! no clock.
61//!
62//! # Example
63//!
64//! ```sh
65//! cargo run --example mabis_bilanzkreisabrechnung -p mako-mabis
66//! ```
67
68#![deny(unsafe_code)]
69#![deny(missing_docs)]
70#![warn(clippy::pedantic, clippy::must_use_candidate)]
71#![allow(clippy::module_name_repetitions)]
72#![allow(clippy::doc_markdown)] // German MaKo terms and BDEW acronyms produce many false positives
73#![allow(clippy::too_many_lines)] // process handle() functions are necessarily verbose
74#![allow(clippy::match_same_arms)] // sometimes intentional for process-family readability
75#![allow(clippy::manual_let_else)] // a `match` on a MaBiS wire code reads better than a let-else
76#![allow(clippy::redundant_closure_for_method_calls)]
77#![allow(clippy::unnested_or_patterns)]
78#![allow(clippy::map_unwrap_or)]
79#![allow(clippy::items_after_statements)]
80
81pub mod anforderung;
82pub mod bilanzkreisabrechnung;
83pub mod clearingliste;
84pub mod fristen;
85pub mod ids;
86pub mod listenabgleich;
87pub mod profile;
88pub mod summenzeitreihe;
89pub mod zeitreihen;
90pub mod zp_lifecycle;
91pub mod zp_register;
92
93pub use anforderung::{
94    ANFORDERUNG_PIDS, AbonnementVorgang, AnforderungCommand, AnforderungData, AnforderungEvent,
95    AnforderungKind, AnforderungState, MabisAnforderungWorkflow,
96    WORKFLOW_NAME as ANFORDERUNG_WORKFLOW_NAME,
97};
98pub use bilanzkreisabrechnung::{
99    AUSFALLARBEIT_PIDS, BillingCommand, BillingData, BillingEvent, BillingProjection,
100    BillingRecord, BillingState, Datenstatus, IFTSTA_ABWEISUNG_PID, IFTSTA_DATENSTATUS_PIDS,
101    IFTSTA_PIDS, IFTSTA_PRUEFMITTEILUNG_PIDS, InvalidSzrVersion, MabisBillingWorkflow,
102    Pruefergebnis, RFF_QUALIFIER_VERSION, STS_KATEGORIE_DATENSTATUS, SUMMENZEITREIHE_PID,
103    SzrVersion, VersionRecord, WORKFLOW_NAME as BILLING_WORKFLOW_NAME, ist_zeitreihen_pid,
104};
105pub use clearingliste::{
106    CLEARINGLISTE_PIDS, ClearinglisteCommand, ClearinglisteData, ClearinglisteEvent,
107    ClearinglisteKind, ClearinglisteState, MabisClearinglisteWorkflow,
108    WORKFLOW_NAME as CLEARINGLISTE_WORKFLOW_NAME,
109};
110pub use fristen::{
111    Abrechnungslauf, BIKO_DATENSTATUS_WERKTAGE, BIKO_WEITERLEITUNG_WERKTAGE, Bilanzierungsmonat,
112    CLEARING_ENDE_LABEL, Fenster, Phase, Stichtag,
113};
114pub use listenabgleich::{
115    LISTEN_FAMILIEN, ListenFamilie, ListenTyp, ListenabgleichCommand, ListenabgleichData,
116    ListenabgleichEvent, ListenabgleichState, MabisListenabgleichWorkflow,
117    WORKFLOW_NAME as LISTENABGLEICH_WORKFLOW_NAME, all_pids as listenabgleich_pids,
118};
119pub use zeitreihen::{
120    Aggregationsebene, Bezugszeitraum, CCI_BEZEICHNUNG_SUMMENZEITREIHE,
121    CCI_KLASSENTYP_VERANTWORTLICHER, Familie, KAPITEL_17_2_ENDE, Kategorie, Messtechnik, Rolle,
122    UnbekannteKategorie, Zeitreihe, aggregationsverantwortung, alle as alle_zeitreihen,
123    cav_aus_zeitreihe, cci_aus_rolle, rolle_aus_cci, zeitreihe_aus_cav,
124};
125// Canonical balance-group topology IDs (defined in `ids`).
126pub use anforderung::{ABLEHNUNG_PID, all_pids as anforderung_pids};
127pub use ids::{BilanzierungsgebietId, BilanzkreisId, InvalidMabisZaehlpunkt, MabisZaehlpunktId};
128pub use profile::{
129    Bilanzierungsverfahren, ERSTLIEFERUNG_WERKTAGE, MabisProfilWorkflow, PROFIL_PIDS,
130    ProfilCommand, ProfilData, ProfilEvent, ProfilState, Profilart, REKLAMATION_EBD,
131    REKLAMATION_PID, WORKFLOW_NAME as PROFIL_WORKFLOW_NAME, all_pids as profil_pids,
132};
133pub use summenzeitreihe::{
134    MABIS_SLOT, SlotResolutionError, SumInterval, Summenzeitreihe, SummenzeitreiheBuilder,
135};
136pub use zp_lifecycle::{
137    MabisZpLifecycleWorkflow, TAEGLICHE_AAUEZ_ENDE, WORKFLOW_NAME as ZP_LIFECYCLE_WORKFLOW_NAME,
138    ZP_FAMILIEN, ZpFamilie, ZpLifecycleCommand, ZpLifecycleData, ZpLifecycleEvent,
139    ZpLifecycleState, ZpSerie, ZpVorgang, all_pids as zp_lifecycle_pids,
140    antwort_ist_zustimmung as zp_antwort_ist_zustimmung, familie_for, ist_antwort_pid,
141    ist_weiterleitung_pid, serien_fuer_pid,
142};
143
144// ── EngineModule ──────────────────────────────────────────────────────────────
145
146/// Engine module for the MaBiS process family.
147///
148/// # PID ownership
149///
150/// | Workflow | PIDs |
151/// |---|---|
152/// | `mabis-billing` | MSCONS 13003 · 13020 · 13023; IFTSTA 21000–21005 |
153/// | `mabis-profile` | MSCONS 13010–13012; ORDERS 17211 |
154/// | `mabis-clearingliste` | UTILMD 55067 · 55069 · 55070 · 55073 |
155/// | `mabis-listenabgleich` | UTILMD 55065/55066 · 55195/55196 · 55201/55202 · 55223/55224 |
156/// | `mabis-zp-lifecycle` | UTILMD 55062–55064 · 55071/55072 · 55197–55214 · 55235–55237 |
157/// | `mabis-anforderung` | ORDERS 17201–17208 · 17210; ORDRSP 19204 |
158///
159/// Each workflow's module docs carry the use case it answers and the Fristen
160/// the Festlegung attaches to it.
161pub struct MabisModule;
162
163impl mako_engine::builder::EngineModule for MabisModule {
164    fn name(&self) -> &'static str {
165        "mabis"
166    }
167
168    fn workflow_names(&self) -> &'static [&'static str] {
169        // Every entry is the owning module's own constant. A literal here can
170        // disagree with the name `register_pids` routes to, and the two are
171        // checked against each other only at `EngineBuilder::build`.
172        &[
173            bilanzkreisabrechnung::WORKFLOW_NAME,
174            profile::WORKFLOW_NAME,
175            clearingliste::WORKFLOW_NAME,
176            zp_lifecycle::WORKFLOW_NAME,
177            anforderung::WORKFLOW_NAME,
178            listenabgleich::WORKFLOW_NAME,
179        ]
180    }
181
182    fn register_pids(&self, router: &mut mako_engine::pid_router::PidRouter) {
183        // ── MSCONS Summenzeitreihen ─────────────────────────────────────────
184        //
185        // 13003 „Summenzeitreihen und Ausfallarbeitssummen" (MSCONS AHB 3.1g §5)
186        // carries every BG-/BK-/LF-SZR, the DZÜ, the NZR and the
187        // Abrechnungssummenzeitreihe.
188        //
189        // 13020 Ausfallarbeitsüberführungszeitreihe and 13023
190        // Lieferantenausfallarbeitssummenzeitreihe are **MaBiS**, not
191        // Redispatch: the PID overview files both under the MaBiS
192        // Prozessbeschreibung and both carry the full Prüfmitteilung/
193        // Datenstatus cycle (IFTSTA 21000/21002–21005). They were routed to a
194        // Redispatch workflow, which had no settlement stream to put them in.
195        //
196        // 13022 stays with `mako-redispatch`: it is the TR-scharfe Einzel-
197        // zeitreihe the BTR and the NB reconcile, not a Summenzeitreihe.
198        // 13021 (meteorologische Ex-post-Daten) and 13026 (EEG-Überführungs-
199        // zeitreihe) are likewise not MaBiS.
200        //
201        // Confirmed absent: PID 13001 does not exist in any MSCONS AHB version.
202        router.register(bilanzkreisabrechnung::SUMMENZEITREIHE_PID, "mabis-billing");
203        for &pid in bilanzkreisabrechnung::AUSFALLARBEIT_PIDS {
204            router.register(pid, "mabis-billing");
205        }
206
207        // ── IFTSTA MaBiS Statusmeldungen 21000–21005 ────────────────────────
208        //
209        // All six route to `mabis-billing` so they correlate with their
210        // settlement stream by conversation ID. Their *direction* is not
211        // uniform: 21000/21001/21005 are this participant's own outbound
212        // Prüfmitteilungen, 21002 is the BIKO's Abweisung, and **both** 21003
213        // and 21004 carry a Datenstatus — 21003 to the NB/ÜNB, 21004 to the
214        // BKV. See `bilanzkreisabrechnung` for the table.
215        //
216        // PID 21006 does not exist. PID 21007 is WiM Strom Teil 1 / WiM Gas and
217        // is registered in `mako-wim` (`wim-device-change`).
218        for &pid in bilanzkreisabrechnung::IFTSTA_PIDS {
219            router.register(pid, "mabis-billing");
220        }
221
222        // ── Normierte Profile (Kap. 6.5 / 6.7) ──────────────────────────────
223        //
224        // MSCONS 13010/13011/13012 deliver the values; ORDERS 17211 is the LF's
225        // Reklamation (EBD E_0100). 17211 was filed with the Redispatch ORDERS
226        // codes, which left the delivery with no correction leg at all.
227        for pid in profile::all_pids() {
228            router.register(pid, profile::WORKFLOW_NAME);
229        }
230
231        // ── Record-only UTILMD lists ────────────────────────────────────────
232        //
233        // 55067 Bilanzkreiszuordnungsliste, 55069 Clearingliste DZR,
234        // 55070 Clearingliste BAS, 55073 Liste der Profildefinitionen.
235        //
236        // 55065 is deliberately **not** here: it owes a 55066 Korrekturliste and
237        // belongs to `mabis-listenabgleich`.
238        for &pid in clearingliste::CLEARINGLISTE_PIDS {
239            router.register(pid, clearingliste::WORKFLOW_NAME);
240        }
241
242        // ── UTILMD lists with a correction leg ──────────────────────────────
243        //
244        // 55065/55066, 55195/55196, 55201/55202, 55223/55224.
245        for pid in listenabgleich::all_pids() {
246            router.register(pid, listenabgleich::WORKFLOW_NAME);
247        }
248
249        // ── MaBiS-Zählpunkt lifecycle ───────────────────────────────────────
250        //
251        // The PID set comes from `zp_lifecycle::ZP_FAMILIEN`, so the router and
252        // the state machine cannot disagree about which codes exist. 55062,
253        // 55063 and 55064 are **generic**: eleven series share them and 55064 is
254        // answered out of twelve different EBDs, so the workflow is keyed on the
255        // series and not on the PID.
256        for pid in zp_lifecycle::all_pids() {
257            router.register(pid, zp_lifecycle::WORKFLOW_NAME);
258        }
259
260        // ── MaBiS Anforderungen ─────────────────────────────────────────────
261        //
262        // ORDERS 17201–17208 and 17210, plus the one Ablehnung the family has,
263        // ORDRSP 19204 (only 17207 can be refused). 17210 was filed with the
264        // Redispatch codes; it asks the ANB for the
265        // Lieferantenausfallarbeitsclearingliste, which is a MaBiS list.
266        for pid in anforderung::all_pids() {
267            router.register(pid, anforderung::WORKFLOW_NAME);
268        }
269    }
270
271    fn profile_requirements(&self) -> &'static [mako_engine::profile::ProfileRequirement] {
272        use mako_engine::profile::ProfileRequirement;
273        &[
274            ProfileRequirement {
275                message_type: "MSCONS",
276                label: "MSCONS Summenzeitreihen und Profile (MaBiS 13003, 13010–13012, 13020, 13023)",
277            },
278            ProfileRequirement {
279                message_type: "IFTSTA",
280                label: "IFTSTA Statusmeldung (MaBiS 21000–21005)",
281            },
282            ProfileRequirement {
283                message_type: "UTILMD",
284                label: "UTILMD MaBiS-Listen und ZP-Lifecycle (55062–55073, 55195–55224)",
285            },
286            ProfileRequirement {
287                message_type: "ORDERS",
288                label: "ORDERS MaBiS Anforderungen (17201–17208, 17210, 17211)",
289            },
290            ProfileRequirement {
291                message_type: "ORDRSP",
292                label: "ORDRSP Ablehnung Ab-/Bestellung der Aggregationsebene (19204)",
293            },
294        ]
295    }
296
297    fn configure(&self) -> Result<(), String> {
298        // No two workflows may claim the same PID: the router is last-write-wins,
299        // so a collision would silently route a message to whichever module
300        // registered second.
301        let mut seen: Vec<(u32, &'static str)> = Vec::new();
302        let mut push = |pids: Vec<u32>, wf: &'static str| -> Result<(), String> {
303            for pid in pids {
304                if let Some((_, other)) = seen.iter().find(|(p, _)| *p == pid) {
305                    return Err(format!("PID {pid} claimed by both {other} and {wf}"));
306                }
307                seen.push((pid, wf));
308            }
309            Ok(())
310        };
311        let mut billing = vec![bilanzkreisabrechnung::SUMMENZEITREIHE_PID];
312        billing.extend_from_slice(bilanzkreisabrechnung::AUSFALLARBEIT_PIDS);
313        billing.extend_from_slice(bilanzkreisabrechnung::IFTSTA_PIDS);
314        push(billing, "mabis-billing")?;
315        push(profile::all_pids(), profile::WORKFLOW_NAME)?;
316        push(
317            clearingliste::CLEARINGLISTE_PIDS.to_vec(),
318            clearingliste::WORKFLOW_NAME,
319        )?;
320        push(listenabgleich::all_pids(), listenabgleich::WORKFLOW_NAME)?;
321        push(zp_lifecycle::all_pids(), zp_lifecycle::WORKFLOW_NAME)?;
322        push(anforderung::all_pids(), anforderung::WORKFLOW_NAME)?;
323        Ok(())
324    }
325}
326
327#[cfg(test)]
328mod module_tests {
329    use super::*;
330    use mako_engine::builder::EngineModule;
331
332    #[test]
333    fn no_two_workflows_claim_the_same_pid() {
334        MabisModule.configure().expect("PID ownership is disjoint");
335    }
336
337    #[test]
338    fn the_lieferantenclearingliste_is_not_record_only() {
339        assert!(!clearingliste::CLEARINGLISTE_PIDS.contains(&55065));
340        assert!(listenabgleich::all_pids().contains(&55065));
341        assert!(listenabgleich::all_pids().contains(&55066));
342    }
343
344    #[test]
345    fn the_mabis_ausfallarbeit_series_are_registered_here() {
346        // 13020 (AAÜZ) and 13023 (LF-AASZR) are MaBiS Summenzeitreihen with a
347        // full Prüfmitteilung/Datenstatus cycle, so they settle here; a
348        // Redispatch workflow has no settlement stream for them.
349        assert_eq!(bilanzkreisabrechnung::AUSFALLARBEIT_PIDS, &[13_020, 13_023]);
350    }
351
352    #[test]
353    fn the_redispatch_only_mscons_pids_stay_out() {
354        // 13021 meteorologische Daten, 13022 Einzelzeitreihe Ausfallarbeit,
355        // 13026 EEG-Überführungszeitreihe are not MaBiS.
356        let mut claimed = vec![bilanzkreisabrechnung::SUMMENZEITREIHE_PID];
357        claimed.extend_from_slice(bilanzkreisabrechnung::AUSFALLARBEIT_PIDS);
358        claimed.extend(profile::all_pids());
359        for pid in [13_021, 13_022, 13_026] {
360            assert!(!claimed.contains(&pid), "{pid} is not a MaBiS PID");
361        }
362    }
363
364    #[test]
365    fn the_hkn_register_ordrsp_codes_stay_out() {
366        // 19301/19302 belong to the Herkunftsnachweisregister exchange, not to
367        // MaBiS and not to Redispatch.
368        for pid in [19_301_u32, 19_302] {
369            assert!(!anforderung::all_pids().contains(&pid));
370        }
371    }
372
373    #[test]
374    fn every_workflow_name_is_prefixed() {
375        for name in MabisModule.workflow_names() {
376            assert!(name.starts_with("mabis-"), "{name}");
377        }
378    }
379}