mako-wim 0.20.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
# mako-wim

**WiM — Wechselprozesse im Messwesen, Strom und Gas**

Process engine workflows for the German metering-point-operator change
processes in **both Sparten**: **BK6-24-174 Anlagen 2a/2b** (WiM Strom Teil 1
and Teil 2) and the **AWH WiM Gas 2.0** (gültig ab 01.10.2026), plus the
EDI@Energy AHBs that carry them.

A **Prüfidentifikator** (PID) is the five-digit BDEW code every message in these
processes carries. It names the exact Anwendungsfall, and with it the rules, the
Frist and the answer tree that apply — so the PID, not the EDIFACT message type,
is what routes.

## One engine, two Sparten

AWH WiM Gas 2.0 restates WiM Strom Teil 1 use-case for use-case, Frist for
Frist. Only the UTILMD PID namespace splits (55xxx / 44xxx); ORDERS, ORDRSP,
REQOTE, QUOTES, IFTSTA and INSRPT are Sparte-neutral AHBs carrying both. The
Sparte comes from the **recipient MP-ID** (BDEW Allgemeine Festlegungen §2.13)
and decides four things:

| | Strom | Gas |
|---|---|---|
| Antwort-Codeliste | `S_00xx` | `G_00xx` |
| Zuordnungszeitpunkt | 00:00 Uhr | **06:00 Uhr** (Gastag) |
| APERAK | positiv **und** negativ, 45 min für UTILMD/ORDERS | **nur negativ**; nächster WT 12:00 (Folgeprozess) / 3 WT (Initialprozess) |
| Störungs-Fristen | nach Messtechnik | flach |

## Fristen

Three clocks run on an inbound MSB-Wechsel order, and they are separate
messages with separate commands:

| Clock | Window | Message | Source |
|---|---|---|---|
| **APERAK** — technical acknowledgement | Strom **45 Minuten** (UTILMD/ORDERS am Werktag); Gas nächster WT 12:00 bzw. **3 WT** auf einem Initialprozess | APERAK BGM+312/313 (Gas: nur 313) | APERAK AHB 1.1 §2.3.1/§2.4.1 |
| **Antwort** — business Bestätigung/Ablehnung | **per PID**, in beiden Sparten gleich: 55039·44039 → 3 WT, 55042·44042 → 5 WT, 55051·44051 → 7 WT, 55168·44168 → 1 WT | the Antwort-PID's UTILMD | WiM Teil 1 Kap. 2.2.2 / 2.3.2 / 2.4.2 · AWH WiM Gas 2.0 Kap. 3.3.2 / 3.5.2 / 3.6.2 |
| **Vorlauffrist** — was the requested date admissible? | 15 / 7 WT (Anmeldung), 20 WT (Abmeldung), ±9 WT Realisierungskorridor | the date inside the message | WiM Teil 1 Kap. 2.3.2 Nr. 1 / 2.4.2 Nr. 1 |

Only the second discharges the Antwortfrist. The business window comes from
`antwort_frist_werktage(pid)` and the Vorlauffristen from
`mako_fristen::vorlauf` — never a flat value. **Saturday is not a Werktag**
(GPKE Teil 1 Kap. 1.7, which WiM Teil 1 §1.7 defers to), and 24.12. and 31.12.
count as holidays.

## Antwortcodes

Every answer carries `SG4 STS+E01` (UTILMD) or `SG2 AJT` (ORDRSP) with a code
from the process's own Entscheidungsbaum. The catalogue and the executable
Prüfschritte are `mako-pruefung` (`role-msb`); this crate resolves against it
before anything reaches the outbox, so a code from the wrong tree is refused at
the command rather than sent.

**DE 1131 / DE 1082 names the Codeliste, not the Entscheidungsbaum.** The AHB
column reads „Codeliste Strom Nr. `S_0090`" for every WiM MSB-Wechsel answer,
and the *cluster* picks which of the pair. `AntwortCode::wire_codeliste()`
returns the wire value.

