mako-gpke 0.20.0

GPKE process engine for German electricity market communication (Lieferbeginn, Lieferende, Netznutzungsabrechnung)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
//! `mako-gpke` — GPKE (Geschäftsprozesse Kundenlieferantenwechsel und
//! Netznutzungsabrechnung) process engine for German electricity market
//! communication (BDEW MaKo).
//!
//! ## Process family
//!
//! GPKE governs the standard market processes for supplier switching,
//! grid connection management, and billing reconciliation in the German
//! electricity market:
//!
//! ### UTILMD-based supplier-switching and feed-in processes (LFW24, S2.1/S2.2)
//!
//! #### Inbound ANFRAGE — routed to `gpke-supplier-change`
//!
//! | PID   | Process name (AHB)                                        |
//! |-------|-----------------------------------------------------------|
//! | 55001 | Anmeldung verb. MaLo — Lieferbeginn (LFN → NB)            |
//! | 55004 | Abmeldung — Lieferende (LFN → NB)                         |
//! | 55077 | Anmeldung erz. MaLo (LFN → NB, BK6-24-174)                |
//!
//! That is the whole of `UTILMD_ANFRAGE_PIDS`. Two neighbours route elsewhere:
//! 55016 (Kündigung Lieferbeginn, LFN → LFA) to `gpke-kuendigung`, because no
//! NB is party to it; 55557 (Änderung MSB-Abrechnungsdaten der MaLo, GPKE
//! Teil 4) to `gpke-stammdatenaenderung`, which answers it with 55559.
//!
//! #### Outbound ANTWORT — derived, NOT routed (NB / LFA role)
//!
//! Each ANFRAGE has exactly one Bestätigung/Ablehnung pair. The pairs are *not*
//! `+1/+2`: 55077 rejects with 55080 because 55079 is unassigned.
//!
//! | PID   | Process name (AHB)                              | Derived from   |
//! |-------|-------------------------------------------------|----------------|
//! | 55002 | Bestätigung Anmeldung verb. MaLo (NB → LFN)     | 55001 accepted |
//! | 55003 | Ablehnung Anmeldung verb. MaLo (NB → LFN)       | 55001 rejected |
//! | 55005 | Bestätigung Abmeldung (NB → LFN)                | 55004 accepted |
//! | 55006 | Ablehnung Abmeldung (NB → LFN)                  | 55004 rejected |
//! | 55017 | Bestätigung Kündigung Lieferbeginn (LFA → LFN)  | 55016 accepted — derived by `GpkeKuendigungWorkflow` |
//! | 55018 | Ablehnung Kündigung Lieferbeginn (LFA → LFN)    | 55016 rejected — derived by `GpkeKuendigungWorkflow` |
//! | 55078 | Bestätigung Anmeldung erz. MaLo (NB → LFN)      | 55077 accepted |
//! | 55080 | Ablehnung Anmeldung erz. MaLo (NB → LFN)        | 55077 rejected |
//!
//! #### Inbound ANTWORT — routed to `gpke-lf-anmeldung` (LF role)
//!
//! When `makod` acts as **Lieferant**, it sends the outbound ANFRAGE and
//! subsequently receives the NB/LFA response via AS4. These are the same eight
//! PIDs derived above, registered to route back to [`GpkeLfAnmeldungWorkflow`]
//! (see `ANTWORT_PIDS_LF`).
//!
//! #### Sperrung / Entsperrung — routed to `gpke-sperrung`
//!
//! | PID   | Process name (AWH)              | Direction |
//! |-------|---------------------------------|-----------|
//! | 17115 | Sperrauftrag                    | LF → NB   |
//! | 17116 | Anfrage Sperrung (NB asks MSB)  | NB → MSB  |
//! | 17117 | Entsperrauftrag                 | LF → NB   |
//!
//! #### Stornierung — routed to `gpke-stornierung`
//!
//! | PID   | Process name (AHB)                      |
//! |-------|-----------------------------------------|
//! | 55022 | Anfrage nach Stornierung (LFN → NB)     |
//! | 55023 | Bestätigung Stornierung (NB → LFN)      |
//! | 55024 | Ablehnung Stornierung (NB → LFN)        |
//!
//! ### Neuanlage — routed to `gpke-neuanlage`
//!
//! | PID   | Process name (AHB)                               |
//! |-------|--------------------------------------------------|
//! | 55600 | Anmeldung neue verb. MaLo (LF → NB)             |
//! | 55601 | Anmeldung neue erz. MaLo (LF → NB)              |
//!
//! ### NB-initiated Lieferende — routed to `gpke-lf-abmeldung`
//!
//! | PID   | Process name (AHB)                                   |
//! |-------|------------------------------------------------------|
//! | 55007 | Ankündigung NB-seitiges Lieferende (NB → LFN)        |
//!
//! ### Ankündigung Zuordnung LF — routed to `gpke-ankuendigung-zuordnung-lf`
//!
//! | PID   | Process name (AHB)                               |
//! |-------|--------------------------------------------------|
//! | 55607 | Ankündigung Zuordnung LF (NB → LFN)              |
//!
//! PIDs 55608 (Bestätigung) and 55609 (Ablehnung) are outbound responses derived
//! by `GpkeAnkuendigungZuordnungLfWorkflow` and never routed as inbound.
//!
//! PIDs 55008 (Bestätigung) and 55009 (Ablehnung) are outbound responses derived
//! by `GpkeLfAbmeldungWorkflow` and never routed as inbound.
//!
//! ### Ersatz-/Grundversorgung — routed to `gpke-eog`
//!
//! | PID   | Process name (AHB)              | Direction |
//! |-------|---------------------------------|-----------|
//! | 55013 | Anmeldung / Zuordnung EOG       | NB → LF   |
//! | 55014 | Bestätigung EOG Anmeldung       | LF → NB   |
//! | 55015 | Ablehnung EOG Anmeldung         | LF → NB   |
//!
//! [`GpkeEogWorkflow`] covers both roles: the NB initiates the Zuordnung
//! (statutory fallback supply, §36/§38 EnWG) and the Grundversorger
//! responds. See [`eog`] for the full model.
//!
//! ### Beendigung der Zuordnung — routed to `gpke-beendigung-zuordnung`
//!
//! | PID   | Process name (AHB)                          | Direction |
//! |-------|---------------------------------------------|-----------|
//! | 55010 | Anfrage zur Beendigung der Zuordnung        | NB → LFA  |
//!
//! PID 55010 is the NB Abmeldeanfrage toward the LFA (BGM+E02 Abmeldungen,
//! UTILMD AHB Strom 2.1). The responses PIDs 55011 (Bestätigung) and 55012
//! (Ablehnung) are derived by [`GpkeBeendigungZuordnungWorkflow`]. See
//! [`beendigung_zuordnung`] for the full model.
//!
//! The 3 answerable inbound ANFRAGE PIDs (`UTILMD_ANFRAGE_PIDS`) share
//! [`GpkeSupplierChangeWorkflow`] (workflow name:
//! `"gpke-supplier-change"`). The `pruefidentifikator` stored in
//! [`wechselprozesse::InitiatedData`] lets read-models distinguish variants.
//! The derived ANTWORT PIDs (55002/55003, 55005/55006, 55078/55080) are recorded
//! in the `AntwortGesendet` event but are not routed as inbound messages — except
//! in an LF-role deployment, where all eight of `ANTWORT_PIDS_LF` route back to
//! [`GpkeLfAnmeldungWorkflow`].
//!
//! ### INVOIC-based billing processes (GPKE Netznutzungsabrechnung / MMM Strom)
//!
//! | PID   | Process name                             |
//! |-------|------------------------------------------|
//! | 31001 | Abschlagsrechnung (Netznutzung)              |
//! | 31002 | NN-Rechnung (Netznutzungsabrechnung)          |
//! | 31005 | MMM-Rechnung (Mehr-/Mindermengensaldo)        |
//! | 31006 | MMM-Rechnung (selbst ausgestellt)            |
//!
//! All 4 PIDs use [`GpkeAbrechnungWorkflow`] (workflow name:
//! `"gpke-abrechnung"`) — the shared INVOIC state machine from `mako-invoic`,
//! which this crate parameterises with the GPKE family. The
//! `pruefidentifikator` stored in `mako_invoic::InvoicData` lets read-models
//! distinguish variants.
//! PID 31003 (WiM-Rechnung) belongs to `mako-wim`. PID 31004 (Stornorechnung)
//! is the Sparte-neutral universal Storno (INVOIC AHB §3.1.2) — checked
//! generically by `invoicd`, not registered as a GPKE billing PID here.
//! PIDs 31007/31008 (Aggreg. MMM-Rechnung NB → MGV, Gas-only) belong to
//! `mako-gabi-gas` `gabi-gas-invoic` — MGV is a Gas-only market role.
//! PID 31009 (MSB-Rechnung, multi-domain: GPKE Teil 3 / WiM Strom Teil 1) is
//! registered by `mako-wim` (`wim-invoic`) to avoid double-registration;
//! see `crates/mako-wim/src/invoic.rs`.
//!
//! ## Architecture
//!
//! Each BDEW process **group** maps to a single parameterised workflow.
//! The PID value, stored in domain state, distinguishes process variants
//! within a group without requiring duplicate workflow implementations.
//!
//! This crate contains **only pure domain logic** — no I/O, no EDIFACT
//! parsing, no network calls. Parsing and validation of raw EDIFACT bytes
//! happen at the transport boundary (AS4 reception layer), **before**
//! constructing a domain command.
//!
//! ## Command construction example (UTILMD)
//!
//! ```rust,ignore
//! use edi_energy::{AnyMessage, EdiEnergyMessage, Platform};
//! use mako_gpke::wechselprozesse::{GpkeSupplierChangeWorkflow, SupplierChangeCommand};
//!
//! let msg    = Platform::with_all_profiles().parse(&raw_bytes)?;
//! let report = msg.validate()?;
//! let AnyMessage::Utilmd(u) = &msg else { anyhow::bail!("not UTILMD") };
//!
//! let cmd = SupplierChangeCommand::ReceiveUtilmd {
//!     pid:               msg.detect_pruefidentifikator()?,
//!     sender:            u.sender().and_then(|n| n.party_id.clone()).unwrap_or_default(),
//!     receiver:          u.receiver().and_then(|n| n.party_id.clone()).unwrap_or_default(),
//!     location_id:       u.transactions().first()
//!                         .and_then(|t| t.marktlokation()).unwrap_or_default(),
//!     document_date:     u.dtm().iter().find(|d| d.is_document_date())
//!                         .and_then(|d| d.value.clone()).unwrap_or_default(),
//!     message_ref:       msg.message_ref().to_owned(),
//!     validation_passed: report.is_valid(),
//!     validation_errors: report.errors().iter()
//!                         .map(|i| format!("{i}")).collect(),
//! };
//!
//! process.execute(cmd).await?;
//! ```

