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