| Process | EBD Strom / Gas | Bestätigung / Ablehnung | Codeliste Strom | Gas |
|---|---|---|---|---|
| Kündigung MSB | `E_0200` / `E_2000` | `E15` `Z01` `Z44` / `E11` `Z12` `Z29` `Z34` (+`ZC9` Strom) | `S_0090` / `S_0054` | `G_0052` / `G_0051` |
| Anmeldung MSB | `E_0201` / `E_2002` | `E15` `Z01` `Z44` / `E11` `E17` `Z09` `Z29` `ZB6` (+`ZC9` Strom) | `S_0055` / `S_0056` | `G_0054` / `G_0053` |
| Ende MSB | `E_0202` / `E_2005` | `E15` `Z01` / `E17` `Z09` | `S_0059` / `S_0060` | `G_0058` / `G_0057` |
| Verpflichtungsanfrage | `E_0240` / `E_2006` | `E15` `Z01` `Z44` / `E17` `Z07` `Z09` `ZB6` | `S_0063` / `S_0064` | `G_0070` / `G_0071` |
| Gesamtvorgang | `E_0232` / `E_2003` | — / `Z66` | `S_0057` | `G_0055` |
| Weiterverpflichtung | `E_0203` / `E_2004` | `Z13` `Z14` / `Z22` | `S_0061` / `S_0062` | `G_0072` / `G_0073` |
| Gerätewechselabsicht | `E_0204` / `E_2007` | `ZB4` / `ZB5`; `E17` `Z07` in beiden | `S_0065` / `S_0066` | `G_0059` / `G_0060` |
| Bestellung Geräteübernahme | `E_0247` / `E_2011` | `Z13` / `5` `Z32` | `S_0067` / `S_0068` | `G_0061` / `G_0074` |
| Messlokationsänderung, direkt beauftragt | `E_0249` (NB) / `E_0250` (LF) | `A02` / `A01` (+ `A03` `A04`) | `E_0249` / `E_0250` | — |
| Anfrage Änderung der Technik (35005) | `E_0278` (NB) / `E_0281` (LF) | — / `A01`–`A04` `A99` resp. `A01`–`A06` `A99` | `E_0278` / `E_0281` | — |
| Bestellung Änderung der Technik nach Angebot | `E_0279` (NB) / `E_0283` (LF) | `A06` / `A01`–`A05` `A99` | `E_0279` / `E_0283` | — |
| Messlokationsänderung durchführen | `E_0286` | — / `A01` `A02` `A03` `A99` | `E_0286` | — |
| Ersteinbau iMS (21029 → 21030/21031) | `E_0233` | `A03` / `A01` `A02` `A04` | `E_0233` | — |

None of these alphabets is a GPKE one — `A02` and `A05` appear in no
MSB-Wechsel tree — and the Gas lists are not the Strom lists.

## PID Inventory

> Legend: **✅ Implemented** — full state machine + AHB rule enforcement, production-safe.
> **⚠️ Registered** — PID routes to the workflow; `handle()` returns
> `WorkflowError::NotImplemented` for unhandled commands (no silent data loss).
> **✗ Not registered** — PID is not in the router; inbound messages are dead-lettered.

### MSB-Wechsel — UTILMD (WiM Strom Teil 1 Kap. 2)

| PID   | Process name                                    | EDIFACT       | Module           | Status                          |
|-------|-------------------------------------------------|---------------|------------------|---------------------------------|
| 55042 | Anmeldung MSB (MSBN → NB)                       | UTILMD S2.x   | `geraetewechsel` | ✅ Implemented · Antwort 55043/55044, **5 WT** (*vorläufig*) |
| 55039 | Kündigung MSB (MSBN → **MSBA**)                 | UTILMD S2.x   | `geraetewechsel` | ✅ Implemented · Antwort 55040/55041, **3 WT** |
| 55051 | Ende MSB / Abmeldung (**MSBA → NB**)            | UTILMD S2.x   | `geraetewechsel` | ✅ Implemented · Antwort 55052/55053, **7 WT** |
| 55168 | Verpflichtungsanfrage / Aufforderung (NB → **gMSB**) | UTILMD S2.x | `geraetewechsel` | ✅ Implemented · Antwort 55169/55170, **1 WT** |
| 44039 | Kündigung MSB Gas (MSBN → **MSBA**)             | UTILMD G1.x   | `geraetewechsel` | ✅ Implemented · Antwort 44040/44041, **3 WT** |
| 44042 | Anmeldung MSB Gas (MSBN → NB)                   | UTILMD G1.x   | `geraetewechsel` | ✅ Implemented · Antwort 44043/44044, **5 WT** (*vorläufig*) |
| 44051 | Ende MSB Gas (**MSBA → NB**)                    | UTILMD G1.x   | `geraetewechsel` | ✅ Implemented · Antwort 44052/44053, **7 WT** |
| 44168 | Verpflichtungsanfrage Gas (NB → **gMSB**)       | UTILMD G1.x   | `geraetewechsel` | ✅ Implemented · Antwort **44169 only** — no Ablehnungs-PID exists, **1 WT** |
| 44183 | Ende MSB von NB (Stilllegung, NB → MSB)         | UTILMD G1.x   | `geraetewechsel` | ✅ Implemented · informational, no answer |

### Mitteilung über Gesamtvorgang — IFTSTA

The Anmeldebestätigung 55043 is *vorläufig*. WiM Teil 1 Kap. 2.1.1: the NB assigns
the MSBN „zu dem Tag des vom MSBN mitgeteilten Termins des erfolgreichen Abschlusses
des Gesamtvorgangs … mit dem Zeitpunkt 00:00 Uhr", and the MSBA's assignment ends at
the same instant. AWH WiM Gas 2.0 Kap. 3.1.1 says the same with **06:00 Uhr** —
the Gastag boundary. This leg is what makes the Wechsel constitutive.

