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 let other_slot = segments.iter().enumerate().any(|(i, s)| {
376 i > 0
377 && s.tag == tag
378 && s.elements
379 .get(el_idx)
380 .and_then(|e| e.get(comp_idx))
381 .is_some_and(|v| !v.is_empty() && !expected.contains(v.as_str()))
382 });
383 if other_slot {
384 return None;
385 }
386 }
387
388 if let Some(mig_num) = rule.mig_number.as_ref() {
389 if let Some((_, seg)) = segments
391 .iter()
392 .enumerate()
393 .find(|(i, s)| number_of(*i, s) == Some(mig_num.as_str()))
394 {
395 return Some(seg);
396 }
397
398 return segments
400 .iter()
401 .enumerate()
402 .find(|(i, s)| s.tag == tag && number_of(*i, s).map_or(true, |m| m == mig_num))
403 .map(|(_, s)| s);
404 }
405
406 segments.iter().find(|s| s.tag == tag)
408}
409
410fn extract_segment_tag(segment_path: &str) -> Option<&str> {
415 segment_path.split('/').find(|part| !part.starts_with("SG"))
416}
417
418fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
420 let elem_idx = rule.element_index.unwrap_or(0);
421 let comp_idx = rule.component_index.unwrap_or(0);
422
423 let element = segment.elements.get(elem_idx)?;
424 let component = element.get(comp_idx)?;
425
426 if component.is_empty() {
427 None
428 } else {
429 Some(component.as_str())
430 }
431}
432
433fn resolve_groups<'a>(
438 assembled_groups: &'a [AssembledGroup],
439 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
440 depth: usize,
441) -> Vec<AhbGroupNode<'a>> {
442 let mut result = Vec::new();
443
444 for assembled_group in assembled_groups {
445 let rules = group_rules.get(&assembled_group.group_id);
446
447 for (instance_index, instance) in assembled_group.repetitions.iter().enumerate() {
448 let mut node =
449 resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
450 node.instance_index = instance_index;
451 result.push(node);
452 }
453 }
454
455 result
456}
457
458fn strip_n_groups(path: &str, n: usize) -> &str {
462 let mut rest = path;
463 for _ in 0..n {
464 match rest.find('/') {
465 Some(idx) => rest = &rest[idx + 1..],
466 None => return rest,
467 }
468 }
469 rest
470}
471
472fn resolve_group_instance<'a>(
481 group_id: &'a str,
482 instance: &'a AssembledGroupInstance,
483 rules: Option<&Vec<&'a AhbFieldRule>>,
484 depth: usize,
485) -> AhbGroupNode<'a> {
486 let strip_count = depth + 1;
488
489 let variant_numbers: std::collections::HashSet<&str> =
494 if !instance.variant_mig_numbers.is_empty() {
495 instance
496 .variant_mig_numbers
497 .iter()
498 .map(|s| s.as_str())
499 .collect()
500 } else {
501 collect_instance_mig_numbers(instance)
502 };
503
504 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
505 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
506 let mut ahb_status: Option<&'a str> = None;
507
508 let merged = rules.and_then(|rules| {
513 let own: Vec<&'a AhbFieldRule> = rules
514 .iter()
515 .copied()
516 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none())
517 .collect();
518 merged_variant(&own, instance)
519 });
520
521 let span = variant_numbers
526 .iter()
527 .filter_map(|n| n.parse::<u32>().ok())
528 .fold(None, |acc: Option<(u32, u32)>, n| {
529 Some(acc.map_or((n, n), |(lo, hi)| (lo.min(n), hi.max(n))))
530 });
531
532 if let Some(rules) = rules {
533 for rule in rules {
534 let stripped = strip_n_groups(&rule.segment_path, strip_count);
536 let child = extract_child_group(stripped);
537
538 if let Some(ref rule_mig) = rule.mig_number {
541 let number = rule_mig.parse::<u32>().ok();
542 let belongs = match (&merged, child) {
543 (Some((_, range)), _) => number.is_some_and(|m| range.contains(&m)),
544 (None, Some(_)) => match (number, span) {
545 (Some(m), Some((lo, hi))) => (lo..=hi).contains(&m),
546 _ => variant_numbers.contains(rule_mig.as_str()),
547 },
548 (None, None) => variant_numbers.contains(rule_mig.as_str()),
549 };
550 if !belongs {
551 continue;
552 }
553 }
554
555 if let Some(child_group_id) = child {
556 child_group_rules
557 .entry(child_group_id.to_owned())
558 .or_default()
559 .push(rule);
560 } else {
561 direct_rules.push(rule);
562 }
563 }
564 }
565 let own_status = |rule: &&'a AhbFieldRule| rule.parent_group_ahb_status.as_deref();
570 let entry = merged
571 .as_ref()
572 .map(|(entry, _)| *entry)
573 .or(instance.entry_mig_number.as_deref());
574 if let Some(status) = direct_rules
575 .iter()
576 .filter(|r| entry.is_some() && r.mig_number.as_deref() == entry)
577 .find_map(own_status)
578 .or_else(|| {
579 direct_rules
580 .iter()
581 .filter(|r| r.mig_number.is_some())
582 .find_map(own_status)
583 })
584 {
585 ahb_status = Some(status);
586 }
587
588 let numbers: Option<Vec<Option<String>>> = merged.as_ref().map(|_| {
593 let mut used: Vec<&str> = Vec::new();
594 instance
595 .segments
596 .iter()
597 .map(|seg| {
598 let of_tag = |r: &&&'a AhbFieldRule| {
599 extract_segment_tag(&r.segment_path) == Some(seg.tag.as_str())
600 };
601 let mut slots: Vec<&str> = direct_rules
602 .iter()
603 .filter(of_tag)
604 .filter_map(|r| r.mig_number.as_deref())
605 .filter(|n| !used.contains(n))
606 .collect();
607 slots.sort_by_key(|n| n.parse::<u32>().unwrap_or(u32::MAX));
608 slots.dedup();
609 let coded = |slot: &str| {
612 direct_rules
613 .iter()
614 .filter(of_tag)
615 .filter(|r| r.mig_number.as_deref() == Some(slot) && !r.codes.is_empty())
616 .collect::<Vec<_>>()
617 };
618 let carries = |slot: &&str| {
619 coded(slot).iter().any(|r| {
620 seg.elements
621 .get(r.element_index.unwrap_or(0))
622 .and_then(|e| e.get(r.component_index.unwrap_or(0)))
623 .is_some_and(|v| r.codes.iter().any(|c| &c.value == v))
624 })
625 };
626 let slot = slots
627 .iter()
628 .find(|s| carries(s))
629 .or_else(|| slots.iter().find(|s| coded(s).is_empty()))
630 .or_else(|| slots.first())
631 .copied();
632 if let Some(slot) = slot {
633 used.push(slot);
634 }
635 slot.map(str::to_string)
636 })
637 .collect()
638 });
639 let fields = resolve_fields(&direct_rules, &instance.segments, numbers.as_deref());
640
641 let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
643
644 AhbGroupNode {
645 group_id,
646 instance_index: 0,
647 ahb_status,
648 fields,
649 children,
650 }
651}
652
653fn collect_instance_mig_numbers(
655 instance: &AssembledGroupInstance,
656) -> std::collections::HashSet<&str> {
657 let mut numbers = std::collections::HashSet::new();
658 for seg in &instance.segments {
659 if let Some(ref num) = seg.mig_number {
660 numbers.insert(num.as_str());
661 }
662 }
663 for child_group in &instance.child_groups {
664 for child_instance in &child_group.repetitions {
665 numbers.extend(collect_instance_mig_numbers(child_instance));
666 }
667 }
668 numbers
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
675 use mig_assembly::assembler::{
676 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
677 };
678 use std::collections::BTreeMap;
679
680 fn empty_workflow() -> AhbWorkflow {
681 AhbWorkflow {
682 pruefidentifikator: "11001".to_string(),
683 description: String::new(),
684 communication_direction: None,
685 fields: vec![],
686 ub_definitions: BTreeMap::new(),
687 }
688 }
689
690 fn empty_tree() -> AssembledTree {
691 AssembledTree {
692 segments: vec![],
693 groups: vec![],
694 post_group_start: 0,
695 inter_group_segments: BTreeMap::new(),
696 }
697 }
698
699 fn make_segment(
700 tag: &str,
701 elements: Vec<Vec<&str>>,
702 mig_number: Option<&str>,
703 ) -> AssembledSegment {
704 AssembledSegment {
705 tag: tag.to_string(),
706 elements: elements
707 .into_iter()
708 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
709 .collect(),
710 mig_number: mig_number.map(|s| s.to_string()),
711 segment_number: None,
712 }
713 }
714
715 fn make_rule(
716 segment_path: &str,
717 name: &str,
718 ahb_status: &str,
719 mig_number: Option<&str>,
720 element_index: Option<usize>,
721 component_index: Option<usize>,
722 ) -> AhbFieldRule {
723 AhbFieldRule {
724 segment_path: segment_path.to_string(),
725 name: name.to_string(),
726 ahb_status: ahb_status.to_string(),
727 codes: vec![],
728 parent_group_ahb_status: None,
729 segment_ahb_status: None,
730 element_index,
731 component_index,
732 mig_number: mig_number.map(|s| s.to_string()),
733 format: None,
734 }
735 }
736
737 #[test]
738 fn test_empty_workflow_empty_tree() {
739 let workflow = empty_workflow();
740 let tree = empty_tree();
741 let result = build_validated_tree(&workflow, &tree);
742
743 assert_eq!(result.pruefidentifikator, "11001");
744 assert!(result.root_fields.is_empty());
745 assert!(result.groups.is_empty());
746 }
747
748 #[test]
749 fn test_root_field_matches_root_segment() {
750 let mut workflow = empty_workflow();
751 workflow.fields.push(make_rule(
752 "BGM/C002/1001",
753 "Nachrichtentyp",
754 "X",
755 Some("0001"),
756 Some(0),
757 Some(0),
758 ));
759
760 let tree = AssembledTree {
761 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
762 groups: vec![],
763 post_group_start: 1,
764 inter_group_segments: BTreeMap::new(),
765 };
766
767 let result = build_validated_tree(&workflow, &tree);
768
769 assert_eq!(result.root_fields.len(), 1);
770 let node = &result.root_fields[0];
771 assert_eq!(node.value, Some("E01"));
772 assert!(node.segment_elements.is_some());
773 assert_eq!(node.rule.name, "Nachrichtentyp");
774 }
775
776 #[test]
777 fn test_sg4_dtm_mig_number_matching() {
778 let mut workflow = empty_workflow();
781
782 workflow.fields.push(make_rule(
784 "SG4/DTM/C507/2380",
785 "Eingangsdatum",
786 "X",
787 Some("0082"),
788 Some(1), Some(0),
790 ));
791
792 workflow.fields.push(make_rule(
794 "SG4/DTM/C507/2380",
795 "Dokumentendatum",
796 "X",
797 Some("0083"),
798 Some(1),
799 Some(0),
800 ));
801
802 let tree = AssembledTree {
803 segments: vec![],
804 groups: vec![AssembledGroup {
805 group_id: "SG4".to_string(),
806 repetitions: vec![AssembledGroupInstance {
807 segments: vec![
808 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
809 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
810 ],
811 child_groups: vec![],
812 entry_mig_number: None,
813 variant_mig_numbers: vec![],
814 skipped_segments: vec![],
815 skipped_positions: Vec::new(),
816 }],
817 }],
818 post_group_start: 0,
819 inter_group_segments: BTreeMap::new(),
820 };
821
822 let result = build_validated_tree(&workflow, &tree);
823
824 assert_eq!(result.groups.len(), 1);
825 let sg4 = &result.groups[0];
826 assert_eq!(sg4.group_id, "SG4");
827 assert_eq!(sg4.fields.len(), 2);
828
829 let eingangsdatum = &sg4.fields[0];
831 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
832 assert_eq!(eingangsdatum.value, Some("20260101"));
833
834 let dokumentendatum = &sg4.fields[1];
836 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
837 assert_eq!(dokumentendatum.value, Some("20260401"));
838 }
839
840 #[test]
841 fn test_rule_filtered_to_correct_variant() {
842 let mut workflow = empty_workflow();
846 workflow.fields.push(make_rule(
847 "SG4/RFF/C506/1154",
848 "Referenz",
849 "X",
850 Some("0099"),
851 Some(0),
852 Some(1),
853 ));
854
855 let tree = AssembledTree {
856 segments: vec![],
857 groups: vec![AssembledGroup {
858 group_id: "SG4".to_string(),
859 repetitions: vec![AssembledGroupInstance {
860 segments: vec![], child_groups: vec![],
862 entry_mig_number: None,
863 variant_mig_numbers: vec![],
864 skipped_segments: vec![],
865 skipped_positions: Vec::new(),
866 }],
867 }],
868 post_group_start: 0,
869 inter_group_segments: BTreeMap::new(),
870 };
871
872 let result = build_validated_tree(&workflow, &tree);
873 assert_eq!(result.groups.len(), 1);
874 assert_eq!(result.groups[0].fields.len(), 0);
876 }
877
878 #[test]
879 fn test_missing_segment_within_correct_variant() {
880 let mut workflow = empty_workflow();
884 workflow.fields.push(make_rule(
886 "SG4/SEQ/1229",
887 "Qualifier",
888 "X",
889 Some("0098"),
890 Some(0),
891 Some(0),
892 ));
893 workflow.fields.push(make_rule(
895 "SG4/RFF/C506/1154",
896 "Referenz",
897 "X",
898 Some("0099"),
899 Some(0),
900 Some(1),
901 ));
902
903 let tree = AssembledTree {
904 segments: vec![],
905 groups: vec![AssembledGroup {
906 group_id: "SG4".to_string(),
907 repetitions: vec![AssembledGroupInstance {
908 segments: vec![
909 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
911 ],
912 child_groups: vec![],
913 entry_mig_number: Some("0098".to_string()),
914 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
916 skipped_segments: vec![],
917 skipped_positions: Vec::new(),
918 }],
919 }],
920 post_group_start: 0,
921 inter_group_segments: BTreeMap::new(),
922 };
923
924 let result = build_validated_tree(&workflow, &tree);
925 assert_eq!(result.groups.len(), 1);
926 assert_eq!(result.groups[0].fields.len(), 2);
928 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
929 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
930 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
932 assert_eq!(result.groups[0].fields[1].value, None);
933 }
934
935 #[test]
936 fn test_missing_group_variant_populates_unmatched_rules() {
937 let mut workflow = empty_workflow();
941
942 workflow.fields.push(make_rule(
944 "SG2/NAD/3035",
945 "MP-ID Absender Qualifier",
946 "X",
947 Some("0010"),
948 Some(0),
949 Some(0),
950 ));
951 workflow.fields.push(make_rule(
953 "SG2/NAD/C082/3039",
954 "MP-ID Absender",
955 "X",
956 Some("0010"),
957 Some(1),
958 Some(0),
959 ));
960 workflow.fields.push(make_rule(
962 "SG2/NAD/3035",
963 "MP-ID Empfänger Qualifier",
964 "X",
965 Some("0011"),
966 Some(0),
967 Some(0),
968 ));
969 workflow.fields.push(make_rule(
971 "SG2/NAD/C082/3039",
972 "MP-ID Empfänger",
973 "X",
974 Some("0011"),
975 Some(1),
976 Some(0),
977 ));
978
979 let tree = AssembledTree {
981 segments: vec![],
982 groups: vec![AssembledGroup {
983 group_id: "SG2".to_string(),
984 repetitions: vec![AssembledGroupInstance {
985 segments: vec![make_segment(
986 "NAD",
987 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
988 Some("0011"),
989 )],
990 child_groups: vec![],
991 entry_mig_number: Some("0011".to_string()),
992 variant_mig_numbers: vec!["0011".to_string()],
993 skipped_segments: vec![],
994 skipped_positions: Vec::new(),
995 }],
996 }],
997 post_group_start: 0,
998 inter_group_segments: BTreeMap::new(),
999 };
1000
1001 let result = build_validated_tree(&workflow, &tree);
1002
1003 assert_eq!(result.groups.len(), 1);
1005 assert_eq!(result.groups[0].fields.len(), 2);
1006 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
1007
1008 assert_eq!(
1010 result.unmatched_rules.len(),
1011 2,
1012 "Expected 2 unmatched rules (NAD+MS), got {}",
1013 result.unmatched_rules.len()
1014 );
1015 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1016 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
1017 }
1018
1019 #[test]
1020 fn test_entirely_absent_group_populates_unmatched_rules() {
1021 let mut workflow = empty_workflow();
1023 workflow.fields.push(make_rule(
1024 "SG2/NAD/3035",
1025 "MP-ID Absender Qualifier",
1026 "X",
1027 Some("0010"),
1028 Some(0),
1029 Some(0),
1030 ));
1031
1032 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
1035
1036 assert!(result.groups.is_empty());
1038
1039 assert_eq!(
1041 result.unmatched_rules.len(),
1042 1,
1043 "Expected 1 unmatched rule, got {}",
1044 result.unmatched_rules.len()
1045 );
1046 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1047 }
1048
1049 #[test]
1050 fn test_fallback_to_tag_when_no_mig_number() {
1051 let mut workflow = empty_workflow();
1052 workflow.fields.push(make_rule(
1053 "BGM/C002/1001",
1054 "Nachrichtentyp",
1055 "X",
1056 None, Some(0),
1058 Some(0),
1059 ));
1060
1061 let tree = AssembledTree {
1062 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
1063 groups: vec![],
1064 post_group_start: 1,
1065 inter_group_segments: BTreeMap::new(),
1066 };
1067
1068 let result = build_validated_tree(&workflow, &tree);
1069 assert_eq!(result.root_fields.len(), 1);
1070 assert_eq!(result.root_fields[0].value, Some("E01"));
1071 }
1072
1073 #[test]
1074 fn test_nested_child_groups() {
1075 let mut workflow = empty_workflow();
1076 workflow.fields.push(make_rule(
1078 "SG4/SG5/LOC/C517/3225",
1079 "Marktlokations-ID",
1080 "X",
1081 Some("0050"),
1082 Some(0),
1083 Some(0),
1084 ));
1085
1086 let tree = AssembledTree {
1087 segments: vec![],
1088 groups: vec![AssembledGroup {
1089 group_id: "SG4".to_string(),
1090 repetitions: vec![AssembledGroupInstance {
1091 segments: vec![],
1092 entry_mig_number: None,
1093 child_groups: vec![AssembledGroup {
1094 group_id: "SG5".to_string(),
1095 repetitions: vec![AssembledGroupInstance {
1096 segments: vec![make_segment(
1097 "LOC",
1098 vec![vec!["DE00012345678"]],
1099 Some("0050"),
1100 )],
1101 child_groups: vec![],
1102 entry_mig_number: None,
1103 variant_mig_numbers: vec![],
1104 skipped_segments: vec![],
1105 skipped_positions: Vec::new(),
1106 }],
1107 }],
1108 variant_mig_numbers: vec![],
1109 skipped_segments: vec![],
1110 skipped_positions: Vec::new(),
1111 }],
1112 }],
1113 post_group_start: 0,
1114 inter_group_segments: BTreeMap::new(),
1115 };
1116
1117 let result = build_validated_tree(&workflow, &tree);
1118 assert_eq!(result.groups.len(), 1);
1119 let sg4 = &result.groups[0];
1120 assert_eq!(sg4.children.len(), 1);
1121
1122 let sg5 = &sg4.children[0];
1123 assert_eq!(sg5.group_id, "SG5");
1124 assert_eq!(sg5.fields.len(), 1);
1125 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
1126 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
1127 }
1128
1129 #[test]
1130 fn test_qualifier_aware_segment_matching_same_tag_variants() {
1131 let mut workflow = empty_workflow();
1145
1146 let mut z33 = make_rule(
1148 "SG10/STS/C601/9015",
1149 "Statuskategorie Z33",
1150 "X",
1151 Some("00035"),
1152 Some(0),
1153 Some(0),
1154 );
1155 z33.codes = vec![super::super::validate::AhbCodeRule {
1156 value: "Z33".into(),
1157 description: String::new(),
1158 ahb_status: "X".into(),
1159 }];
1160 workflow.fields.push(z33);
1161
1162 let mut z32 = make_rule(
1164 "SG10/STS/C601/9015",
1165 "Statuskategorie Z32",
1166 "X",
1167 Some("00036"),
1168 Some(0),
1169 Some(0),
1170 );
1171 z32.codes = vec![super::super::validate::AhbCodeRule {
1172 value: "Z32".into(),
1173 description: String::new(),
1174 ahb_status: "X".into(),
1175 }];
1176 workflow.fields.push(z32);
1177
1178 let mut z40 = make_rule(
1180 "SG10/STS/C601/9015",
1181 "Statuskategorie Z40",
1182 "X",
1183 Some("00038"),
1184 Some(0),
1185 Some(0),
1186 );
1187 z40.codes = vec![super::super::validate::AhbCodeRule {
1188 value: "Z40".into(),
1189 description: String::new(),
1190 ahb_status: "X".into(),
1191 }];
1192 workflow.fields.push(z40);
1193
1194 let tree = AssembledTree {
1197 segments: vec![],
1198 groups: vec![AssembledGroup {
1199 group_id: "SG10".to_string(),
1200 repetitions: vec![AssembledGroupInstance {
1201 segments: vec![
1202 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
1203 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
1204 ],
1205 child_groups: vec![],
1206 entry_mig_number: None,
1207 variant_mig_numbers: vec![
1208 "00035".into(),
1209 "00036".into(),
1210 "00037".into(),
1211 "00038".into(),
1212 ],
1213 skipped_segments: vec![],
1214 skipped_positions: Vec::new(),
1215 }],
1216 }],
1217 post_group_start: 0,
1218 inter_group_segments: BTreeMap::new(),
1219 };
1220
1221 let result = build_validated_tree(&workflow, &tree);
1222 assert_eq!(result.groups.len(), 1);
1223 let sg10 = &result.groups[0];
1224 assert_eq!(
1225 sg10.fields.len(),
1226 3,
1227 "all three rules should pass the variant filter"
1228 );
1229
1230 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
1231
1232 assert_eq!(
1234 by_name("Statuskategorie Z33").value,
1235 None,
1236 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
1237 );
1238
1239 assert_eq!(
1242 by_name("Statuskategorie Z32").value,
1243 Some("Z32"),
1244 "rule for Z32 must attach to the STS segment with qualifier Z32"
1245 );
1246
1247 assert_eq!(
1249 by_name("Statuskategorie Z40").value,
1250 Some("Z40"),
1251 "rule for Z40 must attach to the STS segment with qualifier Z40"
1252 );
1253 }
1254}