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/// Normalize a date string to UNB S004 YYMMDD format (6 digits).
121///
122/// UNB with UNOC:3 syntax uses YYMMDD (6 digits), not CCYYMMDD (8 digits).
123/// If an 8-digit CCYYMMDD date is provided, the century prefix is stripped.
124fn normalize_unb_datum(datum: &str) -> &str {
125    if datum.len() == 8 && datum.as_bytes().iter().all(|b| b.is_ascii_digit()) {
126        &datum[2..]
127    } else {
128        datum
129    }
130}
131
132/// Rebuild a UNB (interchange header) segment from typed `Interchangedaten`.
133///
134/// This is the inverse of `extract_interchangedaten()`.
135/// Fields not present get sensible defaults (UNOC:3, "500" qualifier).
136/// Dates in CCYYMMDD (8-digit) format are automatically normalized to YYMMDD (6-digit).
137pub fn rebuild_unb_from_interchangedaten(data: &Interchangedaten) -> OwnedSegment {
138    let syntax = data.syntax_kennung.as_deref().unwrap_or("UNOC");
139    let sender = data.absender_code.as_deref().unwrap_or("");
140    let receiver = data.empfaenger_code.as_deref().unwrap_or("");
141    let datum = normalize_unb_datum(data.datum.as_deref().unwrap_or(""));
142    let zeit = data.zeit.as_deref().unwrap_or("");
143    let interchange_ref = data.interchange_ref.as_deref().unwrap_or("00000");
144
145    OwnedSegment {
146        id: "UNB".to_string(),
147        elements: vec![
148            vec![syntax.to_string(), "3".to_string()],
149            vec![sender.to_string(), "500".to_string()],
150            vec![receiver.to_string(), "500".to_string()],
151            vec![datum.to_string(), zeit.to_string()],
152            vec![interchange_ref.to_string()],
153        ],
154        segment_number: 0,
155    }
156}
157
158/// Rebuild a UNB (interchange header) segment from nachrichtendaten JSON.
159///
160/// This is the inverse of `extract_nachrichtendaten()`.
161/// Fields not present in the JSON get sensible defaults (UNOC:3, "500" qualifier).
162/// Dates in CCYYMMDD (8-digit) format are automatically normalized to YYMMDD (6-digit).
163pub fn rebuild_unb(nachrichtendaten: &serde_json::Value) -> OwnedSegment {
164    let syntax = nachrichtendaten
165        .get("syntaxKennung")
166        .and_then(|v| v.as_str())
167        .unwrap_or("UNOC");
168    let sender = nachrichtendaten
169        .get("absenderCode")
170        .and_then(|v| v.as_str())
171        .unwrap_or("");
172    let receiver = nachrichtendaten
173        .get("empfaengerCode")
174        .and_then(|v| v.as_str())
175        .unwrap_or("");
176    let datum_raw = nachrichtendaten
177        .get("datum")
178        .and_then(|v| v.as_str())
179        .unwrap_or("");
180    let datum = normalize_unb_datum(datum_raw);
181    let zeit = nachrichtendaten
182        .get("zeit")
183        .and_then(|v| v.as_str())
184        .unwrap_or("");
185    let interchange_ref = nachrichtendaten
186        .get("interchangeRef")
187        .and_then(|v| v.as_str())
188        .unwrap_or("00000");
189
190    OwnedSegment {
191        id: "UNB".to_string(),
192        elements: vec![
193            vec![syntax.to_string(), "3".to_string()],
194            vec![sender.to_string(), "500".to_string()],
195            vec![receiver.to_string(), "500".to_string()],
196            vec![datum.to_string(), zeit.to_string()],
197            vec![interchange_ref.to_string()],
198        ],
199        segment_number: 0,
200    }
201}
202
203/// Rebuild a UNH (message header) segment from reference number and message type.
204///
205/// Produces: `UNH+referenz+typ:D:11A:UN:S2.1`
206pub fn rebuild_unh(referenz: &str, nachrichten_typ: &str) -> OwnedSegment {
207    OwnedSegment {
208        id: "UNH".to_string(),
209        elements: vec![
210            vec![referenz.to_string()],
211            vec![
212                nachrichten_typ.to_string(),
213                "D".to_string(),
214                "11A".to_string(),
215                "UN".to_string(),
216                "S2.1".to_string(),
217            ],
218        ],
219        segment_number: 0,
220    }
221}
222
223/// Rebuild a UNT (message trailer) segment.
224///
225/// Produces: `UNT+count+referenz`
226/// `segment_count` includes UNH and UNT themselves.
227pub fn rebuild_unt(segment_count: usize, referenz: &str) -> OwnedSegment {
228    OwnedSegment {
229        id: "UNT".to_string(),
230        elements: vec![vec![segment_count.to_string()], vec![referenz.to_string()]],
231        segment_number: 0,
232    }
233}
234
235/// Rebuild a UNZ (interchange trailer) segment.
236///
237/// Produces: `UNZ+count+ref`
238pub fn rebuild_unz(message_count: usize, interchange_ref: &str) -> OwnedSegment {
239    OwnedSegment {
240        id: "UNZ".to_string(),
241        elements: vec![
242            vec![message_count.to_string()],
243            vec![interchange_ref.to_string()],
244        ],
245        segment_number: 0,
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_mapped_transaktion_serde_roundtrip() {
255        let tx = MappedTransaktion {
256            stammdaten: serde_json::json!({
257                "prozessdaten": {
258                    "vorgangId": "TX001",
259                    "transaktionsgrund": "E01"
260                },
261                "marktlokation": { "marktlokationsId": "DE000111222333" }
262            }),
263            nesting_info: Default::default(),
264        };
265
266        let json = serde_json::to_string(&tx).unwrap();
267        let de: MappedTransaktion = serde_json::from_str(&json).unwrap();
268        assert_eq!(
269            de.stammdaten["prozessdaten"]["vorgangId"].as_str().unwrap(),
270            "TX001"
271        );
272        assert!(de.stammdaten["marktlokation"].is_object());
273    }
274
275    #[test]
276    fn test_dynamic_nachricht_serde_roundtrip() {
277        let msg: DynamicNachricht = Nachricht {
278            nachrichtendaten: Nachrichtendaten {
279                unh_referenz: "00001".to_string(),
280                nachrichten_typ: "UTILMD".to_string(),
281            },
282            stammdaten: serde_json::json!({
283                "marktteilnehmer": [
284                    { "marktrolle": "MS", "rollencodenummer": "9900123" }
285                ]
286            }),
287            transaktionen: vec![serde_json::json!({})],
288        };
289
290        let json = serde_json::to_string(&msg).unwrap();
291        let de: DynamicNachricht = serde_json::from_str(&json).unwrap();
292        assert_eq!(de.nachrichtendaten.unh_referenz, "00001");
293        assert_eq!(de.nachrichtendaten.nachrichten_typ, "UTILMD");
294        assert_eq!(de.transaktionen.len(), 1);
295    }
296
297    #[test]
298    fn test_dynamic_interchange_serde_roundtrip() {
299        let interchange: DynamicInterchange = Interchange {
300            interchangedaten: Interchangedaten {
301                absender_code: Some("9900123456789".to_string()),
302                empfaenger_code: Some("9900987654321".to_string()),
303                ..Default::default()
304            },
305            nachrichten: vec![Nachricht {
306                nachrichtendaten: Nachrichtendaten {
307                    unh_referenz: "00001".to_string(),
308                    nachrichten_typ: "UTILMD".to_string(),
309                },
310                stammdaten: serde_json::json!({}),
311                transaktionen: vec![],
312            }],
313        };
314
315        let json = serde_json::to_string_pretty(&interchange).unwrap();
316        let de: DynamicInterchange = serde_json::from_str(&json).unwrap();
317        assert_eq!(de.nachrichten.len(), 1);
318        assert_eq!(de.nachrichten[0].nachrichtendaten.unh_referenz, "00001");
319    }
320
321    #[test]
322    fn test_extract_interchangedaten_from_segments() {
323        let envelope = vec![OwnedSegment {
324            id: "UNB".to_string(),
325            elements: vec![
326                vec!["UNOC".to_string(), "3".to_string()],
327                vec!["9900123456789".to_string(), "500".to_string()],
328                vec!["9900987654321".to_string(), "500".to_string()],
329                vec!["210101".to_string(), "1200".to_string()],
330                vec!["REF001".to_string()],
331            ],
332            segment_number: 0,
333        }];
334
335        let data = extract_interchangedaten(&envelope);
336        assert_eq!(data.absender_code.as_deref(), Some("9900123456789"));
337        assert_eq!(data.empfaenger_code.as_deref(), Some("9900987654321"));
338        assert_eq!(data.interchange_ref.as_deref(), Some("REF001"));
339        assert_eq!(data.syntax_kennung.as_deref(), Some("UNOC"));
340        assert_eq!(data.datum.as_deref(), Some("210101"));
341        assert_eq!(data.zeit.as_deref(), Some("1200"));
342    }
343
344    #[test]
345    fn test_extract_envelope_from_segments_json() {
346        let envelope = vec![OwnedSegment {
347            id: "UNB".to_string(),
348            elements: vec![
349                vec!["UNOC".to_string(), "3".to_string()],
350                vec!["9900123456789".to_string(), "500".to_string()],
351                vec!["9900987654321".to_string(), "500".to_string()],
352                vec!["210101".to_string(), "1200".to_string()],
353                vec!["REF001".to_string()],
354            ],
355            segment_number: 0,
356        }];
357
358        let nd = extract_nachrichtendaten(&envelope);
359        assert_eq!(nd["absenderCode"].as_str().unwrap(), "9900123456789");
360        assert_eq!(nd["empfaengerCode"].as_str().unwrap(), "9900987654321");
361        assert_eq!(nd["interchangeRef"].as_str().unwrap(), "REF001");
362        assert_eq!(nd["syntaxKennung"].as_str().unwrap(), "UNOC");
363        assert_eq!(nd["datum"].as_str().unwrap(), "210101");
364        assert_eq!(nd["zeit"].as_str().unwrap(), "1200");
365    }
366
367    #[test]
368    fn test_extract_unh_fields() {
369        let unh = OwnedSegment {
370            id: "UNH".to_string(),
371            elements: vec![
372                vec!["MSG001".to_string()],
373                vec![
374                    "UTILMD".to_string(),
375                    "D".to_string(),
376                    "11A".to_string(),
377                    "UN".to_string(),
378                    "S2.1".to_string(),
379                ],
380            ],
381            segment_number: 0,
382        };
383
384        let (referenz, typ) = extract_unh_fields(&unh);
385        assert_eq!(referenz, "MSG001");
386        assert_eq!(typ, "UTILMD");
387    }
388
389    #[test]
390    fn test_rebuild_unb_from_interchangedaten_typed() {
391        let data = Interchangedaten {
392            syntax_kennung: Some("UNOC".to_string()),
393            absender_code: Some("9900123456789".to_string()),
394            empfaenger_code: Some("9900987654321".to_string()),
395            datum: Some("210101".to_string()),
396            zeit: Some("1200".to_string()),
397            interchange_ref: Some("REF001".to_string()),
398        };
399
400        let unb = rebuild_unb_from_interchangedaten(&data);
401        assert_eq!(unb.id, "UNB");
402        assert_eq!(unb.elements[0], vec!["UNOC", "3"]);
403        assert_eq!(unb.elements[1][0], "9900123456789");
404        assert_eq!(unb.elements[2][0], "9900987654321");
405        assert_eq!(unb.elements[3], vec!["210101", "1200"]);
406        assert_eq!(unb.elements[4], vec!["REF001"]);
407    }
408
409    #[test]
410    fn test_rebuild_unb_from_nachrichtendaten() {
411        let nd = serde_json::json!({
412            "syntaxKennung": "UNOC",
413            "absenderCode": "9900123456789",
414            "empfaengerCode": "9900987654321",
415            "datum": "210101",
416            "zeit": "1200",
417            "interchangeRef": "REF001"
418        });
419
420        let unb = rebuild_unb(&nd);
421        assert_eq!(unb.id, "UNB");
422        assert_eq!(unb.elements[0], vec!["UNOC", "3"]);
423        assert_eq!(unb.elements[1][0], "9900123456789");
424        assert_eq!(unb.elements[2][0], "9900987654321");
425        assert_eq!(unb.elements[3], vec!["210101", "1200"]);
426        assert_eq!(unb.elements[4], vec!["REF001"]);
427    }
428
429    #[test]
430    fn test_rebuild_unb_defaults() {
431        let nd = serde_json::json!({});
432        let unb = rebuild_unb(&nd);
433        assert_eq!(unb.id, "UNB");
434        assert_eq!(unb.elements[0], vec!["UNOC", "3"]);
435    }
436
437    #[test]
438    fn test_rebuild_unh() {
439        let unh = rebuild_unh("00001", "UTILMD");
440        assert_eq!(unh.id, "UNH");
441        assert_eq!(unh.elements[0], vec!["00001"]);
442        assert_eq!(unh.elements[1][0], "UTILMD");
443        assert_eq!(unh.elements[1][1], "D");
444        assert_eq!(unh.elements[1][2], "11A");
445        assert_eq!(unh.elements[1][3], "UN");
446        assert_eq!(unh.elements[1][4], "S2.1");
447    }
448
449    #[test]
450    fn test_rebuild_unt() {
451        let unt = rebuild_unt(25, "00001");
452        assert_eq!(unt.id, "UNT");
453        assert_eq!(unt.elements[0], vec!["25"]);
454        assert_eq!(unt.elements[1], vec!["00001"]);
455    }
456
457    #[test]
458    fn test_rebuild_unz() {
459        let unz = rebuild_unz(1, "REF001");
460        assert_eq!(unz.id, "UNZ");
461        assert_eq!(unz.elements[0], vec!["1"]);
462        assert_eq!(unz.elements[1], vec!["REF001"]);
463    }
464
465    #[test]
466    fn test_roundtrip_interchangedaten_rebuild() {
467        let original = OwnedSegment {
468            id: "UNB".to_string(),
469            elements: vec![
470                vec!["UNOC".to_string(), "3".to_string()],
471                vec!["9900123456789".to_string(), "500".to_string()],
472                vec!["9900987654321".to_string(), "500".to_string()],
473                vec!["210101".to_string(), "1200".to_string()],
474                vec!["REF001".to_string()],
475            ],
476            segment_number: 0,
477        };
478
479        let data = extract_interchangedaten(&[original]);
480        let rebuilt = rebuild_unb_from_interchangedaten(&data);
481        assert_eq!(rebuilt.elements[0], vec!["UNOC", "3"]);
482        assert_eq!(rebuilt.elements[1][0], "9900123456789");
483        assert_eq!(rebuilt.elements[2][0], "9900987654321");
484        assert_eq!(rebuilt.elements[3], vec!["210101", "1200"]);
485        assert_eq!(rebuilt.elements[4], vec!["REF001"]);
486    }
487
488    #[test]
489    fn test_roundtrip_nachrichtendaten_rebuild() {
490        let original = OwnedSegment {
491            id: "UNB".to_string(),
492            elements: vec![
493                vec!["UNOC".to_string(), "3".to_string()],
494                vec!["9900123456789".to_string(), "500".to_string()],
495                vec!["9900987654321".to_string(), "500".to_string()],
496                vec!["210101".to_string(), "1200".to_string()],
497                vec!["REF001".to_string()],
498            ],
499            segment_number: 0,
500        };
501
502        let nd = extract_nachrichtendaten(&[original]);
503        let rebuilt = rebuild_unb(&nd);
504        assert_eq!(rebuilt.elements[0], vec!["UNOC", "3"]);
505        assert_eq!(rebuilt.elements[1][0], "9900123456789");
506        assert_eq!(rebuilt.elements[2][0], "9900987654321");
507        assert_eq!(rebuilt.elements[3], vec!["210101", "1200"]);
508        assert_eq!(rebuilt.elements[4], vec!["REF001"]);
509    }
510
511    #[test]
512    fn test_rebuild_unb_normalizes_ccyymmdd_to_yymmdd() {
513        // UNB S004 datum must be YYMMDD (6 digits), not CCYYMMDD (8 digits)
514        let data = Interchangedaten {
515            syntax_kennung: Some("UNOC".to_string()),
516            absender_code: Some("9900000000003".to_string()),
517            empfaenger_code: Some("9900000000001".to_string()),
518            datum: Some("20260409".to_string()), // 8-digit CCYYMMDD input
519            zeit: Some("0725".to_string()),
520            interchange_ref: Some("00004".to_string()),
521        };
522
523        let unb = rebuild_unb_from_interchangedaten(&data);
524        assert_eq!(unb.elements[3], vec!["260409", "0725"]); // normalized to 6-digit YYMMDD
525
526        // Same via JSON path
527        let nd = serde_json::json!({
528            "syntaxKennung": "UNOC",
529            "absenderCode": "9900000000003",
530            "empfaengerCode": "9900000000001",
531            "datum": "20260409",
532            "zeit": "0725",
533            "interchangeRef": "00004"
534        });
535        let unb_json = rebuild_unb(&nd);
536        assert_eq!(unb_json.elements[3], vec!["260409", "0725"]);
537    }
538
539    #[test]
540    fn test_rebuild_unb_preserves_yymmdd() {
541        // Already 6-digit YYMMDD — should pass through unchanged
542        let data = Interchangedaten {
543            datum: Some("260409".to_string()),
544            zeit: Some("0725".to_string()),
545            ..Default::default()
546        };
547        let unb = rebuild_unb_from_interchangedaten(&data);
548        assert_eq!(unb.elements[3], vec!["260409", "0725"]);
549    }
550
551    #[test]
552    fn test_into_dynamic_nachricht() {
553        let mapped = MappedMessage {
554            stammdaten: serde_json::json!({"marktteilnehmer": []}),
555            transaktionen: vec![MappedTransaktion {
556                stammdaten: serde_json::json!({"prozessdaten": {"id": "1"}}),
557                nesting_info: Default::default(),
558            }],
559            nesting_info: Default::default(),
560        };
561
562        let nd = Nachrichtendaten {
563            unh_referenz: "00001".to_string(),
564            nachrichten_typ: "UTILMD".to_string(),
565        };
566
567        let nachricht = mapped.into_dynamic_nachricht(nd);
568        assert_eq!(nachricht.nachrichtendaten.unh_referenz, "00001");
569        assert_eq!(nachricht.transaktionen.len(), 1);
570        assert!(nachricht.transaktionen[0]["prozessdaten"].is_object());
571    }
572}