1use std::collections::{BTreeMap, HashMap};
8
9use crate::expr::ConditionExpr;
10
11use super::validate::{AhbFieldRule, AhbWorkflow};
12use mig_assembly::assembler::{
13 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
14};
15
16#[derive(Debug)]
18pub struct AhbNode<'a> {
19 pub rule: &'a AhbFieldRule,
21 pub value: Option<&'a str>,
23 pub segment_elements: Option<&'a [Vec<String>]>,
25 pub matched_segment_number: Option<u32>,
28}
29
30#[derive(Debug)]
32pub struct AhbGroupNode<'a> {
33 pub group_id: &'a str,
35 pub instance_index: usize,
37 pub ahb_status: Option<&'a str>,
39 pub fields: Vec<AhbNode<'a>>,
41 pub children: Vec<AhbGroupNode<'a>>,
43}
44
45#[derive(Debug)]
47pub struct ValidatedTree<'a> {
48 pub pruefidentifikator: &'a str,
50 pub ub_definitions: &'a BTreeMap<String, ConditionExpr>,
52 pub root_fields: Vec<AhbNode<'a>>,
54 pub groups: Vec<AhbGroupNode<'a>>,
56 pub unmatched_rules: Vec<&'a AhbFieldRule>,
63}
64
65pub fn build_validated_tree<'a>(
71 workflow: &'a AhbWorkflow,
72 tree: &'a AssembledTree,
73) -> ValidatedTree<'a> {
74 let mut root_rules: Vec<&'a AhbFieldRule> = Vec::new();
76 let mut group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
78
79 for rule in &workflow.fields {
80 match extract_top_group(&rule.segment_path) {
81 Some(group_id) => {
82 group_rules
83 .entry(group_id.to_owned())
84 .or_default()
85 .push(rule);
86 }
87 None => {
88 root_rules.push(rule);
89 }
90 }
91 }
92
93 let root_fields = resolve_fields(&root_rules, &tree.segments, None);
95
96 let groups = resolve_groups(&tree.groups, &group_rules, 0);
98
99 let mut matched_mig_numbers: std::collections::HashSet<&str> = std::collections::HashSet::new();
102 collect_matched_mig_numbers_from_groups(&groups, &mut matched_mig_numbers);
103 for seg in &tree.segments {
105 if let Some(ref num) = seg.mig_number {
106 matched_mig_numbers.insert(num.as_str());
107 }
108 }
109
110 let unmatched_rules: Vec<&AhbFieldRule> = workflow
111 .fields
112 .iter()
113 .filter(|rule| {
114 rule.mig_number
115 .as_ref()
116 .is_some_and(|num| !matched_mig_numbers.contains(num.as_str()))
117 })
118 .collect();
119
120 ValidatedTree {
121 pruefidentifikator: &workflow.pruefidentifikator,
122 ub_definitions: &workflow.ub_definitions,
123 root_fields,
124 groups,
125 unmatched_rules,
126 }
127}
128
129fn collect_matched_mig_numbers_from_groups<'a>(
135 groups: &[AhbGroupNode<'a>],
136 out: &mut std::collections::HashSet<&'a str>,
137) {
138 for group in groups {
139 for node in &group.fields {
140 if let Some(ref num) = node.rule.mig_number {
141 out.insert(num.as_str());
142 }
143 }
144 collect_matched_mig_numbers_from_groups(&group.children, out);
145 }
146}
147
148fn merged_variant<'a>(
154 rules: &[&'a AhbFieldRule],
155 instance: &AssembledGroupInstance,
156) -> Option<(&'a str, std::ops::Range<u32>)> {
157 let entry = instance.segments.first()?;
158 let recorded = instance.entry_mig_number.as_deref()?;
162 if !rules.iter().any(|r| {
163 r.mig_number.as_deref() == Some(recorded)
164 && extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
165 }) {
166 return None;
167 }
168 if instance
172 .segments
173 .iter()
174 .filter(|s| s.tag == entry.tag)
175 .count()
176 != 1
177 {
178 return None;
179 }
180 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
181 let mut positions: Vec<(usize, usize)> = Vec::new();
182 for r in rules {
183 let pos = (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0));
184 if extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
185 && !r.codes.is_empty()
186 && !positions.contains(&pos)
187 {
188 positions.push(pos);
189 }
190 }
191 for (element, component) in positions {
192 let mut entries: Vec<(u32, &'a AhbFieldRule)> = rules
194 .iter()
195 .filter(|r| {
196 extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
197 && r.element_index.unwrap_or(0) == element
198 && r.component_index.unwrap_or(0) == component
199 && !r.codes.is_empty()
200 })
201 .filter_map(|r| Some((number(r)?, *r)))
202 .collect();
203 entries.sort_by_key(|(n, _)| *n);
204 entries.dedup_by_key(|(n, _)| *n);
205 let all_entry_numbers: std::collections::BTreeSet<u32> = rules
209 .iter()
210 .filter(|r| extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str()))
211 .filter_map(|r| number(r))
212 .collect();
213 if entries.len() != all_entry_numbers.len() {
214 continue;
215 }
216 let disjoint = entries.len() >= 2
217 && entries.iter().enumerate().all(|(i, (_, a))| {
218 entries[i + 1..].iter().all(|(_, b)| {
219 !a.codes
220 .iter()
221 .any(|c| b.codes.iter().any(|d| d.value == c.value))
222 })
223 });
224 if !disjoint {
225 continue;
226 }
227 let value = entry.elements.get(element)?.get(component)?;
228 let at = entries
229 .iter()
230 .position(|(_, r)| r.codes.iter().any(|c| &c.value == value))?;
231 let start = entries[at].0;
232 let end = entries.get(at + 1).map_or(u32::MAX, |(n, _)| *n);
233 return Some((entries[at].1.mig_number.as_deref()?, start..end));
234 }
235 None
236}
237
238fn extract_top_group(segment_path: &str) -> Option<&str> {
244 let first = segment_path.split('/').next()?;
245 if first.starts_with("SG") {
246 Some(first)
247 } else {
248 None
249 }
250}
251
252fn extract_child_group(stripped_path: &str) -> Option<&str> {
257 let first = stripped_path.split('/').next()?;
258 if first.starts_with("SG") {
259 Some(first)
260 } else {
261 None
262 }
263}
264
265fn resolve_fields<'a>(
271 rules: &[&'a AhbFieldRule],
272 segments: &'a [AssembledSegment],
273 numbers: Option<&[Option<String>]>,
274) -> Vec<AhbNode<'a>> {
275 rules
276 .iter()
277 .map(|rule| {
278 let matched_segment = find_segment(rule, segments, numbers);
279 let (value, elements, segment_number) = match matched_segment {
280 Some(seg) => {
281 let val = extract_value(seg, rule);
282 (val, Some(seg.elements.as_slice()), seg.segment_number)
283 }
284 None => {
285 let fallback_num = extract_segment_tag(&rule.segment_path)
294 .and_then(|tag| segments.iter().find(|s| s.tag == tag))
295 .and_then(|s| s.segment_number);
296 (None, None, fallback_num)
297 }
298 };
299 AhbNode {
300 rule,
301 value,
302 segment_elements: elements,
303 matched_segment_number: segment_number,
304 }
305 })
306 .collect()
307}
308
309fn find_segment<'a>(
321 rule: &AhbFieldRule,
322 segments: &'a [AssembledSegment],
323 numbers: Option<&[Option<String>]>,
324) -> Option<&'a AssembledSegment> {
325 let number_of = |i: usize, s: &'a AssembledSegment| -> Option<&str> {
327 match numbers {
328 Some(n) => n.get(i).and_then(|n| n.as_deref()),
329 None => s.mig_number.as_deref(),
330 }
331 };
332 let tag = extract_segment_tag(&rule.segment_path)?;
333
334 if !rule.codes.is_empty() {
353 let el_idx = rule.element_index.unwrap_or(0);
354 let comp_idx = rule.component_index.unwrap_or(0);
355 let expected: std::collections::HashSet<&str> =
356 rule.codes.iter().map(|c| c.value.as_str()).collect();
357 if let Some(seg) = segments.iter().find(|s| {
358 s.tag == tag
359 && s.elements
360 .get(el_idx)
361 .and_then(|e| e.get(comp_idx))
362 .is_some_and(|v| expected.contains(v.as_str()))
363 }) {
364 return Some(seg);
365 }
366 let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
367 if same_tag_count > 1 {
368 return None;
369 }
370 }
371
372 if let Some(mig_num) = rule.mig_number.as_ref() {
373 if let Some((_, seg)) = segments
375 .iter()
376 .enumerate()
377 .find(|(i, s)| number_of(*i, s) == Some(mig_num.as_str()))
378 {
379 return Some(seg);
380 }
381
382 return segments
384 .iter()
385 .enumerate()
386 .find(|(i, s)| s.tag == tag && number_of(*i, s).map_or(true, |m| m == mig_num))
387 .map(|(_, s)| s);
388 }
389
390 segments.iter().find(|s| s.tag == tag)
392}
393
394fn extract_segment_tag(segment_path: &str) -> Option<&str> {
399 segment_path.split('/').find(|part| !part.starts_with("SG"))
400}
401
402fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
404 let elem_idx = rule.element_index.unwrap_or(0);
405 let comp_idx = rule.component_index.unwrap_or(0);
406
407 let element = segment.elements.get(elem_idx)?;
408 let component = element.get(comp_idx)?;
409
410 if component.is_empty() {
411 None
412 } else {
413 Some(component.as_str())
414 }
415}
416
417fn resolve_groups<'a>(
422 assembled_groups: &'a [AssembledGroup],
423 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
424 depth: usize,
425) -> Vec<AhbGroupNode<'a>> {
426 let mut result = Vec::new();
427
428 for assembled_group in assembled_groups {
429 let rules = group_rules.get(&assembled_group.group_id);
430
431 for (instance_index, instance) in assembled_group.repetitions.iter().enumerate() {
432 let mut node =
433 resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
434 node.instance_index = instance_index;
435 result.push(node);
436 }
437 }
438
439 result
440}
441
442fn strip_n_groups(path: &str, n: usize) -> &str {
446 let mut rest = path;
447 for _ in 0..n {
448 match rest.find('/') {
449 Some(idx) => rest = &rest[idx + 1..],
450 None => return rest,
451 }
452 }
453 rest
454}
455
456fn resolve_group_instance<'a>(
465 group_id: &'a str,
466 instance: &'a AssembledGroupInstance,
467 rules: Option<&Vec<&'a AhbFieldRule>>,
468 depth: usize,
469) -> AhbGroupNode<'a> {
470 let strip_count = depth + 1;
472
473 let variant_numbers: std::collections::HashSet<&str> =
478 if !instance.variant_mig_numbers.is_empty() {
479 instance
480 .variant_mig_numbers
481 .iter()
482 .map(|s| s.as_str())
483 .collect()
484 } else {
485 collect_instance_mig_numbers(instance)
486 };
487
488 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
489 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
490 let mut ahb_status: Option<&'a str> = None;
491
492 let merged = rules.and_then(|rules| {
497 let own: Vec<&'a AhbFieldRule> = rules
498 .iter()
499 .copied()
500 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none())
501 .collect();
502 merged_variant(&own, instance)
503 });
504
505 let span = variant_numbers
510 .iter()
511 .filter_map(|n| n.parse::<u32>().ok())
512 .fold(None, |acc: Option<(u32, u32)>, n| {
513 Some(acc.map_or((n, n), |(lo, hi)| (lo.min(n), hi.max(n))))
514 });
515
516 if let Some(rules) = rules {
517 for rule in rules {
518 let stripped = strip_n_groups(&rule.segment_path, strip_count);
520 let child = extract_child_group(stripped);
521
522 if let Some(ref rule_mig) = rule.mig_number {
525 let number = rule_mig.parse::<u32>().ok();
526 let belongs = match (&merged, child) {
527 (Some((_, range)), _) => number.is_some_and(|m| range.contains(&m)),
528 (None, Some(_)) => match (number, span) {
529 (Some(m), Some((lo, hi))) => (lo..=hi).contains(&m),
530 _ => variant_numbers.contains(rule_mig.as_str()),
531 },
532 (None, None) => variant_numbers.contains(rule_mig.as_str()),
533 };
534 if !belongs {
535 continue;
536 }
537 }
538
539 if let Some(child_group_id) = child {
540 child_group_rules
541 .entry(child_group_id.to_owned())
542 .or_default()
543 .push(rule);
544 } else {
545 direct_rules.push(rule);
546 }
547 }
548 }
549 let own_status = |rule: &&'a AhbFieldRule| rule.parent_group_ahb_status.as_deref();
554 let entry = merged
555 .as_ref()
556 .map(|(entry, _)| *entry)
557 .or(instance.entry_mig_number.as_deref());
558 if let Some(status) = direct_rules
559 .iter()
560 .filter(|r| entry.is_some() && r.mig_number.as_deref() == entry)
561 .find_map(own_status)
562 .or_else(|| {
563 direct_rules
564 .iter()
565 .filter(|r| r.mig_number.is_some())
566 .find_map(own_status)
567 })
568 {
569 ahb_status = Some(status);
570 }
571
572 let numbers: Option<Vec<Option<String>>> = merged.as_ref().map(|_| {
577 let mut used: Vec<&str> = Vec::new();
578 instance
579 .segments
580 .iter()
581 .map(|seg| {
582 let of_tag = |r: &&&'a AhbFieldRule| {
583 extract_segment_tag(&r.segment_path) == Some(seg.tag.as_str())
584 };
585 let mut slots: Vec<&str> = direct_rules
586 .iter()
587 .filter(of_tag)
588 .filter_map(|r| r.mig_number.as_deref())
589 .filter(|n| !used.contains(n))
590 .collect();
591 slots.sort_by_key(|n| n.parse::<u32>().unwrap_or(u32::MAX));
592 slots.dedup();
593 let coded = |slot: &str| {
596 direct_rules
597 .iter()
598 .filter(of_tag)
599 .filter(|r| r.mig_number.as_deref() == Some(slot) && !r.codes.is_empty())
600 .collect::<Vec<_>>()
601 };
602 let carries = |slot: &&str| {
603 coded(slot).iter().any(|r| {
604 seg.elements
605 .get(r.element_index.unwrap_or(0))
606 .and_then(|e| e.get(r.component_index.unwrap_or(0)))
607 .is_some_and(|v| r.codes.iter().any(|c| &c.value == v))
608 })
609 };
610 let slot = slots
611 .iter()
612 .find(|s| carries(s))
613 .or_else(|| slots.iter().find(|s| coded(s).is_empty()))
614 .or_else(|| slots.first())
615 .copied();
616 if let Some(slot) = slot {
617 used.push(slot);
618 }
619 slot.map(str::to_string)
620 })
621 .collect()
622 });
623 let fields = resolve_fields(&direct_rules, &instance.segments, numbers.as_deref());
624
625 let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
627
628 AhbGroupNode {
629 group_id,
630 instance_index: 0,
631 ahb_status,
632 fields,
633 children,
634 }
635}
636
637fn collect_instance_mig_numbers(
639 instance: &AssembledGroupInstance,
640) -> std::collections::HashSet<&str> {
641 let mut numbers = std::collections::HashSet::new();
642 for seg in &instance.segments {
643 if let Some(ref num) = seg.mig_number {
644 numbers.insert(num.as_str());
645 }
646 }
647 for child_group in &instance.child_groups {
648 for child_instance in &child_group.repetitions {
649 numbers.extend(collect_instance_mig_numbers(child_instance));
650 }
651 }
652 numbers
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
659 use mig_assembly::assembler::{
660 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
661 };
662 use std::collections::BTreeMap;
663
664 fn empty_workflow() -> AhbWorkflow {
665 AhbWorkflow {
666 pruefidentifikator: "11001".to_string(),
667 description: String::new(),
668 communication_direction: None,
669 fields: vec![],
670 ub_definitions: BTreeMap::new(),
671 }
672 }
673
674 fn empty_tree() -> AssembledTree {
675 AssembledTree {
676 segments: vec![],
677 groups: vec![],
678 post_group_start: 0,
679 inter_group_segments: BTreeMap::new(),
680 }
681 }
682
683 fn make_segment(
684 tag: &str,
685 elements: Vec<Vec<&str>>,
686 mig_number: Option<&str>,
687 ) -> AssembledSegment {
688 AssembledSegment {
689 tag: tag.to_string(),
690 elements: elements
691 .into_iter()
692 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
693 .collect(),
694 mig_number: mig_number.map(|s| s.to_string()),
695 segment_number: None,
696 }
697 }
698
699 fn make_rule(
700 segment_path: &str,
701 name: &str,
702 ahb_status: &str,
703 mig_number: Option<&str>,
704 element_index: Option<usize>,
705 component_index: Option<usize>,
706 ) -> AhbFieldRule {
707 AhbFieldRule {
708 segment_path: segment_path.to_string(),
709 name: name.to_string(),
710 ahb_status: ahb_status.to_string(),
711 codes: vec![],
712 parent_group_ahb_status: None,
713 segment_ahb_status: None,
714 element_index,
715 component_index,
716 mig_number: mig_number.map(|s| s.to_string()),
717 }
718 }
719
720 #[test]
721 fn test_empty_workflow_empty_tree() {
722 let workflow = empty_workflow();
723 let tree = empty_tree();
724 let result = build_validated_tree(&workflow, &tree);
725
726 assert_eq!(result.pruefidentifikator, "11001");
727 assert!(result.root_fields.is_empty());
728 assert!(result.groups.is_empty());
729 }
730
731 #[test]
732 fn test_root_field_matches_root_segment() {
733 let mut workflow = empty_workflow();
734 workflow.fields.push(make_rule(
735 "BGM/C002/1001",
736 "Nachrichtentyp",
737 "X",
738 Some("0001"),
739 Some(0),
740 Some(0),
741 ));
742
743 let tree = AssembledTree {
744 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
745 groups: vec![],
746 post_group_start: 1,
747 inter_group_segments: BTreeMap::new(),
748 };
749
750 let result = build_validated_tree(&workflow, &tree);
751
752 assert_eq!(result.root_fields.len(), 1);
753 let node = &result.root_fields[0];
754 assert_eq!(node.value, Some("E01"));
755 assert!(node.segment_elements.is_some());
756 assert_eq!(node.rule.name, "Nachrichtentyp");
757 }
758
759 #[test]
760 fn test_sg4_dtm_mig_number_matching() {
761 let mut workflow = empty_workflow();
764
765 workflow.fields.push(make_rule(
767 "SG4/DTM/C507/2380",
768 "Eingangsdatum",
769 "X",
770 Some("0082"),
771 Some(1), Some(0),
773 ));
774
775 workflow.fields.push(make_rule(
777 "SG4/DTM/C507/2380",
778 "Dokumentendatum",
779 "X",
780 Some("0083"),
781 Some(1),
782 Some(0),
783 ));
784
785 let tree = AssembledTree {
786 segments: vec![],
787 groups: vec![AssembledGroup {
788 group_id: "SG4".to_string(),
789 repetitions: vec![AssembledGroupInstance {
790 segments: vec![
791 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
792 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
793 ],
794 child_groups: vec![],
795 entry_mig_number: None,
796 variant_mig_numbers: vec![],
797 skipped_segments: vec![],
798 skipped_positions: Vec::new(),
799 }],
800 }],
801 post_group_start: 0,
802 inter_group_segments: BTreeMap::new(),
803 };
804
805 let result = build_validated_tree(&workflow, &tree);
806
807 assert_eq!(result.groups.len(), 1);
808 let sg4 = &result.groups[0];
809 assert_eq!(sg4.group_id, "SG4");
810 assert_eq!(sg4.fields.len(), 2);
811
812 let eingangsdatum = &sg4.fields[0];
814 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
815 assert_eq!(eingangsdatum.value, Some("20260101"));
816
817 let dokumentendatum = &sg4.fields[1];
819 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
820 assert_eq!(dokumentendatum.value, Some("20260401"));
821 }
822
823 #[test]
824 fn test_rule_filtered_to_correct_variant() {
825 let mut workflow = empty_workflow();
829 workflow.fields.push(make_rule(
830 "SG4/RFF/C506/1154",
831 "Referenz",
832 "X",
833 Some("0099"),
834 Some(0),
835 Some(1),
836 ));
837
838 let tree = AssembledTree {
839 segments: vec![],
840 groups: vec![AssembledGroup {
841 group_id: "SG4".to_string(),
842 repetitions: vec![AssembledGroupInstance {
843 segments: vec![], child_groups: vec![],
845 entry_mig_number: None,
846 variant_mig_numbers: vec![],
847 skipped_segments: vec![],
848 skipped_positions: Vec::new(),
849 }],
850 }],
851 post_group_start: 0,
852 inter_group_segments: BTreeMap::new(),
853 };
854
855 let result = build_validated_tree(&workflow, &tree);
856 assert_eq!(result.groups.len(), 1);
857 assert_eq!(result.groups[0].fields.len(), 0);
859 }
860
861 #[test]
862 fn test_missing_segment_within_correct_variant() {
863 let mut workflow = empty_workflow();
867 workflow.fields.push(make_rule(
869 "SG4/SEQ/1229",
870 "Qualifier",
871 "X",
872 Some("0098"),
873 Some(0),
874 Some(0),
875 ));
876 workflow.fields.push(make_rule(
878 "SG4/RFF/C506/1154",
879 "Referenz",
880 "X",
881 Some("0099"),
882 Some(0),
883 Some(1),
884 ));
885
886 let tree = AssembledTree {
887 segments: vec![],
888 groups: vec![AssembledGroup {
889 group_id: "SG4".to_string(),
890 repetitions: vec![AssembledGroupInstance {
891 segments: vec![
892 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
894 ],
895 child_groups: vec![],
896 entry_mig_number: Some("0098".to_string()),
897 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
899 skipped_segments: vec![],
900 skipped_positions: Vec::new(),
901 }],
902 }],
903 post_group_start: 0,
904 inter_group_segments: BTreeMap::new(),
905 };
906
907 let result = build_validated_tree(&workflow, &tree);
908 assert_eq!(result.groups.len(), 1);
909 assert_eq!(result.groups[0].fields.len(), 2);
911 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
912 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
913 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
915 assert_eq!(result.groups[0].fields[1].value, None);
916 }
917
918 #[test]
919 fn test_missing_group_variant_populates_unmatched_rules() {
920 let mut workflow = empty_workflow();
924
925 workflow.fields.push(make_rule(
927 "SG2/NAD/3035",
928 "MP-ID Absender Qualifier",
929 "X",
930 Some("0010"),
931 Some(0),
932 Some(0),
933 ));
934 workflow.fields.push(make_rule(
936 "SG2/NAD/C082/3039",
937 "MP-ID Absender",
938 "X",
939 Some("0010"),
940 Some(1),
941 Some(0),
942 ));
943 workflow.fields.push(make_rule(
945 "SG2/NAD/3035",
946 "MP-ID Empfänger Qualifier",
947 "X",
948 Some("0011"),
949 Some(0),
950 Some(0),
951 ));
952 workflow.fields.push(make_rule(
954 "SG2/NAD/C082/3039",
955 "MP-ID Empfänger",
956 "X",
957 Some("0011"),
958 Some(1),
959 Some(0),
960 ));
961
962 let tree = AssembledTree {
964 segments: vec![],
965 groups: vec![AssembledGroup {
966 group_id: "SG2".to_string(),
967 repetitions: vec![AssembledGroupInstance {
968 segments: vec![make_segment(
969 "NAD",
970 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
971 Some("0011"),
972 )],
973 child_groups: vec![],
974 entry_mig_number: Some("0011".to_string()),
975 variant_mig_numbers: vec!["0011".to_string()],
976 skipped_segments: vec![],
977 skipped_positions: Vec::new(),
978 }],
979 }],
980 post_group_start: 0,
981 inter_group_segments: BTreeMap::new(),
982 };
983
984 let result = build_validated_tree(&workflow, &tree);
985
986 assert_eq!(result.groups.len(), 1);
988 assert_eq!(result.groups[0].fields.len(), 2);
989 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
990
991 assert_eq!(
993 result.unmatched_rules.len(),
994 2,
995 "Expected 2 unmatched rules (NAD+MS), got {}",
996 result.unmatched_rules.len()
997 );
998 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
999 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
1000 }
1001
1002 #[test]
1003 fn test_entirely_absent_group_populates_unmatched_rules() {
1004 let mut workflow = empty_workflow();
1006 workflow.fields.push(make_rule(
1007 "SG2/NAD/3035",
1008 "MP-ID Absender Qualifier",
1009 "X",
1010 Some("0010"),
1011 Some(0),
1012 Some(0),
1013 ));
1014
1015 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
1018
1019 assert!(result.groups.is_empty());
1021
1022 assert_eq!(
1024 result.unmatched_rules.len(),
1025 1,
1026 "Expected 1 unmatched rule, got {}",
1027 result.unmatched_rules.len()
1028 );
1029 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1030 }
1031
1032 #[test]
1033 fn test_fallback_to_tag_when_no_mig_number() {
1034 let mut workflow = empty_workflow();
1035 workflow.fields.push(make_rule(
1036 "BGM/C002/1001",
1037 "Nachrichtentyp",
1038 "X",
1039 None, Some(0),
1041 Some(0),
1042 ));
1043
1044 let tree = AssembledTree {
1045 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
1046 groups: vec![],
1047 post_group_start: 1,
1048 inter_group_segments: BTreeMap::new(),
1049 };
1050
1051 let result = build_validated_tree(&workflow, &tree);
1052 assert_eq!(result.root_fields.len(), 1);
1053 assert_eq!(result.root_fields[0].value, Some("E01"));
1054 }
1055
1056 #[test]
1057 fn test_nested_child_groups() {
1058 let mut workflow = empty_workflow();
1059 workflow.fields.push(make_rule(
1061 "SG4/SG5/LOC/C517/3225",
1062 "Marktlokations-ID",
1063 "X",
1064 Some("0050"),
1065 Some(0),
1066 Some(0),
1067 ));
1068
1069 let tree = AssembledTree {
1070 segments: vec![],
1071 groups: vec![AssembledGroup {
1072 group_id: "SG4".to_string(),
1073 repetitions: vec![AssembledGroupInstance {
1074 segments: vec![],
1075 entry_mig_number: None,
1076 child_groups: vec![AssembledGroup {
1077 group_id: "SG5".to_string(),
1078 repetitions: vec![AssembledGroupInstance {
1079 segments: vec![make_segment(
1080 "LOC",
1081 vec![vec!["DE00012345678"]],
1082 Some("0050"),
1083 )],
1084 child_groups: vec![],
1085 entry_mig_number: None,
1086 variant_mig_numbers: vec![],
1087 skipped_segments: vec![],
1088 skipped_positions: Vec::new(),
1089 }],
1090 }],
1091 variant_mig_numbers: vec![],
1092 skipped_segments: vec![],
1093 skipped_positions: Vec::new(),
1094 }],
1095 }],
1096 post_group_start: 0,
1097 inter_group_segments: BTreeMap::new(),
1098 };
1099
1100 let result = build_validated_tree(&workflow, &tree);
1101 assert_eq!(result.groups.len(), 1);
1102 let sg4 = &result.groups[0];
1103 assert_eq!(sg4.children.len(), 1);
1104
1105 let sg5 = &sg4.children[0];
1106 assert_eq!(sg5.group_id, "SG5");
1107 assert_eq!(sg5.fields.len(), 1);
1108 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
1109 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
1110 }
1111
1112 #[test]
1113 fn test_qualifier_aware_segment_matching_same_tag_variants() {
1114 let mut workflow = empty_workflow();
1128
1129 let mut z33 = make_rule(
1131 "SG10/STS/C601/9015",
1132 "Statuskategorie Z33",
1133 "X",
1134 Some("00035"),
1135 Some(0),
1136 Some(0),
1137 );
1138 z33.codes = vec![super::super::validate::AhbCodeRule {
1139 value: "Z33".into(),
1140 description: String::new(),
1141 ahb_status: "X".into(),
1142 }];
1143 workflow.fields.push(z33);
1144
1145 let mut z32 = make_rule(
1147 "SG10/STS/C601/9015",
1148 "Statuskategorie Z32",
1149 "X",
1150 Some("00036"),
1151 Some(0),
1152 Some(0),
1153 );
1154 z32.codes = vec![super::super::validate::AhbCodeRule {
1155 value: "Z32".into(),
1156 description: String::new(),
1157 ahb_status: "X".into(),
1158 }];
1159 workflow.fields.push(z32);
1160
1161 let mut z40 = make_rule(
1163 "SG10/STS/C601/9015",
1164 "Statuskategorie Z40",
1165 "X",
1166 Some("00038"),
1167 Some(0),
1168 Some(0),
1169 );
1170 z40.codes = vec![super::super::validate::AhbCodeRule {
1171 value: "Z40".into(),
1172 description: String::new(),
1173 ahb_status: "X".into(),
1174 }];
1175 workflow.fields.push(z40);
1176
1177 let tree = AssembledTree {
1180 segments: vec![],
1181 groups: vec![AssembledGroup {
1182 group_id: "SG10".to_string(),
1183 repetitions: vec![AssembledGroupInstance {
1184 segments: vec![
1185 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
1186 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
1187 ],
1188 child_groups: vec![],
1189 entry_mig_number: None,
1190 variant_mig_numbers: vec![
1191 "00035".into(),
1192 "00036".into(),
1193 "00037".into(),
1194 "00038".into(),
1195 ],
1196 skipped_segments: vec![],
1197 skipped_positions: Vec::new(),
1198 }],
1199 }],
1200 post_group_start: 0,
1201 inter_group_segments: BTreeMap::new(),
1202 };
1203
1204 let result = build_validated_tree(&workflow, &tree);
1205 assert_eq!(result.groups.len(), 1);
1206 let sg10 = &result.groups[0];
1207 assert_eq!(
1208 sg10.fields.len(),
1209 3,
1210 "all three rules should pass the variant filter"
1211 );
1212
1213 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
1214
1215 assert_eq!(
1217 by_name("Statuskategorie Z33").value,
1218 None,
1219 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
1220 );
1221
1222 assert_eq!(
1225 by_name("Statuskategorie Z32").value,
1226 Some("Z32"),
1227 "rule for Z32 must attach to the STS segment with qualifier Z32"
1228 );
1229
1230 assert_eq!(
1232 by_name("Statuskategorie Z40").value,
1233 Some("Z40"),
1234 "rule for Z40 must attach to the STS segment with qualifier Z40"
1235 );
1236 }
1237}