Skip to main content

mako_gpke/
lib.rs

1//! `mako-gpke` — GPKE (Geschäftsprozesse Kundenlieferantenwechsel und
2//! Netznutzungsabrechnung) process engine for German electricity market
3//! communication (BDEW MaKo).
4//!
5//! ## Process family
6//!
7//! GPKE governs the standard market processes for supplier switching,
8//! grid connection management, and billing reconciliation in the German
9//! electricity market:
10//!
11//! ### UTILMD-based supplier-switching and feed-in processes (LFW24, S2.1/S2.2)
12//!
13//! #### Inbound ANFRAGE — routed to `gpke-supplier-change`
14//!
15//! | PID   | Process name (AHB)                                        |
16//! |-------|-----------------------------------------------------------|
17//! | 55001 | Anmeldung verb. MaLo — Lieferbeginn (LFN → NB)            |
18//! | 55004 | Abmeldung — Lieferende (LFN → NB)                         |
19//! | 55077 | Anmeldung erz. MaLo (LFN → NB, BK6-24-174)                |
20//!
21//! That is the whole of `UTILMD_ANFRAGE_PIDS`. Two neighbours route elsewhere:
22//! 55016 (Kündigung Lieferbeginn, LFN → LFA) to `gpke-kuendigung`, because no
23//! NB is party to it; 55557 (Änderung MSB-Abrechnungsdaten der MaLo, GPKE
24//! Teil 4) to `gpke-stammdatenaenderung`, which answers it with 55559.
25//!
26//! #### Outbound ANTWORT — derived, NOT routed (NB / LFA role)
27//!
28//! Each ANFRAGE has exactly one Bestätigung/Ablehnung pair. The pairs are *not*
29//! `+1/+2`: 55077 rejects with 55080 because 55079 is unassigned.
30//!
31//! | PID   | Process name (AHB)                              | Derived from   |
32//! |-------|-------------------------------------------------|----------------|
33//! | 55002 | Bestätigung Anmeldung verb. MaLo (NB → LFN)     | 55001 accepted |
34//! | 55003 | Ablehnung Anmeldung verb. MaLo (NB → LFN)       | 55001 rejected |
35//! | 55005 | Bestätigung Abmeldung (NB → LFN)                | 55004 accepted |
36//! | 55006 | Ablehnung Abmeldung (NB → LFN)                  | 55004 rejected |
37//! | 55017 | Bestätigung Kündigung Lieferbeginn (LFA → LFN)  | 55016 accepted — derived by `GpkeKuendigungWorkflow` |
38//! | 55018 | Ablehnung Kündigung Lieferbeginn (LFA → LFN)    | 55016 rejected — derived by `GpkeKuendigungWorkflow` |
39//! | 55078 | Bestätigung Anmeldung erz. MaLo (NB → LFN)      | 55077 accepted |
40//! | 55080 | Ablehnung Anmeldung erz. MaLo (NB → LFN)        | 55077 rejected |
41//!
42//! #### Inbound ANTWORT — routed to `gpke-lf-anmeldung` (LF role)
43//!
44//! When `makod` acts as **Lieferant**, it sends the outbound ANFRAGE and
45//! subsequently receives the NB/LFA response via AS4. These are the same eight
46//! PIDs derived above, registered to route back to [`GpkeLfAnmeldungWorkflow`]
47//! (see `ANTWORT_PIDS_LF`).
48//!
49//! #### Sperrung / Entsperrung — routed to `gpke-sperrung`
50//!
51//! | PID   | Process name (AWH)              | Direction |
52//! |-------|---------------------------------|-----------|
53//! | 17115 | Sperrauftrag                    | LF → NB   |
54//! | 17116 | Anfrage Sperrung (NB asks MSB)  | NB → MSB  |
55//! | 17117 | Entsperrauftrag                 | LF → NB   |
56//!
57//! #### Stornierung — routed to `gpke-stornierung`
58//!
59//! | PID   | Process name (AHB)                      |
60//! |-------|-----------------------------------------|
61//! | 55022 | Anfrage nach Stornierung (LFN → NB)     |
62//! | 55023 | Bestätigung Stornierung (NB → LFN)      |
63//! | 55024 | Ablehnung Stornierung (NB → LFN)        |
64//!
65//! ### Neuanlage — routed to `gpke-neuanlage`
66//!
67//! | PID   | Process name (AHB)                               |
68//! |-------|--------------------------------------------------|
69//! | 55600 | Anmeldung neue verb. MaLo (LF → NB)             |
70//! | 55601 | Anmeldung neue erz. MaLo (LF → NB)              |
71//!
72//! ### NB-initiated Lieferende — routed to `gpke-lf-abmeldung`
73//!
74//! | PID   | Process name (AHB)                                   |
75//! |-------|------------------------------------------------------|
76//! | 55007 | Ankündigung NB-seitiges Lieferende (NB → LFN)        |
77//!
78//! ### Ankündigung Zuordnung LF — routed to `gpke-ankuendigung-zuordnung-lf`
79//!
80//! | PID   | Process name (AHB)                               |
81//! |-------|--------------------------------------------------|
82//! | 55607 | Ankündigung Zuordnung LF (NB → LFN)              |
83//!
84//! PIDs 55608 (Bestätigung) and 55609 (Ablehnung) are outbound responses derived
85//! by `GpkeAnkuendigungZuordnungLfWorkflow` and never routed as inbound.
86//!
87//! PIDs 55008 (Bestätigung) and 55009 (Ablehnung) are outbound responses derived
88//! by `GpkeLfAbmeldungWorkflow` and never routed as inbound.
89//!
90//! ### Ersatz-/Grundversorgung — routed to `gpke-eog`
91//!
92//! | PID   | Process name (AHB)              | Direction |
93//! |-------|---------------------------------|-----------|
94//! | 55013 | Anmeldung / Zuordnung EOG       | NB → LF   |
95//! | 55014 | Bestätigung EOG Anmeldung       | LF → NB   |
96//! | 55015 | Ablehnung EOG Anmeldung         | LF → NB   |
97//!
98//! [`GpkeEogWorkflow`] covers both roles: the NB initiates the Zuordnung
99//! (statutory fallback supply, §36/§38 EnWG) and the Grundversorger
100//! responds. See [`eog`] for the full model.
101//!
102//! ### Beendigung der Zuordnung — routed to `gpke-beendigung-zuordnung`
103//!
104//! | PID   | Process name (AHB)                          | Direction |
105//! |-------|---------------------------------------------|-----------|
106//! | 55010 | Anfrage zur Beendigung der Zuordnung        | NB → LFA  |
107//!
108//! PID 55010 is the NB Abmeldeanfrage toward the LFA (BGM+E02 Abmeldungen,
109//! UTILMD AHB Strom 2.1). The responses PIDs 55011 (Bestätigung) and 55012
110//! (Ablehnung) are derived by [`GpkeBeendigungZuordnungWorkflow`]. See
111//! [`beendigung_zuordnung`] for the full model.
112//!
113//! The 3 answerable inbound ANFRAGE PIDs (`UTILMD_ANFRAGE_PIDS`) share
114//! [`GpkeSupplierChangeWorkflow`] (workflow name:
115//! `"gpke-supplier-change"`). The `pruefidentifikator` stored in
116//! [`wechselprozesse::InitiatedData`] lets read-models distinguish variants.
117//! The derived ANTWORT PIDs (55002/55003, 55005/55006, 55078/55080) are recorded
118//! in the `AntwortGesendet` event but are not routed as inbound messages — except
119//! in an LF-role deployment, where all eight of `ANTWORT_PIDS_LF` route back to
120//! [`GpkeLfAnmeldungWorkflow`].
121//!
122//! ### INVOIC-based billing processes (GPKE Netznutzungsabrechnung / MMM Strom)
123//!
124//! | PID   | Process name                             |
125//! |-------|------------------------------------------|
126//! | 31001 | Abschlagsrechnung (Netznutzung)              |
127//! | 31002 | NN-Rechnung (Netznutzungsabrechnung)          |
128//! | 31005 | MMM-Rechnung (Mehr-/Mindermengensaldo)        |
129//! | 31006 | MMM-Rechnung (selbst ausgestellt)            |
130//!
131//! All 4 PIDs use [`GpkeAbrechnungWorkflow`] (workflow name:
132//! `"gpke-abrechnung"`) — the shared INVOIC state machine from `mako-invoic`,
133//! which this crate parameterises with the GPKE family. The
134//! `pruefidentifikator` stored in `mako_invoic::InvoicData` lets read-models
135//! distinguish variants.
136//! PID 31003 (WiM-Rechnung) belongs to `mako-wim`. PID 31004 (Stornorechnung)
137//! is the Sparte-neutral universal Storno (INVOIC AHB §3.1.2) — checked
138//! generically by `invoicd`, not registered as a GPKE billing PID here.
139//! PIDs 31007/31008 (Aggreg. MMM-Rechnung NB → MGV, Gas-only) belong to
140//! `mako-gabi-gas` `gabi-gas-invoic` — MGV is a Gas-only market role.
141//! PID 31009 (MSB-Rechnung, multi-domain: GPKE Teil 3 / WiM Strom Teil 1) is
142//! registered by `mako-wim` (`wim-invoic`) to avoid double-registration;
143//! see `crates/mako-wim/src/invoic.rs`.
144//!
145//! ## Architecture
146//!
147//! Each BDEW process **group** maps to a single parameterised workflow.
148//! The PID value, stored in domain state, distinguishes process variants
149//! within a group without requiring duplicate workflow implementations.
150//!
151//! This crate contains **only pure domain logic** — no I/O, no EDIFACT
152//! parsing, no network calls. Parsing and validation of raw EDIFACT bytes
153//! happen at the transport boundary (AS4 reception layer), **before**
154//! constructing a domain command.
155//!
156//! ## Command construction example (UTILMD)
157//!
158//! ```rust,ignore
159//! use edi_energy::{AnyMessage, EdiEnergyMessage, Platform};
160//! use mako_gpke::wechselprozesse::{GpkeSupplierChangeWorkflow, SupplierChangeCommand};
161//!
162//! let msg    = Platform::with_all_profiles().parse(&raw_bytes)?;
163//! let report = msg.validate()?;
164//! let AnyMessage::Utilmd(u) = &msg else { anyhow::bail!("not UTILMD") };
165//!
166//! let cmd = SupplierChangeCommand::ReceiveUtilmd {
167//!     pid:               msg.detect_pruefidentifikator()?,
168//!     sender:            u.sender().and_then(|n| n.party_id.clone()).unwrap_or_default(),
169//!     receiver:          u.receiver().and_then(|n| n.party_id.clone()).unwrap_or_default(),
170//!     location_id:       u.transactions().first()
171//!                         .and_then(|t| t.marktlokation()).unwrap_or_default(),
172//!     document_date:     u.dtm().iter().find(|d| d.is_document_date())
173//!                         .and_then(|d| d.value.clone()).unwrap_or_default(),
174//!     message_ref:       msg.message_ref().to_owned(),
175//!     validation_passed: report.is_valid(),
176//!     validation_errors: report.errors().iter()
177//!                         .map(|i| format!("{i}")).collect(),
178//! };
179//!
180//! process.execute(cmd).await?;
181//! ```
182
183#![deny(unsafe_code)]
184#![deny(missing_docs)]
185#![warn(clippy::pedantic, clippy::must_use_candidate)]
186#![allow(clippy::module_name_repetitions)]
187#![allow(clippy::doc_markdown)] // German MaKo terms and BDEW acronyms produce many false positives
188#![allow(clippy::too_many_lines)] // process handle() functions are necessarily verbose
189#![allow(clippy::match_same_arms)] // sometimes intentional for process-family readability
190#![allow(clippy::manual_let_else)] // existing code style; rewrite in follow-up
191#![allow(clippy::redundant_closure_for_method_calls)]
192#![allow(clippy::unnested_or_patterns)]
193#![allow(clippy::map_unwrap_or)]
194#![allow(clippy::items_after_statements)]
195
196pub mod abrechnung;
197pub mod abrechnungsdaten;
198pub mod allokationsliste;
199pub mod anfrage_bestellung;
200pub mod ankuendigung_zuordnung_lf;
201pub mod antwortfrist;
202pub mod beendigung_zuordnung;
203pub mod datenabruf;
204pub mod eog;
205pub mod konfiguration;
206pub mod konfiguration_aenderung;
207pub mod kuendigung;
208pub mod lf_abmeldung;
209pub mod lf_anmeldung;
210pub mod lf_antwort;
211pub mod messwerte;
212pub mod neuanlage;
213pub mod partin;
214pub mod post_acceptance;
215pub mod sperrung;
216pub mod sperrung_lf;
217pub mod stammdatenaenderung;
218pub mod stornierung;
219pub mod utilts;
220pub mod wechselprozesse;
221pub mod zuordnungsmeldung;
222
223pub use abrechnung::{
224    ABRECHNUNG_WINDOW_LABEL, GPKE_COMDIS_ABLEHNUNG_PID, GPKE_INVOIC_PIDS, GPKE_REMADV_PIDS,
225    GpkeAbrechnung, GpkeAbrechnungWorkflow, WORKFLOW_NAME as ABRECHNUNG_WORKFLOW_NAME,
226};
227pub use abrechnungsdaten::{
228    ABRECHNUNGSDATEN_PIDS, AbrechnungsdatenCommand, AbrechnungsdatenData, AbrechnungsdatenEvent,
229    AbrechnungsdatenState, BEARBEITUNGSSTAND_PID, BEARBEITUNGSSTAND_WINDOW_LABEL,
230    GpkeAbrechnungsdatenWorkflow, WORKFLOW_NAME as ABRECHNUNGSDATEN_WORKFLOW_NAME,
231};
232pub use allokationsliste::{
233    AllokationslisteCommand, AllokationslisteEvent, AllokationslisteState, AnforderungData,
234    GpkeAllokationslisteWorkflow, MSCONS_RESPONSE_PIDS as ALLOKATIONSLISTE_MSCONS_PIDS,
235    ORDERS_ANFRAGE_PIDS as ALLOKATIONSLISTE_ORDERS_PIDS,
236    ORDRSP_ABLEHNUNG_PIDS as ALLOKATIONSLISTE_ORDRSP_PIDS,
237    WORKFLOW_NAME as ALLOKATIONSLISTE_WORKFLOW_NAME,
238};
239pub use anfrage_bestellung::{
240    ANFRAGE_PID as ANFRAGE_BESTELLUNG_PID, ANFRAGE_WINDOW_LABEL, AnfrageBestellungCommand,
241    AnfrageBestellungEvent, AnfrageBestellungState, AnfrageData, GpkeAnfrageBestellungWorkflow,
242    WORKFLOW_NAME as ANFRAGE_BESTELLUNG_WORKFLOW_NAME,
243};
244pub use ankuendigung_zuordnung_lf::{
245    ANKUENDIGUNG_ZUORDNUNG_ANTWORT_WINDOW_LABEL, ANKUENDIGUNG_ZUORDNUNG_PIDS,
246    AnkuendigungZuordnungLfCommand, AnkuendigungZuordnungLfData, AnkuendigungZuordnungLfEvent,
247    AnkuendigungZuordnungLfState, GpkeAnkuendigungZuordnungLfWorkflow,
248    WORKFLOW_NAME as ANKUENDIGUNG_ZUORDNUNG_LF_WORKFLOW_NAME,
249};
250pub use antwortfrist::{
251    ANTWORT_OBLIGATIONS, AntwortObligation, FristShape, antwort_deadline, antwort_obligation,
252};
253pub use beendigung_zuordnung::{
254    ANFRAGE_PID as BEENDIGUNG_ZUORDNUNG_ANFRAGE_PID,
255    ANTWORT_PIDS as BEENDIGUNG_ZUORDNUNG_ANTWORT_PIDS, BEENDIGUNG_ZUORDNUNG_ANTWORT_WINDOW_LABEL,
256    BEENDIGUNG_ZUORDNUNG_PIDS, BeendigungZuordnungCommand, BeendigungZuordnungData,
257    BeendigungZuordnungEvent, BeendigungZuordnungState, GpkeBeendigungZuordnungWorkflow,
258    NB_ANFRAGE_WINDOW_LABEL, WORKFLOW_NAME as BEENDIGUNG_ZUORDNUNG_WORKFLOW_NAME,
259};
260pub use datenabruf::{
261    DatenabrufCommand, DatenabrufEvent, DatenabrufState, GpkeDatenabrufWorkflow,
262    ORDERS_ANFRAGE_PIDS as DATENABRUF_ORDERS_PIDS, ORDRSP_ABLEHNUNG_PIDS as DATENABRUF_ORDRSP_PIDS,
263    WORKFLOW_NAME as DATENABRUF_WORKFLOW_NAME,
264};
265pub use eog::{
266    EOG_ANMELDUNG_PID, EOG_ANTWORT_PIDS, EOG_PIDS, EOG_RESPONSE_WINDOW_LABEL, EogCommand, EogData,
267    EogEvent, EogState, GpkeEogWorkflow, Versorgungsart, WORKFLOW_NAME as EOG_WORKFLOW_NAME,
268    eog_antwort_due_at, eog_response_pid,
269};
270pub use konfiguration::{
271    BeauftragungData, GpkeKonfigurationWorkflow, KONFIGURATION_WINDOW_LABEL, KonfigurationCommand,
272    KonfigurationEvent, KonfigurationProjection, KonfigurationRecord, KonfigurationState,
273    ORDERS_PIDS, ORDRSP_PIDS, WORKFLOW_NAME as KONFIGURATION_WORKFLOW_NAME,
274};
275pub use konfiguration_aenderung::{
276    GpkeKonfigurationAenderungWorkflow, IFTSTA_PIDS as KONFIGURATION_AENDERUNG_IFTSTA_PIDS,
277    KonfigurationAenderungCommand, KonfigurationAenderungEvent, KonfigurationAenderungState,
278    ORDERS_ANFRAGE_PIDS as KONFIGURATION_AENDERUNG_ORDERS_PIDS,
279    ORDRSP_PIDS as KONFIGURATION_AENDERUNG_ORDRSP_PIDS,
280    WORKFLOW_NAME as KONFIGURATION_AENDERUNG_WORKFLOW_NAME,
281};
282pub use kuendigung::{
283    GpkeKuendigungWorkflow, KuendigungCommand, KuendigungData, KuendigungEvent, KuendigungState,
284};
285pub use lf_abmeldung::{
286    GpkeLfAbmeldungWorkflow, LF_ABMELDUNG_ANTWORT_WINDOW_LABEL, LF_ABMELDUNG_PIDS,
287    LfAbmeldungCommand, LfAbmeldungData, LfAbmeldungEvent, LfAbmeldungState,
288    WORKFLOW_NAME as LF_ABMELDUNG_WORKFLOW_NAME,
289};
290pub use lf_anmeldung::{
291    ANFRAGE_PIDS_LF, ANTWORT_PIDS_LF, GpkeLfAnmeldungWorkflow, LfAnmeldungCommand, LfAnmeldungData,
292    LfAnmeldungEvent, LfAnmeldungState, NB_RESPONSE_WINDOW_LABEL,
293    WORKFLOW_NAME as LF_ANMELDUNG_WORKFLOW_NAME,
294};
295pub use lf_antwort::{
296    AntwortForm, AntwortMesslokation, LfAntwort, LfVorgangsdaten, ZugeordneterMsb, antwort_outbox,
297};
298pub use messwerte::{
299    GpkeMesswerteLieferungWorkflow, MSCONS_PIDS, MesswerteLieferungCommand, MesswerteLieferungData,
300    MesswerteLieferungEvent, MesswerteLieferungState, WORKFLOW_NAME as MESSWERTE_WORKFLOW_NAME,
301};
302pub use neuanlage::{
303    GpkeNeuanlageWorkflow, NEUANLAGE_ANTWORT_WINDOW_LABEL, NEUANLAGE_PIDS, NeuanlageCommand,
304    NeuanlageData, NeuanlageEvent, NeuanlageState, WORKFLOW_NAME as NEUANLAGE_WORKFLOW_NAME,
305};
306pub use partin::{
307    GpkePartinWorkflow, KommunikationsdatenCommand, KommunikationsdatenData,
308    KommunikationsdatenEvent, KommunikationsdatenState, PARTIN_STROM_PIDS,
309    WORKFLOW_NAME as PARTIN_WORKFLOW_NAME,
310};
311pub use sperrung::{
312    GpkeSperrungWorkflow, MSB_ANTWORT_PIDS, ORDCHG_STORNIERUNG_PIDS, SPERRUNG_PIDS,
313    SPERRUNG_WINDOW_LABEL, SperrungCommand, SperrungData, SperrungEvent, SperrungState,
314    WORKFLOW_NAME as SPERRUNG_WORKFLOW_NAME,
315};
316pub use sperrung_lf::{
317    ANTWORT_WINDOW_LABEL as SPERRUNG_LF_ANTWORT_WINDOW_LABEL, GpkeSperrungLfWorkflow,
318    IFTSTA_SPERRUNG_PID, ORDRSP_SPERRUNG_PIDS, ORDRSP_STORNO_PIDS,
319    SPERRUNG_ANFRAGE_PIDS as SPERRUNG_LF_ANFRAGE_PIDS, SperrungAuftragData, SperrungLfCommand,
320    SperrungLfEvent, SperrungLfState, WORKFLOW_NAME as SPERRUNG_LF_WORKFLOW_NAME,
321};
322pub use stammdatenaenderung::{
323    GpkeStammdatenaenderungWorkflow, Qualitaet,
324    RUECKMELDUNG_WINDOW_LABEL as STAMMDATEN_WINDOW_LABEL, STAMMDATEN_PAIRS, StammdatenCommand,
325    StammdatenData, StammdatenEvent, StammdatenObjekt, StammdatenState,
326    WORKFLOW_NAME as STAMMDATEN_WORKFLOW_NAME, is_aenderung_pid, is_rueckmeldung_pid, objekt_of,
327    rueckmeldung_pid_for,
328};
329pub use stornierung::{
330    GpkeStornierungCommand, GpkeStornierungData, GpkeStornierungEvent, GpkeStornierungState,
331    GpkeStornierungWorkflow,
332    STORNIERUNG_ANTWORT_WINDOW_LABEL as STORNIERUNG_GPKE_ANTWORT_WINDOW_LABEL,
333    STORNIERUNG_PIDS as STORNIERUNG_GPKE_PIDS, WORKFLOW_NAME as STORNIERUNG_GPKE_WORKFLOW_NAME,
334};
335pub use utilts::{
336    GpkeUtiltsWorkflow, UTILTS_PIDS, UtiltsKonfigCommand, UtiltsKonfigData, UtiltsKonfigEvent,
337    UtiltsKonfigState, WORKFLOW_NAME as UTILTS_WORKFLOW_NAME,
338};
339pub use wechselprozesse::{
340    GPKE_PROCESS_RESPONSE_LABEL, GpkeSupplierChangeWorkflow, IFTSTA_PIDS as IFTSTA_VOLLZUGS_PIDS,
341    InitiatedData, InitiatedDetails, SupplierChangeCommand, SupplierChangeEvent,
342    SupplierChangeProjection, SupplierChangeRecord, SupplierChangeState, UTILMD_ANFRAGE_PIDS,
343    UTILMD_PIDS, WORKFLOW_NAME as SUPPLIER_CHANGE_WORKFLOW_NAME,
344};
345pub use zuordnungsmeldung::{
346    AUFHEBUNG_PID, BEENDIGUNG_PID, GpkeZuordnungsmeldungWorkflow, INFORMATION_PID,
347    WORKFLOW_NAME as ZUORDNUNGSMELDUNG_WORKFLOW_NAME, ZUORDNUNGSMELDUNG_PIDS, Zuordnungsmeldung,
348    ZuordnungsmeldungCommand, ZuordnungsmeldungEvent, ZuordnungsmeldungState,
349};
350
351// ── EngineModule ──────────────────────────────────────────────────────────────
352
353/// Engine module for the GPKE process family.
354///
355/// Registers all GPKE Prüfidentifikator values:
356/// - PIDs 55001–55002, 55016 (inbound ANFRAGE, UTILMD) → `"gpke-supplier-change"`
357/// - PIDs 55022, 55023, 55024 (Stornierung Anfrage + Antwort, UTILMD) → `"gpke-stornierung"`
358/// - PIDs 55600, 55601 (Neuanlage ANFRAGE, UTILMD) → `"gpke-neuanlage"`
359/// - PID 55007 (NB-seitiges Lieferende, UTILMD) → `"gpke-lf-abmeldung"`
360/// - PID 55607 (Ankündigung Zuordnung LF, UTILMD) → `"gpke-ankuendigung-zuordnung-lf"`
361/// - PIDs 17115/17116/17117 (Sperrung/Entsperrung, ORDERS) → `"gpke-sperrung"`
362/// - **PID 55555** (Anfrage Daten der individuellen Bestellung, UTILMD) → `"gpke-anfrage-bestellung"`
363/// - PIDs 31001, 31002, 31005, 31006 (billing, INVOIC) → `"gpke-abrechnung"`
364///   _(31003 → `mako-wim`; 31004 = Sparte-neutral universal Storno, checked by `invoicd`)_
365/// - PIDs 19001, 19002 (inbound ORDRSP, NB role only) → `"gpke-konfiguration"`
366///
367/// **Role-conditional PIDs (ORDRSP 19001/19002):**
368///
369/// PIDs 19001 (`Bestellbestätigung`) and 19002 (`Ablehnung der Bestellung`)
370/// are registered **only when [`DeploymentRoles`] contains [`Marktrolle::Nb`]**.
371///
372/// In the GPKE Konfiguration workflow:
373/// - NB sends outbound ORDERS 17134/17135 (via outbox) to the designated MSB.
374/// - The MSB responds with inbound ORDRSP 19001/19002, which must route back to
375///   `gpke-konfiguration` for the NB-role makod instance.
376///
377/// On a **nMSB** (Herausforderer-MSB) instance, the same PIDs 19001/19002 are
378/// the response to WiM Geräteübernahme ORDERS 17001 sent by the nMSB to the NB.
379/// They route to `wim-geraeteubernahme` instead. Set explicit [`DeploymentRoles`]
380/// to prevent both modules from claiming the same PIDs.
381///
382/// **Not registered (outbound-only):**
383/// - PIDs 55002/55003, 55005/55006, 55017, 55018 are outbound ANTWORT messages derived by
384///   `GpkeSupplierChangeWorkflow::handle`. They are never routed as inbound.
385/// - PIDs 17134, 17135 are outbound ORDERS messages dispatched via the outbox
386///   by `GpkeKonfigurationWorkflow`. They are never routed as inbound.
387///
388/// PIDs 55007–55009 (NB-seitiges Lieferende) are handled by `GpkeLfAbmeldungWorkflow`.
389/// PIDs 55013–55015 (Ersatz-/Grundversorgung) are handled by `GpkeEogWorkflow`.
390/// PIDs 55010–55012 (Anfrage zur Beendigung der Zuordnung — NB Abmeldeanfrage an
391/// den LFA) are handled by `GpkeBeendigungZuordnungWorkflow`.
392///
393/// [`DeploymentRoles`]: mako_engine::marktrolle::DeploymentRoles
394/// [`Marktrolle::Nb`]: mako_engine::marktrolle::Marktrolle::Nb
395///
396/// Use with [`mako_engine::builder::EngineBuilder::register`]:
397///
398/// ```rust,ignore
399/// use mako_gpke::GpkeModule;
400/// use mako_engine::builder::EngineBuilder;
401/// use mako_engine::marktrolle::DeploymentRoles;
402///
403/// let ctx = EngineBuilder::new()
404///     .with_event_store(store)
405///     .with_deployment_roles(DeploymentRoles::nb())
406///     .register(Box::new(GpkeModule))
407///     .build();
408/// ```
409pub struct GpkeModule;
410
411impl mako_engine::builder::EngineModule for GpkeModule {
412    fn name(&self) -> &'static str {
413        "gpke"
414    }
415
416    fn workflow_names(&self) -> &'static [&'static str] {
417        // Every entry is the owning module's own constant. A literal here can
418        // disagree with the name `register_pids` routes to, and the two are
419        // checked against each other only at `EngineBuilder::build`.
420        &[
421            wechselprozesse::WORKFLOW_NAME,
422            eog::WORKFLOW_NAME,
423            kuendigung::WORKFLOW_NAME,
424            beendigung_zuordnung::WORKFLOW_NAME,
425            stammdatenaenderung::WORKFLOW_NAME,
426            lf_anmeldung::WORKFLOW_NAME,
427            sperrung::WORKFLOW_NAME,
428            sperrung_lf::WORKFLOW_NAME,
429            anfrage_bestellung::WORKFLOW_NAME,
430            abrechnung::WORKFLOW_NAME,
431            konfiguration::WORKFLOW_NAME,
432            neuanlage::WORKFLOW_NAME,
433            lf_abmeldung::WORKFLOW_NAME,
434            ankuendigung_zuordnung_lf::WORKFLOW_NAME,
435            stornierung::WORKFLOW_NAME,
436            messwerte::WORKFLOW_NAME,
437            partin::WORKFLOW_NAME,
438            utilts::WORKFLOW_NAME,
439            konfiguration_aenderung::WORKFLOW_NAME,
440            datenabruf::WORKFLOW_NAME,
441            allokationsliste::WORKFLOW_NAME,
442            abrechnungsdaten::WORKFLOW_NAME,
443            zuordnungsmeldung::WORKFLOW_NAME,
444        ]
445    }
446
447    fn register_pids_with_roles(
448        &self,
449        router: &mut mako_engine::pid_router::PidRouter,
450        roles: &mako_engine::marktrolle::DeploymentRoles,
451    ) {
452        // UTILMD inbound ANFRAGE PIDs — routed to gpke-supplier-change.
453        // Only inbound request PIDs are registered. The outbound ANTWORT PIDs
454        // (55002/55003, 55005/55006, 55017, 55018) are derived internally and never routed as inbound.
455        for &pid in UTILMD_PIDS {
456            router.register(pid, "gpke-supplier-change");
457        }
458
459        // PIDs 55600/55601 (Neuanlage neue Marktlokation) — BK6-24-174 Anlage 1b.
460        for &pid in NEUANLAGE_PIDS {
461            router.register(pid, "gpke-neuanlage");
462        }
463
464        // PID 55007 (NB-seitiges Lieferende, NB→LF) — GPKE Teil 2 §2.5.
465        // LF-role makod receives PID 55007 and responds with 55008/55009.
466        for &pid in LF_ABMELDUNG_PIDS {
467            router.register(pid, "gpke-lf-abmeldung");
468        }
469
470        // PIDs 55036/55037/55038 (Zuordnungs-Meldungen, NB→LFN/LFA/LFZ) — GPKE
471        // Teil 2 § 2.1.2 SD Lieferbeginn Nr. 2 / 10 / 13. One-way: the NB sends
472        // them by command, and a supplier-role deployment records the inbound
473        // ones. Registering them is what stops an inbound Meldung from being
474        // dead-lettered as `MessageStatus::UnknownPid` — there is no
475        // Antwortnachricht for the counterparty to miss instead.
476        for &pid in zuordnungsmeldung::ZUORDNUNGSMELDUNG_PIDS {
477            router.register(pid, zuordnungsmeldung::WORKFLOW_NAME);
478        }
479
480        // PID 55010 (Anfrage zur Beendigung der Zuordnung, NB→LFA) — GPKE Teil 2.
481        // LFA-role makod receives 55010 and responds with 55011/55012.
482        for &pid in beendigung_zuordnung::BEENDIGUNG_ZUORDNUNG_PIDS {
483            router.register(pid, beendigung_zuordnung::WORKFLOW_NAME);
484        }
485
486        // PID 55016 (Kündigung, LFN→LFA) — GPKE Teil 2 § 1.2, EBD `E_0614`.
487        // Its own workflow rather than a `gpke-supplier-change` variant: both
488        // are keyed by Marktlokation, and an integrated NB+LF deployment runs
489        // the NB's Anmeldung on the same MaLo.
490        for &pid in kuendigung::KUENDIGUNG_PIDS {
491            router.register(pid, kuendigung::WORKFLOW_NAME);
492        }
493
494        // PIDs 55013–55015 (Ersatz-/Grundversorgung, §36/§38 EnWG) — gpke-eog.
495        // 55013 spawns the LF/GV responder role; 55014/55015 resume the
496        // NB initiator role (correlated by MaLo).
497        for &pid in EOG_PIDS {
498            router.register(pid, eog::WORKFLOW_NAME);
499        }
500
501        // GPKE Teil 4 Stammdatenänderung (55615–55694, 55109/55110). Both the
502        // Änderung PIDs (inbound change → apply + Rückmeldung) and the
503        // Rückmeldung PIDs (resume a change we initiated) route here.
504        // 55557/55559 (MSB-Abrechnungsdaten der MaLo) belong here too — they
505        // are „Stammdatenänderung vom MSB (verantwortlich) ausgehend"
506        // Prozessschritte 1/2. 21047 (Bearbeitungsstand) stays on the IFTSTA route.
507        for &(aenderung_pid, rueckmeldung_pid, _) in stammdatenaenderung::STAMMDATEN_PAIRS {
508            router.register(aenderung_pid, stammdatenaenderung::WORKFLOW_NAME);
509            router.register(rueckmeldung_pid, stammdatenaenderung::WORKFLOW_NAME);
510        }
511
512        // PIDs 55156/55220/55673 (Rückmeldung/Bestellung Abrechnungsdaten, LF → NB)
513        // — GPKE Teil 2 § 3.1. The NB answers with IFTSTA 21047 by the 2. WT
514        // nach dem ÜT.
515        for &pid in ABRECHNUNGSDATEN_PIDS {
516            router.register(pid, abrechnungsdaten::WORKFLOW_NAME);
517        }
518
519        // PID 55607 (Ankündigung Zuordnung LF, NB→LFN) — GPKE Teil 2 §2.2, BK6-24-174.
520        // LF-role makod receives PID 55607 and responds with 55608/55609.
521        for &pid in ANKUENDIGUNG_ZUORDNUNG_PIDS {
522            router.register(pid, ankuendigung_zuordnung_lf::WORKFLOW_NAME);
523        }
524
525        // ORDERS PIDs 17115/17116/17117 (Sperrung/Entsperrung) — NB-role workflow.
526        // Per BDEW PID overview: "AWH Sperrprozesse" applies to both Strom and Gas.
527        // Direction: LF → NB (17115 Sperrauftrag, 17117 Entsperrauftrag);
528        //            NB → MSB (17116 Anfrage Sperrung).
529        for &pid in SPERRUNG_PIDS {
530            router.register(pid, sperrung::WORKFLOW_NAME);
531        }
532
533        // ORDCHG 39000/39001 (Stornierung Sperr-/Entsperrauftrag).
534        // 39000: LF → NB (LF cancels a pending Sperrauftrag).
535        // 39001: NB → MSB (Weiterleitung der Stornierung — NB forwards LF cancellation to MSB).
536        for &pid in ORDCHG_STORNIERUNG_PIDS {
537            router.register(pid, sperrung::WORKFLOW_NAME);
538        }
539
540        // ORDRSP 19118/19119 (MSB → NB: MSB's response to Anfrage Sperrung 17116).
541        // Only relevant when running in NB role (NB sends Anfrage to MSB and waits).
542        for &pid in MSB_ANTWORT_PIDS {
543            router.register(pid, sperrung::WORKFLOW_NAME);
544        }
545
546        // ORDRSP 19116/19117/19128/19129 (NB → LF: NB's response to Sperrauftrag/Stornierung).
547        // Registered for the LF-role `gpke-sperrung-lf` workflow so LF receives NB's answer.
548        for &pid in ORDRSP_SPERRUNG_PIDS {
549            router.register(pid, sperrung_lf::WORKFLOW_NAME);
550        }
551        for &pid in ORDRSP_STORNO_PIDS {
552            router.register(pid, sperrung_lf::WORKFLOW_NAME);
553        }
554
555        // IFTSTA 21039 (Auftragsstatus Sperren, NB → LF).
556        // LF receives the execution status from NB after the Sperrung is carried out.
557        router.register(IFTSTA_SPERRUNG_PID.as_u32(), sperrung_lf::WORKFLOW_NAME);
558
559        // INVOIC-based: all 6 billing PIDs use `GpkeAbrechnungWorkflow`.
560        for &pid in GPKE_INVOIC_PIDS {
561            router.register(pid, "gpke-abrechnung");
562        }
563
564        // REMADV 33001–33004 — inbound payment advice from payer to invoicer.
565        //
566        // After the NB/MSB sends an INVOIC (billing invoice), the payer (LF/NB)
567        // sends back a REMADV to confirm or dispute the payment. These PIDs must
568        // be routed to `gpke-abrechnung` so the `ReceiveRemadv` command can
569        // correlate the REMADV with the correct INVOIC process stream.
570        //
571        // Without this registration, all inbound REMADV messages are silently
572        // dead-lettered by the AS4 ingest layer (MessageStatus::UnknownPid),
573        // breaking the billing cycle entirely.
574        //
575        // Source: REMADV AHB 1.0, GPKE Teil 2/Teil 3, BK6-24-174.
576        for &pid in GPKE_REMADV_PIDS {
577            router.register(pid, "gpke-abrechnung");
578        }
579
580        // COMDIS 29001 — inbound Ablehnung REMADV (invoicer rejects payer's REMADV).
581        //
582        // After the payer sends a REMADV, the invoicer (NB/MSB) may reject it
583        // via COMDIS 29001. This is a different PID from APERAK 29001 (which is
584        // an outbound Verarbeitbarkeitsfehler acknowledgement). COMDIS 29001 is
585        // inbound from the invoicer and belongs to the billing cycle.
586        //
587        // Source: COMDIS AHB 1.0, GPKE Teil 2/Teil 3, BK6-24-174.
588        //
589        // Registered **Sparte-qualified** as well: 29001 also carries the GaBi
590        // Gas „Ablehnung REMADV", and the Sparte-agnostic table is last-wins, so
591        // a dual-fuel deployment would otherwise route a Strom COMDIS to the Gas
592        // workflow. The recipient MP-ID (UNB DE 0010) decides — every `[[party]]`
593        // covers exactly one Sparte (BDEW §2.13).
594        router.register(GPKE_COMDIS_ABLEHNUNG_PID.as_u32(), "gpke-abrechnung");
595        router.register_with_sparte(
596            GPKE_COMDIS_ABLEHNUNG_PID.as_u32(),
597            mako_engine::types::Sparte::Strom,
598            "gpke-abrechnung",
599        );
600
601        // ORDRSP inbound PIDs for Konfigurationseinrichtung (19001/19002).
602        //
603        // NB role only: the NB sends ORDERS 17134/17135 outbound (via outbox)
604        // to the designated MSB and receives ORDRSP 19001/19002 back.
605        // On nMSB instances these same PIDs are WiM Geräteübernahme responses
606        // and route to `wim-geraeteubernahme` — controlled via DeploymentRoles.
607        if roles.contains(mako_engine::marktrolle::Marktrolle::Nb) {
608            for &pid in ORDRSP_PIDS {
609                // register_with_module enforces the documented guarantee: if both NB
610                // and nMSB roles are active simultaneously, build() panics before any
611                // message is processed instead of silently routing to the wrong workflow.
612                router.register_with_module(pid, "gpke-konfiguration", "gpke");
613            }
614        }
615
616        // LF-side Anmeldung: inbound NB/LFA response PIDs (55002/55003, 55005/55006, 55017, 55018, 55078, 55080).
617        // Registered so the AS4 inbound layer can route them by conversation ID
618        // to the correct GpkeLfAnmeldungWorkflow instance (makod acting as LF).
619        // 55078 = Bestätigung Anmeldung erz. MaLo (NB → LFN)
620        // 55080 = Ablehnung Anmeldung erz. MaLo  (NB → LFN); PID 55079 unassigned
621        for &pid in ANTWORT_PIDS_LF {
622            router.register(pid, lf_anmeldung::WORKFLOW_NAME);
623        }
624
625        // IFTSTA GPKE Vollzugsmeldungen (PIDs 21024–21028, 21033).
626        //
627        // PIDs 21024–21028 are "GPKE / Vollzugsmeldung" per the IFTSTA AHB.
628        // PID 21033 is "GPKE / Statusmeldung Kündigung" (Ablehnung GPKE Teil 3).
629        // PID 21039 (Auftragsstatus Sperren) is registered to `gpke-sperrung-lf` above.
630        for &pid in wechselprozesse::IFTSTA_PIDS {
631            router.register(pid, "gpke-supplier-change");
632        }
633
634        // MSCONS data delivery PIDs (NB/MSB → LF, GPKE Teil 2/4, WiM Strom Teil 2).
635        //
636        // These are inbound MSCONS messages containing metered energy data that the
637        // NB or MSB sends to the LF. Essential for LF billing reconciliation.
638        // Registered unconditionally (both LF and NB deployments receive MSCONS).
639        for &pid in messwerte::MSCONS_PIDS {
640            router.register(pid, messwerte::WORKFLOW_NAME);
641        }
642
643        // PARTIN Kommunikationsdaten Strom (GPKE Teil 4).
644        //
645        // PIDs 37000–37006 exchange Strom market participant communication data
646        // (AS4 endpoints, GLNs, contact details) between LF, NB, MSB, and ÜNB.
647        // Gas PARTIN (PIDs 37008–37014) is handled by mako-geli-gas (geli-gas-partin).
648        for &pid in partin::PARTIN_STROM_PIDS {
649            router.register(pid, partin::WORKFLOW_NAME);
650        }
651
652        // UTILTS Konfigurationsdaten (GPKE Teil 3, WiM Strom/Gas Teil 2).
653        //
654        // UTILTS messages convey metering configuration definitions
655        // (Zählzeit-, Schaltzeit-, Leistungskurvendefinitionen) from NB/MSB to LF.
656        for &pid in utilts::UTILTS_PIDS {
657            router.register(pid, utilts::WORKFLOW_NAME);
658        }
659
660        // GPKE Stornierung PIDs 55022/55023/55024 (Anfrage + Antwort).
661        // NB role: NB receives 55022 inbound and dispatches 55023/55024.
662        // All three are registered so routing works for both inbound legs.
663        for &pid in stornierung::STORNIERUNG_PIDS {
664            router.register(pid, stornierung::WORKFLOW_NAME);
665        }
666
667        // PID 55555 — Anfrage Daten der individuellen Bestellung (GPKE Teil 4).
668        // LFN queries NB for data about a specific order. NB must respond
669        // inside the window `mako_fristen::antwort` resolves (BK6-24-174).
670        router.register(
671            anfrage_bestellung::ANFRAGE_PID.as_u32(),
672            anfrage_bestellung::WORKFLOW_NAME,
673        );
674
675        // GPKE Teil 3 Konfigurationsänderung — LF-initiated config change requests.
676        //
677        // LF sends ORDERS 17120/17122/17123/17128–17131/17133 to NB or MSB.
678        // NB/MSB responds with ORDRSP 19120–19133 (various confirmation/rejection/status PIDs).
679        // IFTSTA 21043/21044 are informational status/completion messages for this process.
680        // All routed to `gpke-konfiguration-aenderung`.
681        for &pid in konfiguration_aenderung::ORDERS_ANFRAGE_PIDS {
682            router.register(pid, konfiguration_aenderung::WORKFLOW_NAME);
683        }
684        for &pid in konfiguration_aenderung::ORDRSP_PIDS {
685            router.register(pid, konfiguration_aenderung::WORKFLOW_NAME);
686        }
687        for &pid in konfiguration_aenderung::IFTSTA_PIDS {
688            router.register(pid, konfiguration_aenderung::WORKFLOW_NAME);
689        }
690
691        // GPKE Datenabruf — LF-initiated data-value requests and reclamations.
692        //
693        // LF sends ORDERS 17102/17113 (Anfrage/Reklamation von Werten) to NB/MSB.
694        // NB/MSB rejects with ORDRSP 19101/19102/19114 (positive response via MSCONS).
695        for &pid in datenabruf::ORDERS_ANFRAGE_PIDS {
696            router.register(pid, datenabruf::WORKFLOW_NAME);
697        }
698        for &pid in datenabruf::ORDRSP_ABLEHNUNG_PIDS {
699            router.register(pid, datenabruf::WORKFLOW_NAME);
700        }
701
702        // GPKE Allokationsliste — LF requests allocation lists (MMM Strom/Gas).
703        //
704        // LF sends ORDERS 17110/17114, NB rejects with ORDRSP 19110/19115.
705        // Positive response comes via MSCONS (13013/13014) — MMM Strom/Gas PIDs,
706        // NOT GeLi Gas. Routed here per the BDEW/VKU/bne/GEODE AWH „Prozesse
707        // Mehr-/Mindermengen Strom Gas" V2.1 (18.03.2025); the MMM processes
708        // sit in an Anwendungshilfe, not in a BNetzA Festlegung.
709        for &pid in allokationsliste::ORDERS_ANFRAGE_PIDS {
710            router.register(pid, allokationsliste::WORKFLOW_NAME);
711        }
712        for &pid in allokationsliste::ORDRSP_ABLEHNUNG_PIDS {
713            router.register(pid, allokationsliste::WORKFLOW_NAME);
714        }
715        for &pid in allokationsliste::MSCONS_RESPONSE_PIDS {
716            router.register(pid, allokationsliste::WORKFLOW_NAME);
717        }
718
719        // EnFG IFTSTA PIDs (21045, 21047) and Rückmeldung (21035) are included
720        // in wechselprozesse::IFTSTA_PIDS and registered above under gpke-supplier-change.
721        // IFTSTA Konfigurationsbestellungsantworten (21043, 21044) are registered above
722        // under gpke-konfiguration-aenderung.
723        // PID 21042 (WiM / Umsetzungsstatus, "Bestellung (WiM)", MSB → ESA;
724        // IFTSTA AHB 2.0g Kap. 6.10) is a WiM Strom Teil 2 message routed by
725        // mako-wim (esa-wertebestellung), not GPKE.
726    }
727
728    fn profile_requirements(&self) -> &'static [mako_engine::profile::ProfileRequirement] {
729        use mako_engine::profile::ProfileRequirement;
730        &[
731            ProfileRequirement {
732                message_type: "UTILMD",
733                label: "UTILMD Strom (GPKE Lieferantenwechsel)",
734            },
735            ProfileRequirement {
736                message_type: "INVOIC",
737                label: "INVOIC Abrechnung (GPKE)",
738            },
739            ProfileRequirement {
740                message_type: "REMADV",
741                label: "REMADV Zahlungsavis (GPKE 33001–33004)",
742            },
743            ProfileRequirement {
744                message_type: "COMDIS",
745                label: "COMDIS Ablehnung REMADV (GPKE 29001)",
746            },
747            ProfileRequirement {
748                message_type: "IFTSTA",
749                label: "IFTSTA Vollzugsmeldung (GPKE 21033) + Auftragsstatus Sperren (21039)",
750            },
751            ProfileRequirement {
752                message_type: "ORDRSP",
753                label: "ORDRSP Sperrung (19116/19117/19118/19119/19128/19129) + Konfiguration (19001/19002)",
754            },
755            ProfileRequirement {
756                message_type: "ORDCHG",
757                label: "ORDCHG Stornierung Sperrauftrag (39000/39001)",
758            },
759            ProfileRequirement {
760                message_type: "MSCONS",
761                label: "MSCONS Messdatenlieferung NB/MSB → LF (13015–13027)",
762            },
763            ProfileRequirement {
764                message_type: "PARTIN",
765                label: "PARTIN Kommunikationsdaten Strom (37000–37006)",
766            },
767            ProfileRequirement {
768                message_type: "UTILTS",
769                label: "UTILTS Konfigurationsdaten GPKE Teil 3 (25001, 25004–25010)",
770            },
771            ProfileRequirement {
772                message_type: "ORDERS",
773                label: "ORDERS Konfigurationsänderung/Datenabruf/Allokationsliste (17102–17133)",
774            },
775            ProfileRequirement {
776                message_type: "IFTSTA",
777                label: "IFTSTA Vollzugsmeldung/Statusmeldung (GPKE 21024-21028, 21033, 21035, 21045, 21047) + Auftragsstatus Sperren (21039) + Konfiguration (21043, 21044)",
778            },
779        ]
780    }
781
782    fn configure(&self) -> Result<(), String> {
783        // Verify that all static PID slices are non-empty.  An empty slice
784        // would mean the module registers no routes, which is always a bug
785        // (e.g. an accidental empty const, a codegen regression, or a stale
786        // feature flag).  Discovered at startup rather than on first inbound
787        // message.
788        let named: &[(&str, &[u32])] = &[
789            ("UTILMD_PIDS", UTILMD_PIDS),
790            ("SPERRUNG_PIDS", SPERRUNG_PIDS),
791            ("ORDCHG_STORNIERUNG_PIDS", ORDCHG_STORNIERUNG_PIDS),
792            ("ORDRSP_SPERRUNG_PIDS", ORDRSP_SPERRUNG_PIDS),
793            ("ORDRSP_STORNO_PIDS", ORDRSP_STORNO_PIDS),
794            ("MSB_ANTWORT_PIDS", MSB_ANTWORT_PIDS),
795            ("GPKE_INVOIC_PIDS", GPKE_INVOIC_PIDS),
796            ("GPKE_REMADV_PIDS", GPKE_REMADV_PIDS),
797            ("ORDRSP_PIDS", ORDRSP_PIDS),
798            ("ANTWORT_PIDS_LF", ANTWORT_PIDS_LF),
799            ("wechselprozesse::IFTSTA_PIDS", wechselprozesse::IFTSTA_PIDS),
800            ("NEUANLAGE_PIDS", NEUANLAGE_PIDS),
801            ("LF_ABMELDUNG_PIDS", LF_ABMELDUNG_PIDS),
802            ("BEENDIGUNG_ZUORDNUNG_PIDS", BEENDIGUNG_ZUORDNUNG_PIDS),
803            (
804                "stornierung::STORNIERUNG_PIDS",
805                stornierung::STORNIERUNG_PIDS,
806            ),
807            ("messwerte::MSCONS_PIDS", messwerte::MSCONS_PIDS),
808            ("partin::PARTIN_STROM_PIDS", partin::PARTIN_STROM_PIDS),
809            ("utilts::UTILTS_PIDS", utilts::UTILTS_PIDS),
810            (
811                "konfiguration_aenderung::ORDERS_ANFRAGE_PIDS",
812                konfiguration_aenderung::ORDERS_ANFRAGE_PIDS,
813            ),
814            (
815                "konfiguration_aenderung::ORDRSP_PIDS",
816                konfiguration_aenderung::ORDRSP_PIDS,
817            ),
818            (
819                "datenabruf::ORDERS_ANFRAGE_PIDS",
820                datenabruf::ORDERS_ANFRAGE_PIDS,
821            ),
822            (
823                "datenabruf::ORDRSP_ABLEHNUNG_PIDS",
824                datenabruf::ORDRSP_ABLEHNUNG_PIDS,
825            ),
826            (
827                "allokationsliste::ORDERS_ANFRAGE_PIDS",
828                allokationsliste::ORDERS_ANFRAGE_PIDS,
829            ),
830            (
831                "allokationsliste::ORDRSP_ABLEHNUNG_PIDS",
832                allokationsliste::ORDRSP_ABLEHNUNG_PIDS,
833            ),
834            (
835                "allokationsliste::MSCONS_RESPONSE_PIDS",
836                allokationsliste::MSCONS_RESPONSE_PIDS,
837            ),
838            (
839                "konfiguration_aenderung::IFTSTA_PIDS",
840                konfiguration_aenderung::IFTSTA_PIDS,
841            ),
842        ];
843        for (name, pids) in named {
844            if pids.is_empty() {
845                return Err(format!(
846                    "gpke: PID slice '{name}' is empty — \
847                     at least one PID must be registered for each workflow group",
848                ));
849            }
850        }
851        // ANFRAGE_PID is a scalar constant (55555); verify it's in the valid
852        // Prüfidentifikator range as a sanity check.
853        if anfrage_bestellung::ANFRAGE_PID.as_u32() < 10_000
854            || anfrage_bestellung::ANFRAGE_PID.as_u32() > 99_999
855        {
856            return Err(format!(
857                "gpke: anfrage_bestellung::ANFRAGE_PID {} is outside the valid \
858                 Prüfidentifikator range 10000–99999",
859                anfrage_bestellung::ANFRAGE_PID.as_u32(),
860            ));
861        }
862        Ok(())
863    }
864}