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