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