1use std::collections::{BTreeMap, BTreeSet, 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 true,
225 &mut report,
226 );
227 }
228
229 let mut instance_counter: HashMap<&str, usize> = HashMap::new();
234 for group in &validated_tree.groups {
235 let instance_index = *instance_counter
236 .entry(group.group_id)
237 .and_modify(|c| *c += 1)
238 .or_insert(0);
239
240 let path_storage = [(group.group_id, instance_index)];
241 let scoped_ctx = ctx.with_scope(GroupScope {
244 path: &path_storage,
245 });
246
247 walk_group_instance(
248 group,
249 &path_storage,
250 &scoped_ctx,
251 &expr_eval,
252 &self.evaluator,
253 validated_tree.ub_definitions,
254 &tag_migs,
255 instance_index,
256 self.report_segments,
257 &mut report,
258 );
259 }
260
261 if let Some((once, transaction)) =
263 crate::eval::once_per_transaction(self.evaluator.message_type())
264 {
265 let mut index: HashMap<&str, usize> = HashMap::new();
266 for group in &validated_tree.groups {
267 let i = *index
268 .entry(group.group_id)
269 .and_modify(|c| *c += 1)
270 .or_insert(0);
271 if group.group_id == transaction {
272 report_repeated_in_transaction(group, i, once, &ctx, &mut report);
273 }
274 }
275 }
276
277 let absent = crate::eval::AbsentTarget(&self.evaluator);
287 let absent_eval = ConditionExprEvaluator::new(&absent);
288 for field in &validated_tree.unmatched_rules {
289 if should_skip_for_parent_group(
290 field,
291 &absent_eval,
292 &ctx,
293 validated_tree.ub_definitions,
294 ) {
295 continue;
296 }
297
298 let status =
299 absent_eval.resolve_status(&field.ahb_status, &ctx, validated_tree.ub_definitions);
300
301 if status.result == ConditionResult::True
302 && status.kind == Some(StatusKind::Muss)
303 && !is_field_present(&ctx, field)
304 && !is_group_variant_absent(
305 &ctx,
306 field,
307 is_parent_group_required(
308 field,
309 &absent_eval,
310 &ctx,
311 validated_tree.ub_definitions,
312 ),
313 )
314 {
315 let mut issue = ValidationIssue::new(
316 Severity::Error,
317 IssueKind::MissingRequiredField {
318 field_name: field.name.clone(),
319 },
320 )
321 .with_field_path(&field.segment_path)
322 .with_rule(&field.ahb_status);
323 if let Some(first_code) = field.codes.first() {
324 issue.expected_value = Some(first_code.value.clone());
325 }
326 issue = attach_field_position(issue, field);
327 report.add_issue(issue);
328 }
329 }
330
331 if matches!(level, ValidationLevel::Full) {
335 let mut all_fields: Vec<AhbFieldRule> = Vec::new();
336 for node in &all_nodes {
337 all_fields.push(node.rule.clone());
338 }
339 for rule in &validated_tree.unmatched_rules {
340 all_fields.push((*rule).clone());
341 }
342 let synthetic_workflow = AhbWorkflow {
343 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
344 description: String::new(),
345 communication_direction: None,
346 fields: all_fields,
347 ub_definitions: validated_tree.ub_definitions.clone(),
348 package_conditions: validated_tree.package_conditions.clone(),
349 };
350 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
351 self.validate_answer_lists(&synthetic_workflow, &ctx, &mut report);
352 self.validate_package_cardinality(&synthetic_workflow, &ctx, &mut report);
353 }
354
355 report
356 }
357
358 fn validate_conditions(
360 &self,
361 workflow: &AhbWorkflow,
362 ctx: &EvaluationContext,
363 report: &mut ValidationReport,
364 ) {
365 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
366
367 for field in &workflow.fields {
368 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
370 continue;
371 }
372
373 let StatusResolution {
376 result: condition_result,
377 kind,
378 unknown_ids,
379 } = expr_eval.resolve_status(&field.ahb_status, ctx, &workflow.ub_definitions);
380 let mandatory = kind == Some(StatusKind::Muss);
381
382 match condition_result {
383 ConditionResult::True => {
384 if mandatory
386 && !is_field_present(ctx, field)
387 && !is_group_variant_absent(
388 ctx,
389 field,
390 is_parent_group_required(
391 field,
392 &expr_eval,
393 ctx,
394 &workflow.ub_definitions,
395 ),
396 )
397 {
398 let mut issue = ValidationIssue::new(
399 Severity::Error,
400 IssueKind::MissingRequiredField {
401 field_name: field.name.clone(),
402 },
403 )
404 .with_field_path(&field.segment_path)
405 .with_rule(&field.ahb_status);
406 if let Some(first_code) = field.codes.first() {
410 issue.expected_value = Some(first_code.value.clone());
411 }
412 report.add_issue(issue);
413 }
414 }
415 ConditionResult::False => {
416 if mandatory && is_field_present(ctx, field) {
422 report.add_issue(
423 ValidationIssue::new(
424 Severity::Error,
425 IssueKind::FieldConditionNotSatisfied {
426 field_name: field.name.clone(),
427 },
428 )
429 .with_field_path(&field.segment_path)
430 .with_rule(&field.ahb_status),
431 );
432 }
433 }
434 ConditionResult::Unknown => {
435 let mut external_ids = Vec::new();
440 let mut undetermined_ids = Vec::new();
441 let mut missing_ids = Vec::new();
442 for id in unknown_ids {
443 if self.evaluator.is_external(id) {
444 external_ids.push(id);
445 } else if self.evaluator.is_known(id) {
446 undetermined_ids.push(id);
447 } else {
448 missing_ids.push(id);
449 }
450 }
451
452 report.add_issue(
453 ValidationIssue::new(
454 Severity::Info,
455 IssueKind::ConditionUnknown {
456 field_name: field.name.clone(),
457 unresolved: UnresolvedConditions {
458 external: external_ids,
459 undetermined: undetermined_ids,
460 missing: missing_ids,
461 },
462 },
463 )
464 .with_field_path(&field.segment_path)
465 .with_rule(&field.ahb_status),
466 );
467 }
468 }
469 }
470
471 self.validate_codes_cross_field(workflow, ctx, report);
476 self.validate_answer_lists(workflow, ctx, report);
477
478 self.validate_package_cardinality(workflow, ctx, report);
481 }
482
483 fn validate_package_cardinality(
490 &self,
491 workflow: &AhbWorkflow,
492 ctx: &EvaluationContext,
493 report: &mut ValidationReport,
494 ) {
495 struct PackageGroup {
498 min: u32,
499 max: u32,
500 max_sum: u32,
503 bounds: BTreeSet<(u32, u32)>,
504 code_values: Vec<String>,
505 element_index: usize,
506 component_index: usize,
507 }
508
509 let mut groups: HashMap<(String, Option<String>, u32, u32, u32), PackageGroup> =
518 HashMap::new();
519 let mut alternatives: HashMap<(String, Option<String>, u32), BTreeSet<u32>> =
523 HashMap::new();
524
525 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
526
527 for field in &workflow.fields {
528 let el_idx = field.element_index.unwrap_or(0);
529 let comp_idx = field.component_index.unwrap_or(0);
530
531 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
533 continue;
534 }
535
536 for code in &field.codes {
537 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
539 let mut packages = Vec::new();
541 collect_packages(&expr, &mut packages);
542
543 if packages.len() > 1 {
544 let ids: BTreeSet<u32> = packages.iter().map(|(id, _, _)| *id).collect();
545 for id in &ids {
546 alternatives
547 .entry((field.segment_path.clone(), field.mig_number.clone(), *id))
548 .or_default()
549 .extend(ids.iter().copied().filter(|other| other != id));
550 }
551 }
552
553 for (pkg_id, pkg_min, pkg_max) in packages {
554 let key = (
555 field.segment_path.clone(),
556 field.mig_number.clone(),
557 pkg_id,
558 0,
559 0,
560 );
561 let group = groups.entry(key).or_insert_with(|| PackageGroup {
562 min: pkg_min,
563 max: pkg_max,
564 max_sum: 0,
565 bounds: BTreeSet::new(),
566 code_values: Vec::new(),
567 element_index: el_idx,
568 component_index: comp_idx,
569 });
570 group.bounds.insert((pkg_min, pkg_max));
577 group.min = group.min.min(pkg_min);
578 if !group.code_values.contains(&code.value) {
579 group.max_sum = group.max_sum.saturating_add(pkg_max);
580 }
581 group.max = group.max.max(pkg_max);
582 group.code_values.push(code.value.clone());
583 }
584 }
585 }
586 }
587
588 let mut field_packages: HashMap<(String, Option<String>), BTreeSet<u32>> = HashMap::new();
599 for (seg_path, mig_number, pkg_id, _, _) in groups.keys() {
600 field_packages
601 .entry((seg_path.clone(), mig_number.clone()))
602 .or_default()
603 .insert(*pkg_id);
604 }
605 let with_alternatives: Vec<(String, Option<String>)> = alternatives
606 .keys()
607 .map(|(p, m, _)| (p.clone(), m.clone()))
608 .collect();
609 for field in with_alternatives {
610 let Some(ids) = field_packages.get(&field) else {
611 continue;
612 };
613 for id in ids {
614 alternatives
615 .entry((field.0.clone(), field.1.clone(), *id))
616 .or_default()
617 .extend(ids.iter().copied().filter(|other| other != id));
618 }
619 }
620
621 type Counted = (Vec<usize>, Vec<bool>, usize, usize, Vec<String>);
623 let mut counted: HashMap<(String, Option<String>, u32, u32, u32), Counted> = HashMap::new();
624 for ((seg_path, mig_number, pkg_id, _, _), group) in &groups {
625 let segment_id = extract_segment_id(seg_path);
626
627 let mut unique_codes: Vec<&str> =
631 group.code_values.iter().map(|s| s.as_str()).collect();
632 unique_codes.sort_unstable();
633 unique_codes.dedup();
634 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
635
636 let group_path_str = extract_group_path_key(seg_path);
637 let group_path: Vec<&str> = if group_path_str.is_empty() {
638 Vec::new()
639 } else {
640 group_path_str.split('/').collect()
641 };
642
643 let min = group.min as usize;
644 let max = if group.bounds.len() > 1 {
645 group.max_sum as usize
646 } else {
647 group.max as usize
648 };
649
650 type Instance<'p> = (usize, Option<Vec<(&'p str, usize)>>);
653 let per_instance_counts: Option<Vec<Instance>> =
654 match (ctx.navigator, group_path.is_empty()) {
655 (Some(nav), false)
663 if group_path.len() > 1
664 && !full_instance_paths(nav, &group_path).is_empty() =>
665 {
666 let paths = full_instance_paths(nav, &group_path);
667 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
675 .as_deref()
676 {
677 Some(m) => (0..group_path.len())
678 .filter_map(|level| {
679 let prefix = group_path[..=level].join("/");
680 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
681 })
682 .collect(),
683 None => Vec::new(),
684 };
685 if paths.is_empty() {
686 None
687 } else {
688 Some(
689 paths
690 .into_iter()
691 .filter(|path| match mig_number.as_deref() {
692 Some(m) => nav.instance_at_has_mig_number(path, m),
693 None => true,
694 })
695 .filter(|path| {
696 variant_entries.iter().all(|(level, entry)| {
697 nav.find_segments_in_instance(
698 &entry.tag,
699 &path[..=*level],
700 )
701 .first()
702 .and_then(|seg| {
703 seg.elements
704 .get(entry.element)
705 .and_then(|e| e.get(entry.component))
706 .cloned()
707 })
708 .map_or(true, |v| entry.codes.contains(v.as_str()))
709 })
710 })
711 .map(|path| {
712 let count = nav
713 .find_segments_in_instance(&segment_id, &path)
714 .iter()
715 .filter_map(|seg| {
716 seg.elements
717 .get(group.element_index)
718 .and_then(|e| e.get(group.component_index))
719 .filter(|v| !v.is_empty())
720 .cloned()
721 })
722 .filter(|v| code_set.contains(v.as_str()))
723 .count();
724 (count, Some(path))
725 })
726 .collect(),
727 )
728 }
729 }
730 (Some(nav), false) => {
731 let instance_count = nav.group_instance_count(&group_path);
732 if instance_count == 0 {
733 None
734 } else {
735 Some(
743 (0..instance_count)
744 .filter(|i| match mig_number.as_deref() {
745 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
746 None => true,
747 })
748 .map(|i| {
749 let count = nav
750 .find_segments_in_group(&segment_id, &group_path, i)
751 .iter()
752 .filter_map(|seg| {
753 seg.elements
754 .get(group.element_index)
755 .and_then(|e| e.get(group.component_index))
756 .filter(|v| !v.is_empty())
757 .cloned()
758 })
759 .filter(|v| code_set.contains(v.as_str()))
760 .count();
761 let path = (group_path.len() == 1)
763 .then(|| vec![(group_path[0], i)]);
764 (count, path)
765 })
766 .collect(),
767 )
768 }
769 }
770 _ => None,
771 };
772
773 let instances = per_instance_counts.unwrap_or_else(|| {
774 let segments = ctx.find_segments(&segment_id);
775 let count = segments
776 .iter()
777 .filter_map(|seg| {
778 seg.elements
779 .get(group.element_index)
780 .and_then(|e| e.get(group.component_index))
781 .filter(|v| !v.is_empty())
782 .map(|s| s.as_str())
783 })
784 .filter(|v| code_set.contains(v))
785 .count();
786 vec![(count, None)]
787 });
788 let applies: Vec<bool> = instances
791 .iter()
792 .map(|(_, path)| match workflow.package_conditions.get(pkg_id) {
793 None => true,
794 Some(condition) => {
795 let result = match path {
796 Some(path) => expr_eval.evaluate(
797 condition,
798 &ctx.with_scope(crate::eval::GroupScope { path }),
799 ),
800 None => expr_eval.evaluate(condition, ctx),
801 };
802 result == ConditionResult::True
803 }
804 })
805 .collect();
806 let counts: Vec<usize> = instances.iter().map(|(c, _)| *c).collect();
807
808 counted.insert(
809 (
810 seg_path.clone(),
811 mig_number.clone(),
812 *pkg_id,
813 group.min,
814 group.max,
815 ),
816 (
817 counts,
818 applies,
819 min,
820 max,
821 unique_codes.iter().map(|s| s.to_string()).collect(),
822 ),
823 );
824 }
825
826 let mut keys: Vec<&(String, Option<String>, u32, u32, u32)> = counted.keys().collect();
831 keys.sort();
832 for key in keys {
833 let (counts, applies, min, max, codes) = &counted[key];
834 let (seg_path, mig_number, pkg_id, _, _) = key;
835 let holds = |c: &[usize], a: &[bool], lo: usize, hi: usize, i: usize| {
836 c.len() == counts.len() && a[i] && (lo..=hi).contains(&c[i]) && c[i] > 0
837 };
838 let alternative_holds = |i: usize| {
839 alternatives
840 .get(&(seg_path.clone(), mig_number.clone(), *pkg_id))
841 .is_some_and(|alts| {
842 counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
843 p == seg_path
844 && m == mig_number
845 && alts.contains(id)
846 && holds(c, a, *lo, *hi, i)
847 })
848 })
849 };
850 let applicable_alternative_holds = |i: usize| {
855 *min > 0
856 && counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
857 p == seg_path
858 && m == mig_number
859 && id != pkg_id
860 && *lo > 0
861 && holds(c, a, *lo, *hi, i)
862 })
863 };
864 let mut reported_counts: HashSet<usize> = HashSet::new();
865 for (i, &present_count) in counts.iter().enumerate() {
866 if applies[i]
867 && (present_count < *min || present_count > *max)
868 && !alternative_holds(i)
869 && !(present_count < *min && applicable_alternative_holds(i))
870 && reported_counts.insert(present_count)
871 {
872 report.add_issue(
873 ValidationIssue::new(
874 Severity::Error,
875 IssueKind::PackageCardinality {
876 package_id: pkg_id.to_string(),
877 present: present_count,
878 min: *min,
879 max: *max,
880 codes: codes.clone(),
881 },
882 )
883 .with_field_path(seg_path)
884 .with_expected(format!("{min}..{max}"))
885 .with_actual(present_count.to_string()),
886 );
887 }
888 }
889 }
890 }
891
892 fn validate_codes_cross_field(
904 &self,
905 workflow: &AhbWorkflow,
906 ctx: &EvaluationContext,
907 report: &mut ValidationReport,
908 ) {
909 if ctx.navigator.is_some() {
910 self.validate_codes_group_scoped(workflow, ctx, report);
911 } else {
912 self.validate_codes_tag_scoped(workflow, ctx, report);
913 }
914 }
915
916 fn validate_codes_group_scoped(
919 &self,
920 workflow: &AhbWorkflow,
921 ctx: &EvaluationContext,
922 report: &mut ValidationReport,
923 ) {
924 let by_loc = partition_codes_by_mig(workflow);
925 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
926 let nav = ctx.navigator.unwrap();
927
928 for ((group_key, tag), migs) in &by_loc {
929 let field_path = if group_key.is_empty() {
930 format!("{tag}/qualifier")
931 } else {
932 format!("{group_key}/{tag}/qualifier")
933 };
934
935 let group_path: Vec<&str> = if group_key.is_empty() {
936 Vec::new()
937 } else {
938 group_key.split('/').collect()
939 };
940
941 let tag_qualifiers = known_qualifiers.get(tag);
942
943 if group_path.is_empty() {
944 Self::validate_segments_per_mig(
945 &ctx.find_segments(tag),
946 migs,
947 tag_qualifiers,
948 tag,
949 &field_path,
950 report,
951 );
952 } else {
953 let instance_count = nav.group_instance_count(&group_path);
954 for i in 0..instance_count {
955 let owned = nav.find_segments_in_group(tag, &group_path, i);
956 let refs: Vec<&OwnedSegment> = owned.iter().collect();
957 Self::validate_segments_per_mig(
958 &refs,
959 migs,
960 tag_qualifiers,
961 tag,
962 &field_path,
963 report,
964 );
965 }
966 }
967 }
968 }
969
970 fn validate_codes_tag_scoped(
974 &self,
975 workflow: &AhbWorkflow,
976 ctx: &EvaluationContext,
977 report: &mut ValidationReport,
978 ) {
979 let by_loc = partition_codes_by_mig(workflow);
980 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
981 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
983 for ((_group_key, tag), migs) in by_loc {
984 let merged = by_tag.entry(tag).or_default();
985 for (mig_key, bucket) in migs {
986 let entry = merged.entry(mig_key).or_default();
987 if entry.qualifier_position.is_none() {
988 entry.qualifier_position = bucket.qualifier_position;
989 }
990 if entry.qualifier_position == bucket.qualifier_position {
991 entry.qualifier_values.extend(&bucket.qualifier_values);
992 }
993 for (pos, codes) in bucket.codes {
994 entry.codes.entry(pos).or_default().extend(codes);
995 }
996 }
997 }
998
999 for (tag, migs) in &by_tag {
1000 let field_path = format!("{tag}/qualifier");
1001 Self::validate_segments_per_mig(
1002 &ctx.find_segments(tag),
1003 migs,
1004 known_qualifiers.get(tag),
1005 tag,
1006 &field_path,
1007 report,
1008 );
1009 }
1010 }
1011
1012 fn validate_segments_per_mig(
1021 segments: &[&OwnedSegment],
1022 migs: &HashMap<Option<String>, MigCodeBucket>,
1023 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
1024 tag: &str,
1025 field_path: &str,
1026 report: &mut ValidationReport,
1027 ) {
1028 for seg in segments {
1029 match match_segment_to_mig(seg, migs) {
1030 Some(bucket) => {
1031 for ((el, c), allowed) in &bucket.codes {
1032 if allowed.is_empty() {
1033 continue;
1034 }
1035 Self::check_segments_against_codes(
1036 vec![*seg],
1037 allowed,
1038 tag,
1039 *el,
1040 *c,
1041 field_path,
1042 report,
1043 );
1044 }
1045 }
1046 None => {
1047 let Some(qualifiers) = tag_qualifiers else {
1048 continue;
1049 };
1050 for ((el, c), allowed) in qualifiers {
1055 let Some(actual) = seg
1056 .elements
1057 .get(*el)
1058 .and_then(|e| e.get(*c))
1059 .filter(|v| !v.is_empty())
1060 .map(|s| s.as_str())
1061 else {
1062 continue;
1063 };
1064 if allowed.iter().any(|v| v == actual) {
1065 break;
1067 }
1068 let bucket_values: HashSet<&str> = migs
1071 .values()
1072 .filter(|b| b.qualifier_position == Some((*el, *c)))
1073 .flat_map(|b| b.qualifier_values.iter().copied())
1074 .collect();
1075 if !bucket_values.is_empty() {
1076 Self::check_segments_against_codes(
1077 vec![*seg],
1078 &bucket_values,
1079 tag,
1080 *el,
1081 *c,
1082 field_path,
1083 report,
1084 );
1085 break;
1086 }
1087 }
1088 }
1089 }
1090 }
1091 }
1092
1093 fn validate_answer_lists(
1102 &self,
1103 workflow: &AhbWorkflow,
1104 ctx: &EvaluationContext,
1105 report: &mut ValidationReport,
1106 ) {
1107 let lists = AnswerCodeLists::embedded();
1108 let fv = self.evaluator.format_version();
1109 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1110 for list_rule in &workflow.fields {
1111 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1112 continue;
1113 };
1114 if list_rule.codes.is_empty()
1115 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1116 {
1117 continue;
1118 }
1119 let Some(answer_rule) = workflow
1120 .fields
1121 .iter()
1122 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1123 else {
1124 continue;
1125 };
1126 let at = |rule: &AhbFieldRule| {
1127 (
1128 rule.element_index.unwrap_or(0),
1129 rule.component_index.unwrap_or(0),
1130 )
1131 };
1132 let (list_el, list_c) = at(list_rule);
1133 let (answer_el, answer_c) = at(answer_rule);
1134 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1135 seg.elements
1136 .get(el)
1137 .and_then(|e| e.get(c))
1138 .filter(|v| !v.is_empty())
1139 .cloned()
1140 };
1141 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1142 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1143 continue;
1144 }
1145 let (Some(list), Some(answer)) =
1146 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1147 else {
1148 continue;
1149 };
1150 if !list_rule.codes.iter().any(|c| c.value == list) {
1151 continue;
1152 }
1153 if answer_rule.codes.iter().any(|c| c.value == answer) {
1157 continue;
1158 }
1159 let Some(codes) = lists.codes(fv, &list) else {
1160 continue;
1161 };
1162 if codes.contains(&answer) {
1163 continue;
1164 }
1165 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1166 allowed.sort_unstable();
1167 report.add_issue(
1168 ValidationIssue::new(
1169 Severity::Warning,
1170 IssueKind::AnswerNotInList {
1171 answer: answer.clone(),
1172 list: list.clone(),
1173 allowed: allowed.clone(),
1174 },
1175 )
1176 .with_field_path(&answer_rule.segment_path)
1177 .with_actual(&answer)
1178 .with_expected(allowed.join(", ")),
1179 );
1180 }
1181 }
1182 }
1183
1184 fn check_segments_against_codes(
1186 segments: Vec<&OwnedSegment>,
1187 allowed_codes: &HashSet<&str>,
1188 _tag: &str,
1189 el_idx: usize,
1190 comp_idx: usize,
1191 field_path: &str,
1192 report: &mut ValidationReport,
1193 ) {
1194 for segment in segments {
1195 if let Some(code_value) = segment
1196 .elements
1197 .get(el_idx)
1198 .and_then(|e| e.get(comp_idx))
1199 .filter(|v| !v.is_empty())
1200 {
1201 if !allowed_codes.contains(code_value.as_str()) {
1202 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1203 sorted_codes.sort_unstable();
1204 let kind = IssueKind::CodeNotAllowedForPid {
1205 value: code_value.to_string(),
1206 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1207 };
1208 report.add_issue(
1209 ValidationIssue::new(Severity::Error, kind)
1210 .with_field_path(field_path)
1211 .with_actual(code_value)
1212 .with_expected(sorted_codes.join(", ")),
1213 );
1214 }
1215 }
1216 }
1217 }
1218}
1219
1220fn full_instance_paths<'p>(
1223 nav: &dyn GroupNavigator,
1224 group_path: &[&'p str],
1225) -> Vec<Vec<(&'p str, usize)>> {
1226 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1227 for g in group_path {
1228 paths = paths
1229 .into_iter()
1230 .flat_map(|parent| {
1231 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1232 let mut path = parent.clone();
1233 path.push((*g, i));
1234 path
1235 })
1236 })
1237 .collect();
1238 }
1239 paths
1240}
1241
1242struct VariantEntry<'a> {
1245 tag: String,
1246 element: usize,
1247 component: usize,
1248 codes: HashSet<&'a str>,
1249}
1250
1251fn merged_variant_entry<'a>(
1257 workflow: &'a AhbWorkflow,
1258 group_path: &str,
1259 mig: &str,
1260) -> Option<VariantEntry<'a>> {
1261 let mig: u32 = mig.parse().ok()?;
1262 let prefix = format!("{group_path}/");
1263 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1265 .fields
1266 .iter()
1267 .filter_map(|r| {
1268 let rest = r.segment_path.strip_prefix(&prefix)?;
1269 let tag = rest.split('/').next()?;
1270 if tag.starts_with("SG") {
1271 return None;
1272 }
1273 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1274 })
1275 .collect();
1276 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1278 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1279 .iter()
1280 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1281 .collect();
1282 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1283 if numbers.len() < 2 {
1284 return None;
1285 }
1286 let own = entries
1287 .iter()
1288 .filter(|(_, n, _)| *n <= mig)
1289 .map(|(_, n, _)| *n)
1290 .max()?;
1291 let (rule, _, tag) = entries
1293 .iter()
1294 .filter(|(_, n, _)| *n == own)
1295 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1296 Some(VariantEntry {
1297 tag: tag.to_string(),
1298 element: rule.element_index.unwrap_or(0),
1299 component: rule.component_index.unwrap_or(0),
1300 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1301 })
1302}
1303
1304fn value_length(format: &str, value: &str) -> usize {
1308 let format = format.trim();
1309 if format.starts_with('n') {
1310 value
1311 .chars()
1312 .filter(|c| !matches!(c, '.' | ',' | '-'))
1313 .count()
1314 } else {
1315 value.chars().count()
1316 }
1317}
1318
1319fn max_length(format: &str) -> Option<usize> {
1322 let digits = format
1323 .trim()
1324 .strip_prefix("an")
1325 .or_else(|| format.trim().strip_prefix('a'))
1326 .or_else(|| format.trim().strip_prefix('n'))?;
1327 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1328}
1329
1330fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1340 let derived_from_codes = field
1341 .codes
1342 .first()
1343 .is_some_and(|c| c.ahb_status == field.ahb_status);
1344 if !derived_from_codes {
1345 return &field.ahb_status;
1346 }
1347 value
1348 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1349 .map_or(&field.ahb_status, |c| &c.ahb_status)
1350}
1351
1352fn should_skip_for_parent_group<E: ConditionEvaluator>(
1358 field: &AhbFieldRule,
1359 expr_eval: &ConditionExprEvaluator<E>,
1360 ctx: &EvaluationContext,
1361 ub_definitions: &BTreeMap<String, ConditionExpr>,
1362) -> bool {
1363 if let Some(ref group_status) = field.parent_group_ahb_status {
1364 if group_status.contains('[') {
1365 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
1366 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
1367 }
1368 }
1369 false
1370}
1371
1372fn is_parent_group_required<E: ConditionEvaluator>(
1379 field: &AhbFieldRule,
1380 expr_eval: &ConditionExprEvaluator<E>,
1381 ctx: &EvaluationContext,
1382 ub_definitions: &BTreeMap<String, ConditionExpr>,
1383) -> bool {
1384 field
1385 .parent_group_ahb_status
1386 .as_deref()
1387 .is_some_and(|status| {
1388 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1389 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1390 })
1391}
1392
1393fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1402 let segment_id = extract_segment_id(&field.segment_path);
1403
1404 if !field.codes.is_empty() {
1410 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1411 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1412 let matching = ctx.find_segments(&segment_id);
1413 return matching.iter().any(|seg| {
1414 seg.elements
1415 .get(el_idx)
1416 .and_then(|e| e.get(comp_idx))
1417 .is_some_and(|v| required_codes.contains(&v.as_str()))
1418 });
1419 }
1420 if is_qualifier_field(&field.segment_path) {
1423 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1424 let el_idx = field.element_index.unwrap_or(0);
1425 let comp_idx = field.component_index.unwrap_or(0);
1426 let matching = ctx.find_segments(&segment_id);
1427 return matching.iter().any(|seg| {
1428 seg.elements
1429 .get(el_idx)
1430 .and_then(|e| e.get(comp_idx))
1431 .is_some_and(|v| required_codes.contains(&v.as_str()))
1432 });
1433 }
1434 }
1435
1436 ctx.has_segment(&segment_id)
1437}
1438
1439fn is_group_variant_absent(
1454 ctx: &EvaluationContext,
1455 field: &AhbFieldRule,
1456 is_group_mandatory: bool,
1457) -> bool {
1458 let group_path: Vec<&str> = field
1459 .segment_path
1460 .split('/')
1461 .take_while(|p| p.starts_with("SG"))
1462 .collect();
1463
1464 if group_path.is_empty() {
1465 return false;
1466 }
1467
1468 let nav = match ctx.navigator {
1469 Some(nav) => nav,
1470 None => return false,
1471 };
1472
1473 let instance_count = nav.group_instance_count(&group_path);
1474
1475 if instance_count == 0 {
1480 if !is_group_mandatory {
1481 return true;
1482 }
1483 return false;
1485 }
1486
1487 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1492 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1494 let segment_id = extract_segment_id(&field.segment_path);
1495 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1496
1497 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1498 nav.find_segments_in_group(&segment_id, &group_path, i)
1499 .iter()
1500 .any(|seg| {
1501 seg.elements
1502 .first()
1503 .and_then(|e| e.first())
1504 .is_some_and(|v| required_codes.contains(&v.as_str()))
1505 })
1506 });
1507
1508 if !any_instance_has_qualifier {
1509 return true; }
1511 }
1512 }
1513
1514 let segment_id = extract_segment_id(&field.segment_path);
1523 let segment_absent_from_all = (0..instance_count).all(|i| {
1524 nav.find_segments_in_group(&segment_id, &group_path, i)
1525 .is_empty()
1526 });
1527 if segment_absent_from_all {
1528 let group_has_other_segments =
1529 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1530 if group_has_other_segments {
1531 return true;
1532 }
1533 }
1534
1535 false
1536}
1537
1538fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1540 out.extend(group.fields.iter());
1541 for child in &group.children {
1542 collect_nodes_depth_first(child, out);
1543 }
1544}
1545
1546#[allow(clippy::too_many_arguments)]
1550fn evaluate_node<E: ConditionEvaluator>(
1551 node: &AhbNode,
1552 ctx: &EvaluationContext,
1553 expr_eval: &ConditionExprEvaluator<E>,
1554 evaluator: &E,
1555 ub_definitions: &BTreeMap<String, ConditionExpr>,
1556 tag_migs: &HashMap<String, HashSet<&str>>,
1557 instance_index: Option<usize>,
1558 slot_holds: bool,
1559 report: &mut ValidationReport,
1560) {
1561 let field = node.rule;
1562
1563 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1565
1566 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1571 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1572 if length > max {
1573 let mut issue = ValidationIssue::new(
1574 Severity::Error,
1575 IssueKind::ValueTooLong {
1576 field_name: field.name.clone(),
1577 length,
1578 max,
1579 },
1580 )
1581 .with_field_path(&field.segment_path)
1582 .with_actual(value)
1583 .with_expected(field.format.clone().unwrap_or_default());
1584 if let Some(idx) = instance_index {
1585 issue = issue.with_instance_index(idx);
1586 }
1587 if let Some(num) = node.matched_segment_number {
1588 issue = issue.with_position(crate::SegmentPosition {
1589 segment_number: num,
1590 byte_offset: 0,
1591 message_number: 1,
1592 });
1593 }
1594 report.add_issue(attach_field_position(issue, field));
1595 }
1596 }
1597
1598 if let (Some(value), Some(format)) = (node.value, field.format.as_deref()) {
1601 if slot_holds
1602 && format.trim().starts_with('n')
1603 && field.codes.is_empty()
1604 && !value.is_empty()
1605 && !crate::eval::format_validators::is_edifact_number(value)
1606 {
1607 let mut issue = ValidationIssue::new(
1608 Severity::Error,
1609 IssueKind::ValueNotNumeric {
1610 field_name: field.name.clone(),
1611 },
1612 )
1613 .with_field_path(&field.segment_path)
1614 .with_actual(value)
1615 .with_expected(format);
1616 if let Some(idx) = instance_index {
1617 issue = issue.with_instance_index(idx);
1618 }
1619 if let Some(num) = node.matched_segment_number {
1620 issue = issue.with_position(crate::SegmentPosition {
1621 segment_number: num,
1622 byte_offset: 0,
1623 message_number: 1,
1624 });
1625 }
1626 report.add_issue(attach_field_position(issue, field));
1627 }
1628 }
1629
1630 if should_skip_for_parent_group(field, expr_eval, ctx, ub_definitions) {
1632 return;
1633 }
1634
1635 let status = governing_status(field, node.value);
1638 let StatusResolution {
1639 result: condition_result,
1640 kind,
1641 unknown_ids,
1642 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1643 let mandatory = kind == Some(StatusKind::Muss);
1644
1645 match condition_result {
1646 ConditionResult::True => {
1647 if mandatory && node.value.is_none() {
1656 let tag = extract_segment_id(&field.segment_path);
1657 let single_variant_present = node.segment_elements.is_none()
1658 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1659 && is_field_present(ctx, field);
1660 let segment_optional_and_absent = node.segment_elements.is_none()
1665 && field
1666 .segment_ahb_status
1667 .as_deref()
1668 .is_some_and(is_optional_segment_status);
1669 let segment_not_required_and_absent = node.segment_elements.is_none()
1675 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1676 expr_eval
1677 .resolve_status(st, &node_ctx, ub_definitions)
1678 .result
1679 != ConditionResult::True
1680 });
1681 if !single_variant_present
1682 && !segment_optional_and_absent
1683 && !segment_not_required_and_absent
1684 {
1685 let mut issue = ValidationIssue::new(
1686 Severity::Error,
1687 IssueKind::MissingRequiredField {
1688 field_name: field.name.clone(),
1689 },
1690 )
1691 .with_field_path(&field.segment_path)
1692 .with_rule(&field.ahb_status);
1693 if let Some(first_code) = field.codes.first() {
1694 issue.expected_value = Some(first_code.value.clone());
1695 }
1696 if let Some(idx) = instance_index {
1697 issue = issue.with_instance_index(idx);
1698 }
1699 if let Some(num) = node.matched_segment_number {
1700 issue = issue.with_position(crate::SegmentPosition {
1701 segment_number: num,
1702 byte_offset: 0,
1703 message_number: 1,
1704 });
1705 }
1706 issue = attach_field_position(issue, field);
1707 report.add_issue(issue);
1708 }
1709 }
1710 }
1711 ConditionResult::False => {
1712 if mandatory && node.value.is_some() {
1715 let mut issue = ValidationIssue::new(
1716 Severity::Error,
1717 IssueKind::FieldConditionNotSatisfied {
1718 field_name: field.name.clone(),
1719 },
1720 )
1721 .with_field_path(&field.segment_path)
1722 .with_rule(&field.ahb_status);
1723 if let Some(idx) = instance_index {
1724 issue = issue.with_instance_index(idx);
1725 }
1726 if let Some(num) = node.matched_segment_number {
1727 issue = issue.with_position(crate::SegmentPosition {
1728 segment_number: num,
1729 byte_offset: 0,
1730 message_number: 1,
1731 });
1732 }
1733 issue = attach_field_position(issue, field);
1734 report.add_issue(issue);
1735 }
1736 }
1737 ConditionResult::Unknown => {
1738 let mut external_ids = Vec::new();
1740 let mut undetermined_ids = Vec::new();
1741 let mut missing_ids = Vec::new();
1742 for id in unknown_ids {
1743 if evaluator.is_external(id) {
1744 external_ids.push(id);
1745 } else if evaluator.is_known(id) {
1746 undetermined_ids.push(id);
1747 } else {
1748 missing_ids.push(id);
1749 }
1750 }
1751
1752 let mut issue = ValidationIssue::new(
1753 Severity::Info,
1754 IssueKind::ConditionUnknown {
1755 field_name: field.name.clone(),
1756 unresolved: UnresolvedConditions {
1757 external: external_ids,
1758 undetermined: undetermined_ids,
1759 missing: missing_ids,
1760 },
1761 },
1762 )
1763 .with_field_path(&field.segment_path)
1764 .with_rule(&field.ahb_status);
1765 if let Some(idx) = instance_index {
1766 issue = issue.with_instance_index(idx);
1767 }
1768 if let Some(num) = node.matched_segment_number {
1769 issue = issue.with_position(crate::SegmentPosition {
1770 segment_number: num,
1771 byte_offset: 0,
1772 message_number: 1,
1773 });
1774 }
1775 issue = attach_field_position(issue, field);
1776 report.add_issue(issue);
1777 }
1778 }
1779}
1780
1781fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1786 match field.element_index {
1787 Some(el) => {
1788 let element_pos = (el as u32) + 2;
1789 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1790 issue.with_field_position(element_pos, component_pos)
1791 }
1792 None => issue,
1793 }
1794}
1795
1796fn report_repeated_in_transaction(
1803 transaction: &AhbGroupNode,
1804 transaction_index: usize,
1805 once: u32,
1806 ctx: &EvaluationContext,
1807 report: &mut ValidationReport,
1808) {
1809 let mut counts: BTreeMap<(String, String), (usize, String)> = BTreeMap::new();
1811 fn visit<'a>(
1812 node: &'a AhbGroupNode<'a>,
1813 path: &mut Vec<(&'a str, usize)>,
1814 once: u32,
1815 ctx: &EvaluationContext,
1816 counts: &mut BTreeMap<(String, String), (usize, String)>,
1817 ) {
1818 let ids: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1819 let mut seen: HashSet<Option<&str>> = HashSet::new();
1820 for n in &node.fields {
1821 let rule = n.rule;
1822 let Some(status) = rule.segment_ahb_status.as_deref() else {
1823 continue;
1824 };
1825 if Some(status) == node.ahb_status
1826 || rule.element_index.unwrap_or(0) != 0
1827 || rule.component_index.unwrap_or(0) != 0
1828 || rule.codes.is_empty()
1829 || !crate::eval::binds_once(status, once)
1830 || !seen.insert(rule.mig_number.as_deref())
1831 {
1832 continue;
1833 }
1834 let tag = extract_segment_id(&rule.segment_path);
1835 let sent = ctx.navigator().map_or(0, |nav| {
1836 nav.find_segments_in_instance(&tag, path)
1837 .iter()
1838 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
1839 .count()
1840 });
1841 let entry = counts
1842 .entry((
1843 format!("{}/{tag}", ids.join("/")),
1844 rule.mig_number.clone().unwrap_or_default(),
1845 ))
1846 .or_insert((0, status.to_string()));
1847 entry.0 += sent;
1848 }
1849 for child in &node.children {
1850 if let Some(status) = child
1851 .ahb_status
1852 .filter(|s| crate::eval::binds_once(s, once))
1853 {
1854 let variant = child
1855 .fields
1856 .first()
1857 .and_then(|n| n.rule.mig_number.clone())
1858 .unwrap_or_default();
1859 let entry = counts
1860 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
1861 .or_insert((0, status.to_string()));
1862 entry.0 += 1;
1863 }
1864 path.push((child.group_id, child.instance_index));
1865 visit(child, path, once, ctx, counts);
1866 path.pop();
1867 }
1868 }
1869 let mut path = vec![(transaction.group_id, transaction_index)];
1870 visit(transaction, &mut path, once, ctx, &mut counts);
1871 for ((path, _), (count, status)) in counts {
1872 if count > 1 {
1873 report.add_issue(
1874 ValidationIssue::new(
1875 Severity::Error,
1876 IssueKind::RepeatedInTransaction {
1877 path: path.clone(),
1878 count,
1879 },
1880 )
1881 .with_field_path(path)
1882 .with_rule(status)
1883 .with_instance_index(transaction_index),
1884 );
1885 }
1886 }
1887}
1888
1889fn refuse_segments<E: ConditionEvaluator>(
1895 group: &AhbGroupNode,
1896 path: &[(&str, usize)],
1897 scoped_ctx: &EvaluationContext,
1898 evaluator: &E,
1899 ub_definitions: &BTreeMap<String, ConditionExpr>,
1900 instance_index: usize,
1901 report: &mut ValidationReport,
1902) {
1903 let mut seen: Vec<u32> = Vec::new();
1904 for node in &group.fields {
1905 let rule = node.rule;
1906 let (Some(status), Some(value), Some(number)) = (
1907 rule.segment_ahb_status.as_deref(),
1908 node.value,
1909 node.matched_segment_number,
1910 ) else {
1911 continue;
1912 };
1913 let qualifier =
1916 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
1917 if !qualifier
1918 || seen.contains(&number)
1919 || Some(status) == group.ahb_status
1920 || !rule.codes.iter().any(|c| c.value == value)
1921 {
1922 continue;
1923 }
1924 seen.push(number);
1925 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1926 continue;
1927 }
1928 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1929 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
1930 report.add_issue(
1931 ValidationIssue::new(
1932 Severity::Warning,
1933 IssueKind::SegmentNotAllowed {
1934 segment: segment.join("/"),
1935 },
1936 )
1937 .with_field_path(segment.join("/"))
1938 .with_rule(status)
1939 .with_instance_index(instance_index)
1940 .with_position(crate::SegmentPosition {
1941 segment_number: number,
1942 byte_offset: 0,
1943 message_number: 1,
1944 }),
1945 );
1946 }
1947}
1948
1949#[allow(clippy::too_many_arguments)]
1958fn walk_group_instance<E: ConditionEvaluator>(
1959 group: &AhbGroupNode,
1960 path: &[(&str, usize)],
1961 scoped_ctx: &EvaluationContext,
1962 expr_eval: &ConditionExprEvaluator<E>,
1963 evaluator: &E,
1964 ub_definitions: &BTreeMap<String, ConditionExpr>,
1965 tag_migs: &HashMap<String, HashSet<&str>>,
1966 instance_index: usize,
1967 report_segments: bool,
1968 report: &mut ValidationReport,
1969) {
1970 let mut group_refused = false;
1973 if let Some(status) = group.ahb_status {
1974 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1975 group_refused = true;
1976 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1977 let mut issue = ValidationIssue::new(
1978 Severity::Warning,
1979 IssueKind::GroupNotAllowed {
1980 group: group_path.join("/"),
1981 },
1982 )
1983 .with_field_path(group_path.join("/"))
1984 .with_rule(status)
1985 .with_instance_index(instance_index);
1986 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
1987 issue = issue.with_position(crate::SegmentPosition {
1988 segment_number: num,
1989 byte_offset: 0,
1990 message_number: 1,
1991 });
1992 }
1993 report.add_issue(issue);
1994 }
1995 }
1996 if report_segments && !group_refused {
1997 refuse_segments(
1998 group,
1999 path,
2000 scoped_ctx,
2001 evaluator,
2002 ub_definitions,
2003 instance_index,
2004 report,
2005 );
2006 }
2007 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
2011 .fields
2012 .iter()
2013 .filter(|n| {
2014 n.rule.element_index.unwrap_or(0) == 0
2015 && n.rule.component_index.unwrap_or(0) == 0
2016 && !n.rule.codes.is_empty()
2017 })
2018 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
2019 .collect();
2020 let holds = |node: &AhbNode| {
2021 let (Some(codes), Some(elements)) = (
2022 slot_codes.get(&node.rule.mig_number.as_deref()),
2023 node.segment_elements,
2024 ) else {
2025 return true;
2026 };
2027 let qualifier = elements
2028 .first()
2029 .and_then(|e| e.first())
2030 .map(String::as_str)
2031 .unwrap_or_default();
2032 codes.iter().any(|c| c.value == qualifier)
2033 };
2034 for node in &group.fields {
2035 evaluate_node(
2036 node,
2037 scoped_ctx,
2038 expr_eval,
2039 evaluator,
2040 ub_definitions,
2041 tag_migs,
2042 Some(instance_index),
2043 holds(node),
2044 report,
2045 );
2046 }
2047 for child in &group.children {
2048 let mut child_path = path.to_vec();
2049 child_path.push((child.group_id, child.instance_index));
2050 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2051 walk_group_instance(
2052 child,
2053 &child_path,
2054 &child_ctx,
2055 expr_eval,
2056 evaluator,
2057 ub_definitions,
2058 tag_migs,
2059 instance_index,
2060 report_segments,
2061 report,
2062 );
2063 }
2064}
2065
2066fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2068 match expr {
2069 ConditionExpr::Package { id, min, max } => {
2070 out.push((*id, *min, *max));
2071 }
2072 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2073 for e in exprs {
2074 collect_packages(e, out);
2075 }
2076 }
2077 ConditionExpr::Xor(left, right) => {
2078 collect_packages(left, out);
2079 collect_packages(right, out);
2080 }
2081 ConditionExpr::Not(inner) => {
2082 collect_packages(inner, out);
2083 }
2084 ConditionExpr::Ref(_) => {}
2085 }
2086}
2087
2088fn is_optional_segment_status(status: &str) -> bool {
2098 let trimmed = status.trim();
2099 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
2100}
2101
2102fn is_qualifier_field(path: &str) -> bool {
2112 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
2113 matches!(parts.len(), 2 | 3)
2115}
2116
2117#[derive(Default)]
2125struct MigCodeBucket<'a> {
2126 qualifier_position: Option<(usize, usize)>,
2129 qualifier_values: HashSet<&'a str>,
2132 codes: HashMap<(usize, usize), HashSet<&'a str>>,
2135}
2136
2137fn partition_codes_by_mig(
2142 workflow: &AhbWorkflow,
2143) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
2144 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
2145 for field in &workflow.fields {
2146 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2147 continue;
2148 }
2149 let tag = extract_segment_id(&field.segment_path);
2150 let group_key = extract_group_path_key(&field.segment_path);
2151 let mig = field.mig_number.clone();
2152 let el = field.element_index.unwrap_or(0);
2153 let c = field.component_index.unwrap_or(0);
2154
2155 let bucket = out
2156 .entry((group_key, tag))
2157 .or_default()
2158 .entry(mig)
2159 .or_default();
2160
2161 let required: Vec<&str> = field
2168 .codes
2169 .iter()
2170 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2171 .map(|code| code.value.as_str())
2172 .collect();
2173
2174 if !required.is_empty() {
2181 if bucket.qualifier_position.is_none() {
2182 bucket.qualifier_position = Some((el, c));
2183 }
2184 if bucket.qualifier_position == Some((el, c)) {
2185 bucket.qualifier_values.extend(required.iter().copied());
2186 }
2187 }
2188
2189 for v in required {
2190 bucket.codes.entry((el, c)).or_default().insert(v);
2191 }
2192 }
2193 out
2194}
2195
2196fn global_qualifiers_by_tag(
2202 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
2203) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
2204 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
2205 for ((_group, tag), migs) in by_loc {
2206 let tag_entry = out.entry(tag.clone()).or_default();
2207 for bucket in migs.values() {
2208 if let Some(pos) = bucket.qualifier_position {
2209 tag_entry
2210 .entry(pos)
2211 .or_default()
2212 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2213 }
2214 }
2215 }
2216 out
2217}
2218
2219fn match_segment_to_mig<'a, 'b>(
2230 seg: &OwnedSegment,
2231 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
2232) -> Option<&'a MigCodeBucket<'b>> {
2233 let actual_at = |el: usize, c: usize| -> &str {
2234 seg.elements
2235 .get(el)
2236 .and_then(|e| e.get(c))
2237 .map(|s| s.as_str())
2238 .unwrap_or("")
2239 };
2240
2241 let mut best: Option<&MigCodeBucket> = None;
2242 let mut best_matches = 0usize;
2243
2244 for bucket in migs.values() {
2245 let Some((el, c)) = bucket.qualifier_position else {
2246 continue;
2247 };
2248 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2249 continue;
2250 }
2251 let extra_matches = bucket
2252 .codes
2253 .iter()
2254 .filter(|(pos, _)| **pos != (el, c))
2255 .filter(|((e, k), allowed)| {
2256 let v = actual_at(*e, *k);
2257 !v.is_empty() && allowed.contains(v)
2258 })
2259 .count();
2260 if best.is_none() || extra_matches > best_matches {
2261 best = Some(bucket);
2262 best_matches = extra_matches;
2263 }
2264 }
2265 best
2266}
2267
2268fn extract_group_path_key(path: &str) -> String {
2273 let sg_parts: Vec<&str> = path
2274 .split('/')
2275 .take_while(|p| p.starts_with("SG"))
2276 .collect();
2277 sg_parts.join("/")
2278}
2279
2280fn extract_segment_id(path: &str) -> String {
2282 for part in path.split('/') {
2283 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2285 continue;
2286 }
2287 if part.len() >= 3
2289 && part
2290 .chars()
2291 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2292 {
2293 return part.to_string();
2294 }
2295 }
2296 path.split('/').next_back().unwrap_or(path).to_string()
2298}
2299
2300pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2312 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2314 if unh_count > 1 {
2315 return Some(ValidationIssue::new(
2316 Severity::Error,
2317 IssueKind::UntCountNotVerifiable { unh_count },
2318 ));
2319 }
2320
2321 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2323 let declared: usize = unt.get_element(0).parse().ok()?;
2324
2325 let actual = segments
2327 .iter()
2328 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2329 .count();
2330
2331 if declared != actual {
2332 Some(
2333 ValidationIssue::new(
2334 Severity::Error,
2335 IssueKind::UntSegmentCountMismatch { declared, actual },
2336 )
2337 .with_field_path("UNT/0074")
2338 .with_expected(actual.to_string())
2339 .with_actual(declared.to_string()),
2340 )
2341 } else {
2342 None
2343 }
2344}
2345
2346#[cfg(test)]
2347mod tests {
2348
2349 #[test]
2350 fn a_numeric_length_counts_digits_only() {
2351 assert_eq!(value_length("n..15", "-12.50"), 4);
2352 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
2353 assert_eq!(value_length("n5", "55001"), 5);
2354 assert_eq!(value_length("an..3", "Z.1"), 3);
2355 assert_eq!(value_length("an..35", "Müller"), 6);
2356 }
2357 use super::super::codes::ErrorCodes;
2358 use super::super::issue::ValidationCategory;
2359 use super::*;
2360 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2361 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2362 use std::collections::HashMap;
2363
2364 fn narrate(issue: &ValidationIssue) -> String {
2367 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2368 }
2369
2370 struct MockEvaluator {
2372 results: HashMap<u32, CR>,
2373 }
2374
2375 impl MockEvaluator {
2376 fn new(results: Vec<(u32, CR)>) -> Self {
2377 Self {
2378 results: results.into_iter().collect(),
2379 }
2380 }
2381
2382 fn all_true(ids: &[u32]) -> Self {
2383 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2384 }
2385 }
2386
2387 impl ConditionEvaluator for MockEvaluator {
2388 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2389 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2390 }
2391 fn is_external(&self, _condition: u32) -> bool {
2392 false
2393 }
2394 fn message_type(&self) -> &str {
2395 "UTILMD"
2396 }
2397 fn format_version(&self) -> &str {
2398 "FV2510"
2399 }
2400 }
2401
2402 #[test]
2405 fn a_present_code_is_judged_by_its_own_status() {
2406 let code = |value: &str, status: &str| AhbCodeRule {
2408 value: value.to_string(),
2409 description: String::new(),
2410 ahb_status: status.to_string(),
2411 };
2412 let field = AhbFieldRule {
2413 segment_path: "SG4/STS/C556/9013".to_string(),
2414 ahb_status: "X [192]".to_string(),
2415 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2416 ..Default::default()
2417 };
2418 assert_eq!(governing_status(&field, Some("E03")), "X");
2419 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2420 assert_eq!(governing_status(&field, None), "X [192]");
2421 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2422
2423 let own = AhbFieldRule {
2425 ahb_status: "X [931]".to_string(),
2426 codes: vec![code("303", "X")],
2427 ..Default::default()
2428 };
2429 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2430 }
2431
2432 #[test]
2433 fn test_extract_segment_id_simple() {
2434 assert_eq!(extract_segment_id("NAD"), "NAD");
2435 }
2436
2437 #[test]
2438 fn test_extract_segment_id_with_sg_prefix() {
2439 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
2440 }
2441
2442 #[test]
2443 fn test_extract_segment_id_nested_sg() {
2444 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
2445 }
2446
2447 #[test]
2450 fn test_validate_missing_mandatory_field() {
2451 let evaluator = MockEvaluator::all_true(&[182, 152]);
2452 let validator = EdifactValidator::new(evaluator);
2453 let external = NoOpExternalProvider;
2454
2455 let workflow = AhbWorkflow {
2456 pruefidentifikator: "11001".to_string(),
2457 description: "Test".to_string(),
2458 communication_direction: None,
2459 fields: vec![AhbFieldRule {
2460 segment_path: "SG2/NAD/C082/3039".to_string(),
2461 name: "MP-ID des MSB".to_string(),
2462 ahb_status: "Muss [182] ∧ [152]".to_string(),
2463 codes: vec![],
2464 parent_group_ahb_status: None,
2465 segment_ahb_status: None,
2466 ..Default::default()
2467 }],
2468 ub_definitions: BTreeMap::new(),
2469 package_conditions: Default::default(),
2470 };
2471
2472 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2474
2475 assert!(!report.is_valid());
2477 let errors: Vec<_> = report.errors().collect();
2478 assert_eq!(errors.len(), 1);
2479 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2480 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
2481 }
2482
2483 #[test]
2484 fn test_validate_condition_false_no_error() {
2485 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
2487 let validator = EdifactValidator::new(evaluator);
2488 let external = NoOpExternalProvider;
2489
2490 let workflow = AhbWorkflow {
2491 pruefidentifikator: "11001".to_string(),
2492 description: "Test".to_string(),
2493 communication_direction: None,
2494 fields: vec![AhbFieldRule {
2495 segment_path: "NAD".to_string(),
2496 name: "Partnerrolle".to_string(),
2497 ahb_status: "Muss [182] ∧ [152]".to_string(),
2498 codes: vec![],
2499 parent_group_ahb_status: None,
2500 segment_ahb_status: None,
2501 ..Default::default()
2502 }],
2503 ub_definitions: BTreeMap::new(),
2504 package_conditions: Default::default(),
2505 };
2506
2507 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2508
2509 assert!(report.is_valid());
2511 }
2512
2513 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
2515 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
2516 let workflow = AhbWorkflow {
2517 pruefidentifikator: "11001".to_string(),
2518 description: "Test".to_string(),
2519 communication_direction: None,
2520 fields: vec![AhbFieldRule {
2521 segment_path: "SG2/NAD/C082/3039".to_string(),
2522 name: "MP-ID".to_string(),
2523 ahb_status: status.to_string(),
2524 ..Default::default()
2525 }],
2526 ub_definitions: BTreeMap::new(),
2527 package_conditions: Default::default(),
2528 };
2529 validator.validate(
2530 &[],
2531 &workflow,
2532 &NoOpExternalProvider,
2533 ValidationLevel::Conditions,
2534 )
2535 }
2536
2537 #[test]
2538 fn a_later_soll_line_alone_does_not_require_the_field() {
2539 let report = validate_absent_field(
2541 "Muss [2119] \r\nSoll [130]",
2542 vec![(2119, CR::False), (130, CR::True)],
2543 );
2544 assert!(
2545 report.is_valid(),
2546 "{:?}",
2547 report.errors().collect::<Vec<_>>()
2548 );
2549 }
2550
2551 #[test]
2552 fn a_later_muss_line_requires_the_field() {
2553 let report = validate_absent_field(
2555 "Soll [47]\r\nMuss [46]",
2556 vec![(47, CR::False), (46, CR::True)],
2557 );
2558 let errors: Vec<_> = report.errors().collect();
2559 assert_eq!(errors.len(), 1);
2560 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2561 }
2562
2563 #[test]
2564 fn a_weaker_unknown_line_does_not_stop_a_muss() {
2565 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
2567 let errors: Vec<_> = report.errors().collect();
2568 assert_eq!(errors.len(), 1);
2569 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2570 }
2571
2572 #[test]
2573 fn a_stricter_unknown_line_leaves_the_status_undecided() {
2574 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
2575 assert!(report.is_valid());
2576 let infos: Vec<_> = report.infos().collect();
2577 assert_eq!(infos.len(), 1, "{infos:?}");
2578 match &infos[0].kind {
2579 IssueKind::ConditionUnknown { unresolved, .. } => {
2580 assert_eq!(unresolved.missing, vec![2119]);
2581 }
2582 other => panic!("expected ConditionUnknown, got {other:?}"),
2583 }
2584 }
2585
2586 #[test]
2587 fn test_validate_condition_unknown_adds_info() {
2588 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
2590 let validator = EdifactValidator::new(evaluator);
2592 let external = NoOpExternalProvider;
2593
2594 let workflow = AhbWorkflow {
2595 pruefidentifikator: "11001".to_string(),
2596 description: "Test".to_string(),
2597 communication_direction: None,
2598 fields: vec![AhbFieldRule {
2599 segment_path: "NAD".to_string(),
2600 name: "Partnerrolle".to_string(),
2601 ahb_status: "Muss [182] ∧ [152]".to_string(),
2602 codes: vec![],
2603 parent_group_ahb_status: None,
2604 segment_ahb_status: None,
2605 ..Default::default()
2606 }],
2607 ub_definitions: BTreeMap::new(),
2608 package_conditions: Default::default(),
2609 };
2610
2611 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2612
2613 assert!(report.is_valid());
2615 let infos: Vec<_> = report.infos().collect();
2616 assert_eq!(infos.len(), 1);
2617 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
2618 }
2619
2620 #[test]
2621 fn test_validate_structure_level_skips_conditions() {
2622 let evaluator = MockEvaluator::all_true(&[182, 152]);
2623 let validator = EdifactValidator::new(evaluator);
2624 let external = NoOpExternalProvider;
2625
2626 let workflow = AhbWorkflow {
2627 pruefidentifikator: "11001".to_string(),
2628 description: "Test".to_string(),
2629 communication_direction: None,
2630 fields: vec![AhbFieldRule {
2631 segment_path: "NAD".to_string(),
2632 name: "Partnerrolle".to_string(),
2633 ahb_status: "Muss [182] ∧ [152]".to_string(),
2634 codes: vec![],
2635 parent_group_ahb_status: None,
2636 segment_ahb_status: None,
2637 ..Default::default()
2638 }],
2639 ub_definitions: BTreeMap::new(),
2640 package_conditions: Default::default(),
2641 };
2642
2643 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
2645
2646 assert!(report.is_valid());
2648 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
2649 }
2650
2651 #[test]
2652 fn test_validate_empty_workflow_no_condition_errors() {
2653 let evaluator = MockEvaluator::all_true(&[]);
2654 let validator = EdifactValidator::new(evaluator);
2655 let external = NoOpExternalProvider;
2656
2657 let empty_workflow = AhbWorkflow {
2658 pruefidentifikator: String::new(),
2659 description: String::new(),
2660 communication_direction: None,
2661 fields: vec![],
2662 ub_definitions: BTreeMap::new(),
2663 package_conditions: Default::default(),
2664 };
2665
2666 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
2667
2668 assert!(report.is_valid());
2669 }
2670
2671 #[test]
2672 fn test_validate_bare_muss_always_required() {
2673 let evaluator = MockEvaluator::new(vec![]);
2674 let validator = EdifactValidator::new(evaluator);
2675 let external = NoOpExternalProvider;
2676
2677 let workflow = AhbWorkflow {
2678 pruefidentifikator: "55001".to_string(),
2679 description: "Test".to_string(),
2680 communication_direction: Some("NB an LF".to_string()),
2681 fields: vec![AhbFieldRule {
2682 segment_path: "SG2/NAD/3035".to_string(),
2683 name: "Partnerrolle".to_string(),
2684 ahb_status: "Muss".to_string(), codes: vec![],
2686 parent_group_ahb_status: None,
2687 segment_ahb_status: None,
2688 ..Default::default()
2689 }],
2690 ub_definitions: BTreeMap::new(),
2691 package_conditions: Default::default(),
2692 };
2693
2694 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2695
2696 assert!(!report.is_valid());
2698 assert_eq!(report.error_count(), 1);
2699 }
2700
2701 #[test]
2702 fn test_validate_x_status_is_mandatory() {
2703 let evaluator = MockEvaluator::new(vec![]);
2704 let validator = EdifactValidator::new(evaluator);
2705 let external = NoOpExternalProvider;
2706
2707 let workflow = AhbWorkflow {
2708 pruefidentifikator: "55001".to_string(),
2709 description: "Test".to_string(),
2710 communication_direction: None,
2711 fields: vec![AhbFieldRule {
2712 segment_path: "DTM".to_string(),
2713 name: "Datum".to_string(),
2714 ahb_status: "X".to_string(),
2715 codes: vec![],
2716 parent_group_ahb_status: None,
2717 segment_ahb_status: None,
2718 ..Default::default()
2719 }],
2720 ub_definitions: BTreeMap::new(),
2721 package_conditions: Default::default(),
2722 };
2723
2724 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2725
2726 assert!(!report.is_valid());
2727 let errors: Vec<_> = report.errors().collect();
2728 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2729 }
2730
2731 #[test]
2732 fn test_validate_soll_not_mandatory() {
2733 let evaluator = MockEvaluator::new(vec![]);
2734 let validator = EdifactValidator::new(evaluator);
2735 let external = NoOpExternalProvider;
2736
2737 let workflow = AhbWorkflow {
2738 pruefidentifikator: "55001".to_string(),
2739 description: "Test".to_string(),
2740 communication_direction: None,
2741 fields: vec![AhbFieldRule {
2742 segment_path: "DTM".to_string(),
2743 name: "Datum".to_string(),
2744 ahb_status: "Soll".to_string(),
2745 codes: vec![],
2746 parent_group_ahb_status: None,
2747 segment_ahb_status: None,
2748 ..Default::default()
2749 }],
2750 ub_definitions: BTreeMap::new(),
2751 package_conditions: Default::default(),
2752 };
2753
2754 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2755
2756 assert!(report.is_valid());
2758 }
2759
2760 #[test]
2761 fn test_report_includes_metadata() {
2762 let evaluator = MockEvaluator::new(vec![]);
2763 let validator = EdifactValidator::new(evaluator);
2764 let external = NoOpExternalProvider;
2765
2766 let workflow = AhbWorkflow {
2767 pruefidentifikator: "55001".to_string(),
2768 description: String::new(),
2769 communication_direction: None,
2770 fields: vec![],
2771 ub_definitions: BTreeMap::new(),
2772 package_conditions: Default::default(),
2773 };
2774
2775 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
2776
2777 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
2778 assert_eq!(report.level, ValidationLevel::Full);
2779 assert_eq!(report.message_type, "UTILMD");
2780 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
2781 }
2782
2783 #[test]
2784 fn test_validate_with_navigator_returns_report() {
2785 let evaluator = MockEvaluator::all_true(&[]);
2786 let validator = EdifactValidator::new(evaluator);
2787 let external = NoOpExternalProvider;
2788 let nav = crate::eval::NoOpGroupNavigator;
2789
2790 let workflow = AhbWorkflow {
2791 pruefidentifikator: "55001".to_string(),
2792 description: "Test".to_string(),
2793 communication_direction: None,
2794 fields: vec![],
2795 ub_definitions: BTreeMap::new(),
2796 package_conditions: Default::default(),
2797 };
2798
2799 let report = validator.validate_with_navigator(
2800 &[],
2801 &workflow,
2802 &external,
2803 ValidationLevel::Full,
2804 &nav,
2805 );
2806 assert!(report.is_valid());
2807 }
2808
2809 #[test]
2810 fn test_code_validation_composite_paths_valid_codes() {
2811 let evaluator = MockEvaluator::new(vec![]);
2815 let validator = EdifactValidator::new(evaluator);
2816 let external = NoOpExternalProvider;
2817
2818 let unh_segment = OwnedSegment {
2819 id: "UNH".to_string(),
2820 elements: vec![
2821 vec!["ALEXANDE951842".to_string()],
2822 vec![
2823 "UTILMD".to_string(),
2824 "D".to_string(),
2825 "11A".to_string(),
2826 "UN".to_string(),
2827 "S2.1".to_string(),
2828 ],
2829 ],
2830 segment_number: 1,
2831 };
2832
2833 let workflow = AhbWorkflow {
2834 pruefidentifikator: "55001".to_string(),
2835 description: "Test".to_string(),
2836 communication_direction: None,
2837 fields: vec![
2838 AhbFieldRule {
2839 segment_path: "UNH/S009/0065".to_string(),
2840 name: "Nachrichtentyp".to_string(),
2841 ahb_status: "X".to_string(),
2842 codes: vec![AhbCodeRule {
2843 value: "UTILMD".to_string(),
2844 description: "Stammdaten".to_string(),
2845 ahb_status: "X".to_string(),
2846 }],
2847 parent_group_ahb_status: None,
2848 segment_ahb_status: None,
2849 element_index: Some(1),
2850 component_index: Some(0),
2851 ..Default::default()
2852 },
2853 AhbFieldRule {
2854 segment_path: "UNH/S009/0052".to_string(),
2855 name: "Version".to_string(),
2856 ahb_status: "X".to_string(),
2857 codes: vec![AhbCodeRule {
2858 value: "D".to_string(),
2859 description: "Draft".to_string(),
2860 ahb_status: "X".to_string(),
2861 }],
2862 parent_group_ahb_status: None,
2863 segment_ahb_status: None,
2864 element_index: Some(1),
2865 component_index: Some(1),
2866 ..Default::default()
2867 },
2868 ],
2869 ub_definitions: BTreeMap::new(),
2870 package_conditions: Default::default(),
2871 };
2872
2873 let report = validator.validate(
2874 &[unh_segment],
2875 &workflow,
2876 &external,
2877 ValidationLevel::Conditions,
2878 );
2879
2880 let code_errors: Vec<_> = report
2881 .by_category(ValidationCategory::Code)
2882 .filter(|i| i.severity == Severity::Error)
2883 .collect();
2884 assert!(
2885 code_errors.is_empty(),
2886 "Expected no code errors when composite values match allowed codes, got: {:?}",
2887 code_errors
2888 );
2889 }
2890
2891 #[test]
2892 fn test_code_validation_partitions_by_mig_number() {
2893 let evaluator = MockEvaluator::new(vec![]);
2897 let validator = EdifactValidator::new(evaluator);
2898 let external = NoOpExternalProvider;
2899
2900 let sts_7 = OwnedSegment {
2901 id: "STS".to_string(),
2902 elements: vec![
2903 vec!["7".to_string()],
2904 vec![String::new()],
2905 vec!["GH02".to_string()],
2906 vec!["ZW4".to_string()],
2907 ],
2908 segment_number: 1,
2909 };
2910 let sts_e01 = OwnedSegment {
2911 id: "STS".to_string(),
2912 elements: vec![
2913 vec!["E01".to_string()],
2914 vec![String::new()],
2915 vec!["A99".to_string(), "E_0614".to_string()],
2916 ],
2917 segment_number: 2,
2918 };
2919
2920 let workflow = AhbWorkflow {
2921 pruefidentifikator: "55018".to_string(),
2922 description: "Test".to_string(),
2923 communication_direction: None,
2924 fields: vec![
2925 AhbFieldRule {
2927 segment_path: "SG4/STS/C601/9015".to_string(),
2928 name: "Statuskategorie".to_string(),
2929 ahb_status: "X".to_string(),
2930 codes: vec![AhbCodeRule {
2931 value: "7".to_string(),
2932 description: "Transaktionsgrund".to_string(),
2933 ahb_status: "X".to_string(),
2934 }],
2935 parent_group_ahb_status: None,
2936 segment_ahb_status: None,
2937 element_index: Some(0),
2938 component_index: Some(0),
2939 mig_number: Some("00035".to_string()),
2940 format: None,
2941 },
2942 AhbFieldRule {
2943 segment_path: "SG4/STS/C556/9013".to_string(),
2944 name: "Statusanlaß".to_string(),
2945 ahb_status: "X".to_string(),
2946 codes: vec![AhbCodeRule {
2947 value: "E03".to_string(),
2948 description: "Transaktionsgrund".to_string(),
2949 ahb_status: "X".to_string(),
2950 }],
2951 parent_group_ahb_status: None,
2952 segment_ahb_status: None,
2953 element_index: Some(2),
2954 component_index: Some(0),
2955 mig_number: Some("00035".to_string()),
2956 format: None,
2957 },
2958 AhbFieldRule {
2960 segment_path: "SG4/STS/C601/9015".to_string(),
2961 name: "Statuskategorie".to_string(),
2962 ahb_status: "X".to_string(),
2963 codes: vec![AhbCodeRule {
2964 value: "E01".to_string(),
2965 description: "Antwort".to_string(),
2966 ahb_status: "X".to_string(),
2967 }],
2968 parent_group_ahb_status: None,
2969 segment_ahb_status: None,
2970 element_index: Some(0),
2971 component_index: Some(0),
2972 mig_number: Some("00036".to_string()),
2973 format: None,
2974 },
2975 ],
2976 ub_definitions: BTreeMap::new(),
2977 package_conditions: Default::default(),
2978 };
2979
2980 let report = validator.validate(
2981 &[sts_7, sts_e01],
2982 &workflow,
2983 &external,
2984 ValidationLevel::Conditions,
2985 );
2986
2987 let code_errors: Vec<_> = report
2988 .by_category(ValidationCategory::Code)
2989 .filter(|i| {
2990 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2991 })
2992 .collect();
2993 assert_eq!(
2994 code_errors.len(),
2995 1,
2996 "Expected one COD002 (for GH02 only), got: {:?}",
2997 code_errors
2998 );
2999 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3000 }
3001
3002 #[test]
3003 fn test_code_validation_composite_paths_detects_invalid_code() {
3004 let evaluator = MockEvaluator::new(vec![]);
3007 let validator = EdifactValidator::new(evaluator);
3008 let external = NoOpExternalProvider;
3009
3010 let sts_segment = OwnedSegment {
3011 id: "STS".to_string(),
3012 elements: vec![
3013 vec!["7".to_string()],
3014 vec![String::new()],
3015 vec!["GH02".to_string()],
3016 vec!["ZW4".to_string()],
3017 ],
3018 segment_number: 1,
3019 };
3020
3021 let workflow = AhbWorkflow {
3022 pruefidentifikator: "55018".to_string(),
3023 description: "Test".to_string(),
3024 communication_direction: None,
3025 fields: vec![AhbFieldRule {
3026 segment_path: "SG4/STS/C556/9013".to_string(),
3027 name: "Statusanlaß".to_string(),
3028 ahb_status: "X".to_string(),
3029 codes: vec![AhbCodeRule {
3030 value: "E03".to_string(),
3031 description: "Transaktionsgrund".to_string(),
3032 ahb_status: "X".to_string(),
3033 }],
3034 parent_group_ahb_status: None,
3035 segment_ahb_status: None,
3036 element_index: Some(2),
3037 component_index: Some(0),
3038 ..Default::default()
3039 }],
3040 ub_definitions: BTreeMap::new(),
3041 package_conditions: Default::default(),
3042 };
3043
3044 let report = validator.validate(
3045 &[sts_segment],
3046 &workflow,
3047 &external,
3048 ValidationLevel::Conditions,
3049 );
3050
3051 let code_errors: Vec<_> = report
3052 .by_category(ValidationCategory::Code)
3053 .filter(|i| {
3054 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3055 })
3056 .collect();
3057 assert_eq!(
3058 code_errors.len(),
3059 1,
3060 "Expected COD002 for GH02, got: {:?}",
3061 code_errors
3062 );
3063 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3064 }
3065
3066 #[test]
3067 fn test_cross_field_code_validation_valid_qualifiers() {
3068 let evaluator = MockEvaluator::new(vec![]);
3071 let validator = EdifactValidator::new(evaluator);
3072 let external = NoOpExternalProvider;
3073
3074 let nad_ms = OwnedSegment {
3075 id: "NAD".to_string(),
3076 elements: vec![vec!["MS".to_string()]],
3077 segment_number: 4,
3078 };
3079 let nad_mr = OwnedSegment {
3080 id: "NAD".to_string(),
3081 elements: vec![vec!["MR".to_string()]],
3082 segment_number: 5,
3083 };
3084
3085 let workflow = AhbWorkflow {
3086 pruefidentifikator: "55001".to_string(),
3087 description: "Test".to_string(),
3088 communication_direction: None,
3089 fields: vec![
3090 AhbFieldRule {
3091 segment_path: "SG2/NAD/3035".to_string(),
3092 name: "Absender".to_string(),
3093 ahb_status: "X".to_string(),
3094 codes: vec![AhbCodeRule {
3095 value: "MS".to_string(),
3096 description: "Absender".to_string(),
3097 ahb_status: "X".to_string(),
3098 }],
3099 parent_group_ahb_status: None,
3100 segment_ahb_status: None,
3101 ..Default::default()
3102 },
3103 AhbFieldRule {
3104 segment_path: "SG2/NAD/3035".to_string(),
3105 name: "Empfaenger".to_string(),
3106 ahb_status: "X".to_string(),
3107 codes: vec![AhbCodeRule {
3108 value: "MR".to_string(),
3109 description: "Empfaenger".to_string(),
3110 ahb_status: "X".to_string(),
3111 }],
3112 parent_group_ahb_status: None,
3113 segment_ahb_status: None,
3114 ..Default::default()
3115 },
3116 ],
3117 ub_definitions: BTreeMap::new(),
3118 package_conditions: Default::default(),
3119 };
3120
3121 let report = validator.validate(
3122 &[nad_ms, nad_mr],
3123 &workflow,
3124 &external,
3125 ValidationLevel::Conditions,
3126 );
3127
3128 let code_errors: Vec<_> = report
3129 .by_category(ValidationCategory::Code)
3130 .filter(|i| i.severity == Severity::Error)
3131 .collect();
3132 assert!(
3133 code_errors.is_empty(),
3134 "Expected no code errors for valid qualifiers, got: {:?}",
3135 code_errors
3136 );
3137 }
3138
3139 #[test]
3140 fn test_cross_field_code_validation_catches_invalid_qualifier() {
3141 let evaluator = MockEvaluator::new(vec![]);
3143 let validator = EdifactValidator::new(evaluator);
3144 let external = NoOpExternalProvider;
3145
3146 let nad_ms = OwnedSegment {
3147 id: "NAD".to_string(),
3148 elements: vec![vec!["MS".to_string()]],
3149 segment_number: 4,
3150 };
3151 let nad_mt = OwnedSegment {
3152 id: "NAD".to_string(),
3153 elements: vec![vec!["MT".to_string()]], segment_number: 5,
3155 };
3156
3157 let workflow = AhbWorkflow {
3158 pruefidentifikator: "55001".to_string(),
3159 description: "Test".to_string(),
3160 communication_direction: None,
3161 fields: vec![
3162 AhbFieldRule {
3163 segment_path: "SG2/NAD/3035".to_string(),
3164 name: "Absender".to_string(),
3165 ahb_status: "X".to_string(),
3166 codes: vec![AhbCodeRule {
3167 value: "MS".to_string(),
3168 description: "Absender".to_string(),
3169 ahb_status: "X".to_string(),
3170 }],
3171 parent_group_ahb_status: None,
3172 segment_ahb_status: None,
3173 ..Default::default()
3174 },
3175 AhbFieldRule {
3176 segment_path: "SG2/NAD/3035".to_string(),
3177 name: "Empfaenger".to_string(),
3178 ahb_status: "X".to_string(),
3179 codes: vec![AhbCodeRule {
3180 value: "MR".to_string(),
3181 description: "Empfaenger".to_string(),
3182 ahb_status: "X".to_string(),
3183 }],
3184 parent_group_ahb_status: None,
3185 segment_ahb_status: None,
3186 ..Default::default()
3187 },
3188 ],
3189 ub_definitions: BTreeMap::new(),
3190 package_conditions: Default::default(),
3191 };
3192
3193 let report = validator.validate(
3194 &[nad_ms, nad_mt],
3195 &workflow,
3196 &external,
3197 ValidationLevel::Conditions,
3198 );
3199
3200 let code_errors: Vec<_> = report
3201 .by_category(ValidationCategory::Code)
3202 .filter(|i| i.severity == Severity::Error)
3203 .collect();
3204 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
3205 assert!(narrate(code_errors[0]).contains("MT"));
3206 assert!(narrate(code_errors[0]).contains("MR"));
3207 assert!(narrate(code_errors[0]).contains("MS"));
3208 }
3209
3210 #[test]
3211 fn test_cross_field_code_validation_unions_across_groups() {
3212 let evaluator = MockEvaluator::new(vec![]);
3216 let validator = EdifactValidator::new(evaluator);
3217 let external = NoOpExternalProvider;
3218
3219 let segments = vec![
3220 OwnedSegment {
3221 id: "NAD".to_string(),
3222 elements: vec![vec!["MS".to_string()]],
3223 segment_number: 3,
3224 },
3225 OwnedSegment {
3226 id: "NAD".to_string(),
3227 elements: vec![vec!["MR".to_string()]],
3228 segment_number: 4,
3229 },
3230 OwnedSegment {
3231 id: "NAD".to_string(),
3232 elements: vec![vec!["Z04".to_string()]],
3233 segment_number: 20,
3234 },
3235 OwnedSegment {
3236 id: "NAD".to_string(),
3237 elements: vec![vec!["Z09".to_string()]],
3238 segment_number: 21,
3239 },
3240 OwnedSegment {
3241 id: "NAD".to_string(),
3242 elements: vec![vec!["MT".to_string()]], segment_number: 22,
3244 },
3245 ];
3246
3247 let workflow = AhbWorkflow {
3248 pruefidentifikator: "55001".to_string(),
3249 description: "Test".to_string(),
3250 communication_direction: None,
3251 fields: vec![
3252 AhbFieldRule {
3253 segment_path: "SG2/NAD/3035".to_string(),
3254 name: "Absender".to_string(),
3255 ahb_status: "X".to_string(),
3256 codes: vec![AhbCodeRule {
3257 value: "MS".to_string(),
3258 description: "Absender".to_string(),
3259 ahb_status: "X".to_string(),
3260 }],
3261 parent_group_ahb_status: None,
3262 segment_ahb_status: None,
3263 ..Default::default()
3264 },
3265 AhbFieldRule {
3266 segment_path: "SG2/NAD/3035".to_string(),
3267 name: "Empfaenger".to_string(),
3268 ahb_status: "X".to_string(),
3269 codes: vec![AhbCodeRule {
3270 value: "MR".to_string(),
3271 description: "Empfaenger".to_string(),
3272 ahb_status: "X".to_string(),
3273 }],
3274 parent_group_ahb_status: None,
3275 segment_ahb_status: None,
3276 ..Default::default()
3277 },
3278 AhbFieldRule {
3279 segment_path: "SG4/SG12/NAD/3035".to_string(),
3280 name: "Anschlussnutzer".to_string(),
3281 ahb_status: "X".to_string(),
3282 codes: vec![AhbCodeRule {
3283 value: "Z04".to_string(),
3284 description: "Anschlussnutzer".to_string(),
3285 ahb_status: "X".to_string(),
3286 }],
3287 parent_group_ahb_status: None,
3288 segment_ahb_status: None,
3289 ..Default::default()
3290 },
3291 AhbFieldRule {
3292 segment_path: "SG4/SG12/NAD/3035".to_string(),
3293 name: "Korrespondenzanschrift".to_string(),
3294 ahb_status: "X".to_string(),
3295 codes: vec![AhbCodeRule {
3296 value: "Z09".to_string(),
3297 description: "Korrespondenzanschrift".to_string(),
3298 ahb_status: "X".to_string(),
3299 }],
3300 parent_group_ahb_status: None,
3301 segment_ahb_status: None,
3302 ..Default::default()
3303 },
3304 ],
3305 ub_definitions: BTreeMap::new(),
3306 package_conditions: Default::default(),
3307 };
3308
3309 let report =
3310 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3311
3312 let code_errors: Vec<_> = report
3313 .by_category(ValidationCategory::Code)
3314 .filter(|i| i.severity == Severity::Error)
3315 .collect();
3316 assert_eq!(
3317 code_errors.len(),
3318 1,
3319 "Expected exactly one COD002 error for MT, got: {:?}",
3320 code_errors
3321 );
3322 assert!(narrate(code_errors[0]).contains("MT"));
3323 }
3324
3325 #[test]
3326 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3327 let evaluator = MockEvaluator::new(vec![]);
3336 let validator = EdifactValidator::new(evaluator);
3337 let external = NoOpExternalProvider;
3338
3339 let qty_67 = OwnedSegment {
3340 id: "QTY".to_string(),
3341 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
3342 segment_number: 10,
3343 };
3344
3345 let workflow = AhbWorkflow {
3346 pruefidentifikator: "13025".to_string(),
3347 description: "Test".to_string(),
3348 communication_direction: None,
3349 fields: vec![AhbFieldRule {
3350 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
3351 name: "Menge, Qualifier".to_string(),
3352 ahb_status: "X".to_string(),
3353 codes: vec![
3354 AhbCodeRule {
3355 value: "220".to_string(),
3356 description: "Wahrer Wert".to_string(),
3357 ahb_status: "X".to_string(),
3358 },
3359 AhbCodeRule {
3360 value: "67".to_string(),
3361 description: "Ersatzwert".to_string(),
3362 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
3363 },
3364 AhbCodeRule {
3365 value: "Z18".to_string(),
3366 description: "Vorläufiger Wert".to_string(),
3367 ahb_status: "X [35]".to_string(),
3368 },
3369 ],
3370 parent_group_ahb_status: None,
3371 segment_ahb_status: None,
3372 element_index: Some(0),
3373 component_index: Some(0),
3374 ..Default::default()
3375 }],
3376 ub_definitions: BTreeMap::new(),
3377 package_conditions: Default::default(),
3378 };
3379
3380 let report =
3381 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3382
3383 let code_errors: Vec<_> = report
3384 .by_category(ValidationCategory::Code)
3385 .filter(|i| i.severity == Severity::Error)
3386 .collect();
3387 assert!(
3388 code_errors.is_empty(),
3389 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3390 code_errors
3391 );
3392 }
3393
3394 #[test]
3395 fn test_is_qualifier_field_simple_paths() {
3396 assert!(is_qualifier_field("NAD/3035"));
3397 assert!(is_qualifier_field("SG2/NAD/3035"));
3398 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3399 assert!(is_qualifier_field("LOC/3227"));
3400 }
3401
3402 #[test]
3403 fn test_is_qualifier_field_composite_paths() {
3404 assert!(is_qualifier_field("UNH/S009/0065"));
3408 assert!(is_qualifier_field("NAD/C082/3039"));
3409 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3410 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3411 }
3412
3413 #[test]
3414 fn test_is_qualifier_field_bare_segment() {
3415 assert!(!is_qualifier_field("NAD"));
3416 assert!(!is_qualifier_field("SG2/NAD"));
3417 }
3418
3419 #[test]
3420 fn test_is_qualifier_field_rejects_deep_paths() {
3421 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3423 }
3424
3425 #[test]
3426 fn test_missing_qualifier_instance_is_detected() {
3427 let evaluator = MockEvaluator::new(vec![]);
3430 let validator = EdifactValidator::new(evaluator);
3431 let external = NoOpExternalProvider;
3432
3433 let nad_ms = OwnedSegment {
3434 id: "NAD".to_string(),
3435 elements: vec![vec!["MS".to_string()]],
3436 segment_number: 3,
3437 };
3438
3439 let workflow = AhbWorkflow {
3440 pruefidentifikator: "55001".to_string(),
3441 description: "Test".to_string(),
3442 communication_direction: None,
3443 fields: vec![
3444 AhbFieldRule {
3445 segment_path: "SG2/NAD/3035".to_string(),
3446 name: "Absender".to_string(),
3447 ahb_status: "X".to_string(),
3448 codes: vec![AhbCodeRule {
3449 value: "MS".to_string(),
3450 description: "Absender".to_string(),
3451 ahb_status: "X".to_string(),
3452 }],
3453 parent_group_ahb_status: None,
3454 segment_ahb_status: None,
3455 ..Default::default()
3456 },
3457 AhbFieldRule {
3458 segment_path: "SG2/NAD/3035".to_string(),
3459 name: "Empfaenger".to_string(),
3460 ahb_status: "Muss".to_string(),
3461 codes: vec![AhbCodeRule {
3462 value: "MR".to_string(),
3463 description: "Empfaenger".to_string(),
3464 ahb_status: "X".to_string(),
3465 }],
3466 parent_group_ahb_status: None,
3467 segment_ahb_status: None,
3468 ..Default::default()
3469 },
3470 ],
3471 ub_definitions: BTreeMap::new(),
3472 package_conditions: Default::default(),
3473 };
3474
3475 let report =
3476 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
3477
3478 let ahb_errors: Vec<_> = report
3479 .by_category(ValidationCategory::Ahb)
3480 .filter(|i| i.severity == Severity::Error)
3481 .collect();
3482 assert_eq!(
3483 ahb_errors.len(),
3484 1,
3485 "Expected AHB001 for missing NAD+MR, got: {:?}",
3486 ahb_errors
3487 );
3488 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3489 }
3490
3491 #[test]
3492 fn test_present_qualifier_instance_no_error() {
3493 let evaluator = MockEvaluator::new(vec![]);
3495 let validator = EdifactValidator::new(evaluator);
3496 let external = NoOpExternalProvider;
3497
3498 let segments = vec![
3499 OwnedSegment {
3500 id: "NAD".to_string(),
3501 elements: vec![vec!["MS".to_string()]],
3502 segment_number: 3,
3503 },
3504 OwnedSegment {
3505 id: "NAD".to_string(),
3506 elements: vec![vec!["MR".to_string()]],
3507 segment_number: 4,
3508 },
3509 ];
3510
3511 let workflow = AhbWorkflow {
3512 pruefidentifikator: "55001".to_string(),
3513 description: "Test".to_string(),
3514 communication_direction: None,
3515 fields: vec![
3516 AhbFieldRule {
3517 segment_path: "SG2/NAD/3035".to_string(),
3518 name: "Absender".to_string(),
3519 ahb_status: "Muss".to_string(),
3520 codes: vec![AhbCodeRule {
3521 value: "MS".to_string(),
3522 description: "Absender".to_string(),
3523 ahb_status: "X".to_string(),
3524 }],
3525 parent_group_ahb_status: None,
3526 segment_ahb_status: None,
3527 ..Default::default()
3528 },
3529 AhbFieldRule {
3530 segment_path: "SG2/NAD/3035".to_string(),
3531 name: "Empfaenger".to_string(),
3532 ahb_status: "Muss".to_string(),
3533 codes: vec![AhbCodeRule {
3534 value: "MR".to_string(),
3535 description: "Empfaenger".to_string(),
3536 ahb_status: "X".to_string(),
3537 }],
3538 parent_group_ahb_status: None,
3539 segment_ahb_status: None,
3540 ..Default::default()
3541 },
3542 ],
3543 ub_definitions: BTreeMap::new(),
3544 package_conditions: Default::default(),
3545 };
3546
3547 let report =
3548 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3549
3550 let ahb_errors: Vec<_> = report
3551 .by_category(ValidationCategory::Ahb)
3552 .filter(|i| i.severity == Severity::Error)
3553 .collect();
3554 assert!(
3555 ahb_errors.is_empty(),
3556 "Expected no AHB001 errors, got: {:?}",
3557 ahb_errors
3558 );
3559 }
3560
3561 #[test]
3562 fn test_extract_group_path_key() {
3563 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
3564 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
3565 assert_eq!(extract_group_path_key("NAD/3035"), "");
3566 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
3567 }
3568
3569 fn validate_field_of_absent_group(
3571 group_status: &str,
3572 conditions: Vec<(u32, CR)>,
3573 ) -> ValidationReport {
3574 use mig_types::navigator::GroupNavigator;
3575
3576 struct NoSg9;
3577 impl GroupNavigator for NoSg9 {
3578 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3579 vec![]
3580 }
3581 fn find_segments_with_qualifier_in_group(
3582 &self,
3583 _: &str,
3584 _: usize,
3585 _: &str,
3586 _: &[&str],
3587 _: usize,
3588 ) -> Vec<OwnedSegment> {
3589 vec![]
3590 }
3591 fn group_instance_count(&self, _: &[&str]) -> usize {
3592 0
3593 }
3594 }
3595
3596 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3597 let workflow = AhbWorkflow {
3598 pruefidentifikator: "44001".to_string(),
3599 description: "Test".to_string(),
3600 communication_direction: None,
3601 fields: vec![AhbFieldRule {
3602 segment_path: "SG9/QTY/C186/6060".to_string(),
3603 name: "Menge".to_string(),
3604 ahb_status: "X".to_string(),
3605 parent_group_ahb_status: Some(group_status.to_string()),
3606 ..Default::default()
3607 }],
3608 ub_definitions: BTreeMap::new(),
3609 package_conditions: Default::default(),
3610 };
3611 validator.validate_with_navigator(
3612 &[],
3613 &workflow,
3614 &NoOpExternalProvider,
3615 ValidationLevel::Conditions,
3616 &NoSg9,
3617 )
3618 }
3619
3620 #[test]
3621 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
3622 let report = validate_field_of_absent_group(
3624 "Soll [47]\r\nMuss [46]",
3625 vec![(47, CR::True), (46, CR::False)],
3626 );
3627 assert!(
3628 report.is_valid(),
3629 "{:?}",
3630 report.errors().collect::<Vec<_>>()
3631 );
3632 }
3633
3634 #[test]
3635 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
3636 let report = validate_field_of_absent_group(
3637 "Soll [47]\r\nMuss [46]",
3638 vec![(47, CR::False), (46, CR::True)],
3639 );
3640 let errors: Vec<_> = report.errors().collect();
3641 assert_eq!(errors.len(), 1, "{errors:?}");
3642 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3643 }
3644
3645 #[test]
3646 fn test_absent_optional_group_no_missing_field_error() {
3647 use mig_types::navigator::GroupNavigator;
3650
3651 struct NavWithoutSG3;
3652 impl GroupNavigator for NavWithoutSG3 {
3653 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3654 vec![]
3655 }
3656 fn find_segments_with_qualifier_in_group(
3657 &self,
3658 _: &str,
3659 _: usize,
3660 _: &str,
3661 _: &[&str],
3662 _: usize,
3663 ) -> Vec<OwnedSegment> {
3664 vec![]
3665 }
3666 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3667 match group_path {
3668 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
3671 }
3672 }
3673 }
3674
3675 let evaluator = MockEvaluator::new(vec![]);
3676 let validator = EdifactValidator::new(evaluator);
3677 let external = NoOpExternalProvider;
3678 let nav = NavWithoutSG3;
3679
3680 let segments = vec![
3682 OwnedSegment {
3683 id: "NAD".into(),
3684 elements: vec![vec!["MS".into()]],
3685 segment_number: 3,
3686 },
3687 OwnedSegment {
3688 id: "NAD".into(),
3689 elements: vec![vec!["MR".into()]],
3690 segment_number: 4,
3691 },
3692 ];
3693
3694 let workflow = AhbWorkflow {
3695 pruefidentifikator: "55001".to_string(),
3696 description: "Test".to_string(),
3697 communication_direction: None,
3698 fields: vec![
3699 AhbFieldRule {
3700 segment_path: "SG2/SG3/CTA/3139".to_string(),
3701 name: "Funktion des Ansprechpartners, Code".to_string(),
3702 ahb_status: "Muss".to_string(),
3703 codes: vec![],
3704 parent_group_ahb_status: None,
3705 segment_ahb_status: None,
3706 ..Default::default()
3707 },
3708 AhbFieldRule {
3709 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
3710 name: "Name vom Ansprechpartner".to_string(),
3711 ahb_status: "X".to_string(),
3712 codes: vec![],
3713 parent_group_ahb_status: None,
3714 segment_ahb_status: None,
3715 ..Default::default()
3716 },
3717 ],
3718 ub_definitions: BTreeMap::new(),
3719 package_conditions: Default::default(),
3720 };
3721
3722 let report = validator.validate_with_navigator(
3723 &segments,
3724 &workflow,
3725 &external,
3726 ValidationLevel::Conditions,
3727 &nav,
3728 );
3729
3730 let ahb_errors: Vec<_> = report
3731 .by_category(ValidationCategory::Ahb)
3732 .filter(|i| i.severity == Severity::Error)
3733 .collect();
3734 assert!(
3735 ahb_errors.is_empty(),
3736 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
3737 ahb_errors
3738 );
3739 }
3740
3741 #[test]
3742 fn test_present_group_still_checks_mandatory_fields() {
3743 use mig_types::navigator::GroupNavigator;
3745
3746 struct NavWithSG3;
3747 impl GroupNavigator for NavWithSG3 {
3748 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3749 vec![]
3750 }
3751 fn find_segments_with_qualifier_in_group(
3752 &self,
3753 _: &str,
3754 _: usize,
3755 _: &str,
3756 _: &[&str],
3757 _: usize,
3758 ) -> Vec<OwnedSegment> {
3759 vec![]
3760 }
3761 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3762 match group_path {
3763 ["SG2"] => 1,
3764 ["SG2", "SG3"] => 1, _ => 0,
3766 }
3767 }
3768 }
3769
3770 let evaluator = MockEvaluator::new(vec![]);
3771 let validator = EdifactValidator::new(evaluator);
3772 let external = NoOpExternalProvider;
3773 let nav = NavWithSG3;
3774
3775 let segments = vec![OwnedSegment {
3777 id: "NAD".into(),
3778 elements: vec![vec!["MS".into()]],
3779 segment_number: 3,
3780 }];
3781
3782 let workflow = AhbWorkflow {
3783 pruefidentifikator: "55001".to_string(),
3784 description: "Test".to_string(),
3785 communication_direction: None,
3786 fields: vec![AhbFieldRule {
3787 segment_path: "SG2/SG3/CTA/3139".to_string(),
3788 name: "Funktion des Ansprechpartners, Code".to_string(),
3789 ahb_status: "Muss".to_string(),
3790 codes: vec![],
3791 parent_group_ahb_status: None,
3792 segment_ahb_status: None,
3793 ..Default::default()
3794 }],
3795 ub_definitions: BTreeMap::new(),
3796 package_conditions: Default::default(),
3797 };
3798
3799 let report = validator.validate_with_navigator(
3800 &segments,
3801 &workflow,
3802 &external,
3803 ValidationLevel::Conditions,
3804 &nav,
3805 );
3806
3807 let ahb_errors: Vec<_> = report
3808 .by_category(ValidationCategory::Ahb)
3809 .filter(|i| i.severity == Severity::Error)
3810 .collect();
3811 assert_eq!(
3812 ahb_errors.len(),
3813 1,
3814 "Expected AHB001 error when SG3 is present but CTA missing"
3815 );
3816 assert!(narrate(ahb_errors[0]).contains("CTA"));
3817 }
3818
3819 #[test]
3820 fn test_missing_qualifier_with_navigator_is_detected() {
3821 use mig_types::navigator::GroupNavigator;
3824
3825 struct NavWithSG2;
3826 impl GroupNavigator for NavWithSG2 {
3827 fn find_segments_in_group(
3828 &self,
3829 segment_id: &str,
3830 group_path: &[&str],
3831 instance_index: usize,
3832 ) -> Vec<OwnedSegment> {
3833 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
3834 vec![OwnedSegment {
3835 id: "NAD".into(),
3836 elements: vec![vec!["MS".into()]],
3837 segment_number: 3,
3838 }]
3839 } else {
3840 vec![]
3841 }
3842 }
3843 fn find_segments_with_qualifier_in_group(
3844 &self,
3845 _: &str,
3846 _: usize,
3847 _: &str,
3848 _: &[&str],
3849 _: usize,
3850 ) -> Vec<OwnedSegment> {
3851 vec![]
3852 }
3853 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3854 match group_path {
3855 ["SG2"] => 1,
3856 _ => 0,
3857 }
3858 }
3859 }
3860
3861 let evaluator = MockEvaluator::new(vec![]);
3862 let validator = EdifactValidator::new(evaluator);
3863 let external = NoOpExternalProvider;
3864 let nav = NavWithSG2;
3865
3866 let segments = vec![OwnedSegment {
3867 id: "NAD".into(),
3868 elements: vec![vec!["MS".into()]],
3869 segment_number: 3,
3870 }];
3871
3872 let workflow = AhbWorkflow {
3873 pruefidentifikator: "55001".to_string(),
3874 description: "Test".to_string(),
3875 communication_direction: None,
3876 fields: vec![
3877 AhbFieldRule {
3878 segment_path: "SG2/NAD/3035".to_string(),
3879 name: "Absender".to_string(),
3880 ahb_status: "X".to_string(),
3881 codes: vec![AhbCodeRule {
3882 value: "MS".to_string(),
3883 description: "Absender".to_string(),
3884 ahb_status: "X".to_string(),
3885 }],
3886 parent_group_ahb_status: None,
3887 segment_ahb_status: None,
3888 ..Default::default()
3889 },
3890 AhbFieldRule {
3891 segment_path: "SG2/NAD/3035".to_string(),
3892 name: "Empfaenger".to_string(),
3893 ahb_status: "Muss".to_string(),
3894 codes: vec![AhbCodeRule {
3895 value: "MR".to_string(),
3896 description: "Empfaenger".to_string(),
3897 ahb_status: "X".to_string(),
3898 }],
3899 parent_group_ahb_status: None,
3900 segment_ahb_status: None,
3901 ..Default::default()
3902 },
3903 ],
3904 ub_definitions: BTreeMap::new(),
3905 package_conditions: Default::default(),
3906 };
3907
3908 let report = validator.validate_with_navigator(
3909 &segments,
3910 &workflow,
3911 &external,
3912 ValidationLevel::Conditions,
3913 &nav,
3914 );
3915
3916 let ahb_errors: Vec<_> = report
3917 .by_category(ValidationCategory::Ahb)
3918 .filter(|i| i.severity == Severity::Error)
3919 .collect();
3920 assert_eq!(
3921 ahb_errors.len(),
3922 1,
3923 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
3924 ahb_errors
3925 );
3926 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3927 }
3928
3929 #[test]
3930 fn test_optional_group_variant_absent_no_error() {
3931 use mig_types::navigator::GroupNavigator;
3936
3937 struct TestNav;
3938 impl GroupNavigator for TestNav {
3939 fn find_segments_in_group(
3940 &self,
3941 segment_id: &str,
3942 group_path: &[&str],
3943 instance_index: usize,
3944 ) -> Vec<OwnedSegment> {
3945 match (segment_id, group_path, instance_index) {
3946 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
3947 id: "LOC".into(),
3948 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3949 segment_number: 10,
3950 }],
3951 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
3952 id: "NAD".into(),
3953 elements: vec![vec!["MS".into()]],
3954 segment_number: 3,
3955 }],
3956 _ => vec![],
3957 }
3958 }
3959 fn find_segments_with_qualifier_in_group(
3960 &self,
3961 _: &str,
3962 _: usize,
3963 _: &str,
3964 _: &[&str],
3965 _: usize,
3966 ) -> Vec<OwnedSegment> {
3967 vec![]
3968 }
3969 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3970 match group_path {
3971 ["SG2"] => 1,
3972 ["SG4"] => 1,
3973 ["SG4", "SG5"] => 1, _ => 0,
3975 }
3976 }
3977 }
3978
3979 let evaluator = MockEvaluator::new(vec![]);
3980 let validator = EdifactValidator::new(evaluator);
3981 let external = NoOpExternalProvider;
3982 let nav = TestNav;
3983
3984 let segments = vec![
3985 OwnedSegment {
3986 id: "NAD".into(),
3987 elements: vec![vec!["MS".into()]],
3988 segment_number: 3,
3989 },
3990 OwnedSegment {
3991 id: "LOC".into(),
3992 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3993 segment_number: 10,
3994 },
3995 ];
3996
3997 let workflow = AhbWorkflow {
3998 pruefidentifikator: "55001".to_string(),
3999 description: "Test".to_string(),
4000 communication_direction: None,
4001 fields: vec![
4002 AhbFieldRule {
4004 segment_path: "SG2/NAD/3035".to_string(),
4005 name: "Absender".to_string(),
4006 ahb_status: "X".to_string(),
4007 codes: vec![AhbCodeRule {
4008 value: "MS".to_string(),
4009 description: "Absender".to_string(),
4010 ahb_status: "X".to_string(),
4011 }],
4012 parent_group_ahb_status: Some("Muss".to_string()),
4013 segment_ahb_status: None,
4014 ..Default::default()
4015 },
4016 AhbFieldRule {
4017 segment_path: "SG2/NAD/3035".to_string(),
4018 name: "Empfaenger".to_string(),
4019 ahb_status: "Muss".to_string(),
4020 codes: vec![AhbCodeRule {
4021 value: "MR".to_string(),
4022 description: "Empfaenger".to_string(),
4023 ahb_status: "X".to_string(),
4024 }],
4025 parent_group_ahb_status: Some("Muss".to_string()),
4026 segment_ahb_status: None,
4027 ..Default::default()
4028 },
4029 AhbFieldRule {
4031 segment_path: "SG4/SG5/LOC/3227".to_string(),
4032 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4033 ahb_status: "X".to_string(),
4034 codes: vec![AhbCodeRule {
4035 value: "Z16".to_string(),
4036 description: "Marktlokation".to_string(),
4037 ahb_status: "X".to_string(),
4038 }],
4039 parent_group_ahb_status: Some("Kann".to_string()),
4040 segment_ahb_status: None,
4041 ..Default::default()
4042 },
4043 AhbFieldRule {
4044 segment_path: "SG4/SG5/LOC/3227".to_string(),
4045 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4046 ahb_status: "Muss".to_string(),
4047 codes: vec![AhbCodeRule {
4048 value: "Z17".to_string(),
4049 description: "Messlokation".to_string(),
4050 ahb_status: "X".to_string(),
4051 }],
4052 parent_group_ahb_status: Some("Kann".to_string()),
4053 segment_ahb_status: None,
4054 ..Default::default()
4055 },
4056 ],
4057 ub_definitions: BTreeMap::new(),
4058 package_conditions: Default::default(),
4059 };
4060
4061 let report = validator.validate_with_navigator(
4062 &segments,
4063 &workflow,
4064 &external,
4065 ValidationLevel::Conditions,
4066 &nav,
4067 );
4068
4069 let ahb_errors: Vec<_> = report
4070 .by_category(ValidationCategory::Ahb)
4071 .filter(|i| i.severity == Severity::Error)
4072 .collect();
4073
4074 assert_eq!(
4077 ahb_errors.len(),
4078 1,
4079 "Expected only AHB001 for missing NAD+MR, got: {:?}",
4080 ahb_errors
4081 );
4082 assert!(
4083 narrate(ahb_errors[0]).contains("Empfaenger"),
4084 "Error should be for missing NAD+MR (Empfaenger)"
4085 );
4086 }
4087
4088 #[test]
4092 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4093 use mig_types::navigator::GroupNavigator;
4094
4095 struct TestNav;
4096 impl GroupNavigator for TestNav {
4097 fn find_segments_in_group(
4098 &self,
4099 segment_id: &str,
4100 group_path: &[&str],
4101 instance_index: usize,
4102 ) -> Vec<OwnedSegment> {
4103 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4104 vec![OwnedSegment {
4105 id: "SEQ".into(),
4106 elements: vec![vec!["Z03".into()]],
4107 segment_number: 10,
4108 }]
4109 } else {
4110 vec![]
4111 }
4112 }
4113 fn find_segments_with_qualifier_in_group(
4114 &self,
4115 _: &str,
4116 _: usize,
4117 _: &str,
4118 _: &[&str],
4119 _: usize,
4120 ) -> Vec<OwnedSegment> {
4121 vec![]
4122 }
4123 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4124 match group_path {
4125 ["SG4"] => 1,
4126 ["SG4", "SG8"] => 1,
4127 _ => 0,
4128 }
4129 }
4130 }
4131
4132 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
4133 let validator = EdifactValidator::new(evaluator);
4134 let segments = vec![OwnedSegment {
4135 id: "SEQ".into(),
4136 elements: vec![vec!["Z03".into()]],
4137 segment_number: 10,
4138 }];
4139 let seq = |code: &str, group: &str| AhbFieldRule {
4140 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4141 name: format!("Handlung, Code ({code})"),
4142 ahb_status: "X".to_string(),
4143 codes: vec![AhbCodeRule {
4144 value: code.to_string(),
4145 description: String::new(),
4146 ahb_status: "X".to_string(),
4147 }],
4148 parent_group_ahb_status: Some(group.to_string()),
4149 segment_ahb_status: None,
4150 ..Default::default()
4151 };
4152 let workflow = AhbWorkflow {
4153 pruefidentifikator: "44143".to_string(),
4154 description: "Test".to_string(),
4155 communication_direction: None,
4156 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
4157 ub_definitions: BTreeMap::new(),
4158 package_conditions: Default::default(),
4159 };
4160
4161 let report = validator.validate_with_navigator(
4162 &segments,
4163 &workflow,
4164 &NoOpExternalProvider,
4165 ValidationLevel::Conditions,
4166 &TestNav,
4167 );
4168 let missing: Vec<_> = report
4169 .issues
4170 .iter()
4171 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
4172 .collect();
4173 assert!(missing.is_empty(), "{missing:#?}");
4174 }
4175
4176 #[test]
4177 fn test_conditional_group_variant_absent_no_error() {
4178 use mig_types::navigator::GroupNavigator;
4183
4184 struct TestNav;
4185 impl GroupNavigator for TestNav {
4186 fn find_segments_in_group(
4187 &self,
4188 segment_id: &str,
4189 group_path: &[&str],
4190 instance_index: usize,
4191 ) -> Vec<OwnedSegment> {
4192 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
4193 vec![OwnedSegment {
4194 id: "LOC".into(),
4195 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4196 segment_number: 10,
4197 }]
4198 } else {
4199 vec![]
4200 }
4201 }
4202 fn find_segments_with_qualifier_in_group(
4203 &self,
4204 _: &str,
4205 _: usize,
4206 _: &str,
4207 _: &[&str],
4208 _: usize,
4209 ) -> Vec<OwnedSegment> {
4210 vec![]
4211 }
4212 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4213 match group_path {
4214 ["SG4"] => 1,
4215 ["SG4", "SG5"] => 1, _ => 0,
4217 }
4218 }
4219 }
4220
4221 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
4224 let validator = EdifactValidator::new(evaluator);
4225 let external = NoOpExternalProvider;
4226 let nav = TestNav;
4227
4228 let segments = vec![OwnedSegment {
4229 id: "LOC".into(),
4230 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4231 segment_number: 10,
4232 }];
4233
4234 let workflow = AhbWorkflow {
4235 pruefidentifikator: "55001".to_string(),
4236 description: "Test".to_string(),
4237 communication_direction: None,
4238 fields: vec![
4239 AhbFieldRule {
4241 segment_path: "SG4/SG5/LOC/3227".to_string(),
4242 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4243 ahb_status: "X".to_string(),
4244 codes: vec![AhbCodeRule {
4245 value: "Z16".to_string(),
4246 description: "Marktlokation".to_string(),
4247 ahb_status: "X".to_string(),
4248 }],
4249 parent_group_ahb_status: Some("Muss [2061]".to_string()),
4250 segment_ahb_status: None,
4251 ..Default::default()
4252 },
4253 AhbFieldRule {
4255 segment_path: "SG4/SG5/LOC/3227".to_string(),
4256 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4257 ahb_status: "X".to_string(),
4258 codes: vec![AhbCodeRule {
4259 value: "Z17".to_string(),
4260 description: "Messlokation".to_string(),
4261 ahb_status: "X".to_string(),
4262 }],
4263 parent_group_ahb_status: Some("Soll [165]".to_string()),
4264 segment_ahb_status: None,
4265 ..Default::default()
4266 },
4267 ],
4268 ub_definitions: BTreeMap::new(),
4269 package_conditions: Default::default(),
4270 };
4271
4272 let report = validator.validate_with_navigator(
4273 &segments,
4274 &workflow,
4275 &external,
4276 ValidationLevel::Conditions,
4277 &nav,
4278 );
4279
4280 let ahb_errors: Vec<_> = report
4281 .by_category(ValidationCategory::Ahb)
4282 .filter(|i| i.severity == Severity::Error)
4283 .collect();
4284
4285 assert!(
4287 ahb_errors.is_empty(),
4288 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4289 ahb_errors
4290 );
4291 }
4292
4293 #[test]
4294 fn test_conditional_group_variant_unknown_no_error() {
4295 let evaluator = MockEvaluator::new(vec![]);
4301 let validator = EdifactValidator::new(evaluator);
4302 let external = NoOpExternalProvider;
4303
4304 let workflow = AhbWorkflow {
4305 pruefidentifikator: "55001".to_string(),
4306 description: "Test".to_string(),
4307 communication_direction: None,
4308 fields: vec![AhbFieldRule {
4309 segment_path: "SG4/SG5/LOC/3227".to_string(),
4310 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4311 ahb_status: "X".to_string(),
4312 codes: vec![AhbCodeRule {
4313 value: "Z17".to_string(),
4314 description: "Messlokation".to_string(),
4315 ahb_status: "X".to_string(),
4316 }],
4317 parent_group_ahb_status: Some("Soll [165]".to_string()),
4318 segment_ahb_status: None,
4319 ..Default::default()
4320 }],
4321 ub_definitions: BTreeMap::new(),
4322 package_conditions: Default::default(),
4323 };
4324
4325 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4326
4327 let ahb_errors: Vec<_> = report
4328 .by_category(ValidationCategory::Ahb)
4329 .filter(|i| i.severity == Severity::Error)
4330 .collect();
4331
4332 assert!(
4334 ahb_errors.is_empty(),
4335 "Expected no errors when parent group condition is Unknown, got: {:?}",
4336 ahb_errors
4337 );
4338 }
4339
4340 #[test]
4341 fn test_segment_absent_within_present_group_no_error() {
4342 use mig_types::navigator::GroupNavigator;
4346
4347 struct TestNav;
4348 impl GroupNavigator for TestNav {
4349 fn find_segments_in_group(
4350 &self,
4351 segment_id: &str,
4352 group_path: &[&str],
4353 instance_index: usize,
4354 ) -> Vec<OwnedSegment> {
4355 if segment_id == "QTY"
4357 && group_path == ["SG5", "SG6", "SG9", "SG10"]
4358 && instance_index == 0
4359 {
4360 vec![OwnedSegment {
4361 id: "QTY".into(),
4362 elements: vec![vec!["220".into(), "0".into()]],
4363 segment_number: 14,
4364 }]
4365 } else {
4366 vec![]
4367 }
4368 }
4369 fn find_segments_with_qualifier_in_group(
4370 &self,
4371 _: &str,
4372 _: usize,
4373 _: &str,
4374 _: &[&str],
4375 _: usize,
4376 ) -> Vec<OwnedSegment> {
4377 vec![]
4378 }
4379 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4380 match group_path {
4381 ["SG5"] => 1,
4382 ["SG5", "SG6"] => 1,
4383 ["SG5", "SG6", "SG9"] => 1,
4384 ["SG5", "SG6", "SG9", "SG10"] => 1,
4385 _ => 0,
4386 }
4387 }
4388 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4389 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4391 }
4392 }
4393
4394 let evaluator = MockEvaluator::all_true(&[]);
4395 let validator = EdifactValidator::new(evaluator);
4396 let external = NoOpExternalProvider;
4397 let nav = TestNav;
4398
4399 let segments = vec![OwnedSegment {
4400 id: "QTY".into(),
4401 elements: vec![vec!["220".into(), "0".into()]],
4402 segment_number: 14,
4403 }];
4404
4405 let workflow = AhbWorkflow {
4406 pruefidentifikator: "13017".to_string(),
4407 description: "Test".to_string(),
4408 communication_direction: None,
4409 fields: vec![
4410 AhbFieldRule {
4412 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4413 name: "Statuskategorie, Code".to_string(),
4414 ahb_status: "X".to_string(),
4415 codes: vec![],
4416 parent_group_ahb_status: Some("Muss".to_string()),
4417 segment_ahb_status: None,
4418 ..Default::default()
4419 },
4420 AhbFieldRule {
4422 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4423 name: "Statusanlaß, Code".to_string(),
4424 ahb_status: "X [5]".to_string(),
4425 codes: vec![],
4426 parent_group_ahb_status: Some("Muss".to_string()),
4427 segment_ahb_status: None,
4428 ..Default::default()
4429 },
4430 ],
4431 ub_definitions: BTreeMap::new(),
4432 package_conditions: Default::default(),
4433 };
4434
4435 let report = validator.validate_with_navigator(
4436 &segments,
4437 &workflow,
4438 &external,
4439 ValidationLevel::Conditions,
4440 &nav,
4441 );
4442
4443 let ahb_errors: Vec<_> = report
4444 .by_category(ValidationCategory::Ahb)
4445 .filter(|i| i.severity == Severity::Error)
4446 .collect();
4447
4448 assert!(
4449 ahb_errors.is_empty(),
4450 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
4451 ahb_errors
4452 );
4453 }
4454
4455 #[test]
4456 fn test_group_scoped_code_validation_with_navigator() {
4457 use mig_types::navigator::GroupNavigator;
4461
4462 struct TestNav;
4463 impl GroupNavigator for TestNav {
4464 fn find_segments_in_group(
4465 &self,
4466 segment_id: &str,
4467 group_path: &[&str],
4468 _instance_index: usize,
4469 ) -> Vec<OwnedSegment> {
4470 if segment_id != "NAD" {
4471 return vec![];
4472 }
4473 match group_path {
4474 ["SG2"] => vec![
4475 OwnedSegment {
4476 id: "NAD".into(),
4477 elements: vec![vec!["MS".into()]],
4478 segment_number: 3,
4479 },
4480 OwnedSegment {
4481 id: "NAD".into(),
4482 elements: vec![vec!["MT".into()]], segment_number: 4,
4484 },
4485 ],
4486 ["SG4", "SG12"] => vec![
4487 OwnedSegment {
4488 id: "NAD".into(),
4489 elements: vec![vec!["Z04".into()]],
4490 segment_number: 20,
4491 },
4492 OwnedSegment {
4493 id: "NAD".into(),
4494 elements: vec![vec!["Z09".into()]],
4495 segment_number: 21,
4496 },
4497 ],
4498 _ => vec![],
4499 }
4500 }
4501 fn find_segments_with_qualifier_in_group(
4502 &self,
4503 _: &str,
4504 _: usize,
4505 _: &str,
4506 _: &[&str],
4507 _: usize,
4508 ) -> Vec<OwnedSegment> {
4509 vec![]
4510 }
4511 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4512 match group_path {
4513 ["SG2"] | ["SG4", "SG12"] => 1,
4514 _ => 0,
4515 }
4516 }
4517 }
4518
4519 let evaluator = MockEvaluator::new(vec![]);
4520 let validator = EdifactValidator::new(evaluator);
4521 let external = NoOpExternalProvider;
4522 let nav = TestNav;
4523
4524 let workflow = AhbWorkflow {
4525 pruefidentifikator: "55001".to_string(),
4526 description: "Test".to_string(),
4527 communication_direction: None,
4528 fields: vec![
4529 AhbFieldRule {
4530 segment_path: "SG2/NAD/3035".to_string(),
4531 name: "Absender".to_string(),
4532 ahb_status: "X".to_string(),
4533 codes: vec![AhbCodeRule {
4534 value: "MS".to_string(),
4535 description: "Absender".to_string(),
4536 ahb_status: "X".to_string(),
4537 }],
4538 parent_group_ahb_status: None,
4539 segment_ahb_status: None,
4540 ..Default::default()
4541 },
4542 AhbFieldRule {
4543 segment_path: "SG2/NAD/3035".to_string(),
4544 name: "Empfaenger".to_string(),
4545 ahb_status: "X".to_string(),
4546 codes: vec![AhbCodeRule {
4547 value: "MR".to_string(),
4548 description: "Empfaenger".to_string(),
4549 ahb_status: "X".to_string(),
4550 }],
4551 parent_group_ahb_status: None,
4552 segment_ahb_status: None,
4553 ..Default::default()
4554 },
4555 AhbFieldRule {
4556 segment_path: "SG4/SG12/NAD/3035".to_string(),
4557 name: "Anschlussnutzer".to_string(),
4558 ahb_status: "X".to_string(),
4559 codes: vec![AhbCodeRule {
4560 value: "Z04".to_string(),
4561 description: "Anschlussnutzer".to_string(),
4562 ahb_status: "X".to_string(),
4563 }],
4564 parent_group_ahb_status: None,
4565 segment_ahb_status: None,
4566 ..Default::default()
4567 },
4568 AhbFieldRule {
4569 segment_path: "SG4/SG12/NAD/3035".to_string(),
4570 name: "Korrespondenzanschrift".to_string(),
4571 ahb_status: "X".to_string(),
4572 codes: vec![AhbCodeRule {
4573 value: "Z09".to_string(),
4574 description: "Korrespondenzanschrift".to_string(),
4575 ahb_status: "X".to_string(),
4576 }],
4577 parent_group_ahb_status: None,
4578 segment_ahb_status: None,
4579 ..Default::default()
4580 },
4581 ],
4582 ub_definitions: BTreeMap::new(),
4583 package_conditions: Default::default(),
4584 };
4585
4586 let all_segments = vec![
4588 OwnedSegment {
4589 id: "NAD".into(),
4590 elements: vec![vec!["MS".into()]],
4591 segment_number: 3,
4592 },
4593 OwnedSegment {
4594 id: "NAD".into(),
4595 elements: vec![vec!["MT".into()]],
4596 segment_number: 4,
4597 },
4598 OwnedSegment {
4599 id: "NAD".into(),
4600 elements: vec![vec!["Z04".into()]],
4601 segment_number: 20,
4602 },
4603 OwnedSegment {
4604 id: "NAD".into(),
4605 elements: vec![vec!["Z09".into()]],
4606 segment_number: 21,
4607 },
4608 ];
4609
4610 let report = validator.validate_with_navigator(
4611 &all_segments,
4612 &workflow,
4613 &external,
4614 ValidationLevel::Conditions,
4615 &nav,
4616 );
4617
4618 let code_errors: Vec<_> = report
4619 .by_category(ValidationCategory::Code)
4620 .filter(|i| i.severity == Severity::Error)
4621 .collect();
4622
4623 assert_eq!(
4626 code_errors.len(),
4627 1,
4628 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
4629 code_errors
4630 );
4631 assert!(narrate(code_errors[0]).contains("MT"));
4632 assert!(narrate(code_errors[0]).contains("MR"));
4634 assert!(narrate(code_errors[0]).contains("MS"));
4635 assert!(
4636 !narrate(code_errors[0]).contains("Z04"),
4637 "SG4/SG12 codes should not leak into SG2 error"
4638 );
4639 assert!(
4641 code_errors[0]
4642 .field_path
4643 .as_deref()
4644 .unwrap_or("")
4645 .contains("SG2"),
4646 "Error field_path should reference SG2, got: {:?}",
4647 code_errors[0].field_path
4648 );
4649 }
4650
4651 #[test]
4654 fn test_package_cardinality_within_bounds() {
4655 let evaluator = MockEvaluator::all_true(&[]);
4657 let validator = EdifactValidator::new(evaluator);
4658 let external = NoOpExternalProvider;
4659
4660 let segments = vec![OwnedSegment {
4661 id: "STS".into(),
4662 elements: vec![
4663 vec!["Z33".into()], vec![], vec!["E01".into()], ],
4667 segment_number: 5,
4668 }];
4669
4670 let workflow = AhbWorkflow {
4671 pruefidentifikator: "13017".to_string(),
4672 description: "Test".to_string(),
4673 communication_direction: None,
4674 ub_definitions: BTreeMap::new(),
4675 package_conditions: Default::default(),
4676 fields: vec![AhbFieldRule {
4677 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4678 name: "Statusanlaß, Code".to_string(),
4679 ahb_status: "X".to_string(),
4680 element_index: Some(2),
4681 component_index: Some(0),
4682 codes: vec![
4683 AhbCodeRule {
4684 value: "E01".into(),
4685 description: "Code 1".into(),
4686 ahb_status: "X [4P0..1]".into(),
4687 },
4688 AhbCodeRule {
4689 value: "E02".into(),
4690 description: "Code 2".into(),
4691 ahb_status: "X [4P0..1]".into(),
4692 },
4693 ],
4694 parent_group_ahb_status: Some("Muss".to_string()),
4695 segment_ahb_status: None,
4696 mig_number: None,
4697 format: None,
4698 }],
4699 };
4700
4701 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4702 let pkg_errors: Vec<_> = report
4703 .by_category(ValidationCategory::Ahb)
4704 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4705 .collect();
4706 assert!(
4707 pkg_errors.is_empty(),
4708 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
4709 pkg_errors
4710 );
4711 }
4712
4713 #[test]
4714 fn test_package_cardinality_zero_present_min_zero() {
4715 let evaluator = MockEvaluator::all_true(&[]);
4717 let validator = EdifactValidator::new(evaluator);
4718 let external = NoOpExternalProvider;
4719
4720 let segments = vec![OwnedSegment {
4721 id: "STS".into(),
4722 elements: vec![
4723 vec!["Z33".into()],
4724 vec![],
4725 vec!["X99".into()], ],
4727 segment_number: 5,
4728 }];
4729
4730 let workflow = AhbWorkflow {
4731 pruefidentifikator: "13017".to_string(),
4732 description: "Test".to_string(),
4733 communication_direction: None,
4734 ub_definitions: BTreeMap::new(),
4735 package_conditions: Default::default(),
4736 fields: vec![AhbFieldRule {
4737 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4738 name: "Statusanlaß, Code".to_string(),
4739 ahb_status: "X".to_string(),
4740 element_index: Some(2),
4741 component_index: Some(0),
4742 codes: vec![
4743 AhbCodeRule {
4744 value: "E01".into(),
4745 description: "Code 1".into(),
4746 ahb_status: "X [4P0..1]".into(),
4747 },
4748 AhbCodeRule {
4749 value: "E02".into(),
4750 description: "Code 2".into(),
4751 ahb_status: "X [4P0..1]".into(),
4752 },
4753 ],
4754 parent_group_ahb_status: Some("Muss".to_string()),
4755 segment_ahb_status: None,
4756 mig_number: None,
4757 format: None,
4758 }],
4759 };
4760
4761 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4762 let pkg_errors: Vec<_> = report
4763 .by_category(ValidationCategory::Ahb)
4764 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4765 .collect();
4766 assert!(
4767 pkg_errors.is_empty(),
4768 "0 codes, min=0 — no error expected, got: {:?}",
4769 pkg_errors
4770 );
4771 }
4772
4773 #[test]
4774 fn test_package_cardinality_too_many() {
4775 let evaluator = MockEvaluator::all_true(&[]);
4777 let validator = EdifactValidator::new(evaluator);
4778 let external = NoOpExternalProvider;
4779
4780 let segments = vec![
4782 OwnedSegment {
4783 id: "STS".into(),
4784 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
4785 segment_number: 5,
4786 },
4787 OwnedSegment {
4788 id: "STS".into(),
4789 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
4790 segment_number: 6,
4791 },
4792 ];
4793
4794 let workflow = AhbWorkflow {
4795 pruefidentifikator: "13017".to_string(),
4796 description: "Test".to_string(),
4797 communication_direction: None,
4798 ub_definitions: BTreeMap::new(),
4799 package_conditions: Default::default(),
4800 fields: vec![AhbFieldRule {
4801 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4802 name: "Statusanlaß, Code".to_string(),
4803 ahb_status: "X".to_string(),
4804 element_index: Some(2),
4805 component_index: Some(0),
4806 codes: vec![
4807 AhbCodeRule {
4808 value: "E01".into(),
4809 description: "Code 1".into(),
4810 ahb_status: "X [4P0..1]".into(),
4811 },
4812 AhbCodeRule {
4813 value: "E02".into(),
4814 description: "Code 2".into(),
4815 ahb_status: "X [4P0..1]".into(),
4816 },
4817 ],
4818 parent_group_ahb_status: Some("Muss".to_string()),
4819 segment_ahb_status: None,
4820 mig_number: None,
4821 format: None,
4822 }],
4823 };
4824
4825 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4826 let pkg_errors: Vec<_> = report
4827 .by_category(ValidationCategory::Ahb)
4828 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4829 .collect();
4830 assert_eq!(
4831 pkg_errors.len(),
4832 1,
4833 "2 codes present, max=1 — expected 1 error, got: {:?}",
4834 pkg_errors
4835 );
4836 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
4837 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
4838 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
4839 }
4840
4841 #[test]
4842 fn test_package_cardinality_too_few() {
4843 let evaluator = MockEvaluator::all_true(&[]);
4845 let validator = EdifactValidator::new(evaluator);
4846 let external = NoOpExternalProvider;
4847
4848 let segments = vec![OwnedSegment {
4849 id: "STS".into(),
4850 elements: vec![
4851 vec!["Z33".into()],
4852 vec![],
4853 vec!["X99".into()], ],
4855 segment_number: 5,
4856 }];
4857
4858 let workflow = AhbWorkflow {
4859 pruefidentifikator: "13017".to_string(),
4860 description: "Test".to_string(),
4861 communication_direction: None,
4862 ub_definitions: BTreeMap::new(),
4863 package_conditions: Default::default(),
4864 fields: vec![AhbFieldRule {
4865 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4866 name: "Statusanlaß, Code".to_string(),
4867 ahb_status: "X".to_string(),
4868 element_index: Some(2),
4869 component_index: Some(0),
4870 codes: vec![
4871 AhbCodeRule {
4872 value: "E01".into(),
4873 description: "Code 1".into(),
4874 ahb_status: "X [5P1..3]".into(),
4875 },
4876 AhbCodeRule {
4877 value: "E02".into(),
4878 description: "Code 2".into(),
4879 ahb_status: "X [5P1..3]".into(),
4880 },
4881 AhbCodeRule {
4882 value: "E03".into(),
4883 description: "Code 3".into(),
4884 ahb_status: "X [5P1..3]".into(),
4885 },
4886 ],
4887 parent_group_ahb_status: Some("Muss".to_string()),
4888 segment_ahb_status: None,
4889 mig_number: None,
4890 format: None,
4891 }],
4892 };
4893
4894 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4895 let pkg_errors: Vec<_> = report
4896 .by_category(ValidationCategory::Ahb)
4897 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4898 .collect();
4899 assert_eq!(
4900 pkg_errors.len(),
4901 1,
4902 "0 codes present, min=1 — expected 1 error, got: {:?}",
4903 pkg_errors
4904 );
4905 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
4906 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
4907 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
4908 }
4909
4910 #[test]
4911 fn test_package_cardinality_no_packages_in_workflow() {
4912 let evaluator = MockEvaluator::all_true(&[]);
4914 let validator = EdifactValidator::new(evaluator);
4915 let external = NoOpExternalProvider;
4916
4917 let segments = vec![OwnedSegment {
4918 id: "STS".into(),
4919 elements: vec![vec!["E01".into()]],
4920 segment_number: 5,
4921 }];
4922
4923 let workflow = AhbWorkflow {
4924 pruefidentifikator: "13017".to_string(),
4925 description: "Test".to_string(),
4926 communication_direction: None,
4927 ub_definitions: BTreeMap::new(),
4928 package_conditions: Default::default(),
4929 fields: vec![AhbFieldRule {
4930 segment_path: "STS/9015".to_string(),
4931 name: "Status Code".to_string(),
4932 ahb_status: "X".to_string(),
4933 codes: vec![AhbCodeRule {
4934 value: "E01".into(),
4935 description: "Code 1".into(),
4936 ahb_status: "X".into(),
4937 }],
4938 parent_group_ahb_status: Some("Muss".to_string()),
4939 segment_ahb_status: None,
4940 ..Default::default()
4941 }],
4942 };
4943
4944 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4945 let pkg_errors: Vec<_> = report
4946 .by_category(ValidationCategory::Ahb)
4947 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4948 .collect();
4949 assert!(
4950 pkg_errors.is_empty(),
4951 "No packages in workflow — no errors expected"
4952 );
4953 }
4954
4955 #[test]
4956 fn test_package_cardinality_with_condition_and_package() {
4957 let evaluator = MockEvaluator::all_true(&[901]);
4959 let validator = EdifactValidator::new(evaluator);
4960 let external = NoOpExternalProvider;
4961
4962 let segments = vec![OwnedSegment {
4963 id: "STS".into(),
4964 elements: vec![vec![], vec![], vec!["E01".into()]],
4965 segment_number: 5,
4966 }];
4967
4968 let workflow = AhbWorkflow {
4969 pruefidentifikator: "13017".to_string(),
4970 description: "Test".to_string(),
4971 communication_direction: None,
4972 ub_definitions: BTreeMap::new(),
4973 package_conditions: Default::default(),
4974 fields: vec![AhbFieldRule {
4975 segment_path: "SG10/STS/C556/9013".to_string(),
4976 name: "Code".to_string(),
4977 ahb_status: "X".to_string(),
4978 element_index: Some(2),
4979 component_index: Some(0),
4980 codes: vec![
4981 AhbCodeRule {
4982 value: "E01".into(),
4983 description: "Code 1".into(),
4984 ahb_status: "X [901] [4P0..1]".into(),
4985 },
4986 AhbCodeRule {
4987 value: "E02".into(),
4988 description: "Code 2".into(),
4989 ahb_status: "X [901] [4P0..1]".into(),
4990 },
4991 ],
4992 parent_group_ahb_status: Some("Muss".to_string()),
4993 segment_ahb_status: None,
4994 mig_number: None,
4995 format: None,
4996 }],
4997 };
4998
4999 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5000 let pkg_errors: Vec<_> = report
5001 .by_category(ValidationCategory::Ahb)
5002 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5003 .collect();
5004 assert!(
5005 pkg_errors.is_empty(),
5006 "1 code within [4P0..1] bounds — no error, got: {:?}",
5007 pkg_errors
5008 );
5009 }
5010
5011 #[test]
5012 fn test_package_binds_only_where_its_condition_holds() {
5013 let report_for = |package_condition: CR| {
5017 let validator =
5018 EdifactValidator::new(MockEvaluator::new(vec![(902, package_condition)]));
5019 let segments = vec![OwnedSegment {
5020 id: "STS".into(),
5021 elements: vec![vec![], vec![], vec!["E09".into()]],
5022 segment_number: 5,
5023 }];
5024 let workflow = AhbWorkflow {
5025 pruefidentifikator: "13017".to_string(),
5026 description: "Test".to_string(),
5027 communication_direction: None,
5028 ub_definitions: BTreeMap::new(),
5029 package_conditions: BTreeMap::from([(5, ConditionExpr::Ref(902))]),
5030 fields: vec![AhbFieldRule {
5031 segment_path: "SG10/STS/C556/9013".to_string(),
5032 name: "Code".to_string(),
5033 ahb_status: "X".to_string(),
5034 element_index: Some(2),
5035 component_index: Some(0),
5036 codes: vec![
5037 AhbCodeRule {
5038 value: "E01".into(),
5039 description: "Code 1".into(),
5040 ahb_status: "X [5P1..1]".into(),
5041 },
5042 AhbCodeRule {
5043 value: "E09".into(),
5044 description: "Code 9".into(),
5045 ahb_status: "X".into(),
5046 },
5047 ],
5048 parent_group_ahb_status: Some("Muss".to_string()),
5049 segment_ahb_status: None,
5050 mig_number: None,
5051 format: None,
5052 }],
5053 };
5054 validator
5055 .validate(
5056 &segments,
5057 &workflow,
5058 &NoOpExternalProvider,
5059 ValidationLevel::Full,
5060 )
5061 .by_category(ValidationCategory::Ahb)
5062 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5063 .count()
5064 };
5065 assert_eq!(report_for(CR::False), 0, "[902] false: [5P] does not bind");
5066 assert_eq!(report_for(CR::True), 1, "[902] true: [5P1..1] binds");
5067 }
5068
5069 #[test]
5070 fn test_package_cardinality_scoped_per_group_instance() {
5071 use mig_types::navigator::GroupNavigator;
5079
5080 struct TwoSg10s {
5081 sts_a: OwnedSegment,
5082 sts_b: OwnedSegment,
5083 }
5084 impl GroupNavigator for TwoSg10s {
5085 fn find_segments_in_group(
5086 &self,
5087 segment_id: &str,
5088 group_path: &[&str],
5089 instance_index: usize,
5090 ) -> Vec<OwnedSegment> {
5091 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
5092 match instance_index {
5093 0 => vec![self.sts_a.clone()],
5094 1 => vec![self.sts_b.clone()],
5095 _ => vec![],
5096 }
5097 } else {
5098 vec![]
5099 }
5100 }
5101 fn find_segments_with_qualifier_in_group(
5102 &self,
5103 _: &str,
5104 _: usize,
5105 _: &str,
5106 _: &[&str],
5107 _: usize,
5108 ) -> Vec<OwnedSegment> {
5109 vec![]
5110 }
5111 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5112 match group_path {
5113 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
5114 ["SG5", "SG6", "SG9", "SG10"] => 2,
5115 _ => 0,
5116 }
5117 }
5118 }
5119
5120 let sts_a = OwnedSegment {
5121 id: "STS".into(),
5122 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5123 segment_number: 10,
5124 };
5125 let sts_b = OwnedSegment {
5126 id: "STS".into(),
5127 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5128 segment_number: 15,
5129 };
5130 let nav = TwoSg10s {
5131 sts_a: sts_a.clone(),
5132 sts_b: sts_b.clone(),
5133 };
5134
5135 let evaluator = MockEvaluator::all_true(&[]);
5136 let validator = EdifactValidator::new(evaluator);
5137 let external = NoOpExternalProvider;
5138
5139 let workflow = AhbWorkflow {
5140 pruefidentifikator: "13025".to_string(),
5141 description: "Test".to_string(),
5142 communication_direction: None,
5143 ub_definitions: BTreeMap::new(),
5144 package_conditions: Default::default(),
5145 fields: vec![AhbFieldRule {
5146 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5147 name: "Statusanlaß, Code".to_string(),
5148 ahb_status: "X".to_string(),
5149 element_index: Some(2),
5150 component_index: Some(0),
5151 codes: vec![
5152 AhbCodeRule {
5153 value: "E01".into(),
5154 description: "Code 1".into(),
5155 ahb_status: "X [4P0..1]".into(),
5156 },
5157 AhbCodeRule {
5158 value: "E02".into(),
5159 description: "Code 2".into(),
5160 ahb_status: "X [4P0..1]".into(),
5161 },
5162 ],
5163 parent_group_ahb_status: Some("Muss".to_string()),
5164 segment_ahb_status: None,
5165 mig_number: None,
5166 format: None,
5167 }],
5168 };
5169
5170 let report = validator.validate_with_navigator(
5171 &[sts_a, sts_b],
5172 &workflow,
5173 &external,
5174 ValidationLevel::Full,
5175 &nav,
5176 );
5177 let pkg_errors: Vec<_> = report
5178 .by_category(ValidationCategory::Ahb)
5179 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5180 .collect();
5181 assert!(
5182 pkg_errors.is_empty(),
5183 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
5184 pkg_errors
5185 );
5186 }
5187
5188 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
5189 OwnedSegment {
5190 id: id.to_string(),
5191 elements: elements
5192 .into_iter()
5193 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
5194 .collect(),
5195 segment_number: 0,
5196 }
5197 }
5198
5199 #[test]
5210 fn test_package_cardinality_scoped_to_rule_mig_variant() {
5211 use mig_types::navigator::GroupNavigator;
5212
5213 struct TwoSg8Variants {
5214 seq_z01: OwnedSegment,
5215 seq_z45: OwnedSegment,
5216 }
5217 impl GroupNavigator for TwoSg8Variants {
5218 fn find_segments_in_group(
5219 &self,
5220 segment_id: &str,
5221 group_path: &[&str],
5222 instance_index: usize,
5223 ) -> Vec<OwnedSegment> {
5224 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
5225 match instance_index {
5226 0 => vec![self.seq_z01.clone()],
5227 1 => vec![self.seq_z45.clone()],
5228 _ => vec![],
5229 }
5230 } else {
5231 vec![]
5232 }
5233 }
5234 fn find_segments_with_qualifier_in_group(
5235 &self,
5236 _: &str,
5237 _: usize,
5238 _: &str,
5239 _: &[&str],
5240 _: usize,
5241 ) -> Vec<OwnedSegment> {
5242 vec![]
5243 }
5244 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5245 match group_path {
5246 ["SG4"] => 1,
5247 ["SG4", "SG8"] => 2,
5248 _ => 0,
5249 }
5250 }
5251 fn instance_has_mig_number(
5252 &self,
5253 group_path: &[&str],
5254 instance_index: usize,
5255 mig_number: &str,
5256 ) -> bool {
5257 if group_path != ["SG4", "SG8"] {
5258 return true;
5259 }
5260 match (instance_index, mig_number) {
5261 (0, "00115") => true,
5262 (0, _) => false,
5263 (1, "00171") => true,
5264 (1, _) => false,
5265 _ => false,
5266 }
5267 }
5268 }
5269
5270 let seq_z01 = OwnedSegment {
5271 id: "SEQ".into(),
5272 elements: vec![vec!["Z01".into()], vec!["1".into()]],
5273 segment_number: 10,
5274 };
5275 let seq_z45 = OwnedSegment {
5276 id: "SEQ".into(),
5277 elements: vec![vec!["Z45".into()], vec!["1".into()]],
5278 segment_number: 20,
5279 };
5280 let nav = TwoSg8Variants {
5281 seq_z01: seq_z01.clone(),
5282 seq_z45: seq_z45.clone(),
5283 };
5284
5285 let evaluator = MockEvaluator::all_true(&[]);
5286 let validator = EdifactValidator::new(evaluator);
5287 let external = NoOpExternalProvider;
5288
5289 let workflow = AhbWorkflow {
5290 pruefidentifikator: "55218".to_string(),
5291 description: "Test".to_string(),
5292 communication_direction: None,
5293 ub_definitions: BTreeMap::new(),
5294 package_conditions: Default::default(),
5295 fields: vec![AhbFieldRule {
5296 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5297 name: "Handlung, Code".to_string(),
5298 ahb_status: "X".to_string(),
5299 element_index: Some(0),
5300 component_index: Some(0),
5301 codes: vec![
5302 AhbCodeRule {
5303 value: "Z45".into(),
5304 description: "NNA".into(),
5305 ahb_status: "X [1P1..4294967295]".into(),
5306 },
5307 AhbCodeRule {
5308 value: "Z84".into(),
5309 description: "Differenz-NNA".into(),
5310 ahb_status: "X [1P0..4294967295]".into(),
5311 },
5312 ],
5313 parent_group_ahb_status: Some("Muss".to_string()),
5314 segment_ahb_status: None,
5315 mig_number: Some("00171".to_string()),
5316 format: None,
5317 }],
5318 };
5319
5320 let report = validator.validate_with_navigator(
5321 &[seq_z01, seq_z45],
5322 &workflow,
5323 &external,
5324 ValidationLevel::Full,
5325 &nav,
5326 );
5327 let pkg_errors: Vec<_> = report
5328 .by_category(ValidationCategory::Ahb)
5329 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5330 .collect();
5331 assert!(
5332 pkg_errors.is_empty(),
5333 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
5334 pkg_errors
5335 );
5336 }
5337
5338 #[test]
5339 fn test_unt_count_correct() {
5340 let segments = vec![
5342 make_segment("UNH", vec![vec!["001"]]),
5343 make_segment("BGM", vec![vec!["E01"]]),
5344 make_segment("DTM", vec![vec!["137", "20250401"]]),
5345 make_segment("UNT", vec![vec!["4", "001"]]),
5346 ];
5347 assert!(
5348 validate_unt_segment_count(&segments).is_none(),
5349 "Correct count should produce no issue"
5350 );
5351 }
5352
5353 #[test]
5354 fn test_unt_count_mismatch() {
5355 let segments = vec![
5357 make_segment("UNH", vec![vec!["001"]]),
5358 make_segment("BGM", vec![vec!["E01"]]),
5359 make_segment("UNT", vec![vec!["5", "001"]]),
5360 ];
5361 let issue =
5362 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
5363 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5364 assert_eq!(issue.severity, Severity::Error);
5365 assert!(narrate(&issue).contains("declared 5"));
5366 assert!(narrate(&issue).contains("actual 3"));
5367 }
5368
5369 #[test]
5370 fn test_unt_count_excludes_envelope() {
5371 let segments = vec![
5373 make_segment("UNA", vec![]),
5374 make_segment("UNB", vec![vec!["UNOC", "3"]]),
5375 make_segment("UNH", vec![vec!["001"]]),
5376 make_segment("BGM", vec![vec!["E01"]]),
5377 make_segment("UNT", vec![vec!["3", "001"]]),
5378 make_segment("UNZ", vec![vec!["1"]]),
5379 ];
5380 assert!(
5381 validate_unt_segment_count(&segments).is_none(),
5382 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
5383 );
5384 }
5385
5386 #[test]
5387 fn test_unt_count_no_unt_returns_none() {
5388 let segments = vec![
5389 make_segment("UNH", vec![vec!["001"]]),
5390 make_segment("BGM", vec![vec!["E01"]]),
5391 ];
5392 assert!(
5393 validate_unt_segment_count(&segments).is_none(),
5394 "No UNT segment should return None (not our problem)"
5395 );
5396 }
5397
5398 #[test]
5399 fn test_unt_count_rejects_multi_message_input() {
5400 let segments = vec![
5402 make_segment("UNH", vec![vec!["001"]]),
5403 make_segment("BGM", vec![vec!["E01"]]),
5404 make_segment("UNT", vec![vec!["3", "001"]]),
5405 make_segment("UNH", vec![vec!["002"]]),
5406 make_segment("BGM", vec![vec!["E02"]]),
5407 make_segment("UNT", vec![vec!["3", "002"]]),
5408 ];
5409 let issue = validate_unt_segment_count(&segments)
5410 .expect("Multi-message input should produce an error");
5411 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5412 assert!(
5413 narrate(&issue).contains("2 UNH"),
5414 "Should mention UNH count: {}",
5415 narrate(&issue)
5416 );
5417 }
5418
5419 #[test]
5420 fn test_code_validation_accepts_multi_code_variant_qualifier() {
5421 let evaluator = MockEvaluator::new(vec![]);
5428 let validator = EdifactValidator::new(evaluator);
5429 let external = NoOpExternalProvider;
5430
5431 let rff_z39 = OwnedSegment {
5432 id: "RFF".to_string(),
5433 elements: vec![
5434 vec!["RFF".to_string()],
5435 vec!["Z39".to_string(), "REF1".to_string()],
5436 ],
5437 segment_number: 1,
5438 };
5439
5440 let workflow = AhbWorkflow {
5441 pruefidentifikator: "55035".to_string(),
5442 description: "Test".to_string(),
5443 communication_direction: None,
5444 fields: vec![
5445 AhbFieldRule {
5447 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5448 name: "Referenznummer Qualifier".to_string(),
5449 ahb_status: "Muss".to_string(),
5450 codes: vec![
5451 AhbCodeRule {
5452 value: "Z31".to_string(),
5453 description: "".to_string(),
5454 ahb_status: "X".to_string(),
5455 },
5456 AhbCodeRule {
5457 value: "Z39".to_string(),
5458 description: "".to_string(),
5459 ahb_status: "X".to_string(),
5460 },
5461 ],
5462 parent_group_ahb_status: None,
5463 segment_ahb_status: None,
5464 element_index: Some(1),
5465 component_index: Some(0),
5466 mig_number: Some("00075".to_string()),
5467 format: None,
5468 },
5469 AhbFieldRule {
5471 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5472 name: "Referenznummer Qualifier".to_string(),
5473 ahb_status: "Muss".to_string(),
5474 codes: vec![AhbCodeRule {
5475 value: "Z33".to_string(),
5476 description: "".to_string(),
5477 ahb_status: "X".to_string(),
5478 }],
5479 parent_group_ahb_status: None,
5480 segment_ahb_status: None,
5481 element_index: Some(1),
5482 component_index: Some(0),
5483 mig_number: Some("00078".to_string()),
5484 format: None,
5485 },
5486 ],
5487 ub_definitions: BTreeMap::new(),
5488 package_conditions: Default::default(),
5489 };
5490
5491 let report = validator.validate(
5492 &[rff_z39],
5493 &workflow,
5494 &external,
5495 ValidationLevel::Conditions,
5496 );
5497
5498 let code_errors: Vec<_> = report
5499 .by_category(ValidationCategory::Code)
5500 .filter(|i| {
5501 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5502 })
5503 .collect();
5504 assert!(
5505 code_errors.is_empty(),
5506 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
5507 code_errors
5508 );
5509 }
5510
5511 #[test]
5512 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
5513 let evaluator = MockEvaluator::new(vec![]);
5519 let validator = EdifactValidator::new(evaluator);
5520 let external = NoOpExternalProvider;
5521
5522 let pia_5_z12 = OwnedSegment {
5526 id: "PIA".to_string(),
5527 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
5528 segment_number: 1,
5529 };
5530 let pia_5_srw = OwnedSegment {
5531 id: "PIA".to_string(),
5532 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
5533 segment_number: 2,
5534 };
5535
5536 let make_rules = |mig: &str, composite_code: &str| {
5537 vec![
5538 AhbFieldRule {
5539 segment_path: "SG4/SG8/PIA/4347".to_string(),
5540 name: "Produkt-ID-Funktion".to_string(),
5541 ahb_status: "Muss".to_string(),
5542 codes: vec![AhbCodeRule {
5543 value: "5".to_string(),
5544 description: "".to_string(),
5545 ahb_status: "X".to_string(),
5546 }],
5547 parent_group_ahb_status: None,
5548 segment_ahb_status: None,
5549 element_index: Some(0),
5550 component_index: Some(0),
5551 mig_number: Some(mig.to_string()),
5552 format: None,
5553 },
5554 AhbFieldRule {
5555 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
5556 name: "Artikel/Dienstleistung-Identifikator".to_string(),
5557 ahb_status: "Muss".to_string(),
5558 codes: vec![AhbCodeRule {
5559 value: composite_code.to_string(),
5560 description: "".to_string(),
5561 ahb_status: "X".to_string(),
5562 }],
5563 parent_group_ahb_status: None,
5564 segment_ahb_status: None,
5565 element_index: Some(1),
5566 component_index: Some(0),
5567 mig_number: Some(mig.to_string()),
5568 format: None,
5569 },
5570 ]
5571 };
5572
5573 let mut fields = make_rules("00108", "Z12");
5574 fields.extend(make_rules("00197", "SRW"));
5575
5576 let workflow = AhbWorkflow {
5577 pruefidentifikator: "55035".to_string(),
5578 description: "Test".to_string(),
5579 communication_direction: None,
5580 fields,
5581 ub_definitions: BTreeMap::new(),
5582 package_conditions: Default::default(),
5583 };
5584
5585 let report = validator.validate(
5586 &[pia_5_z12, pia_5_srw],
5587 &workflow,
5588 &external,
5589 ValidationLevel::Conditions,
5590 );
5591
5592 let code_errors: Vec<_> = report
5593 .by_category(ValidationCategory::Code)
5594 .filter(|i| {
5595 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5596 })
5597 .collect();
5598 assert!(
5599 code_errors.is_empty(),
5600 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
5601 code_errors
5602 );
5603 }
5604}