mako-wim 0.15.0

WiM process engine for German smart-meter market communication (Wechsel des Messstellenbetreibers)
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
//! WiM ESA Wertebestellung — **ESA origination side**.
//!
//! The mirror of [`super::wertebestellung`] (which is the MSB side). Here the
//! deployment **is** the Energieserviceanbieter: it *originates* the Werteanfrage
//! (REQOTE 35002), Bestellung (ORDERS 17007), Stornierung (ORDCHG 39002) and
//! Abbestellung (ORDERS 17008), and *receives* the MSB's answers (QUOTES 15003,
//! ORDRSP 19011/19012/19013/19014).
//!
//! §49 Abs. 2 Nr. 9 MsbG makes the ESA a consent-derived role: it may request a
//! location's values only while it holds a GDPR-Art.-7 Einwilligung. That guard
//! is enforced at the makod command boundary (the `esa_outbound` consent check)
//! before `SendWerteanfrage`/`SendBestellung` reach this pure workflow. GDPR
//! Art. 7(3) revocation drives `SendAbbestellung` (17008) — the only market
//! mechanism that stops a running delivery — which is therefore **not** gated.
//!
//! # Message flow
//!
//! ```text
//! ESA ──REQOTE 35002 Anfrage──────────────────────────────────────────▶ MSB
//! ESA ◀─QUOTES 15003 Angebot──────────── 5 WT nach ÜT der Anfrage ────── MSB
//! ESA ──ORDERS 17007 Bestellung──────────── bis Ablauf der Bindungsfrist ▶ MSB
//! ESA ◀─ORDRSP 19011 / 19012──────────── 2 WT nach ÜT der Bestellung ─── MSB
//!
//! (before delivery starts)
//! ESA ──ORDCHG 39002 Stornierung──────────────────────────────────────▶ MSB
//! ESA ◀─ORDRSP 19013 / 19014─────────── 2 WT nach ÜT der Stornierung ─── MSB
//!
//! (once delivery is running — the Art. 7(3) revocation path)
//! ESA ──ORDERS 17008 Abbestellung─────────────────────────────────────▶ MSB
//! ESA ◀─ORDRSP 19011 / 19012──────────── 2 WT nach ÜT der Abbestellung ─ MSB
//! ```

use mako_engine::{
    error::WorkflowError,
    ids::DeadlineId,
    outbox::PendingOutbox,
    types::{MarktpartnerCode, MessageRef, Pruefidentifikator},
    workflow::{CommandPayload, EventPayload, PendingDeadline, Workflow, WorkflowOutput},
};
use time::OffsetDateTime;

// Reuse the shared vocabulary from the MSB side so both directions speak the
// same PIDs, Fristen and location model.
pub use super::wertebestellung::{
    ABBESTELLUNG_PID, ABLEHNUNG_PID, ANFRAGE_PID, ANGEBOT_PID, ANTWORT_FRIST_WT,
    BEENDIGUNG_MSB_PID, BESTAETIGUNG_PID, BESTELLUNG_PID, Lokationsebene, STORNIERUNG_PID,
    STORNO_ABLEHNUNG_PID, STORNO_BESTAETIGUNG_PID, STS_BEENDET, Zustellquittung,
};

/// Workflow name used for PID routing and `WorkflowId` construction.
pub const WORKFLOW_NAME: &str = "esa-wertebestellung";

/// Deadline label for the Angebot the ESA awaits after its Anfrage (5 WT).
pub const ANGEBOT_WINDOW_LABEL: &str = "esa-wertebestellung-angebot";

/// Deadline label for the Bindungsfrist within which the ESA must order.
pub const BINDUNGSFRIST_LABEL: &str = "esa-wertebestellung-bindungsfrist";

/// Deadline label for the ORDRSP answer the ESA awaits (2 WT).
pub const ANTWORT_WINDOW_LABEL: &str = "esa-wertebestellung-antwort";

/// PIDs an ESA deployment receives inbound (MSB → ESA). Identical to
/// [`super::wertebestellung::ESA_INBOUND_PIDS`]; re-exported for routing clarity.
pub use super::wertebestellung::ESA_INBOUND_PIDS;

