1use std::collections::{BTreeMap, HashMap, HashSet};
4
5use crate::expr::{ConditionExpr, ConditionParser, StatusKind};
6
7use crate::eval::answer_lists::{answer_path, is_answer_list_id};
8use crate::eval::{
9 AnswerCodeLists, ConditionEvaluator, ConditionExprEvaluator, ConditionResult,
10 EvaluationContext, ExternalConditionProvider, GroupScope, StatusResolution,
11};
12use mig_types::navigator::GroupNavigator;
13use mig_types::segment::OwnedSegment;
14
15use super::tree::{AhbGroupNode, AhbNode, ValidatedTree};
16
17use super::issue::{Severity, ValidationIssue};
18use super::level::ValidationLevel;
19use super::report::ValidationReport;
20use crate::{IssueKind, UnresolvedConditions};
21
22pub use ahb_types::{AhbCodeRule, AhbFieldRule, AhbWorkflow};
28
29pub struct EdifactValidator<E: ConditionEvaluator> {
62 evaluator: E,
63 report_segments: bool,
64}
65
66impl<E: ConditionEvaluator> EdifactValidator<E> {
67 pub fn new(evaluator: E) -> Self {
69 Self {
70 evaluator,
71 report_segments: false,
72 }
73 }
74
75 pub fn reporting_segments(mut self) -> Self {
81 self.report_segments = true;
82 self
83 }
84
85 pub fn validate(
98 &self,
99 segments: &[OwnedSegment],
100 workflow: &AhbWorkflow,
101 external: &dyn ExternalConditionProvider,
102 level: ValidationLevel,
103 ) -> ValidationReport {
104 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
105 .with_format_version(self.evaluator.format_version())
106 .with_pruefidentifikator(&workflow.pruefidentifikator);
107
108 let ctx = EvaluationContext::new(&workflow.pruefidentifikator, external, segments);
109
110 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
111 self.validate_conditions(workflow, &ctx, &mut report);
112 }
113
114 report
115 }
116
117 pub fn validate_with_navigator(
123 &self,
124 segments: &[OwnedSegment],
125 workflow: &AhbWorkflow,
126 external: &dyn ExternalConditionProvider,
127 level: ValidationLevel,
128 navigator: &dyn GroupNavigator,
129 ) -> ValidationReport {
130 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
131 .with_format_version(self.evaluator.format_version())
132 .with_pruefidentifikator(&workflow.pruefidentifikator);
133
134 let ctx = EvaluationContext::with_navigator(
135 &workflow.pruefidentifikator,
136 external,
137 segments,
138 navigator,
139 );
140
141 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
142 self.validate_conditions(workflow, &ctx, &mut report);
143 }
144
145 report
146 }
147
148 pub fn validate_tree(
155 &self,
156 validated_tree: &ValidatedTree,
157 segments: &[OwnedSegment],
158 external: &dyn ExternalConditionProvider,
159 level: ValidationLevel,
160 navigator: Option<&dyn GroupNavigator>,
161 ) -> ValidationReport {
162 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
163 .with_format_version(self.evaluator.format_version())
164 .with_pruefidentifikator(validated_tree.pruefidentifikator);
165
166 if !matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
167 return report;
168 }
169
170 let ctx = match navigator {
171 Some(nav) => EvaluationContext::with_navigator(
172 validated_tree.pruefidentifikator,
173 external,
174 segments,
175 nav,
176 ),
177 None => EvaluationContext::new(validated_tree.pruefidentifikator, external, segments),
178 };
179
180 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
181
182 let mut all_nodes: Vec<&AhbNode> = Vec::new();
185 all_nodes.extend(validated_tree.root_fields.iter());
186 for group in &validated_tree.groups {
187 collect_nodes_depth_first(group, &mut all_nodes);
188 }
189
190 let mut tag_migs: HashMap<String, HashSet<&str>> = HashMap::new();
197 for node in &all_nodes {
198 if let Some(ref m) = node.rule.mig_number {
199 tag_migs
200 .entry(extract_segment_id(&node.rule.segment_path))
201 .or_default()
202 .insert(m.as_str());
203 }
204 }
205 for rule in &validated_tree.unmatched_rules {
206 if let Some(ref m) = rule.mig_number {
207 tag_migs
208 .entry(extract_segment_id(&rule.segment_path))
209 .or_default()
210 .insert(m.as_str());
211 }
212 }
213
214 for node in &validated_tree.root_fields {
216 evaluate_node(
217 node,
218 &ctx,
219 &expr_eval,
220 &self.evaluator,
221 validated_tree.ub_definitions,
222 &tag_migs,
223 None,
224 &mut report,
225 );
226 }
227
228 let mut instance_counter: HashMap<&str, usize> = HashMap::new();
233 for group in &validated_tree.groups {
234 let instance_index = *instance_counter
235 .entry(group.group_id)
236 .and_modify(|c| *c += 1)
237 .or_insert(0);
238
239 let path_storage = [(group.group_id, instance_index)];
240 let scoped_ctx = ctx.with_scope(GroupScope {
243 path: &path_storage,
244 });
245
246 walk_group_instance(
247 group,
248 &path_storage,
249 &scoped_ctx,
250 &expr_eval,
251 &self.evaluator,
252 validated_tree.ub_definitions,
253 &tag_migs,
254 instance_index,
255 self.report_segments,
256 &mut report,
257 );
258 }
259
260 for field in &validated_tree.unmatched_rules {
266 if should_skip_for_parent_group(field, &expr_eval, &ctx, validated_tree.ub_definitions)
267 {
268 continue;
269 }
270
271 let status =
272 expr_eval.resolve_status(&field.ahb_status, &ctx, validated_tree.ub_definitions);
273
274 if status.result == ConditionResult::True
275 && status.kind == Some(StatusKind::Muss)
276 && !is_field_present(&ctx, field)
277 && !is_group_variant_absent(
278 &ctx,
279 field,
280 is_parent_group_required(
281 field,
282 &expr_eval,
283 &ctx,
284 validated_tree.ub_definitions,
285 ),
286 )
287 {
288 let mut issue = ValidationIssue::new(
289 Severity::Error,
290 IssueKind::MissingRequiredField {
291 field_name: field.name.clone(),
292 },
293 )
294 .with_field_path(&field.segment_path)
295 .with_rule(&field.ahb_status);
296 if let Some(first_code) = field.codes.first() {
297 issue.expected_value = Some(first_code.value.clone());
298 }
299 issue = attach_field_position(issue, field);
300 report.add_issue(issue);
301 }
302 }
303
304 if matches!(level, ValidationLevel::Full) {
308 let mut all_fields: Vec<AhbFieldRule> = Vec::new();
309 for node in &all_nodes {
310 all_fields.push(node.rule.clone());
311 }
312 for rule in &validated_tree.unmatched_rules {
313 all_fields.push((*rule).clone());
314 }
315 let synthetic_workflow = AhbWorkflow {
316 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
317 description: String::new(),
318 communication_direction: None,
319 fields: all_fields,
320 ub_definitions: validated_tree.ub_definitions.clone(),
321 };
322 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
323 self.validate_answer_lists(&synthetic_workflow, &ctx, &mut report);
324 self.validate_package_cardinality(&synthetic_workflow, &ctx, &mut report);
325 }
326
327 report
328 }
329
330 fn validate_conditions(
332 &self,
333 workflow: &AhbWorkflow,
334 ctx: &EvaluationContext,
335 report: &mut ValidationReport,
336 ) {
337 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
338
339 for field in &workflow.fields {
340 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
342 continue;
343 }
344
345 let StatusResolution {
348 result: condition_result,
349 kind,
350 unknown_ids,
351 } = expr_eval.resolve_status(&field.ahb_status, ctx, &workflow.ub_definitions);
352 let mandatory = kind == Some(StatusKind::Muss);
353
354 match condition_result {
355 ConditionResult::True => {
356 if mandatory
358 && !is_field_present(ctx, field)
359 && !is_group_variant_absent(
360 ctx,
361 field,
362 is_parent_group_required(
363 field,
364 &expr_eval,
365 ctx,
366 &workflow.ub_definitions,
367 ),
368 )
369 {
370 let mut issue = ValidationIssue::new(
371 Severity::Error,
372 IssueKind::MissingRequiredField {
373 field_name: field.name.clone(),
374 },
375 )
376 .with_field_path(&field.segment_path)
377 .with_rule(&field.ahb_status);
378 if let Some(first_code) = field.codes.first() {
382 issue.expected_value = Some(first_code.value.clone());
383 }
384 report.add_issue(issue);
385 }
386 }
387 ConditionResult::False => {
388 if mandatory && is_field_present(ctx, field) {
394 report.add_issue(
395 ValidationIssue::new(
396 Severity::Error,
397 IssueKind::FieldConditionNotSatisfied {
398 field_name: field.name.clone(),
399 },
400 )
401 .with_field_path(&field.segment_path)
402 .with_rule(&field.ahb_status),
403 );
404 }
405 }
406 ConditionResult::Unknown => {
407 let mut external_ids = Vec::new();
412 let mut undetermined_ids = Vec::new();
413 let mut missing_ids = Vec::new();
414 for id in unknown_ids {
415 if self.evaluator.is_external(id) {
416 external_ids.push(id);
417 } else if self.evaluator.is_known(id) {
418 undetermined_ids.push(id);
419 } else {
420 missing_ids.push(id);
421 }
422 }
423
424 report.add_issue(
425 ValidationIssue::new(
426 Severity::Info,
427 IssueKind::ConditionUnknown {
428 field_name: field.name.clone(),
429 unresolved: UnresolvedConditions {
430 external: external_ids,
431 undetermined: undetermined_ids,
432 missing: missing_ids,
433 },
434 },
435 )
436 .with_field_path(&field.segment_path)
437 .with_rule(&field.ahb_status),
438 );
439 }
440 }
441 }
442
443 self.validate_codes_cross_field(workflow, ctx, report);
448 self.validate_answer_lists(workflow, ctx, report);
449
450 self.validate_package_cardinality(workflow, ctx, report);
453 }
454
455 fn validate_package_cardinality(
462 &self,
463 workflow: &AhbWorkflow,
464 ctx: &EvaluationContext,
465 report: &mut ValidationReport,
466 ) {
467 struct PackageGroup {
470 min: u32,
471 max: u32,
472 code_values: Vec<String>,
473 element_index: usize,
474 component_index: usize,
475 }
476
477 let mut groups: HashMap<(String, Option<String>, u32), PackageGroup> = HashMap::new();
486
487 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
488
489 for field in &workflow.fields {
490 let el_idx = field.element_index.unwrap_or(0);
491 let comp_idx = field.component_index.unwrap_or(0);
492
493 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
495 continue;
496 }
497
498 for code in &field.codes {
499 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
501 let mut packages = Vec::new();
503 collect_packages(&expr, &mut packages);
504
505 for (pkg_id, pkg_min, pkg_max) in packages {
506 let key = (field.segment_path.clone(), field.mig_number.clone(), pkg_id);
507 let group = groups.entry(key).or_insert_with(|| PackageGroup {
508 min: pkg_min,
509 max: pkg_max,
510 code_values: Vec::new(),
511 element_index: el_idx,
512 component_index: comp_idx,
513 });
514 group.min = group.min.max(pkg_min);
517 group.max = group.max.min(pkg_max);
518 group.code_values.push(code.value.clone());
519 }
520 }
521 }
522 }
523
524 for ((seg_path, mig_number, pkg_id), group) in &groups {
532 let segment_id = extract_segment_id(seg_path);
533
534 let mut unique_codes: Vec<&str> =
538 group.code_values.iter().map(|s| s.as_str()).collect();
539 unique_codes.sort_unstable();
540 unique_codes.dedup();
541 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
542
543 let group_path_str = extract_group_path_key(seg_path);
544 let group_path: Vec<&str> = if group_path_str.is_empty() {
545 Vec::new()
546 } else {
547 group_path_str.split('/').collect()
548 };
549
550 let min = group.min as usize;
551 let max = group.max as usize;
552
553 let per_instance_counts: Option<Vec<usize>> =
554 match (ctx.navigator, group_path.is_empty()) {
555 (Some(nav), false)
563 if group_path.len() > 1
564 && !full_instance_paths(nav, &group_path).is_empty() =>
565 {
566 let paths = full_instance_paths(nav, &group_path);
567 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
575 .as_deref()
576 {
577 Some(m) => (0..group_path.len())
578 .filter_map(|level| {
579 let prefix = group_path[..=level].join("/");
580 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
581 })
582 .collect(),
583 None => Vec::new(),
584 };
585 if paths.is_empty() {
586 None
587 } else {
588 Some(
589 paths
590 .iter()
591 .filter(|path| match mig_number.as_deref() {
592 Some(m) => nav.instance_at_has_mig_number(path, m),
593 None => true,
594 })
595 .filter(|path| {
596 variant_entries.iter().all(|(level, entry)| {
597 nav.find_segments_in_instance(
598 &entry.tag,
599 &path[..=*level],
600 )
601 .first()
602 .and_then(|seg| {
603 seg.elements
604 .get(entry.element)
605 .and_then(|e| e.get(entry.component))
606 .cloned()
607 })
608 .map_or(true, |v| entry.codes.contains(v.as_str()))
609 })
610 })
611 .map(|path| {
612 nav.find_segments_in_instance(&segment_id, path)
613 .iter()
614 .filter_map(|seg| {
615 seg.elements
616 .get(group.element_index)
617 .and_then(|e| e.get(group.component_index))
618 .filter(|v| !v.is_empty())
619 .cloned()
620 })
621 .filter(|v| code_set.contains(v.as_str()))
622 .count()
623 })
624 .collect(),
625 )
626 }
627 }
628 (Some(nav), false) => {
629 let instance_count = nav.group_instance_count(&group_path);
630 if instance_count == 0 {
631 None
632 } else {
633 Some(
641 (0..instance_count)
642 .filter(|i| match mig_number.as_deref() {
643 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
644 None => true,
645 })
646 .map(|i| {
647 nav.find_segments_in_group(&segment_id, &group_path, i)
648 .iter()
649 .filter_map(|seg| {
650 seg.elements
651 .get(group.element_index)
652 .and_then(|e| e.get(group.component_index))
653 .filter(|v| !v.is_empty())
654 .cloned()
655 })
656 .filter(|v| code_set.contains(v.as_str()))
657 .count()
658 })
659 .collect(),
660 )
661 }
662 }
663 _ => None,
664 };
665
666 let counts: Vec<usize> = per_instance_counts.unwrap_or_else(|| {
667 let segments = ctx.find_segments(&segment_id);
668 let count = segments
669 .iter()
670 .filter_map(|seg| {
671 seg.elements
672 .get(group.element_index)
673 .and_then(|e| e.get(group.component_index))
674 .filter(|v| !v.is_empty())
675 .map(|s| s.as_str())
676 })
677 .filter(|v| code_set.contains(v))
678 .count();
679 vec![count]
680 });
681
682 let mut reported_counts: HashSet<usize> = HashSet::new();
686 for present_count in counts {
687 if (present_count < min || present_count > max)
688 && reported_counts.insert(present_count)
689 {
690 report.add_issue(
691 ValidationIssue::new(
692 Severity::Error,
693 IssueKind::PackageCardinality {
694 package_id: pkg_id.to_string(),
695 present: present_count,
696 min,
697 max,
698 codes: unique_codes.iter().map(|s| s.to_string()).collect(),
699 },
700 )
701 .with_field_path(seg_path)
702 .with_expected(format!("{}..{}", group.min, group.max))
703 .with_actual(present_count.to_string()),
704 );
705 }
706 }
707 }
708 }
709
710 fn validate_codes_cross_field(
722 &self,
723 workflow: &AhbWorkflow,
724 ctx: &EvaluationContext,
725 report: &mut ValidationReport,
726 ) {
727 if ctx.navigator.is_some() {
728 self.validate_codes_group_scoped(workflow, ctx, report);
729 } else {
730 self.validate_codes_tag_scoped(workflow, ctx, report);
731 }
732 }
733
734 fn validate_codes_group_scoped(
737 &self,
738 workflow: &AhbWorkflow,
739 ctx: &EvaluationContext,
740 report: &mut ValidationReport,
741 ) {
742 let by_loc = partition_codes_by_mig(workflow);
743 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
744 let nav = ctx.navigator.unwrap();
745
746 for ((group_key, tag), migs) in &by_loc {
747 let field_path = if group_key.is_empty() {
748 format!("{tag}/qualifier")
749 } else {
750 format!("{group_key}/{tag}/qualifier")
751 };
752
753 let group_path: Vec<&str> = if group_key.is_empty() {
754 Vec::new()
755 } else {
756 group_key.split('/').collect()
757 };
758
759 let tag_qualifiers = known_qualifiers.get(tag);
760
761 if group_path.is_empty() {
762 Self::validate_segments_per_mig(
763 &ctx.find_segments(tag),
764 migs,
765 tag_qualifiers,
766 tag,
767 &field_path,
768 report,
769 );
770 } else {
771 let instance_count = nav.group_instance_count(&group_path);
772 for i in 0..instance_count {
773 let owned = nav.find_segments_in_group(tag, &group_path, i);
774 let refs: Vec<&OwnedSegment> = owned.iter().collect();
775 Self::validate_segments_per_mig(
776 &refs,
777 migs,
778 tag_qualifiers,
779 tag,
780 &field_path,
781 report,
782 );
783 }
784 }
785 }
786 }
787
788 fn validate_codes_tag_scoped(
792 &self,
793 workflow: &AhbWorkflow,
794 ctx: &EvaluationContext,
795 report: &mut ValidationReport,
796 ) {
797 let by_loc = partition_codes_by_mig(workflow);
798 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
799 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
801 for ((_group_key, tag), migs) in by_loc {
802 let merged = by_tag.entry(tag).or_default();
803 for (mig_key, bucket) in migs {
804 let entry = merged.entry(mig_key).or_default();
805 if entry.qualifier_position.is_none() {
806 entry.qualifier_position = bucket.qualifier_position;
807 }
808 if entry.qualifier_position == bucket.qualifier_position {
809 entry.qualifier_values.extend(&bucket.qualifier_values);
810 }
811 for (pos, codes) in bucket.codes {
812 entry.codes.entry(pos).or_default().extend(codes);
813 }
814 }
815 }
816
817 for (tag, migs) in &by_tag {
818 let field_path = format!("{tag}/qualifier");
819 Self::validate_segments_per_mig(
820 &ctx.find_segments(tag),
821 migs,
822 known_qualifiers.get(tag),
823 tag,
824 &field_path,
825 report,
826 );
827 }
828 }
829
830 fn validate_segments_per_mig(
839 segments: &[&OwnedSegment],
840 migs: &HashMap<Option<String>, MigCodeBucket>,
841 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
842 tag: &str,
843 field_path: &str,
844 report: &mut ValidationReport,
845 ) {
846 for seg in segments {
847 match match_segment_to_mig(seg, migs) {
848 Some(bucket) => {
849 for ((el, c), allowed) in &bucket.codes {
850 if allowed.is_empty() {
851 continue;
852 }
853 Self::check_segments_against_codes(
854 vec![*seg],
855 allowed,
856 tag,
857 *el,
858 *c,
859 field_path,
860 report,
861 );
862 }
863 }
864 None => {
865 let Some(qualifiers) = tag_qualifiers else {
866 continue;
867 };
868 for ((el, c), allowed) in qualifiers {
873 let Some(actual) = seg
874 .elements
875 .get(*el)
876 .and_then(|e| e.get(*c))
877 .filter(|v| !v.is_empty())
878 .map(|s| s.as_str())
879 else {
880 continue;
881 };
882 if allowed.iter().any(|v| v == actual) {
883 break;
885 }
886 let bucket_values: HashSet<&str> = migs
889 .values()
890 .filter(|b| b.qualifier_position == Some((*el, *c)))
891 .flat_map(|b| b.qualifier_values.iter().copied())
892 .collect();
893 if !bucket_values.is_empty() {
894 Self::check_segments_against_codes(
895 vec![*seg],
896 &bucket_values,
897 tag,
898 *el,
899 *c,
900 field_path,
901 report,
902 );
903 break;
904 }
905 }
906 }
907 }
908 }
909 }
910
911 fn validate_answer_lists(
920 &self,
921 workflow: &AhbWorkflow,
922 ctx: &EvaluationContext,
923 report: &mut ValidationReport,
924 ) {
925 let lists = AnswerCodeLists::embedded();
926 let fv = self.evaluator.format_version();
927 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
928 for list_rule in &workflow.fields {
929 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
930 continue;
931 };
932 if list_rule.codes.is_empty()
933 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
934 {
935 continue;
936 }
937 let Some(answer_rule) = workflow
938 .fields
939 .iter()
940 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
941 else {
942 continue;
943 };
944 let at = |rule: &AhbFieldRule| {
945 (
946 rule.element_index.unwrap_or(0),
947 rule.component_index.unwrap_or(0),
948 )
949 };
950 let (list_el, list_c) = at(list_rule);
951 let (answer_el, answer_c) = at(answer_rule);
952 let value = |seg: &OwnedSegment, el: usize, c: usize| {
953 seg.elements
954 .get(el)
955 .and_then(|e| e.get(c))
956 .filter(|v| !v.is_empty())
957 .cloned()
958 };
959 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
960 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
961 continue;
962 }
963 let (Some(list), Some(answer)) =
964 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
965 else {
966 continue;
967 };
968 if !list_rule.codes.iter().any(|c| c.value == list) {
969 continue;
970 }
971 if answer_rule.codes.iter().any(|c| c.value == answer) {
975 continue;
976 }
977 let Some(codes) = lists.codes(fv, &list) else {
978 continue;
979 };
980 if codes.contains(&answer) {
981 continue;
982 }
983 let mut allowed: Vec<String> = codes.iter().cloned().collect();
984 allowed.sort_unstable();
985 report.add_issue(
986 ValidationIssue::new(
987 Severity::Warning,
988 IssueKind::AnswerNotInList {
989 answer: answer.clone(),
990 list: list.clone(),
991 allowed: allowed.clone(),
992 },
993 )
994 .with_field_path(&answer_rule.segment_path)
995 .with_actual(&answer)
996 .with_expected(allowed.join(", ")),
997 );
998 }
999 }
1000 }
1001
1002 fn check_segments_against_codes(
1004 segments: Vec<&OwnedSegment>,
1005 allowed_codes: &HashSet<&str>,
1006 _tag: &str,
1007 el_idx: usize,
1008 comp_idx: usize,
1009 field_path: &str,
1010 report: &mut ValidationReport,
1011 ) {
1012 for segment in segments {
1013 if let Some(code_value) = segment
1014 .elements
1015 .get(el_idx)
1016 .and_then(|e| e.get(comp_idx))
1017 .filter(|v| !v.is_empty())
1018 {
1019 if !allowed_codes.contains(code_value.as_str()) {
1020 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1021 sorted_codes.sort_unstable();
1022 let kind = IssueKind::CodeNotAllowedForPid {
1023 value: code_value.to_string(),
1024 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1025 };
1026 report.add_issue(
1027 ValidationIssue::new(Severity::Error, kind)
1028 .with_field_path(field_path)
1029 .with_actual(code_value)
1030 .with_expected(sorted_codes.join(", ")),
1031 );
1032 }
1033 }
1034 }
1035 }
1036}
1037
1038fn full_instance_paths<'p>(
1041 nav: &dyn GroupNavigator,
1042 group_path: &[&'p str],
1043) -> Vec<Vec<(&'p str, usize)>> {
1044 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1045 for g in group_path {
1046 paths = paths
1047 .into_iter()
1048 .flat_map(|parent| {
1049 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1050 let mut path = parent.clone();
1051 path.push((*g, i));
1052 path
1053 })
1054 })
1055 .collect();
1056 }
1057 paths
1058}
1059
1060struct VariantEntry<'a> {
1063 tag: String,
1064 element: usize,
1065 component: usize,
1066 codes: HashSet<&'a str>,
1067}
1068
1069fn merged_variant_entry<'a>(
1075 workflow: &'a AhbWorkflow,
1076 group_path: &str,
1077 mig: &str,
1078) -> Option<VariantEntry<'a>> {
1079 let mig: u32 = mig.parse().ok()?;
1080 let prefix = format!("{group_path}/");
1081 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1083 .fields
1084 .iter()
1085 .filter_map(|r| {
1086 let rest = r.segment_path.strip_prefix(&prefix)?;
1087 let tag = rest.split('/').next()?;
1088 if tag.starts_with("SG") {
1089 return None;
1090 }
1091 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1092 })
1093 .collect();
1094 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1096 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1097 .iter()
1098 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1099 .collect();
1100 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1101 if numbers.len() < 2 {
1102 return None;
1103 }
1104 let own = entries
1105 .iter()
1106 .filter(|(_, n, _)| *n <= mig)
1107 .map(|(_, n, _)| *n)
1108 .max()?;
1109 let (rule, _, tag) = entries
1111 .iter()
1112 .filter(|(_, n, _)| *n == own)
1113 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1114 Some(VariantEntry {
1115 tag: tag.to_string(),
1116 element: rule.element_index.unwrap_or(0),
1117 component: rule.component_index.unwrap_or(0),
1118 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1119 })
1120}
1121
1122fn max_length(format: &str) -> Option<usize> {
1125 let digits = format
1126 .trim()
1127 .strip_prefix("an")
1128 .or_else(|| format.trim().strip_prefix('a'))
1129 .or_else(|| format.trim().strip_prefix('n'))?;
1130 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1131}
1132
1133fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1143 let derived_from_codes = field
1144 .codes
1145 .first()
1146 .is_some_and(|c| c.ahb_status == field.ahb_status);
1147 if !derived_from_codes {
1148 return &field.ahb_status;
1149 }
1150 value
1151 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1152 .map_or(&field.ahb_status, |c| &c.ahb_status)
1153}
1154
1155fn should_skip_for_parent_group<E: ConditionEvaluator>(
1161 field: &AhbFieldRule,
1162 expr_eval: &ConditionExprEvaluator<E>,
1163 ctx: &EvaluationContext,
1164 ub_definitions: &BTreeMap<String, ConditionExpr>,
1165) -> bool {
1166 if let Some(ref group_status) = field.parent_group_ahb_status {
1167 if group_status.contains('[') {
1168 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
1169 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
1170 }
1171 }
1172 false
1173}
1174
1175fn is_parent_group_required<E: ConditionEvaluator>(
1182 field: &AhbFieldRule,
1183 expr_eval: &ConditionExprEvaluator<E>,
1184 ctx: &EvaluationContext,
1185 ub_definitions: &BTreeMap<String, ConditionExpr>,
1186) -> bool {
1187 field
1188 .parent_group_ahb_status
1189 .as_deref()
1190 .is_some_and(|status| {
1191 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1192 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1193 })
1194}
1195
1196fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1205 let segment_id = extract_segment_id(&field.segment_path);
1206
1207 if !field.codes.is_empty() {
1213 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1214 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1215 let matching = ctx.find_segments(&segment_id);
1216 return matching.iter().any(|seg| {
1217 seg.elements
1218 .get(el_idx)
1219 .and_then(|e| e.get(comp_idx))
1220 .is_some_and(|v| required_codes.contains(&v.as_str()))
1221 });
1222 }
1223 if is_qualifier_field(&field.segment_path) {
1226 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1227 let el_idx = field.element_index.unwrap_or(0);
1228 let comp_idx = field.component_index.unwrap_or(0);
1229 let matching = ctx.find_segments(&segment_id);
1230 return matching.iter().any(|seg| {
1231 seg.elements
1232 .get(el_idx)
1233 .and_then(|e| e.get(comp_idx))
1234 .is_some_and(|v| required_codes.contains(&v.as_str()))
1235 });
1236 }
1237 }
1238
1239 ctx.has_segment(&segment_id)
1240}
1241
1242fn is_group_variant_absent(
1257 ctx: &EvaluationContext,
1258 field: &AhbFieldRule,
1259 is_group_mandatory: bool,
1260) -> bool {
1261 let group_path: Vec<&str> = field
1262 .segment_path
1263 .split('/')
1264 .take_while(|p| p.starts_with("SG"))
1265 .collect();
1266
1267 if group_path.is_empty() {
1268 return false;
1269 }
1270
1271 let nav = match ctx.navigator {
1272 Some(nav) => nav,
1273 None => return false,
1274 };
1275
1276 let instance_count = nav.group_instance_count(&group_path);
1277
1278 if instance_count == 0 {
1283 if !is_group_mandatory {
1284 return true;
1285 }
1286 return false;
1288 }
1289
1290 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1295 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1297 let segment_id = extract_segment_id(&field.segment_path);
1298 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1299
1300 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1301 nav.find_segments_in_group(&segment_id, &group_path, i)
1302 .iter()
1303 .any(|seg| {
1304 seg.elements
1305 .first()
1306 .and_then(|e| e.first())
1307 .is_some_and(|v| required_codes.contains(&v.as_str()))
1308 })
1309 });
1310
1311 if !any_instance_has_qualifier {
1312 return true; }
1314 }
1315 }
1316
1317 let segment_id = extract_segment_id(&field.segment_path);
1326 let segment_absent_from_all = (0..instance_count).all(|i| {
1327 nav.find_segments_in_group(&segment_id, &group_path, i)
1328 .is_empty()
1329 });
1330 if segment_absent_from_all {
1331 let group_has_other_segments =
1332 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1333 if group_has_other_segments {
1334 return true;
1335 }
1336 }
1337
1338 false
1339}
1340
1341fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1343 out.extend(group.fields.iter());
1344 for child in &group.children {
1345 collect_nodes_depth_first(child, out);
1346 }
1347}
1348
1349#[allow(clippy::too_many_arguments)]
1353fn evaluate_node<E: ConditionEvaluator>(
1354 node: &AhbNode,
1355 ctx: &EvaluationContext,
1356 expr_eval: &ConditionExprEvaluator<E>,
1357 evaluator: &E,
1358 ub_definitions: &BTreeMap<String, ConditionExpr>,
1359 tag_migs: &HashMap<String, HashSet<&str>>,
1360 instance_index: Option<usize>,
1361 report: &mut ValidationReport,
1362) {
1363 let field = node.rule;
1364
1365 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1367
1368 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1373 let length = value.chars().count();
1374 if length > max {
1375 let mut issue = ValidationIssue::new(
1376 Severity::Error,
1377 IssueKind::ValueTooLong {
1378 field_name: field.name.clone(),
1379 length,
1380 max,
1381 },
1382 )
1383 .with_field_path(&field.segment_path)
1384 .with_actual(value)
1385 .with_expected(field.format.clone().unwrap_or_default());
1386 if let Some(idx) = instance_index {
1387 issue = issue.with_instance_index(idx);
1388 }
1389 if let Some(num) = node.matched_segment_number {
1390 issue = issue.with_position(crate::SegmentPosition {
1391 segment_number: num,
1392 byte_offset: 0,
1393 message_number: 1,
1394 });
1395 }
1396 report.add_issue(attach_field_position(issue, field));
1397 }
1398 }
1399
1400 if should_skip_for_parent_group(field, expr_eval, ctx, ub_definitions) {
1402 return;
1403 }
1404
1405 let status = governing_status(field, node.value);
1408 let StatusResolution {
1409 result: condition_result,
1410 kind,
1411 unknown_ids,
1412 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1413 let mandatory = kind == Some(StatusKind::Muss);
1414
1415 match condition_result {
1416 ConditionResult::True => {
1417 if mandatory && node.value.is_none() {
1426 let tag = extract_segment_id(&field.segment_path);
1427 let single_variant_present = node.segment_elements.is_none()
1428 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1429 && is_field_present(ctx, field);
1430 let segment_optional_and_absent = node.segment_elements.is_none()
1435 && field
1436 .segment_ahb_status
1437 .as_deref()
1438 .is_some_and(is_optional_segment_status);
1439 let segment_not_required_and_absent = node.segment_elements.is_none()
1445 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1446 expr_eval
1447 .resolve_status(st, &node_ctx, ub_definitions)
1448 .result
1449 != ConditionResult::True
1450 });
1451 if !single_variant_present
1452 && !segment_optional_and_absent
1453 && !segment_not_required_and_absent
1454 {
1455 let mut issue = ValidationIssue::new(
1456 Severity::Error,
1457 IssueKind::MissingRequiredField {
1458 field_name: field.name.clone(),
1459 },
1460 )
1461 .with_field_path(&field.segment_path)
1462 .with_rule(&field.ahb_status);
1463 if let Some(first_code) = field.codes.first() {
1464 issue.expected_value = Some(first_code.value.clone());
1465 }
1466 if let Some(idx) = instance_index {
1467 issue = issue.with_instance_index(idx);
1468 }
1469 if let Some(num) = node.matched_segment_number {
1470 issue = issue.with_position(crate::SegmentPosition {
1471 segment_number: num,
1472 byte_offset: 0,
1473 message_number: 1,
1474 });
1475 }
1476 issue = attach_field_position(issue, field);
1477 report.add_issue(issue);
1478 }
1479 }
1480 }
1481 ConditionResult::False => {
1482 if mandatory && node.value.is_some() {
1485 let mut issue = ValidationIssue::new(
1486 Severity::Error,
1487 IssueKind::FieldConditionNotSatisfied {
1488 field_name: field.name.clone(),
1489 },
1490 )
1491 .with_field_path(&field.segment_path)
1492 .with_rule(&field.ahb_status);
1493 if let Some(idx) = instance_index {
1494 issue = issue.with_instance_index(idx);
1495 }
1496 if let Some(num) = node.matched_segment_number {
1497 issue = issue.with_position(crate::SegmentPosition {
1498 segment_number: num,
1499 byte_offset: 0,
1500 message_number: 1,
1501 });
1502 }
1503 issue = attach_field_position(issue, field);
1504 report.add_issue(issue);
1505 }
1506 }
1507 ConditionResult::Unknown => {
1508 let mut external_ids = Vec::new();
1510 let mut undetermined_ids = Vec::new();
1511 let mut missing_ids = Vec::new();
1512 for id in unknown_ids {
1513 if evaluator.is_external(id) {
1514 external_ids.push(id);
1515 } else if evaluator.is_known(id) {
1516 undetermined_ids.push(id);
1517 } else {
1518 missing_ids.push(id);
1519 }
1520 }
1521
1522 let mut issue = ValidationIssue::new(
1523 Severity::Info,
1524 IssueKind::ConditionUnknown {
1525 field_name: field.name.clone(),
1526 unresolved: UnresolvedConditions {
1527 external: external_ids,
1528 undetermined: undetermined_ids,
1529 missing: missing_ids,
1530 },
1531 },
1532 )
1533 .with_field_path(&field.segment_path)
1534 .with_rule(&field.ahb_status);
1535 if let Some(idx) = instance_index {
1536 issue = issue.with_instance_index(idx);
1537 }
1538 if let Some(num) = node.matched_segment_number {
1539 issue = issue.with_position(crate::SegmentPosition {
1540 segment_number: num,
1541 byte_offset: 0,
1542 message_number: 1,
1543 });
1544 }
1545 issue = attach_field_position(issue, field);
1546 report.add_issue(issue);
1547 }
1548 }
1549}
1550
1551fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1556 match field.element_index {
1557 Some(el) => {
1558 let element_pos = (el as u32) + 2;
1559 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1560 issue.with_field_position(element_pos, component_pos)
1561 }
1562 None => issue,
1563 }
1564}
1565
1566fn refuse_segments<E: ConditionEvaluator>(
1572 group: &AhbGroupNode,
1573 path: &[(&str, usize)],
1574 scoped_ctx: &EvaluationContext,
1575 evaluator: &E,
1576 ub_definitions: &BTreeMap<String, ConditionExpr>,
1577 instance_index: usize,
1578 report: &mut ValidationReport,
1579) {
1580 let mut seen: Vec<u32> = Vec::new();
1581 for node in &group.fields {
1582 let rule = node.rule;
1583 let (Some(status), Some(value), Some(number)) = (
1584 rule.segment_ahb_status.as_deref(),
1585 node.value,
1586 node.matched_segment_number,
1587 ) else {
1588 continue;
1589 };
1590 let qualifier =
1593 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
1594 if !qualifier
1595 || seen.contains(&number)
1596 || Some(status) == group.ahb_status
1597 || !rule.codes.iter().any(|c| c.value == value)
1598 {
1599 continue;
1600 }
1601 seen.push(number);
1602 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1603 continue;
1604 }
1605 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1606 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
1607 report.add_issue(
1608 ValidationIssue::new(
1609 Severity::Warning,
1610 IssueKind::SegmentNotAllowed {
1611 segment: segment.join("/"),
1612 },
1613 )
1614 .with_field_path(segment.join("/"))
1615 .with_rule(status)
1616 .with_instance_index(instance_index)
1617 .with_position(crate::SegmentPosition {
1618 segment_number: number,
1619 byte_offset: 0,
1620 message_number: 1,
1621 }),
1622 );
1623 }
1624}
1625
1626#[allow(clippy::too_many_arguments)]
1635fn walk_group_instance<E: ConditionEvaluator>(
1636 group: &AhbGroupNode,
1637 path: &[(&str, usize)],
1638 scoped_ctx: &EvaluationContext,
1639 expr_eval: &ConditionExprEvaluator<E>,
1640 evaluator: &E,
1641 ub_definitions: &BTreeMap<String, ConditionExpr>,
1642 tag_migs: &HashMap<String, HashSet<&str>>,
1643 instance_index: usize,
1644 report_segments: bool,
1645 report: &mut ValidationReport,
1646) {
1647 let mut group_refused = false;
1650 if let Some(status) = group.ahb_status {
1651 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1652 group_refused = true;
1653 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1654 let mut issue = ValidationIssue::new(
1655 Severity::Warning,
1656 IssueKind::GroupNotAllowed {
1657 group: group_path.join("/"),
1658 },
1659 )
1660 .with_field_path(group_path.join("/"))
1661 .with_rule(status)
1662 .with_instance_index(instance_index);
1663 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
1664 issue = issue.with_position(crate::SegmentPosition {
1665 segment_number: num,
1666 byte_offset: 0,
1667 message_number: 1,
1668 });
1669 }
1670 report.add_issue(issue);
1671 }
1672 }
1673 if report_segments && !group_refused {
1674 refuse_segments(
1675 group,
1676 path,
1677 scoped_ctx,
1678 evaluator,
1679 ub_definitions,
1680 instance_index,
1681 report,
1682 );
1683 }
1684 for node in &group.fields {
1685 evaluate_node(
1686 node,
1687 scoped_ctx,
1688 expr_eval,
1689 evaluator,
1690 ub_definitions,
1691 tag_migs,
1692 Some(instance_index),
1693 report,
1694 );
1695 }
1696 for child in &group.children {
1697 let mut child_path = path.to_vec();
1698 child_path.push((child.group_id, child.instance_index));
1699 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
1700 walk_group_instance(
1701 child,
1702 &child_path,
1703 &child_ctx,
1704 expr_eval,
1705 evaluator,
1706 ub_definitions,
1707 tag_migs,
1708 instance_index,
1709 report_segments,
1710 report,
1711 );
1712 }
1713}
1714
1715fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
1717 match expr {
1718 ConditionExpr::Package { id, min, max } => {
1719 out.push((*id, *min, *max));
1720 }
1721 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1722 for e in exprs {
1723 collect_packages(e, out);
1724 }
1725 }
1726 ConditionExpr::Xor(left, right) => {
1727 collect_packages(left, out);
1728 collect_packages(right, out);
1729 }
1730 ConditionExpr::Not(inner) => {
1731 collect_packages(inner, out);
1732 }
1733 ConditionExpr::Ref(_) => {}
1734 }
1735}
1736
1737fn is_optional_segment_status(status: &str) -> bool {
1747 let trimmed = status.trim();
1748 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
1749}
1750
1751fn is_qualifier_field(path: &str) -> bool {
1761 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
1762 matches!(parts.len(), 2 | 3)
1764}
1765
1766#[derive(Default)]
1774struct MigCodeBucket<'a> {
1775 qualifier_position: Option<(usize, usize)>,
1778 qualifier_values: HashSet<&'a str>,
1781 codes: HashMap<(usize, usize), HashSet<&'a str>>,
1784}
1785
1786fn partition_codes_by_mig(
1791 workflow: &AhbWorkflow,
1792) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
1793 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
1794 for field in &workflow.fields {
1795 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
1796 continue;
1797 }
1798 let tag = extract_segment_id(&field.segment_path);
1799 let group_key = extract_group_path_key(&field.segment_path);
1800 let mig = field.mig_number.clone();
1801 let el = field.element_index.unwrap_or(0);
1802 let c = field.component_index.unwrap_or(0);
1803
1804 let bucket = out
1805 .entry((group_key, tag))
1806 .or_default()
1807 .entry(mig)
1808 .or_default();
1809
1810 let required: Vec<&str> = field
1817 .codes
1818 .iter()
1819 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
1820 .map(|code| code.value.as_str())
1821 .collect();
1822
1823 if !required.is_empty() {
1830 if bucket.qualifier_position.is_none() {
1831 bucket.qualifier_position = Some((el, c));
1832 }
1833 if bucket.qualifier_position == Some((el, c)) {
1834 bucket.qualifier_values.extend(required.iter().copied());
1835 }
1836 }
1837
1838 for v in required {
1839 bucket.codes.entry((el, c)).or_default().insert(v);
1840 }
1841 }
1842 out
1843}
1844
1845fn global_qualifiers_by_tag(
1851 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
1852) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
1853 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
1854 for ((_group, tag), migs) in by_loc {
1855 let tag_entry = out.entry(tag.clone()).or_default();
1856 for bucket in migs.values() {
1857 if let Some(pos) = bucket.qualifier_position {
1858 tag_entry
1859 .entry(pos)
1860 .or_default()
1861 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
1862 }
1863 }
1864 }
1865 out
1866}
1867
1868fn match_segment_to_mig<'a, 'b>(
1879 seg: &OwnedSegment,
1880 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
1881) -> Option<&'a MigCodeBucket<'b>> {
1882 let actual_at = |el: usize, c: usize| -> &str {
1883 seg.elements
1884 .get(el)
1885 .and_then(|e| e.get(c))
1886 .map(|s| s.as_str())
1887 .unwrap_or("")
1888 };
1889
1890 let mut best: Option<&MigCodeBucket> = None;
1891 let mut best_matches = 0usize;
1892
1893 for bucket in migs.values() {
1894 let Some((el, c)) = bucket.qualifier_position else {
1895 continue;
1896 };
1897 if !bucket.qualifier_values.contains(actual_at(el, c)) {
1898 continue;
1899 }
1900 let extra_matches = bucket
1901 .codes
1902 .iter()
1903 .filter(|(pos, _)| **pos != (el, c))
1904 .filter(|((e, k), allowed)| {
1905 let v = actual_at(*e, *k);
1906 !v.is_empty() && allowed.contains(v)
1907 })
1908 .count();
1909 if best.is_none() || extra_matches > best_matches {
1910 best = Some(bucket);
1911 best_matches = extra_matches;
1912 }
1913 }
1914 best
1915}
1916
1917fn extract_group_path_key(path: &str) -> String {
1922 let sg_parts: Vec<&str> = path
1923 .split('/')
1924 .take_while(|p| p.starts_with("SG"))
1925 .collect();
1926 sg_parts.join("/")
1927}
1928
1929fn extract_segment_id(path: &str) -> String {
1931 for part in path.split('/') {
1932 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
1934 continue;
1935 }
1936 if part.len() >= 3
1938 && part
1939 .chars()
1940 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
1941 {
1942 return part.to_string();
1943 }
1944 }
1945 path.split('/').next_back().unwrap_or(path).to_string()
1947}
1948
1949pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
1961 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
1963 if unh_count > 1 {
1964 return Some(ValidationIssue::new(
1965 Severity::Error,
1966 IssueKind::UntCountNotVerifiable { unh_count },
1967 ));
1968 }
1969
1970 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
1972 let declared: usize = unt.get_element(0).parse().ok()?;
1973
1974 let actual = segments
1976 .iter()
1977 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
1978 .count();
1979
1980 if declared != actual {
1981 Some(
1982 ValidationIssue::new(
1983 Severity::Error,
1984 IssueKind::UntSegmentCountMismatch { declared, actual },
1985 )
1986 .with_field_path("UNT/0074")
1987 .with_expected(actual.to_string())
1988 .with_actual(declared.to_string()),
1989 )
1990 } else {
1991 None
1992 }
1993}
1994
1995#[cfg(test)]
1996mod tests {
1997 use super::super::codes::ErrorCodes;
1998 use super::super::issue::ValidationCategory;
1999 use super::*;
2000 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2001 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2002 use std::collections::HashMap;
2003
2004 fn narrate(issue: &ValidationIssue) -> String {
2007 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2008 }
2009
2010 struct MockEvaluator {
2012 results: HashMap<u32, CR>,
2013 }
2014
2015 impl MockEvaluator {
2016 fn new(results: Vec<(u32, CR)>) -> Self {
2017 Self {
2018 results: results.into_iter().collect(),
2019 }
2020 }
2021
2022 fn all_true(ids: &[u32]) -> Self {
2023 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2024 }
2025 }
2026
2027 impl ConditionEvaluator for MockEvaluator {
2028 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2029 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2030 }
2031 fn is_external(&self, _condition: u32) -> bool {
2032 false
2033 }
2034 fn message_type(&self) -> &str {
2035 "UTILMD"
2036 }
2037 fn format_version(&self) -> &str {
2038 "FV2510"
2039 }
2040 }
2041
2042 #[test]
2045 fn a_present_code_is_judged_by_its_own_status() {
2046 let code = |value: &str, status: &str| AhbCodeRule {
2048 value: value.to_string(),
2049 description: String::new(),
2050 ahb_status: status.to_string(),
2051 };
2052 let field = AhbFieldRule {
2053 segment_path: "SG4/STS/C556/9013".to_string(),
2054 ahb_status: "X [192]".to_string(),
2055 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2056 ..Default::default()
2057 };
2058 assert_eq!(governing_status(&field, Some("E03")), "X");
2059 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2060 assert_eq!(governing_status(&field, None), "X [192]");
2061 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2062
2063 let own = AhbFieldRule {
2065 ahb_status: "X [931]".to_string(),
2066 codes: vec![code("303", "X")],
2067 ..Default::default()
2068 };
2069 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2070 }
2071
2072 #[test]
2073 fn test_extract_segment_id_simple() {
2074 assert_eq!(extract_segment_id("NAD"), "NAD");
2075 }
2076
2077 #[test]
2078 fn test_extract_segment_id_with_sg_prefix() {
2079 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
2080 }
2081
2082 #[test]
2083 fn test_extract_segment_id_nested_sg() {
2084 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
2085 }
2086
2087 #[test]
2090 fn test_validate_missing_mandatory_field() {
2091 let evaluator = MockEvaluator::all_true(&[182, 152]);
2092 let validator = EdifactValidator::new(evaluator);
2093 let external = NoOpExternalProvider;
2094
2095 let workflow = AhbWorkflow {
2096 pruefidentifikator: "11001".to_string(),
2097 description: "Test".to_string(),
2098 communication_direction: None,
2099 fields: vec![AhbFieldRule {
2100 segment_path: "SG2/NAD/C082/3039".to_string(),
2101 name: "MP-ID des MSB".to_string(),
2102 ahb_status: "Muss [182] ∧ [152]".to_string(),
2103 codes: vec![],
2104 parent_group_ahb_status: None,
2105 segment_ahb_status: None,
2106 ..Default::default()
2107 }],
2108 ub_definitions: BTreeMap::new(),
2109 };
2110
2111 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2113
2114 assert!(!report.is_valid());
2116 let errors: Vec<_> = report.errors().collect();
2117 assert_eq!(errors.len(), 1);
2118 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2119 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
2120 }
2121
2122 #[test]
2123 fn test_validate_condition_false_no_error() {
2124 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
2126 let validator = EdifactValidator::new(evaluator);
2127 let external = NoOpExternalProvider;
2128
2129 let workflow = AhbWorkflow {
2130 pruefidentifikator: "11001".to_string(),
2131 description: "Test".to_string(),
2132 communication_direction: None,
2133 fields: vec![AhbFieldRule {
2134 segment_path: "NAD".to_string(),
2135 name: "Partnerrolle".to_string(),
2136 ahb_status: "Muss [182] ∧ [152]".to_string(),
2137 codes: vec![],
2138 parent_group_ahb_status: None,
2139 segment_ahb_status: None,
2140 ..Default::default()
2141 }],
2142 ub_definitions: BTreeMap::new(),
2143 };
2144
2145 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2146
2147 assert!(report.is_valid());
2149 }
2150
2151 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
2153 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
2154 let workflow = AhbWorkflow {
2155 pruefidentifikator: "11001".to_string(),
2156 description: "Test".to_string(),
2157 communication_direction: None,
2158 fields: vec![AhbFieldRule {
2159 segment_path: "SG2/NAD/C082/3039".to_string(),
2160 name: "MP-ID".to_string(),
2161 ahb_status: status.to_string(),
2162 ..Default::default()
2163 }],
2164 ub_definitions: BTreeMap::new(),
2165 };
2166 validator.validate(
2167 &[],
2168 &workflow,
2169 &NoOpExternalProvider,
2170 ValidationLevel::Conditions,
2171 )
2172 }
2173
2174 #[test]
2175 fn a_later_soll_line_alone_does_not_require_the_field() {
2176 let report = validate_absent_field(
2178 "Muss [2119] \r\nSoll [130]",
2179 vec![(2119, CR::False), (130, CR::True)],
2180 );
2181 assert!(
2182 report.is_valid(),
2183 "{:?}",
2184 report.errors().collect::<Vec<_>>()
2185 );
2186 }
2187
2188 #[test]
2189 fn a_later_muss_line_requires_the_field() {
2190 let report = validate_absent_field(
2192 "Soll [47]\r\nMuss [46]",
2193 vec![(47, CR::False), (46, CR::True)],
2194 );
2195 let errors: Vec<_> = report.errors().collect();
2196 assert_eq!(errors.len(), 1);
2197 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2198 }
2199
2200 #[test]
2201 fn a_weaker_unknown_line_does_not_stop_a_muss() {
2202 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
2204 let errors: Vec<_> = report.errors().collect();
2205 assert_eq!(errors.len(), 1);
2206 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2207 }
2208
2209 #[test]
2210 fn a_stricter_unknown_line_leaves_the_status_undecided() {
2211 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
2212 assert!(report.is_valid());
2213 let infos: Vec<_> = report.infos().collect();
2214 assert_eq!(infos.len(), 1, "{infos:?}");
2215 match &infos[0].kind {
2216 IssueKind::ConditionUnknown { unresolved, .. } => {
2217 assert_eq!(unresolved.missing, vec![2119]);
2218 }
2219 other => panic!("expected ConditionUnknown, got {other:?}"),
2220 }
2221 }
2222
2223 #[test]
2224 fn test_validate_condition_unknown_adds_info() {
2225 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
2227 let validator = EdifactValidator::new(evaluator);
2229 let external = NoOpExternalProvider;
2230
2231 let workflow = AhbWorkflow {
2232 pruefidentifikator: "11001".to_string(),
2233 description: "Test".to_string(),
2234 communication_direction: None,
2235 fields: vec![AhbFieldRule {
2236 segment_path: "NAD".to_string(),
2237 name: "Partnerrolle".to_string(),
2238 ahb_status: "Muss [182] ∧ [152]".to_string(),
2239 codes: vec![],
2240 parent_group_ahb_status: None,
2241 segment_ahb_status: None,
2242 ..Default::default()
2243 }],
2244 ub_definitions: BTreeMap::new(),
2245 };
2246
2247 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2248
2249 assert!(report.is_valid());
2251 let infos: Vec<_> = report.infos().collect();
2252 assert_eq!(infos.len(), 1);
2253 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
2254 }
2255
2256 #[test]
2257 fn test_validate_structure_level_skips_conditions() {
2258 let evaluator = MockEvaluator::all_true(&[182, 152]);
2259 let validator = EdifactValidator::new(evaluator);
2260 let external = NoOpExternalProvider;
2261
2262 let workflow = AhbWorkflow {
2263 pruefidentifikator: "11001".to_string(),
2264 description: "Test".to_string(),
2265 communication_direction: None,
2266 fields: vec![AhbFieldRule {
2267 segment_path: "NAD".to_string(),
2268 name: "Partnerrolle".to_string(),
2269 ahb_status: "Muss [182] ∧ [152]".to_string(),
2270 codes: vec![],
2271 parent_group_ahb_status: None,
2272 segment_ahb_status: None,
2273 ..Default::default()
2274 }],
2275 ub_definitions: BTreeMap::new(),
2276 };
2277
2278 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
2280
2281 assert!(report.is_valid());
2283 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
2284 }
2285
2286 #[test]
2287 fn test_validate_empty_workflow_no_condition_errors() {
2288 let evaluator = MockEvaluator::all_true(&[]);
2289 let validator = EdifactValidator::new(evaluator);
2290 let external = NoOpExternalProvider;
2291
2292 let empty_workflow = AhbWorkflow {
2293 pruefidentifikator: String::new(),
2294 description: String::new(),
2295 communication_direction: None,
2296 fields: vec![],
2297 ub_definitions: BTreeMap::new(),
2298 };
2299
2300 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
2301
2302 assert!(report.is_valid());
2303 }
2304
2305 #[test]
2306 fn test_validate_bare_muss_always_required() {
2307 let evaluator = MockEvaluator::new(vec![]);
2308 let validator = EdifactValidator::new(evaluator);
2309 let external = NoOpExternalProvider;
2310
2311 let workflow = AhbWorkflow {
2312 pruefidentifikator: "55001".to_string(),
2313 description: "Test".to_string(),
2314 communication_direction: Some("NB an LF".to_string()),
2315 fields: vec![AhbFieldRule {
2316 segment_path: "SG2/NAD/3035".to_string(),
2317 name: "Partnerrolle".to_string(),
2318 ahb_status: "Muss".to_string(), codes: vec![],
2320 parent_group_ahb_status: None,
2321 segment_ahb_status: None,
2322 ..Default::default()
2323 }],
2324 ub_definitions: BTreeMap::new(),
2325 };
2326
2327 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2328
2329 assert!(!report.is_valid());
2331 assert_eq!(report.error_count(), 1);
2332 }
2333
2334 #[test]
2335 fn test_validate_x_status_is_mandatory() {
2336 let evaluator = MockEvaluator::new(vec![]);
2337 let validator = EdifactValidator::new(evaluator);
2338 let external = NoOpExternalProvider;
2339
2340 let workflow = AhbWorkflow {
2341 pruefidentifikator: "55001".to_string(),
2342 description: "Test".to_string(),
2343 communication_direction: None,
2344 fields: vec![AhbFieldRule {
2345 segment_path: "DTM".to_string(),
2346 name: "Datum".to_string(),
2347 ahb_status: "X".to_string(),
2348 codes: vec![],
2349 parent_group_ahb_status: None,
2350 segment_ahb_status: None,
2351 ..Default::default()
2352 }],
2353 ub_definitions: BTreeMap::new(),
2354 };
2355
2356 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2357
2358 assert!(!report.is_valid());
2359 let errors: Vec<_> = report.errors().collect();
2360 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2361 }
2362
2363 #[test]
2364 fn test_validate_soll_not_mandatory() {
2365 let evaluator = MockEvaluator::new(vec![]);
2366 let validator = EdifactValidator::new(evaluator);
2367 let external = NoOpExternalProvider;
2368
2369 let workflow = AhbWorkflow {
2370 pruefidentifikator: "55001".to_string(),
2371 description: "Test".to_string(),
2372 communication_direction: None,
2373 fields: vec![AhbFieldRule {
2374 segment_path: "DTM".to_string(),
2375 name: "Datum".to_string(),
2376 ahb_status: "Soll".to_string(),
2377 codes: vec![],
2378 parent_group_ahb_status: None,
2379 segment_ahb_status: None,
2380 ..Default::default()
2381 }],
2382 ub_definitions: BTreeMap::new(),
2383 };
2384
2385 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2386
2387 assert!(report.is_valid());
2389 }
2390
2391 #[test]
2392 fn test_report_includes_metadata() {
2393 let evaluator = MockEvaluator::new(vec![]);
2394 let validator = EdifactValidator::new(evaluator);
2395 let external = NoOpExternalProvider;
2396
2397 let workflow = AhbWorkflow {
2398 pruefidentifikator: "55001".to_string(),
2399 description: String::new(),
2400 communication_direction: None,
2401 fields: vec![],
2402 ub_definitions: BTreeMap::new(),
2403 };
2404
2405 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
2406
2407 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
2408 assert_eq!(report.level, ValidationLevel::Full);
2409 assert_eq!(report.message_type, "UTILMD");
2410 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
2411 }
2412
2413 #[test]
2414 fn test_validate_with_navigator_returns_report() {
2415 let evaluator = MockEvaluator::all_true(&[]);
2416 let validator = EdifactValidator::new(evaluator);
2417 let external = NoOpExternalProvider;
2418 let nav = crate::eval::NoOpGroupNavigator;
2419
2420 let workflow = AhbWorkflow {
2421 pruefidentifikator: "55001".to_string(),
2422 description: "Test".to_string(),
2423 communication_direction: None,
2424 fields: vec![],
2425 ub_definitions: BTreeMap::new(),
2426 };
2427
2428 let report = validator.validate_with_navigator(
2429 &[],
2430 &workflow,
2431 &external,
2432 ValidationLevel::Full,
2433 &nav,
2434 );
2435 assert!(report.is_valid());
2436 }
2437
2438 #[test]
2439 fn test_code_validation_composite_paths_valid_codes() {
2440 let evaluator = MockEvaluator::new(vec![]);
2444 let validator = EdifactValidator::new(evaluator);
2445 let external = NoOpExternalProvider;
2446
2447 let unh_segment = OwnedSegment {
2448 id: "UNH".to_string(),
2449 elements: vec![
2450 vec!["ALEXANDE951842".to_string()],
2451 vec![
2452 "UTILMD".to_string(),
2453 "D".to_string(),
2454 "11A".to_string(),
2455 "UN".to_string(),
2456 "S2.1".to_string(),
2457 ],
2458 ],
2459 segment_number: 1,
2460 };
2461
2462 let workflow = AhbWorkflow {
2463 pruefidentifikator: "55001".to_string(),
2464 description: "Test".to_string(),
2465 communication_direction: None,
2466 fields: vec![
2467 AhbFieldRule {
2468 segment_path: "UNH/S009/0065".to_string(),
2469 name: "Nachrichtentyp".to_string(),
2470 ahb_status: "X".to_string(),
2471 codes: vec![AhbCodeRule {
2472 value: "UTILMD".to_string(),
2473 description: "Stammdaten".to_string(),
2474 ahb_status: "X".to_string(),
2475 }],
2476 parent_group_ahb_status: None,
2477 segment_ahb_status: None,
2478 element_index: Some(1),
2479 component_index: Some(0),
2480 ..Default::default()
2481 },
2482 AhbFieldRule {
2483 segment_path: "UNH/S009/0052".to_string(),
2484 name: "Version".to_string(),
2485 ahb_status: "X".to_string(),
2486 codes: vec![AhbCodeRule {
2487 value: "D".to_string(),
2488 description: "Draft".to_string(),
2489 ahb_status: "X".to_string(),
2490 }],
2491 parent_group_ahb_status: None,
2492 segment_ahb_status: None,
2493 element_index: Some(1),
2494 component_index: Some(1),
2495 ..Default::default()
2496 },
2497 ],
2498 ub_definitions: BTreeMap::new(),
2499 };
2500
2501 let report = validator.validate(
2502 &[unh_segment],
2503 &workflow,
2504 &external,
2505 ValidationLevel::Conditions,
2506 );
2507
2508 let code_errors: Vec<_> = report
2509 .by_category(ValidationCategory::Code)
2510 .filter(|i| i.severity == Severity::Error)
2511 .collect();
2512 assert!(
2513 code_errors.is_empty(),
2514 "Expected no code errors when composite values match allowed codes, got: {:?}",
2515 code_errors
2516 );
2517 }
2518
2519 #[test]
2520 fn test_code_validation_partitions_by_mig_number() {
2521 let evaluator = MockEvaluator::new(vec![]);
2525 let validator = EdifactValidator::new(evaluator);
2526 let external = NoOpExternalProvider;
2527
2528 let sts_7 = OwnedSegment {
2529 id: "STS".to_string(),
2530 elements: vec![
2531 vec!["7".to_string()],
2532 vec![String::new()],
2533 vec!["GH02".to_string()],
2534 vec!["ZW4".to_string()],
2535 ],
2536 segment_number: 1,
2537 };
2538 let sts_e01 = OwnedSegment {
2539 id: "STS".to_string(),
2540 elements: vec![
2541 vec!["E01".to_string()],
2542 vec![String::new()],
2543 vec!["A99".to_string(), "E_0614".to_string()],
2544 ],
2545 segment_number: 2,
2546 };
2547
2548 let workflow = AhbWorkflow {
2549 pruefidentifikator: "55018".to_string(),
2550 description: "Test".to_string(),
2551 communication_direction: None,
2552 fields: vec![
2553 AhbFieldRule {
2555 segment_path: "SG4/STS/C601/9015".to_string(),
2556 name: "Statuskategorie".to_string(),
2557 ahb_status: "X".to_string(),
2558 codes: vec![AhbCodeRule {
2559 value: "7".to_string(),
2560 description: "Transaktionsgrund".to_string(),
2561 ahb_status: "X".to_string(),
2562 }],
2563 parent_group_ahb_status: None,
2564 segment_ahb_status: None,
2565 element_index: Some(0),
2566 component_index: Some(0),
2567 mig_number: Some("00035".to_string()),
2568 format: None,
2569 },
2570 AhbFieldRule {
2571 segment_path: "SG4/STS/C556/9013".to_string(),
2572 name: "Statusanlaß".to_string(),
2573 ahb_status: "X".to_string(),
2574 codes: vec![AhbCodeRule {
2575 value: "E03".to_string(),
2576 description: "Transaktionsgrund".to_string(),
2577 ahb_status: "X".to_string(),
2578 }],
2579 parent_group_ahb_status: None,
2580 segment_ahb_status: None,
2581 element_index: Some(2),
2582 component_index: Some(0),
2583 mig_number: Some("00035".to_string()),
2584 format: None,
2585 },
2586 AhbFieldRule {
2588 segment_path: "SG4/STS/C601/9015".to_string(),
2589 name: "Statuskategorie".to_string(),
2590 ahb_status: "X".to_string(),
2591 codes: vec![AhbCodeRule {
2592 value: "E01".to_string(),
2593 description: "Antwort".to_string(),
2594 ahb_status: "X".to_string(),
2595 }],
2596 parent_group_ahb_status: None,
2597 segment_ahb_status: None,
2598 element_index: Some(0),
2599 component_index: Some(0),
2600 mig_number: Some("00036".to_string()),
2601 format: None,
2602 },
2603 ],
2604 ub_definitions: BTreeMap::new(),
2605 };
2606
2607 let report = validator.validate(
2608 &[sts_7, sts_e01],
2609 &workflow,
2610 &external,
2611 ValidationLevel::Conditions,
2612 );
2613
2614 let code_errors: Vec<_> = report
2615 .by_category(ValidationCategory::Code)
2616 .filter(|i| {
2617 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2618 })
2619 .collect();
2620 assert_eq!(
2621 code_errors.len(),
2622 1,
2623 "Expected one COD002 (for GH02 only), got: {:?}",
2624 code_errors
2625 );
2626 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2627 }
2628
2629 #[test]
2630 fn test_code_validation_composite_paths_detects_invalid_code() {
2631 let evaluator = MockEvaluator::new(vec![]);
2634 let validator = EdifactValidator::new(evaluator);
2635 let external = NoOpExternalProvider;
2636
2637 let sts_segment = OwnedSegment {
2638 id: "STS".to_string(),
2639 elements: vec![
2640 vec!["7".to_string()],
2641 vec![String::new()],
2642 vec!["GH02".to_string()],
2643 vec!["ZW4".to_string()],
2644 ],
2645 segment_number: 1,
2646 };
2647
2648 let workflow = AhbWorkflow {
2649 pruefidentifikator: "55018".to_string(),
2650 description: "Test".to_string(),
2651 communication_direction: None,
2652 fields: vec![AhbFieldRule {
2653 segment_path: "SG4/STS/C556/9013".to_string(),
2654 name: "Statusanlaß".to_string(),
2655 ahb_status: "X".to_string(),
2656 codes: vec![AhbCodeRule {
2657 value: "E03".to_string(),
2658 description: "Transaktionsgrund".to_string(),
2659 ahb_status: "X".to_string(),
2660 }],
2661 parent_group_ahb_status: None,
2662 segment_ahb_status: None,
2663 element_index: Some(2),
2664 component_index: Some(0),
2665 ..Default::default()
2666 }],
2667 ub_definitions: BTreeMap::new(),
2668 };
2669
2670 let report = validator.validate(
2671 &[sts_segment],
2672 &workflow,
2673 &external,
2674 ValidationLevel::Conditions,
2675 );
2676
2677 let code_errors: Vec<_> = report
2678 .by_category(ValidationCategory::Code)
2679 .filter(|i| {
2680 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2681 })
2682 .collect();
2683 assert_eq!(
2684 code_errors.len(),
2685 1,
2686 "Expected COD002 for GH02, got: {:?}",
2687 code_errors
2688 );
2689 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2690 }
2691
2692 #[test]
2693 fn test_cross_field_code_validation_valid_qualifiers() {
2694 let evaluator = MockEvaluator::new(vec![]);
2697 let validator = EdifactValidator::new(evaluator);
2698 let external = NoOpExternalProvider;
2699
2700 let nad_ms = OwnedSegment {
2701 id: "NAD".to_string(),
2702 elements: vec![vec!["MS".to_string()]],
2703 segment_number: 4,
2704 };
2705 let nad_mr = OwnedSegment {
2706 id: "NAD".to_string(),
2707 elements: vec![vec!["MR".to_string()]],
2708 segment_number: 5,
2709 };
2710
2711 let workflow = AhbWorkflow {
2712 pruefidentifikator: "55001".to_string(),
2713 description: "Test".to_string(),
2714 communication_direction: None,
2715 fields: vec![
2716 AhbFieldRule {
2717 segment_path: "SG2/NAD/3035".to_string(),
2718 name: "Absender".to_string(),
2719 ahb_status: "X".to_string(),
2720 codes: vec![AhbCodeRule {
2721 value: "MS".to_string(),
2722 description: "Absender".to_string(),
2723 ahb_status: "X".to_string(),
2724 }],
2725 parent_group_ahb_status: None,
2726 segment_ahb_status: None,
2727 ..Default::default()
2728 },
2729 AhbFieldRule {
2730 segment_path: "SG2/NAD/3035".to_string(),
2731 name: "Empfaenger".to_string(),
2732 ahb_status: "X".to_string(),
2733 codes: vec![AhbCodeRule {
2734 value: "MR".to_string(),
2735 description: "Empfaenger".to_string(),
2736 ahb_status: "X".to_string(),
2737 }],
2738 parent_group_ahb_status: None,
2739 segment_ahb_status: None,
2740 ..Default::default()
2741 },
2742 ],
2743 ub_definitions: BTreeMap::new(),
2744 };
2745
2746 let report = validator.validate(
2747 &[nad_ms, nad_mr],
2748 &workflow,
2749 &external,
2750 ValidationLevel::Conditions,
2751 );
2752
2753 let code_errors: Vec<_> = report
2754 .by_category(ValidationCategory::Code)
2755 .filter(|i| i.severity == Severity::Error)
2756 .collect();
2757 assert!(
2758 code_errors.is_empty(),
2759 "Expected no code errors for valid qualifiers, got: {:?}",
2760 code_errors
2761 );
2762 }
2763
2764 #[test]
2765 fn test_cross_field_code_validation_catches_invalid_qualifier() {
2766 let evaluator = MockEvaluator::new(vec![]);
2768 let validator = EdifactValidator::new(evaluator);
2769 let external = NoOpExternalProvider;
2770
2771 let nad_ms = OwnedSegment {
2772 id: "NAD".to_string(),
2773 elements: vec![vec!["MS".to_string()]],
2774 segment_number: 4,
2775 };
2776 let nad_mt = OwnedSegment {
2777 id: "NAD".to_string(),
2778 elements: vec![vec!["MT".to_string()]], segment_number: 5,
2780 };
2781
2782 let workflow = AhbWorkflow {
2783 pruefidentifikator: "55001".to_string(),
2784 description: "Test".to_string(),
2785 communication_direction: None,
2786 fields: vec![
2787 AhbFieldRule {
2788 segment_path: "SG2/NAD/3035".to_string(),
2789 name: "Absender".to_string(),
2790 ahb_status: "X".to_string(),
2791 codes: vec![AhbCodeRule {
2792 value: "MS".to_string(),
2793 description: "Absender".to_string(),
2794 ahb_status: "X".to_string(),
2795 }],
2796 parent_group_ahb_status: None,
2797 segment_ahb_status: None,
2798 ..Default::default()
2799 },
2800 AhbFieldRule {
2801 segment_path: "SG2/NAD/3035".to_string(),
2802 name: "Empfaenger".to_string(),
2803 ahb_status: "X".to_string(),
2804 codes: vec![AhbCodeRule {
2805 value: "MR".to_string(),
2806 description: "Empfaenger".to_string(),
2807 ahb_status: "X".to_string(),
2808 }],
2809 parent_group_ahb_status: None,
2810 segment_ahb_status: None,
2811 ..Default::default()
2812 },
2813 ],
2814 ub_definitions: BTreeMap::new(),
2815 };
2816
2817 let report = validator.validate(
2818 &[nad_ms, nad_mt],
2819 &workflow,
2820 &external,
2821 ValidationLevel::Conditions,
2822 );
2823
2824 let code_errors: Vec<_> = report
2825 .by_category(ValidationCategory::Code)
2826 .filter(|i| i.severity == Severity::Error)
2827 .collect();
2828 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
2829 assert!(narrate(code_errors[0]).contains("MT"));
2830 assert!(narrate(code_errors[0]).contains("MR"));
2831 assert!(narrate(code_errors[0]).contains("MS"));
2832 }
2833
2834 #[test]
2835 fn test_cross_field_code_validation_unions_across_groups() {
2836 let evaluator = MockEvaluator::new(vec![]);
2840 let validator = EdifactValidator::new(evaluator);
2841 let external = NoOpExternalProvider;
2842
2843 let segments = vec![
2844 OwnedSegment {
2845 id: "NAD".to_string(),
2846 elements: vec![vec!["MS".to_string()]],
2847 segment_number: 3,
2848 },
2849 OwnedSegment {
2850 id: "NAD".to_string(),
2851 elements: vec![vec!["MR".to_string()]],
2852 segment_number: 4,
2853 },
2854 OwnedSegment {
2855 id: "NAD".to_string(),
2856 elements: vec![vec!["Z04".to_string()]],
2857 segment_number: 20,
2858 },
2859 OwnedSegment {
2860 id: "NAD".to_string(),
2861 elements: vec![vec!["Z09".to_string()]],
2862 segment_number: 21,
2863 },
2864 OwnedSegment {
2865 id: "NAD".to_string(),
2866 elements: vec![vec!["MT".to_string()]], segment_number: 22,
2868 },
2869 ];
2870
2871 let workflow = AhbWorkflow {
2872 pruefidentifikator: "55001".to_string(),
2873 description: "Test".to_string(),
2874 communication_direction: None,
2875 fields: vec![
2876 AhbFieldRule {
2877 segment_path: "SG2/NAD/3035".to_string(),
2878 name: "Absender".to_string(),
2879 ahb_status: "X".to_string(),
2880 codes: vec![AhbCodeRule {
2881 value: "MS".to_string(),
2882 description: "Absender".to_string(),
2883 ahb_status: "X".to_string(),
2884 }],
2885 parent_group_ahb_status: None,
2886 segment_ahb_status: None,
2887 ..Default::default()
2888 },
2889 AhbFieldRule {
2890 segment_path: "SG2/NAD/3035".to_string(),
2891 name: "Empfaenger".to_string(),
2892 ahb_status: "X".to_string(),
2893 codes: vec![AhbCodeRule {
2894 value: "MR".to_string(),
2895 description: "Empfaenger".to_string(),
2896 ahb_status: "X".to_string(),
2897 }],
2898 parent_group_ahb_status: None,
2899 segment_ahb_status: None,
2900 ..Default::default()
2901 },
2902 AhbFieldRule {
2903 segment_path: "SG4/SG12/NAD/3035".to_string(),
2904 name: "Anschlussnutzer".to_string(),
2905 ahb_status: "X".to_string(),
2906 codes: vec![AhbCodeRule {
2907 value: "Z04".to_string(),
2908 description: "Anschlussnutzer".to_string(),
2909 ahb_status: "X".to_string(),
2910 }],
2911 parent_group_ahb_status: None,
2912 segment_ahb_status: None,
2913 ..Default::default()
2914 },
2915 AhbFieldRule {
2916 segment_path: "SG4/SG12/NAD/3035".to_string(),
2917 name: "Korrespondenzanschrift".to_string(),
2918 ahb_status: "X".to_string(),
2919 codes: vec![AhbCodeRule {
2920 value: "Z09".to_string(),
2921 description: "Korrespondenzanschrift".to_string(),
2922 ahb_status: "X".to_string(),
2923 }],
2924 parent_group_ahb_status: None,
2925 segment_ahb_status: None,
2926 ..Default::default()
2927 },
2928 ],
2929 ub_definitions: BTreeMap::new(),
2930 };
2931
2932 let report =
2933 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
2934
2935 let code_errors: Vec<_> = report
2936 .by_category(ValidationCategory::Code)
2937 .filter(|i| i.severity == Severity::Error)
2938 .collect();
2939 assert_eq!(
2940 code_errors.len(),
2941 1,
2942 "Expected exactly one COD002 error for MT, got: {:?}",
2943 code_errors
2944 );
2945 assert!(narrate(code_errors[0]).contains("MT"));
2946 }
2947
2948 #[test]
2949 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
2950 let evaluator = MockEvaluator::new(vec![]);
2959 let validator = EdifactValidator::new(evaluator);
2960 let external = NoOpExternalProvider;
2961
2962 let qty_67 = OwnedSegment {
2963 id: "QTY".to_string(),
2964 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
2965 segment_number: 10,
2966 };
2967
2968 let workflow = AhbWorkflow {
2969 pruefidentifikator: "13025".to_string(),
2970 description: "Test".to_string(),
2971 communication_direction: None,
2972 fields: vec![AhbFieldRule {
2973 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
2974 name: "Menge, Qualifier".to_string(),
2975 ahb_status: "X".to_string(),
2976 codes: vec![
2977 AhbCodeRule {
2978 value: "220".to_string(),
2979 description: "Wahrer Wert".to_string(),
2980 ahb_status: "X".to_string(),
2981 },
2982 AhbCodeRule {
2983 value: "67".to_string(),
2984 description: "Ersatzwert".to_string(),
2985 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
2986 },
2987 AhbCodeRule {
2988 value: "Z18".to_string(),
2989 description: "Vorläufiger Wert".to_string(),
2990 ahb_status: "X [35]".to_string(),
2991 },
2992 ],
2993 parent_group_ahb_status: None,
2994 segment_ahb_status: None,
2995 element_index: Some(0),
2996 component_index: Some(0),
2997 ..Default::default()
2998 }],
2999 ub_definitions: BTreeMap::new(),
3000 };
3001
3002 let report =
3003 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3004
3005 let code_errors: Vec<_> = report
3006 .by_category(ValidationCategory::Code)
3007 .filter(|i| i.severity == Severity::Error)
3008 .collect();
3009 assert!(
3010 code_errors.is_empty(),
3011 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3012 code_errors
3013 );
3014 }
3015
3016 #[test]
3017 fn test_is_qualifier_field_simple_paths() {
3018 assert!(is_qualifier_field("NAD/3035"));
3019 assert!(is_qualifier_field("SG2/NAD/3035"));
3020 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3021 assert!(is_qualifier_field("LOC/3227"));
3022 }
3023
3024 #[test]
3025 fn test_is_qualifier_field_composite_paths() {
3026 assert!(is_qualifier_field("UNH/S009/0065"));
3030 assert!(is_qualifier_field("NAD/C082/3039"));
3031 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3032 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3033 }
3034
3035 #[test]
3036 fn test_is_qualifier_field_bare_segment() {
3037 assert!(!is_qualifier_field("NAD"));
3038 assert!(!is_qualifier_field("SG2/NAD"));
3039 }
3040
3041 #[test]
3042 fn test_is_qualifier_field_rejects_deep_paths() {
3043 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3045 }
3046
3047 #[test]
3048 fn test_missing_qualifier_instance_is_detected() {
3049 let evaluator = MockEvaluator::new(vec![]);
3052 let validator = EdifactValidator::new(evaluator);
3053 let external = NoOpExternalProvider;
3054
3055 let nad_ms = OwnedSegment {
3056 id: "NAD".to_string(),
3057 elements: vec![vec!["MS".to_string()]],
3058 segment_number: 3,
3059 };
3060
3061 let workflow = AhbWorkflow {
3062 pruefidentifikator: "55001".to_string(),
3063 description: "Test".to_string(),
3064 communication_direction: None,
3065 fields: vec![
3066 AhbFieldRule {
3067 segment_path: "SG2/NAD/3035".to_string(),
3068 name: "Absender".to_string(),
3069 ahb_status: "X".to_string(),
3070 codes: vec![AhbCodeRule {
3071 value: "MS".to_string(),
3072 description: "Absender".to_string(),
3073 ahb_status: "X".to_string(),
3074 }],
3075 parent_group_ahb_status: None,
3076 segment_ahb_status: None,
3077 ..Default::default()
3078 },
3079 AhbFieldRule {
3080 segment_path: "SG2/NAD/3035".to_string(),
3081 name: "Empfaenger".to_string(),
3082 ahb_status: "Muss".to_string(),
3083 codes: vec![AhbCodeRule {
3084 value: "MR".to_string(),
3085 description: "Empfaenger".to_string(),
3086 ahb_status: "X".to_string(),
3087 }],
3088 parent_group_ahb_status: None,
3089 segment_ahb_status: None,
3090 ..Default::default()
3091 },
3092 ],
3093 ub_definitions: BTreeMap::new(),
3094 };
3095
3096 let report =
3097 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
3098
3099 let ahb_errors: Vec<_> = report
3100 .by_category(ValidationCategory::Ahb)
3101 .filter(|i| i.severity == Severity::Error)
3102 .collect();
3103 assert_eq!(
3104 ahb_errors.len(),
3105 1,
3106 "Expected AHB001 for missing NAD+MR, got: {:?}",
3107 ahb_errors
3108 );
3109 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3110 }
3111
3112 #[test]
3113 fn test_present_qualifier_instance_no_error() {
3114 let evaluator = MockEvaluator::new(vec![]);
3116 let validator = EdifactValidator::new(evaluator);
3117 let external = NoOpExternalProvider;
3118
3119 let segments = vec![
3120 OwnedSegment {
3121 id: "NAD".to_string(),
3122 elements: vec![vec!["MS".to_string()]],
3123 segment_number: 3,
3124 },
3125 OwnedSegment {
3126 id: "NAD".to_string(),
3127 elements: vec![vec!["MR".to_string()]],
3128 segment_number: 4,
3129 },
3130 ];
3131
3132 let workflow = AhbWorkflow {
3133 pruefidentifikator: "55001".to_string(),
3134 description: "Test".to_string(),
3135 communication_direction: None,
3136 fields: vec![
3137 AhbFieldRule {
3138 segment_path: "SG2/NAD/3035".to_string(),
3139 name: "Absender".to_string(),
3140 ahb_status: "Muss".to_string(),
3141 codes: vec![AhbCodeRule {
3142 value: "MS".to_string(),
3143 description: "Absender".to_string(),
3144 ahb_status: "X".to_string(),
3145 }],
3146 parent_group_ahb_status: None,
3147 segment_ahb_status: None,
3148 ..Default::default()
3149 },
3150 AhbFieldRule {
3151 segment_path: "SG2/NAD/3035".to_string(),
3152 name: "Empfaenger".to_string(),
3153 ahb_status: "Muss".to_string(),
3154 codes: vec![AhbCodeRule {
3155 value: "MR".to_string(),
3156 description: "Empfaenger".to_string(),
3157 ahb_status: "X".to_string(),
3158 }],
3159 parent_group_ahb_status: None,
3160 segment_ahb_status: None,
3161 ..Default::default()
3162 },
3163 ],
3164 ub_definitions: BTreeMap::new(),
3165 };
3166
3167 let report =
3168 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3169
3170 let ahb_errors: Vec<_> = report
3171 .by_category(ValidationCategory::Ahb)
3172 .filter(|i| i.severity == Severity::Error)
3173 .collect();
3174 assert!(
3175 ahb_errors.is_empty(),
3176 "Expected no AHB001 errors, got: {:?}",
3177 ahb_errors
3178 );
3179 }
3180
3181 #[test]
3182 fn test_extract_group_path_key() {
3183 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
3184 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
3185 assert_eq!(extract_group_path_key("NAD/3035"), "");
3186 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
3187 }
3188
3189 fn validate_field_of_absent_group(
3191 group_status: &str,
3192 conditions: Vec<(u32, CR)>,
3193 ) -> ValidationReport {
3194 use mig_types::navigator::GroupNavigator;
3195
3196 struct NoSg9;
3197 impl GroupNavigator for NoSg9 {
3198 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3199 vec![]
3200 }
3201 fn find_segments_with_qualifier_in_group(
3202 &self,
3203 _: &str,
3204 _: usize,
3205 _: &str,
3206 _: &[&str],
3207 _: usize,
3208 ) -> Vec<OwnedSegment> {
3209 vec![]
3210 }
3211 fn group_instance_count(&self, _: &[&str]) -> usize {
3212 0
3213 }
3214 }
3215
3216 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3217 let workflow = AhbWorkflow {
3218 pruefidentifikator: "44001".to_string(),
3219 description: "Test".to_string(),
3220 communication_direction: None,
3221 fields: vec![AhbFieldRule {
3222 segment_path: "SG9/QTY/C186/6060".to_string(),
3223 name: "Menge".to_string(),
3224 ahb_status: "X".to_string(),
3225 parent_group_ahb_status: Some(group_status.to_string()),
3226 ..Default::default()
3227 }],
3228 ub_definitions: BTreeMap::new(),
3229 };
3230 validator.validate_with_navigator(
3231 &[],
3232 &workflow,
3233 &NoOpExternalProvider,
3234 ValidationLevel::Conditions,
3235 &NoSg9,
3236 )
3237 }
3238
3239 #[test]
3240 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
3241 let report = validate_field_of_absent_group(
3243 "Soll [47]\r\nMuss [46]",
3244 vec![(47, CR::True), (46, CR::False)],
3245 );
3246 assert!(
3247 report.is_valid(),
3248 "{:?}",
3249 report.errors().collect::<Vec<_>>()
3250 );
3251 }
3252
3253 #[test]
3254 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
3255 let report = validate_field_of_absent_group(
3256 "Soll [47]\r\nMuss [46]",
3257 vec![(47, CR::False), (46, CR::True)],
3258 );
3259 let errors: Vec<_> = report.errors().collect();
3260 assert_eq!(errors.len(), 1, "{errors:?}");
3261 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3262 }
3263
3264 #[test]
3265 fn test_absent_optional_group_no_missing_field_error() {
3266 use mig_types::navigator::GroupNavigator;
3269
3270 struct NavWithoutSG3;
3271 impl GroupNavigator for NavWithoutSG3 {
3272 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3273 vec![]
3274 }
3275 fn find_segments_with_qualifier_in_group(
3276 &self,
3277 _: &str,
3278 _: usize,
3279 _: &str,
3280 _: &[&str],
3281 _: usize,
3282 ) -> Vec<OwnedSegment> {
3283 vec![]
3284 }
3285 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3286 match group_path {
3287 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
3290 }
3291 }
3292 }
3293
3294 let evaluator = MockEvaluator::new(vec![]);
3295 let validator = EdifactValidator::new(evaluator);
3296 let external = NoOpExternalProvider;
3297 let nav = NavWithoutSG3;
3298
3299 let segments = vec![
3301 OwnedSegment {
3302 id: "NAD".into(),
3303 elements: vec![vec!["MS".into()]],
3304 segment_number: 3,
3305 },
3306 OwnedSegment {
3307 id: "NAD".into(),
3308 elements: vec![vec!["MR".into()]],
3309 segment_number: 4,
3310 },
3311 ];
3312
3313 let workflow = AhbWorkflow {
3314 pruefidentifikator: "55001".to_string(),
3315 description: "Test".to_string(),
3316 communication_direction: None,
3317 fields: vec![
3318 AhbFieldRule {
3319 segment_path: "SG2/SG3/CTA/3139".to_string(),
3320 name: "Funktion des Ansprechpartners, Code".to_string(),
3321 ahb_status: "Muss".to_string(),
3322 codes: vec![],
3323 parent_group_ahb_status: None,
3324 segment_ahb_status: None,
3325 ..Default::default()
3326 },
3327 AhbFieldRule {
3328 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
3329 name: "Name vom Ansprechpartner".to_string(),
3330 ahb_status: "X".to_string(),
3331 codes: vec![],
3332 parent_group_ahb_status: None,
3333 segment_ahb_status: None,
3334 ..Default::default()
3335 },
3336 ],
3337 ub_definitions: BTreeMap::new(),
3338 };
3339
3340 let report = validator.validate_with_navigator(
3341 &segments,
3342 &workflow,
3343 &external,
3344 ValidationLevel::Conditions,
3345 &nav,
3346 );
3347
3348 let ahb_errors: Vec<_> = report
3349 .by_category(ValidationCategory::Ahb)
3350 .filter(|i| i.severity == Severity::Error)
3351 .collect();
3352 assert!(
3353 ahb_errors.is_empty(),
3354 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
3355 ahb_errors
3356 );
3357 }
3358
3359 #[test]
3360 fn test_present_group_still_checks_mandatory_fields() {
3361 use mig_types::navigator::GroupNavigator;
3363
3364 struct NavWithSG3;
3365 impl GroupNavigator for NavWithSG3 {
3366 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3367 vec![]
3368 }
3369 fn find_segments_with_qualifier_in_group(
3370 &self,
3371 _: &str,
3372 _: usize,
3373 _: &str,
3374 _: &[&str],
3375 _: usize,
3376 ) -> Vec<OwnedSegment> {
3377 vec![]
3378 }
3379 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3380 match group_path {
3381 ["SG2"] => 1,
3382 ["SG2", "SG3"] => 1, _ => 0,
3384 }
3385 }
3386 }
3387
3388 let evaluator = MockEvaluator::new(vec![]);
3389 let validator = EdifactValidator::new(evaluator);
3390 let external = NoOpExternalProvider;
3391 let nav = NavWithSG3;
3392
3393 let segments = vec![OwnedSegment {
3395 id: "NAD".into(),
3396 elements: vec![vec!["MS".into()]],
3397 segment_number: 3,
3398 }];
3399
3400 let workflow = AhbWorkflow {
3401 pruefidentifikator: "55001".to_string(),
3402 description: "Test".to_string(),
3403 communication_direction: None,
3404 fields: vec![AhbFieldRule {
3405 segment_path: "SG2/SG3/CTA/3139".to_string(),
3406 name: "Funktion des Ansprechpartners, Code".to_string(),
3407 ahb_status: "Muss".to_string(),
3408 codes: vec![],
3409 parent_group_ahb_status: None,
3410 segment_ahb_status: None,
3411 ..Default::default()
3412 }],
3413 ub_definitions: BTreeMap::new(),
3414 };
3415
3416 let report = validator.validate_with_navigator(
3417 &segments,
3418 &workflow,
3419 &external,
3420 ValidationLevel::Conditions,
3421 &nav,
3422 );
3423
3424 let ahb_errors: Vec<_> = report
3425 .by_category(ValidationCategory::Ahb)
3426 .filter(|i| i.severity == Severity::Error)
3427 .collect();
3428 assert_eq!(
3429 ahb_errors.len(),
3430 1,
3431 "Expected AHB001 error when SG3 is present but CTA missing"
3432 );
3433 assert!(narrate(ahb_errors[0]).contains("CTA"));
3434 }
3435
3436 #[test]
3437 fn test_missing_qualifier_with_navigator_is_detected() {
3438 use mig_types::navigator::GroupNavigator;
3441
3442 struct NavWithSG2;
3443 impl GroupNavigator for NavWithSG2 {
3444 fn find_segments_in_group(
3445 &self,
3446 segment_id: &str,
3447 group_path: &[&str],
3448 instance_index: usize,
3449 ) -> Vec<OwnedSegment> {
3450 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
3451 vec![OwnedSegment {
3452 id: "NAD".into(),
3453 elements: vec![vec!["MS".into()]],
3454 segment_number: 3,
3455 }]
3456 } else {
3457 vec![]
3458 }
3459 }
3460 fn find_segments_with_qualifier_in_group(
3461 &self,
3462 _: &str,
3463 _: usize,
3464 _: &str,
3465 _: &[&str],
3466 _: usize,
3467 ) -> Vec<OwnedSegment> {
3468 vec![]
3469 }
3470 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3471 match group_path {
3472 ["SG2"] => 1,
3473 _ => 0,
3474 }
3475 }
3476 }
3477
3478 let evaluator = MockEvaluator::new(vec![]);
3479 let validator = EdifactValidator::new(evaluator);
3480 let external = NoOpExternalProvider;
3481 let nav = NavWithSG2;
3482
3483 let segments = vec![OwnedSegment {
3484 id: "NAD".into(),
3485 elements: vec![vec!["MS".into()]],
3486 segment_number: 3,
3487 }];
3488
3489 let workflow = AhbWorkflow {
3490 pruefidentifikator: "55001".to_string(),
3491 description: "Test".to_string(),
3492 communication_direction: None,
3493 fields: vec![
3494 AhbFieldRule {
3495 segment_path: "SG2/NAD/3035".to_string(),
3496 name: "Absender".to_string(),
3497 ahb_status: "X".to_string(),
3498 codes: vec![AhbCodeRule {
3499 value: "MS".to_string(),
3500 description: "Absender".to_string(),
3501 ahb_status: "X".to_string(),
3502 }],
3503 parent_group_ahb_status: None,
3504 segment_ahb_status: None,
3505 ..Default::default()
3506 },
3507 AhbFieldRule {
3508 segment_path: "SG2/NAD/3035".to_string(),
3509 name: "Empfaenger".to_string(),
3510 ahb_status: "Muss".to_string(),
3511 codes: vec![AhbCodeRule {
3512 value: "MR".to_string(),
3513 description: "Empfaenger".to_string(),
3514 ahb_status: "X".to_string(),
3515 }],
3516 parent_group_ahb_status: None,
3517 segment_ahb_status: None,
3518 ..Default::default()
3519 },
3520 ],
3521 ub_definitions: BTreeMap::new(),
3522 };
3523
3524 let report = validator.validate_with_navigator(
3525 &segments,
3526 &workflow,
3527 &external,
3528 ValidationLevel::Conditions,
3529 &nav,
3530 );
3531
3532 let ahb_errors: Vec<_> = report
3533 .by_category(ValidationCategory::Ahb)
3534 .filter(|i| i.severity == Severity::Error)
3535 .collect();
3536 assert_eq!(
3537 ahb_errors.len(),
3538 1,
3539 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
3540 ahb_errors
3541 );
3542 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3543 }
3544
3545 #[test]
3546 fn test_optional_group_variant_absent_no_error() {
3547 use mig_types::navigator::GroupNavigator;
3552
3553 struct TestNav;
3554 impl GroupNavigator for TestNav {
3555 fn find_segments_in_group(
3556 &self,
3557 segment_id: &str,
3558 group_path: &[&str],
3559 instance_index: usize,
3560 ) -> Vec<OwnedSegment> {
3561 match (segment_id, group_path, instance_index) {
3562 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
3563 id: "LOC".into(),
3564 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3565 segment_number: 10,
3566 }],
3567 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
3568 id: "NAD".into(),
3569 elements: vec![vec!["MS".into()]],
3570 segment_number: 3,
3571 }],
3572 _ => vec![],
3573 }
3574 }
3575 fn find_segments_with_qualifier_in_group(
3576 &self,
3577 _: &str,
3578 _: usize,
3579 _: &str,
3580 _: &[&str],
3581 _: usize,
3582 ) -> Vec<OwnedSegment> {
3583 vec![]
3584 }
3585 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3586 match group_path {
3587 ["SG2"] => 1,
3588 ["SG4"] => 1,
3589 ["SG4", "SG5"] => 1, _ => 0,
3591 }
3592 }
3593 }
3594
3595 let evaluator = MockEvaluator::new(vec![]);
3596 let validator = EdifactValidator::new(evaluator);
3597 let external = NoOpExternalProvider;
3598 let nav = TestNav;
3599
3600 let segments = vec![
3601 OwnedSegment {
3602 id: "NAD".into(),
3603 elements: vec![vec!["MS".into()]],
3604 segment_number: 3,
3605 },
3606 OwnedSegment {
3607 id: "LOC".into(),
3608 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3609 segment_number: 10,
3610 },
3611 ];
3612
3613 let workflow = AhbWorkflow {
3614 pruefidentifikator: "55001".to_string(),
3615 description: "Test".to_string(),
3616 communication_direction: None,
3617 fields: vec![
3618 AhbFieldRule {
3620 segment_path: "SG2/NAD/3035".to_string(),
3621 name: "Absender".to_string(),
3622 ahb_status: "X".to_string(),
3623 codes: vec![AhbCodeRule {
3624 value: "MS".to_string(),
3625 description: "Absender".to_string(),
3626 ahb_status: "X".to_string(),
3627 }],
3628 parent_group_ahb_status: Some("Muss".to_string()),
3629 segment_ahb_status: None,
3630 ..Default::default()
3631 },
3632 AhbFieldRule {
3633 segment_path: "SG2/NAD/3035".to_string(),
3634 name: "Empfaenger".to_string(),
3635 ahb_status: "Muss".to_string(),
3636 codes: vec![AhbCodeRule {
3637 value: "MR".to_string(),
3638 description: "Empfaenger".to_string(),
3639 ahb_status: "X".to_string(),
3640 }],
3641 parent_group_ahb_status: Some("Muss".to_string()),
3642 segment_ahb_status: None,
3643 ..Default::default()
3644 },
3645 AhbFieldRule {
3647 segment_path: "SG4/SG5/LOC/3227".to_string(),
3648 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3649 ahb_status: "X".to_string(),
3650 codes: vec![AhbCodeRule {
3651 value: "Z16".to_string(),
3652 description: "Marktlokation".to_string(),
3653 ahb_status: "X".to_string(),
3654 }],
3655 parent_group_ahb_status: Some("Kann".to_string()),
3656 segment_ahb_status: None,
3657 ..Default::default()
3658 },
3659 AhbFieldRule {
3660 segment_path: "SG4/SG5/LOC/3227".to_string(),
3661 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3662 ahb_status: "Muss".to_string(),
3663 codes: vec![AhbCodeRule {
3664 value: "Z17".to_string(),
3665 description: "Messlokation".to_string(),
3666 ahb_status: "X".to_string(),
3667 }],
3668 parent_group_ahb_status: Some("Kann".to_string()),
3669 segment_ahb_status: None,
3670 ..Default::default()
3671 },
3672 ],
3673 ub_definitions: BTreeMap::new(),
3674 };
3675
3676 let report = validator.validate_with_navigator(
3677 &segments,
3678 &workflow,
3679 &external,
3680 ValidationLevel::Conditions,
3681 &nav,
3682 );
3683
3684 let ahb_errors: Vec<_> = report
3685 .by_category(ValidationCategory::Ahb)
3686 .filter(|i| i.severity == Severity::Error)
3687 .collect();
3688
3689 assert_eq!(
3692 ahb_errors.len(),
3693 1,
3694 "Expected only AHB001 for missing NAD+MR, got: {:?}",
3695 ahb_errors
3696 );
3697 assert!(
3698 narrate(ahb_errors[0]).contains("Empfaenger"),
3699 "Error should be for missing NAD+MR (Empfaenger)"
3700 );
3701 }
3702
3703 #[test]
3707 fn test_optional_group_variant_with_true_condition_absent_no_error() {
3708 use mig_types::navigator::GroupNavigator;
3709
3710 struct TestNav;
3711 impl GroupNavigator for TestNav {
3712 fn find_segments_in_group(
3713 &self,
3714 segment_id: &str,
3715 group_path: &[&str],
3716 instance_index: usize,
3717 ) -> Vec<OwnedSegment> {
3718 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
3719 vec![OwnedSegment {
3720 id: "SEQ".into(),
3721 elements: vec![vec!["Z03".into()]],
3722 segment_number: 10,
3723 }]
3724 } else {
3725 vec![]
3726 }
3727 }
3728 fn find_segments_with_qualifier_in_group(
3729 &self,
3730 _: &str,
3731 _: usize,
3732 _: &str,
3733 _: &[&str],
3734 _: usize,
3735 ) -> Vec<OwnedSegment> {
3736 vec![]
3737 }
3738 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3739 match group_path {
3740 ["SG4"] => 1,
3741 ["SG4", "SG8"] => 1,
3742 _ => 0,
3743 }
3744 }
3745 }
3746
3747 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
3748 let validator = EdifactValidator::new(evaluator);
3749 let segments = vec![OwnedSegment {
3750 id: "SEQ".into(),
3751 elements: vec![vec!["Z03".into()]],
3752 segment_number: 10,
3753 }];
3754 let seq = |code: &str, group: &str| AhbFieldRule {
3755 segment_path: "SG4/SG8/SEQ/1229".to_string(),
3756 name: format!("Handlung, Code ({code})"),
3757 ahb_status: "X".to_string(),
3758 codes: vec![AhbCodeRule {
3759 value: code.to_string(),
3760 description: String::new(),
3761 ahb_status: "X".to_string(),
3762 }],
3763 parent_group_ahb_status: Some(group.to_string()),
3764 segment_ahb_status: None,
3765 ..Default::default()
3766 };
3767 let workflow = AhbWorkflow {
3768 pruefidentifikator: "44143".to_string(),
3769 description: "Test".to_string(),
3770 communication_direction: None,
3771 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
3772 ub_definitions: BTreeMap::new(),
3773 };
3774
3775 let report = validator.validate_with_navigator(
3776 &segments,
3777 &workflow,
3778 &NoOpExternalProvider,
3779 ValidationLevel::Conditions,
3780 &TestNav,
3781 );
3782 let missing: Vec<_> = report
3783 .issues
3784 .iter()
3785 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
3786 .collect();
3787 assert!(missing.is_empty(), "{missing:#?}");
3788 }
3789
3790 #[test]
3791 fn test_conditional_group_variant_absent_no_error() {
3792 use mig_types::navigator::GroupNavigator;
3797
3798 struct TestNav;
3799 impl GroupNavigator for TestNav {
3800 fn find_segments_in_group(
3801 &self,
3802 segment_id: &str,
3803 group_path: &[&str],
3804 instance_index: usize,
3805 ) -> Vec<OwnedSegment> {
3806 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
3807 vec![OwnedSegment {
3808 id: "LOC".into(),
3809 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3810 segment_number: 10,
3811 }]
3812 } else {
3813 vec![]
3814 }
3815 }
3816 fn find_segments_with_qualifier_in_group(
3817 &self,
3818 _: &str,
3819 _: usize,
3820 _: &str,
3821 _: &[&str],
3822 _: usize,
3823 ) -> Vec<OwnedSegment> {
3824 vec![]
3825 }
3826 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3827 match group_path {
3828 ["SG4"] => 1,
3829 ["SG4", "SG5"] => 1, _ => 0,
3831 }
3832 }
3833 }
3834
3835 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
3838 let validator = EdifactValidator::new(evaluator);
3839 let external = NoOpExternalProvider;
3840 let nav = TestNav;
3841
3842 let segments = vec![OwnedSegment {
3843 id: "LOC".into(),
3844 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3845 segment_number: 10,
3846 }];
3847
3848 let workflow = AhbWorkflow {
3849 pruefidentifikator: "55001".to_string(),
3850 description: "Test".to_string(),
3851 communication_direction: None,
3852 fields: vec![
3853 AhbFieldRule {
3855 segment_path: "SG4/SG5/LOC/3227".to_string(),
3856 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3857 ahb_status: "X".to_string(),
3858 codes: vec![AhbCodeRule {
3859 value: "Z16".to_string(),
3860 description: "Marktlokation".to_string(),
3861 ahb_status: "X".to_string(),
3862 }],
3863 parent_group_ahb_status: Some("Muss [2061]".to_string()),
3864 segment_ahb_status: None,
3865 ..Default::default()
3866 },
3867 AhbFieldRule {
3869 segment_path: "SG4/SG5/LOC/3227".to_string(),
3870 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3871 ahb_status: "X".to_string(),
3872 codes: vec![AhbCodeRule {
3873 value: "Z17".to_string(),
3874 description: "Messlokation".to_string(),
3875 ahb_status: "X".to_string(),
3876 }],
3877 parent_group_ahb_status: Some("Soll [165]".to_string()),
3878 segment_ahb_status: None,
3879 ..Default::default()
3880 },
3881 ],
3882 ub_definitions: BTreeMap::new(),
3883 };
3884
3885 let report = validator.validate_with_navigator(
3886 &segments,
3887 &workflow,
3888 &external,
3889 ValidationLevel::Conditions,
3890 &nav,
3891 );
3892
3893 let ahb_errors: Vec<_> = report
3894 .by_category(ValidationCategory::Ahb)
3895 .filter(|i| i.severity == Severity::Error)
3896 .collect();
3897
3898 assert!(
3900 ahb_errors.is_empty(),
3901 "Expected no errors when conditional group variant [165]=False, got: {:?}",
3902 ahb_errors
3903 );
3904 }
3905
3906 #[test]
3907 fn test_conditional_group_variant_unknown_no_error() {
3908 let evaluator = MockEvaluator::new(vec![]);
3914 let validator = EdifactValidator::new(evaluator);
3915 let external = NoOpExternalProvider;
3916
3917 let workflow = AhbWorkflow {
3918 pruefidentifikator: "55001".to_string(),
3919 description: "Test".to_string(),
3920 communication_direction: None,
3921 fields: vec![AhbFieldRule {
3922 segment_path: "SG4/SG5/LOC/3227".to_string(),
3923 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3924 ahb_status: "X".to_string(),
3925 codes: vec![AhbCodeRule {
3926 value: "Z17".to_string(),
3927 description: "Messlokation".to_string(),
3928 ahb_status: "X".to_string(),
3929 }],
3930 parent_group_ahb_status: Some("Soll [165]".to_string()),
3931 segment_ahb_status: None,
3932 ..Default::default()
3933 }],
3934 ub_definitions: BTreeMap::new(),
3935 };
3936
3937 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3938
3939 let ahb_errors: Vec<_> = report
3940 .by_category(ValidationCategory::Ahb)
3941 .filter(|i| i.severity == Severity::Error)
3942 .collect();
3943
3944 assert!(
3946 ahb_errors.is_empty(),
3947 "Expected no errors when parent group condition is Unknown, got: {:?}",
3948 ahb_errors
3949 );
3950 }
3951
3952 #[test]
3953 fn test_segment_absent_within_present_group_no_error() {
3954 use mig_types::navigator::GroupNavigator;
3958
3959 struct TestNav;
3960 impl GroupNavigator for TestNav {
3961 fn find_segments_in_group(
3962 &self,
3963 segment_id: &str,
3964 group_path: &[&str],
3965 instance_index: usize,
3966 ) -> Vec<OwnedSegment> {
3967 if segment_id == "QTY"
3969 && group_path == ["SG5", "SG6", "SG9", "SG10"]
3970 && instance_index == 0
3971 {
3972 vec![OwnedSegment {
3973 id: "QTY".into(),
3974 elements: vec![vec!["220".into(), "0".into()]],
3975 segment_number: 14,
3976 }]
3977 } else {
3978 vec![]
3979 }
3980 }
3981 fn find_segments_with_qualifier_in_group(
3982 &self,
3983 _: &str,
3984 _: usize,
3985 _: &str,
3986 _: &[&str],
3987 _: usize,
3988 ) -> Vec<OwnedSegment> {
3989 vec![]
3990 }
3991 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3992 match group_path {
3993 ["SG5"] => 1,
3994 ["SG5", "SG6"] => 1,
3995 ["SG5", "SG6", "SG9"] => 1,
3996 ["SG5", "SG6", "SG9", "SG10"] => 1,
3997 _ => 0,
3998 }
3999 }
4000 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4001 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4003 }
4004 }
4005
4006 let evaluator = MockEvaluator::all_true(&[]);
4007 let validator = EdifactValidator::new(evaluator);
4008 let external = NoOpExternalProvider;
4009 let nav = TestNav;
4010
4011 let segments = vec![OwnedSegment {
4012 id: "QTY".into(),
4013 elements: vec![vec!["220".into(), "0".into()]],
4014 segment_number: 14,
4015 }];
4016
4017 let workflow = AhbWorkflow {
4018 pruefidentifikator: "13017".to_string(),
4019 description: "Test".to_string(),
4020 communication_direction: None,
4021 fields: vec![
4022 AhbFieldRule {
4024 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4025 name: "Statuskategorie, Code".to_string(),
4026 ahb_status: "X".to_string(),
4027 codes: vec![],
4028 parent_group_ahb_status: Some("Muss".to_string()),
4029 segment_ahb_status: None,
4030 ..Default::default()
4031 },
4032 AhbFieldRule {
4034 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4035 name: "Statusanlaß, Code".to_string(),
4036 ahb_status: "X [5]".to_string(),
4037 codes: vec![],
4038 parent_group_ahb_status: Some("Muss".to_string()),
4039 segment_ahb_status: None,
4040 ..Default::default()
4041 },
4042 ],
4043 ub_definitions: BTreeMap::new(),
4044 };
4045
4046 let report = validator.validate_with_navigator(
4047 &segments,
4048 &workflow,
4049 &external,
4050 ValidationLevel::Conditions,
4051 &nav,
4052 );
4053
4054 let ahb_errors: Vec<_> = report
4055 .by_category(ValidationCategory::Ahb)
4056 .filter(|i| i.severity == Severity::Error)
4057 .collect();
4058
4059 assert!(
4060 ahb_errors.is_empty(),
4061 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
4062 ahb_errors
4063 );
4064 }
4065
4066 #[test]
4067 fn test_group_scoped_code_validation_with_navigator() {
4068 use mig_types::navigator::GroupNavigator;
4072
4073 struct TestNav;
4074 impl GroupNavigator for TestNav {
4075 fn find_segments_in_group(
4076 &self,
4077 segment_id: &str,
4078 group_path: &[&str],
4079 _instance_index: usize,
4080 ) -> Vec<OwnedSegment> {
4081 if segment_id != "NAD" {
4082 return vec![];
4083 }
4084 match group_path {
4085 ["SG2"] => vec![
4086 OwnedSegment {
4087 id: "NAD".into(),
4088 elements: vec![vec!["MS".into()]],
4089 segment_number: 3,
4090 },
4091 OwnedSegment {
4092 id: "NAD".into(),
4093 elements: vec![vec!["MT".into()]], segment_number: 4,
4095 },
4096 ],
4097 ["SG4", "SG12"] => vec![
4098 OwnedSegment {
4099 id: "NAD".into(),
4100 elements: vec![vec!["Z04".into()]],
4101 segment_number: 20,
4102 },
4103 OwnedSegment {
4104 id: "NAD".into(),
4105 elements: vec![vec!["Z09".into()]],
4106 segment_number: 21,
4107 },
4108 ],
4109 _ => vec![],
4110 }
4111 }
4112 fn find_segments_with_qualifier_in_group(
4113 &self,
4114 _: &str,
4115 _: usize,
4116 _: &str,
4117 _: &[&str],
4118 _: usize,
4119 ) -> Vec<OwnedSegment> {
4120 vec![]
4121 }
4122 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4123 match group_path {
4124 ["SG2"] | ["SG4", "SG12"] => 1,
4125 _ => 0,
4126 }
4127 }
4128 }
4129
4130 let evaluator = MockEvaluator::new(vec![]);
4131 let validator = EdifactValidator::new(evaluator);
4132 let external = NoOpExternalProvider;
4133 let nav = TestNav;
4134
4135 let workflow = AhbWorkflow {
4136 pruefidentifikator: "55001".to_string(),
4137 description: "Test".to_string(),
4138 communication_direction: None,
4139 fields: vec![
4140 AhbFieldRule {
4141 segment_path: "SG2/NAD/3035".to_string(),
4142 name: "Absender".to_string(),
4143 ahb_status: "X".to_string(),
4144 codes: vec![AhbCodeRule {
4145 value: "MS".to_string(),
4146 description: "Absender".to_string(),
4147 ahb_status: "X".to_string(),
4148 }],
4149 parent_group_ahb_status: None,
4150 segment_ahb_status: None,
4151 ..Default::default()
4152 },
4153 AhbFieldRule {
4154 segment_path: "SG2/NAD/3035".to_string(),
4155 name: "Empfaenger".to_string(),
4156 ahb_status: "X".to_string(),
4157 codes: vec![AhbCodeRule {
4158 value: "MR".to_string(),
4159 description: "Empfaenger".to_string(),
4160 ahb_status: "X".to_string(),
4161 }],
4162 parent_group_ahb_status: None,
4163 segment_ahb_status: None,
4164 ..Default::default()
4165 },
4166 AhbFieldRule {
4167 segment_path: "SG4/SG12/NAD/3035".to_string(),
4168 name: "Anschlussnutzer".to_string(),
4169 ahb_status: "X".to_string(),
4170 codes: vec![AhbCodeRule {
4171 value: "Z04".to_string(),
4172 description: "Anschlussnutzer".to_string(),
4173 ahb_status: "X".to_string(),
4174 }],
4175 parent_group_ahb_status: None,
4176 segment_ahb_status: None,
4177 ..Default::default()
4178 },
4179 AhbFieldRule {
4180 segment_path: "SG4/SG12/NAD/3035".to_string(),
4181 name: "Korrespondenzanschrift".to_string(),
4182 ahb_status: "X".to_string(),
4183 codes: vec![AhbCodeRule {
4184 value: "Z09".to_string(),
4185 description: "Korrespondenzanschrift".to_string(),
4186 ahb_status: "X".to_string(),
4187 }],
4188 parent_group_ahb_status: None,
4189 segment_ahb_status: None,
4190 ..Default::default()
4191 },
4192 ],
4193 ub_definitions: BTreeMap::new(),
4194 };
4195
4196 let all_segments = vec![
4198 OwnedSegment {
4199 id: "NAD".into(),
4200 elements: vec![vec!["MS".into()]],
4201 segment_number: 3,
4202 },
4203 OwnedSegment {
4204 id: "NAD".into(),
4205 elements: vec![vec!["MT".into()]],
4206 segment_number: 4,
4207 },
4208 OwnedSegment {
4209 id: "NAD".into(),
4210 elements: vec![vec!["Z04".into()]],
4211 segment_number: 20,
4212 },
4213 OwnedSegment {
4214 id: "NAD".into(),
4215 elements: vec![vec!["Z09".into()]],
4216 segment_number: 21,
4217 },
4218 ];
4219
4220 let report = validator.validate_with_navigator(
4221 &all_segments,
4222 &workflow,
4223 &external,
4224 ValidationLevel::Conditions,
4225 &nav,
4226 );
4227
4228 let code_errors: Vec<_> = report
4229 .by_category(ValidationCategory::Code)
4230 .filter(|i| i.severity == Severity::Error)
4231 .collect();
4232
4233 assert_eq!(
4236 code_errors.len(),
4237 1,
4238 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
4239 code_errors
4240 );
4241 assert!(narrate(code_errors[0]).contains("MT"));
4242 assert!(narrate(code_errors[0]).contains("MR"));
4244 assert!(narrate(code_errors[0]).contains("MS"));
4245 assert!(
4246 !narrate(code_errors[0]).contains("Z04"),
4247 "SG4/SG12 codes should not leak into SG2 error"
4248 );
4249 assert!(
4251 code_errors[0]
4252 .field_path
4253 .as_deref()
4254 .unwrap_or("")
4255 .contains("SG2"),
4256 "Error field_path should reference SG2, got: {:?}",
4257 code_errors[0].field_path
4258 );
4259 }
4260
4261 #[test]
4264 fn test_package_cardinality_within_bounds() {
4265 let evaluator = MockEvaluator::all_true(&[]);
4267 let validator = EdifactValidator::new(evaluator);
4268 let external = NoOpExternalProvider;
4269
4270 let segments = vec![OwnedSegment {
4271 id: "STS".into(),
4272 elements: vec![
4273 vec!["Z33".into()], vec![], vec!["E01".into()], ],
4277 segment_number: 5,
4278 }];
4279
4280 let workflow = AhbWorkflow {
4281 pruefidentifikator: "13017".to_string(),
4282 description: "Test".to_string(),
4283 communication_direction: None,
4284 ub_definitions: BTreeMap::new(),
4285 fields: vec![AhbFieldRule {
4286 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4287 name: "Statusanlaß, Code".to_string(),
4288 ahb_status: "X".to_string(),
4289 element_index: Some(2),
4290 component_index: Some(0),
4291 codes: vec![
4292 AhbCodeRule {
4293 value: "E01".into(),
4294 description: "Code 1".into(),
4295 ahb_status: "X [4P0..1]".into(),
4296 },
4297 AhbCodeRule {
4298 value: "E02".into(),
4299 description: "Code 2".into(),
4300 ahb_status: "X [4P0..1]".into(),
4301 },
4302 ],
4303 parent_group_ahb_status: Some("Muss".to_string()),
4304 segment_ahb_status: None,
4305 mig_number: None,
4306 format: None,
4307 }],
4308 };
4309
4310 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4311 let pkg_errors: Vec<_> = report
4312 .by_category(ValidationCategory::Ahb)
4313 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4314 .collect();
4315 assert!(
4316 pkg_errors.is_empty(),
4317 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
4318 pkg_errors
4319 );
4320 }
4321
4322 #[test]
4323 fn test_package_cardinality_zero_present_min_zero() {
4324 let evaluator = MockEvaluator::all_true(&[]);
4326 let validator = EdifactValidator::new(evaluator);
4327 let external = NoOpExternalProvider;
4328
4329 let segments = vec![OwnedSegment {
4330 id: "STS".into(),
4331 elements: vec![
4332 vec!["Z33".into()],
4333 vec![],
4334 vec!["X99".into()], ],
4336 segment_number: 5,
4337 }];
4338
4339 let workflow = AhbWorkflow {
4340 pruefidentifikator: "13017".to_string(),
4341 description: "Test".to_string(),
4342 communication_direction: None,
4343 ub_definitions: BTreeMap::new(),
4344 fields: vec![AhbFieldRule {
4345 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4346 name: "Statusanlaß, Code".to_string(),
4347 ahb_status: "X".to_string(),
4348 element_index: Some(2),
4349 component_index: Some(0),
4350 codes: vec![
4351 AhbCodeRule {
4352 value: "E01".into(),
4353 description: "Code 1".into(),
4354 ahb_status: "X [4P0..1]".into(),
4355 },
4356 AhbCodeRule {
4357 value: "E02".into(),
4358 description: "Code 2".into(),
4359 ahb_status: "X [4P0..1]".into(),
4360 },
4361 ],
4362 parent_group_ahb_status: Some("Muss".to_string()),
4363 segment_ahb_status: None,
4364 mig_number: None,
4365 format: None,
4366 }],
4367 };
4368
4369 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4370 let pkg_errors: Vec<_> = report
4371 .by_category(ValidationCategory::Ahb)
4372 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4373 .collect();
4374 assert!(
4375 pkg_errors.is_empty(),
4376 "0 codes, min=0 — no error expected, got: {:?}",
4377 pkg_errors
4378 );
4379 }
4380
4381 #[test]
4382 fn test_package_cardinality_too_many() {
4383 let evaluator = MockEvaluator::all_true(&[]);
4385 let validator = EdifactValidator::new(evaluator);
4386 let external = NoOpExternalProvider;
4387
4388 let segments = vec![
4390 OwnedSegment {
4391 id: "STS".into(),
4392 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
4393 segment_number: 5,
4394 },
4395 OwnedSegment {
4396 id: "STS".into(),
4397 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
4398 segment_number: 6,
4399 },
4400 ];
4401
4402 let workflow = AhbWorkflow {
4403 pruefidentifikator: "13017".to_string(),
4404 description: "Test".to_string(),
4405 communication_direction: None,
4406 ub_definitions: BTreeMap::new(),
4407 fields: vec![AhbFieldRule {
4408 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4409 name: "Statusanlaß, Code".to_string(),
4410 ahb_status: "X".to_string(),
4411 element_index: Some(2),
4412 component_index: Some(0),
4413 codes: vec![
4414 AhbCodeRule {
4415 value: "E01".into(),
4416 description: "Code 1".into(),
4417 ahb_status: "X [4P0..1]".into(),
4418 },
4419 AhbCodeRule {
4420 value: "E02".into(),
4421 description: "Code 2".into(),
4422 ahb_status: "X [4P0..1]".into(),
4423 },
4424 ],
4425 parent_group_ahb_status: Some("Muss".to_string()),
4426 segment_ahb_status: None,
4427 mig_number: None,
4428 format: None,
4429 }],
4430 };
4431
4432 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4433 let pkg_errors: Vec<_> = report
4434 .by_category(ValidationCategory::Ahb)
4435 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4436 .collect();
4437 assert_eq!(
4438 pkg_errors.len(),
4439 1,
4440 "2 codes present, max=1 — expected 1 error, got: {:?}",
4441 pkg_errors
4442 );
4443 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
4444 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
4445 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
4446 }
4447
4448 #[test]
4449 fn test_package_cardinality_too_few() {
4450 let evaluator = MockEvaluator::all_true(&[]);
4452 let validator = EdifactValidator::new(evaluator);
4453 let external = NoOpExternalProvider;
4454
4455 let segments = vec![OwnedSegment {
4456 id: "STS".into(),
4457 elements: vec![
4458 vec!["Z33".into()],
4459 vec![],
4460 vec!["X99".into()], ],
4462 segment_number: 5,
4463 }];
4464
4465 let workflow = AhbWorkflow {
4466 pruefidentifikator: "13017".to_string(),
4467 description: "Test".to_string(),
4468 communication_direction: None,
4469 ub_definitions: BTreeMap::new(),
4470 fields: vec![AhbFieldRule {
4471 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4472 name: "Statusanlaß, Code".to_string(),
4473 ahb_status: "X".to_string(),
4474 element_index: Some(2),
4475 component_index: Some(0),
4476 codes: vec![
4477 AhbCodeRule {
4478 value: "E01".into(),
4479 description: "Code 1".into(),
4480 ahb_status: "X [5P1..3]".into(),
4481 },
4482 AhbCodeRule {
4483 value: "E02".into(),
4484 description: "Code 2".into(),
4485 ahb_status: "X [5P1..3]".into(),
4486 },
4487 AhbCodeRule {
4488 value: "E03".into(),
4489 description: "Code 3".into(),
4490 ahb_status: "X [5P1..3]".into(),
4491 },
4492 ],
4493 parent_group_ahb_status: Some("Muss".to_string()),
4494 segment_ahb_status: None,
4495 mig_number: None,
4496 format: None,
4497 }],
4498 };
4499
4500 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4501 let pkg_errors: Vec<_> = report
4502 .by_category(ValidationCategory::Ahb)
4503 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4504 .collect();
4505 assert_eq!(
4506 pkg_errors.len(),
4507 1,
4508 "0 codes present, min=1 — expected 1 error, got: {:?}",
4509 pkg_errors
4510 );
4511 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
4512 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
4513 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
4514 }
4515
4516 #[test]
4517 fn test_package_cardinality_no_packages_in_workflow() {
4518 let evaluator = MockEvaluator::all_true(&[]);
4520 let validator = EdifactValidator::new(evaluator);
4521 let external = NoOpExternalProvider;
4522
4523 let segments = vec![OwnedSegment {
4524 id: "STS".into(),
4525 elements: vec![vec!["E01".into()]],
4526 segment_number: 5,
4527 }];
4528
4529 let workflow = AhbWorkflow {
4530 pruefidentifikator: "13017".to_string(),
4531 description: "Test".to_string(),
4532 communication_direction: None,
4533 ub_definitions: BTreeMap::new(),
4534 fields: vec![AhbFieldRule {
4535 segment_path: "STS/9015".to_string(),
4536 name: "Status Code".to_string(),
4537 ahb_status: "X".to_string(),
4538 codes: vec![AhbCodeRule {
4539 value: "E01".into(),
4540 description: "Code 1".into(),
4541 ahb_status: "X".into(),
4542 }],
4543 parent_group_ahb_status: Some("Muss".to_string()),
4544 segment_ahb_status: None,
4545 ..Default::default()
4546 }],
4547 };
4548
4549 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4550 let pkg_errors: Vec<_> = report
4551 .by_category(ValidationCategory::Ahb)
4552 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4553 .collect();
4554 assert!(
4555 pkg_errors.is_empty(),
4556 "No packages in workflow — no errors expected"
4557 );
4558 }
4559
4560 #[test]
4561 fn test_package_cardinality_with_condition_and_package() {
4562 let evaluator = MockEvaluator::all_true(&[901]);
4564 let validator = EdifactValidator::new(evaluator);
4565 let external = NoOpExternalProvider;
4566
4567 let segments = vec![OwnedSegment {
4568 id: "STS".into(),
4569 elements: vec![vec![], vec![], vec!["E01".into()]],
4570 segment_number: 5,
4571 }];
4572
4573 let workflow = AhbWorkflow {
4574 pruefidentifikator: "13017".to_string(),
4575 description: "Test".to_string(),
4576 communication_direction: None,
4577 ub_definitions: BTreeMap::new(),
4578 fields: vec![AhbFieldRule {
4579 segment_path: "SG10/STS/C556/9013".to_string(),
4580 name: "Code".to_string(),
4581 ahb_status: "X".to_string(),
4582 element_index: Some(2),
4583 component_index: Some(0),
4584 codes: vec![
4585 AhbCodeRule {
4586 value: "E01".into(),
4587 description: "Code 1".into(),
4588 ahb_status: "X [901] [4P0..1]".into(),
4589 },
4590 AhbCodeRule {
4591 value: "E02".into(),
4592 description: "Code 2".into(),
4593 ahb_status: "X [901] [4P0..1]".into(),
4594 },
4595 ],
4596 parent_group_ahb_status: Some("Muss".to_string()),
4597 segment_ahb_status: None,
4598 mig_number: None,
4599 format: None,
4600 }],
4601 };
4602
4603 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4604 let pkg_errors: Vec<_> = report
4605 .by_category(ValidationCategory::Ahb)
4606 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4607 .collect();
4608 assert!(
4609 pkg_errors.is_empty(),
4610 "1 code within [4P0..1] bounds — no error, got: {:?}",
4611 pkg_errors
4612 );
4613 }
4614
4615 #[test]
4616 fn test_package_cardinality_scoped_per_group_instance() {
4617 use mig_types::navigator::GroupNavigator;
4625
4626 struct TwoSg10s {
4627 sts_a: OwnedSegment,
4628 sts_b: OwnedSegment,
4629 }
4630 impl GroupNavigator for TwoSg10s {
4631 fn find_segments_in_group(
4632 &self,
4633 segment_id: &str,
4634 group_path: &[&str],
4635 instance_index: usize,
4636 ) -> Vec<OwnedSegment> {
4637 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
4638 match instance_index {
4639 0 => vec![self.sts_a.clone()],
4640 1 => vec![self.sts_b.clone()],
4641 _ => vec![],
4642 }
4643 } else {
4644 vec![]
4645 }
4646 }
4647 fn find_segments_with_qualifier_in_group(
4648 &self,
4649 _: &str,
4650 _: usize,
4651 _: &str,
4652 _: &[&str],
4653 _: usize,
4654 ) -> Vec<OwnedSegment> {
4655 vec![]
4656 }
4657 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4658 match group_path {
4659 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
4660 ["SG5", "SG6", "SG9", "SG10"] => 2,
4661 _ => 0,
4662 }
4663 }
4664 }
4665
4666 let sts_a = OwnedSegment {
4667 id: "STS".into(),
4668 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4669 segment_number: 10,
4670 };
4671 let sts_b = OwnedSegment {
4672 id: "STS".into(),
4673 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4674 segment_number: 15,
4675 };
4676 let nav = TwoSg10s {
4677 sts_a: sts_a.clone(),
4678 sts_b: sts_b.clone(),
4679 };
4680
4681 let evaluator = MockEvaluator::all_true(&[]);
4682 let validator = EdifactValidator::new(evaluator);
4683 let external = NoOpExternalProvider;
4684
4685 let workflow = AhbWorkflow {
4686 pruefidentifikator: "13025".to_string(),
4687 description: "Test".to_string(),
4688 communication_direction: None,
4689 ub_definitions: BTreeMap::new(),
4690 fields: vec![AhbFieldRule {
4691 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4692 name: "Statusanlaß, Code".to_string(),
4693 ahb_status: "X".to_string(),
4694 element_index: Some(2),
4695 component_index: Some(0),
4696 codes: vec![
4697 AhbCodeRule {
4698 value: "E01".into(),
4699 description: "Code 1".into(),
4700 ahb_status: "X [4P0..1]".into(),
4701 },
4702 AhbCodeRule {
4703 value: "E02".into(),
4704 description: "Code 2".into(),
4705 ahb_status: "X [4P0..1]".into(),
4706 },
4707 ],
4708 parent_group_ahb_status: Some("Muss".to_string()),
4709 segment_ahb_status: None,
4710 mig_number: None,
4711 format: None,
4712 }],
4713 };
4714
4715 let report = validator.validate_with_navigator(
4716 &[sts_a, sts_b],
4717 &workflow,
4718 &external,
4719 ValidationLevel::Full,
4720 &nav,
4721 );
4722 let pkg_errors: Vec<_> = report
4723 .by_category(ValidationCategory::Ahb)
4724 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4725 .collect();
4726 assert!(
4727 pkg_errors.is_empty(),
4728 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
4729 pkg_errors
4730 );
4731 }
4732
4733 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
4734 OwnedSegment {
4735 id: id.to_string(),
4736 elements: elements
4737 .into_iter()
4738 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
4739 .collect(),
4740 segment_number: 0,
4741 }
4742 }
4743
4744 #[test]
4755 fn test_package_cardinality_scoped_to_rule_mig_variant() {
4756 use mig_types::navigator::GroupNavigator;
4757
4758 struct TwoSg8Variants {
4759 seq_z01: OwnedSegment,
4760 seq_z45: OwnedSegment,
4761 }
4762 impl GroupNavigator for TwoSg8Variants {
4763 fn find_segments_in_group(
4764 &self,
4765 segment_id: &str,
4766 group_path: &[&str],
4767 instance_index: usize,
4768 ) -> Vec<OwnedSegment> {
4769 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
4770 match instance_index {
4771 0 => vec![self.seq_z01.clone()],
4772 1 => vec![self.seq_z45.clone()],
4773 _ => vec![],
4774 }
4775 } else {
4776 vec![]
4777 }
4778 }
4779 fn find_segments_with_qualifier_in_group(
4780 &self,
4781 _: &str,
4782 _: usize,
4783 _: &str,
4784 _: &[&str],
4785 _: usize,
4786 ) -> Vec<OwnedSegment> {
4787 vec![]
4788 }
4789 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4790 match group_path {
4791 ["SG4"] => 1,
4792 ["SG4", "SG8"] => 2,
4793 _ => 0,
4794 }
4795 }
4796 fn instance_has_mig_number(
4797 &self,
4798 group_path: &[&str],
4799 instance_index: usize,
4800 mig_number: &str,
4801 ) -> bool {
4802 if group_path != ["SG4", "SG8"] {
4803 return true;
4804 }
4805 match (instance_index, mig_number) {
4806 (0, "00115") => true,
4807 (0, _) => false,
4808 (1, "00171") => true,
4809 (1, _) => false,
4810 _ => false,
4811 }
4812 }
4813 }
4814
4815 let seq_z01 = OwnedSegment {
4816 id: "SEQ".into(),
4817 elements: vec![vec!["Z01".into()], vec!["1".into()]],
4818 segment_number: 10,
4819 };
4820 let seq_z45 = OwnedSegment {
4821 id: "SEQ".into(),
4822 elements: vec![vec!["Z45".into()], vec!["1".into()]],
4823 segment_number: 20,
4824 };
4825 let nav = TwoSg8Variants {
4826 seq_z01: seq_z01.clone(),
4827 seq_z45: seq_z45.clone(),
4828 };
4829
4830 let evaluator = MockEvaluator::all_true(&[]);
4831 let validator = EdifactValidator::new(evaluator);
4832 let external = NoOpExternalProvider;
4833
4834 let workflow = AhbWorkflow {
4835 pruefidentifikator: "55218".to_string(),
4836 description: "Test".to_string(),
4837 communication_direction: None,
4838 ub_definitions: BTreeMap::new(),
4839 fields: vec![AhbFieldRule {
4840 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4841 name: "Handlung, Code".to_string(),
4842 ahb_status: "X".to_string(),
4843 element_index: Some(0),
4844 component_index: Some(0),
4845 codes: vec![
4846 AhbCodeRule {
4847 value: "Z45".into(),
4848 description: "NNA".into(),
4849 ahb_status: "X [1P1..4294967295]".into(),
4850 },
4851 AhbCodeRule {
4852 value: "Z84".into(),
4853 description: "Differenz-NNA".into(),
4854 ahb_status: "X [1P0..4294967295]".into(),
4855 },
4856 ],
4857 parent_group_ahb_status: Some("Muss".to_string()),
4858 segment_ahb_status: None,
4859 mig_number: Some("00171".to_string()),
4860 format: None,
4861 }],
4862 };
4863
4864 let report = validator.validate_with_navigator(
4865 &[seq_z01, seq_z45],
4866 &workflow,
4867 &external,
4868 ValidationLevel::Full,
4869 &nav,
4870 );
4871 let pkg_errors: Vec<_> = report
4872 .by_category(ValidationCategory::Ahb)
4873 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4874 .collect();
4875 assert!(
4876 pkg_errors.is_empty(),
4877 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
4878 pkg_errors
4879 );
4880 }
4881
4882 #[test]
4883 fn test_unt_count_correct() {
4884 let segments = vec![
4886 make_segment("UNH", vec![vec!["001"]]),
4887 make_segment("BGM", vec![vec!["E01"]]),
4888 make_segment("DTM", vec![vec!["137", "20250401"]]),
4889 make_segment("UNT", vec![vec!["4", "001"]]),
4890 ];
4891 assert!(
4892 validate_unt_segment_count(&segments).is_none(),
4893 "Correct count should produce no issue"
4894 );
4895 }
4896
4897 #[test]
4898 fn test_unt_count_mismatch() {
4899 let segments = vec![
4901 make_segment("UNH", vec![vec!["001"]]),
4902 make_segment("BGM", vec![vec!["E01"]]),
4903 make_segment("UNT", vec![vec!["5", "001"]]),
4904 ];
4905 let issue =
4906 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
4907 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
4908 assert_eq!(issue.severity, Severity::Error);
4909 assert!(narrate(&issue).contains("declared 5"));
4910 assert!(narrate(&issue).contains("actual 3"));
4911 }
4912
4913 #[test]
4914 fn test_unt_count_excludes_envelope() {
4915 let segments = vec![
4917 make_segment("UNA", vec![]),
4918 make_segment("UNB", vec![vec!["UNOC", "3"]]),
4919 make_segment("UNH", vec![vec!["001"]]),
4920 make_segment("BGM", vec![vec!["E01"]]),
4921 make_segment("UNT", vec![vec!["3", "001"]]),
4922 make_segment("UNZ", vec![vec!["1"]]),
4923 ];
4924 assert!(
4925 validate_unt_segment_count(&segments).is_none(),
4926 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
4927 );
4928 }
4929
4930 #[test]
4931 fn test_unt_count_no_unt_returns_none() {
4932 let segments = vec![
4933 make_segment("UNH", vec![vec!["001"]]),
4934 make_segment("BGM", vec![vec!["E01"]]),
4935 ];
4936 assert!(
4937 validate_unt_segment_count(&segments).is_none(),
4938 "No UNT segment should return None (not our problem)"
4939 );
4940 }
4941
4942 #[test]
4943 fn test_unt_count_rejects_multi_message_input() {
4944 let segments = vec![
4946 make_segment("UNH", vec![vec!["001"]]),
4947 make_segment("BGM", vec![vec!["E01"]]),
4948 make_segment("UNT", vec![vec!["3", "001"]]),
4949 make_segment("UNH", vec![vec!["002"]]),
4950 make_segment("BGM", vec![vec!["E02"]]),
4951 make_segment("UNT", vec![vec!["3", "002"]]),
4952 ];
4953 let issue = validate_unt_segment_count(&segments)
4954 .expect("Multi-message input should produce an error");
4955 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
4956 assert!(
4957 narrate(&issue).contains("2 UNH"),
4958 "Should mention UNH count: {}",
4959 narrate(&issue)
4960 );
4961 }
4962
4963 #[test]
4964 fn test_code_validation_accepts_multi_code_variant_qualifier() {
4965 let evaluator = MockEvaluator::new(vec![]);
4972 let validator = EdifactValidator::new(evaluator);
4973 let external = NoOpExternalProvider;
4974
4975 let rff_z39 = OwnedSegment {
4976 id: "RFF".to_string(),
4977 elements: vec![
4978 vec!["RFF".to_string()],
4979 vec!["Z39".to_string(), "REF1".to_string()],
4980 ],
4981 segment_number: 1,
4982 };
4983
4984 let workflow = AhbWorkflow {
4985 pruefidentifikator: "55035".to_string(),
4986 description: "Test".to_string(),
4987 communication_direction: None,
4988 fields: vec![
4989 AhbFieldRule {
4991 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
4992 name: "Referenznummer Qualifier".to_string(),
4993 ahb_status: "Muss".to_string(),
4994 codes: vec![
4995 AhbCodeRule {
4996 value: "Z31".to_string(),
4997 description: "".to_string(),
4998 ahb_status: "X".to_string(),
4999 },
5000 AhbCodeRule {
5001 value: "Z39".to_string(),
5002 description: "".to_string(),
5003 ahb_status: "X".to_string(),
5004 },
5005 ],
5006 parent_group_ahb_status: None,
5007 segment_ahb_status: None,
5008 element_index: Some(1),
5009 component_index: Some(0),
5010 mig_number: Some("00075".to_string()),
5011 format: None,
5012 },
5013 AhbFieldRule {
5015 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5016 name: "Referenznummer Qualifier".to_string(),
5017 ahb_status: "Muss".to_string(),
5018 codes: vec![AhbCodeRule {
5019 value: "Z33".to_string(),
5020 description: "".to_string(),
5021 ahb_status: "X".to_string(),
5022 }],
5023 parent_group_ahb_status: None,
5024 segment_ahb_status: None,
5025 element_index: Some(1),
5026 component_index: Some(0),
5027 mig_number: Some("00078".to_string()),
5028 format: None,
5029 },
5030 ],
5031 ub_definitions: BTreeMap::new(),
5032 };
5033
5034 let report = validator.validate(
5035 &[rff_z39],
5036 &workflow,
5037 &external,
5038 ValidationLevel::Conditions,
5039 );
5040
5041 let code_errors: Vec<_> = report
5042 .by_category(ValidationCategory::Code)
5043 .filter(|i| {
5044 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5045 })
5046 .collect();
5047 assert!(
5048 code_errors.is_empty(),
5049 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
5050 code_errors
5051 );
5052 }
5053
5054 #[test]
5055 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
5056 let evaluator = MockEvaluator::new(vec![]);
5062 let validator = EdifactValidator::new(evaluator);
5063 let external = NoOpExternalProvider;
5064
5065 let pia_5_z12 = OwnedSegment {
5069 id: "PIA".to_string(),
5070 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
5071 segment_number: 1,
5072 };
5073 let pia_5_srw = OwnedSegment {
5074 id: "PIA".to_string(),
5075 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
5076 segment_number: 2,
5077 };
5078
5079 let make_rules = |mig: &str, composite_code: &str| {
5080 vec![
5081 AhbFieldRule {
5082 segment_path: "SG4/SG8/PIA/4347".to_string(),
5083 name: "Produkt-ID-Funktion".to_string(),
5084 ahb_status: "Muss".to_string(),
5085 codes: vec![AhbCodeRule {
5086 value: "5".to_string(),
5087 description: "".to_string(),
5088 ahb_status: "X".to_string(),
5089 }],
5090 parent_group_ahb_status: None,
5091 segment_ahb_status: None,
5092 element_index: Some(0),
5093 component_index: Some(0),
5094 mig_number: Some(mig.to_string()),
5095 format: None,
5096 },
5097 AhbFieldRule {
5098 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
5099 name: "Artikel/Dienstleistung-Identifikator".to_string(),
5100 ahb_status: "Muss".to_string(),
5101 codes: vec![AhbCodeRule {
5102 value: composite_code.to_string(),
5103 description: "".to_string(),
5104 ahb_status: "X".to_string(),
5105 }],
5106 parent_group_ahb_status: None,
5107 segment_ahb_status: None,
5108 element_index: Some(1),
5109 component_index: Some(0),
5110 mig_number: Some(mig.to_string()),
5111 format: None,
5112 },
5113 ]
5114 };
5115
5116 let mut fields = make_rules("00108", "Z12");
5117 fields.extend(make_rules("00197", "SRW"));
5118
5119 let workflow = AhbWorkflow {
5120 pruefidentifikator: "55035".to_string(),
5121 description: "Test".to_string(),
5122 communication_direction: None,
5123 fields,
5124 ub_definitions: BTreeMap::new(),
5125 };
5126
5127 let report = validator.validate(
5128 &[pia_5_z12, pia_5_srw],
5129 &workflow,
5130 &external,
5131 ValidationLevel::Conditions,
5132 );
5133
5134 let code_errors: Vec<_> = report
5135 .by_category(ValidationCategory::Code)
5136 .filter(|i| {
5137 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5138 })
5139 .collect();
5140 assert!(
5141 code_errors.is_empty(),
5142 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
5143 code_errors
5144 );
5145 }
5146}