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 package_conditions: &'a BTreeMap<u32, ConditionExpr>,
54 pub root_fields: Vec<AhbNode<'a>>,
56 pub groups: Vec<AhbGroupNode<'a>>,
58 pub unmatched_rules: Vec<&'a AhbFieldRule>,
65}
66
67pub fn build_validated_tree<'a>(
73 workflow: &'a AhbWorkflow,
74 tree: &'a AssembledTree,
75) -> ValidatedTree<'a> {
76 let mut root_rules: Vec<&'a AhbFieldRule> = Vec::new();
78 let mut group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
80
81 for rule in &workflow.fields {
82 match extract_top_group(&rule.segment_path) {
83 Some(group_id) => {
84 group_rules
85 .entry(group_id.to_owned())
86 .or_default()
87 .push(rule);
88 }
89 None => {
90 root_rules.push(rule);
91 }
92 }
93 }
94
95 let root_fields = resolve_fields(&root_rules, &tree.segments, None);
97
98 let groups = resolve_groups(&tree.groups, &group_rules, 0);
100
101 let mut matched_mig_numbers: std::collections::HashSet<&str> = std::collections::HashSet::new();
104 collect_matched_mig_numbers_from_groups(&groups, &mut matched_mig_numbers);
105 for seg in &tree.segments {
107 if let Some(ref num) = seg.mig_number {
108 matched_mig_numbers.insert(num.as_str());
109 }
110 }
111
112 let unmatched_rules: Vec<&AhbFieldRule> = workflow
113 .fields
114 .iter()
115 .filter(|rule| {
116 rule.mig_number
117 .as_ref()
118 .is_some_and(|num| !matched_mig_numbers.contains(num.as_str()))
119 })
120 .collect();
121
122 ValidatedTree {
123 pruefidentifikator: &workflow.pruefidentifikator,
124 ub_definitions: &workflow.ub_definitions,
125 package_conditions: &workflow.package_conditions,
126 root_fields,
127 groups,
128 unmatched_rules,
129 }
130}
131
132fn collect_matched_mig_numbers_from_groups<'a>(
138 groups: &[AhbGroupNode<'a>],
139 out: &mut std::collections::HashSet<&'a str>,
140) {
141 for group in groups {
142 for node in &group.fields {
143 if let Some(ref num) = node.rule.mig_number {
144 out.insert(num.as_str());
145 }
146 }
147 collect_matched_mig_numbers_from_groups(&group.children, out);
148 }
149}
150
151fn merged_variant<'a>(
157 rules: &[&'a AhbFieldRule],
158 instance: &AssembledGroupInstance,
159) -> Option<(&'a str, std::ops::Range<u32>)> {
160 let entry = instance.segments.first()?;
161 let recorded = instance.entry_mig_number.as_deref()?;
165 if !rules.iter().any(|r| {
166 r.mig_number.as_deref() == Some(recorded)
167 && extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
168 }) {
169 return None;
170 }
171 if instance
175 .segments
176 .iter()
177 .filter(|s| s.tag == entry.tag)
178 .count()
179 != 1
180 {
181 return None;
182 }
183 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
184 let mut positions: Vec<(usize, usize)> = Vec::new();
185 for r in rules {
186 let pos = (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0));
187 if extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
188 && !r.codes.is_empty()
189 && !positions.contains(&pos)
190 {
191 positions.push(pos);
192 }
193 }
194 for (element, component) in positions {
195 let mut entries: Vec<(u32, &'a AhbFieldRule)> = rules
197 .iter()
198 .filter(|r| {
199 extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
200 && r.element_index.unwrap_or(0) == element
201 && r.component_index.unwrap_or(0) == component
202 && !r.codes.is_empty()
203 })
204 .filter_map(|r| Some((number(r)?, *r)))
205 .collect();
206 entries.sort_by_key(|(n, _)| *n);
207 entries.dedup_by_key(|(n, _)| *n);
208 let all_entry_numbers: std::collections::BTreeSet<u32> = rules
212 .iter()
213 .filter(|r| extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str()))
214 .filter_map(|r| number(r))
215 .collect();
216 if entries.len() != all_entry_numbers.len() {
217 continue;
218 }
219 let disjoint = entries.len() >= 2
220 && entries.iter().enumerate().all(|(i, (_, a))| {
221 entries[i + 1..].iter().all(|(_, b)| {
222 !a.codes
223 .iter()
224 .any(|c| b.codes.iter().any(|d| d.value == c.value))
225 })
226 });
227 if !disjoint {
228 continue;
229 }
230 let value = entry.elements.get(element)?.get(component)?;
231 let at = entries
232 .iter()
233 .position(|(_, r)| r.codes.iter().any(|c| &c.value == value))?;
234 let start = entries[at].0;
235 let end = entries.get(at + 1).map_or(u32::MAX, |(n, _)| *n);
236 return Some((entries[at].1.mig_number.as_deref()?, start..end));
237 }
238 None
239}
240
241fn extract_top_group(segment_path: &str) -> Option<&str> {
247 let first = segment_path.split('/').next()?;
248 if first.starts_with("SG") {
249 Some(first)
250 } else {
251 None
252 }
253}
254
255fn extract_child_group(stripped_path: &str) -> Option<&str> {
260 let first = stripped_path.split('/').next()?;
261 if first.starts_with("SG") {
262 Some(first)
263 } else {
264 None
265 }
266}
267
268fn resolve_fields<'a>(
274 rules: &[&'a AhbFieldRule],
275 segments: &'a [AssembledSegment],
276 numbers: Option<&[Option<String>]>,
277) -> Vec<AhbNode<'a>> {
278 rules
279 .iter()
280 .map(|rule| {
281 let matched_segment = find_segment(rule, segments, numbers);
282 let (value, elements, segment_number) = match matched_segment {
283 Some(seg) => {
284 let val = extract_value(seg, rule);
285 (val, Some(seg.elements.as_slice()), seg.segment_number)
286 }
287 None => {
288 let fallback_num = extract_segment_tag(&rule.segment_path)
297 .and_then(|tag| segments.iter().find(|s| s.tag == tag))
298 .and_then(|s| s.segment_number);
299 (None, None, fallback_num)
300 }
301 };
302 AhbNode {
303 rule,
304 value,
305 segment_elements: elements,
306 matched_segment_number: segment_number,
307 }
308 })
309 .collect()
310}
311
312fn find_segment<'a>(
324 rule: &AhbFieldRule,
325 segments: &'a [AssembledSegment],
326 numbers: Option<&[Option<String>]>,
327) -> Option<&'a AssembledSegment> {
328 let number_of = |i: usize, s: &'a AssembledSegment| -> Option<&str> {
330 match numbers {
331 Some(n) => n.get(i).and_then(|n| n.as_deref()),
332 None => s.mig_number.as_deref(),
333 }
334 };
335 let tag = extract_segment_tag(&rule.segment_path)?;
336
337 if !rule.codes.is_empty() {
356 let el_idx = rule.element_index.unwrap_or(0);
357 let comp_idx = rule.component_index.unwrap_or(0);
358 let expected: std::collections::HashSet<&str> =
359 rule.codes.iter().map(|c| c.value.as_str()).collect();
360 if let Some(seg) = segments.iter().find(|s| {
361 s.tag == tag
362 && s.elements
363 .get(el_idx)
364 .and_then(|e| e.get(comp_idx))
365 .is_some_and(|v| expected.contains(v.as_str()))
366 }) {
367 return Some(seg);
368 }
369 let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
370 if same_tag_count > 1 {
371 return None;
372 }
373 let other_slot = segments.iter().enumerate().any(|(i, s)| {
379 i > 0
380 && s.tag == tag
381 && s.elements
382 .get(el_idx)
383 .and_then(|e| e.get(comp_idx))
384 .is_some_and(|v| !v.is_empty() && !expected.contains(v.as_str()))
385 });
386 if other_slot {
387 return None;
388 }
389 }
390
391 if let Some(mig_num) = rule.mig_number.as_ref() {
392 if let Some((_, seg)) = segments
394 .iter()
395 .enumerate()
396 .find(|(i, s)| number_of(*i, s) == Some(mig_num.as_str()))
397 {
398 return Some(seg);
399 }
400
401 return segments
403 .iter()
404 .enumerate()
405 .find(|(i, s)| s.tag == tag && number_of(*i, s).map_or(true, |m| m == mig_num))
406 .map(|(_, s)| s);
407 }
408
409 segments.iter().find(|s| s.tag == tag)
411}
412
413fn extract_segment_tag(segment_path: &str) -> Option<&str> {
418 segment_path.split('/').find(|part| !part.starts_with("SG"))
419}
420
421fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
423 let elem_idx = rule.element_index.unwrap_or(0);
424 let comp_idx = rule.component_index.unwrap_or(0);
425
426 let element = segment.elements.get(elem_idx)?;
427 let component = element.get(comp_idx)?;
428
429 if component.is_empty() {
430 None
431 } else {
432 Some(component.as_str())
433 }
434}
435
436fn resolve_groups<'a>(
441 assembled_groups: &'a [AssembledGroup],
442 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
443 depth: usize,
444) -> Vec<AhbGroupNode<'a>> {
445 let mut result = Vec::new();
446
447 for assembled_group in assembled_groups {
448 let rules = group_rules.get(&assembled_group.group_id);
449
450 for (instance_index, instance) in assembled_group.repetitions.iter().enumerate() {
451 let mut node =
452 resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
453 node.instance_index = instance_index;
454 result.push(node);
455 }
456 }
457
458 result
459}
460
461fn strip_n_groups(path: &str, n: usize) -> &str {
465 let mut rest = path;
466 for _ in 0..n {
467 match rest.find('/') {
468 Some(idx) => rest = &rest[idx + 1..],
469 None => return rest,
470 }
471 }
472 rest
473}
474
475fn resolve_group_instance<'a>(
484 group_id: &'a str,
485 instance: &'a AssembledGroupInstance,
486 rules: Option<&Vec<&'a AhbFieldRule>>,
487 depth: usize,
488) -> AhbGroupNode<'a> {
489 let strip_count = depth + 1;
491
492 let variant_numbers: std::collections::HashSet<&str> =
497 if !instance.variant_mig_numbers.is_empty() {
498 instance
499 .variant_mig_numbers
500 .iter()
501 .map(|s| s.as_str())
502 .collect()
503 } else {
504 collect_instance_mig_numbers(instance)
505 };
506
507 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
508 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
509 let mut ahb_status: Option<&'a str> = None;
510
511 let merged = rules.and_then(|rules| {
516 let own: Vec<&'a AhbFieldRule> = rules
517 .iter()
518 .copied()
519 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none())
520 .collect();
521 merged_variant(&own, instance)
522 });
523
524 let span = variant_numbers
529 .iter()
530 .filter_map(|n| n.parse::<u32>().ok())
531 .fold(None, |acc: Option<(u32, u32)>, n| {
532 Some(acc.map_or((n, n), |(lo, hi)| (lo.min(n), hi.max(n))))
533 });
534
535 if let Some(rules) = rules {
536 for rule in rules {
537 let stripped = strip_n_groups(&rule.segment_path, strip_count);
539 let child = extract_child_group(stripped);
540
541 if let Some(ref rule_mig) = rule.mig_number {
544 let number = rule_mig.parse::<u32>().ok();
545 let belongs = match (&merged, child) {
546 (Some((_, range)), _) => number.is_some_and(|m| range.contains(&m)),
547 (None, Some(_)) => match (number, span) {
548 (Some(m), Some((lo, hi))) => (lo..=hi).contains(&m),
549 _ => variant_numbers.contains(rule_mig.as_str()),
550 },
551 (None, None) => variant_numbers.contains(rule_mig.as_str()),
552 };
553 if !belongs {
554 continue;
555 }
556 }
557
558 if let Some(child_group_id) = child {
559 child_group_rules
560 .entry(child_group_id.to_owned())
561 .or_default()
562 .push(rule);
563 } else {
564 direct_rules.push(rule);
565 }
566 }
567 }
568 let own_status = |rule: &&'a AhbFieldRule| rule.parent_group_ahb_status.as_deref();
573 let entry = merged
574 .as_ref()
575 .map(|(entry, _)| *entry)
576 .or(instance.entry_mig_number.as_deref());
577 if let Some(status) = direct_rules
578 .iter()
579 .filter(|r| entry.is_some() && r.mig_number.as_deref() == entry)
580 .find_map(own_status)
581 .or_else(|| {
582 direct_rules
583 .iter()
584 .filter(|r| r.mig_number.is_some())
585 .find_map(own_status)
586 })
587 {
588 ahb_status = Some(status);
589 }
590
591 let numbers: Option<Vec<Option<String>>> = merged.as_ref().map(|_| {
596 let mut used: Vec<&str> = Vec::new();
597 instance
598 .segments
599 .iter()
600 .map(|seg| {
601 let of_tag = |r: &&&'a AhbFieldRule| {
602 extract_segment_tag(&r.segment_path) == Some(seg.tag.as_str())
603 };
604 let mut slots: Vec<&str> = direct_rules
605 .iter()
606 .filter(of_tag)
607 .filter_map(|r| r.mig_number.as_deref())
608 .filter(|n| !used.contains(n))
609 .collect();
610 slots.sort_by_key(|n| n.parse::<u32>().unwrap_or(u32::MAX));
611 slots.dedup();
612 let coded = |slot: &str| {
615 direct_rules
616 .iter()
617 .filter(of_tag)
618 .filter(|r| r.mig_number.as_deref() == Some(slot) && !r.codes.is_empty())
619 .collect::<Vec<_>>()
620 };
621 let carries = |slot: &&str| {
622 coded(slot).iter().any(|r| {
623 seg.elements
624 .get(r.element_index.unwrap_or(0))
625 .and_then(|e| e.get(r.component_index.unwrap_or(0)))
626 .is_some_and(|v| r.codes.iter().any(|c| &c.value == v))
627 })
628 };
629 let slot = slots
630 .iter()
631 .find(|s| carries(s))
632 .or_else(|| slots.iter().find(|s| coded(s).is_empty()))
633 .or_else(|| slots.first())
634 .copied();
635 if let Some(slot) = slot {
636 used.push(slot);
637 }
638 slot.map(str::to_string)
639 })
640 .collect()
641 });
642 let fields = resolve_fields(&direct_rules, &instance.segments, numbers.as_deref());
643
644 let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
646
647 AhbGroupNode {
648 group_id,
649 instance_index: 0,
650 ahb_status,
651 fields,
652 children,
653 }
654}
655
656fn collect_instance_mig_numbers(
658 instance: &AssembledGroupInstance,
659) -> std::collections::HashSet<&str> {
660 let mut numbers = std::collections::HashSet::new();
661 for seg in &instance.segments {
662 if let Some(ref num) = seg.mig_number {
663 numbers.insert(num.as_str());
664 }
665 }
666 for child_group in &instance.child_groups {
667 for child_instance in &child_group.repetitions {
668 numbers.extend(collect_instance_mig_numbers(child_instance));
669 }
670 }
671 numbers
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
678 use mig_assembly::assembler::{
679 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
680 };
681 use std::collections::BTreeMap;
682
683 fn empty_workflow() -> AhbWorkflow {
684 AhbWorkflow {
685 pruefidentifikator: "11001".to_string(),
686 description: String::new(),
687 communication_direction: None,
688 fields: vec![],
689 ub_definitions: BTreeMap::new(),
690 package_conditions: Default::default(),
691 }
692 }
693
694 fn empty_tree() -> AssembledTree {
695 AssembledTree {
696 segments: vec![],
697 groups: vec![],
698 post_group_start: 0,
699 inter_group_segments: BTreeMap::new(),
700 }
701 }
702
703 fn make_segment(
704 tag: &str,
705 elements: Vec<Vec<&str>>,
706 mig_number: Option<&str>,
707 ) -> AssembledSegment {
708 AssembledSegment {
709 tag: tag.to_string(),
710 elements: elements
711 .into_iter()
712 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
713 .collect(),
714 mig_number: mig_number.map(|s| s.to_string()),
715 segment_number: None,
716 }
717 }
718
719 fn make_rule(
720 segment_path: &str,
721 name: &str,
722 ahb_status: &str,
723 mig_number: Option<&str>,
724 element_index: Option<usize>,
725 component_index: Option<usize>,
726 ) -> AhbFieldRule {
727 AhbFieldRule {
728 segment_path: segment_path.to_string(),
729 name: name.to_string(),
730 ahb_status: ahb_status.to_string(),
731 codes: vec![],
732 parent_group_ahb_status: None,
733 segment_ahb_status: None,
734 element_index,
735 component_index,
736 mig_number: mig_number.map(|s| s.to_string()),
737 format: None,
738 }
739 }
740
741 #[test]
742 fn test_empty_workflow_empty_tree() {
743 let workflow = empty_workflow();
744 let tree = empty_tree();
745 let result = build_validated_tree(&workflow, &tree);
746
747 assert_eq!(result.pruefidentifikator, "11001");
748 assert!(result.root_fields.is_empty());
749 assert!(result.groups.is_empty());
750 }
751
752 #[test]
753 fn test_root_field_matches_root_segment() {
754 let mut workflow = empty_workflow();
755 workflow.fields.push(make_rule(
756 "BGM/C002/1001",
757 "Nachrichtentyp",
758 "X",
759 Some("0001"),
760 Some(0),
761 Some(0),
762 ));
763
764 let tree = AssembledTree {
765 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
766 groups: vec![],
767 post_group_start: 1,
768 inter_group_segments: BTreeMap::new(),
769 };
770
771 let result = build_validated_tree(&workflow, &tree);
772
773 assert_eq!(result.root_fields.len(), 1);
774 let node = &result.root_fields[0];
775 assert_eq!(node.value, Some("E01"));
776 assert!(node.segment_elements.is_some());
777 assert_eq!(node.rule.name, "Nachrichtentyp");
778 }
779
780 #[test]
781 fn test_sg4_dtm_mig_number_matching() {
782 let mut workflow = empty_workflow();
785
786 workflow.fields.push(make_rule(
788 "SG4/DTM/C507/2380",
789 "Eingangsdatum",
790 "X",
791 Some("0082"),
792 Some(1), Some(0),
794 ));
795
796 workflow.fields.push(make_rule(
798 "SG4/DTM/C507/2380",
799 "Dokumentendatum",
800 "X",
801 Some("0083"),
802 Some(1),
803 Some(0),
804 ));
805
806 let tree = AssembledTree {
807 segments: vec![],
808 groups: vec![AssembledGroup {
809 group_id: "SG4".to_string(),
810 repetitions: vec![AssembledGroupInstance {
811 segments: vec![
812 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
813 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
814 ],
815 child_groups: vec![],
816 entry_mig_number: None,
817 variant_mig_numbers: vec![],
818 skipped_segments: vec![],
819 skipped_positions: Vec::new(),
820 }],
821 }],
822 post_group_start: 0,
823 inter_group_segments: BTreeMap::new(),
824 };
825
826 let result = build_validated_tree(&workflow, &tree);
827
828 assert_eq!(result.groups.len(), 1);
829 let sg4 = &result.groups[0];
830 assert_eq!(sg4.group_id, "SG4");
831 assert_eq!(sg4.fields.len(), 2);
832
833 let eingangsdatum = &sg4.fields[0];
835 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
836 assert_eq!(eingangsdatum.value, Some("20260101"));
837
838 let dokumentendatum = &sg4.fields[1];
840 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
841 assert_eq!(dokumentendatum.value, Some("20260401"));
842 }
843
844 #[test]
845 fn test_rule_filtered_to_correct_variant() {
846 let mut workflow = empty_workflow();
850 workflow.fields.push(make_rule(
851 "SG4/RFF/C506/1154",
852 "Referenz",
853 "X",
854 Some("0099"),
855 Some(0),
856 Some(1),
857 ));
858
859 let tree = AssembledTree {
860 segments: vec![],
861 groups: vec![AssembledGroup {
862 group_id: "SG4".to_string(),
863 repetitions: vec![AssembledGroupInstance {
864 segments: vec![], child_groups: vec![],
866 entry_mig_number: None,
867 variant_mig_numbers: vec![],
868 skipped_segments: vec![],
869 skipped_positions: Vec::new(),
870 }],
871 }],
872 post_group_start: 0,
873 inter_group_segments: BTreeMap::new(),
874 };
875
876 let result = build_validated_tree(&workflow, &tree);
877 assert_eq!(result.groups.len(), 1);
878 assert_eq!(result.groups[0].fields.len(), 0);
880 }
881
882 #[test]
883 fn test_missing_segment_within_correct_variant() {
884 let mut workflow = empty_workflow();
888 workflow.fields.push(make_rule(
890 "SG4/SEQ/1229",
891 "Qualifier",
892 "X",
893 Some("0098"),
894 Some(0),
895 Some(0),
896 ));
897 workflow.fields.push(make_rule(
899 "SG4/RFF/C506/1154",
900 "Referenz",
901 "X",
902 Some("0099"),
903 Some(0),
904 Some(1),
905 ));
906
907 let tree = AssembledTree {
908 segments: vec![],
909 groups: vec![AssembledGroup {
910 group_id: "SG4".to_string(),
911 repetitions: vec![AssembledGroupInstance {
912 segments: vec![
913 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
915 ],
916 child_groups: vec![],
917 entry_mig_number: Some("0098".to_string()),
918 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
920 skipped_segments: vec![],
921 skipped_positions: Vec::new(),
922 }],
923 }],
924 post_group_start: 0,
925 inter_group_segments: BTreeMap::new(),
926 };
927
928 let result = build_validated_tree(&workflow, &tree);
929 assert_eq!(result.groups.len(), 1);
930 assert_eq!(result.groups[0].fields.len(), 2);
932 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
933 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
934 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
936 assert_eq!(result.groups[0].fields[1].value, None);
937 }
938
939 #[test]
940 fn test_missing_group_variant_populates_unmatched_rules() {
941 let mut workflow = empty_workflow();
945
946 workflow.fields.push(make_rule(
948 "SG2/NAD/3035",
949 "MP-ID Absender Qualifier",
950 "X",
951 Some("0010"),
952 Some(0),
953 Some(0),
954 ));
955 workflow.fields.push(make_rule(
957 "SG2/NAD/C082/3039",
958 "MP-ID Absender",
959 "X",
960 Some("0010"),
961 Some(1),
962 Some(0),
963 ));
964 workflow.fields.push(make_rule(
966 "SG2/NAD/3035",
967 "MP-ID Empfänger Qualifier",
968 "X",
969 Some("0011"),
970 Some(0),
971 Some(0),
972 ));
973 workflow.fields.push(make_rule(
975 "SG2/NAD/C082/3039",
976 "MP-ID Empfänger",
977 "X",
978 Some("0011"),
979 Some(1),
980 Some(0),
981 ));
982
983 let tree = AssembledTree {
985 segments: vec![],
986 groups: vec![AssembledGroup {
987 group_id: "SG2".to_string(),
988 repetitions: vec![AssembledGroupInstance {
989 segments: vec![make_segment(
990 "NAD",
991 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
992 Some("0011"),
993 )],
994 child_groups: vec![],
995 entry_mig_number: Some("0011".to_string()),
996 variant_mig_numbers: vec!["0011".to_string()],
997 skipped_segments: vec![],
998 skipped_positions: Vec::new(),
999 }],
1000 }],
1001 post_group_start: 0,
1002 inter_group_segments: BTreeMap::new(),
1003 };
1004
1005 let result = build_validated_tree(&workflow, &tree);
1006
1007 assert_eq!(result.groups.len(), 1);
1009 assert_eq!(result.groups[0].fields.len(), 2);
1010 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
1011
1012 assert_eq!(
1014 result.unmatched_rules.len(),
1015 2,
1016 "Expected 2 unmatched rules (NAD+MS), got {}",
1017 result.unmatched_rules.len()
1018 );
1019 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1020 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
1021 }
1022
1023 #[test]
1024 fn test_entirely_absent_group_populates_unmatched_rules() {
1025 let mut workflow = empty_workflow();
1027 workflow.fields.push(make_rule(
1028 "SG2/NAD/3035",
1029 "MP-ID Absender Qualifier",
1030 "X",
1031 Some("0010"),
1032 Some(0),
1033 Some(0),
1034 ));
1035
1036 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
1039
1040 assert!(result.groups.is_empty());
1042
1043 assert_eq!(
1045 result.unmatched_rules.len(),
1046 1,
1047 "Expected 1 unmatched rule, got {}",
1048 result.unmatched_rules.len()
1049 );
1050 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1051 }
1052
1053 #[test]
1054 fn test_fallback_to_tag_when_no_mig_number() {
1055 let mut workflow = empty_workflow();
1056 workflow.fields.push(make_rule(
1057 "BGM/C002/1001",
1058 "Nachrichtentyp",
1059 "X",
1060 None, Some(0),
1062 Some(0),
1063 ));
1064
1065 let tree = AssembledTree {
1066 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
1067 groups: vec![],
1068 post_group_start: 1,
1069 inter_group_segments: BTreeMap::new(),
1070 };
1071
1072 let result = build_validated_tree(&workflow, &tree);
1073 assert_eq!(result.root_fields.len(), 1);
1074 assert_eq!(result.root_fields[0].value, Some("E01"));
1075 }
1076
1077 #[test]
1078 fn test_nested_child_groups() {
1079 let mut workflow = empty_workflow();
1080 workflow.fields.push(make_rule(
1082 "SG4/SG5/LOC/C517/3225",
1083 "Marktlokations-ID",
1084 "X",
1085 Some("0050"),
1086 Some(0),
1087 Some(0),
1088 ));
1089
1090 let tree = AssembledTree {
1091 segments: vec![],
1092 groups: vec![AssembledGroup {
1093 group_id: "SG4".to_string(),
1094 repetitions: vec![AssembledGroupInstance {
1095 segments: vec![],
1096 entry_mig_number: None,
1097 child_groups: vec![AssembledGroup {
1098 group_id: "SG5".to_string(),
1099 repetitions: vec![AssembledGroupInstance {
1100 segments: vec![make_segment(
1101 "LOC",
1102 vec![vec!["DE00012345678"]],
1103 Some("0050"),
1104 )],
1105 child_groups: vec![],
1106 entry_mig_number: None,
1107 variant_mig_numbers: vec![],
1108 skipped_segments: vec![],
1109 skipped_positions: Vec::new(),
1110 }],
1111 }],
1112 variant_mig_numbers: vec![],
1113 skipped_segments: vec![],
1114 skipped_positions: Vec::new(),
1115 }],
1116 }],
1117 post_group_start: 0,
1118 inter_group_segments: BTreeMap::new(),
1119 };
1120
1121 let result = build_validated_tree(&workflow, &tree);
1122 assert_eq!(result.groups.len(), 1);
1123 let sg4 = &result.groups[0];
1124 assert_eq!(sg4.children.len(), 1);
1125
1126 let sg5 = &sg4.children[0];
1127 assert_eq!(sg5.group_id, "SG5");
1128 assert_eq!(sg5.fields.len(), 1);
1129 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
1130 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
1131 }
1132
1133 #[test]
1134 fn test_qualifier_aware_segment_matching_same_tag_variants() {
1135 let mut workflow = empty_workflow();
1149
1150 let mut z33 = make_rule(
1152 "SG10/STS/C601/9015",
1153 "Statuskategorie Z33",
1154 "X",
1155 Some("00035"),
1156 Some(0),
1157 Some(0),
1158 );
1159 z33.codes = vec![super::super::validate::AhbCodeRule {
1160 value: "Z33".into(),
1161 description: String::new(),
1162 ahb_status: "X".into(),
1163 }];
1164 workflow.fields.push(z33);
1165
1166 let mut z32 = make_rule(
1168 "SG10/STS/C601/9015",
1169 "Statuskategorie Z32",
1170 "X",
1171 Some("00036"),
1172 Some(0),
1173 Some(0),
1174 );
1175 z32.codes = vec![super::super::validate::AhbCodeRule {
1176 value: "Z32".into(),
1177 description: String::new(),
1178 ahb_status: "X".into(),
1179 }];
1180 workflow.fields.push(z32);
1181
1182 let mut z40 = make_rule(
1184 "SG10/STS/C601/9015",
1185 "Statuskategorie Z40",
1186 "X",
1187 Some("00038"),
1188 Some(0),
1189 Some(0),
1190 );
1191 z40.codes = vec![super::super::validate::AhbCodeRule {
1192 value: "Z40".into(),
1193 description: String::new(),
1194 ahb_status: "X".into(),
1195 }];
1196 workflow.fields.push(z40);
1197
1198 let tree = AssembledTree {
1201 segments: vec![],
1202 groups: vec![AssembledGroup {
1203 group_id: "SG10".to_string(),
1204 repetitions: vec![AssembledGroupInstance {
1205 segments: vec![
1206 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
1207 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
1208 ],
1209 child_groups: vec![],
1210 entry_mig_number: None,
1211 variant_mig_numbers: vec![
1212 "00035".into(),
1213 "00036".into(),
1214 "00037".into(),
1215 "00038".into(),
1216 ],
1217 skipped_segments: vec![],
1218 skipped_positions: Vec::new(),
1219 }],
1220 }],
1221 post_group_start: 0,
1222 inter_group_segments: BTreeMap::new(),
1223 };
1224
1225 let result = build_validated_tree(&workflow, &tree);
1226 assert_eq!(result.groups.len(), 1);
1227 let sg10 = &result.groups[0];
1228 assert_eq!(
1229 sg10.fields.len(),
1230 3,
1231 "all three rules should pass the variant filter"
1232 );
1233
1234 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
1235
1236 assert_eq!(
1238 by_name("Statuskategorie Z33").value,
1239 None,
1240 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
1241 );
1242
1243 assert_eq!(
1246 by_name("Statuskategorie Z32").value,
1247 Some("Z32"),
1248 "rule for Z32 must attach to the STS segment with qualifier Z32"
1249 );
1250
1251 assert_eq!(
1253 by_name("Statuskategorie Z40").value,
1254 Some("Z40"),
1255 "rule for Z40 must attach to the STS segment with qualifier Z40"
1256 );
1257 }
1258}