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}