#![deny(unsafe_code)]
#![deny(missing_docs)]
#![warn(clippy::pedantic, clippy::must_use_candidate)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::doc_markdown)] // German MaKo terms and BDEW acronyms produce many false positives
#![allow(clippy::too_many_lines)] // process handle() functions are necessarily verbose
#![allow(clippy::match_same_arms)] // sometimes intentional for process-family readability
#![allow(clippy::manual_let_else)] // existing code style; rewrite in follow-up
#![allow(clippy::redundant_closure_for_method_calls)]
#![allow(clippy::unnested_or_patterns)]
#![allow(clippy::map_unwrap_or)]
#![allow(clippy::items_after_statements)]

pub mod abrechnung;
pub mod abrechnungsdaten;
pub mod allokationsliste;
pub mod anfrage_bestellung;
pub mod ankuendigung_zuordnung_lf;
pub mod antwortfrist;
pub mod beendigung_zuordnung;
pub mod datenabruf;
pub mod eog;
pub mod konfiguration;
pub mod konfiguration_aenderung;
pub mod kuendigung;
pub mod lf_abmeldung;
pub mod lf_anmeldung;
pub mod lf_antwort;
pub mod messwerte;
pub mod neuanlage;
pub mod partin;
pub mod post_acceptance;
pub mod sperrung;
pub mod sperrung_lf;
pub mod stammdatenaenderung;
pub mod stornierung;
pub mod utilts;
pub mod wechselprozesse;
pub mod zuordnungsmeldung;