// ── Domain events ─────────────────────────────────────────────────────────────

/// Events emitted by the ESA-origination workflow.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum EsaWertebestellungEvent {
    /// REQOTE 35002 sent — the ESA asked the MSB for values.
    AnfrageGesendet {
        /// GLN of this ESA.
        esa: MarktpartnerCode,
        /// GLN of the MSB addressed.
        msb: MarktpartnerCode,
        /// Location level requested.
        ebene: Lokationsebene,
        /// MaLo-ID, ZPB or NeLo-ID.
        lokations_id: String,
        /// Reference of the outbound REQOTE.
        message_ref: MessageRef,
    },
    /// QUOTES 15003 Angebot received — the ESA may order until `bindungsfrist`.
    AngebotErhalten {
        /// Reference of the inbound QUOTES.
        message_ref: MessageRef,
        /// End of the MSB's Bindungsfrist.
        bindungsfrist: OffsetDateTime,
    },
    /// QUOTES 15003 Ablehnung received — the MSB refused the Anfrage; the process
    /// ends. (Distinguished from an Angebot by carrying no Bindungsfrist.)
    AnfrageAbgelehnt {
        /// Reason communicated by the MSB.
        reason: String,
    },
    /// ORDERS 17007 Bestellung sent.
    BestellungGesendet {
        /// Reference of the outbound ORDERS.
        message_ref: MessageRef,
    },
    /// ORDRSP 19011 received — delivery is authorised.
    BestellungBestaetigt {
        /// Reference of the inbound ORDRSP.
        message_ref: MessageRef,
    },
    /// ORDRSP 19012 received — the MSB refused the Bestellung; the process ends.
    BestellungAbgelehnt {
        /// Reason communicated by the MSB.
        reason: String,
    },
    /// ORDCHG 39002 Stornierung sent (before delivery began).
    StornierungGesendet {
        /// Reference of the outbound ORDCHG.
        message_ref: MessageRef,
    },
    /// ORDRSP 19013 received — the Stornierung was accepted; the order is void.
    StornierungBestaetigt {
        /// Reference of the inbound ORDRSP.
        message_ref: MessageRef,
    },
    /// ORDRSP 19014 received — the Stornierung was refused; the order stands.
    StornierungAbgelehnt {
        /// Reason communicated by the MSB.
        reason: String,
    },
    /// ORDERS 17008 Abbestellung sent (the Art. 7(3) revocation path).
    AbbestellungGesendet {
        /// Reference of the outbound ORDERS.
        message_ref: MessageRef,
        /// Date delivery is to stop.
        beendigung_zum: OffsetDateTime,
        /// Trigger — typically `einwilligung_widerrufen`.
        grund: String,
    },
    /// ORDRSP 19011 received for the Abbestellung — delivery has ended.
    AbbestellungBestaetigt {
        /// Reference of the inbound ORDRSP.
        message_ref: MessageRef,
    },
    /// ORDRSP 19012 received for the Abbestellung — the MSB refused to stop;
    /// delivery continues. Surfaced so the operator can escalate (refusing a
    /// GDPR-Art.-7(3) Widerruf is a compliance incident).
    AbbestellungAbgelehnt {
        /// Reason communicated by the MSB.
        reason: String,
    },
    /// First values arrived; the Stornierung window closes.
    LieferungBegonnen,
    /// IFTSTA 21042 received (UC 4.4) — the MSB has ended the value delivery.
    BeendetDurchMsb {
        /// Reference of the inbound IFTSTA.
        message_ref: MessageRef,
        /// Date the MSB stops delivering.
        beendigung_zum: OffsetDateTime,
        /// Reason communicated by the MSB, when present.
        reason: Option<String>,
    },
    /// A regulatory window elapsed without the awaited answer.
    FristVersaeumt {
        /// Deadline label that fired.
        label: String,
    },
}

