mako_redispatch/lib.rs
1//! `mako-redispatch` — Redispatch 2.0 process engine for German grid
2//! congestion management (§§ 13, 13a, 14 `EnWG`).
3//!
4//! # Three-crate architecture for Redispatch 2.0
5//!
6//! | Crate | Responsibility |
7//! |---|---|
8//! | `edi-energy` | IFTSTA status messages (EDIFACT, PIDs 21037/21038) |
9//! | `redispatch-xml` | XML/XSD format parsing (`ActivationDocument`, `Stammdaten`, …) |
10//! | `mako-redispatch` ← **this crate** | Process engine — workflows, routing, deadlines |
11//!
12//! # Domain background
13//!
14//! **Redispatch 2.0** entered into force on **1 October 2021** via the
15//! Netzausbaubeschleunigungsgesetz (NABEG). It requires all German TSOs
16//! (ÜNB) and DSOs (VNB) to coordinate congestion management across
17//! transmission and distribution networks using CIM/IEC 62325 XML documents.
18//!
19//! Unlike GPKE/WiM/GeLi Gas (EDIFACT `RFF+Z13` Prüfidentifikatoren), routing
20//! here is document-type-driven via [`RedispatchRouter`].
21//!
22//! # Regulatory basis
23//!
24//! | `BNetzA` decision | Topic |
25//! |---|---|
26//! | BK6-20-059 | `AcknowledgementDocument` (6h), `StatusRequest` (24h) |
27//! | BK6-20-060 | `Stammdaten` (1 Werktag), Activation (5 min) |
28//! | BK6-20-061 | `Kostenblatt` (15th of following month) |
29//!
30//! # Regulatory deadlines
31//!
32//! | Obligation | Deadline | Clock |
33//! |---|---|---|
34//! | `AcknowledgementDocument` | 6 wall-clock hours | **UTC** |
35//! | `StatusRequest` response | 24 wall-clock hours | **UTC** |
36//! | Stammdaten forward (VNB→ÜNB) | 1 Werktag | German local time |
37//! | Activation (ACO) response | **5 minutes** | **UTC** |
38//! | Kostenblatt submission | 15th of following month | German local time |
39//!
40//! > **Clock semantics differ from GPKE/WiM.** Redispatch 2.0 uses UTC
41//! > wall-clock hours for the acknowledgement and activation deadlines.
42//! > Only the Stammdaten-forwarding and Kostenblatt obligations follow
43//! > German local time (CET/CEST) + Werktag rules.
44//!
45//! # Deployment role gate
46//!
47//! `RedispatchModule` should only be registered when `DeploymentRoles` contains
48//! at least one of `Marktrolle::Nb`, `Marktrolle::Unb`, or `Marktrolle::Anb`.
49//! Lieferant (LF) and MSB deployments are out of scope for Redispatch 2.0.
50//!
51//! # IFTSTA PIDs (confirmed from IFTSTA AHB 2.1 + PID 4.0)
52//!
53//! | PID | Perspective | Process |
54//! |-------|-------------|---------|
55//! | 21037 | NB (VNB) | Kommunikationsprozesse Redispatch — Ansicht NB |
56//! | 21038 | BTR | Kommunikationsprozesse Redispatch — Ansicht BTR |
57//!
58//! These PIDs are registered into the `PidRouter` by [`RedispatchModule`] and
59//! route to the [`aktivierung`] workflow via conversation-ID lookup.
60//!
61//! # Module overview
62//!
63//! | Module | Workflow name | Document type |
64//! |---|---|---|
65//! | [`stammdaten`] | `redispatch-stammdaten` | `Stammdaten` |
66//! | [`aktivierung`] | `redispatch-aktivierung` | `ActivationDocument` |
67//! | [`ack_forward`] (Verfügbarkeit) | `redispatch-verfuegbarkeit` | `UnavailabilityMarketDocument` |
68//! | [`ack_forward`] (Netzengpass) | `redispatch-netzengpass` | `NetworkConstraintDocument` |
69//! | [`ack_forward`] (Kaskade) | `redispatch-kaskade` | `Kaskade` |
70//! | [`ack_forward`] (Planungsdaten) | `redispatch-planungsdaten` | `PlannedResourceScheduleDocument` |
71//! | [`ack_forward`] (Statusanfrage) | `redispatch-statusanfrage` | `StatusRequest_MarketDocument` |
72//! | [`ack_forward`] (Kostenblatt) | `redispatch-kostenblatt` | `Kostenblatt` |
73
74#![deny(unsafe_code)]
75#![deny(missing_docs)]
76#![warn(clippy::pedantic)]
77
78pub mod ack_forward;
79pub mod aktivierung;
80pub mod router;
81pub mod stammdaten;
82
83pub use router::{RedispatchDocumentKind, RedispatchRouter};
84
85use mako_engine::{builder::EngineModule, pid_router::PidRouter, profile::ProfileRequirement};
86
87// ── RedispatchModule ──────────────────────────────────────────────────────────
88
89/// Engine module for the Redispatch 2.0 process family.
90///
91/// Registers:
92/// - All 8 Redispatch 2.0 workflows into the caller's `RedispatchRouter`
93/// (XML document-type routing, not PID routing).
94/// - IFTSTA PIDs 21037 and 21038 into the `PidRouter`
95/// (EDIFACT-based Vollzugsmeldung, routes to `redispatch-aktivierung`).
96///
97/// # Deployment gate
98///
99/// Only register this module when `DeploymentRoles` contains at least one of
100/// `Marktrolle::Nb`, `Marktrolle::Unb`, or `Marktrolle::Anb`:
101///
102/// ```rust,ignore
103/// if roles.contains_any(&[Marktrolle::Nb, Marktrolle::Unb, Marktrolle::Anb]) {
104/// builder.register(Box::new(RedispatchModule));
105/// }
106/// ```
107pub struct RedispatchModule;
108
109impl RedispatchModule {
110 /// Build a fully-populated [`RedispatchRouter`] for `makod` inbound dispatch.
111 ///
112 /// Called once during daemon startup, before the HTTP/AS4 servers are bound.
113 ///
114 /// # Acknowledgement routing
115 ///
116 /// `AcknowledgementDocument` is intentionally **not** registered in this
117 /// router. Inbound ACKs carry a `ReceivingDocumentIdentification` field that
118 /// identifies the workflow instance they belong to. The `makod` dispatcher
119 /// resolves that correlation key against the `ProcessRegistry` and delivers
120 /// the ACK directly to the correct workflow instance — no document-type
121 /// routing is needed.
122 #[must_use]
123 pub fn build_router() -> RedispatchRouter {
124 let mut router = RedispatchRouter::new();
125 router.register(
126 RedispatchDocumentKind::Activation,
127 aktivierung::WORKFLOW_NAME,
128 );
129 router.register(
130 RedispatchDocumentKind::PlannedResourceSchedule,
131 ack_forward::names::PLANUNGSDATEN,
132 );
133 // Acknowledgement is routed by correlation (ReceivingDocumentIdentification),
134 // not by document kind — do NOT register it here.
135 router.register(
136 RedispatchDocumentKind::Stammdaten,
137 stammdaten::WORKFLOW_NAME,
138 );
139 router.register(
140 RedispatchDocumentKind::StatusRequest,
141 ack_forward::names::STATUSANFRAGE,
142 );
143 router.register(
144 RedispatchDocumentKind::Unavailability,
145 ack_forward::names::VERFUEGBARKEIT,
146 );
147 router.register(RedispatchDocumentKind::Kaskade, ack_forward::names::KASKADE);
148 router.register(
149 RedispatchDocumentKind::NetworkConstraint,
150 ack_forward::names::NETZENGPASS,
151 );
152 router.register(
153 RedispatchDocumentKind::Kostenblatt,
154 ack_forward::names::KOSTENBLATT,
155 );
156 router
157 }
158}
159
160impl EngineModule for RedispatchModule {
161 fn name(&self) -> &'static str {
162 "redispatch"
163 }
164
165 fn workflow_names(&self) -> &'static [&'static str] {
166 &[
167 stammdaten::WORKFLOW_NAME,
168 aktivierung::WORKFLOW_NAME,
169 ack_forward::names::VERFUEGBARKEIT,
170 ack_forward::names::NETZENGPASS,
171 ack_forward::names::KASKADE,
172 ack_forward::names::PLANUNGSDATEN,
173 ack_forward::names::STATUSANFRAGE,
174 ack_forward::names::KOSTENBLATT,
175 ]
176 }
177
178 fn register_pids(&self, router: &mut PidRouter) {
179 // Redispatch 2.0 uses XML document-type routing, not EDIFACT PIDs.
180 // EDIFACT IFTSTA PIDs carry Redispatch status messages:
181 //
182 // PID 21035 — Redispatch / Statusmeldung
183 // PID 21036 — Redispatch / Statusmeldung Aktivierungsauftrag
184 // PID 21037 — Redispatch / Statusmeldung Einspeisemanagement
185 // PID 21038 — Redispatch / Statusmeldung Abrechnungsinformation
186 // PID 21040 — Redispatch / Statusmeldung Bilanzkreiszuordnung
187 //
188 // Source: IFTSTA AHB 2.1 + PID 4.0 (01.04.2026).
189 // These route to the Aktivierung workflow via conversation-ID lookup.
190 for &pid in aktivierung::IFTSTA_PIDS {
191 router.register(pid, aktivierung::WORKFLOW_NAME);
192 }
193
194 // Redispatch 2.0 MSCONS time-series data (PIDs 13020–13026).
195 //
196 // These carry Ausfallarbeit, meteorological data, and EEG
197 // transfer time-series correlated to the Aktivierung process.
198 for &pid in aktivierung::MSCONS_PIDS {
199 router.register(pid, aktivierung::WORKFLOW_NAME);
200 }
201
202 // Redispatch 2.0 ORDERS and ORDRSP PIDs (Ausfallarbeit / Abo-Verwaltung).
203 //
204 // ORDERS 17209/17210/17211: anfNB requests Ausfallarbeit or
205 // Lieferantenausfallarbeitsclearingliste, or files a Reklamation.
206 // ORDRSP 19204/19301/19302: BTR/ÜNB responds to subscription/aggregation requests.
207 for &pid in aktivierung::ORDERS_PIDS {
208 router.register(pid, aktivierung::WORKFLOW_NAME);
209 }
210 for &pid in aktivierung::ORDRSP_PIDS {
211 router.register(pid, aktivierung::WORKFLOW_NAME);
212 }
213 }
214
215 fn profile_requirements(&self) -> &'static [ProfileRequirement] {
216 &[
217 ProfileRequirement {
218 message_type: "IFTSTA",
219 label: "IFTSTA (Redispatch 2.0 Statusmeldungen — PIDs 21035/21036/21037/21038/21040)",
220 },
221 ProfileRequirement {
222 message_type: "MSCONS",
223 label: "MSCONS Redispatch Ausfallarbeit/EEG (13020–13023, 13026)",
224 },
225 ProfileRequirement {
226 message_type: "ORDERS",
227 label: "ORDERS Redispatch Ausfallarbeit (17209–17211)",
228 },
229 ProfileRequirement {
230 message_type: "ORDRSP",
231 label: "ORDRSP Redispatch Abo/Aggregation (19204, 19301–19302)",
232 },
233 ]
234 }
235
236 fn configure(&self) -> Result<(), String> {
237 // Verify that the router covers all document kinds that use kind-based routing.
238 // Acknowledgement is excluded: it is routed by correlation key, not
239 // by document kind (see build_router() doc comment).
240 let router = Self::build_router();
241 for dk in [
242 RedispatchDocumentKind::Activation,
243 RedispatchDocumentKind::PlannedResourceSchedule,
244 RedispatchDocumentKind::Stammdaten,
245 RedispatchDocumentKind::StatusRequest,
246 RedispatchDocumentKind::Unavailability,
247 RedispatchDocumentKind::NetworkConstraint,
248 RedispatchDocumentKind::Kaskade,
249 RedispatchDocumentKind::Kostenblatt,
250 ] {
251 router.route(dk).map_err(|e| format!("redispatch: {e}"))?;
252 }
253 Ok(())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn build_router_covers_all_primary_doc_types() {
263 let router = RedispatchModule::build_router();
264 // All document kinds that use document-kind routing must be registered.
265 // Acknowledgement is excluded: it uses correlation-key routing.
266 for dk in [
267 RedispatchDocumentKind::Activation,
268 RedispatchDocumentKind::PlannedResourceSchedule,
269 RedispatchDocumentKind::Stammdaten,
270 RedispatchDocumentKind::StatusRequest,
271 RedispatchDocumentKind::Unavailability,
272 RedispatchDocumentKind::Kaskade,
273 RedispatchDocumentKind::NetworkConstraint,
274 RedispatchDocumentKind::Kostenblatt,
275 ] {
276 assert!(
277 router.is_registered(dk),
278 "RedispatchDocumentKind {dk:?} must be registered in RedispatchModule router"
279 );
280 }
281 // Acknowledgement must NOT be registered — it is routed by correlation key.
282 assert!(
283 !router.is_registered(RedispatchDocumentKind::Acknowledgement),
284 "Acknowledgement must not be in the document-kind router"
285 );
286 }
287
288 #[test]
289 fn configure_succeeds() {
290 assert!(RedispatchModule.configure().is_ok());
291 }
292
293 #[test]
294 fn iftsta_pids_are_correct() {
295 // Confirmed from IFTSTA AHB 2.1 §8 and PID 4.0 (2026-04-01).
296 // Only PIDs 21037 (Ansicht NB/VNB) and 21038 (Ansicht BTR) belong to
297 // Redispatch 2.0. PIDs 21035 (GPKE Rückmeldung Lieferstelle → gpke-supplier-change),
298 // 21036 (WiM Strom Teil 1, unassigned), and 21040 (AWH Sperrprozesse Gas, unassigned)
299 // are not Redispatch PIDs — see docs/pid-reference.md.
300 assert_eq!(aktivierung::IFTSTA_PIDS, &[21_037, 21_038]);
301 }
302
303 #[test]
304 fn mscons_pids_are_correct() {
305 // Confirmed from MSCONS AHB (Redispatch 2.0 Annex) + PID 4.0.
306 assert_eq!(
307 aktivierung::MSCONS_PIDS,
308 &[13_020, 13_021, 13_022, 13_023, 13_026]
309 );
310 }
311
312 #[test]
313 fn workflow_names_are_non_empty() {
314 assert!(!RedispatchModule.workflow_names().is_empty());
315 for name in RedispatchModule.workflow_names() {
316 assert!(
317 name.starts_with("redispatch-"),
318 "workflow name '{name}' must start with 'redispatch-'"
319 );
320 }
321 }
322}