pub use abrechnung::{
    ABRECHNUNG_WINDOW_LABEL, GPKE_COMDIS_ABLEHNUNG_PID, GPKE_INVOIC_PIDS, GPKE_REMADV_PIDS,
    GpkeAbrechnung, GpkeAbrechnungWorkflow, WORKFLOW_NAME as ABRECHNUNG_WORKFLOW_NAME,
};
pub use abrechnungsdaten::{
    ABRECHNUNGSDATEN_PIDS, AbrechnungsdatenCommand, AbrechnungsdatenData, AbrechnungsdatenEvent,
    AbrechnungsdatenState, BEARBEITUNGSSTAND_PID, BEARBEITUNGSSTAND_WINDOW_LABEL,
    GpkeAbrechnungsdatenWorkflow, WORKFLOW_NAME as ABRECHNUNGSDATEN_WORKFLOW_NAME,
};
pub use allokationsliste::{
    AllokationslisteCommand, AllokationslisteEvent, AllokationslisteState, AnforderungData,
    GpkeAllokationslisteWorkflow, MSCONS_RESPONSE_PIDS as ALLOKATIONSLISTE_MSCONS_PIDS,
    ORDERS_ANFRAGE_PIDS as ALLOKATIONSLISTE_ORDERS_PIDS,
    ORDRSP_ABLEHNUNG_PIDS as ALLOKATIONSLISTE_ORDRSP_PIDS,
    WORKFLOW_NAME as ALLOKATIONSLISTE_WORKFLOW_NAME,
};
pub use anfrage_bestellung::{
    ANFRAGE_PID as ANFRAGE_BESTELLUNG_PID, ANFRAGE_WINDOW_LABEL, AnfrageBestellungCommand,
    AnfrageBestellungEvent, AnfrageBestellungState, AnfrageData, GpkeAnfrageBestellungWorkflow,
    WORKFLOW_NAME as ANFRAGE_BESTELLUNG_WORKFLOW_NAME,
};
pub use ankuendigung_zuordnung_lf::{
    ANKUENDIGUNG_ZUORDNUNG_ANTWORT_WINDOW_LABEL, ANKUENDIGUNG_ZUORDNUNG_PIDS,
    AnkuendigungZuordnungLfCommand, AnkuendigungZuordnungLfData, AnkuendigungZuordnungLfEvent,
    AnkuendigungZuordnungLfState, GpkeAnkuendigungZuordnungLfWorkflow,
    WORKFLOW_NAME as ANKUENDIGUNG_ZUORDNUNG_LF_WORKFLOW_NAME,
};
pub use antwortfrist::{
    ANTWORT_OBLIGATIONS, AntwortObligation, FristShape, antwort_deadline, antwort_obligation,
};
pub use beendigung_zuordnung::{
    ANFRAGE_PID as BEENDIGUNG_ZUORDNUNG_ANFRAGE_PID,
    ANTWORT_PIDS as BEENDIGUNG_ZUORDNUNG_ANTWORT_PIDS, BEENDIGUNG_ZUORDNUNG_ANTWORT_WINDOW_LABEL,
    BEENDIGUNG_ZUORDNUNG_PIDS, BeendigungZuordnungCommand, BeendigungZuordnungData,
    BeendigungZuordnungEvent, BeendigungZuordnungState, GpkeBeendigungZuordnungWorkflow,
    NB_ANFRAGE_WINDOW_LABEL, WORKFLOW_NAME as BEENDIGUNG_ZUORDNUNG_WORKFLOW_NAME,
};
pub use datenabruf::{
    DatenabrufCommand, DatenabrufEvent, DatenabrufState, GpkeDatenabrufWorkflow,
    ORDERS_ANFRAGE_PIDS as DATENABRUF_ORDERS_PIDS, ORDRSP_ABLEHNUNG_PIDS as DATENABRUF_ORDRSP_PIDS,
    WORKFLOW_NAME as DATENABRUF_WORKFLOW_NAME,
};
pub use eog::{
    EOG_ANMELDUNG_PID, EOG_ANTWORT_PIDS, EOG_PIDS, EOG_RESPONSE_WINDOW_LABEL, EogCommand, EogData,
    EogEvent, EogState, GpkeEogWorkflow, Versorgungsart, WORKFLOW_NAME as EOG_WORKFLOW_NAME,
    eog_antwort_due_at, eog_response_pid,
};
pub use konfiguration::{
    BeauftragungData, GpkeKonfigurationWorkflow, KONFIGURATION_WINDOW_LABEL, KonfigurationCommand,
    KonfigurationEvent, KonfigurationProjection, KonfigurationRecord, KonfigurationState,
    ORDERS_PIDS, ORDRSP_PIDS, WORKFLOW_NAME as KONFIGURATION_WORKFLOW_NAME,
};
pub use konfiguration_aenderung::{
    GpkeKonfigurationAenderungWorkflow, IFTSTA_PIDS as KONFIGURATION_AENDERUNG_IFTSTA_PIDS,
    KonfigurationAenderungCommand, KonfigurationAenderungEvent, KonfigurationAenderungState,
    ORDERS_ANFRAGE_PIDS as KONFIGURATION_AENDERUNG_ORDERS_PIDS,
    ORDRSP_PIDS as KONFIGURATION_AENDERUNG_ORDRSP_PIDS,
    WORKFLOW_NAME as KONFIGURATION_AENDERUNG_WORKFLOW_NAME,
};
pub use kuendigung::{
    GpkeKuendigungWorkflow, KuendigungCommand, KuendigungData, KuendigungEvent, KuendigungState,
};
pub use lf_abmeldung::{
    GpkeLfAbmeldungWorkflow, LF_ABMELDUNG_ANTWORT_WINDOW_LABEL, LF_ABMELDUNG_PIDS,
    LfAbmeldungCommand, LfAbmeldungData, LfAbmeldungEvent, LfAbmeldungState,
    WORKFLOW_NAME as LF_ABMELDUNG_WORKFLOW_NAME,
};
pub use lf_anmeldung::{
    ANFRAGE_PIDS_LF, ANTWORT_PIDS_LF, GpkeLfAnmeldungWorkflow, LfAnmeldungCommand, LfAnmeldungData,
    LfAnmeldungEvent, LfAnmeldungState, NB_RESPONSE_WINDOW_LABEL,
    WORKFLOW_NAME as LF_ANMELDUNG_WORKFLOW_NAME,
};
pub use lf_antwort::{
    AntwortForm, AntwortMesslokation, LfAntwort, LfVorgangsdaten, ZugeordneterMsb, antwort_outbox,
};
pub use messwerte::{
    GpkeMesswerteLieferungWorkflow, MSCONS_PIDS, MesswerteLieferungCommand, MesswerteLieferungData,
    MesswerteLieferungEvent, MesswerteLieferungState, WORKFLOW_NAME as MESSWERTE_WORKFLOW_NAME,
};
pub use neuanlage::{
    GpkeNeuanlageWorkflow, NEUANLAGE_ANTWORT_WINDOW_LABEL, NEUANLAGE_PIDS, NeuanlageCommand,
    NeuanlageData, NeuanlageEvent, NeuanlageState, WORKFLOW_NAME as NEUANLAGE_WORKFLOW_NAME,
};
pub use partin::{
    GpkePartinWorkflow, KommunikationsdatenCommand, KommunikationsdatenData,
    KommunikationsdatenEvent, KommunikationsdatenState, PARTIN_STROM_PIDS,
    WORKFLOW_NAME as PARTIN_WORKFLOW_NAME,
};
pub use sperrung::{
    GpkeSperrungWorkflow, MSB_ANTWORT_PIDS, ORDCHG_STORNIERUNG_PIDS, SPERRUNG_PIDS,
    SPERRUNG_WINDOW_LABEL, SperrungCommand, SperrungData, SperrungEvent, SperrungState,
    WORKFLOW_NAME as SPERRUNG_WORKFLOW_NAME,
};
pub use sperrung_lf::{
    ANTWORT_WINDOW_LABEL as SPERRUNG_LF_ANTWORT_WINDOW_LABEL, GpkeSperrungLfWorkflow,
    IFTSTA_SPERRUNG_PID, ORDRSP_SPERRUNG_PIDS, ORDRSP_STORNO_PIDS,
    SPERRUNG_ANFRAGE_PIDS as SPERRUNG_LF_ANFRAGE_PIDS, SperrungAuftragData, SperrungLfCommand,
    SperrungLfEvent, SperrungLfState, WORKFLOW_NAME as SPERRUNG_LF_WORKFLOW_NAME,
};
pub use stammdatenaenderung::{
    GpkeStammdatenaenderungWorkflow, Qualitaet,
    RUECKMELDUNG_WINDOW_LABEL as STAMMDATEN_WINDOW_LABEL, STAMMDATEN_PAIRS, StammdatenCommand,
    StammdatenData, StammdatenEvent, StammdatenObjekt, StammdatenState,
    WORKFLOW_NAME as STAMMDATEN_WORKFLOW_NAME, is_aenderung_pid, is_rueckmeldung_pid, objekt_of,
    rueckmeldung_pid_for,
};
pub use stornierung::{
    GpkeStornierungCommand, GpkeStornierungData, GpkeStornierungEvent, GpkeStornierungState,
    GpkeStornierungWorkflow,
    STORNIERUNG_ANTWORT_WINDOW_LABEL as STORNIERUNG_GPKE_ANTWORT_WINDOW_LABEL,
    STORNIERUNG_PIDS as STORNIERUNG_GPKE_PIDS, WORKFLOW_NAME as STORNIERUNG_GPKE_WORKFLOW_NAME,
};
pub use utilts::{
    GpkeUtiltsWorkflow, UTILTS_PIDS, UtiltsKonfigCommand, UtiltsKonfigData, UtiltsKonfigEvent,
    UtiltsKonfigState, WORKFLOW_NAME as UTILTS_WORKFLOW_NAME,
};
pub use wechselprozesse::{
    GPKE_PROCESS_RESPONSE_LABEL, GpkeSupplierChangeWorkflow, IFTSTA_PIDS as IFTSTA_VOLLZUGS_PIDS,
    InitiatedData, InitiatedDetails, SupplierChangeCommand, SupplierChangeEvent,
    SupplierChangeProjection, SupplierChangeRecord, SupplierChangeState, UTILMD_ANFRAGE_PIDS,
    UTILMD_PIDS, WORKFLOW_NAME as SUPPLIER_CHANGE_WORKFLOW_NAME,
};
pub use zuordnungsmeldung::{
    AUFHEBUNG_PID, BEENDIGUNG_PID, GpkeZuordnungsmeldungWorkflow, INFORMATION_PID,
    WORKFLOW_NAME as ZUORDNUNGSMELDUNG_WORKFLOW_NAME, ZUORDNUNGSMELDUNG_PIDS, Zuordnungsmeldung,
    ZuordnungsmeldungCommand, ZuordnungsmeldungEvent, ZuordnungsmeldungState,
};