impl EventPayload for EsaWertebestellungEvent {
    fn event_type(&self) -> &'static str {
        match self {
            Self::AnfrageGesendet { .. } => "EsaWertebestellungAnfrageGesendet",
            Self::AngebotErhalten { .. } => "EsaWertebestellungAngebotErhalten",
            Self::BestellungGesendet { .. } => "EsaWertebestellungBestellungGesendet",
            Self::BestellungBestaetigt { .. } => "EsaWertebestellungBestellungBestaetigt",
            Self::AnfrageAbgelehnt { .. } => "EsaWertebestellungAnfrageAbgelehnt",
            Self::BestellungAbgelehnt { .. } => "EsaWertebestellungBestellungAbgelehnt",
            Self::StornierungGesendet { .. } => "EsaWertebestellungStornierungGesendet",
            Self::StornierungBestaetigt { .. } => "EsaWertebestellungStornierungBestaetigt",
            Self::StornierungAbgelehnt { .. } => "EsaWertebestellungStornierungAbgelehnt",
            Self::AbbestellungGesendet { .. } => "EsaWertebestellungAbbestellungGesendet",
            Self::AbbestellungBestaetigt { .. } => "EsaWertebestellungAbbestellungBestaetigt",
            Self::AbbestellungAbgelehnt { .. } => "EsaWertebestellungAbbestellungAbgelehnt",
            Self::LieferungBegonnen => "EsaWertebestellungLieferungBegonnen",
            Self::BeendetDurchMsb { .. } => "EsaWertebestellungBeendetDurchMsb",
            Self::FristVersaeumt { .. } => "EsaWertebestellungFristVersaeumt",
        }
    }
}

// ── Domain state ──────────────────────────────────────────────────────────────

/// Business data carried from the Anfrage through the whole process.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EsaWertebestellungData {
    /// GLN of this ESA.
    pub esa: MarktpartnerCode,
    /// GLN of the MSB.
    pub msb: MarktpartnerCode,
    /// Location level requested.
    pub ebene: Lokationsebene,
    /// MaLo-ID, ZPB or NeLo-ID.
    pub lokations_id: String,
    /// Belegnummer of the ORDERS Bestellung this ESA sent. A later ORDCHG
    /// Stornierung references it (`RFF+ON`) so the MSB can correlate the
    /// cancellation — an ORDCHG carries no LOC.
    #[serde(default)]
    pub bestellung_ref: Option<String>,
}

/// State of an ESA-origination Wertebestellung process.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(tag = "status", content = "data")]
pub enum EsaWertebestellungState {
    /// No events yet.
    #[default]
    New,
    /// REQOTE sent; the ESA awaits an Angebot within 5 WT.
    AnfrageGesendet(Box<EsaWertebestellungData>),
    /// Angebot received; the ESA may order until the Bindungsfrist lapses.
    AngebotErhalten {
        /// Process data.
        data: Box<EsaWertebestellungData>,
        /// End of the MSB's Bindungsfrist.
        bindungsfrist: OffsetDateTime,
    },
    /// Bestellung sent; the ESA awaits an ORDRSP within 2 WT.
    BestellungGesendet(Box<EsaWertebestellungData>),
    /// Bestellung confirmed — delivery is authorised and may be running.
    Beliefert {
        /// Process data.
        data: Box<EsaWertebestellungData>,
        /// `true` once the first values arrived, which closes the Stornierung
        /// window (UC 4.3 Vorbedingung).
        lieferung_begonnen: bool,
    },
    /// Stornierung sent; the ESA awaits an ORDRSP 19013/19014 within 2 WT.
    StornierungGesendet(Box<EsaWertebestellungData>),
    /// Abbestellung sent; the ESA awaits an ORDRSP 19011 within 2 WT.
    AbbestellungGesendet(Box<EsaWertebestellungData>),
    /// Order cancelled before delivery began.
    Storniert(Box<EsaWertebestellungData>),
    /// Delivery ended (Abbestellung confirmed).
    Beendet(Box<EsaWertebestellungData>),
    /// Terminal rejection (Anfrage timed out or Bestellung refused).
    Abgelehnt {
        /// Reason.
        reason: String,
    },
}

