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);
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 extract_top_group(segment_path: &str) -> Option<&str> {
154 let first = segment_path.split('/').next()?;
155 if first.starts_with("SG") {
156 Some(first)
157 } else {
158 None
159 }
160}
161
162fn extract_child_group(stripped_path: &str) -> Option<&str> {
167 let first = stripped_path.split('/').next()?;
168 if first.starts_with("SG") {
169 Some(first)
170 } else {
171 None
172 }
173}
174
175fn resolve_fields<'a>(
181 rules: &[&'a AhbFieldRule],
182 segments: &'a [AssembledSegment],
183) -> Vec<AhbNode<'a>> {
184 rules
185 .iter()
186 .map(|rule| {
187 let matched_segment = find_segment(rule, segments);
188 let (value, elements, segment_number) = match matched_segment {
189 Some(seg) => {
190 let val = extract_value(seg, rule);
191 (val, Some(seg.elements.as_slice()), seg.segment_number)
192 }
193 None => {
194 let fallback_num = extract_segment_tag(&rule.segment_path)
203 .and_then(|tag| segments.iter().find(|s| s.tag == tag))
204 .and_then(|s| s.segment_number);
205 (None, None, fallback_num)
206 }
207 };
208 AhbNode {
209 rule,
210 value,
211 segment_elements: elements,
212 matched_segment_number: segment_number,
213 }
214 })
215 .collect()
216}
217
218fn find_segment<'a>(
230 rule: &AhbFieldRule,
231 segments: &'a [AssembledSegment],
232) -> Option<&'a AssembledSegment> {
233 let tag = extract_segment_tag(&rule.segment_path)?;
234
235 if !rule.codes.is_empty() {
254 let el_idx = rule.element_index.unwrap_or(0);
255 let comp_idx = rule.component_index.unwrap_or(0);
256 let expected: std::collections::HashSet<&str> =
257 rule.codes.iter().map(|c| c.value.as_str()).collect();
258 if let Some(seg) = segments.iter().find(|s| {
259 s.tag == tag
260 && s.elements
261 .get(el_idx)
262 .and_then(|e| e.get(comp_idx))
263 .is_some_and(|v| expected.contains(v.as_str()))
264 }) {
265 return Some(seg);
266 }
267 let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
268 if same_tag_count > 1 {
269 return None;
270 }
271 }
272
273 if let Some(ref mig_num) = rule.mig_number {
274 if let Some(seg) = segments
276 .iter()
277 .find(|s| s.mig_number.as_deref() == Some(mig_num.as_str()))
278 {
279 return Some(seg);
280 }
281
282 return segments
284 .iter()
285 .find(|s| s.tag == tag && s.mig_number.as_ref().map_or(true, |m| m == mig_num));
286 }
287
288 segments.iter().find(|s| s.tag == tag)
290}
291
292fn extract_segment_tag(segment_path: &str) -> Option<&str> {
297 segment_path.split('/').find(|part| !part.starts_with("SG"))
298}
299
300fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
302 let elem_idx = rule.element_index.unwrap_or(0);
303 let comp_idx = rule.component_index.unwrap_or(0);
304
305 let element = segment.elements.get(elem_idx)?;
306 let component = element.get(comp_idx)?;
307
308 if component.is_empty() {
309 None
310 } else {
311 Some(component.as_str())
312 }
313}
314
315fn resolve_groups<'a>(
320 assembled_groups: &'a [AssembledGroup],
321 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
322 depth: usize,
323) -> Vec<AhbGroupNode<'a>> {
324 let mut result = Vec::new();
325
326 for assembled_group in assembled_groups {
327 let rules = group_rules.get(&assembled_group.group_id);
328
329 for (instance_index, instance) in assembled_group.repetitions.iter().enumerate() {
330 let mut node =
331 resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
332 node.instance_index = instance_index;
333 result.push(node);
334 }
335 }
336
337 result
338}
339
340fn strip_n_groups(path: &str, n: usize) -> &str {
344 let mut rest = path;
345 for _ in 0..n {
346 match rest.find('/') {
347 Some(idx) => rest = &rest[idx + 1..],
348 None => return rest,
349 }
350 }
351 rest
352}
353
354fn resolve_group_instance<'a>(
363 group_id: &'a str,
364 instance: &'a AssembledGroupInstance,
365 rules: Option<&Vec<&'a AhbFieldRule>>,
366 depth: usize,
367) -> AhbGroupNode<'a> {
368 let strip_count = depth + 1;
370
371 let variant_numbers: std::collections::HashSet<&str> =
376 if !instance.variant_mig_numbers.is_empty() {
377 instance
378 .variant_mig_numbers
379 .iter()
380 .map(|s| s.as_str())
381 .collect()
382 } else {
383 collect_instance_mig_numbers(instance)
384 };
385
386 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
387 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
388 let mut ahb_status: Option<&'a str> = None;
389
390 if let Some(rules) = rules {
391 for rule in rules {
392 if let Some(ref rule_mig) = rule.mig_number {
395 if !variant_numbers.contains(rule_mig.as_str()) {
396 continue;
397 }
398 }
399
400 let stripped = strip_n_groups(&rule.segment_path, strip_count);
402
403 if let Some(child_group_id) = extract_child_group(stripped) {
404 child_group_rules
405 .entry(child_group_id.to_owned())
406 .or_default()
407 .push(rule);
408 } else {
409 direct_rules.push(rule);
410 }
411 }
412 }
413 let own_status = |rule: &&'a AhbFieldRule| rule.parent_group_ahb_status.as_deref();
418 let entry = instance.entry_mig_number.as_deref();
419 if let Some(status) = direct_rules
420 .iter()
421 .filter(|r| entry.is_some() && r.mig_number.as_deref() == entry)
422 .find_map(own_status)
423 .or_else(|| {
424 direct_rules
425 .iter()
426 .filter(|r| r.mig_number.is_some())
427 .find_map(own_status)
428 })
429 {
430 ahb_status = Some(status);
431 }
432
433 let fields = resolve_fields(&direct_rules, &instance.segments);
435
436 let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
438
439 AhbGroupNode {
440 group_id,
441 instance_index: 0,
442 ahb_status,
443 fields,
444 children,
445 }
446}
447
448fn collect_instance_mig_numbers(
450 instance: &AssembledGroupInstance,
451) -> std::collections::HashSet<&str> {
452 let mut numbers = std::collections::HashSet::new();
453 for seg in &instance.segments {
454 if let Some(ref num) = seg.mig_number {
455 numbers.insert(num.as_str());
456 }
457 }
458 for child_group in &instance.child_groups {
459 for child_instance in &child_group.repetitions {
460 numbers.extend(collect_instance_mig_numbers(child_instance));
461 }
462 }
463 numbers
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
470 use mig_assembly::assembler::{
471 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
472 };
473 use std::collections::BTreeMap;
474
475 fn empty_workflow() -> AhbWorkflow {
476 AhbWorkflow {
477 pruefidentifikator: "11001".to_string(),
478 description: String::new(),
479 communication_direction: None,
480 fields: vec![],
481 ub_definitions: BTreeMap::new(),
482 }
483 }
484
485 fn empty_tree() -> AssembledTree {
486 AssembledTree {
487 segments: vec![],
488 groups: vec![],
489 post_group_start: 0,
490 inter_group_segments: BTreeMap::new(),
491 }
492 }
493
494 fn make_segment(
495 tag: &str,
496 elements: Vec<Vec<&str>>,
497 mig_number: Option<&str>,
498 ) -> AssembledSegment {
499 AssembledSegment {
500 tag: tag.to_string(),
501 elements: elements
502 .into_iter()
503 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
504 .collect(),
505 mig_number: mig_number.map(|s| s.to_string()),
506 segment_number: None,
507 }
508 }
509
510 fn make_rule(
511 segment_path: &str,
512 name: &str,
513 ahb_status: &str,
514 mig_number: Option<&str>,
515 element_index: Option<usize>,
516 component_index: Option<usize>,
517 ) -> AhbFieldRule {
518 AhbFieldRule {
519 segment_path: segment_path.to_string(),
520 name: name.to_string(),
521 ahb_status: ahb_status.to_string(),
522 codes: vec![],
523 parent_group_ahb_status: None,
524 segment_ahb_status: None,
525 element_index,
526 component_index,
527 mig_number: mig_number.map(|s| s.to_string()),
528 }
529 }
530
531 #[test]
532 fn test_empty_workflow_empty_tree() {
533 let workflow = empty_workflow();
534 let tree = empty_tree();
535 let result = build_validated_tree(&workflow, &tree);
536
537 assert_eq!(result.pruefidentifikator, "11001");
538 assert!(result.root_fields.is_empty());
539 assert!(result.groups.is_empty());
540 }
541
542 #[test]
543 fn test_root_field_matches_root_segment() {
544 let mut workflow = empty_workflow();
545 workflow.fields.push(make_rule(
546 "BGM/C002/1001",
547 "Nachrichtentyp",
548 "X",
549 Some("0001"),
550 Some(0),
551 Some(0),
552 ));
553
554 let tree = AssembledTree {
555 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
556 groups: vec![],
557 post_group_start: 1,
558 inter_group_segments: BTreeMap::new(),
559 };
560
561 let result = build_validated_tree(&workflow, &tree);
562
563 assert_eq!(result.root_fields.len(), 1);
564 let node = &result.root_fields[0];
565 assert_eq!(node.value, Some("E01"));
566 assert!(node.segment_elements.is_some());
567 assert_eq!(node.rule.name, "Nachrichtentyp");
568 }
569
570 #[test]
571 fn test_sg4_dtm_mig_number_matching() {
572 let mut workflow = empty_workflow();
575
576 workflow.fields.push(make_rule(
578 "SG4/DTM/C507/2380",
579 "Eingangsdatum",
580 "X",
581 Some("0082"),
582 Some(1), Some(0),
584 ));
585
586 workflow.fields.push(make_rule(
588 "SG4/DTM/C507/2380",
589 "Dokumentendatum",
590 "X",
591 Some("0083"),
592 Some(1),
593 Some(0),
594 ));
595
596 let tree = AssembledTree {
597 segments: vec![],
598 groups: vec![AssembledGroup {
599 group_id: "SG4".to_string(),
600 repetitions: vec![AssembledGroupInstance {
601 segments: vec![
602 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
603 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
604 ],
605 child_groups: vec![],
606 entry_mig_number: None,
607 variant_mig_numbers: vec![],
608 skipped_segments: vec![],
609 skipped_positions: Vec::new(),
610 }],
611 }],
612 post_group_start: 0,
613 inter_group_segments: BTreeMap::new(),
614 };
615
616 let result = build_validated_tree(&workflow, &tree);
617
618 assert_eq!(result.groups.len(), 1);
619 let sg4 = &result.groups[0];
620 assert_eq!(sg4.group_id, "SG4");
621 assert_eq!(sg4.fields.len(), 2);
622
623 let eingangsdatum = &sg4.fields[0];
625 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
626 assert_eq!(eingangsdatum.value, Some("20260101"));
627
628 let dokumentendatum = &sg4.fields[1];
630 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
631 assert_eq!(dokumentendatum.value, Some("20260401"));
632 }
633
634 #[test]
635 fn test_rule_filtered_to_correct_variant() {
636 let mut workflow = empty_workflow();
640 workflow.fields.push(make_rule(
641 "SG4/RFF/C506/1154",
642 "Referenz",
643 "X",
644 Some("0099"),
645 Some(0),
646 Some(1),
647 ));
648
649 let tree = AssembledTree {
650 segments: vec![],
651 groups: vec![AssembledGroup {
652 group_id: "SG4".to_string(),
653 repetitions: vec![AssembledGroupInstance {
654 segments: vec![], child_groups: vec![],
656 entry_mig_number: None,
657 variant_mig_numbers: vec![],
658 skipped_segments: vec![],
659 skipped_positions: Vec::new(),
660 }],
661 }],
662 post_group_start: 0,
663 inter_group_segments: BTreeMap::new(),
664 };
665
666 let result = build_validated_tree(&workflow, &tree);
667 assert_eq!(result.groups.len(), 1);
668 assert_eq!(result.groups[0].fields.len(), 0);
670 }
671
672 #[test]
673 fn test_missing_segment_within_correct_variant() {
674 let mut workflow = empty_workflow();
678 workflow.fields.push(make_rule(
680 "SG4/SEQ/1229",
681 "Qualifier",
682 "X",
683 Some("0098"),
684 Some(0),
685 Some(0),
686 ));
687 workflow.fields.push(make_rule(
689 "SG4/RFF/C506/1154",
690 "Referenz",
691 "X",
692 Some("0099"),
693 Some(0),
694 Some(1),
695 ));
696
697 let tree = AssembledTree {
698 segments: vec![],
699 groups: vec![AssembledGroup {
700 group_id: "SG4".to_string(),
701 repetitions: vec![AssembledGroupInstance {
702 segments: vec![
703 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
705 ],
706 child_groups: vec![],
707 entry_mig_number: Some("0098".to_string()),
708 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
710 skipped_segments: vec![],
711 skipped_positions: Vec::new(),
712 }],
713 }],
714 post_group_start: 0,
715 inter_group_segments: BTreeMap::new(),
716 };
717
718 let result = build_validated_tree(&workflow, &tree);
719 assert_eq!(result.groups.len(), 1);
720 assert_eq!(result.groups[0].fields.len(), 2);
722 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
723 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
724 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
726 assert_eq!(result.groups[0].fields[1].value, None);
727 }
728
729 #[test]
730 fn test_missing_group_variant_populates_unmatched_rules() {
731 let mut workflow = empty_workflow();
735
736 workflow.fields.push(make_rule(
738 "SG2/NAD/3035",
739 "MP-ID Absender Qualifier",
740 "X",
741 Some("0010"),
742 Some(0),
743 Some(0),
744 ));
745 workflow.fields.push(make_rule(
747 "SG2/NAD/C082/3039",
748 "MP-ID Absender",
749 "X",
750 Some("0010"),
751 Some(1),
752 Some(0),
753 ));
754 workflow.fields.push(make_rule(
756 "SG2/NAD/3035",
757 "MP-ID Empfänger Qualifier",
758 "X",
759 Some("0011"),
760 Some(0),
761 Some(0),
762 ));
763 workflow.fields.push(make_rule(
765 "SG2/NAD/C082/3039",
766 "MP-ID Empfänger",
767 "X",
768 Some("0011"),
769 Some(1),
770 Some(0),
771 ));
772
773 let tree = AssembledTree {
775 segments: vec![],
776 groups: vec![AssembledGroup {
777 group_id: "SG2".to_string(),
778 repetitions: vec![AssembledGroupInstance {
779 segments: vec![make_segment(
780 "NAD",
781 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
782 Some("0011"),
783 )],
784 child_groups: vec![],
785 entry_mig_number: Some("0011".to_string()),
786 variant_mig_numbers: vec!["0011".to_string()],
787 skipped_segments: vec![],
788 skipped_positions: Vec::new(),
789 }],
790 }],
791 post_group_start: 0,
792 inter_group_segments: BTreeMap::new(),
793 };
794
795 let result = build_validated_tree(&workflow, &tree);
796
797 assert_eq!(result.groups.len(), 1);
799 assert_eq!(result.groups[0].fields.len(), 2);
800 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
801
802 assert_eq!(
804 result.unmatched_rules.len(),
805 2,
806 "Expected 2 unmatched rules (NAD+MS), got {}",
807 result.unmatched_rules.len()
808 );
809 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
810 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
811 }
812
813 #[test]
814 fn test_entirely_absent_group_populates_unmatched_rules() {
815 let mut workflow = empty_workflow();
817 workflow.fields.push(make_rule(
818 "SG2/NAD/3035",
819 "MP-ID Absender Qualifier",
820 "X",
821 Some("0010"),
822 Some(0),
823 Some(0),
824 ));
825
826 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
829
830 assert!(result.groups.is_empty());
832
833 assert_eq!(
835 result.unmatched_rules.len(),
836 1,
837 "Expected 1 unmatched rule, got {}",
838 result.unmatched_rules.len()
839 );
840 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
841 }
842
843 #[test]
844 fn test_fallback_to_tag_when_no_mig_number() {
845 let mut workflow = empty_workflow();
846 workflow.fields.push(make_rule(
847 "BGM/C002/1001",
848 "Nachrichtentyp",
849 "X",
850 None, Some(0),
852 Some(0),
853 ));
854
855 let tree = AssembledTree {
856 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
857 groups: vec![],
858 post_group_start: 1,
859 inter_group_segments: BTreeMap::new(),
860 };
861
862 let result = build_validated_tree(&workflow, &tree);
863 assert_eq!(result.root_fields.len(), 1);
864 assert_eq!(result.root_fields[0].value, Some("E01"));
865 }
866
867 #[test]
868 fn test_nested_child_groups() {
869 let mut workflow = empty_workflow();
870 workflow.fields.push(make_rule(
872 "SG4/SG5/LOC/C517/3225",
873 "Marktlokations-ID",
874 "X",
875 Some("0050"),
876 Some(0),
877 Some(0),
878 ));
879
880 let tree = AssembledTree {
881 segments: vec![],
882 groups: vec![AssembledGroup {
883 group_id: "SG4".to_string(),
884 repetitions: vec![AssembledGroupInstance {
885 segments: vec![],
886 entry_mig_number: None,
887 child_groups: vec![AssembledGroup {
888 group_id: "SG5".to_string(),
889 repetitions: vec![AssembledGroupInstance {
890 segments: vec![make_segment(
891 "LOC",
892 vec![vec!["DE00012345678"]],
893 Some("0050"),
894 )],
895 child_groups: vec![],
896 entry_mig_number: None,
897 variant_mig_numbers: vec![],
898 skipped_segments: vec![],
899 skipped_positions: Vec::new(),
900 }],
901 }],
902 variant_mig_numbers: vec![],
903 skipped_segments: vec![],
904 skipped_positions: Vec::new(),
905 }],
906 }],
907 post_group_start: 0,
908 inter_group_segments: BTreeMap::new(),
909 };
910
911 let result = build_validated_tree(&workflow, &tree);
912 assert_eq!(result.groups.len(), 1);
913 let sg4 = &result.groups[0];
914 assert_eq!(sg4.children.len(), 1);
915
916 let sg5 = &sg4.children[0];
917 assert_eq!(sg5.group_id, "SG5");
918 assert_eq!(sg5.fields.len(), 1);
919 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
920 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
921 }
922
923 #[test]
924 fn test_qualifier_aware_segment_matching_same_tag_variants() {
925 let mut workflow = empty_workflow();
939
940 let mut z33 = make_rule(
942 "SG10/STS/C601/9015",
943 "Statuskategorie Z33",
944 "X",
945 Some("00035"),
946 Some(0),
947 Some(0),
948 );
949 z33.codes = vec![super::super::validate::AhbCodeRule {
950 value: "Z33".into(),
951 description: String::new(),
952 ahb_status: "X".into(),
953 }];
954 workflow.fields.push(z33);
955
956 let mut z32 = make_rule(
958 "SG10/STS/C601/9015",
959 "Statuskategorie Z32",
960 "X",
961 Some("00036"),
962 Some(0),
963 Some(0),
964 );
965 z32.codes = vec![super::super::validate::AhbCodeRule {
966 value: "Z32".into(),
967 description: String::new(),
968 ahb_status: "X".into(),
969 }];
970 workflow.fields.push(z32);
971
972 let mut z40 = make_rule(
974 "SG10/STS/C601/9015",
975 "Statuskategorie Z40",
976 "X",
977 Some("00038"),
978 Some(0),
979 Some(0),
980 );
981 z40.codes = vec![super::super::validate::AhbCodeRule {
982 value: "Z40".into(),
983 description: String::new(),
984 ahb_status: "X".into(),
985 }];
986 workflow.fields.push(z40);
987
988 let tree = AssembledTree {
991 segments: vec![],
992 groups: vec![AssembledGroup {
993 group_id: "SG10".to_string(),
994 repetitions: vec![AssembledGroupInstance {
995 segments: vec![
996 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
997 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
998 ],
999 child_groups: vec![],
1000 entry_mig_number: None,
1001 variant_mig_numbers: vec![
1002 "00035".into(),
1003 "00036".into(),
1004 "00037".into(),
1005 "00038".into(),
1006 ],
1007 skipped_segments: vec![],
1008 skipped_positions: Vec::new(),
1009 }],
1010 }],
1011 post_group_start: 0,
1012 inter_group_segments: BTreeMap::new(),
1013 };
1014
1015 let result = build_validated_tree(&workflow, &tree);
1016 assert_eq!(result.groups.len(), 1);
1017 let sg10 = &result.groups[0];
1018 assert_eq!(
1019 sg10.fields.len(),
1020 3,
1021 "all three rules should pass the variant filter"
1022 );
1023
1024 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
1025
1026 assert_eq!(
1028 by_name("Statuskategorie Z33").value,
1029 None,
1030 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
1031 );
1032
1033 assert_eq!(
1036 by_name("Statuskategorie Z32").value,
1037 Some("Z32"),
1038 "rule for Z32 must attach to the STS segment with qualifier Z32"
1039 );
1040
1041 assert_eq!(
1043 by_name("Statuskategorie Z40").value,
1044 Some("Z40"),
1045 "rule for Z40 must attach to the STS segment with qualifier Z40"
1046 );
1047 }
1048}