// ── EngineModule ──────────────────────────────────────────────────────────────

/// Engine module for the GPKE process family.
///
/// Registers all GPKE Prüfidentifikator values:
/// - PIDs 55001–55002, 55016 (inbound ANFRAGE, UTILMD) → `"gpke-supplier-change"`
/// - PIDs 55022, 55023, 55024 (Stornierung Anfrage + Antwort, UTILMD) → `"gpke-stornierung"`
/// - PIDs 55600, 55601 (Neuanlage ANFRAGE, UTILMD) → `"gpke-neuanlage"`
/// - PID 55007 (NB-seitiges Lieferende, UTILMD) → `"gpke-lf-abmeldung"`
/// - PID 55607 (Ankündigung Zuordnung LF, UTILMD) → `"gpke-ankuendigung-zuordnung-lf"`
/// - PIDs 17115/17116/17117 (Sperrung/Entsperrung, ORDERS) → `"gpke-sperrung"`
/// - **PID 55555** (Anfrage Daten der individuellen Bestellung, UTILMD) → `"gpke-anfrage-bestellung"`
/// - PIDs 31001, 31002, 31005, 31006 (billing, INVOIC) → `"gpke-abrechnung"`
///   _(31003 → `mako-wim`; 31004 = Sparte-neutral universal Storno, checked by `invoicd`)_
/// - PIDs 19001, 19002 (inbound ORDRSP, NB role only) → `"gpke-konfiguration"`
///
/// **Role-conditional PIDs (ORDRSP 19001/19002):**
///
/// PIDs 19001 (`Bestellbestätigung`) and 19002 (`Ablehnung der Bestellung`)
/// are registered **only when [`DeploymentRoles`] contains [`Marktrolle::Nb`]**.
///
/// In the GPKE Konfiguration workflow:
/// - NB sends outbound ORDERS 17134/17135 (via outbox) to the designated MSB.
/// - The MSB responds with inbound ORDRSP 19001/19002, which must route back to
///   `gpke-konfiguration` for the NB-role makod instance.
///
/// On a **nMSB** (Herausforderer-MSB) instance, the same PIDs 19001/19002 are
/// the response to WiM Geräteübernahme ORDERS 17001 sent by the nMSB to the NB.
/// They route to `wim-geraeteubernahme` instead. Set explicit [`DeploymentRoles`]
/// to prevent both modules from claiming the same PIDs.
///
/// **Not registered (outbound-only):**
/// - PIDs 55002/55003, 55005/55006, 55017, 55018 are outbound ANTWORT messages derived by
///   `GpkeSupplierChangeWorkflow::handle`. They are never routed as inbound.
/// - PIDs 17134, 17135 are outbound ORDERS messages dispatched via the outbox
///   by `GpkeKonfigurationWorkflow`. They are never routed as inbound.
///
/// PIDs 55007–55009 (NB-seitiges Lieferende) are handled by `GpkeLfAbmeldungWorkflow`.
/// PIDs 55013–55015 (Ersatz-/Grundversorgung) are handled by `GpkeEogWorkflow`.
/// PIDs 55010–55012 (Anfrage zur Beendigung der Zuordnung — NB Abmeldeanfrage an
/// den LFA) are handled by `GpkeBeendigungZuordnungWorkflow`.
///
/// [`DeploymentRoles`]: mako_engine::marktrolle::DeploymentRoles
/// [`Marktrolle::Nb`]: mako_engine::marktrolle::Marktrolle::Nb
///
/// Use with [`mako_engine::builder::EngineBuilder::register`]:
///
/// ```rust,ignore
/// use mako_gpke::GpkeModule;
/// use mako_engine::builder::EngineBuilder;
/// use mako_engine::marktrolle::DeploymentRoles;
///
/// let ctx = EngineBuilder::new()
///     .with_event_store(store)
///     .with_deployment_roles(DeploymentRoles::nb())
///     .register(Box::new(GpkeModule))
///     .build();
/// ```
pub struct GpkeModule;