| PID   | Process name                                        | Von → An            | Frist |
|-------|-----------------------------------------------------|---------------------|-------|
| 21010 | Statusmeldung (**erfolgreich**), `DTM+2380`         | MSBN → NB           | 10 WT nach dem bestätigten Zuordnungsbeginn |
| 21009 | Statusmeldung (**gescheitert**)                     | MSBN → NB           | — |
| 21012 | Statusmeldung (erfolgreich) — die Zuordnung         | NB → MSBN           | 1 WT |
| 21011 | Statusmeldung (MSB-Scheitermeldung, `Z66`)          | NB → MSBN/MSBA/LF   | 1 WT |
| 21013 | Statusmeldung (gescheitert) — keine Meldung eingegangen | NB → MSBN/MSBA/LF | 11 WT |

> The numeric order is the reverse of the reading order: **21009 is the failure and
> 21010 the success** (IFTSTA AHB 2.1 § 6.2).
>
> Every failure path leaves the MSBA assigned. `marktd` derives the per-Messlokation
> MSB timeline from 21012 alone.

### Geräteübernahme — ORDERS / ORDRSP

| PID(s)       | Process name                                      | EDIFACT       | Module               | Status          |
|--------------|---------------------------------------------------|---------------|----------------------|-----------------|
| 17001        | Bestellung Geräteübernahmeangebot (MSBN → MSBA)   | ORDERS 1.4b   | `geraeteubernahme`   | ✅ Implemented · Antwort 19001/19002, **2 WT** |
| 17009        | Anzeige Gerätewechselabsicht (MSBN → MSBA)        | ORDERS 1.4b   | `geraeteubernahme`   | ✅ Implemented · Antwort 19015/19016, **2 WT vor dem Wechseltermin**; ein Termin näher als 4 WT wird mit `E17` abgelehnt |
| 19001, 19002 | ORDRSP Bestellbestätigung / Ablehnung (MSBA → MSBN) | ORDRSP 1.4c | `geraeteubernahme`   | ✅ Registered (nMSB role only) |
| 19015, 19016 | ORDRSP Eigenausbau ja/nein (MSBA → MSBN)          | ORDRSP 1.4c   | `geraeteubernahme`   | ✅ Registered (nMSB role only) |
| 17002 → 19003/19004 | Weiterverpflichtung des MSB (**NB → MSBA**) | ORDERS 1.4b   | `weiterverpflichtung`| ✅ Implemented · **1 WT**, `E_0203` |

> The **Anforderung** eines Geräteübernahmeangebots is REQOTE 35001, answered by
> QUOTES 15001 within **4 WT** — `preisanfrage` owns that leg.
>
> 19016 is named „Ablehnung Gerätewechselabsicht" but carries `ZB5` „Kein
> Eigenausbau des MSBA": it settles who removes the old device, not whether the
> Gerätewechsel happens.

> PIDs 19001/19002/19015/19016 are only registered when `DeploymentRoles` includes `Marktrolle::Nmsb`.
> On NB instances these PIDs belong to `mako-gpke` (GPKE Konfiguration). Never register both simultaneously.
>
> All six ORDERS/ORDRSP PIDs carry the Strom **and** the Gas Use-Case; the
> recipient MP-ID's Sparte picks `E_0247`/`E_0204` against `E_2011`/`E_2007`.

### Stammdaten — ORDERS

| PID(s)        | Process name                                     | EDIFACT     | Module       | Status         |
|---------------|--------------------------------------------------|-------------|--------------|----------------|
| 17132         | Stammdaten Anforderung Strom (NB → MSB)          | ORDERS 1.4b | `stammdaten` | ✅ Implemented |
| 17102–17133   | Stammdatenübermittlung responses (MSB → NB)      | ORDERS 1.4b | `stammdaten` | ✅ Implemented |

### Weitere Prozesse

