Skip to main content

mig_bo4e/
model.rs

1//! Output model types for the MIG-driven mapping pipeline.
2//!
3//! Public types `Interchange`, `Nachricht`, `DynamicInterchange`, `DynamicNachricht`,
4//! `Interchangedaten`, `Nachrichtendaten` are re-exported from `bo4e-edifact-types`.
5//!
6//! Internal engine types `MappedMessage` and `MappedTransaktion` carry forward-mapping
7//! results including `nesting_info` metadata that is not part of the public API.
8
9use mig_types::segment::OwnedSegment;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13// Re-export public model types from bo4e-edifact-types
14pub use bo4e_edifact_types::{
15    DynamicInterchange, DynamicNachricht, Interchange, Interchangedaten, Nachricht,
16    Nachrichtendaten,
17};
18
19/// Internal engine type for a forward-mapped transaction.
20///
21/// Contains all BO4E entities (including prozessdaten) in `stammdaten`,
22/// plus nesting distribution info used by the reverse mapper.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct MappedTransaktion {
26    /// All BO4E entities mapped from this transaction's segment groups.
27    /// Keys are entity names in camelCase (e.g., "prozessdaten", "marktlokation", "messlokation").
28    pub stammdaten: serde_json::Value,
29
30    /// Nesting distribution info for transaction-level entities.
31    ///
32    /// Maps entity key (camelCase) -> parent rep index for each child element.
33    /// Used by the reverse mapper to distribute children among parent group reps
34    /// within a transaction (e.g., SG36->SG40 in PRICAT).
35    /// Derived from the tree structure during forward mapping; never serialized.
36    #[serde(skip)]
37    pub nesting_info: HashMap<String, Vec<usize>>,
38}
39
40/// Intermediate result from mapping a single message's assembled tree.
41///
42/// Contains message-level stammdaten and per-transaction results.
43/// Used by `MappingEngine::map_interchange()` before wrapping into `Nachricht`.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct MappedMessage {
47    /// Message-level BO4E entities (e.g., Marktteilnehmer from SG2).
48    pub stammdaten: serde_json::Value,
49
50    /// Per-transaction results (one per SG4 instance).
51    pub transaktionen: Vec<MappedTransaktion>,
52
53    /// Nesting distribution info for message-level entities.
54    ///
55    /// Maps entity key (camelCase) -> parent rep index for each child element.
56    /// Used by the reverse mapper to distribute children among parent group reps.
57    /// Derived from the tree structure during forward mapping; never serialized.
58    #[serde(skip)]
59    pub nesting_info: HashMap<String, Vec<usize>>,
60}
61
62impl MappedMessage {
63    /// Convert this internal engine result into a public `DynamicNachricht`.
64    ///
65    /// Each `MappedTransaktion.stammdaten` becomes a transaction entry in the
66    /// `DynamicNachricht.transaktionen` Vec.
67    pub fn into_dynamic_nachricht(self, nachrichtendaten: Nachrichtendaten) -> DynamicNachricht {
68        Nachricht {
69            nachrichtendaten,
70            stammdaten: self.stammdaten,
71            transaktionen: self
72                .transaktionen
73                .into_iter()
74                .map(|t| t.stammdaten)
75                .collect(),
76        }
77    }
78}
79
80/// Extract message reference and message type from a UNH segment.
81pub fn extract_unh_fields(unh: &OwnedSegment) -> (String, String) {
82    let referenz = unh.get_element(0).to_string();
83    let typ = unh.get_component(1, 0).to_string();
84    (referenz, typ)
85}
86
87/// Extract typed interchange-level metadata from envelope segments (UNB).
88pub fn extract_interchangedaten(envelope: &[OwnedSegment]) -> Interchangedaten {
89    let mut result = Interchangedaten::default();
90
91    for seg in envelope {
92        if seg.is("UNB") {
93            let val = |s: &str| {
94                if s.is_empty() {
95                    None
96                } else {
97                    Some(s.to_string())
98                }
99            };
100            result.syntax_kennung = val(seg.get_component(0, 0));
101            result.absender_code = val(seg.get_component(1, 0));
102            result.empfaenger_code = val(seg.get_component(2, 0));
103            result.datum = val(seg.get_component(3, 0));
104            result.zeit = val(seg.get_component(3, 1));
105            result.interchange_ref = val(seg.get_element(4));
106        }
107    }
108
109    result
110}
111
112/// Extract interchange-level metadata from envelope segments (UNB) as JSON.
113///
114/// Kept for backward compatibility. Prefer `extract_interchangedaten()` for typed access.
115pub fn extract_nachrichtendaten(envelope: &[OwnedSegment]) -> serde_json::Value {
116    let data = extract_interchangedaten(envelope);
117    serde_json::to_value(&data).unwrap_or_default()
118}
119
120/// Rebuild a UNB (interchange header) segment from typed `Interchangedaten`.
121///
122/// This is the inverse of `extract_interchangedaten()`.
123/// Fields not present get sensible defaults (UNOC:3, "500" qualifier).
124pub fn rebuild_unb_from_interchangedaten(data: &Interchangedaten) -> OwnedSegment {
125    let syntax = data.syntax_kennung.as_deref().unwrap_or("UNOC");
126    let sender = data.absender_code.as_deref().unwrap_or("");
127    let receiver = data.empfaenger_code.as_deref().unwrap_or("");
128    let datum = data.datum.as_deref().unwrap_or("");
129    let zeit = data.zeit.as_deref().unwrap_or("");
130    let interchange_ref = data.interchange_ref.as_deref().unwrap_or("00000");
131
132    OwnedSegment {
133        id: "UNB".to_string(),
134        elements: vec![
135            vec![syntax.to_string(), "3".to_string()],
136            vec![sender.to_string(), "500".to_string()],
137            vec![receiver.to_string(), "500".to_string()],
138            vec![datum.to_string(), zeit.to_string()],
139            vec![interchange_ref.to_string()],
140        ],
141        segment_number: 0,
142    }
143}
144
145/// Rebuild a UNB (interchange header) segment from nachrichtendaten JSON.
146///
147/// This is the inverse of `extract_nachrichtendaten()`.
148/// Fields not present in the JSON get sensible defaults (UNOC:3, "500" qualifier).
149pub fn rebuild_unb(nachrichtendaten: &serde_json::Value) -> OwnedSegment {
150    let syntax = nachrichtendaten
151        .get("syntaxKennung")
152        .and_then(|v| v.as_str())
153        .unwrap_or("UNOC");
154    let sender = nachrichtendaten
155        .get("absenderCode")
156        .and_then(|v| v.as_str())
157        .unwrap_or("");
158    let receiver = nachrichtendaten
159        .get("empfaengerCode")
160        .and_then(|v| v.as_str())
161        .unwrap_or("");
162    let datum = nachrichtendaten
163        .get("datum")
164        .and_then(|v| v.as_str())
165        .unwrap_or("");
166    let zeit = nachrichtendaten
167        .get("zeit")
168        .and_then(|v| v.as_str())
169        .unwrap_or("");
170    let interchange_ref = nachrichtendaten
171        .get("interchangeRef")
172        .and_then(|v| v.as_str())
173        .unwrap_or("00000");
174
175    OwnedSegment {
176        id: "UNB".to_string(),
177        elements: vec![
178            vec![syntax.to_string(), "3".to_string()],
179            vec![sender.to_string(), "500".to_string()],
180            vec![receiver.to_string(), "500".to_string()],
181            vec![datum.to_string(), zeit.to_string()],
182            vec![interchange_ref.to_string()],
183        ],
184        segment_number: 0,
185    }
186}
187
188/// Rebuild a UNH (message header) segment from reference number and message type.
189///
190/// Produces: `UNH+referenz+typ:D:11A:UN:S2.1`
191pub fn rebuild_unh(referenz: &str, nachrichten_typ: &str) -> OwnedSegment {
192    OwnedSegment {
193        id: "UNH".to_string(),
194        elements: vec![
195            vec![referenz.to_string()],
196            vec![
197                nachrichten_typ.to_string(),
198                "D".to_string(),
199                "11A".to_string(),
200                "UN".to_string(),
201                "S2.1".to_string(),
202            ],
203        ],
204        segment_number: 0,
205    }
206}
207
208/// Rebuild a UNT (message trailer) segment.
209///
210/// Produces: `UNT+count+referenz`
211/// `segment_count` includes UNH and UNT themselves.
212pub fn rebuild_unt(segment_count: usize, referenz: &str) -> OwnedSegment {
213    OwnedSegment {
214        id: "UNT".to_string(),
215        elements: vec![vec![segment_count.to_string()], vec![referenz.to_string()]],
216        segment_number: 0,
217    }
218}
219
220/// Rebuild a UNZ (interchange trailer) segment.
221///
222/// Produces: `UNZ+count+ref`
223pub fn rebuild_unz(message_count: usize, interchange_ref: &str) -> OwnedSegment {
224    OwnedSegment {
225        id: "UNZ".to_string(),
226        elements: vec![
227            vec![message_count.to_string()],
228            vec![interchange_ref.to_string()],
229        ],
230        segment_number: 0,
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_mapped_transaktion_serde_roundtrip() {
240        let tx = MappedTransaktion {
241            stammdaten: serde_json::json!({
242                "prozessdaten": {
243                    "vorgangId": "TX001",
244                    "transaktionsgrund": "E01"
245                },
246                "marktlokation": { "marktlokationsId": "DE000111222333" }
247            }),
248            nesting_info: Default::default(),
249        };
250
251        let json = serde_json::to_string(&tx).unwrap();
252        let de: MappedTransaktion = serde_json::from_str(&json).unwrap();
253        assert_eq!(
254            de.stammdaten["prozessdaten"]["vorgangId"].as_str().unwrap(),
255            "TX001"
256        );
257        assert!(de.stammdaten["marktlokation"].is_object());
258    }
259
260    #[test]
261    fn test_dynamic_nachricht_serde_roundtrip() {
262        let msg: DynamicNachricht = Nachricht {
263            nachrichtendaten: Nachrichtendaten {
264                unh_referenz: "00001".to_string(),
265                nachrichten_typ: "UTILMD".to_string(),
266            },
267            stammdaten: serde_json::json!({
268                "marktteilnehmer": [
269                    { "marktrolle": "MS", "rollencodenummer": "9900123" }
270                ]
271            }),
272            transaktionen: vec![serde_json::json!({})],
273        };
274
275        let json = serde_json::to_string(&msg).unwrap();
276        let de: DynamicNachricht = serde_json::from_str(&json).unwrap();
277        assert_eq!(de.nachrichtendaten.unh_referenz, "00001");
278        assert_eq!(de.nachrichtendaten.nachrichten_typ, "UTILMD");
279        assert_eq!(de.transaktionen.len(), 1);
280    }
281
282    #[test]
283    fn test_dynamic_interchange_serde_roundtrip() {
284        let interchange: DynamicInterchange = Interchange {
285            interchangedaten: Interchangedaten {
286                absender_code: Some("9900123456789".to_string()),
287                empfaenger_code: Some("9900987654321".to_string()),
288                ..Default::default()
289            },
290            nachrichten: vec![Nachricht {
291                nachrichtendaten: Nachrichtendaten {
292                    unh_referenz: "00001".to_string(),
293                    nachrichten_typ: "UTILMD".to_string(),
294                },
295                stammdaten: serde_json::json!({}),
296                transaktionen: vec![],
297            }],
298        };
299
300        let json = serde_json::to_string_pretty(&interchange).unwrap();
301        let de: DynamicInterchange = serde_json::from_str(&json).unwrap();
302        assert_eq!(de.nachrichten.len(), 1);
303        assert_eq!(de.nachrichten[0].nachrichtendaten.unh_referenz, "00001");
304    }
305
306    #[test]
307    fn test_extract_interchangedaten_from_segments() {
308        let envelope = vec![OwnedSegment {
309            id: "UNB".to_string(),
310            elements: vec![
311                vec!["UNOC".to_string(), "3".to_string()],
312                vec!["9900123456789".to_string(), "500".to_string()],
313                vec!["9900987654321".to_string(), "500".to_string()],
314                vec!["210101".to_string(), "1200".to_string()],
315                vec!["REF001".to_string()],
316            ],
317            segment_number: 0,
318        }];
319
320        let data = extract_interchangedaten(&envelope);
321        assert_eq!(data.absender_code.as_deref(), Some("9900123456789"));
322        assert_eq!(data.empfaenger_code.as_deref(), Some("9900987654321"));
323        assert_eq!(data.interchange_ref.as_deref(), Some("REF001"));
324        assert_eq!(data.syntax_kennung.as_deref(), Some("UNOC"));
325        assert_eq!(data.datum.as_deref(), Some("210101"));
326        assert_eq!(data.zeit.as_deref(), Some("1200"));
327    }
328
329    #[test]
330    fn test_extract_envelope_from_segments_json() {
331        let envelope = vec![OwnedSegment {
332            id: "UNB".to_string(),
333            elements: vec![
334                vec!["UNOC".to_string(), "3".to_string()],
335                vec!["9900123456789".to_string(), "500".to_string()],
336                vec!["9900987654321".to_string(), "500".to_string()],
337                vec!["210101".to_string(), "1200".to_string()],
338                vec!["REF001".to_string()],
339            ],
340            segment_number: 0,
341        }];
342
343        let nd = extract_nachrichtendaten(&envelope);
344        assert_eq!(nd["absenderCode"].as_str().unwrap(), "9900123456789");
345        assert_eq!(nd["empfaengerCode"].as_str().unwrap(), "9900987654321");
346        assert_eq!(nd["interchangeRef"].as_str().unwrap(), "REF001");
347        assert_eq!(nd["syntaxKennung"].as_str().unwrap(), "UNOC");
348        assert_eq!(nd["datum"].as_str().unwrap(), "210101");
349        assert_eq!(nd["zeit"].as_str().unwrap(), "1200");
350    }
351
352    #[test]
353    fn test_extract_unh_fields() {
354        let unh = OwnedSegment {
355            id: "UNH".to_string(),
356            elements: vec![
357                vec!["MSG001".to_string()],
358                vec![
359                    "UTILMD".to_string(),
360                    "D".to_string(),
361                    "11A".to_string(),
362                    "UN".to_string(),
363                    "S2.1".to_string(),
364                ],
365            ],
366            segment_number: 0,
367        };
368
369        let (referenz, typ) = extract_unh_fields(&unh);
370        assert_eq!(referenz, "MSG001");
371        assert_eq!(typ, "UTILMD");
372    }
373
374    #[test]
375    fn test_rebuild_unb_from_interchangedaten_typed() {
376        let data = Interchangedaten {
377            syntax_kennung: Some("UNOC".to_string()),
378            absender_code: Some("9900123456789".to_string()),
379            empfaenger_code: Some("9900987654321".to_string()),
380            datum: Some("210101".to_string()),
381            zeit: Some("1200".to_string()),
382            interchange_ref: Some("REF001".to_string()),
383        };
384
385        let unb = rebuild_unb_from_interchangedaten(&data);
386        assert_eq!(unb.id, "UNB");
387        assert_eq!(unb.elements[0], vec!["UNOC", "3"]);
388        assert_eq!(unb.elements[1][0], "9900123456789");
389        assert_eq!(unb.elements[2][0], "9900987654321");
390        assert_eq!(unb.elements[3], vec!["210101", "1200"]);
391        assert_eq!(unb.elements[4], vec!["REF001"]);
392    }
393
394    #[test]
395    fn test_rebuild_unb_from_nachrichtendaten() {
396        let nd = serde_json::json!({
397            "syntaxKennung": "UNOC",
398            "absenderCode": "9900123456789",
399            "empfaengerCode": "9900987654321",
400            "datum": "210101",
401            "zeit": "1200",
402            "interchangeRef": "REF001"
403        });
404
405        let unb = rebuild_unb(&nd);
406        assert_eq!(unb.id, "UNB");
407        assert_eq!(unb.elements[0], vec!["UNOC", "3"]);
408        assert_eq!(unb.elements[1][0], "9900123456789");
409        assert_eq!(unb.elements[2][0], "9900987654321");
410        assert_eq!(unb.elements[3], vec!["210101", "1200"]);
411        assert_eq!(unb.elements[4], vec!["REF001"]);
412    }
413
414    #[test]
415    fn test_rebuild_unb_defaults() {
416        let nd = serde_json::json!({});
417        let unb = rebuild_unb(&nd);
418        assert_eq!(unb.id, "UNB");
419        assert_eq!(unb.elements[0], vec!["UNOC", "3"]);
420    }
421
422    #[test]
423    fn test_rebuild_unh() {
424        let unh = rebuild_unh("00001", "UTILMD");
425        assert_eq!(unh.id, "UNH");
426        assert_eq!(unh.elements[0], vec!["00001"]);
427        assert_eq!(unh.elements[1][0], "UTILMD");
428        assert_eq!(unh.elements[1][1], "D");
429        assert_eq!(unh.elements[1][2], "11A");
430        assert_eq!(unh.elements[1][3], "UN");
431        assert_eq!(unh.elements[1][4], "S2.1");
432    }
433
434    #[test]
435    fn test_rebuild_unt() {
436        let unt = rebuild_unt(25, "00001");
437        assert_eq!(unt.id, "UNT");
438        assert_eq!(unt.elements[0], vec!["25"]);
439        assert_eq!(unt.elements[1], vec!["00001"]);
440    }
441
442    #[test]
443    fn test_rebuild_unz() {
444        let unz = rebuild_unz(1, "REF001");
445        assert_eq!(unz.id, "UNZ");
446        assert_eq!(unz.elements[0], vec!["1"]);
447        assert_eq!(unz.elements[1], vec!["REF001"]);
448    }
449
450    #[test]
451    fn test_roundtrip_interchangedaten_rebuild() {
452        let original = OwnedSegment {
453            id: "UNB".to_string(),
454            elements: vec![
455                vec!["UNOC".to_string(), "3".to_string()],
456                vec!["9900123456789".to_string(), "500".to_string()],
457                vec!["9900987654321".to_string(), "500".to_string()],
458                vec!["210101".to_string(), "1200".to_string()],
459                vec!["REF001".to_string()],
460            ],
461            segment_number: 0,
462        };
463
464        let data = extract_interchangedaten(&[original]);
465        let rebuilt = rebuild_unb_from_interchangedaten(&data);
466        assert_eq!(rebuilt.elements[0], vec!["UNOC", "3"]);
467        assert_eq!(rebuilt.elements[1][0], "9900123456789");
468        assert_eq!(rebuilt.elements[2][0], "9900987654321");
469        assert_eq!(rebuilt.elements[3], vec!["210101", "1200"]);
470        assert_eq!(rebuilt.elements[4], vec!["REF001"]);
471    }
472
473    #[test]
474    fn test_roundtrip_nachrichtendaten_rebuild() {
475        let original = OwnedSegment {
476            id: "UNB".to_string(),
477            elements: vec![
478                vec!["UNOC".to_string(), "3".to_string()],
479                vec!["9900123456789".to_string(), "500".to_string()],
480                vec!["9900987654321".to_string(), "500".to_string()],
481                vec!["210101".to_string(), "1200".to_string()],
482                vec!["REF001".to_string()],
483            ],
484            segment_number: 0,
485        };
486
487        let nd = extract_nachrichtendaten(&[original]);
488        let rebuilt = rebuild_unb(&nd);
489        assert_eq!(rebuilt.elements[0], vec!["UNOC", "3"]);
490        assert_eq!(rebuilt.elements[1][0], "9900123456789");
491        assert_eq!(rebuilt.elements[2][0], "9900987654321");
492        assert_eq!(rebuilt.elements[3], vec!["210101", "1200"]);
493        assert_eq!(rebuilt.elements[4], vec!["REF001"]);
494    }
495
496    #[test]
497    fn test_into_dynamic_nachricht() {
498        let mapped = MappedMessage {
499            stammdaten: serde_json::json!({"marktteilnehmer": []}),
500            transaktionen: vec![MappedTransaktion {
501                stammdaten: serde_json::json!({"prozessdaten": {"id": "1"}}),
502                nesting_info: Default::default(),
503            }],
504            nesting_info: Default::default(),
505        };
506
507        let nd = Nachrichtendaten {
508            unh_referenz: "00001".to_string(),
509            nachrichten_typ: "UTILMD".to_string(),
510        };
511
512        let nachricht = mapped.into_dynamic_nachricht(nd);
513        assert_eq!(nachricht.nachrichtendaten.unh_referenz, "00001");
514        assert_eq!(nachricht.transaktionen.len(), 1);
515        assert!(nachricht.transaktionen[0]["prozessdaten"].is_object());
516    }
517}