impl EsaWertebestellungState {
    /// Stable string label for the current variant.
    #[must_use]
    pub const fn label(&self) -> &'static str {
        match self {
            Self::New => "New",
            Self::AnfrageGesendet(_) => "AnfrageGesendet",
            Self::AngebotErhalten { .. } => "AngebotErhalten",
            Self::BestellungGesendet(_) => "BestellungGesendet",
            Self::Beliefert { .. } => "Beliefert",
            Self::StornierungGesendet(_) => "StornierungGesendet",
            Self::AbbestellungGesendet(_) => "AbbestellungGesendet",
            Self::Storniert(_) => "Storniert",
            Self::Beendet(_) => "Beendet",
            Self::Abgelehnt { .. } => "Abgelehnt",
        }
    }

    /// `true` when delivery to the ESA is authorised (a confirmed Bestellung).
    #[must_use]
    pub const fn beliefert(&self) -> bool {
        matches!(self, Self::Beliefert { .. } | Self::AbbestellungGesendet(_))
    }

    /// Process data, when the process has advanced past `New`.
    #[must_use]
    pub const fn data(&self) -> Option<&EsaWertebestellungData> {
        match self {
            Self::AnfrageGesendet(d)
            | Self::BestellungGesendet(d)
            | Self::StornierungGesendet(d)
            | Self::AbbestellungGesendet(d)
            | Self::Storniert(d)
            | Self::Beendet(d) => Some(d),
            Self::AngebotErhalten { data, .. } | Self::Beliefert { data, .. } => Some(data),
            Self::New | Self::Abgelehnt { .. } => None,
        }
    }
}

// ── Domain commands ───────────────────────────────────────────────────────────

/// Commands for the ESA-origination workflow.
#[derive(Clone)]
pub enum EsaWertebestellungCommand {
    /// Originate REQOTE 35002 (UC 4.1 Nr. 1). Consent-gated at the makod
    /// boundary (`esa_outbound`) before it reaches this workflow.
    SendWerteanfrage {
        /// GLN of this ESA.
        esa: MarktpartnerCode,
        /// GLN of the MSB addressed.
        msb: MarktpartnerCode,
        /// Location level.
        ebene: Lokationsebene,
        /// MaLo-ID, ZPB or NeLo-ID.
        lokations_id: String,
        /// Reference of the outbound REQOTE.
        message_ref: MessageRef,
    },
    /// QUOTES 15003 Angebot received (UC 4.1 Nr. 2).
    ReceiveAngebot {
        /// Reference of the inbound QUOTES.
        message_ref: MessageRef,
        /// End of the MSB's Bindungsfrist.
        bindungsfrist: OffsetDateTime,
    },
    /// QUOTES 15003 Ablehnung received — the MSB refused the Anfrage.
    ReceiveAnfrageAblehnung {
        /// Reason communicated by the MSB.
        reason: Option<String>,
    },
    /// Originate ORDERS 17007 Bestellung (UC 4.1 Nr. 3). Consent-gated.
    SendBestellung {
        /// Reference of the outbound ORDERS.
        message_ref: MessageRef,
    },
    /// ORDRSP 19011 received — a **Bestätigung** of the Bestellung (UC 4.1 Nr. 4)
    /// or, once running, of the Abbestellung (UC 4.3 Nr. 2). One PID, resolved
    /// against the current state.
    ReceiveBestaetigung {
        /// Reference of the inbound ORDRSP.
        message_ref: MessageRef,
    },
    /// ORDRSP 19012 received — an **Ablehnung** of the Bestellung (ends the
    /// process) or of the Abbestellung (delivery continues). Resolved against
    /// the current state.
    ReceiveAblehnung {
        /// Reference of the inbound ORDRSP.
        message_ref: MessageRef,
        /// Reason communicated by the MSB.
        reason: Option<String>,
    },
    /// Originate ORDCHG 39002 Stornierung (UC 4.1 Nr. 5) before delivery began.
    SendStornierung {
        /// Reference of the outbound ORDCHG.
        message_ref: MessageRef,
    },
    /// ORDRSP 19013/19014 received answering the Stornierung (UC 4.1 Nr. 6).
    ReceiveStornierungAntwort {
        /// Prüfidentifikator of the inbound ORDRSP (19013 or 19014).
        pid: Pruefidentifikator,
        /// Reference of the inbound ORDRSP.
        message_ref: MessageRef,
        /// Reason, present on rejection (19014).
        reason: Option<String>,
    },
    /// Originate ORDERS 17008 Abbestellung (UC 4.3 Nr. 1) — the GDPR Art. 7(3)
    /// revocation path. **Not** consent-gated: it is the act of stopping.
    SendAbbestellung {
        /// Reference of the outbound ORDERS.
        message_ref: MessageRef,
        /// Date delivery is to stop.
        beendigung_zum: OffsetDateTime,
        /// Trigger — typically `einwilligung_widerrufen`.
        grund: String,
    },
    /// IFTSTA 21042 received (UC 4.4) — the MSB has ended the value delivery
    /// (STS 4405 = 105 „beendet"). Terminal; needs no ESA answer.
    ReceiveBeendigungDurchMsb {
        /// Reference of the inbound IFTSTA.
        message_ref: MessageRef,
        /// Date the MSB stops delivering.
        beendigung_zum: OffsetDateTime,
        /// Reason communicated by the MSB, when present.
        reason: Option<String>,
    },
    /// Mark the first values as delivered, closing the Stornierung window.
    MarkLieferungBegonnen,
    /// A registered deadline fired.
    TimeoutExpired {
        /// Unique deadline ID.
        deadline_id: DeadlineId,
        /// Deadline label.
        label: Box<str>,
    },
}