| PID(s)                 | Process name                          | EDIFACT         | Module             | Status         |
|------------------------|---------------------------------------|-----------------|--------------------|----------------|
| 39002                  | ESA Stornierung der Bestellung von Werten (ORDCHG) | ORDCHG 1.1 | `wertebestellung`  | ✅ Implemented |
| 31009                  | MSB-Rechnung (MSB → NB/LF/ESA)        | INVOIC 2.8e     | `invoic`           | ✅ Implemented (send + receive) · Antwort **zum Zahlungsziel**, an den NB **4. WT davor** |
| 33001–33004 (REMADV)   | Zahlungsavis / itemized Abweisung     | REMADV 1.0a     | `invoic`           | ✅ Implemented (33003/34 = Strom Kopf+Summe / Position) |
| 29001 (COMDIS)         | Ablehnung REMADV                      | COMDIS 1.0      | `invoic`           | ✅ Implemented · **2. WT vor dem Zahlungsziel** |
| 35001 → 15001 (REQOTE/QUOTES) | Anforderung Geräteübernahmeangebot (MSBN → MSBA) | REQOTE 1.3c | `preisanfrage` | ✅ Implemented · **4 WT** |
| 35002 → 15002 | Anfrage Rechnungsabwicklung über den LF (LF → MSB) | REQOTE 1.3c | `preisanfrage` | ✅ Implemented · **5 WT** |
| 35004 → 15004 | Anfrage einer Konfiguration (GPKE Teil 3, NB/LF → MSB) | REQOTE 1.3c | `preisanfrage` | ✅ Implemented · **2 WT** |
| 35005 → 15005 | Anfrage Angebot Änderung Technik (NB/LF → MSB) | REQOTE 1.3c | `preisanfrage` | ✅ Implemented · **10 WT** |
| 17005/17006 → 19009/19010 | Rechnungsabwicklung MSB über LF | ORDERS/ORDRSP | `rechnungsabwicklung` | ✅ Implemented · **8 WT** |
| 27001–27003            | Preisliste (PRICAT)                   | PRICAT 2.1      | `preisliste`       | ✅ Implemented · Preisblatt an LF **3 Monate** vor Wirksamwerden, an NB initial **3 WT** / bei Änderung **20 WT** vor Inkrafttreten |
| 23001, 23003, 23004, 23008 | Störungsmeldung / Antwort / Ergebnisbericht | INSRPT 1.1a | `insrpt`     | ✅ Implemented, **beide Seiten** · Antwort Strom **3 WT** (kME ohne RLM, mME) / **1 WT** (kME mit RLM, iMS), Gas flach **3 WT**; Ergebnis Strom **7/4/2 WT** nach Messtechnik und Spannungsebene, Gas flach **7 WT** |
| 23005, 23009           | Informationsmeldungen an den NB (Gas) | INSRPT 1.1a     | `insrpt`           | ✅ Implemented |
| 23011, 23012           | Weiterleitung an betroffene Marktlokationen (Strom) | INSRPT 1.1a | `insrpt`     | ✅ Implemented · **1 WT**, und das Fenster überlebt den Ergebnisbericht — die 23012 ist danach fällig |
| 31003 → 33001/33002    | WiM-Rechnung (MSBA → NB / MSBN), beide Sparten | INVOIC 2.8e | `invoic`        | ✅ Implemented · Rechnung **20. WT** nach Leistungsende, Antwort **zum Zahlungsziel** |
| 31004 → 33001/33002    | Stornorechnung, Sparte-neutral        | INVOIC 2.8e     | `invoic`           | ✅ Implemented |
| 11021–11023            | iMS Bestellung (Universalbestellprozess) | REST/JSON    | `steuerungsauftrag`| ✅ Implemented (API-Webdienste channel) |

## EDIFACT Format Versions

WiM rides eleven message types and they do **not** share a release cadence, so
there is no single WiM format version. The profiles this crate validates against:

| Message | `FV2025-10-01` | `FV2026-10-01` |
|---------|----------------|----------------|
| UTILMD Strom | AHB 2.1 (MIG S2.1) | AHB 2.2 (MIG S2.2) |
| REQOTE | AHB 1.1 (MIG 1.3c) | AHB 1.2 (MIG 1.3c) |
| QUOTES | AHB 1.1 (MIG 1.3b) | AHB 1.1a (MIG 1.3c) |
| PRICAT | AHB 2.0f (MIG 2.0e) | AHB 2.1 (MIG 2.1) |
| IFTSTA | AHB 2.0h (MIG 2.0g) | AHB 2.1 (MIG 2.1) |
| ORDERS / ORDRSP | — (`FV2026-04-01`: AHB 1.1a, MIG 1.4b) | AHB 1.1b (MIG 1.4c) |

INSRPT ships only at `FV2026-01-01` (AHB 1.1g, MIG 1.1a), INVOIC and REMADV from
`FV2026-04-01`, ORDCHG at `FV2025-04-01` and `FV2026-10-01`. The **AHB and the
MIG carry different version numbers** for every message type except UTILMD — the
tables above name the MIG release, which is what the „EDIFACT" column of the PID
inventories cites.

## Modules

