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 pub variant_rules: Vec<&'a AhbFieldRule>,
45 pub absent_children: Vec<&'a AhbFieldRule>,
49}
50
51#[derive(Debug)]
53pub struct ValidatedTree<'a> {
54 pub pruefidentifikator: &'a str,
56 pub ub_definitions: &'a BTreeMap<String, ConditionExpr>,
58 pub package_conditions: &'a BTreeMap<u32, ConditionExpr>,
60 pub root_fields: Vec<AhbNode<'a>>,
62 pub groups: Vec<AhbGroupNode<'a>>,
64 pub unmatched_rules: Vec<&'a AhbFieldRule>,
71}
72
73pub fn build_validated_tree<'a>(
79 workflow: &'a AhbWorkflow,
80 tree: &'a AssembledTree,
81) -> ValidatedTree<'a> {
82 let mut root_rules: Vec<&'a AhbFieldRule> = Vec::new();
84 let mut group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
86
87 for rule in &workflow.fields {
88 match extract_top_group(&rule.segment_path) {
89 Some(group_id) => {
90 group_rules
91 .entry(group_id.to_owned())
92 .or_default()
93 .push(rule);
94 }
95 None => {
96 root_rules.push(rule);
97 }
98 }
99 }
100
101 let root_fields = resolve_fields(&root_rules, &tree.segments, None);
103
104 let groups = resolve_groups(&tree.groups, &group_rules, 0);
106
107 let mut matched_mig_numbers: std::collections::HashSet<&str> = std::collections::HashSet::new();
110 collect_matched_mig_numbers_from_groups(&groups, &mut matched_mig_numbers);
111 for seg in &tree.segments {
113 if let Some(ref num) = seg.mig_number {
114 matched_mig_numbers.insert(num.as_str());
115 }
116 }
117
118 let unmatched_rules: Vec<&AhbFieldRule> = workflow
119 .fields
120 .iter()
121 .filter(|rule| {
122 rule.mig_number
123 .as_ref()
124 .is_some_and(|num| !matched_mig_numbers.contains(num.as_str()))
125 })
126 .collect();
127
128 ValidatedTree {
129 pruefidentifikator: &workflow.pruefidentifikator,
130 ub_definitions: &workflow.ub_definitions,
131 package_conditions: &workflow.package_conditions,
132 root_fields,
133 groups,
134 unmatched_rules,
135 }
136}
137
138fn collect_matched_mig_numbers_from_groups<'a>(
144 groups: &[AhbGroupNode<'a>],
145 out: &mut std::collections::HashSet<&'a str>,
146) {
147 for group in groups {
148 for node in &group.fields {
149 if let Some(ref num) = node.rule.mig_number {
150 out.insert(num.as_str());
151 }
152 }
153 collect_matched_mig_numbers_from_groups(&group.children, out);
154 }
155}
156
157fn merged_variant<'a>(
163 rules: &[&'a AhbFieldRule],
164 instance: &AssembledGroupInstance,
165) -> Option<(&'a str, std::ops::Range<u32>)> {
166 let entry = instance.segments.first()?;
167 let recorded = instance.entry_mig_number.as_deref()?;
171 if !rules.iter().any(|r| {
172 r.mig_number.as_deref() == Some(recorded)
173 && extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
174 }) {
175 return None;
176 }
177 if instance
181 .segments
182 .iter()
183 .filter(|s| s.tag == entry.tag)
184 .count()
185 != 1
186 {
187 return None;
188 }
189 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
190 let mut positions: Vec<(usize, usize)> = Vec::new();
191 for r in rules {
192 let pos = (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0));
193 if extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
194 && !r.codes.is_empty()
195 && !positions.contains(&pos)
196 {
197 positions.push(pos);
198 }
199 }
200 for (element, component) in positions {
201 let mut entries: Vec<(u32, &'a AhbFieldRule)> = rules
203 .iter()
204 .filter(|r| {
205 extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
206 && r.element_index.unwrap_or(0) == element
207 && r.component_index.unwrap_or(0) == component
208 && !r.codes.is_empty()
209 })
210 .filter_map(|r| Some((number(r)?, *r)))
211 .collect();
212 entries.sort_by_key(|(n, _)| *n);
213 entries.dedup_by_key(|(n, _)| *n);
214 let all_entry_numbers: std::collections::BTreeSet<u32> = rules
218 .iter()
219 .filter(|r| extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str()))
220 .filter_map(|r| number(r))
221 .collect();
222 if entries.len() != all_entry_numbers.len() {
223 continue;
224 }
225 let disjoint = entries.len() >= 2
226 && entries.iter().enumerate().all(|(i, (_, a))| {
227 entries[i + 1..].iter().all(|(_, b)| {
228 !a.codes
229 .iter()
230 .any(|c| b.codes.iter().any(|d| d.value == c.value))
231 })
232 });
233 if !disjoint {
234 continue;
235 }
236 let value = entry.elements.get(element)?.get(component)?;
237 let at = entries
238 .iter()
239 .position(|(_, r)| r.codes.iter().any(|c| &c.value == value))?;
240 let start = entries[at].0;
241 let end = entries.get(at + 1).map_or(u32::MAX, |(n, _)| *n);
242 return Some((entries[at].1.mig_number.as_deref()?, start..end));
243 }
244 None
245}
246
247fn extract_top_group(segment_path: &str) -> Option<&str> {
253 let first = segment_path.split('/').next()?;
254 if first.starts_with("SG") {
255 Some(first)
256 } else {
257 None
258 }
259}
260
261fn extract_child_group(stripped_path: &str) -> Option<&str> {
266 let first = stripped_path.split('/').next()?;
267 if first.starts_with("SG") {
268 Some(first)
269 } else {
270 None
271 }
272}
273
274fn resolve_fields<'a>(
280 rules: &[&'a AhbFieldRule],
281 segments: &'a [AssembledSegment],
282 numbers: Option<&[Option<String>]>,
283) -> Vec<AhbNode<'a>> {
284 rules
285 .iter()
286 .map(|rule| {
287 let matched_segment = find_segment(rule, segments, numbers);
288 let (value, elements, segment_number) = match matched_segment {
289 Some(seg) => {
290 let val = extract_value(seg, rule);
291 (val, Some(seg.elements.as_slice()), seg.segment_number)
292 }
293 None => {
294 let fallback_num = extract_segment_tag(&rule.segment_path)
303 .and_then(|tag| segments.iter().find(|s| s.tag == tag))
304 .and_then(|s| s.segment_number);
305 (None, None, fallback_num)
306 }
307 };
308 AhbNode {
309 rule,
310 value,
311 segment_elements: elements,
312 matched_segment_number: segment_number,
313 }
314 })
315 .collect()
316}
317
318fn find_segment<'a>(
330 rule: &AhbFieldRule,
331 segments: &'a [AssembledSegment],
332 numbers: Option<&[Option<String>]>,
333) -> Option<&'a AssembledSegment> {
334 let number_of = |i: usize, s: &'a AssembledSegment| -> Option<&str> {
336 match numbers {
337 Some(n) => n.get(i).and_then(|n| n.as_deref()),
338 None => s.mig_number.as_deref(),
339 }
340 };
341 let tag = extract_segment_tag(&rule.segment_path)?;
342
343 if !rule.codes.is_empty() {
362 let el_idx = rule.element_index.unwrap_or(0);
363 let comp_idx = rule.component_index.unwrap_or(0);
364 let expected: std::collections::HashSet<&str> =
365 rule.codes.iter().map(|c| c.value.as_str()).collect();
366 if let Some(seg) = segments.iter().find(|s| {
367 s.tag == tag
368 && s.elements
369 .get(el_idx)
370 .and_then(|e| e.get(comp_idx))
371 .is_some_and(|v| expected.contains(v.as_str()))
372 }) {
373 return Some(seg);
374 }
375 let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
376 if same_tag_count > 1 {
377 return None;
378 }
379 let other_slot = segments.iter().enumerate().any(|(i, s)| {
385 i > 0
386 && s.tag == tag
387 && s.elements
388 .get(el_idx)
389 .and_then(|e| e.get(comp_idx))
390 .is_some_and(|v| !v.is_empty() && !expected.contains(v.as_str()))
391 });
392 if other_slot {
393 return None;
394 }
395 }
396
397 if let Some(mig_num) = rule.mig_number.as_ref() {
398 if let Some((_, seg)) = segments
400 .iter()
401 .enumerate()
402 .find(|(i, s)| number_of(*i, s) == Some(mig_num.as_str()))
403 {
404 return Some(seg);
405 }
406
407 return segments
409 .iter()
410 .enumerate()
411 .find(|(i, s)| s.tag == tag && number_of(*i, s).map_or(true, |m| m == mig_num))
412 .map(|(_, s)| s);
413 }
414
415 segments.iter().find(|s| s.tag == tag)
417}
418
419fn extract_segment_tag(segment_path: &str) -> Option<&str> {
424 segment_path.split('/').find(|part| !part.starts_with("SG"))
425}
426
427fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
429 let elem_idx = rule.element_index.unwrap_or(0);
430 let comp_idx = rule.component_index.unwrap_or(0);
431
432 let element = segment.elements.get(elem_idx)?;
433 let component = element.get(comp_idx)?;
434
435 if component.is_empty() {
436 None
437 } else {
438 Some(component.as_str())
439 }
440}
441
442fn resolve_groups<'a>(
447 assembled_groups: &'a [AssembledGroup],
448 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
449 depth: usize,
450) -> Vec<AhbGroupNode<'a>> {
451 let mut result = Vec::new();
452
453 for assembled_group in assembled_groups {
454 let rules = group_rules.get(&assembled_group.group_id);
455
456 for (instance_index, instance) in assembled_group.repetitions.iter().enumerate() {
457 let mut node =
458 resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
459 node.instance_index = instance_index;
460 result.push(node);
461 }
462 }
463
464 result
465}
466
467fn strip_n_groups(path: &str, n: usize) -> &str {
471 let mut rest = path;
472 for _ in 0..n {
473 match rest.find('/') {
474 Some(idx) => rest = &rest[idx + 1..],
475 None => return rest,
476 }
477 }
478 rest
479}
480
481fn resolve_group_instance<'a>(
490 group_id: &'a str,
491 instance: &'a AssembledGroupInstance,
492 rules: Option<&Vec<&'a AhbFieldRule>>,
493 depth: usize,
494) -> AhbGroupNode<'a> {
495 let strip_count = depth + 1;
497
498 let variant_numbers: std::collections::HashSet<&str> =
503 if !instance.variant_mig_numbers.is_empty() {
504 instance
505 .variant_mig_numbers
506 .iter()
507 .map(|s| s.as_str())
508 .collect()
509 } else {
510 collect_instance_mig_numbers(instance)
511 };
512
513 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
514 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
515 let mut ahb_status: Option<&'a str> = None;
516
517 let merged = rules.and_then(|rules| {
522 let own: Vec<&'a AhbFieldRule> = rules
523 .iter()
524 .copied()
525 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none())
526 .collect();
527 merged_variant(&own, instance)
528 });
529
530 let span = variant_numbers
535 .iter()
536 .filter_map(|n| n.parse::<u32>().ok())
537 .fold(None, |acc: Option<(u32, u32)>, n| {
538 Some(acc.map_or((n, n), |(lo, hi)| (lo.min(n), hi.max(n))))
539 });
540
541 if let Some(rules) = rules {
542 for rule in rules {
543 let stripped = strip_n_groups(&rule.segment_path, strip_count);
545 let child = extract_child_group(stripped);
546
547 if let Some(ref rule_mig) = rule.mig_number {
550 let number = rule_mig.parse::<u32>().ok();
551 let belongs = match (&merged, child) {
552 (Some((_, range)), _) => number.is_some_and(|m| range.contains(&m)),
553 (None, Some(_)) => match (number, span) {
554 (Some(m), Some((lo, hi))) => (lo..=hi).contains(&m),
555 _ => variant_numbers.contains(rule_mig.as_str()),
556 },
557 (None, None) => variant_numbers.contains(rule_mig.as_str()),
558 };
559 if !belongs {
560 continue;
561 }
562 }
563
564 if let Some(child_group_id) = child {
565 child_group_rules
566 .entry(child_group_id.to_owned())
567 .or_default()
568 .push(rule);
569 } else {
570 direct_rules.push(rule);
571 }
572 }
573 }
574 let own_status = |rule: &&'a AhbFieldRule| rule.parent_group_ahb_status.as_deref();
579 let entry = merged
580 .as_ref()
581 .map(|(entry, _)| *entry)
582 .or(instance.entry_mig_number.as_deref());
583 if let Some(status) = direct_rules
584 .iter()
585 .filter(|r| entry.is_some() && r.mig_number.as_deref() == entry)
586 .find_map(own_status)
587 .or_else(|| {
588 direct_rules
589 .iter()
590 .filter(|r| r.mig_number.is_some())
591 .find_map(own_status)
592 })
593 {
594 ahb_status = Some(status);
595 }
596
597 let numbers: Option<Vec<Option<String>>> = merged.as_ref().map(|_| {
602 let mut used: Vec<&str> = Vec::new();
603 instance
604 .segments
605 .iter()
606 .map(|seg| {
607 let of_tag = |r: &&&'a AhbFieldRule| {
608 extract_segment_tag(&r.segment_path) == Some(seg.tag.as_str())
609 };
610 let mut slots: Vec<&str> = direct_rules
611 .iter()
612 .filter(of_tag)
613 .filter_map(|r| r.mig_number.as_deref())
614 .filter(|n| !used.contains(n))
615 .collect();
616 slots.sort_by_key(|n| n.parse::<u32>().unwrap_or(u32::MAX));
617 slots.dedup();
618 let coded = |slot: &str| {
621 direct_rules
622 .iter()
623 .filter(of_tag)
624 .filter(|r| r.mig_number.as_deref() == Some(slot) && !r.codes.is_empty())
625 .collect::<Vec<_>>()
626 };
627 let carries = |slot: &&str| {
628 coded(slot).iter().any(|r| {
629 seg.elements
630 .get(r.element_index.unwrap_or(0))
631 .and_then(|e| e.get(r.component_index.unwrap_or(0)))
632 .is_some_and(|v| r.codes.iter().any(|c| &c.value == v))
633 })
634 };
635 let slot = slots
636 .iter()
637 .find(|s| carries(s))
638 .or_else(|| slots.iter().find(|s| coded(s).is_empty()))
639 .or_else(|| slots.first())
640 .copied();
641 if let Some(slot) = slot {
642 used.push(slot);
643 }
644 slot.map(str::to_string)
645 })
646 .collect()
647 });
648 let fields = resolve_fields(&direct_rules, &instance.segments, numbers.as_deref());
649
650 let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
652
653 let mut absent_children = Vec::new();
654 for (child_id, rules) in &child_group_rules {
655 absent_children.extend(absent_variant_rules(
656 child_id,
657 rules,
658 &children,
659 strip_count + 1,
660 ));
661 }
662 absent_children.sort_by_key(|r| r.mig_number.as_deref().and_then(|n| n.parse::<u32>().ok()));
663 let mut variant_rules = direct_rules;
664 variant_rules.extend(child_group_rules.into_values().flatten());
665
666 AhbGroupNode {
667 group_id,
668 instance_index: 0,
669 ahb_status,
670 fields,
671 children,
672 variant_rules,
673 absent_children,
674 }
675}
676
677type Profile<'a> = std::collections::BTreeMap<(usize, usize), Vec<&'a str>>;
679
680fn absent_variant_rules<'a>(
693 child_id: &str,
694 rules: &[&'a AhbFieldRule],
695 children: &[AhbGroupNode<'a>],
696 strip: usize,
697) -> Vec<&'a AhbFieldRule> {
698 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
699 let own: Vec<&'a AhbFieldRule> = rules
700 .iter()
701 .copied()
702 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip)).is_none())
703 .filter(|r| number(r).is_some())
704 .collect();
705 let Some(entry_tag) = own
706 .iter()
707 .min_by_key(|r| number(r))
708 .and_then(|r| extract_segment_tag(&r.segment_path))
709 else {
710 return Vec::new();
711 };
712 let is_entry_tag = |r: &AhbFieldRule| extract_segment_tag(&r.segment_path) == Some(entry_tag);
713 let starts: std::collections::BTreeSet<u32> = own
714 .iter()
715 .filter(|r| is_entry_tag(r))
716 .filter_map(|r| number(r))
717 .collect();
718 let mut entries: Vec<(u32, &'a AhbFieldRule, Profile<'a>)> = Vec::new();
721 for &start in &starts {
722 let mut profile = Profile::new();
723 let mut entry = None;
724 for r in own
725 .iter()
726 .copied()
727 .filter(|r| is_entry_tag(r) && number(r) == Some(start))
728 {
729 entry.get_or_insert(r);
730 profile
731 .entry((r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))
732 .or_default()
733 .extend(r.codes.iter().map(|c| c.value.as_str()));
734 }
735 if let Some(entry) = entry {
736 entries.push((start, entry, profile));
737 }
738 }
739 let coded = |p: &Profile| p.values().any(|codes| !codes.is_empty());
740 let apart = |a: &Profile, b: &Profile| {
742 a.iter().any(|(pos, codes)| {
743 !codes.is_empty()
744 && b.get(pos).map_or(true, |other| {
745 !other.is_empty() && !codes.iter().any(|c| other.contains(c))
746 })
747 })
748 };
749 let distinct = entries.iter().all(|(_, _, p)| coded(p))
750 && entries.iter().enumerate().all(|(i, (_, _, a))| {
751 entries[i + 1..]
752 .iter()
753 .all(|(_, _, b)| apart(a, b) || apart(b, a))
754 });
755 if !distinct {
756 return Vec::new();
757 }
758 let claimed: std::collections::HashSet<&str> = children
759 .iter()
760 .filter(|c| c.group_id == child_id)
761 .flat_map(|c| c.fields.iter().filter_map(|n| n.rule.mig_number.as_deref()))
762 .collect();
763 let mut out = Vec::new();
764 for (i, (start, entry, _)) in entries.iter().enumerate() {
765 if claimed.contains(entry.mig_number.as_deref().unwrap_or_default()) {
766 continue;
767 }
768 let end = entries.get(i + 1).map_or(u32::MAX, |(n, _, _)| *n);
769 out.extend(
770 own.iter()
771 .copied()
772 .filter(|r| number(r).is_some_and(|n| (*start..end).contains(&n))),
773 );
774 }
775 out
776}
777
778fn collect_instance_mig_numbers(
780 instance: &AssembledGroupInstance,
781) -> std::collections::HashSet<&str> {
782 let mut numbers = std::collections::HashSet::new();
783 for seg in &instance.segments {
784 if let Some(ref num) = seg.mig_number {
785 numbers.insert(num.as_str());
786 }
787 }
788 for child_group in &instance.child_groups {
789 for child_instance in &child_group.repetitions {
790 numbers.extend(collect_instance_mig_numbers(child_instance));
791 }
792 }
793 numbers
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
800 use mig_assembly::assembler::{
801 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
802 };
803 use std::collections::BTreeMap;
804
805 fn empty_workflow() -> AhbWorkflow {
806 AhbWorkflow {
807 pruefidentifikator: "11001".to_string(),
808 description: String::new(),
809 communication_direction: None,
810 fields: vec![],
811 ub_definitions: BTreeMap::new(),
812 package_conditions: Default::default(),
813 }
814 }
815
816 fn empty_tree() -> AssembledTree {
817 AssembledTree {
818 segments: vec![],
819 groups: vec![],
820 post_group_start: 0,
821 inter_group_segments: BTreeMap::new(),
822 }
823 }
824
825 fn make_segment(
826 tag: &str,
827 elements: Vec<Vec<&str>>,
828 mig_number: Option<&str>,
829 ) -> AssembledSegment {
830 AssembledSegment {
831 tag: tag.to_string(),
832 elements: elements
833 .into_iter()
834 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
835 .collect(),
836 mig_number: mig_number.map(|s| s.to_string()),
837 segment_number: None,
838 }
839 }
840
841 fn make_rule(
842 segment_path: &str,
843 name: &str,
844 ahb_status: &str,
845 mig_number: Option<&str>,
846 element_index: Option<usize>,
847 component_index: Option<usize>,
848 ) -> AhbFieldRule {
849 AhbFieldRule {
850 segment_path: segment_path.to_string(),
851 name: name.to_string(),
852 ahb_status: ahb_status.to_string(),
853 codes: vec![],
854 parent_group_ahb_status: None,
855 segment_ahb_status: None,
856 element_index,
857 component_index,
858 mig_number: mig_number.map(|s| s.to_string()),
859 format: None,
860 }
861 }
862
863 #[test]
864 fn test_empty_workflow_empty_tree() {
865 let workflow = empty_workflow();
866 let tree = empty_tree();
867 let result = build_validated_tree(&workflow, &tree);
868
869 assert_eq!(result.pruefidentifikator, "11001");
870 assert!(result.root_fields.is_empty());
871 assert!(result.groups.is_empty());
872 }
873
874 #[test]
875 fn test_root_field_matches_root_segment() {
876 let mut workflow = empty_workflow();
877 workflow.fields.push(make_rule(
878 "BGM/C002/1001",
879 "Nachrichtentyp",
880 "X",
881 Some("0001"),
882 Some(0),
883 Some(0),
884 ));
885
886 let tree = AssembledTree {
887 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
888 groups: vec![],
889 post_group_start: 1,
890 inter_group_segments: BTreeMap::new(),
891 };
892
893 let result = build_validated_tree(&workflow, &tree);
894
895 assert_eq!(result.root_fields.len(), 1);
896 let node = &result.root_fields[0];
897 assert_eq!(node.value, Some("E01"));
898 assert!(node.segment_elements.is_some());
899 assert_eq!(node.rule.name, "Nachrichtentyp");
900 }
901
902 #[test]
903 fn test_sg4_dtm_mig_number_matching() {
904 let mut workflow = empty_workflow();
907
908 workflow.fields.push(make_rule(
910 "SG4/DTM/C507/2380",
911 "Eingangsdatum",
912 "X",
913 Some("0082"),
914 Some(1), Some(0),
916 ));
917
918 workflow.fields.push(make_rule(
920 "SG4/DTM/C507/2380",
921 "Dokumentendatum",
922 "X",
923 Some("0083"),
924 Some(1),
925 Some(0),
926 ));
927
928 let tree = AssembledTree {
929 segments: vec![],
930 groups: vec![AssembledGroup {
931 group_id: "SG4".to_string(),
932 repetitions: vec![AssembledGroupInstance {
933 segments: vec![
934 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
935 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
936 ],
937 child_groups: vec![],
938 entry_mig_number: None,
939 variant_mig_numbers: vec![],
940 skipped_segments: vec![],
941 skipped_positions: Vec::new(),
942 }],
943 }],
944 post_group_start: 0,
945 inter_group_segments: BTreeMap::new(),
946 };
947
948 let result = build_validated_tree(&workflow, &tree);
949
950 assert_eq!(result.groups.len(), 1);
951 let sg4 = &result.groups[0];
952 assert_eq!(sg4.group_id, "SG4");
953 assert_eq!(sg4.fields.len(), 2);
954
955 let eingangsdatum = &sg4.fields[0];
957 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
958 assert_eq!(eingangsdatum.value, Some("20260101"));
959
960 let dokumentendatum = &sg4.fields[1];
962 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
963 assert_eq!(dokumentendatum.value, Some("20260401"));
964 }
965
966 #[test]
967 fn test_rule_filtered_to_correct_variant() {
968 let mut workflow = empty_workflow();
972 workflow.fields.push(make_rule(
973 "SG4/RFF/C506/1154",
974 "Referenz",
975 "X",
976 Some("0099"),
977 Some(0),
978 Some(1),
979 ));
980
981 let tree = AssembledTree {
982 segments: vec![],
983 groups: vec![AssembledGroup {
984 group_id: "SG4".to_string(),
985 repetitions: vec![AssembledGroupInstance {
986 segments: vec![], child_groups: vec![],
988 entry_mig_number: None,
989 variant_mig_numbers: vec![],
990 skipped_segments: vec![],
991 skipped_positions: Vec::new(),
992 }],
993 }],
994 post_group_start: 0,
995 inter_group_segments: BTreeMap::new(),
996 };
997
998 let result = build_validated_tree(&workflow, &tree);
999 assert_eq!(result.groups.len(), 1);
1000 assert_eq!(result.groups[0].fields.len(), 0);
1002 }
1003
1004 #[test]
1005 fn test_missing_segment_within_correct_variant() {
1006 let mut workflow = empty_workflow();
1010 workflow.fields.push(make_rule(
1012 "SG4/SEQ/1229",
1013 "Qualifier",
1014 "X",
1015 Some("0098"),
1016 Some(0),
1017 Some(0),
1018 ));
1019 workflow.fields.push(make_rule(
1021 "SG4/RFF/C506/1154",
1022 "Referenz",
1023 "X",
1024 Some("0099"),
1025 Some(0),
1026 Some(1),
1027 ));
1028
1029 let tree = AssembledTree {
1030 segments: vec![],
1031 groups: vec![AssembledGroup {
1032 group_id: "SG4".to_string(),
1033 repetitions: vec![AssembledGroupInstance {
1034 segments: vec![
1035 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
1037 ],
1038 child_groups: vec![],
1039 entry_mig_number: Some("0098".to_string()),
1040 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
1042 skipped_segments: vec![],
1043 skipped_positions: Vec::new(),
1044 }],
1045 }],
1046 post_group_start: 0,
1047 inter_group_segments: BTreeMap::new(),
1048 };
1049
1050 let result = build_validated_tree(&workflow, &tree);
1051 assert_eq!(result.groups.len(), 1);
1052 assert_eq!(result.groups[0].fields.len(), 2);
1054 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
1055 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
1056 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
1058 assert_eq!(result.groups[0].fields[1].value, None);
1059 }
1060
1061 #[test]
1062 fn test_missing_group_variant_populates_unmatched_rules() {
1063 let mut workflow = empty_workflow();
1067
1068 workflow.fields.push(make_rule(
1070 "SG2/NAD/3035",
1071 "MP-ID Absender Qualifier",
1072 "X",
1073 Some("0010"),
1074 Some(0),
1075 Some(0),
1076 ));
1077 workflow.fields.push(make_rule(
1079 "SG2/NAD/C082/3039",
1080 "MP-ID Absender",
1081 "X",
1082 Some("0010"),
1083 Some(1),
1084 Some(0),
1085 ));
1086 workflow.fields.push(make_rule(
1088 "SG2/NAD/3035",
1089 "MP-ID Empfänger Qualifier",
1090 "X",
1091 Some("0011"),
1092 Some(0),
1093 Some(0),
1094 ));
1095 workflow.fields.push(make_rule(
1097 "SG2/NAD/C082/3039",
1098 "MP-ID Empfänger",
1099 "X",
1100 Some("0011"),
1101 Some(1),
1102 Some(0),
1103 ));
1104
1105 let tree = AssembledTree {
1107 segments: vec![],
1108 groups: vec![AssembledGroup {
1109 group_id: "SG2".to_string(),
1110 repetitions: vec![AssembledGroupInstance {
1111 segments: vec![make_segment(
1112 "NAD",
1113 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
1114 Some("0011"),
1115 )],
1116 child_groups: vec![],
1117 entry_mig_number: Some("0011".to_string()),
1118 variant_mig_numbers: vec!["0011".to_string()],
1119 skipped_segments: vec![],
1120 skipped_positions: Vec::new(),
1121 }],
1122 }],
1123 post_group_start: 0,
1124 inter_group_segments: BTreeMap::new(),
1125 };
1126
1127 let result = build_validated_tree(&workflow, &tree);
1128
1129 assert_eq!(result.groups.len(), 1);
1131 assert_eq!(result.groups[0].fields.len(), 2);
1132 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
1133
1134 assert_eq!(
1136 result.unmatched_rules.len(),
1137 2,
1138 "Expected 2 unmatched rules (NAD+MS), got {}",
1139 result.unmatched_rules.len()
1140 );
1141 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1142 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
1143 }
1144
1145 #[test]
1146 fn test_entirely_absent_group_populates_unmatched_rules() {
1147 let mut workflow = empty_workflow();
1149 workflow.fields.push(make_rule(
1150 "SG2/NAD/3035",
1151 "MP-ID Absender Qualifier",
1152 "X",
1153 Some("0010"),
1154 Some(0),
1155 Some(0),
1156 ));
1157
1158 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
1161
1162 assert!(result.groups.is_empty());
1164
1165 assert_eq!(
1167 result.unmatched_rules.len(),
1168 1,
1169 "Expected 1 unmatched rule, got {}",
1170 result.unmatched_rules.len()
1171 );
1172 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1173 }
1174
1175 #[test]
1176 fn test_fallback_to_tag_when_no_mig_number() {
1177 let mut workflow = empty_workflow();
1178 workflow.fields.push(make_rule(
1179 "BGM/C002/1001",
1180 "Nachrichtentyp",
1181 "X",
1182 None, Some(0),
1184 Some(0),
1185 ));
1186
1187 let tree = AssembledTree {
1188 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
1189 groups: vec![],
1190 post_group_start: 1,
1191 inter_group_segments: BTreeMap::new(),
1192 };
1193
1194 let result = build_validated_tree(&workflow, &tree);
1195 assert_eq!(result.root_fields.len(), 1);
1196 assert_eq!(result.root_fields[0].value, Some("E01"));
1197 }
1198
1199 #[test]
1200 fn test_nested_child_groups() {
1201 let mut workflow = empty_workflow();
1202 workflow.fields.push(make_rule(
1204 "SG4/SG5/LOC/C517/3225",
1205 "Marktlokations-ID",
1206 "X",
1207 Some("0050"),
1208 Some(0),
1209 Some(0),
1210 ));
1211
1212 let tree = AssembledTree {
1213 segments: vec![],
1214 groups: vec![AssembledGroup {
1215 group_id: "SG4".to_string(),
1216 repetitions: vec![AssembledGroupInstance {
1217 segments: vec![],
1218 entry_mig_number: None,
1219 child_groups: vec![AssembledGroup {
1220 group_id: "SG5".to_string(),
1221 repetitions: vec![AssembledGroupInstance {
1222 segments: vec![make_segment(
1223 "LOC",
1224 vec![vec!["DE00012345678"]],
1225 Some("0050"),
1226 )],
1227 child_groups: vec![],
1228 entry_mig_number: None,
1229 variant_mig_numbers: vec![],
1230 skipped_segments: vec![],
1231 skipped_positions: Vec::new(),
1232 }],
1233 }],
1234 variant_mig_numbers: vec![],
1235 skipped_segments: vec![],
1236 skipped_positions: Vec::new(),
1237 }],
1238 }],
1239 post_group_start: 0,
1240 inter_group_segments: BTreeMap::new(),
1241 };
1242
1243 let result = build_validated_tree(&workflow, &tree);
1244 assert_eq!(result.groups.len(), 1);
1245 let sg4 = &result.groups[0];
1246 assert_eq!(sg4.children.len(), 1);
1247
1248 let sg5 = &sg4.children[0];
1249 assert_eq!(sg5.group_id, "SG5");
1250 assert_eq!(sg5.fields.len(), 1);
1251 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
1252 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
1253 }
1254
1255 #[test]
1256 fn test_qualifier_aware_segment_matching_same_tag_variants() {
1257 let mut workflow = empty_workflow();
1271
1272 let mut z33 = make_rule(
1274 "SG10/STS/C601/9015",
1275 "Statuskategorie Z33",
1276 "X",
1277 Some("00035"),
1278 Some(0),
1279 Some(0),
1280 );
1281 z33.codes = vec![super::super::validate::AhbCodeRule {
1282 value: "Z33".into(),
1283 description: String::new(),
1284 ahb_status: "X".into(),
1285 }];
1286 workflow.fields.push(z33);
1287
1288 let mut z32 = make_rule(
1290 "SG10/STS/C601/9015",
1291 "Statuskategorie Z32",
1292 "X",
1293 Some("00036"),
1294 Some(0),
1295 Some(0),
1296 );
1297 z32.codes = vec![super::super::validate::AhbCodeRule {
1298 value: "Z32".into(),
1299 description: String::new(),
1300 ahb_status: "X".into(),
1301 }];
1302 workflow.fields.push(z32);
1303
1304 let mut z40 = make_rule(
1306 "SG10/STS/C601/9015",
1307 "Statuskategorie Z40",
1308 "X",
1309 Some("00038"),
1310 Some(0),
1311 Some(0),
1312 );
1313 z40.codes = vec![super::super::validate::AhbCodeRule {
1314 value: "Z40".into(),
1315 description: String::new(),
1316 ahb_status: "X".into(),
1317 }];
1318 workflow.fields.push(z40);
1319
1320 let tree = AssembledTree {
1323 segments: vec![],
1324 groups: vec![AssembledGroup {
1325 group_id: "SG10".to_string(),
1326 repetitions: vec![AssembledGroupInstance {
1327 segments: vec![
1328 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
1329 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
1330 ],
1331 child_groups: vec![],
1332 entry_mig_number: None,
1333 variant_mig_numbers: vec![
1334 "00035".into(),
1335 "00036".into(),
1336 "00037".into(),
1337 "00038".into(),
1338 ],
1339 skipped_segments: vec![],
1340 skipped_positions: Vec::new(),
1341 }],
1342 }],
1343 post_group_start: 0,
1344 inter_group_segments: BTreeMap::new(),
1345 };
1346
1347 let result = build_validated_tree(&workflow, &tree);
1348 assert_eq!(result.groups.len(), 1);
1349 let sg10 = &result.groups[0];
1350 assert_eq!(
1351 sg10.fields.len(),
1352 3,
1353 "all three rules should pass the variant filter"
1354 );
1355
1356 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
1357
1358 assert_eq!(
1360 by_name("Statuskategorie Z33").value,
1361 None,
1362 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
1363 );
1364
1365 assert_eq!(
1368 by_name("Statuskategorie Z32").value,
1369 Some("Z32"),
1370 "rule for Z32 must attach to the STS segment with qualifier Z32"
1371 );
1372
1373 assert_eq!(
1375 by_name("Statuskategorie Z40").value,
1376 Some("Z40"),
1377 "rule for Z40 must attach to the STS segment with qualifier Z40"
1378 );
1379 }
1380}