impl CommandPayload for EsaWertebestellungCommand {}

// ── Workflow ──────────────────────────────────────────────────────────────────

/// ESA-origination Wertebestellung workflow (WiM Strom Teil 2, Kapitel 4).
pub struct EsaWertebestellungWorkflow;

fn require_pid(
    pid: Pruefidentifikator,
    allowed: &[Pruefidentifikator],
    what: &str,
) -> Result<(), WorkflowError> {
    if allowed.contains(&pid) {
        Ok(())
    } else {
        let allowed: Vec<u32> = allowed.iter().map(|a| a.as_u32()).collect();
        Err(WorkflowError::rejected(format!(
            "{what} erwartet PID {allowed:?}, erhielt {pid}"
        )))
    }
}

impl Workflow for EsaWertebestellungWorkflow {
    type State = EsaWertebestellungState;
    type Event = EsaWertebestellungEvent;
    type Command = EsaWertebestellungCommand;

    fn apply(state: Self::State, event: &Self::Event) -> Self::State {
        use EsaWertebestellungEvent as E;
        use EsaWertebestellungState as S;
        match event {
            E::AnfrageGesendet {
                esa,
                msb,
                ebene,
                lokations_id,
                ..
            } => S::AnfrageGesendet(Box::new(EsaWertebestellungData {
                esa: esa.clone(),
                msb: msb.clone(),
                ebene: *ebene,
                lokations_id: lokations_id.clone(),
                bestellung_ref: None,
            })),
            E::AngebotErhalten { bindungsfrist, .. } => match state {
                S::AnfrageGesendet(data) => S::AngebotErhalten {
                    data,
                    bindungsfrist: *bindungsfrist,
                },
                other => other,
            },
            E::AnfrageAbgelehnt { reason } => match state {
                S::AnfrageGesendet(_) => S::Abgelehnt {
                    reason: reason.clone(),
                },
                other => other,
            },
            E::BestellungGesendet { message_ref } => match state {
                S::AngebotErhalten { mut data, .. } => {
                    data.bestellung_ref = Some(message_ref.as_str().to_owned());
                    S::BestellungGesendet(data)
                }
                other => other,
            },
            E::BestellungBestaetigt { .. } => match state {
                S::BestellungGesendet(data) => S::Beliefert {
                    data,
                    lieferung_begonnen: false,
                },
                other => other,
            },
            E::BestellungAbgelehnt { reason } => S::Abgelehnt {
                reason: reason.clone(),
            },
            E::StornierungGesendet { .. } => match state {
                S::Beliefert { data, .. } => S::StornierungGesendet(data),
                other => other,
            },
            E::StornierungBestaetigt { .. } => match state {
                S::StornierungGesendet(data) => S::Storniert(data),
                other => other,
            },
            // A refused Stornierung leaves the delivery running.
            E::StornierungAbgelehnt { .. } => match state {
                S::StornierungGesendet(data) => S::Beliefert {
                    data,
                    lieferung_begonnen: false,
                },
                other => other,
            },
            E::AbbestellungGesendet { .. } => match state {
                S::Beliefert { data, .. } => S::AbbestellungGesendet(data),
                other => other,
            },
            E::AbbestellungBestaetigt { .. } => match state {
                S::AbbestellungGesendet(data) => S::Beendet(data),
                other => other,
            },
            // UC 4.4: the MSB ended the delivery — terminal from any
            // delivery-authorised state.
            E::BeendetDurchMsb { .. } => match state {
                S::Beliefert { data, .. } | S::AbbestellungGesendet(data) => S::Beendet(data),
                other => other,
            },
            // A refused Abbestellung leaves delivery running.
            E::AbbestellungAbgelehnt { .. } => match state {
                S::AbbestellungGesendet(data) => S::Beliefert {
                    data,
                    lieferung_begonnen: true,
                },
                other => other,
            },
            E::LieferungBegonnen => match state {
                S::Beliefert { data, .. } => S::Beliefert {
                    data,
                    lieferung_begonnen: true,
                },
                other => other,
            },
            E::FristVersaeumt { .. } => match state {
                // Only an outstanding Angebot turns into a terminal rejection;
                // a missed ORDRSP is a process anomaly surfaced by the event but
                // does not collapse an authorised delivery.
                S::AnfrageGesendet(_) => S::Abgelehnt {
                    reason: "Angebot nicht innerhalb der Frist erhalten".to_owned(),
                },
                other => other,
            },
        }
    }