| Rust module        | Contents                                                                  |
|--------------------|---------------------------------------------------------------------------|
| `geraetewechsel`   | PIDs 55039/55042/55051/55168 and their Gas twins 44039/44042/44051/44168, plus 44183 and IFTSTA 21007/21009–21013/21015/21018/21036, of which 21009–21013 are the Gesamtvorgang leg (`GESAMTVORGANG_PIDS`) — MSB-Wechsel workflow + projection. Handles both directions: inbound UTILMD (`ReceiveUtilmd` → APERAK → `DispatchAntwort` → `ReceiveGesamtvorgang` → `DispatchZuordnung`) and ERP-initiated outbound orders (`InitiateDeviceChange` → `ReceiveAntwort` → `MeldeGesamtvorgang` → `ReceiveZuordnungsantwort`). Antwortfrist per process via `antwort_frist_werktage()`; the Realisierungskorridor is enforced on the Gesamtvorgang date. |
| `geraeteubernahme` | ORDERS 17001 → ORDRSP 19001/19002 (Bestellbestätigung/Ablehnung) and ORDERS 17009 → 19015/19016 (Eigenausbau ja/nein) — WiM Teil 1 Kap. 3.1/3.2. The 17009 Mindestvorlaufzeit is checked against `mako_fristen::vorlauf` before the answer window opens: a Gerätewechseltermin closer than the 4. Werktag is refused with `E17` naming the earliest date still reachable, rather than accepted with a window that expired before the message arrived |
| `weiterverpflichtung` | ORDERS 17002 → ORDRSP 19003/19004 — the NB keeping the abgebender MSB on the Messlokation while the gMSB prepares to take over (Kap. 2.4.2 Nr. 5/6, `E_0203`) |
| `technik_aenderung` | REQOTE 35005 → QUOTES 15005 / IFTSTA 21033, ORDERS 17011/17118 → ORDRSP 19005/19006, IFTSTA 21025/21027 — Messlokationsänderung auf **beiden** Wegen; **10 WT** Antwort, **20 WT** Vorlauffrist nur auf der direkten Beauftragung (Kap. 3.3 / AWH Änd. Technik) |
| `ersteinbau` | IFTSTA 21029 → 21030/21031 — Ersteinbau eines iMS in eine bestehende Messlokation, **3 WT** Antwort aus `E_0233` (Kap. 3.5, Strom only) |
| `stammdaten`       | PIDs 17102–17133, 17132 — Stammdaten Anforderung / Übermittlung           |
| `wertebestellung`  | PIDs 35003/15003/17007/17008, ORDCHG 39002 (Stornierung, answered by ORDRSP 19013/19014), ORDRSP 19011/19012, IFTSTA 21042 — **ESA Wertebestellung** (WiM Teil 2 Kap. 4): Anfrage → Angebot → Bestellung → Stornierung/Abbestellung, plus MSB-initiated termination. Fristen keyed on the positive AS4-Zustellquittung (ÜT); answers carry an `E_0254`/`E_0256`/`E_0257` Antwortcode. |
| `invoic`           | INVOIC 31009 (MSB-Rechnung, Strom) · 31003 (Dienstleistungen, beide Sparten) · 31004 (Stornorechnung, Sparte-neutral); inbound REMADV 33001–33004 and COMDIS 29001 — see below |
| `preisanfrage`     | PIDs 35001/35002/35004/35005 (REQOTE), 15001/15002/15004/15005 (QUOTES) — Preisanfrage            |
| `preisliste`       | PIDs 27001–27003 — Preisliste PRICAT                                      |
| `esa_wertebestellung` | The **ESA side** of the same relationship (`esa-wertebestellung`): it sends 35003/17007/17008/39002 and ingests the MSB's 15003/19011–19014/21042/13027. Disjoint commands from `wertebestellung`, so one deployment may hold both roles |
| `insrpt`           | INSRPT 23001 → 23003/23004 (Entscheidungen), 23005/23009/23011/23012 (Informationsmeldungen), 23008 (Ergebnisbericht) — Störungsmeldung, both sides |
| `rechnungsabwicklung` | ORDERS 17005/17006 → ORDRSP 19009/19010 — Rechnungsabwicklung des MSB über den LF, **8 WT** (`E_0205`–`E_0209`) |
| `steuerungsauftrag`| PIDs 11021–11023 — iMS Steuerungsauftrag (API-Webdienste REST channel)    |

### The `invoic` family

Both sides run here: the **MSB** sends with `SendInvoic` and awaits the REMADV;
the **NB/LF/ESA** ingests with `ReceiveInvoic`, then settles or disputes.

Replies correlate by conversation ID (`RFF+Z13` → the 31009 reference), so they
resume this family even though the shared REMADV PID statically resolves to
GPKE. The state machine itself is `mako-invoic`'s, shared with the GPKE, GaBi
Gas and GeLi Gas billing families; this module declares only the family and the
Gas Ablehnungs-Entscheidungsbaum.

## Usage

```rust
use mako_wim::{WimDeviceChangeWorkflow, DeviceChangeCommand};
use mako_engine::{builder::EngineBuilder, event_store::InMemoryEventStore};

// In tests (requires `testing` feature or `#[cfg(test)]`):
#[cfg(test)]
let ctx = EngineBuilder::new()
    .with_event_store(InMemoryEventStore::new())
    .build();

// In production, explicitly provide all stores:
// let ctx = EngineBuilder::with_stores(outbox, deadline, registry)
//     .with_event_store(my_slatedb_store)
//     .build();

