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 if seen.contains(&number)
1282 || Some(status) == group.ahb_status
1283 || !rule.codes.iter().any(|c| c.value == value)
1284 {
1285 continue;
1286 }
1287 seen.push(number);
1288 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1289 continue;
1290 }
1291 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1292 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
1293 report.add_issue(
1294 ValidationIssue::new(
1295 Severity::Warning,
1296 IssueKind::SegmentNotAllowed {
1297 segment: segment.join("/"),
1298 },
1299 )
1300 .with_field_path(segment.join("/"))
1301 .with_rule(status)
1302 .with_instance_index(instance_index)
1303 .with_position(crate::SegmentPosition {
1304 segment_number: number,
1305 byte_offset: 0,
1306 message_number: 1,
1307 }),
1308 );
1309 }
1310}
1311
1312#[allow(clippy::too_many_arguments)]
1321fn walk_group_instance<E: ConditionEvaluator>(
1322 group: &AhbGroupNode,
1323 path: &[(&str, usize)],
1324 scoped_ctx: &EvaluationContext,
1325 expr_eval: &ConditionExprEvaluator<E>,
1326 evaluator: &E,
1327 ub_definitions: &BTreeMap<String, ConditionExpr>,
1328 tag_migs: &HashMap<String, HashSet<&str>>,
1329 instance_index: usize,
1330 report_segments: bool,
1331 report: &mut ValidationReport,
1332) {
1333 let mut group_refused = false;
1336 if let Some(status) = group.ahb_status {
1337 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1338 group_refused = true;
1339 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1340 let mut issue = ValidationIssue::new(
1341 Severity::Warning,
1342 IssueKind::GroupNotAllowed {
1343 group: group_path.join("/"),
1344 },
1345 )
1346 .with_field_path(group_path.join("/"))
1347 .with_rule(status)
1348 .with_instance_index(instance_index);
1349 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
1350 issue = issue.with_position(crate::SegmentPosition {
1351 segment_number: num,
1352 byte_offset: 0,
1353 message_number: 1,
1354 });
1355 }
1356 report.add_issue(issue);
1357 }
1358 }
1359 if report_segments && !group_refused {
1360 refuse_segments(
1361 group,
1362 path,
1363 scoped_ctx,
1364 evaluator,
1365 ub_definitions,
1366 instance_index,
1367 report,
1368 );
1369 }
1370 for node in &group.fields {
1371 evaluate_node(
1372 node,
1373 scoped_ctx,
1374 expr_eval,
1375 evaluator,
1376 ub_definitions,
1377 tag_migs,
1378 Some(instance_index),
1379 report,
1380 );
1381 }
1382 for child in &group.children {
1383 let mut child_path = path.to_vec();
1384 child_path.push((child.group_id, child.instance_index));
1385 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
1386 walk_group_instance(
1387 child,
1388 &child_path,
1389 &child_ctx,
1390 expr_eval,
1391 evaluator,
1392 ub_definitions,
1393 tag_migs,
1394 instance_index,
1395 report_segments,
1396 report,
1397 );
1398 }
1399}
1400
1401fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
1403 match expr {
1404 ConditionExpr::Package { id, min, max } => {
1405 out.push((*id, *min, *max));
1406 }
1407 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1408 for e in exprs {
1409 collect_packages(e, out);
1410 }
1411 }
1412 ConditionExpr::Xor(left, right) => {
1413 collect_packages(left, out);
1414 collect_packages(right, out);
1415 }
1416 ConditionExpr::Not(inner) => {
1417 collect_packages(inner, out);
1418 }
1419 ConditionExpr::Ref(_) => {}
1420 }
1421}
1422
1423fn is_optional_segment_status(status: &str) -> bool {
1433 let trimmed = status.trim();
1434 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
1435}
1436
1437fn is_qualifier_field(path: &str) -> bool {
1447 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
1448 matches!(parts.len(), 2 | 3)
1450}
1451
1452#[derive(Default)]
1460struct MigCodeBucket<'a> {
1461 qualifier_position: Option<(usize, usize)>,
1464 qualifier_values: HashSet<&'a str>,
1467 codes: HashMap<(usize, usize), HashSet<&'a str>>,
1470}
1471
1472fn partition_codes_by_mig(
1477 workflow: &AhbWorkflow,
1478) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
1479 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
1480 for field in &workflow.fields {
1481 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
1482 continue;
1483 }
1484 let tag = extract_segment_id(&field.segment_path);
1485 let group_key = extract_group_path_key(&field.segment_path);
1486 let mig = field.mig_number.clone();
1487 let el = field.element_index.unwrap_or(0);
1488 let c = field.component_index.unwrap_or(0);
1489
1490 let bucket = out
1491 .entry((group_key, tag))
1492 .or_default()
1493 .entry(mig)
1494 .or_default();
1495
1496 let required: Vec<&str> = field
1503 .codes
1504 .iter()
1505 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
1506 .map(|code| code.value.as_str())
1507 .collect();
1508
1509 if !required.is_empty() {
1516 if bucket.qualifier_position.is_none() {
1517 bucket.qualifier_position = Some((el, c));
1518 }
1519 if bucket.qualifier_position == Some((el, c)) {
1520 bucket.qualifier_values.extend(required.iter().copied());
1521 }
1522 }
1523
1524 for v in required {
1525 bucket.codes.entry((el, c)).or_default().insert(v);
1526 }
1527 }
1528 out
1529}
1530
1531fn global_qualifiers_by_tag(
1537 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
1538) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
1539 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
1540 for ((_group, tag), migs) in by_loc {
1541 let tag_entry = out.entry(tag.clone()).or_default();
1542 for bucket in migs.values() {
1543 if let Some(pos) = bucket.qualifier_position {
1544 tag_entry
1545 .entry(pos)
1546 .or_default()
1547 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
1548 }
1549 }
1550 }
1551 out
1552}
1553
1554fn match_segment_to_mig<'a, 'b>(
1565 seg: &OwnedSegment,
1566 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
1567) -> Option<&'a MigCodeBucket<'b>> {
1568 let actual_at = |el: usize, c: usize| -> &str {
1569 seg.elements
1570 .get(el)
1571 .and_then(|e| e.get(c))
1572 .map(|s| s.as_str())
1573 .unwrap_or("")
1574 };
1575
1576 let mut best: Option<&MigCodeBucket> = None;
1577 let mut best_matches = 0usize;
1578
1579 for bucket in migs.values() {
1580 let Some((el, c)) = bucket.qualifier_position else {
1581 continue;
1582 };
1583 if !bucket.qualifier_values.contains(actual_at(el, c)) {
1584 continue;
1585 }
1586 let extra_matches = bucket
1587 .codes
1588 .iter()
1589 .filter(|(pos, _)| **pos != (el, c))
1590 .filter(|((e, k), allowed)| {
1591 let v = actual_at(*e, *k);
1592 !v.is_empty() && allowed.contains(v)
1593 })
1594 .count();
1595 if best.is_none() || extra_matches > best_matches {
1596 best = Some(bucket);
1597 best_matches = extra_matches;
1598 }
1599 }
1600 best
1601}
1602
1603fn extract_group_path_key(path: &str) -> String {
1608 let sg_parts: Vec<&str> = path
1609 .split('/')
1610 .take_while(|p| p.starts_with("SG"))
1611 .collect();
1612 sg_parts.join("/")
1613}
1614
1615fn extract_segment_id(path: &str) -> String {
1617 for part in path.split('/') {
1618 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
1620 continue;
1621 }
1622 if part.len() >= 3
1624 && part
1625 .chars()
1626 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
1627 {
1628 return part.to_string();
1629 }
1630 }
1631 path.split('/').next_back().unwrap_or(path).to_string()
1633}
1634
1635pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
1647 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
1649 if unh_count > 1 {
1650 return Some(ValidationIssue::new(
1651 Severity::Error,
1652 IssueKind::UntCountNotVerifiable { unh_count },
1653 ));
1654 }
1655
1656 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
1658 let declared: usize = unt.get_element(0).parse().ok()?;
1659
1660 let actual = segments
1662 .iter()
1663 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
1664 .count();
1665
1666 if declared != actual {
1667 Some(
1668 ValidationIssue::new(
1669 Severity::Error,
1670 IssueKind::UntSegmentCountMismatch { declared, actual },
1671 )
1672 .with_field_path("UNT/0074")
1673 .with_expected(actual.to_string())
1674 .with_actual(declared.to_string()),
1675 )
1676 } else {
1677 None
1678 }
1679}
1680
1681#[cfg(test)]
1682mod tests {
1683 use super::super::codes::ErrorCodes;
1684 use super::super::issue::ValidationCategory;
1685 use super::*;
1686 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
1687 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
1688 use std::collections::HashMap;
1689
1690 fn narrate(issue: &ValidationIssue) -> String {
1693 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
1694 }
1695
1696 struct MockEvaluator {
1698 results: HashMap<u32, CR>,
1699 }
1700
1701 impl MockEvaluator {
1702 fn new(results: Vec<(u32, CR)>) -> Self {
1703 Self {
1704 results: results.into_iter().collect(),
1705 }
1706 }
1707
1708 fn all_true(ids: &[u32]) -> Self {
1709 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
1710 }
1711 }
1712
1713 impl ConditionEvaluator for MockEvaluator {
1714 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
1715 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
1716 }
1717 fn is_external(&self, _condition: u32) -> bool {
1718 false
1719 }
1720 fn message_type(&self) -> &str {
1721 "UTILMD"
1722 }
1723 fn format_version(&self) -> &str {
1724 "FV2510"
1725 }
1726 }
1727
1728 #[test]
1731 fn a_present_code_is_judged_by_its_own_status() {
1732 let code = |value: &str, status: &str| AhbCodeRule {
1734 value: value.to_string(),
1735 description: String::new(),
1736 ahb_status: status.to_string(),
1737 };
1738 let field = AhbFieldRule {
1739 segment_path: "SG4/STS/C556/9013".to_string(),
1740 ahb_status: "X [192]".to_string(),
1741 codes: vec![code("E01", "X [192]"), code("E03", "X")],
1742 ..Default::default()
1743 };
1744 assert_eq!(governing_status(&field, Some("E03")), "X");
1745 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
1746 assert_eq!(governing_status(&field, None), "X [192]");
1747 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
1748
1749 let own = AhbFieldRule {
1751 ahb_status: "X [931]".to_string(),
1752 codes: vec![code("303", "X")],
1753 ..Default::default()
1754 };
1755 assert_eq!(governing_status(&own, Some("303")), "X [931]");
1756 }
1757
1758 #[test]
1759 fn test_extract_segment_id_simple() {
1760 assert_eq!(extract_segment_id("NAD"), "NAD");
1761 }
1762
1763 #[test]
1764 fn test_extract_segment_id_with_sg_prefix() {
1765 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
1766 }
1767
1768 #[test]
1769 fn test_extract_segment_id_nested_sg() {
1770 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
1771 }
1772
1773 #[test]
1776 fn test_validate_missing_mandatory_field() {
1777 let evaluator = MockEvaluator::all_true(&[182, 152]);
1778 let validator = EdifactValidator::new(evaluator);
1779 let external = NoOpExternalProvider;
1780
1781 let workflow = AhbWorkflow {
1782 pruefidentifikator: "11001".to_string(),
1783 description: "Test".to_string(),
1784 communication_direction: None,
1785 fields: vec![AhbFieldRule {
1786 segment_path: "SG2/NAD/C082/3039".to_string(),
1787 name: "MP-ID des MSB".to_string(),
1788 ahb_status: "Muss [182] ∧ [152]".to_string(),
1789 codes: vec![],
1790 parent_group_ahb_status: None,
1791 segment_ahb_status: None,
1792 ..Default::default()
1793 }],
1794 ub_definitions: BTreeMap::new(),
1795 };
1796
1797 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1799
1800 assert!(!report.is_valid());
1802 let errors: Vec<_> = report.errors().collect();
1803 assert_eq!(errors.len(), 1);
1804 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
1805 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
1806 }
1807
1808 #[test]
1809 fn test_validate_condition_false_no_error() {
1810 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
1812 let validator = EdifactValidator::new(evaluator);
1813 let external = NoOpExternalProvider;
1814
1815 let workflow = AhbWorkflow {
1816 pruefidentifikator: "11001".to_string(),
1817 description: "Test".to_string(),
1818 communication_direction: None,
1819 fields: vec![AhbFieldRule {
1820 segment_path: "NAD".to_string(),
1821 name: "Partnerrolle".to_string(),
1822 ahb_status: "Muss [182] ∧ [152]".to_string(),
1823 codes: vec![],
1824 parent_group_ahb_status: None,
1825 segment_ahb_status: None,
1826 ..Default::default()
1827 }],
1828 ub_definitions: BTreeMap::new(),
1829 };
1830
1831 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1832
1833 assert!(report.is_valid());
1835 }
1836
1837 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
1839 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
1840 let workflow = AhbWorkflow {
1841 pruefidentifikator: "11001".to_string(),
1842 description: "Test".to_string(),
1843 communication_direction: None,
1844 fields: vec![AhbFieldRule {
1845 segment_path: "SG2/NAD/C082/3039".to_string(),
1846 name: "MP-ID".to_string(),
1847 ahb_status: status.to_string(),
1848 ..Default::default()
1849 }],
1850 ub_definitions: BTreeMap::new(),
1851 };
1852 validator.validate(
1853 &[],
1854 &workflow,
1855 &NoOpExternalProvider,
1856 ValidationLevel::Conditions,
1857 )
1858 }
1859
1860 #[test]
1861 fn a_later_soll_line_alone_does_not_require_the_field() {
1862 let report = validate_absent_field(
1864 "Muss [2119] \r\nSoll [130]",
1865 vec![(2119, CR::False), (130, CR::True)],
1866 );
1867 assert!(
1868 report.is_valid(),
1869 "{:?}",
1870 report.errors().collect::<Vec<_>>()
1871 );
1872 }
1873
1874 #[test]
1875 fn a_later_muss_line_requires_the_field() {
1876 let report = validate_absent_field(
1878 "Soll [47]\r\nMuss [46]",
1879 vec![(47, CR::False), (46, CR::True)],
1880 );
1881 let errors: Vec<_> = report.errors().collect();
1882 assert_eq!(errors.len(), 1);
1883 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
1884 }
1885
1886 #[test]
1887 fn a_weaker_unknown_line_does_not_stop_a_muss() {
1888 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
1890 let errors: Vec<_> = report.errors().collect();
1891 assert_eq!(errors.len(), 1);
1892 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
1893 }
1894
1895 #[test]
1896 fn a_stricter_unknown_line_leaves_the_status_undecided() {
1897 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
1898 assert!(report.is_valid());
1899 let infos: Vec<_> = report.infos().collect();
1900 assert_eq!(infos.len(), 1, "{infos:?}");
1901 match &infos[0].kind {
1902 IssueKind::ConditionUnknown { unresolved, .. } => {
1903 assert_eq!(unresolved.missing, vec![2119]);
1904 }
1905 other => panic!("expected ConditionUnknown, got {other:?}"),
1906 }
1907 }
1908
1909 #[test]
1910 fn test_validate_condition_unknown_adds_info() {
1911 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
1913 let validator = EdifactValidator::new(evaluator);
1915 let external = NoOpExternalProvider;
1916
1917 let workflow = AhbWorkflow {
1918 pruefidentifikator: "11001".to_string(),
1919 description: "Test".to_string(),
1920 communication_direction: None,
1921 fields: vec![AhbFieldRule {
1922 segment_path: "NAD".to_string(),
1923 name: "Partnerrolle".to_string(),
1924 ahb_status: "Muss [182] ∧ [152]".to_string(),
1925 codes: vec![],
1926 parent_group_ahb_status: None,
1927 segment_ahb_status: None,
1928 ..Default::default()
1929 }],
1930 ub_definitions: BTreeMap::new(),
1931 };
1932
1933 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1934
1935 assert!(report.is_valid());
1937 let infos: Vec<_> = report.infos().collect();
1938 assert_eq!(infos.len(), 1);
1939 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
1940 }
1941
1942 #[test]
1943 fn test_validate_structure_level_skips_conditions() {
1944 let evaluator = MockEvaluator::all_true(&[182, 152]);
1945 let validator = EdifactValidator::new(evaluator);
1946 let external = NoOpExternalProvider;
1947
1948 let workflow = AhbWorkflow {
1949 pruefidentifikator: "11001".to_string(),
1950 description: "Test".to_string(),
1951 communication_direction: None,
1952 fields: vec![AhbFieldRule {
1953 segment_path: "NAD".to_string(),
1954 name: "Partnerrolle".to_string(),
1955 ahb_status: "Muss [182] ∧ [152]".to_string(),
1956 codes: vec![],
1957 parent_group_ahb_status: None,
1958 segment_ahb_status: None,
1959 ..Default::default()
1960 }],
1961 ub_definitions: BTreeMap::new(),
1962 };
1963
1964 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
1966
1967 assert!(report.is_valid());
1969 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
1970 }
1971
1972 #[test]
1973 fn test_validate_empty_workflow_no_condition_errors() {
1974 let evaluator = MockEvaluator::all_true(&[]);
1975 let validator = EdifactValidator::new(evaluator);
1976 let external = NoOpExternalProvider;
1977
1978 let empty_workflow = AhbWorkflow {
1979 pruefidentifikator: String::new(),
1980 description: String::new(),
1981 communication_direction: None,
1982 fields: vec![],
1983 ub_definitions: BTreeMap::new(),
1984 };
1985
1986 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
1987
1988 assert!(report.is_valid());
1989 }
1990
1991 #[test]
1992 fn test_validate_bare_muss_always_required() {
1993 let evaluator = MockEvaluator::new(vec![]);
1994 let validator = EdifactValidator::new(evaluator);
1995 let external = NoOpExternalProvider;
1996
1997 let workflow = AhbWorkflow {
1998 pruefidentifikator: "55001".to_string(),
1999 description: "Test".to_string(),
2000 communication_direction: Some("NB an LF".to_string()),
2001 fields: vec![AhbFieldRule {
2002 segment_path: "SG2/NAD/3035".to_string(),
2003 name: "Partnerrolle".to_string(),
2004 ahb_status: "Muss".to_string(), codes: vec![],
2006 parent_group_ahb_status: None,
2007 segment_ahb_status: None,
2008 ..Default::default()
2009 }],
2010 ub_definitions: BTreeMap::new(),
2011 };
2012
2013 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2014
2015 assert!(!report.is_valid());
2017 assert_eq!(report.error_count(), 1);
2018 }
2019
2020 #[test]
2021 fn test_validate_x_status_is_mandatory() {
2022 let evaluator = MockEvaluator::new(vec![]);
2023 let validator = EdifactValidator::new(evaluator);
2024 let external = NoOpExternalProvider;
2025
2026 let workflow = AhbWorkflow {
2027 pruefidentifikator: "55001".to_string(),
2028 description: "Test".to_string(),
2029 communication_direction: None,
2030 fields: vec![AhbFieldRule {
2031 segment_path: "DTM".to_string(),
2032 name: "Datum".to_string(),
2033 ahb_status: "X".to_string(),
2034 codes: vec![],
2035 parent_group_ahb_status: None,
2036 segment_ahb_status: None,
2037 ..Default::default()
2038 }],
2039 ub_definitions: BTreeMap::new(),
2040 };
2041
2042 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2043
2044 assert!(!report.is_valid());
2045 let errors: Vec<_> = report.errors().collect();
2046 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2047 }
2048
2049 #[test]
2050 fn test_validate_soll_not_mandatory() {
2051 let evaluator = MockEvaluator::new(vec![]);
2052 let validator = EdifactValidator::new(evaluator);
2053 let external = NoOpExternalProvider;
2054
2055 let workflow = AhbWorkflow {
2056 pruefidentifikator: "55001".to_string(),
2057 description: "Test".to_string(),
2058 communication_direction: None,
2059 fields: vec![AhbFieldRule {
2060 segment_path: "DTM".to_string(),
2061 name: "Datum".to_string(),
2062 ahb_status: "Soll".to_string(),
2063 codes: vec![],
2064 parent_group_ahb_status: None,
2065 segment_ahb_status: None,
2066 ..Default::default()
2067 }],
2068 ub_definitions: BTreeMap::new(),
2069 };
2070
2071 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2072
2073 assert!(report.is_valid());
2075 }
2076
2077 #[test]
2078 fn test_report_includes_metadata() {
2079 let evaluator = MockEvaluator::new(vec![]);
2080 let validator = EdifactValidator::new(evaluator);
2081 let external = NoOpExternalProvider;
2082
2083 let workflow = AhbWorkflow {
2084 pruefidentifikator: "55001".to_string(),
2085 description: String::new(),
2086 communication_direction: None,
2087 fields: vec![],
2088 ub_definitions: BTreeMap::new(),
2089 };
2090
2091 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
2092
2093 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
2094 assert_eq!(report.level, ValidationLevel::Full);
2095 assert_eq!(report.message_type, "UTILMD");
2096 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
2097 }
2098
2099 #[test]
2100 fn test_validate_with_navigator_returns_report() {
2101 let evaluator = MockEvaluator::all_true(&[]);
2102 let validator = EdifactValidator::new(evaluator);
2103 let external = NoOpExternalProvider;
2104 let nav = crate::eval::NoOpGroupNavigator;
2105
2106 let workflow = AhbWorkflow {
2107 pruefidentifikator: "55001".to_string(),
2108 description: "Test".to_string(),
2109 communication_direction: None,
2110 fields: vec![],
2111 ub_definitions: BTreeMap::new(),
2112 };
2113
2114 let report = validator.validate_with_navigator(
2115 &[],
2116 &workflow,
2117 &external,
2118 ValidationLevel::Full,
2119 &nav,
2120 );
2121 assert!(report.is_valid());
2122 }
2123
2124 #[test]
2125 fn test_code_validation_composite_paths_valid_codes() {
2126 let evaluator = MockEvaluator::new(vec![]);
2130 let validator = EdifactValidator::new(evaluator);
2131 let external = NoOpExternalProvider;
2132
2133 let unh_segment = OwnedSegment {
2134 id: "UNH".to_string(),
2135 elements: vec![
2136 vec!["ALEXANDE951842".to_string()],
2137 vec![
2138 "UTILMD".to_string(),
2139 "D".to_string(),
2140 "11A".to_string(),
2141 "UN".to_string(),
2142 "S2.1".to_string(),
2143 ],
2144 ],
2145 segment_number: 1,
2146 };
2147
2148 let workflow = AhbWorkflow {
2149 pruefidentifikator: "55001".to_string(),
2150 description: "Test".to_string(),
2151 communication_direction: None,
2152 fields: vec![
2153 AhbFieldRule {
2154 segment_path: "UNH/S009/0065".to_string(),
2155 name: "Nachrichtentyp".to_string(),
2156 ahb_status: "X".to_string(),
2157 codes: vec![AhbCodeRule {
2158 value: "UTILMD".to_string(),
2159 description: "Stammdaten".to_string(),
2160 ahb_status: "X".to_string(),
2161 }],
2162 parent_group_ahb_status: None,
2163 segment_ahb_status: None,
2164 element_index: Some(1),
2165 component_index: Some(0),
2166 ..Default::default()
2167 },
2168 AhbFieldRule {
2169 segment_path: "UNH/S009/0052".to_string(),
2170 name: "Version".to_string(),
2171 ahb_status: "X".to_string(),
2172 codes: vec![AhbCodeRule {
2173 value: "D".to_string(),
2174 description: "Draft".to_string(),
2175 ahb_status: "X".to_string(),
2176 }],
2177 parent_group_ahb_status: None,
2178 segment_ahb_status: None,
2179 element_index: Some(1),
2180 component_index: Some(1),
2181 ..Default::default()
2182 },
2183 ],
2184 ub_definitions: BTreeMap::new(),
2185 };
2186
2187 let report = validator.validate(
2188 &[unh_segment],
2189 &workflow,
2190 &external,
2191 ValidationLevel::Conditions,
2192 );
2193
2194 let code_errors: Vec<_> = report
2195 .by_category(ValidationCategory::Code)
2196 .filter(|i| i.severity == Severity::Error)
2197 .collect();
2198 assert!(
2199 code_errors.is_empty(),
2200 "Expected no code errors when composite values match allowed codes, got: {:?}",
2201 code_errors
2202 );
2203 }
2204
2205 #[test]
2206 fn test_code_validation_partitions_by_mig_number() {
2207 let evaluator = MockEvaluator::new(vec![]);
2211 let validator = EdifactValidator::new(evaluator);
2212 let external = NoOpExternalProvider;
2213
2214 let sts_7 = OwnedSegment {
2215 id: "STS".to_string(),
2216 elements: vec![
2217 vec!["7".to_string()],
2218 vec![String::new()],
2219 vec!["GH02".to_string()],
2220 vec!["ZW4".to_string()],
2221 ],
2222 segment_number: 1,
2223 };
2224 let sts_e01 = OwnedSegment {
2225 id: "STS".to_string(),
2226 elements: vec![
2227 vec!["E01".to_string()],
2228 vec![String::new()],
2229 vec!["A99".to_string(), "E_0614".to_string()],
2230 ],
2231 segment_number: 2,
2232 };
2233
2234 let workflow = AhbWorkflow {
2235 pruefidentifikator: "55018".to_string(),
2236 description: "Test".to_string(),
2237 communication_direction: None,
2238 fields: vec![
2239 AhbFieldRule {
2241 segment_path: "SG4/STS/C601/9015".to_string(),
2242 name: "Statuskategorie".to_string(),
2243 ahb_status: "X".to_string(),
2244 codes: vec![AhbCodeRule {
2245 value: "7".to_string(),
2246 description: "Transaktionsgrund".to_string(),
2247 ahb_status: "X".to_string(),
2248 }],
2249 parent_group_ahb_status: None,
2250 segment_ahb_status: None,
2251 element_index: Some(0),
2252 component_index: Some(0),
2253 mig_number: Some("00035".to_string()),
2254 },
2255 AhbFieldRule {
2256 segment_path: "SG4/STS/C556/9013".to_string(),
2257 name: "Statusanlaß".to_string(),
2258 ahb_status: "X".to_string(),
2259 codes: vec![AhbCodeRule {
2260 value: "E03".to_string(),
2261 description: "Transaktionsgrund".to_string(),
2262 ahb_status: "X".to_string(),
2263 }],
2264 parent_group_ahb_status: None,
2265 segment_ahb_status: None,
2266 element_index: Some(2),
2267 component_index: Some(0),
2268 mig_number: Some("00035".to_string()),
2269 },
2270 AhbFieldRule {
2272 segment_path: "SG4/STS/C601/9015".to_string(),
2273 name: "Statuskategorie".to_string(),
2274 ahb_status: "X".to_string(),
2275 codes: vec![AhbCodeRule {
2276 value: "E01".to_string(),
2277 description: "Antwort".to_string(),
2278 ahb_status: "X".to_string(),
2279 }],
2280 parent_group_ahb_status: None,
2281 segment_ahb_status: None,
2282 element_index: Some(0),
2283 component_index: Some(0),
2284 mig_number: Some("00036".to_string()),
2285 },
2286 ],
2287 ub_definitions: BTreeMap::new(),
2288 };
2289
2290 let report = validator.validate(
2291 &[sts_7, sts_e01],
2292 &workflow,
2293 &external,
2294 ValidationLevel::Conditions,
2295 );
2296
2297 let code_errors: Vec<_> = report
2298 .by_category(ValidationCategory::Code)
2299 .filter(|i| {
2300 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2301 })
2302 .collect();
2303 assert_eq!(
2304 code_errors.len(),
2305 1,
2306 "Expected one COD002 (for GH02 only), got: {:?}",
2307 code_errors
2308 );
2309 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2310 }
2311
2312 #[test]
2313 fn test_code_validation_composite_paths_detects_invalid_code() {
2314 let evaluator = MockEvaluator::new(vec![]);
2317 let validator = EdifactValidator::new(evaluator);
2318 let external = NoOpExternalProvider;
2319
2320 let sts_segment = OwnedSegment {
2321 id: "STS".to_string(),
2322 elements: vec![
2323 vec!["7".to_string()],
2324 vec![String::new()],
2325 vec!["GH02".to_string()],
2326 vec!["ZW4".to_string()],
2327 ],
2328 segment_number: 1,
2329 };
2330
2331 let workflow = AhbWorkflow {
2332 pruefidentifikator: "55018".to_string(),
2333 description: "Test".to_string(),
2334 communication_direction: None,
2335 fields: vec![AhbFieldRule {
2336 segment_path: "SG4/STS/C556/9013".to_string(),
2337 name: "Statusanlaß".to_string(),
2338 ahb_status: "X".to_string(),
2339 codes: vec![AhbCodeRule {
2340 value: "E03".to_string(),
2341 description: "Transaktionsgrund".to_string(),
2342 ahb_status: "X".to_string(),
2343 }],
2344 parent_group_ahb_status: None,
2345 segment_ahb_status: None,
2346 element_index: Some(2),
2347 component_index: Some(0),
2348 ..Default::default()
2349 }],
2350 ub_definitions: BTreeMap::new(),
2351 };
2352
2353 let report = validator.validate(
2354 &[sts_segment],
2355 &workflow,
2356 &external,
2357 ValidationLevel::Conditions,
2358 );
2359
2360 let code_errors: Vec<_> = report
2361 .by_category(ValidationCategory::Code)
2362 .filter(|i| {
2363 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2364 })
2365 .collect();
2366 assert_eq!(
2367 code_errors.len(),
2368 1,
2369 "Expected COD002 for GH02, got: {:?}",
2370 code_errors
2371 );
2372 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2373 }
2374
2375 #[test]
2376 fn test_cross_field_code_validation_valid_qualifiers() {
2377 let evaluator = MockEvaluator::new(vec![]);
2380 let validator = EdifactValidator::new(evaluator);
2381 let external = NoOpExternalProvider;
2382
2383 let nad_ms = OwnedSegment {
2384 id: "NAD".to_string(),
2385 elements: vec![vec!["MS".to_string()]],
2386 segment_number: 4,
2387 };
2388 let nad_mr = OwnedSegment {
2389 id: "NAD".to_string(),
2390 elements: vec![vec!["MR".to_string()]],
2391 segment_number: 5,
2392 };
2393
2394 let workflow = AhbWorkflow {
2395 pruefidentifikator: "55001".to_string(),
2396 description: "Test".to_string(),
2397 communication_direction: None,
2398 fields: vec![
2399 AhbFieldRule {
2400 segment_path: "SG2/NAD/3035".to_string(),
2401 name: "Absender".to_string(),
2402 ahb_status: "X".to_string(),
2403 codes: vec![AhbCodeRule {
2404 value: "MS".to_string(),
2405 description: "Absender".to_string(),
2406 ahb_status: "X".to_string(),
2407 }],
2408 parent_group_ahb_status: None,
2409 segment_ahb_status: None,
2410 ..Default::default()
2411 },
2412 AhbFieldRule {
2413 segment_path: "SG2/NAD/3035".to_string(),
2414 name: "Empfaenger".to_string(),
2415 ahb_status: "X".to_string(),
2416 codes: vec![AhbCodeRule {
2417 value: "MR".to_string(),
2418 description: "Empfaenger".to_string(),
2419 ahb_status: "X".to_string(),
2420 }],
2421 parent_group_ahb_status: None,
2422 segment_ahb_status: None,
2423 ..Default::default()
2424 },
2425 ],
2426 ub_definitions: BTreeMap::new(),
2427 };
2428
2429 let report = validator.validate(
2430 &[nad_ms, nad_mr],
2431 &workflow,
2432 &external,
2433 ValidationLevel::Conditions,
2434 );
2435
2436 let code_errors: Vec<_> = report
2437 .by_category(ValidationCategory::Code)
2438 .filter(|i| i.severity == Severity::Error)
2439 .collect();
2440 assert!(
2441 code_errors.is_empty(),
2442 "Expected no code errors for valid qualifiers, got: {:?}",
2443 code_errors
2444 );
2445 }
2446
2447 #[test]
2448 fn test_cross_field_code_validation_catches_invalid_qualifier() {
2449 let evaluator = MockEvaluator::new(vec![]);
2451 let validator = EdifactValidator::new(evaluator);
2452 let external = NoOpExternalProvider;
2453
2454 let nad_ms = OwnedSegment {
2455 id: "NAD".to_string(),
2456 elements: vec![vec!["MS".to_string()]],
2457 segment_number: 4,
2458 };
2459 let nad_mt = OwnedSegment {
2460 id: "NAD".to_string(),
2461 elements: vec![vec!["MT".to_string()]], segment_number: 5,
2463 };
2464
2465 let workflow = AhbWorkflow {
2466 pruefidentifikator: "55001".to_string(),
2467 description: "Test".to_string(),
2468 communication_direction: None,
2469 fields: vec![
2470 AhbFieldRule {
2471 segment_path: "SG2/NAD/3035".to_string(),
2472 name: "Absender".to_string(),
2473 ahb_status: "X".to_string(),
2474 codes: vec![AhbCodeRule {
2475 value: "MS".to_string(),
2476 description: "Absender".to_string(),
2477 ahb_status: "X".to_string(),
2478 }],
2479 parent_group_ahb_status: None,
2480 segment_ahb_status: None,
2481 ..Default::default()
2482 },
2483 AhbFieldRule {
2484 segment_path: "SG2/NAD/3035".to_string(),
2485 name: "Empfaenger".to_string(),
2486 ahb_status: "X".to_string(),
2487 codes: vec![AhbCodeRule {
2488 value: "MR".to_string(),
2489 description: "Empfaenger".to_string(),
2490 ahb_status: "X".to_string(),
2491 }],
2492 parent_group_ahb_status: None,
2493 segment_ahb_status: None,
2494 ..Default::default()
2495 },
2496 ],
2497 ub_definitions: BTreeMap::new(),
2498 };
2499
2500 let report = validator.validate(
2501 &[nad_ms, nad_mt],
2502 &workflow,
2503 &external,
2504 ValidationLevel::Conditions,
2505 );
2506
2507 let code_errors: Vec<_> = report
2508 .by_category(ValidationCategory::Code)
2509 .filter(|i| i.severity == Severity::Error)
2510 .collect();
2511 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
2512 assert!(narrate(code_errors[0]).contains("MT"));
2513 assert!(narrate(code_errors[0]).contains("MR"));
2514 assert!(narrate(code_errors[0]).contains("MS"));
2515 }
2516
2517 #[test]
2518 fn test_cross_field_code_validation_unions_across_groups() {
2519 let evaluator = MockEvaluator::new(vec![]);
2523 let validator = EdifactValidator::new(evaluator);
2524 let external = NoOpExternalProvider;
2525
2526 let segments = vec![
2527 OwnedSegment {
2528 id: "NAD".to_string(),
2529 elements: vec![vec!["MS".to_string()]],
2530 segment_number: 3,
2531 },
2532 OwnedSegment {
2533 id: "NAD".to_string(),
2534 elements: vec![vec!["MR".to_string()]],
2535 segment_number: 4,
2536 },
2537 OwnedSegment {
2538 id: "NAD".to_string(),
2539 elements: vec![vec!["Z04".to_string()]],
2540 segment_number: 20,
2541 },
2542 OwnedSegment {
2543 id: "NAD".to_string(),
2544 elements: vec![vec!["Z09".to_string()]],
2545 segment_number: 21,
2546 },
2547 OwnedSegment {
2548 id: "NAD".to_string(),
2549 elements: vec![vec!["MT".to_string()]], segment_number: 22,
2551 },
2552 ];
2553
2554 let workflow = AhbWorkflow {
2555 pruefidentifikator: "55001".to_string(),
2556 description: "Test".to_string(),
2557 communication_direction: None,
2558 fields: vec![
2559 AhbFieldRule {
2560 segment_path: "SG2/NAD/3035".to_string(),
2561 name: "Absender".to_string(),
2562 ahb_status: "X".to_string(),
2563 codes: vec![AhbCodeRule {
2564 value: "MS".to_string(),
2565 description: "Absender".to_string(),
2566 ahb_status: "X".to_string(),
2567 }],
2568 parent_group_ahb_status: None,
2569 segment_ahb_status: None,
2570 ..Default::default()
2571 },
2572 AhbFieldRule {
2573 segment_path: "SG2/NAD/3035".to_string(),
2574 name: "Empfaenger".to_string(),
2575 ahb_status: "X".to_string(),
2576 codes: vec![AhbCodeRule {
2577 value: "MR".to_string(),
2578 description: "Empfaenger".to_string(),
2579 ahb_status: "X".to_string(),
2580 }],
2581 parent_group_ahb_status: None,
2582 segment_ahb_status: None,
2583 ..Default::default()
2584 },
2585 AhbFieldRule {
2586 segment_path: "SG4/SG12/NAD/3035".to_string(),
2587 name: "Anschlussnutzer".to_string(),
2588 ahb_status: "X".to_string(),
2589 codes: vec![AhbCodeRule {
2590 value: "Z04".to_string(),
2591 description: "Anschlussnutzer".to_string(),
2592 ahb_status: "X".to_string(),
2593 }],
2594 parent_group_ahb_status: None,
2595 segment_ahb_status: None,
2596 ..Default::default()
2597 },
2598 AhbFieldRule {
2599 segment_path: "SG4/SG12/NAD/3035".to_string(),
2600 name: "Korrespondenzanschrift".to_string(),
2601 ahb_status: "X".to_string(),
2602 codes: vec![AhbCodeRule {
2603 value: "Z09".to_string(),
2604 description: "Korrespondenzanschrift".to_string(),
2605 ahb_status: "X".to_string(),
2606 }],
2607 parent_group_ahb_status: None,
2608 segment_ahb_status: None,
2609 ..Default::default()
2610 },
2611 ],
2612 ub_definitions: BTreeMap::new(),
2613 };
2614
2615 let report =
2616 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
2617
2618 let code_errors: Vec<_> = report
2619 .by_category(ValidationCategory::Code)
2620 .filter(|i| i.severity == Severity::Error)
2621 .collect();
2622 assert_eq!(
2623 code_errors.len(),
2624 1,
2625 "Expected exactly one COD002 error for MT, got: {:?}",
2626 code_errors
2627 );
2628 assert!(narrate(code_errors[0]).contains("MT"));
2629 }
2630
2631 #[test]
2632 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
2633 let evaluator = MockEvaluator::new(vec![]);
2642 let validator = EdifactValidator::new(evaluator);
2643 let external = NoOpExternalProvider;
2644
2645 let qty_67 = OwnedSegment {
2646 id: "QTY".to_string(),
2647 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
2648 segment_number: 10,
2649 };
2650
2651 let workflow = AhbWorkflow {
2652 pruefidentifikator: "13025".to_string(),
2653 description: "Test".to_string(),
2654 communication_direction: None,
2655 fields: vec![AhbFieldRule {
2656 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
2657 name: "Menge, Qualifier".to_string(),
2658 ahb_status: "X".to_string(),
2659 codes: vec![
2660 AhbCodeRule {
2661 value: "220".to_string(),
2662 description: "Wahrer Wert".to_string(),
2663 ahb_status: "X".to_string(),
2664 },
2665 AhbCodeRule {
2666 value: "67".to_string(),
2667 description: "Ersatzwert".to_string(),
2668 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
2669 },
2670 AhbCodeRule {
2671 value: "Z18".to_string(),
2672 description: "Vorläufiger Wert".to_string(),
2673 ahb_status: "X [35]".to_string(),
2674 },
2675 ],
2676 parent_group_ahb_status: None,
2677 segment_ahb_status: None,
2678 element_index: Some(0),
2679 component_index: Some(0),
2680 ..Default::default()
2681 }],
2682 ub_definitions: BTreeMap::new(),
2683 };
2684
2685 let report =
2686 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
2687
2688 let code_errors: Vec<_> = report
2689 .by_category(ValidationCategory::Code)
2690 .filter(|i| i.severity == Severity::Error)
2691 .collect();
2692 assert!(
2693 code_errors.is_empty(),
2694 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
2695 code_errors
2696 );
2697 }
2698
2699 #[test]
2700 fn test_is_qualifier_field_simple_paths() {
2701 assert!(is_qualifier_field("NAD/3035"));
2702 assert!(is_qualifier_field("SG2/NAD/3035"));
2703 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
2704 assert!(is_qualifier_field("LOC/3227"));
2705 }
2706
2707 #[test]
2708 fn test_is_qualifier_field_composite_paths() {
2709 assert!(is_qualifier_field("UNH/S009/0065"));
2713 assert!(is_qualifier_field("NAD/C082/3039"));
2714 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
2715 assert!(is_qualifier_field("SG4/STS/C556/9013"));
2716 }
2717
2718 #[test]
2719 fn test_is_qualifier_field_bare_segment() {
2720 assert!(!is_qualifier_field("NAD"));
2721 assert!(!is_qualifier_field("SG2/NAD"));
2722 }
2723
2724 #[test]
2725 fn test_is_qualifier_field_rejects_deep_paths() {
2726 assert!(!is_qualifier_field("SEG/A/B/C/D"));
2728 }
2729
2730 #[test]
2731 fn test_missing_qualifier_instance_is_detected() {
2732 let evaluator = MockEvaluator::new(vec![]);
2735 let validator = EdifactValidator::new(evaluator);
2736 let external = NoOpExternalProvider;
2737
2738 let nad_ms = OwnedSegment {
2739 id: "NAD".to_string(),
2740 elements: vec![vec!["MS".to_string()]],
2741 segment_number: 3,
2742 };
2743
2744 let workflow = AhbWorkflow {
2745 pruefidentifikator: "55001".to_string(),
2746 description: "Test".to_string(),
2747 communication_direction: None,
2748 fields: vec![
2749 AhbFieldRule {
2750 segment_path: "SG2/NAD/3035".to_string(),
2751 name: "Absender".to_string(),
2752 ahb_status: "X".to_string(),
2753 codes: vec![AhbCodeRule {
2754 value: "MS".to_string(),
2755 description: "Absender".to_string(),
2756 ahb_status: "X".to_string(),
2757 }],
2758 parent_group_ahb_status: None,
2759 segment_ahb_status: None,
2760 ..Default::default()
2761 },
2762 AhbFieldRule {
2763 segment_path: "SG2/NAD/3035".to_string(),
2764 name: "Empfaenger".to_string(),
2765 ahb_status: "Muss".to_string(),
2766 codes: vec![AhbCodeRule {
2767 value: "MR".to_string(),
2768 description: "Empfaenger".to_string(),
2769 ahb_status: "X".to_string(),
2770 }],
2771 parent_group_ahb_status: None,
2772 segment_ahb_status: None,
2773 ..Default::default()
2774 },
2775 ],
2776 ub_definitions: BTreeMap::new(),
2777 };
2778
2779 let report =
2780 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
2781
2782 let ahb_errors: Vec<_> = report
2783 .by_category(ValidationCategory::Ahb)
2784 .filter(|i| i.severity == Severity::Error)
2785 .collect();
2786 assert_eq!(
2787 ahb_errors.len(),
2788 1,
2789 "Expected AHB001 for missing NAD+MR, got: {:?}",
2790 ahb_errors
2791 );
2792 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
2793 }
2794
2795 #[test]
2796 fn test_present_qualifier_instance_no_error() {
2797 let evaluator = MockEvaluator::new(vec![]);
2799 let validator = EdifactValidator::new(evaluator);
2800 let external = NoOpExternalProvider;
2801
2802 let segments = vec![
2803 OwnedSegment {
2804 id: "NAD".to_string(),
2805 elements: vec![vec!["MS".to_string()]],
2806 segment_number: 3,
2807 },
2808 OwnedSegment {
2809 id: "NAD".to_string(),
2810 elements: vec![vec!["MR".to_string()]],
2811 segment_number: 4,
2812 },
2813 ];
2814
2815 let workflow = AhbWorkflow {
2816 pruefidentifikator: "55001".to_string(),
2817 description: "Test".to_string(),
2818 communication_direction: None,
2819 fields: vec![
2820 AhbFieldRule {
2821 segment_path: "SG2/NAD/3035".to_string(),
2822 name: "Absender".to_string(),
2823 ahb_status: "Muss".to_string(),
2824 codes: vec![AhbCodeRule {
2825 value: "MS".to_string(),
2826 description: "Absender".to_string(),
2827 ahb_status: "X".to_string(),
2828 }],
2829 parent_group_ahb_status: None,
2830 segment_ahb_status: None,
2831 ..Default::default()
2832 },
2833 AhbFieldRule {
2834 segment_path: "SG2/NAD/3035".to_string(),
2835 name: "Empfaenger".to_string(),
2836 ahb_status: "Muss".to_string(),
2837 codes: vec![AhbCodeRule {
2838 value: "MR".to_string(),
2839 description: "Empfaenger".to_string(),
2840 ahb_status: "X".to_string(),
2841 }],
2842 parent_group_ahb_status: None,
2843 segment_ahb_status: None,
2844 ..Default::default()
2845 },
2846 ],
2847 ub_definitions: BTreeMap::new(),
2848 };
2849
2850 let report =
2851 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
2852
2853 let ahb_errors: Vec<_> = report
2854 .by_category(ValidationCategory::Ahb)
2855 .filter(|i| i.severity == Severity::Error)
2856 .collect();
2857 assert!(
2858 ahb_errors.is_empty(),
2859 "Expected no AHB001 errors, got: {:?}",
2860 ahb_errors
2861 );
2862 }
2863
2864 #[test]
2865 fn test_extract_group_path_key() {
2866 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
2867 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
2868 assert_eq!(extract_group_path_key("NAD/3035"), "");
2869 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
2870 }
2871
2872 fn validate_field_of_absent_group(
2874 group_status: &str,
2875 conditions: Vec<(u32, CR)>,
2876 ) -> ValidationReport {
2877 use mig_types::navigator::GroupNavigator;
2878
2879 struct NoSg9;
2880 impl GroupNavigator for NoSg9 {
2881 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2882 vec![]
2883 }
2884 fn find_segments_with_qualifier_in_group(
2885 &self,
2886 _: &str,
2887 _: usize,
2888 _: &str,
2889 _: &[&str],
2890 _: usize,
2891 ) -> Vec<OwnedSegment> {
2892 vec![]
2893 }
2894 fn group_instance_count(&self, _: &[&str]) -> usize {
2895 0
2896 }
2897 }
2898
2899 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
2900 let workflow = AhbWorkflow {
2901 pruefidentifikator: "44001".to_string(),
2902 description: "Test".to_string(),
2903 communication_direction: None,
2904 fields: vec![AhbFieldRule {
2905 segment_path: "SG9/QTY/C186/6060".to_string(),
2906 name: "Menge".to_string(),
2907 ahb_status: "X".to_string(),
2908 parent_group_ahb_status: Some(group_status.to_string()),
2909 ..Default::default()
2910 }],
2911 ub_definitions: BTreeMap::new(),
2912 };
2913 validator.validate_with_navigator(
2914 &[],
2915 &workflow,
2916 &NoOpExternalProvider,
2917 ValidationLevel::Conditions,
2918 &NoSg9,
2919 )
2920 }
2921
2922 #[test]
2923 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
2924 let report = validate_field_of_absent_group(
2926 "Soll [47]\r\nMuss [46]",
2927 vec![(47, CR::True), (46, CR::False)],
2928 );
2929 assert!(
2930 report.is_valid(),
2931 "{:?}",
2932 report.errors().collect::<Vec<_>>()
2933 );
2934 }
2935
2936 #[test]
2937 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
2938 let report = validate_field_of_absent_group(
2939 "Soll [47]\r\nMuss [46]",
2940 vec![(47, CR::False), (46, CR::True)],
2941 );
2942 let errors: Vec<_> = report.errors().collect();
2943 assert_eq!(errors.len(), 1, "{errors:?}");
2944 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2945 }
2946
2947 #[test]
2948 fn test_absent_optional_group_no_missing_field_error() {
2949 use mig_types::navigator::GroupNavigator;
2952
2953 struct NavWithoutSG3;
2954 impl GroupNavigator for NavWithoutSG3 {
2955 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2956 vec![]
2957 }
2958 fn find_segments_with_qualifier_in_group(
2959 &self,
2960 _: &str,
2961 _: usize,
2962 _: &str,
2963 _: &[&str],
2964 _: usize,
2965 ) -> Vec<OwnedSegment> {
2966 vec![]
2967 }
2968 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2969 match group_path {
2970 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
2973 }
2974 }
2975 }
2976
2977 let evaluator = MockEvaluator::new(vec![]);
2978 let validator = EdifactValidator::new(evaluator);
2979 let external = NoOpExternalProvider;
2980 let nav = NavWithoutSG3;
2981
2982 let segments = vec![
2984 OwnedSegment {
2985 id: "NAD".into(),
2986 elements: vec![vec!["MS".into()]],
2987 segment_number: 3,
2988 },
2989 OwnedSegment {
2990 id: "NAD".into(),
2991 elements: vec![vec!["MR".into()]],
2992 segment_number: 4,
2993 },
2994 ];
2995
2996 let workflow = AhbWorkflow {
2997 pruefidentifikator: "55001".to_string(),
2998 description: "Test".to_string(),
2999 communication_direction: None,
3000 fields: vec![
3001 AhbFieldRule {
3002 segment_path: "SG2/SG3/CTA/3139".to_string(),
3003 name: "Funktion des Ansprechpartners, Code".to_string(),
3004 ahb_status: "Muss".to_string(),
3005 codes: vec![],
3006 parent_group_ahb_status: None,
3007 segment_ahb_status: None,
3008 ..Default::default()
3009 },
3010 AhbFieldRule {
3011 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
3012 name: "Name vom Ansprechpartner".to_string(),
3013 ahb_status: "X".to_string(),
3014 codes: vec![],
3015 parent_group_ahb_status: None,
3016 segment_ahb_status: None,
3017 ..Default::default()
3018 },
3019 ],
3020 ub_definitions: BTreeMap::new(),
3021 };
3022
3023 let report = validator.validate_with_navigator(
3024 &segments,
3025 &workflow,
3026 &external,
3027 ValidationLevel::Conditions,
3028 &nav,
3029 );
3030
3031 let ahb_errors: Vec<_> = report
3032 .by_category(ValidationCategory::Ahb)
3033 .filter(|i| i.severity == Severity::Error)
3034 .collect();
3035 assert!(
3036 ahb_errors.is_empty(),
3037 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
3038 ahb_errors
3039 );
3040 }
3041
3042 #[test]
3043 fn test_present_group_still_checks_mandatory_fields() {
3044 use mig_types::navigator::GroupNavigator;
3046
3047 struct NavWithSG3;
3048 impl GroupNavigator for NavWithSG3 {
3049 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3050 vec![]
3051 }
3052 fn find_segments_with_qualifier_in_group(
3053 &self,
3054 _: &str,
3055 _: usize,
3056 _: &str,
3057 _: &[&str],
3058 _: usize,
3059 ) -> Vec<OwnedSegment> {
3060 vec![]
3061 }
3062 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3063 match group_path {
3064 ["SG2"] => 1,
3065 ["SG2", "SG3"] => 1, _ => 0,
3067 }
3068 }
3069 }
3070
3071 let evaluator = MockEvaluator::new(vec![]);
3072 let validator = EdifactValidator::new(evaluator);
3073 let external = NoOpExternalProvider;
3074 let nav = NavWithSG3;
3075
3076 let segments = vec![OwnedSegment {
3078 id: "NAD".into(),
3079 elements: vec![vec!["MS".into()]],
3080 segment_number: 3,
3081 }];
3082
3083 let workflow = AhbWorkflow {
3084 pruefidentifikator: "55001".to_string(),
3085 description: "Test".to_string(),
3086 communication_direction: None,
3087 fields: vec![AhbFieldRule {
3088 segment_path: "SG2/SG3/CTA/3139".to_string(),
3089 name: "Funktion des Ansprechpartners, Code".to_string(),
3090 ahb_status: "Muss".to_string(),
3091 codes: vec![],
3092 parent_group_ahb_status: None,
3093 segment_ahb_status: None,
3094 ..Default::default()
3095 }],
3096 ub_definitions: BTreeMap::new(),
3097 };
3098
3099 let report = validator.validate_with_navigator(
3100 &segments,
3101 &workflow,
3102 &external,
3103 ValidationLevel::Conditions,
3104 &nav,
3105 );
3106
3107 let ahb_errors: Vec<_> = report
3108 .by_category(ValidationCategory::Ahb)
3109 .filter(|i| i.severity == Severity::Error)
3110 .collect();
3111 assert_eq!(
3112 ahb_errors.len(),
3113 1,
3114 "Expected AHB001 error when SG3 is present but CTA missing"
3115 );
3116 assert!(narrate(ahb_errors[0]).contains("CTA"));
3117 }
3118
3119 #[test]
3120 fn test_missing_qualifier_with_navigator_is_detected() {
3121 use mig_types::navigator::GroupNavigator;
3124
3125 struct NavWithSG2;
3126 impl GroupNavigator for NavWithSG2 {
3127 fn find_segments_in_group(
3128 &self,
3129 segment_id: &str,
3130 group_path: &[&str],
3131 instance_index: usize,
3132 ) -> Vec<OwnedSegment> {
3133 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
3134 vec![OwnedSegment {
3135 id: "NAD".into(),
3136 elements: vec![vec!["MS".into()]],
3137 segment_number: 3,
3138 }]
3139 } else {
3140 vec![]
3141 }
3142 }
3143 fn find_segments_with_qualifier_in_group(
3144 &self,
3145 _: &str,
3146 _: usize,
3147 _: &str,
3148 _: &[&str],
3149 _: usize,
3150 ) -> Vec<OwnedSegment> {
3151 vec![]
3152 }
3153 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3154 match group_path {
3155 ["SG2"] => 1,
3156 _ => 0,
3157 }
3158 }
3159 }
3160
3161 let evaluator = MockEvaluator::new(vec![]);
3162 let validator = EdifactValidator::new(evaluator);
3163 let external = NoOpExternalProvider;
3164 let nav = NavWithSG2;
3165
3166 let segments = vec![OwnedSegment {
3167 id: "NAD".into(),
3168 elements: vec![vec!["MS".into()]],
3169 segment_number: 3,
3170 }];
3171
3172 let workflow = AhbWorkflow {
3173 pruefidentifikator: "55001".to_string(),
3174 description: "Test".to_string(),
3175 communication_direction: None,
3176 fields: vec![
3177 AhbFieldRule {
3178 segment_path: "SG2/NAD/3035".to_string(),
3179 name: "Absender".to_string(),
3180 ahb_status: "X".to_string(),
3181 codes: vec![AhbCodeRule {
3182 value: "MS".to_string(),
3183 description: "Absender".to_string(),
3184 ahb_status: "X".to_string(),
3185 }],
3186 parent_group_ahb_status: None,
3187 segment_ahb_status: None,
3188 ..Default::default()
3189 },
3190 AhbFieldRule {
3191 segment_path: "SG2/NAD/3035".to_string(),
3192 name: "Empfaenger".to_string(),
3193 ahb_status: "Muss".to_string(),
3194 codes: vec![AhbCodeRule {
3195 value: "MR".to_string(),
3196 description: "Empfaenger".to_string(),
3197 ahb_status: "X".to_string(),
3198 }],
3199 parent_group_ahb_status: None,
3200 segment_ahb_status: None,
3201 ..Default::default()
3202 },
3203 ],
3204 ub_definitions: BTreeMap::new(),
3205 };
3206
3207 let report = validator.validate_with_navigator(
3208 &segments,
3209 &workflow,
3210 &external,
3211 ValidationLevel::Conditions,
3212 &nav,
3213 );
3214
3215 let ahb_errors: Vec<_> = report
3216 .by_category(ValidationCategory::Ahb)
3217 .filter(|i| i.severity == Severity::Error)
3218 .collect();
3219 assert_eq!(
3220 ahb_errors.len(),
3221 1,
3222 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
3223 ahb_errors
3224 );
3225 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3226 }
3227
3228 #[test]
3229 fn test_optional_group_variant_absent_no_error() {
3230 use mig_types::navigator::GroupNavigator;
3235
3236 struct TestNav;
3237 impl GroupNavigator for TestNav {
3238 fn find_segments_in_group(
3239 &self,
3240 segment_id: &str,
3241 group_path: &[&str],
3242 instance_index: usize,
3243 ) -> Vec<OwnedSegment> {
3244 match (segment_id, group_path, instance_index) {
3245 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
3246 id: "LOC".into(),
3247 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3248 segment_number: 10,
3249 }],
3250 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
3251 id: "NAD".into(),
3252 elements: vec![vec!["MS".into()]],
3253 segment_number: 3,
3254 }],
3255 _ => vec![],
3256 }
3257 }
3258 fn find_segments_with_qualifier_in_group(
3259 &self,
3260 _: &str,
3261 _: usize,
3262 _: &str,
3263 _: &[&str],
3264 _: usize,
3265 ) -> Vec<OwnedSegment> {
3266 vec![]
3267 }
3268 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3269 match group_path {
3270 ["SG2"] => 1,
3271 ["SG4"] => 1,
3272 ["SG4", "SG5"] => 1, _ => 0,
3274 }
3275 }
3276 }
3277
3278 let evaluator = MockEvaluator::new(vec![]);
3279 let validator = EdifactValidator::new(evaluator);
3280 let external = NoOpExternalProvider;
3281 let nav = TestNav;
3282
3283 let segments = vec![
3284 OwnedSegment {
3285 id: "NAD".into(),
3286 elements: vec![vec!["MS".into()]],
3287 segment_number: 3,
3288 },
3289 OwnedSegment {
3290 id: "LOC".into(),
3291 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3292 segment_number: 10,
3293 },
3294 ];
3295
3296 let workflow = AhbWorkflow {
3297 pruefidentifikator: "55001".to_string(),
3298 description: "Test".to_string(),
3299 communication_direction: None,
3300 fields: vec![
3301 AhbFieldRule {
3303 segment_path: "SG2/NAD/3035".to_string(),
3304 name: "Absender".to_string(),
3305 ahb_status: "X".to_string(),
3306 codes: vec![AhbCodeRule {
3307 value: "MS".to_string(),
3308 description: "Absender".to_string(),
3309 ahb_status: "X".to_string(),
3310 }],
3311 parent_group_ahb_status: Some("Muss".to_string()),
3312 segment_ahb_status: None,
3313 ..Default::default()
3314 },
3315 AhbFieldRule {
3316 segment_path: "SG2/NAD/3035".to_string(),
3317 name: "Empfaenger".to_string(),
3318 ahb_status: "Muss".to_string(),
3319 codes: vec![AhbCodeRule {
3320 value: "MR".to_string(),
3321 description: "Empfaenger".to_string(),
3322 ahb_status: "X".to_string(),
3323 }],
3324 parent_group_ahb_status: Some("Muss".to_string()),
3325 segment_ahb_status: None,
3326 ..Default::default()
3327 },
3328 AhbFieldRule {
3330 segment_path: "SG4/SG5/LOC/3227".to_string(),
3331 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3332 ahb_status: "X".to_string(),
3333 codes: vec![AhbCodeRule {
3334 value: "Z16".to_string(),
3335 description: "Marktlokation".to_string(),
3336 ahb_status: "X".to_string(),
3337 }],
3338 parent_group_ahb_status: Some("Kann".to_string()),
3339 segment_ahb_status: None,
3340 ..Default::default()
3341 },
3342 AhbFieldRule {
3343 segment_path: "SG4/SG5/LOC/3227".to_string(),
3344 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3345 ahb_status: "Muss".to_string(),
3346 codes: vec![AhbCodeRule {
3347 value: "Z17".to_string(),
3348 description: "Messlokation".to_string(),
3349 ahb_status: "X".to_string(),
3350 }],
3351 parent_group_ahb_status: Some("Kann".to_string()),
3352 segment_ahb_status: None,
3353 ..Default::default()
3354 },
3355 ],
3356 ub_definitions: BTreeMap::new(),
3357 };
3358
3359 let report = validator.validate_with_navigator(
3360 &segments,
3361 &workflow,
3362 &external,
3363 ValidationLevel::Conditions,
3364 &nav,
3365 );
3366
3367 let ahb_errors: Vec<_> = report
3368 .by_category(ValidationCategory::Ahb)
3369 .filter(|i| i.severity == Severity::Error)
3370 .collect();
3371
3372 assert_eq!(
3375 ahb_errors.len(),
3376 1,
3377 "Expected only AHB001 for missing NAD+MR, got: {:?}",
3378 ahb_errors
3379 );
3380 assert!(
3381 narrate(ahb_errors[0]).contains("Empfaenger"),
3382 "Error should be for missing NAD+MR (Empfaenger)"
3383 );
3384 }
3385
3386 #[test]
3390 fn test_optional_group_variant_with_true_condition_absent_no_error() {
3391 use mig_types::navigator::GroupNavigator;
3392
3393 struct TestNav;
3394 impl GroupNavigator for TestNav {
3395 fn find_segments_in_group(
3396 &self,
3397 segment_id: &str,
3398 group_path: &[&str],
3399 instance_index: usize,
3400 ) -> Vec<OwnedSegment> {
3401 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
3402 vec![OwnedSegment {
3403 id: "SEQ".into(),
3404 elements: vec![vec!["Z03".into()]],
3405 segment_number: 10,
3406 }]
3407 } else {
3408 vec![]
3409 }
3410 }
3411 fn find_segments_with_qualifier_in_group(
3412 &self,
3413 _: &str,
3414 _: usize,
3415 _: &str,
3416 _: &[&str],
3417 _: usize,
3418 ) -> Vec<OwnedSegment> {
3419 vec![]
3420 }
3421 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3422 match group_path {
3423 ["SG4"] => 1,
3424 ["SG4", "SG8"] => 1,
3425 _ => 0,
3426 }
3427 }
3428 }
3429
3430 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
3431 let validator = EdifactValidator::new(evaluator);
3432 let segments = vec![OwnedSegment {
3433 id: "SEQ".into(),
3434 elements: vec![vec!["Z03".into()]],
3435 segment_number: 10,
3436 }];
3437 let seq = |code: &str, group: &str| AhbFieldRule {
3438 segment_path: "SG4/SG8/SEQ/1229".to_string(),
3439 name: format!("Handlung, Code ({code})"),
3440 ahb_status: "X".to_string(),
3441 codes: vec![AhbCodeRule {
3442 value: code.to_string(),
3443 description: String::new(),
3444 ahb_status: "X".to_string(),
3445 }],
3446 parent_group_ahb_status: Some(group.to_string()),
3447 segment_ahb_status: None,
3448 ..Default::default()
3449 };
3450 let workflow = AhbWorkflow {
3451 pruefidentifikator: "44143".to_string(),
3452 description: "Test".to_string(),
3453 communication_direction: None,
3454 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
3455 ub_definitions: BTreeMap::new(),
3456 };
3457
3458 let report = validator.validate_with_navigator(
3459 &segments,
3460 &workflow,
3461 &NoOpExternalProvider,
3462 ValidationLevel::Conditions,
3463 &TestNav,
3464 );
3465 let missing: Vec<_> = report
3466 .issues
3467 .iter()
3468 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
3469 .collect();
3470 assert!(missing.is_empty(), "{missing:#?}");
3471 }
3472
3473 #[test]
3474 fn test_conditional_group_variant_absent_no_error() {
3475 use mig_types::navigator::GroupNavigator;
3480
3481 struct TestNav;
3482 impl GroupNavigator for TestNav {
3483 fn find_segments_in_group(
3484 &self,
3485 segment_id: &str,
3486 group_path: &[&str],
3487 instance_index: usize,
3488 ) -> Vec<OwnedSegment> {
3489 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
3490 vec![OwnedSegment {
3491 id: "LOC".into(),
3492 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3493 segment_number: 10,
3494 }]
3495 } else {
3496 vec![]
3497 }
3498 }
3499 fn find_segments_with_qualifier_in_group(
3500 &self,
3501 _: &str,
3502 _: usize,
3503 _: &str,
3504 _: &[&str],
3505 _: usize,
3506 ) -> Vec<OwnedSegment> {
3507 vec![]
3508 }
3509 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3510 match group_path {
3511 ["SG4"] => 1,
3512 ["SG4", "SG5"] => 1, _ => 0,
3514 }
3515 }
3516 }
3517
3518 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
3521 let validator = EdifactValidator::new(evaluator);
3522 let external = NoOpExternalProvider;
3523 let nav = TestNav;
3524
3525 let segments = vec![OwnedSegment {
3526 id: "LOC".into(),
3527 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3528 segment_number: 10,
3529 }];
3530
3531 let workflow = AhbWorkflow {
3532 pruefidentifikator: "55001".to_string(),
3533 description: "Test".to_string(),
3534 communication_direction: None,
3535 fields: vec![
3536 AhbFieldRule {
3538 segment_path: "SG4/SG5/LOC/3227".to_string(),
3539 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3540 ahb_status: "X".to_string(),
3541 codes: vec![AhbCodeRule {
3542 value: "Z16".to_string(),
3543 description: "Marktlokation".to_string(),
3544 ahb_status: "X".to_string(),
3545 }],
3546 parent_group_ahb_status: Some("Muss [2061]".to_string()),
3547 segment_ahb_status: None,
3548 ..Default::default()
3549 },
3550 AhbFieldRule {
3552 segment_path: "SG4/SG5/LOC/3227".to_string(),
3553 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3554 ahb_status: "X".to_string(),
3555 codes: vec![AhbCodeRule {
3556 value: "Z17".to_string(),
3557 description: "Messlokation".to_string(),
3558 ahb_status: "X".to_string(),
3559 }],
3560 parent_group_ahb_status: Some("Soll [165]".to_string()),
3561 segment_ahb_status: None,
3562 ..Default::default()
3563 },
3564 ],
3565 ub_definitions: BTreeMap::new(),
3566 };
3567
3568 let report = validator.validate_with_navigator(
3569 &segments,
3570 &workflow,
3571 &external,
3572 ValidationLevel::Conditions,
3573 &nav,
3574 );
3575
3576 let ahb_errors: Vec<_> = report
3577 .by_category(ValidationCategory::Ahb)
3578 .filter(|i| i.severity == Severity::Error)
3579 .collect();
3580
3581 assert!(
3583 ahb_errors.is_empty(),
3584 "Expected no errors when conditional group variant [165]=False, got: {:?}",
3585 ahb_errors
3586 );
3587 }
3588
3589 #[test]
3590 fn test_conditional_group_variant_unknown_no_error() {
3591 let evaluator = MockEvaluator::new(vec![]);
3597 let validator = EdifactValidator::new(evaluator);
3598 let external = NoOpExternalProvider;
3599
3600 let workflow = AhbWorkflow {
3601 pruefidentifikator: "55001".to_string(),
3602 description: "Test".to_string(),
3603 communication_direction: None,
3604 fields: vec![AhbFieldRule {
3605 segment_path: "SG4/SG5/LOC/3227".to_string(),
3606 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3607 ahb_status: "X".to_string(),
3608 codes: vec![AhbCodeRule {
3609 value: "Z17".to_string(),
3610 description: "Messlokation".to_string(),
3611 ahb_status: "X".to_string(),
3612 }],
3613 parent_group_ahb_status: Some("Soll [165]".to_string()),
3614 segment_ahb_status: None,
3615 ..Default::default()
3616 }],
3617 ub_definitions: BTreeMap::new(),
3618 };
3619
3620 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3621
3622 let ahb_errors: Vec<_> = report
3623 .by_category(ValidationCategory::Ahb)
3624 .filter(|i| i.severity == Severity::Error)
3625 .collect();
3626
3627 assert!(
3629 ahb_errors.is_empty(),
3630 "Expected no errors when parent group condition is Unknown, got: {:?}",
3631 ahb_errors
3632 );
3633 }
3634
3635 #[test]
3636 fn test_segment_absent_within_present_group_no_error() {
3637 use mig_types::navigator::GroupNavigator;
3641
3642 struct TestNav;
3643 impl GroupNavigator for TestNav {
3644 fn find_segments_in_group(
3645 &self,
3646 segment_id: &str,
3647 group_path: &[&str],
3648 instance_index: usize,
3649 ) -> Vec<OwnedSegment> {
3650 if segment_id == "QTY"
3652 && group_path == ["SG5", "SG6", "SG9", "SG10"]
3653 && instance_index == 0
3654 {
3655 vec![OwnedSegment {
3656 id: "QTY".into(),
3657 elements: vec![vec!["220".into(), "0".into()]],
3658 segment_number: 14,
3659 }]
3660 } else {
3661 vec![]
3662 }
3663 }
3664 fn find_segments_with_qualifier_in_group(
3665 &self,
3666 _: &str,
3667 _: usize,
3668 _: &str,
3669 _: &[&str],
3670 _: usize,
3671 ) -> Vec<OwnedSegment> {
3672 vec![]
3673 }
3674 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3675 match group_path {
3676 ["SG5"] => 1,
3677 ["SG5", "SG6"] => 1,
3678 ["SG5", "SG6", "SG9"] => 1,
3679 ["SG5", "SG6", "SG9", "SG10"] => 1,
3680 _ => 0,
3681 }
3682 }
3683 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
3684 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
3686 }
3687 }
3688
3689 let evaluator = MockEvaluator::all_true(&[]);
3690 let validator = EdifactValidator::new(evaluator);
3691 let external = NoOpExternalProvider;
3692 let nav = TestNav;
3693
3694 let segments = vec![OwnedSegment {
3695 id: "QTY".into(),
3696 elements: vec![vec!["220".into(), "0".into()]],
3697 segment_number: 14,
3698 }];
3699
3700 let workflow = AhbWorkflow {
3701 pruefidentifikator: "13017".to_string(),
3702 description: "Test".to_string(),
3703 communication_direction: None,
3704 fields: vec![
3705 AhbFieldRule {
3707 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
3708 name: "Statuskategorie, Code".to_string(),
3709 ahb_status: "X".to_string(),
3710 codes: vec![],
3711 parent_group_ahb_status: Some("Muss".to_string()),
3712 segment_ahb_status: None,
3713 ..Default::default()
3714 },
3715 AhbFieldRule {
3717 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3718 name: "Statusanlaß, Code".to_string(),
3719 ahb_status: "X [5]".to_string(),
3720 codes: vec![],
3721 parent_group_ahb_status: Some("Muss".to_string()),
3722 segment_ahb_status: None,
3723 ..Default::default()
3724 },
3725 ],
3726 ub_definitions: BTreeMap::new(),
3727 };
3728
3729 let report = validator.validate_with_navigator(
3730 &segments,
3731 &workflow,
3732 &external,
3733 ValidationLevel::Conditions,
3734 &nav,
3735 );
3736
3737 let ahb_errors: Vec<_> = report
3738 .by_category(ValidationCategory::Ahb)
3739 .filter(|i| i.severity == Severity::Error)
3740 .collect();
3741
3742 assert!(
3743 ahb_errors.is_empty(),
3744 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
3745 ahb_errors
3746 );
3747 }
3748
3749 #[test]
3750 fn test_group_scoped_code_validation_with_navigator() {
3751 use mig_types::navigator::GroupNavigator;
3755
3756 struct TestNav;
3757 impl GroupNavigator for TestNav {
3758 fn find_segments_in_group(
3759 &self,
3760 segment_id: &str,
3761 group_path: &[&str],
3762 _instance_index: usize,
3763 ) -> Vec<OwnedSegment> {
3764 if segment_id != "NAD" {
3765 return vec![];
3766 }
3767 match group_path {
3768 ["SG2"] => vec![
3769 OwnedSegment {
3770 id: "NAD".into(),
3771 elements: vec![vec!["MS".into()]],
3772 segment_number: 3,
3773 },
3774 OwnedSegment {
3775 id: "NAD".into(),
3776 elements: vec![vec!["MT".into()]], segment_number: 4,
3778 },
3779 ],
3780 ["SG4", "SG12"] => vec![
3781 OwnedSegment {
3782 id: "NAD".into(),
3783 elements: vec![vec!["Z04".into()]],
3784 segment_number: 20,
3785 },
3786 OwnedSegment {
3787 id: "NAD".into(),
3788 elements: vec![vec!["Z09".into()]],
3789 segment_number: 21,
3790 },
3791 ],
3792 _ => vec![],
3793 }
3794 }
3795 fn find_segments_with_qualifier_in_group(
3796 &self,
3797 _: &str,
3798 _: usize,
3799 _: &str,
3800 _: &[&str],
3801 _: usize,
3802 ) -> Vec<OwnedSegment> {
3803 vec![]
3804 }
3805 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3806 match group_path {
3807 ["SG2"] | ["SG4", "SG12"] => 1,
3808 _ => 0,
3809 }
3810 }
3811 }
3812
3813 let evaluator = MockEvaluator::new(vec![]);
3814 let validator = EdifactValidator::new(evaluator);
3815 let external = NoOpExternalProvider;
3816 let nav = TestNav;
3817
3818 let workflow = AhbWorkflow {
3819 pruefidentifikator: "55001".to_string(),
3820 description: "Test".to_string(),
3821 communication_direction: None,
3822 fields: vec![
3823 AhbFieldRule {
3824 segment_path: "SG2/NAD/3035".to_string(),
3825 name: "Absender".to_string(),
3826 ahb_status: "X".to_string(),
3827 codes: vec![AhbCodeRule {
3828 value: "MS".to_string(),
3829 description: "Absender".to_string(),
3830 ahb_status: "X".to_string(),
3831 }],
3832 parent_group_ahb_status: None,
3833 segment_ahb_status: None,
3834 ..Default::default()
3835 },
3836 AhbFieldRule {
3837 segment_path: "SG2/NAD/3035".to_string(),
3838 name: "Empfaenger".to_string(),
3839 ahb_status: "X".to_string(),
3840 codes: vec![AhbCodeRule {
3841 value: "MR".to_string(),
3842 description: "Empfaenger".to_string(),
3843 ahb_status: "X".to_string(),
3844 }],
3845 parent_group_ahb_status: None,
3846 segment_ahb_status: None,
3847 ..Default::default()
3848 },
3849 AhbFieldRule {
3850 segment_path: "SG4/SG12/NAD/3035".to_string(),
3851 name: "Anschlussnutzer".to_string(),
3852 ahb_status: "X".to_string(),
3853 codes: vec![AhbCodeRule {
3854 value: "Z04".to_string(),
3855 description: "Anschlussnutzer".to_string(),
3856 ahb_status: "X".to_string(),
3857 }],
3858 parent_group_ahb_status: None,
3859 segment_ahb_status: None,
3860 ..Default::default()
3861 },
3862 AhbFieldRule {
3863 segment_path: "SG4/SG12/NAD/3035".to_string(),
3864 name: "Korrespondenzanschrift".to_string(),
3865 ahb_status: "X".to_string(),
3866 codes: vec![AhbCodeRule {
3867 value: "Z09".to_string(),
3868 description: "Korrespondenzanschrift".to_string(),
3869 ahb_status: "X".to_string(),
3870 }],
3871 parent_group_ahb_status: None,
3872 segment_ahb_status: None,
3873 ..Default::default()
3874 },
3875 ],
3876 ub_definitions: BTreeMap::new(),
3877 };
3878
3879 let all_segments = vec![
3881 OwnedSegment {
3882 id: "NAD".into(),
3883 elements: vec![vec!["MS".into()]],
3884 segment_number: 3,
3885 },
3886 OwnedSegment {
3887 id: "NAD".into(),
3888 elements: vec![vec!["MT".into()]],
3889 segment_number: 4,
3890 },
3891 OwnedSegment {
3892 id: "NAD".into(),
3893 elements: vec![vec!["Z04".into()]],
3894 segment_number: 20,
3895 },
3896 OwnedSegment {
3897 id: "NAD".into(),
3898 elements: vec![vec!["Z09".into()]],
3899 segment_number: 21,
3900 },
3901 ];
3902
3903 let report = validator.validate_with_navigator(
3904 &all_segments,
3905 &workflow,
3906 &external,
3907 ValidationLevel::Conditions,
3908 &nav,
3909 );
3910
3911 let code_errors: Vec<_> = report
3912 .by_category(ValidationCategory::Code)
3913 .filter(|i| i.severity == Severity::Error)
3914 .collect();
3915
3916 assert_eq!(
3919 code_errors.len(),
3920 1,
3921 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
3922 code_errors
3923 );
3924 assert!(narrate(code_errors[0]).contains("MT"));
3925 assert!(narrate(code_errors[0]).contains("MR"));
3927 assert!(narrate(code_errors[0]).contains("MS"));
3928 assert!(
3929 !narrate(code_errors[0]).contains("Z04"),
3930 "SG4/SG12 codes should not leak into SG2 error"
3931 );
3932 assert!(
3934 code_errors[0]
3935 .field_path
3936 .as_deref()
3937 .unwrap_or("")
3938 .contains("SG2"),
3939 "Error field_path should reference SG2, got: {:?}",
3940 code_errors[0].field_path
3941 );
3942 }
3943
3944 #[test]
3947 fn test_package_cardinality_within_bounds() {
3948 let evaluator = MockEvaluator::all_true(&[]);
3950 let validator = EdifactValidator::new(evaluator);
3951 let external = NoOpExternalProvider;
3952
3953 let segments = vec![OwnedSegment {
3954 id: "STS".into(),
3955 elements: vec![
3956 vec!["Z33".into()], vec![], vec!["E01".into()], ],
3960 segment_number: 5,
3961 }];
3962
3963 let workflow = AhbWorkflow {
3964 pruefidentifikator: "13017".to_string(),
3965 description: "Test".to_string(),
3966 communication_direction: None,
3967 ub_definitions: BTreeMap::new(),
3968 fields: vec![AhbFieldRule {
3969 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3970 name: "Statusanlaß, Code".to_string(),
3971 ahb_status: "X".to_string(),
3972 element_index: Some(2),
3973 component_index: Some(0),
3974 codes: vec![
3975 AhbCodeRule {
3976 value: "E01".into(),
3977 description: "Code 1".into(),
3978 ahb_status: "X [4P0..1]".into(),
3979 },
3980 AhbCodeRule {
3981 value: "E02".into(),
3982 description: "Code 2".into(),
3983 ahb_status: "X [4P0..1]".into(),
3984 },
3985 ],
3986 parent_group_ahb_status: Some("Muss".to_string()),
3987 segment_ahb_status: None,
3988 mig_number: None,
3989 }],
3990 };
3991
3992 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3993 let pkg_errors: Vec<_> = report
3994 .by_category(ValidationCategory::Ahb)
3995 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3996 .collect();
3997 assert!(
3998 pkg_errors.is_empty(),
3999 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
4000 pkg_errors
4001 );
4002 }
4003
4004 #[test]
4005 fn test_package_cardinality_zero_present_min_zero() {
4006 let evaluator = MockEvaluator::all_true(&[]);
4008 let validator = EdifactValidator::new(evaluator);
4009 let external = NoOpExternalProvider;
4010
4011 let segments = vec![OwnedSegment {
4012 id: "STS".into(),
4013 elements: vec![
4014 vec!["Z33".into()],
4015 vec![],
4016 vec!["X99".into()], ],
4018 segment_number: 5,
4019 }];
4020
4021 let workflow = AhbWorkflow {
4022 pruefidentifikator: "13017".to_string(),
4023 description: "Test".to_string(),
4024 communication_direction: None,
4025 ub_definitions: BTreeMap::new(),
4026 fields: vec![AhbFieldRule {
4027 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4028 name: "Statusanlaß, Code".to_string(),
4029 ahb_status: "X".to_string(),
4030 element_index: Some(2),
4031 component_index: Some(0),
4032 codes: vec![
4033 AhbCodeRule {
4034 value: "E01".into(),
4035 description: "Code 1".into(),
4036 ahb_status: "X [4P0..1]".into(),
4037 },
4038 AhbCodeRule {
4039 value: "E02".into(),
4040 description: "Code 2".into(),
4041 ahb_status: "X [4P0..1]".into(),
4042 },
4043 ],
4044 parent_group_ahb_status: Some("Muss".to_string()),
4045 segment_ahb_status: None,
4046 mig_number: None,
4047 }],
4048 };
4049
4050 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4051 let pkg_errors: Vec<_> = report
4052 .by_category(ValidationCategory::Ahb)
4053 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4054 .collect();
4055 assert!(
4056 pkg_errors.is_empty(),
4057 "0 codes, min=0 — no error expected, got: {:?}",
4058 pkg_errors
4059 );
4060 }
4061
4062 #[test]
4063 fn test_package_cardinality_too_many() {
4064 let evaluator = MockEvaluator::all_true(&[]);
4066 let validator = EdifactValidator::new(evaluator);
4067 let external = NoOpExternalProvider;
4068
4069 let segments = vec![
4071 OwnedSegment {
4072 id: "STS".into(),
4073 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
4074 segment_number: 5,
4075 },
4076 OwnedSegment {
4077 id: "STS".into(),
4078 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
4079 segment_number: 6,
4080 },
4081 ];
4082
4083 let workflow = AhbWorkflow {
4084 pruefidentifikator: "13017".to_string(),
4085 description: "Test".to_string(),
4086 communication_direction: None,
4087 ub_definitions: BTreeMap::new(),
4088 fields: vec![AhbFieldRule {
4089 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4090 name: "Statusanlaß, Code".to_string(),
4091 ahb_status: "X".to_string(),
4092 element_index: Some(2),
4093 component_index: Some(0),
4094 codes: vec![
4095 AhbCodeRule {
4096 value: "E01".into(),
4097 description: "Code 1".into(),
4098 ahb_status: "X [4P0..1]".into(),
4099 },
4100 AhbCodeRule {
4101 value: "E02".into(),
4102 description: "Code 2".into(),
4103 ahb_status: "X [4P0..1]".into(),
4104 },
4105 ],
4106 parent_group_ahb_status: Some("Muss".to_string()),
4107 segment_ahb_status: None,
4108 mig_number: None,
4109 }],
4110 };
4111
4112 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4113 let pkg_errors: Vec<_> = report
4114 .by_category(ValidationCategory::Ahb)
4115 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4116 .collect();
4117 assert_eq!(
4118 pkg_errors.len(),
4119 1,
4120 "2 codes present, max=1 — expected 1 error, got: {:?}",
4121 pkg_errors
4122 );
4123 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
4124 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
4125 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
4126 }
4127
4128 #[test]
4129 fn test_package_cardinality_too_few() {
4130 let evaluator = MockEvaluator::all_true(&[]);
4132 let validator = EdifactValidator::new(evaluator);
4133 let external = NoOpExternalProvider;
4134
4135 let segments = vec![OwnedSegment {
4136 id: "STS".into(),
4137 elements: vec![
4138 vec!["Z33".into()],
4139 vec![],
4140 vec!["X99".into()], ],
4142 segment_number: 5,
4143 }];
4144
4145 let workflow = AhbWorkflow {
4146 pruefidentifikator: "13017".to_string(),
4147 description: "Test".to_string(),
4148 communication_direction: None,
4149 ub_definitions: BTreeMap::new(),
4150 fields: vec![AhbFieldRule {
4151 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4152 name: "Statusanlaß, Code".to_string(),
4153 ahb_status: "X".to_string(),
4154 element_index: Some(2),
4155 component_index: Some(0),
4156 codes: vec![
4157 AhbCodeRule {
4158 value: "E01".into(),
4159 description: "Code 1".into(),
4160 ahb_status: "X [5P1..3]".into(),
4161 },
4162 AhbCodeRule {
4163 value: "E02".into(),
4164 description: "Code 2".into(),
4165 ahb_status: "X [5P1..3]".into(),
4166 },
4167 AhbCodeRule {
4168 value: "E03".into(),
4169 description: "Code 3".into(),
4170 ahb_status: "X [5P1..3]".into(),
4171 },
4172 ],
4173 parent_group_ahb_status: Some("Muss".to_string()),
4174 segment_ahb_status: None,
4175 mig_number: None,
4176 }],
4177 };
4178
4179 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4180 let pkg_errors: Vec<_> = report
4181 .by_category(ValidationCategory::Ahb)
4182 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4183 .collect();
4184 assert_eq!(
4185 pkg_errors.len(),
4186 1,
4187 "0 codes present, min=1 — expected 1 error, got: {:?}",
4188 pkg_errors
4189 );
4190 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
4191 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
4192 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
4193 }
4194
4195 #[test]
4196 fn test_package_cardinality_no_packages_in_workflow() {
4197 let evaluator = MockEvaluator::all_true(&[]);
4199 let validator = EdifactValidator::new(evaluator);
4200 let external = NoOpExternalProvider;
4201
4202 let segments = vec![OwnedSegment {
4203 id: "STS".into(),
4204 elements: vec![vec!["E01".into()]],
4205 segment_number: 5,
4206 }];
4207
4208 let workflow = AhbWorkflow {
4209 pruefidentifikator: "13017".to_string(),
4210 description: "Test".to_string(),
4211 communication_direction: None,
4212 ub_definitions: BTreeMap::new(),
4213 fields: vec![AhbFieldRule {
4214 segment_path: "STS/9015".to_string(),
4215 name: "Status Code".to_string(),
4216 ahb_status: "X".to_string(),
4217 codes: vec![AhbCodeRule {
4218 value: "E01".into(),
4219 description: "Code 1".into(),
4220 ahb_status: "X".into(),
4221 }],
4222 parent_group_ahb_status: Some("Muss".to_string()),
4223 segment_ahb_status: None,
4224 ..Default::default()
4225 }],
4226 };
4227
4228 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4229 let pkg_errors: Vec<_> = report
4230 .by_category(ValidationCategory::Ahb)
4231 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4232 .collect();
4233 assert!(
4234 pkg_errors.is_empty(),
4235 "No packages in workflow — no errors expected"
4236 );
4237 }
4238
4239 #[test]
4240 fn test_package_cardinality_with_condition_and_package() {
4241 let evaluator = MockEvaluator::all_true(&[901]);
4243 let validator = EdifactValidator::new(evaluator);
4244 let external = NoOpExternalProvider;
4245
4246 let segments = vec![OwnedSegment {
4247 id: "STS".into(),
4248 elements: vec![vec![], vec![], vec!["E01".into()]],
4249 segment_number: 5,
4250 }];
4251
4252 let workflow = AhbWorkflow {
4253 pruefidentifikator: "13017".to_string(),
4254 description: "Test".to_string(),
4255 communication_direction: None,
4256 ub_definitions: BTreeMap::new(),
4257 fields: vec![AhbFieldRule {
4258 segment_path: "SG10/STS/C556/9013".to_string(),
4259 name: "Code".to_string(),
4260 ahb_status: "X".to_string(),
4261 element_index: Some(2),
4262 component_index: Some(0),
4263 codes: vec![
4264 AhbCodeRule {
4265 value: "E01".into(),
4266 description: "Code 1".into(),
4267 ahb_status: "X [901] [4P0..1]".into(),
4268 },
4269 AhbCodeRule {
4270 value: "E02".into(),
4271 description: "Code 2".into(),
4272 ahb_status: "X [901] [4P0..1]".into(),
4273 },
4274 ],
4275 parent_group_ahb_status: Some("Muss".to_string()),
4276 segment_ahb_status: None,
4277 mig_number: None,
4278 }],
4279 };
4280
4281 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4282 let pkg_errors: Vec<_> = report
4283 .by_category(ValidationCategory::Ahb)
4284 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4285 .collect();
4286 assert!(
4287 pkg_errors.is_empty(),
4288 "1 code within [4P0..1] bounds — no error, got: {:?}",
4289 pkg_errors
4290 );
4291 }
4292
4293 #[test]
4294 fn test_package_cardinality_scoped_per_group_instance() {
4295 use mig_types::navigator::GroupNavigator;
4303
4304 struct TwoSg10s {
4305 sts_a: OwnedSegment,
4306 sts_b: OwnedSegment,
4307 }
4308 impl GroupNavigator for TwoSg10s {
4309 fn find_segments_in_group(
4310 &self,
4311 segment_id: &str,
4312 group_path: &[&str],
4313 instance_index: usize,
4314 ) -> Vec<OwnedSegment> {
4315 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
4316 match instance_index {
4317 0 => vec![self.sts_a.clone()],
4318 1 => vec![self.sts_b.clone()],
4319 _ => vec![],
4320 }
4321 } else {
4322 vec![]
4323 }
4324 }
4325 fn find_segments_with_qualifier_in_group(
4326 &self,
4327 _: &str,
4328 _: usize,
4329 _: &str,
4330 _: &[&str],
4331 _: usize,
4332 ) -> Vec<OwnedSegment> {
4333 vec![]
4334 }
4335 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4336 match group_path {
4337 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
4338 ["SG5", "SG6", "SG9", "SG10"] => 2,
4339 _ => 0,
4340 }
4341 }
4342 }
4343
4344 let sts_a = OwnedSegment {
4345 id: "STS".into(),
4346 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4347 segment_number: 10,
4348 };
4349 let sts_b = OwnedSegment {
4350 id: "STS".into(),
4351 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4352 segment_number: 15,
4353 };
4354 let nav = TwoSg10s {
4355 sts_a: sts_a.clone(),
4356 sts_b: sts_b.clone(),
4357 };
4358
4359 let evaluator = MockEvaluator::all_true(&[]);
4360 let validator = EdifactValidator::new(evaluator);
4361 let external = NoOpExternalProvider;
4362
4363 let workflow = AhbWorkflow {
4364 pruefidentifikator: "13025".to_string(),
4365 description: "Test".to_string(),
4366 communication_direction: None,
4367 ub_definitions: BTreeMap::new(),
4368 fields: vec![AhbFieldRule {
4369 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4370 name: "Statusanlaß, Code".to_string(),
4371 ahb_status: "X".to_string(),
4372 element_index: Some(2),
4373 component_index: Some(0),
4374 codes: vec![
4375 AhbCodeRule {
4376 value: "E01".into(),
4377 description: "Code 1".into(),
4378 ahb_status: "X [4P0..1]".into(),
4379 },
4380 AhbCodeRule {
4381 value: "E02".into(),
4382 description: "Code 2".into(),
4383 ahb_status: "X [4P0..1]".into(),
4384 },
4385 ],
4386 parent_group_ahb_status: Some("Muss".to_string()),
4387 segment_ahb_status: None,
4388 mig_number: None,
4389 }],
4390 };
4391
4392 let report = validator.validate_with_navigator(
4393 &[sts_a, sts_b],
4394 &workflow,
4395 &external,
4396 ValidationLevel::Full,
4397 &nav,
4398 );
4399 let pkg_errors: Vec<_> = report
4400 .by_category(ValidationCategory::Ahb)
4401 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4402 .collect();
4403 assert!(
4404 pkg_errors.is_empty(),
4405 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
4406 pkg_errors
4407 );
4408 }
4409
4410 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
4411 OwnedSegment {
4412 id: id.to_string(),
4413 elements: elements
4414 .into_iter()
4415 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
4416 .collect(),
4417 segment_number: 0,
4418 }
4419 }
4420
4421 #[test]
4432 fn test_package_cardinality_scoped_to_rule_mig_variant() {
4433 use mig_types::navigator::GroupNavigator;
4434
4435 struct TwoSg8Variants {
4436 seq_z01: OwnedSegment,
4437 seq_z45: OwnedSegment,
4438 }
4439 impl GroupNavigator for TwoSg8Variants {
4440 fn find_segments_in_group(
4441 &self,
4442 segment_id: &str,
4443 group_path: &[&str],
4444 instance_index: usize,
4445 ) -> Vec<OwnedSegment> {
4446 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
4447 match instance_index {
4448 0 => vec![self.seq_z01.clone()],
4449 1 => vec![self.seq_z45.clone()],
4450 _ => vec![],
4451 }
4452 } else {
4453 vec![]
4454 }
4455 }
4456 fn find_segments_with_qualifier_in_group(
4457 &self,
4458 _: &str,
4459 _: usize,
4460 _: &str,
4461 _: &[&str],
4462 _: usize,
4463 ) -> Vec<OwnedSegment> {
4464 vec![]
4465 }
4466 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4467 match group_path {
4468 ["SG4"] => 1,
4469 ["SG4", "SG8"] => 2,
4470 _ => 0,
4471 }
4472 }
4473 fn instance_has_mig_number(
4474 &self,
4475 group_path: &[&str],
4476 instance_index: usize,
4477 mig_number: &str,
4478 ) -> bool {
4479 if group_path != ["SG4", "SG8"] {
4480 return true;
4481 }
4482 match (instance_index, mig_number) {
4483 (0, "00115") => true,
4484 (0, _) => false,
4485 (1, "00171") => true,
4486 (1, _) => false,
4487 _ => false,
4488 }
4489 }
4490 }
4491
4492 let seq_z01 = OwnedSegment {
4493 id: "SEQ".into(),
4494 elements: vec![vec!["Z01".into()], vec!["1".into()]],
4495 segment_number: 10,
4496 };
4497 let seq_z45 = OwnedSegment {
4498 id: "SEQ".into(),
4499 elements: vec![vec!["Z45".into()], vec!["1".into()]],
4500 segment_number: 20,
4501 };
4502 let nav = TwoSg8Variants {
4503 seq_z01: seq_z01.clone(),
4504 seq_z45: seq_z45.clone(),
4505 };
4506
4507 let evaluator = MockEvaluator::all_true(&[]);
4508 let validator = EdifactValidator::new(evaluator);
4509 let external = NoOpExternalProvider;
4510
4511 let workflow = AhbWorkflow {
4512 pruefidentifikator: "55218".to_string(),
4513 description: "Test".to_string(),
4514 communication_direction: None,
4515 ub_definitions: BTreeMap::new(),
4516 fields: vec![AhbFieldRule {
4517 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4518 name: "Handlung, Code".to_string(),
4519 ahb_status: "X".to_string(),
4520 element_index: Some(0),
4521 component_index: Some(0),
4522 codes: vec![
4523 AhbCodeRule {
4524 value: "Z45".into(),
4525 description: "NNA".into(),
4526 ahb_status: "X [1P1..4294967295]".into(),
4527 },
4528 AhbCodeRule {
4529 value: "Z84".into(),
4530 description: "Differenz-NNA".into(),
4531 ahb_status: "X [1P0..4294967295]".into(),
4532 },
4533 ],
4534 parent_group_ahb_status: Some("Muss".to_string()),
4535 segment_ahb_status: None,
4536 mig_number: Some("00171".to_string()),
4537 }],
4538 };
4539
4540 let report = validator.validate_with_navigator(
4541 &[seq_z01, seq_z45],
4542 &workflow,
4543 &external,
4544 ValidationLevel::Full,
4545 &nav,
4546 );
4547 let pkg_errors: Vec<_> = report
4548 .by_category(ValidationCategory::Ahb)
4549 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4550 .collect();
4551 assert!(
4552 pkg_errors.is_empty(),
4553 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
4554 pkg_errors
4555 );
4556 }
4557
4558 #[test]
4559 fn test_unt_count_correct() {
4560 let segments = vec![
4562 make_segment("UNH", vec![vec!["001"]]),
4563 make_segment("BGM", vec![vec!["E01"]]),
4564 make_segment("DTM", vec![vec!["137", "20250401"]]),
4565 make_segment("UNT", vec![vec!["4", "001"]]),
4566 ];
4567 assert!(
4568 validate_unt_segment_count(&segments).is_none(),
4569 "Correct count should produce no issue"
4570 );
4571 }
4572
4573 #[test]
4574 fn test_unt_count_mismatch() {
4575 let segments = vec![
4577 make_segment("UNH", vec![vec!["001"]]),
4578 make_segment("BGM", vec![vec!["E01"]]),
4579 make_segment("UNT", vec![vec!["5", "001"]]),
4580 ];
4581 let issue =
4582 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
4583 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
4584 assert_eq!(issue.severity, Severity::Error);
4585 assert!(narrate(&issue).contains("declared 5"));
4586 assert!(narrate(&issue).contains("actual 3"));
4587 }
4588
4589 #[test]
4590 fn test_unt_count_excludes_envelope() {
4591 let segments = vec![
4593 make_segment("UNA", vec![]),
4594 make_segment("UNB", vec![vec!["UNOC", "3"]]),
4595 make_segment("UNH", vec![vec!["001"]]),
4596 make_segment("BGM", vec![vec!["E01"]]),
4597 make_segment("UNT", vec![vec!["3", "001"]]),
4598 make_segment("UNZ", vec![vec!["1"]]),
4599 ];
4600 assert!(
4601 validate_unt_segment_count(&segments).is_none(),
4602 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
4603 );
4604 }
4605
4606 #[test]
4607 fn test_unt_count_no_unt_returns_none() {
4608 let segments = vec![
4609 make_segment("UNH", vec![vec!["001"]]),
4610 make_segment("BGM", vec![vec!["E01"]]),
4611 ];
4612 assert!(
4613 validate_unt_segment_count(&segments).is_none(),
4614 "No UNT segment should return None (not our problem)"
4615 );
4616 }
4617
4618 #[test]
4619 fn test_unt_count_rejects_multi_message_input() {
4620 let segments = vec![
4622 make_segment("UNH", vec![vec!["001"]]),
4623 make_segment("BGM", vec![vec!["E01"]]),
4624 make_segment("UNT", vec![vec!["3", "001"]]),
4625 make_segment("UNH", vec![vec!["002"]]),
4626 make_segment("BGM", vec![vec!["E02"]]),
4627 make_segment("UNT", vec![vec!["3", "002"]]),
4628 ];
4629 let issue = validate_unt_segment_count(&segments)
4630 .expect("Multi-message input should produce an error");
4631 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
4632 assert!(
4633 narrate(&issue).contains("2 UNH"),
4634 "Should mention UNH count: {}",
4635 narrate(&issue)
4636 );
4637 }
4638
4639 #[test]
4640 fn test_code_validation_accepts_multi_code_variant_qualifier() {
4641 let evaluator = MockEvaluator::new(vec![]);
4648 let validator = EdifactValidator::new(evaluator);
4649 let external = NoOpExternalProvider;
4650
4651 let rff_z39 = OwnedSegment {
4652 id: "RFF".to_string(),
4653 elements: vec![
4654 vec!["RFF".to_string()],
4655 vec!["Z39".to_string(), "REF1".to_string()],
4656 ],
4657 segment_number: 1,
4658 };
4659
4660 let workflow = AhbWorkflow {
4661 pruefidentifikator: "55035".to_string(),
4662 description: "Test".to_string(),
4663 communication_direction: None,
4664 fields: vec![
4665 AhbFieldRule {
4667 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
4668 name: "Referenznummer Qualifier".to_string(),
4669 ahb_status: "Muss".to_string(),
4670 codes: vec![
4671 AhbCodeRule {
4672 value: "Z31".to_string(),
4673 description: "".to_string(),
4674 ahb_status: "X".to_string(),
4675 },
4676 AhbCodeRule {
4677 value: "Z39".to_string(),
4678 description: "".to_string(),
4679 ahb_status: "X".to_string(),
4680 },
4681 ],
4682 parent_group_ahb_status: None,
4683 segment_ahb_status: None,
4684 element_index: Some(1),
4685 component_index: Some(0),
4686 mig_number: Some("00075".to_string()),
4687 },
4688 AhbFieldRule {
4690 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
4691 name: "Referenznummer Qualifier".to_string(),
4692 ahb_status: "Muss".to_string(),
4693 codes: vec![AhbCodeRule {
4694 value: "Z33".to_string(),
4695 description: "".to_string(),
4696 ahb_status: "X".to_string(),
4697 }],
4698 parent_group_ahb_status: None,
4699 segment_ahb_status: None,
4700 element_index: Some(1),
4701 component_index: Some(0),
4702 mig_number: Some("00078".to_string()),
4703 },
4704 ],
4705 ub_definitions: BTreeMap::new(),
4706 };
4707
4708 let report = validator.validate(
4709 &[rff_z39],
4710 &workflow,
4711 &external,
4712 ValidationLevel::Conditions,
4713 );
4714
4715 let code_errors: Vec<_> = report
4716 .by_category(ValidationCategory::Code)
4717 .filter(|i| {
4718 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4719 })
4720 .collect();
4721 assert!(
4722 code_errors.is_empty(),
4723 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
4724 code_errors
4725 );
4726 }
4727
4728 #[test]
4729 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
4730 let evaluator = MockEvaluator::new(vec![]);
4736 let validator = EdifactValidator::new(evaluator);
4737 let external = NoOpExternalProvider;
4738
4739 let pia_5_z12 = OwnedSegment {
4743 id: "PIA".to_string(),
4744 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
4745 segment_number: 1,
4746 };
4747 let pia_5_srw = OwnedSegment {
4748 id: "PIA".to_string(),
4749 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
4750 segment_number: 2,
4751 };
4752
4753 let make_rules = |mig: &str, composite_code: &str| {
4754 vec![
4755 AhbFieldRule {
4756 segment_path: "SG4/SG8/PIA/4347".to_string(),
4757 name: "Produkt-ID-Funktion".to_string(),
4758 ahb_status: "Muss".to_string(),
4759 codes: vec![AhbCodeRule {
4760 value: "5".to_string(),
4761 description: "".to_string(),
4762 ahb_status: "X".to_string(),
4763 }],
4764 parent_group_ahb_status: None,
4765 segment_ahb_status: None,
4766 element_index: Some(0),
4767 component_index: Some(0),
4768 mig_number: Some(mig.to_string()),
4769 },
4770 AhbFieldRule {
4771 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
4772 name: "Artikel/Dienstleistung-Identifikator".to_string(),
4773 ahb_status: "Muss".to_string(),
4774 codes: vec![AhbCodeRule {
4775 value: composite_code.to_string(),
4776 description: "".to_string(),
4777 ahb_status: "X".to_string(),
4778 }],
4779 parent_group_ahb_status: None,
4780 segment_ahb_status: None,
4781 element_index: Some(1),
4782 component_index: Some(0),
4783 mig_number: Some(mig.to_string()),
4784 },
4785 ]
4786 };
4787
4788 let mut fields = make_rules("00108", "Z12");
4789 fields.extend(make_rules("00197", "SRW"));
4790
4791 let workflow = AhbWorkflow {
4792 pruefidentifikator: "55035".to_string(),
4793 description: "Test".to_string(),
4794 communication_direction: None,
4795 fields,
4796 ub_definitions: BTreeMap::new(),
4797 };
4798
4799 let report = validator.validate(
4800 &[pia_5_z12, pia_5_srw],
4801 &workflow,
4802 &external,
4803 ValidationLevel::Conditions,
4804 );
4805
4806 let code_errors: Vec<_> = report
4807 .by_category(ValidationCategory::Code)
4808 .filter(|i| {
4809 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4810 })
4811 .collect();
4812 assert!(
4813 code_errors.is_empty(),
4814 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
4815 code_errors
4816 );
4817 }
4818}