    fn handle(
        state: &Self::State,
        command: Self::Command,
    ) -> Result<WorkflowOutput<Self::Event>, WorkflowError> {
        // Build the outbound render intent that puts a message on the wire to
        // the MSB. The renderer turns this into REQOTE/ORDERS/ORDCHG with the
        // PID in BGM DE 1004 and the location in LOC.
        fn esa_send(
            message_type: &'static str,
            pid: Pruefidentifikator,
            data: &EsaWertebestellungData,
            message_ref: &MessageRef,
            order_reference: Option<&str>,
        ) -> PendingOutbox {
            PendingOutbox::new(
                message_type,
                data.msb.as_str(),
                serde_json::json!({
                    "pid": pid,
                    "sender": data.esa.as_str(),
                    "receiver": data.msb.as_str(),
                    "message_ref": message_ref.as_str(),
                    "location": data.lokations_id,
                    // The ORDCHG Stornierung carries no LOC; it references the
                    // original Bestellung's Belegnummer in `RFF+ON` instead so
                    // the MSB can correlate it.
                    "order_reference": order_reference,
                }),
            )
        }

        use EsaWertebestellungCommand as C;
        use EsaWertebestellungEvent as E;
        use EsaWertebestellungState as S;

        match command {
            C::SendWerteanfrage {
                esa,
                msb,
                ebene,
                lokations_id,
                message_ref,
            } => {
                if !matches!(state, S::New) {
                    return Err(WorkflowError::invalid_state("New", state.label()));
                }
                if lokations_id.trim().is_empty() {
                    return Err(WorkflowError::rejected(format!(
                        "Werteanfrage auf Ebene {} ohne Lokations-ID",
                        ebene.as_str()
                    )));
                }
                let data = EsaWertebestellungData {
                    esa: esa.clone(),
                    msb: msb.clone(),
                    ebene,
                    lokations_id: lokations_id.clone(),
                    bestellung_ref: None,
                };
                let outbox = esa_send("REQOTE", ANFRAGE_PID, &data, &message_ref, None);
                // The MSB owes an Angebot within 5 WT; arm the window from now
                // (the AS4 Receipt for our REQOTE is issued in the same request).
                let due = mako_engine::fristen::deadline_at_werktage(
                    OffsetDateTime::now_utc(),
                    super::wertebestellung::ANGEBOT_FRIST_WT,
                    mako_engine::fristen::HolidayCalendar::BdewMaKo,
                );
                Ok(WorkflowOutput {
                    events: vec![E::AnfrageGesendet {
                        esa,
                        msb,
                        ebene,
                        lokations_id,
                        message_ref,
                    }],
                    outbox: vec![outbox],
                    deadlines: vec![PendingDeadline::new(ANGEBOT_WINDOW_LABEL, due)],
                })
            }

            C::ReceiveAngebot {
                message_ref,
                bindungsfrist,
            } => {
                if !matches!(state, S::AnfrageGesendet(_)) {
                    return Err(WorkflowError::invalid_state(
                        "AnfrageGesendet",
                        state.label(),
                    ));
                }
                Ok(WorkflowOutput {
                    events: vec![E::AngebotErhalten {
                        message_ref,
                        bindungsfrist,
                    }],
                    outbox: Vec::new(),
                    deadlines: vec![PendingDeadline::new(BINDUNGSFRIST_LABEL, bindungsfrist)],
                })
            }

            C::ReceiveAnfrageAblehnung { reason } => {
                if !matches!(state, S::AnfrageGesendet(_)) {
                    return Err(WorkflowError::invalid_state(
                        "AnfrageGesendet",
                        state.label(),
                    ));
                }
                Ok(WorkflowOutput::events(vec![E::AnfrageAbgelehnt {
                    reason: reason.unwrap_or_else(|| "Anfrage vom MSB abgelehnt".to_owned()),
                }]))
            }

            C::SendBestellung { message_ref } => {
                let S::AngebotErhalten {
                    data,
                    bindungsfrist,
                } = state
                else {
                    return Err(WorkflowError::invalid_state(
                        "AngebotErhalten",
                        state.label(),
                    ));
                };
                // UC 4.1 Nr. 3: order only within the MSB's Bindungsfrist.
                if OffsetDateTime::now_utc() > *bindungsfrist {
                    return Err(WorkflowError::rejected(format!(
                        "Bindungsfrist des Angebots endete am {bindungsfrist}"
                    )));
                }
                let outbox = esa_send("ORDERS", BESTELLUNG_PID, data, &message_ref, None);
                let due = mako_engine::fristen::deadline_at_werktage(
                    OffsetDateTime::now_utc(),
                    ANTWORT_FRIST_WT,
                    mako_engine::fristen::HolidayCalendar::BdewMaKo,
                );
                Ok(WorkflowOutput {
                    events: vec![E::BestellungGesendet { message_ref }],
                    outbox: vec![outbox],
                    deadlines: vec![PendingDeadline::new(ANTWORT_WINDOW_LABEL, due)],
                })
            }

            C::ReceiveBestaetigung { message_ref } => match state {
                // ORDRSP 19011 confirms the Bestellung → delivery authorised.
                S::BestellungGesendet(_) => {
                    Ok(WorkflowOutput::events(vec![E::BestellungBestaetigt {
                        message_ref,
                    }]))
                }
                // ORDRSP 19011 confirms the Abbestellung → delivery ended.
                S::AbbestellungGesendet(_) => {
                    Ok(WorkflowOutput::events(vec![E::AbbestellungBestaetigt {
                        message_ref,
                    }]))
                }
                _ => Err(WorkflowError::invalid_state(
                    "BestellungGesendet|AbbestellungGesendet",
                    state.label(),
                )),
            },

            C::ReceiveAblehnung {
                message_ref: _,
                reason,
            } => match state {
                // ORDRSP 19012 refuses the Bestellung → the process ends.
                S::BestellungGesendet(_) => {
                    Ok(WorkflowOutput::events(vec![E::BestellungAbgelehnt {
                        reason: reason.unwrap_or_else(|| "ohne Begründung".to_owned()),
                    }]))
                }
                // ORDRSP 19012 refuses the Abbestellung → delivery continues.
                S::AbbestellungGesendet(_) => {
                    Ok(WorkflowOutput::events(vec![E::AbbestellungAbgelehnt {
                        reason: reason.unwrap_or_else(|| "ohne Begründung".to_owned()),
                    }]))
                }
                _ => Err(WorkflowError::invalid_state(
                    "BestellungGesendet|AbbestellungGesendet",
                    state.label(),
                )),
            },

            C::SendStornierung { message_ref } => {
                let S::Beliefert {
                    data,
                    lieferung_begonnen,
                } = state
                else {
                    return Err(WorkflowError::invalid_state("Beliefert", state.label()));
                };
                if *lieferung_begonnen {
                    return Err(WorkflowError::rejected(
                        "Stornierung ist nach Lieferbeginn nicht mehr möglich \
                         (UC 4.3 Vorbedingung) — nutze die Abbestellung (17008)",
                    ));
                }
                let outbox = esa_send(
                    "ORDCHG",
                    STORNIERUNG_PID,
                    data,
                    &message_ref,
                    data.bestellung_ref.as_deref(),
                );
                let due = mako_engine::fristen::deadline_at_werktage(
                    OffsetDateTime::now_utc(),
                    ANTWORT_FRIST_WT,
                    mako_engine::fristen::HolidayCalendar::BdewMaKo,
                );
                Ok(WorkflowOutput {
                    events: vec![E::StornierungGesendet { message_ref }],
                    outbox: vec![outbox],
                    deadlines: vec![PendingDeadline::new(ANTWORT_WINDOW_LABEL, due)],
                })
            }

            C::ReceiveStornierungAntwort {
                pid,
                message_ref,
                reason,
            } => {
                if !matches!(state, S::StornierungGesendet(_)) {
                    return Err(WorkflowError::invalid_state(
                        "StornierungGesendet",
                        state.label(),
                    ));
                }
                require_pid(
                    pid,
                    &[STORNO_BESTAETIGUNG_PID, STORNO_ABLEHNUNG_PID],
                    "Antwort auf Stornierung",
                )?;
                if pid == STORNO_BESTAETIGUNG_PID {
                    Ok(WorkflowOutput::events(vec![E::StornierungBestaetigt {
                        message_ref,
                    }]))
                } else {
                    Ok(WorkflowOutput::events(vec![E::StornierungAbgelehnt {
                        reason: reason.unwrap_or_else(|| "ohne Begründung".to_owned()),
                    }]))
                }
            }

            C::SendAbbestellung {
                message_ref,
                beendigung_zum,
                grund,
            } => {
                let S::Beliefert { data, .. } = state else {
                    return Err(WorkflowError::invalid_state("Beliefert", state.label()));
                };
                // UC 4.3 Nr. 1: the Abbestellung (17008) ends a running delivery.
                let outbox = esa_send("ORDERS", ABBESTELLUNG_PID, data, &message_ref, None);
                let due = mako_engine::fristen::deadline_at_werktage(
                    OffsetDateTime::now_utc(),
                    ANTWORT_FRIST_WT,
                    mako_engine::fristen::HolidayCalendar::BdewMaKo,
                );
                Ok(WorkflowOutput {
                    events: vec![E::AbbestellungGesendet {
                        message_ref,
                        beendigung_zum,
                        grund,
                    }],
                    outbox: vec![outbox],
                    deadlines: vec![PendingDeadline::new(ANTWORT_WINDOW_LABEL, due)],
                })
            }

            C::MarkLieferungBegonnen => {
                if !matches!(state, S::Beliefert { .. }) {
                    return Err(WorkflowError::invalid_state("Beliefert", state.label()));
                }
                Ok(WorkflowOutput::events(vec![E::LieferungBegonnen]))
            }

            C::ReceiveBeendigungDurchMsb {
                message_ref,
                beendigung_zum,
                reason,
            } => {
                // UC 4.4: only a delivery-authorised process can be ended by the
                // MSB. Idempotent if already Beendet.
                if !state.beliefert() && !matches!(state, S::Beendet(_)) {
                    return Err(WorkflowError::invalid_state("Beliefert", state.label()));
                }
                Ok(WorkflowOutput::events(vec![E::BeendetDurchMsb {
                    message_ref,
                    beendigung_zum,
                    reason,
                }]))
            }

            C::TimeoutExpired { label, .. } => {
                Ok(WorkflowOutput::events(vec![E::FristVersaeumt {
                    label: label.to_string(),
                }]))
            }
        }
    }
}