let process = ctx.spawn::<WimDeviceChangeWorkflow>(tenant_id, workflow_id);
let events = process.execute(DeviceChangeCommand::ReceiveUtilmd {
    pid: 55042,  // Anmeldung MSB (nMSB → NB)
    // …
}).await?;
```

## Geräteübernahme (WiM Teil 1, Kapitel 3.2)

| Step | Direction | Message | PID | Frist |
|---|---|---|---|---|
| 1 Anforderung Geräteübernahmeangebot | MSBN → MSBA | REQOTE | — | — |
| 2 Geräteübernahmeangebot | MSBA → MSBN | QUOTES | 15001 | **4 WT** nach ÜT von Nr. 1 |
| 3 Bestellung | MSBN → MSBA | ORDERS | 17001 | **3 WT** nach ÜT von Nr. 2 |
| 4 Bestellbestätigung | MSBA → MSBN | ORDRSP | 19001 / 19002 | **2 WT** nach ÜT von Nr. 3 |
| 5 Zählerstand zur Geräteübernahme | MSBA → MSBN | MSCONS | — | 3 WT vor Ablauf des 28. T |

Adjacent processes sharing the workflow: ORDERS 17002 (Weiterverpflichtung MSBA)
answered by ORDRSP 19003/19004, and ORDERS 17009 (Ankündigung
Gerätewechselabsicht) answered by ORDRSP 19015/19016.

## ESA Wertebestellung (WiM Teil 2, Kapitel 4)

§34 Abs. 2 S. 2 Nr. 10 MsbG makes serving an Energieserviceanbieter a mandatory,
non-discriminatory Zusatzleistung, so an MSB must be able to process the order
that authorises value delivery and the one that stops it. Both sides are
modelled: `wertebestellung` (MSB) and `esa_wertebestellung` (ESA), over disjoint
PID sets so one deployment may hold both roles.

| UC step | Direction | Message | PID | Frist | EBD |
|---|---|---|---|---|---|
| 4.1 Nr. 1 Anfrage | ESA → MSB | REQOTE | 35003 | — | `E_0252` |
| 4.1 Nr. 2 Angebot / Ablehnung | MSB → ESA | QUOTES | 15003 | **5 WT** nach ÜT der Anfrage | — |
| 4.1 Nr. 3 Bestellung | ESA → MSB | ORDERS | 17007 | bis Ablauf der **Bindungsfrist** | — |
| 4.1 Nr. 4 Antwort | MSB → ESA | ORDRSP | 19011 / 19012 | **2 WT** nach ÜT der Bestellung | `E_0256` |
| 4.1 Nr. 5 Stornierung | ESA → MSB | ORDCHG | 39002 | unverzüglich | — |
| 4.1 Nr. 6 Antwort | MSB → ESA | ORDRSP | 19013 / 19014 | **2 WT** nach ÜT der Stornierung | `E_0257` |
| 4.2 Werteübermittlung | MSB → ESA | MSCONS | 13027 | per Messprodukt | — |
| 4.3 Nr. 1 Abbestellung | ESA → MSB | ORDERS | 17008 | unverzüglich | — |
| 4.3 Nr. 2 Antwort | MSB → ESA | ORDRSP | 19011 / 19012 | **2 WT** nach ÜT der Abbestellung | `E_0254` |
| 4.4 Nr. 1 Beendigung durch MSB | MSB → ESA | IFTSTA | 21042 (`STS+Z21` 4405 = 105) | unverzüglich | — |

### What is ordered

The [`esa`](src/esa.rs) module holds the *Codeliste der Konfigurationen* 1.4
Kapitel 4.6 catalogue — the only Messprodukte the role may order — as data:
delivery path (4.6.1 EDIFACT back-end vs 4.6.2 SM-PKI from the iMS),
Lokationsebene, Werteart, Energieflussrichtung, cadence, and whether BNetzA
*Mitteilung Nr. 3* makes the product mandatory. A `Bestellgegenstand` pairs a
Messprodukt-Code with the `DTM+76` Wunschtermin and the `IMD+7081` Abonnement
mode, and is carried through both aggregates: without it the process could not
say what a confirmed delivery is supposed to contain.

A subscription is the **(Meldepunkt, Messprodukt) pair** (`esa::business_key`) —
one Marktlokation can carry several products at once, so every follow-up message
and command has to say which one it means.

An order is validated against the catalogue before it leaves the system — a
product outside Kapitel 4.6, one defined for a different Lokationsebene than the
request addresses, or a 4.6.2 product without its SM-PKI target is refused.

**The product decides the Lokationsebene** (`esa::ebene_fuer_messprodukt`).
REQOTE AHB 1.2 §4.3 gives `LOC+172` DE 3225 four permitted shapes and lets the
Marktlokations-ID format (`[950]`) serve both the Marktlokation (hint `[502]`)
and the **Tranche** (hint `[504]`), so an identifier cannot resolve the level —
and the Tranche carries a Pflichtprodukt (`9991 00000 306 4`).

**„Pflicht" and „nutzbar ab" are two different dates**, and both are dates rather
than flags: `9991 00000 077 1` and `078 9` read „Optional ab 01.10.2023, Pflicht
ab 06.08.2024" while being usable from 01.10.2023. `Verbindlichkeit::Pflicht { ab }`
carries its start and `Messprodukt::ist_pflicht_am` is what `E_0252` Prüfschritt 1
asks — a Vergangenheitswerte-Anfrage may reach back before the cut-over.

### The Angebot is a priced offer

UC 4.1.1 has the ESA asking for „die Übermittlung von Werten **und die damit
verbundenen Kosten**". QUOTES AHB 1.1a §4.3 makes the substance Muss — `SG4 CUX`,
one to three `SG27 PIA+Z02` Artikel-IDs, one `SG31 PRI+CAL` each (`Z01`
Einrichtungs- / `Z02` Transaktions- / `Z03` Betriebspreis) and one to 23
`PIA+5 … :SRW` OBIS-Kennzahlen — so `esa::Angebot` carries all of it and the
process keeps it past the Bestellung: it is what the INVOIC 31009 is checked
against and what a delivery-surveillance sweep expects to arrive.

It is also the **discriminator**. `DTM+273` is Muss on the only published 15003
use case, so a refusal carries a Bindungsfrist too; the priced position is what
tells an Angebot from an Ablehnung.

### The Prüfidentifikator is not in BGM

`BGM` DE 1004 is a **Dokumentennummer** throughout these handbooks; the PID
travels in `SG1 RFF+Z13`. DE 1001 carries a BDEW document code: `Z57` on the
order handshake, `Z83` on the MSCONS 13027 delivery, `Z09` on the IFTSTA 21042.

### Correlation

Only the opening REQOTE is keyed on a location. A conformant ORDERS, ORDCHG,
ORDRSP or IFTSTA of Kapitel 4 carries **no `LOC` at all** and correlates by a
Belegnummer, under the Zuordnungsschlüssel the BDEW *Anwendungsübersicht der
Prüfidentifikatoren* 4.0 publishes per PID:

| PID | Schlüssel | Segment | Points at |
|---|---|---|---|
| 35003 | `ZO-T17` | `SG11 LOC+172` | the Meldepunkt |
| 15003 | `ZG-T16` | `SG1 RFF+AAV` | the REQOTE |
| 17007 | `ZG-T24` | `SG1 RFF+AAG` | the QUOTES Angebot |
| 17008 | `ZG-T41` | `SG1 RFF+ACW` | the ORDERS Bestellung |
| 39002 | `ZG-T51` | `SG1 RFF+ON` | the ORDERS Bestellung |
| 19011 / 19012 | `ZG-T14` | `SG1 RFF+ON` | the ORDERS answered |
| 19013 / 19014 | `ZG-T50` | `SG1 RFF+ACW` | the ORDCHG |
| 21042 | `ZG-T47` | `SG15 RFF+AGI` | the ORDERS Bestellung |
| 13027 | `ZG-T42` (of `EZ-03`) | `SG1 RFF+AGI` | the ORDERS Bestellung |

`esa::korrelation` is that table; the renderer and the ingest dispatcher both
read it, so the qualifier they emit and the one they look for cannot drift. Note
that the same `AGI` qualifier sits in a different segment group per message —
`SG15` on the IFTSTA, `SG1` on the MSCONS — which is why the lookup searches by
qualifier rather than by position. `EZ-03` continues past it to `ZO-T20`
(Gerätenummer) and `ZO-T21` (OBIS-Kennzahl); those two assign the values to a
Zählwerk and belong to `edmd`.

### Answers are Antwortcodes, not booleans

`SG2 AJT` is Muss on all four answer PIDs (ORDRSP AHB 1.1b §4.15) and carries the
Prüfschritt code in DE 4465 with its EBD in DE 1082. Conditions [17]/[18] require
the code to sit in that tree's Zustimmungs- resp. Ablehnungs-Cluster, so **the
cluster selects the answer PID**. The MSB commands therefore take an
`antwort_code` resolved against `mako_pruefung::esa::wertebestellung`, never an `accept`
flag alongside it.

**And it is the whole content of a refusal on the receiving side too.** Those
four use cases publish **no free-text segment at all**: the only `FTX` a
conformant 19011 may carry is `SG27 FTX+Z27` — the MSB's IP address, Muss when
the confirmed order named a Kapitel-4.6.2 SMGW product (`FTX+Z28` for a range),
and the source the ESA has to admit before the iMS can reach it.

The ESA side therefore records `esa::Antwort`, the typed `(Antwortcode, EBD)`
pair, rather than prose — that is what tells `A08` (Einwilligung abgelaufen:
renew and re-order) from `A10` (Lokationsbündel: split the request) from `A09`
(Gerätetechnik: nothing to retry). `Antwort::widerspricht_pid` flags a code whose
Cluster contradicts the PID that carried it; such an answer is recorded as a
conflict and resolved **by the PID**, since resolving it by the code would
silently turn a confirmation into a refusal.

### Every inbound step notifies

The MSB-side workflow emits a `ProcessInitiated` outbox entry on each of
35003/17007/39002/17008. That notification is the entire input to `processd`'s
ESA module — the four `mako-pruefung` walks, the operator queue and its Fristen —
and its payload is a contract: every field is a Prüfschritt input, so an omitted
one does not fail, it escalates a decision that could have been answered. In
particular `abo_beginn` (the confirmed `DTM+203` of the Bestellung) is what
`E_0254` Prüfschritt 2 compares a requested end against.

### What a subscription remembers

Both aggregates keep `lieferung_begonnen` in their process **data**, not inside
the state variant that happens to be current. It is a fact about the
subscription: it has to survive a Storno or Abbestellung round trip, and a first
delivery landing while a 39002 is in flight is exactly the case `E_0257` `A02`
exists for. The MSB side additionally remembers its own Bindungsfrist (`E_0256`
Prüfschritt 1 asks about it one state later), the Abo start, the reach of its
latest delivery and any date it already ended on — the three inputs `E_0254`
Prüfschritte 2–4 need.

An Angebot whose Bindungsfrist lapses is **terminal** on the ESA side: UC 4.1
Nr. 3 admits no Bestellung after it, so the process releases its (Meldepunkt,
Messprodukt) business key rather than blocking every replacement order.

19011/19012 answer both the Bestellung and the Beendigung; the `IMD+7081` on the
answer is what says which tree its code came from.

### Stornierung and Abbestellung are not interchangeable

UC 4.1 Nr. 5 admits a Stornierung only while the einmalige Übermittlung has not
happened or the turnusmäßige has not begun; UC 4.3's Vorbedingung then states
*"Eine Stornierung der Bestellung ist nicht mehr möglich"*.
`MarkLieferungBegonnen` flips the state that enforces this. On the MSB side the
two trees make the boundary explicit: `E_0254` `A01` refuses a Beendigung of a
one-shot order, and `E_0257` refuses a Stornierung of a started delivery with
**different codes** per Abo mode (`A02` Abo, `A03` einmalig).

## Regulatory references

- BDEW WiM Wechselprozesse im Messwesen
- MsbG — Messstellenbetriebsgesetz
- BNetzA **BK6-24-174** Anlagen 2a/2b (WiM Strom Teil 1 und Teil 2)
- BDEW/VKU/GEODE/FNBGas **AWH WiM Gas 2.0** (gültig ab 01.10.2026)
- EDI@Energy UTILMD Strom **AHB 2.2** (MIG release S2.2, `FV2026-10-01`)
- EDI@Energy **APERAK AHB 1.1** (`FV2026-10-01`) — § 2.4.1 Strom, § 2.3.1 Gas.
  2.2 is the APERAK **MIG** revision; AHB and MIG carry different version numbers
  for every message type except UTILMD

## Related crates

The format layer and the domain packs meet in `makod`: a workflow crate knows the
`Pruefidentifikator` and its own domain types, never an EDIFACT message type.

| Crate | Role |
|---|---|
| [`mako-wim`](https://docs.rs/mako-wim) ← **this crate** | WiM workflows for both Sparten, PID routing, `WimModule` |
| [`edi-energy`](https://docs.rs/edi-energy) | EDI@Energy EDIFACT — parse · validate · build (UTILMD, MSCONS, ORDERS, INVOIC, APERAK, …); joined to these workflows in `makod`, not depended on |
| [`mako-engine`](https://docs.rs/mako-engine) | Event-sourced workflow runtime — `Workflow`, `Process`, `EventStore`, deadlines |
| [`mako-fristen`](https://docs.rs/mako-fristen) | *When* an answer is due — Werktage, the MaKo holiday calendar, the per-PID Antwortfristen |
| [`mako-pruefung`](https://docs.rs/mako-pruefung) | *What* the answer must be — the BDEW Entscheidungsbäume, executable |
| [`mako-invoic`](https://docs.rs/mako-invoic) | The INVOIC settle/dispute state machine every billing family shares |
| [`energy-api`](https://docs.rs/energy-api) | The parallel API-Webdienste channel for iMS values |
| [`makod`](https://hupe1980.github.io/mako/docs/services/makod/) | Production daemon — routes, adapts and renders these workflows |

Part of **mako**, an open-source Rust platform for German energy market
communication (Marktkommunikation). Full documentation: <https://hupe1980.github.io/mako/>