impl mako_engine::builder::EngineModule for GpkeModule {
    fn name(&self) -> &'static str {
        "gpke"
    }

    fn workflow_names(&self) -> &'static [&'static str] {
        // Every entry is the owning module's own constant. A literal here can
        // disagree with the name `register_pids` routes to, and the two are
        // checked against each other only at `EngineBuilder::build`.
        &[
            wechselprozesse::WORKFLOW_NAME,
            eog::WORKFLOW_NAME,
            kuendigung::WORKFLOW_NAME,
            beendigung_zuordnung::WORKFLOW_NAME,
            stammdatenaenderung::WORKFLOW_NAME,
            lf_anmeldung::WORKFLOW_NAME,
            sperrung::WORKFLOW_NAME,
            sperrung_lf::WORKFLOW_NAME,
            anfrage_bestellung::WORKFLOW_NAME,
            abrechnung::WORKFLOW_NAME,
            konfiguration::WORKFLOW_NAME,
            neuanlage::WORKFLOW_NAME,
            lf_abmeldung::WORKFLOW_NAME,
            ankuendigung_zuordnung_lf::WORKFLOW_NAME,
            stornierung::WORKFLOW_NAME,
            messwerte::WORKFLOW_NAME,
            partin::WORKFLOW_NAME,
            utilts::WORKFLOW_NAME,
            konfiguration_aenderung::WORKFLOW_NAME,
            datenabruf::WORKFLOW_NAME,
            allokationsliste::WORKFLOW_NAME,
            abrechnungsdaten::WORKFLOW_NAME,
            zuordnungsmeldung::WORKFLOW_NAME,
        ]
    }

    fn register_pids_with_roles(
        &self,
        router: &mut mako_engine::pid_router::PidRouter,
        roles: &mako_engine::marktrolle::DeploymentRoles,
    ) {
        // UTILMD inbound ANFRAGE PIDs — routed to gpke-supplier-change.
        // Only inbound request PIDs are registered. The outbound ANTWORT PIDs
        // (55002/55003, 55005/55006, 55017, 55018) are derived internally and never routed as inbound.
        for &pid in UTILMD_PIDS {
            router.register(pid, "gpke-supplier-change");
        }

        // PIDs 55600/55601 (Neuanlage neue Marktlokation) — BK6-24-174 Anlage 1b.
        for &pid in NEUANLAGE_PIDS {
            router.register(pid, "gpke-neuanlage");
        }

        // PID 55007 (NB-seitiges Lieferende, NB→LF) — GPKE Teil 2 §2.5.
        // LF-role makod receives PID 55007 and responds with 55008/55009.
        for &pid in LF_ABMELDUNG_PIDS {
            router.register(pid, "gpke-lf-abmeldung");
        }

        // PIDs 55036/55037/55038 (Zuordnungs-Meldungen, NB→LFN/LFA/LFZ) — GPKE
        // Teil 2 § 2.1.2 SD Lieferbeginn Nr. 2 / 10 / 13. One-way: the NB sends
        // them by command, and a supplier-role deployment records the inbound
        // ones. Registering them is what stops an inbound Meldung from being
        // dead-lettered as `MessageStatus::UnknownPid` — there is no
        // Antwortnachricht for the counterparty to miss instead.
        for &pid in zuordnungsmeldung::ZUORDNUNGSMELDUNG_PIDS {
            router.register(pid, zuordnungsmeldung::WORKFLOW_NAME);
        }

        // PID 55010 (Anfrage zur Beendigung der Zuordnung, NB→LFA) — GPKE Teil 2.
        // LFA-role makod receives 55010 and responds with 55011/55012.
        for &pid in beendigung_zuordnung::BEENDIGUNG_ZUORDNUNG_PIDS {
            router.register(pid, beendigung_zuordnung::WORKFLOW_NAME);
        }

        // PID 55016 (Kündigung, LFN→LFA) — GPKE Teil 2 § 1.2, EBD `E_0614`.
        // Its own workflow rather than a `gpke-supplier-change` variant: both
        // are keyed by Marktlokation, and an integrated NB+LF deployment runs
        // the NB's Anmeldung on the same MaLo.
        for &pid in kuendigung::KUENDIGUNG_PIDS {
            router.register(pid, kuendigung::WORKFLOW_NAME);
        }

        // PIDs 55013–55015 (Ersatz-/Grundversorgung, §36/§38 EnWG) — gpke-eog.
        // 55013 spawns the LF/GV responder role; 55014/55015 resume the
        // NB initiator role (correlated by MaLo).
        for &pid in EOG_PIDS {
            router.register(pid, eog::WORKFLOW_NAME);
        }

        // GPKE Teil 4 Stammdatenänderung (55615–55694, 55109/55110). Both the
        // Änderung PIDs (inbound change → apply + Rückmeldung) and the
        // Rückmeldung PIDs (resume a change we initiated) route here.
        // 55557/55559 (MSB-Abrechnungsdaten der MaLo) belong here too — they
        // are „Stammdatenänderung vom MSB (verantwortlich) ausgehend"
        // Prozessschritte 1/2. 21047 (Bearbeitungsstand) stays on the IFTSTA route.
        for &(aenderung_pid, rueckmeldung_pid, _) in stammdatenaenderung::STAMMDATEN_PAIRS {
            router.register(aenderung_pid, stammdatenaenderung::WORKFLOW_NAME);
            router.register(rueckmeldung_pid, stammdatenaenderung::WORKFLOW_NAME);
        }

        // PIDs 55156/55220/55673 (Rückmeldung/Bestellung Abrechnungsdaten, LF → NB)
        // — GPKE Teil 2 § 3.1. The NB answers with IFTSTA 21047 by the 2. WT
        // nach dem ÜT.
        for &pid in ABRECHNUNGSDATEN_PIDS {
            router.register(pid, abrechnungsdaten::WORKFLOW_NAME);
        }

        // PID 55607 (Ankündigung Zuordnung LF, NB→LFN) — GPKE Teil 2 §2.2, BK6-24-174.
        // LF-role makod receives PID 55607 and responds with 55608/55609.
        for &pid in ANKUENDIGUNG_ZUORDNUNG_PIDS {
            router.register(pid, ankuendigung_zuordnung_lf::WORKFLOW_NAME);
        }

        // ORDERS PIDs 17115/17116/17117 (Sperrung/Entsperrung) — NB-role workflow.
        // Per BDEW PID overview: "AWH Sperrprozesse" applies to both Strom and Gas.
        // Direction: LF → NB (17115 Sperrauftrag, 17117 Entsperrauftrag);
        //            NB → MSB (17116 Anfrage Sperrung).
        for &pid in SPERRUNG_PIDS {
            router.register(pid, sperrung::WORKFLOW_NAME);
        }

        // ORDCHG 39000/39001 (Stornierung Sperr-/Entsperrauftrag).
        // 39000: LF → NB (LF cancels a pending Sperrauftrag).
        // 39001: NB → MSB (Weiterleitung der Stornierung — NB forwards LF cancellation to MSB).
        for &pid in ORDCHG_STORNIERUNG_PIDS {
            router.register(pid, sperrung::WORKFLOW_NAME);
        }

        // ORDRSP 19118/19119 (MSB → NB: MSB's response to Anfrage Sperrung 17116).
        // Only relevant when running in NB role (NB sends Anfrage to MSB and waits).
        for &pid in MSB_ANTWORT_PIDS {
            router.register(pid, sperrung::WORKFLOW_NAME);
        }

        // ORDRSP 19116/19117/19128/19129 (NB → LF: NB's response to Sperrauftrag/Stornierung).
        // Registered for the LF-role `gpke-sperrung-lf` workflow so LF receives NB's answer.
        for &pid in ORDRSP_SPERRUNG_PIDS {
            router.register(pid, sperrung_lf::WORKFLOW_NAME);
        }
        for &pid in ORDRSP_STORNO_PIDS {
            router.register(pid, sperrung_lf::WORKFLOW_NAME);
        }

        // IFTSTA 21039 (Auftragsstatus Sperren, NB → LF).
        // LF receives the execution status from NB after the Sperrung is carried out.
        router.register(IFTSTA_SPERRUNG_PID.as_u32(), sperrung_lf::WORKFLOW_NAME);

        // INVOIC-based: all 6 billing PIDs use `GpkeAbrechnungWorkflow`.
        for &pid in GPKE_INVOIC_PIDS {
            router.register(pid, "gpke-abrechnung");
        }

        // REMADV 33001–33004 — inbound payment advice from payer to invoicer.
        //
        // After the NB/MSB sends an INVOIC (billing invoice), the payer (LF/NB)
        // sends back a REMADV to confirm or dispute the payment. These PIDs must
        // be routed to `gpke-abrechnung` so the `ReceiveRemadv` command can
        // correlate the REMADV with the correct INVOIC process stream.
        //
        // Without this registration, all inbound REMADV messages are silently
        // dead-lettered by the AS4 ingest layer (MessageStatus::UnknownPid),
        // breaking the billing cycle entirely.
        //
        // Source: REMADV AHB 1.0, GPKE Teil 2/Teil 3, BK6-24-174.
        for &pid in GPKE_REMADV_PIDS {
            router.register(pid, "gpke-abrechnung");
        }

        // COMDIS 29001 — inbound Ablehnung REMADV (invoicer rejects payer's REMADV).
        //
        // After the payer sends a REMADV, the invoicer (NB/MSB) may reject it
        // via COMDIS 29001. This is a different PID from APERAK 29001 (which is
        // an outbound Verarbeitbarkeitsfehler acknowledgement). COMDIS 29001 is
        // inbound from the invoicer and belongs to the billing cycle.
        //
        // Source: COMDIS AHB 1.0, GPKE Teil 2/Teil 3, BK6-24-174.
        //
        // Registered **Sparte-qualified** as well: 29001 also carries the GaBi
        // Gas „Ablehnung REMADV", and the Sparte-agnostic table is last-wins, so
        // a dual-fuel deployment would otherwise route a Strom COMDIS to the Gas
        // workflow. The recipient MP-ID (UNB DE 0010) decides — every `[[party]]`
        // covers exactly one Sparte (BDEW §2.13).
        router.register(GPKE_COMDIS_ABLEHNUNG_PID.as_u32(), "gpke-abrechnung");
        router.register_with_sparte(
            GPKE_COMDIS_ABLEHNUNG_PID.as_u32(),
            mako_engine::types::Sparte::Strom,
            "gpke-abrechnung",
        );

        // ORDRSP inbound PIDs for Konfigurationseinrichtung (19001/19002).
        //
        // NB role only: the NB sends ORDERS 17134/17135 outbound (via outbox)
        // to the designated MSB and receives ORDRSP 19001/19002 back.
        // On nMSB instances these same PIDs are WiM Geräteübernahme responses
        // and route to `wim-geraeteubernahme` — controlled via DeploymentRoles.
        if roles.contains(mako_engine::marktrolle::Marktrolle::Nb) {
            for &pid in ORDRSP_PIDS {
                // register_with_module enforces the documented guarantee: if both NB
                // and nMSB roles are active simultaneously, build() panics before any
                // message is processed instead of silently routing to the wrong workflow.
                router.register_with_module(pid, "gpke-konfiguration", "gpke");
            }
        }

        // LF-side Anmeldung: inbound NB/LFA response PIDs (55002/55003, 55005/55006, 55017, 55018, 55078, 55080).
        // Registered so the AS4 inbound layer can route them by conversation ID
        // to the correct GpkeLfAnmeldungWorkflow instance (makod acting as LF).
        // 55078 = Bestätigung Anmeldung erz. MaLo (NB → LFN)
        // 55080 = Ablehnung Anmeldung erz. MaLo  (NB → LFN); PID 55079 unassigned
        for &pid in ANTWORT_PIDS_LF {
            router.register(pid, lf_anmeldung::WORKFLOW_NAME);
        }

        // IFTSTA GPKE Vollzugsmeldungen (PIDs 21024–21028, 21033).
        //
        // PIDs 21024–21028 are "GPKE / Vollzugsmeldung" per the IFTSTA AHB.
        // PID 21033 is "GPKE / Statusmeldung Kündigung" (Ablehnung GPKE Teil 3).
        // PID 21039 (Auftragsstatus Sperren) is registered to `gpke-sperrung-lf` above.
        for &pid in wechselprozesse::IFTSTA_PIDS {
            router.register(pid, "gpke-supplier-change");
        }

        // MSCONS data delivery PIDs (NB/MSB → LF, GPKE Teil 2/4, WiM Strom Teil 2).
        //
        // These are inbound MSCONS messages containing metered energy data that the
        // NB or MSB sends to the LF. Essential for LF billing reconciliation.
        // Registered unconditionally (both LF and NB deployments receive MSCONS).
        for &pid in messwerte::MSCONS_PIDS {
            router.register(pid, messwerte::WORKFLOW_NAME);
        }

        // PARTIN Kommunikationsdaten Strom (GPKE Teil 4).
        //
        // PIDs 37000–37006 exchange Strom market participant communication data
        // (AS4 endpoints, GLNs, contact details) between LF, NB, MSB, and ÜNB.
        // Gas PARTIN (PIDs 37008–37014) is handled by mako-geli-gas (geli-gas-partin).
        for &pid in partin::PARTIN_STROM_PIDS {
            router.register(pid, partin::WORKFLOW_NAME);
        }

        // UTILTS Konfigurationsdaten (GPKE Teil 3, WiM Strom/Gas Teil 2).
        //
        // UTILTS messages convey metering configuration definitions
        // (Zählzeit-, Schaltzeit-, Leistungskurvendefinitionen) from NB/MSB to LF.
        for &pid in utilts::UTILTS_PIDS {
            router.register(pid, utilts::WORKFLOW_NAME);
        }

        // GPKE Stornierung PIDs 55022/55023/55024 (Anfrage + Antwort).
        // NB role: NB receives 55022 inbound and dispatches 55023/55024.
        // All three are registered so routing works for both inbound legs.
        for &pid in stornierung::STORNIERUNG_PIDS {
            router.register(pid, stornierung::WORKFLOW_NAME);
        }

        // PID 55555 — Anfrage Daten der individuellen Bestellung (GPKE Teil 4).
        // LFN queries NB for data about a specific order. NB must respond
        // inside the window `mako_fristen::antwort` resolves (BK6-24-174).
        router.register(
            anfrage_bestellung::ANFRAGE_PID.as_u32(),
            anfrage_bestellung::WORKFLOW_NAME,
        );

        // GPKE Teil 3 Konfigurationsänderung — LF-initiated config change requests.
        //
        // LF sends ORDERS 17120/17122/17123/17128–17131/17133 to NB or MSB.
        // NB/MSB responds with ORDRSP 19120–19133 (various confirmation/rejection/status PIDs).
        // IFTSTA 21043/21044 are informational status/completion messages for this process.
        // All routed to `gpke-konfiguration-aenderung`.
        for &pid in konfiguration_aenderung::ORDERS_ANFRAGE_PIDS {
            router.register(pid, konfiguration_aenderung::WORKFLOW_NAME);
        }
        for &pid in konfiguration_aenderung::ORDRSP_PIDS {
            router.register(pid, konfiguration_aenderung::WORKFLOW_NAME);
        }
        for &pid in konfiguration_aenderung::IFTSTA_PIDS {
            router.register(pid, konfiguration_aenderung::WORKFLOW_NAME);
        }

        // GPKE Datenabruf — LF-initiated data-value requests and reclamations.
        //
        // LF sends ORDERS 17102/17113 (Anfrage/Reklamation von Werten) to NB/MSB.
        // NB/MSB rejects with ORDRSP 19101/19102/19114 (positive response via MSCONS).
        for &pid in datenabruf::ORDERS_ANFRAGE_PIDS {
            router.register(pid, datenabruf::WORKFLOW_NAME);
        }
        for &pid in datenabruf::ORDRSP_ABLEHNUNG_PIDS {
            router.register(pid, datenabruf::WORKFLOW_NAME);
        }

        // GPKE Allokationsliste — LF requests allocation lists (MMM Strom/Gas).
        //
        // LF sends ORDERS 17110/17114, NB rejects with ORDRSP 19110/19115.
        // Positive response comes via MSCONS (13013/13014) — MMM Strom/Gas PIDs,
        // NOT GeLi Gas. Routed here per the BDEW/VKU/bne/GEODE AWH „Prozesse
        // Mehr-/Mindermengen Strom Gas" V2.1 (18.03.2025); the MMM processes
        // sit in an Anwendungshilfe, not in a BNetzA Festlegung.
        for &pid in allokationsliste::ORDERS_ANFRAGE_PIDS {
            router.register(pid, allokationsliste::WORKFLOW_NAME);
        }
        for &pid in allokationsliste::ORDRSP_ABLEHNUNG_PIDS {
            router.register(pid, allokationsliste::WORKFLOW_NAME);
        }
        for &pid in allokationsliste::MSCONS_RESPONSE_PIDS {
            router.register(pid, allokationsliste::WORKFLOW_NAME);
        }

        // EnFG IFTSTA PIDs (21045, 21047) and Rückmeldung (21035) are included
        // in wechselprozesse::IFTSTA_PIDS and registered above under gpke-supplier-change.
        // IFTSTA Konfigurationsbestellungsantworten (21043, 21044) are registered above
        // under gpke-konfiguration-aenderung.
        // PID 21042 (WiM / Umsetzungsstatus, "Bestellung (WiM)", MSB → ESA;
        // IFTSTA AHB 2.0g Kap. 6.10) is a WiM Strom Teil 2 message routed by
        // mako-wim (esa-wertebestellung), not GPKE.
    }

    fn profile_requirements(&self) -> &'static [mako_engine::profile::ProfileRequirement] {
        use mako_engine::profile::ProfileRequirement;
        &[
            ProfileRequirement {
                message_type: "UTILMD",
                label: "UTILMD Strom (GPKE Lieferantenwechsel)",
            },
            ProfileRequirement {
                message_type: "INVOIC",
                label: "INVOIC Abrechnung (GPKE)",
            },
            ProfileRequirement {
                message_type: "REMADV",
                label: "REMADV Zahlungsavis (GPKE 33001–33004)",
            },
            ProfileRequirement {
                message_type: "COMDIS",
                label: "COMDIS Ablehnung REMADV (GPKE 29001)",
            },
            ProfileRequirement {
                message_type: "IFTSTA",
                label: "IFTSTA Vollzugsmeldung (GPKE 21033) + Auftragsstatus Sperren (21039)",
            },
            ProfileRequirement {
                message_type: "ORDRSP",
                label: "ORDRSP Sperrung (19116/19117/19118/19119/19128/19129) + Konfiguration (19001/19002)",
            },
            ProfileRequirement {
                message_type: "ORDCHG",
                label: "ORDCHG Stornierung Sperrauftrag (39000/39001)",
            },
            ProfileRequirement {
                message_type: "MSCONS",
                label: "MSCONS Messdatenlieferung NB/MSB → LF (13015–13027)",
            },
            ProfileRequirement {
                message_type: "PARTIN",
                label: "PARTIN Kommunikationsdaten Strom (37000–37006)",
            },
            ProfileRequirement {
                message_type: "UTILTS",
                label: "UTILTS Konfigurationsdaten GPKE Teil 3 (25001, 25004–25010)",
            },
            ProfileRequirement {
                message_type: "ORDERS",
                label: "ORDERS Konfigurationsänderung/Datenabruf/Allokationsliste (17102–17133)",
            },
            ProfileRequirement {
                message_type: "IFTSTA",
                label: "IFTSTA Vollzugsmeldung/Statusmeldung (GPKE 21024-21028, 21033, 21035, 21045, 21047) + Auftragsstatus Sperren (21039) + Konfiguration (21043, 21044)",
            },
        ]
    }

    fn configure(&self) -> Result<(), String> {
        // Verify that all static PID slices are non-empty.  An empty slice
        // would mean the module registers no routes, which is always a bug
        // (e.g. an accidental empty const, a codegen regression, or a stale
        // feature flag).  Discovered at startup rather than on first inbound
        // message.
        let named: &[(&str, &[u32])] = &[
            ("UTILMD_PIDS", UTILMD_PIDS),
            ("SPERRUNG_PIDS", SPERRUNG_PIDS),
            ("ORDCHG_STORNIERUNG_PIDS", ORDCHG_STORNIERUNG_PIDS),
            ("ORDRSP_SPERRUNG_PIDS", ORDRSP_SPERRUNG_PIDS),
            ("ORDRSP_STORNO_PIDS", ORDRSP_STORNO_PIDS),
            ("MSB_ANTWORT_PIDS", MSB_ANTWORT_PIDS),
            ("GPKE_INVOIC_PIDS", GPKE_INVOIC_PIDS),
            ("GPKE_REMADV_PIDS", GPKE_REMADV_PIDS),
            ("ORDRSP_PIDS", ORDRSP_PIDS),
            ("ANTWORT_PIDS_LF", ANTWORT_PIDS_LF),
            ("wechselprozesse::IFTSTA_PIDS", wechselprozesse::IFTSTA_PIDS),
            ("NEUANLAGE_PIDS", NEUANLAGE_PIDS),
            ("LF_ABMELDUNG_PIDS", LF_ABMELDUNG_PIDS),
            ("BEENDIGUNG_ZUORDNUNG_PIDS", BEENDIGUNG_ZUORDNUNG_PIDS),
            (
                "stornierung::STORNIERUNG_PIDS",
                stornierung::STORNIERUNG_PIDS,
            ),
            ("messwerte::MSCONS_PIDS", messwerte::MSCONS_PIDS),
            ("partin::PARTIN_STROM_PIDS", partin::PARTIN_STROM_PIDS),
            ("utilts::UTILTS_PIDS", utilts::UTILTS_PIDS),
            (
                "konfiguration_aenderung::ORDERS_ANFRAGE_PIDS",
                konfiguration_aenderung::ORDERS_ANFRAGE_PIDS,
            ),
            (
                "konfiguration_aenderung::ORDRSP_PIDS",
                konfiguration_aenderung::ORDRSP_PIDS,
            ),
            (
                "datenabruf::ORDERS_ANFRAGE_PIDS",
                datenabruf::ORDERS_ANFRAGE_PIDS,
            ),
            (
                "datenabruf::ORDRSP_ABLEHNUNG_PIDS",
                datenabruf::ORDRSP_ABLEHNUNG_PIDS,
            ),
            (
                "allokationsliste::ORDERS_ANFRAGE_PIDS",
                allokationsliste::ORDERS_ANFRAGE_PIDS,
            ),
            (
                "allokationsliste::ORDRSP_ABLEHNUNG_PIDS",
                allokationsliste::ORDRSP_ABLEHNUNG_PIDS,
            ),
            (
                "allokationsliste::MSCONS_RESPONSE_PIDS",
                allokationsliste::MSCONS_RESPONSE_PIDS,
            ),
            (
                "konfiguration_aenderung::IFTSTA_PIDS",
                konfiguration_aenderung::IFTSTA_PIDS,
            ),
        ];
        for (name, pids) in named {
            if pids.is_empty() {
                return Err(format!(
                    "gpke: PID slice '{name}' is empty — \
                     at least one PID must be registered for each workflow group",
                ));
            }
        }
        // ANFRAGE_PID is a scalar constant (55555); verify it's in the valid
        // Prüfidentifikator range as a sanity check.
        if anfrage_bestellung::ANFRAGE_PID.as_u32() < 10_000
            || anfrage_bestellung::ANFRAGE_PID.as_u32() > 99_999
        {
            return Err(format!(
                "gpke: anfrage_bestellung::ANFRAGE_PID {} is outside the valid \
                 Prüfidentifikator range 10000–99999",
                anfrage_bestellung::ANFRAGE_PID.as_u32(),
            ));
        }
        Ok(())
    }
}