1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
4
5use crate::expr::{ConditionExpr, ConditionParser, StatusKind};
6
7use crate::eval::answer_lists::{answer_path, is_answer_list_id};
8use crate::eval::{
9 AnswerCodeLists, ConditionEvaluator, ConditionExprEvaluator, ConditionResult,
10 EvaluationContext, ExternalConditionProvider, GroupScope, StatusResolution,
11};
12use mig_types::navigator::GroupNavigator;
13use mig_types::segment::OwnedSegment;
14
15use super::tree::{AhbGroupNode, AhbNode, ValidatedTree};
16
17use super::issue::{Severity, ValidationIssue};
18use super::level::ValidationLevel;
19use super::report::ValidationReport;
20use crate::{IssueKind, UnresolvedConditions};
21
22pub use ahb_types::{AhbCodeRule, AhbFieldRule, AhbWorkflow};
28
29pub struct EdifactValidator<E: ConditionEvaluator> {
62 evaluator: E,
63 report_segments: bool,
64}
65
66impl<E: ConditionEvaluator> EdifactValidator<E> {
67 pub fn new(evaluator: E) -> Self {
69 Self {
70 evaluator,
71 report_segments: false,
72 }
73 }
74
75 pub fn reporting_segments(mut self) -> Self {
81 self.report_segments = true;
82 self
83 }
84
85 pub fn validate(
98 &self,
99 segments: &[OwnedSegment],
100 workflow: &AhbWorkflow,
101 external: &dyn ExternalConditionProvider,
102 level: ValidationLevel,
103 ) -> ValidationReport {
104 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
105 .with_format_version(self.evaluator.format_version())
106 .with_pruefidentifikator(&workflow.pruefidentifikator);
107
108 let ctx = EvaluationContext::new(&workflow.pruefidentifikator, external, segments);
109
110 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
111 self.validate_conditions(workflow, &ctx, &mut report);
112 }
113
114 report
115 }
116
117 pub fn validate_with_navigator(
123 &self,
124 segments: &[OwnedSegment],
125 workflow: &AhbWorkflow,
126 external: &dyn ExternalConditionProvider,
127 level: ValidationLevel,
128 navigator: &dyn GroupNavigator,
129 ) -> ValidationReport {
130 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
131 .with_format_version(self.evaluator.format_version())
132 .with_pruefidentifikator(&workflow.pruefidentifikator);
133
134 let ctx = EvaluationContext::with_navigator(
135 &workflow.pruefidentifikator,
136 external,
137 segments,
138 navigator,
139 );
140
141 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
142 self.validate_conditions(workflow, &ctx, &mut report);
143 }
144
145 report
146 }
147
148 pub fn validate_tree(
155 &self,
156 validated_tree: &ValidatedTree,
157 segments: &[OwnedSegment],
158 external: &dyn ExternalConditionProvider,
159 level: ValidationLevel,
160 navigator: Option<&dyn GroupNavigator>,
161 ) -> ValidationReport {
162 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
163 .with_format_version(self.evaluator.format_version())
164 .with_pruefidentifikator(validated_tree.pruefidentifikator);
165
166 if !matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
167 return report;
168 }
169
170 let ctx = match navigator {
171 Some(nav) => EvaluationContext::with_navigator(
172 validated_tree.pruefidentifikator,
173 external,
174 segments,
175 nav,
176 ),
177 None => EvaluationContext::new(validated_tree.pruefidentifikator, external, segments),
178 };
179
180 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
181
182 let mut all_nodes: Vec<&AhbNode> = Vec::new();
185 all_nodes.extend(validated_tree.root_fields.iter());
186 for group in &validated_tree.groups {
187 collect_nodes_depth_first(group, &mut all_nodes);
188 }
189
190 let mut tag_migs: HashMap<String, HashSet<&str>> = HashMap::new();
197 for node in &all_nodes {
198 if let Some(ref m) = node.rule.mig_number {
199 tag_migs
200 .entry(extract_segment_id(&node.rule.segment_path))
201 .or_default()
202 .insert(m.as_str());
203 }
204 }
205 for rule in &validated_tree.unmatched_rules {
206 if let Some(ref m) = rule.mig_number {
207 tag_migs
208 .entry(extract_segment_id(&rule.segment_path))
209 .or_default()
210 .insert(m.as_str());
211 }
212 }
213
214 for node in &validated_tree.root_fields {
216 evaluate_node(
217 node,
218 &ctx,
219 &expr_eval,
220 &self.evaluator,
221 validated_tree.ub_definitions,
222 &tag_migs,
223 None,
224 &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 max_sum: u32,
475 bounds: BTreeSet<(u32, u32)>,
476 code_values: Vec<String>,
477 element_index: usize,
478 component_index: usize,
479 }
480
481 let mut groups: HashMap<(String, Option<String>, u32, u32, u32), PackageGroup> =
490 HashMap::new();
491 let mut alternatives: HashMap<(String, Option<String>, u32), BTreeSet<u32>> =
495 HashMap::new();
496
497 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
498
499 for field in &workflow.fields {
500 let el_idx = field.element_index.unwrap_or(0);
501 let comp_idx = field.component_index.unwrap_or(0);
502
503 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
505 continue;
506 }
507
508 for code in &field.codes {
509 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
511 let mut packages = Vec::new();
513 collect_packages(&expr, &mut packages);
514
515 if packages.len() > 1 {
516 let ids: BTreeSet<u32> = packages.iter().map(|(id, _, _)| *id).collect();
517 for id in &ids {
518 alternatives
519 .entry((field.segment_path.clone(), field.mig_number.clone(), *id))
520 .or_default()
521 .extend(ids.iter().copied().filter(|other| other != id));
522 }
523 }
524
525 for (pkg_id, pkg_min, pkg_max) in packages {
526 let key = (
527 field.segment_path.clone(),
528 field.mig_number.clone(),
529 pkg_id,
530 0,
531 0,
532 );
533 let group = groups.entry(key).or_insert_with(|| PackageGroup {
534 min: pkg_min,
535 max: pkg_max,
536 max_sum: 0,
537 bounds: BTreeSet::new(),
538 code_values: Vec::new(),
539 element_index: el_idx,
540 component_index: comp_idx,
541 });
542 group.bounds.insert((pkg_min, pkg_max));
549 group.min = group.min.min(pkg_min);
550 if !group.code_values.contains(&code.value) {
551 group.max_sum = group.max_sum.saturating_add(pkg_max);
552 }
553 group.max = group.max.max(pkg_max);
554 group.code_values.push(code.value.clone());
555 }
556 }
557 }
558 }
559
560 let mut field_packages: HashMap<(String, Option<String>), BTreeSet<u32>> = HashMap::new();
571 for (seg_path, mig_number, pkg_id, _, _) in groups.keys() {
572 field_packages
573 .entry((seg_path.clone(), mig_number.clone()))
574 .or_default()
575 .insert(*pkg_id);
576 }
577 let with_alternatives: Vec<(String, Option<String>)> = alternatives
578 .keys()
579 .map(|(p, m, _)| (p.clone(), m.clone()))
580 .collect();
581 for field in with_alternatives {
582 let Some(ids) = field_packages.get(&field) else {
583 continue;
584 };
585 for id in ids {
586 alternatives
587 .entry((field.0.clone(), field.1.clone(), *id))
588 .or_default()
589 .extend(ids.iter().copied().filter(|other| other != id));
590 }
591 }
592
593 type Counted = (Vec<usize>, usize, usize, Vec<String>);
595 let mut counted: HashMap<(String, Option<String>, u32, u32, u32), Counted> = HashMap::new();
596 for ((seg_path, mig_number, pkg_id, _, _), group) in &groups {
597 let segment_id = extract_segment_id(seg_path);
598
599 let mut unique_codes: Vec<&str> =
603 group.code_values.iter().map(|s| s.as_str()).collect();
604 unique_codes.sort_unstable();
605 unique_codes.dedup();
606 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
607
608 let group_path_str = extract_group_path_key(seg_path);
609 let group_path: Vec<&str> = if group_path_str.is_empty() {
610 Vec::new()
611 } else {
612 group_path_str.split('/').collect()
613 };
614
615 let min = group.min as usize;
616 let max = if group.bounds.len() > 1 {
617 group.max_sum as usize
618 } else {
619 group.max as usize
620 };
621
622 let per_instance_counts: Option<Vec<usize>> =
623 match (ctx.navigator, group_path.is_empty()) {
624 (Some(nav), false)
632 if group_path.len() > 1
633 && !full_instance_paths(nav, &group_path).is_empty() =>
634 {
635 let paths = full_instance_paths(nav, &group_path);
636 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
644 .as_deref()
645 {
646 Some(m) => (0..group_path.len())
647 .filter_map(|level| {
648 let prefix = group_path[..=level].join("/");
649 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
650 })
651 .collect(),
652 None => Vec::new(),
653 };
654 if paths.is_empty() {
655 None
656 } else {
657 Some(
658 paths
659 .iter()
660 .filter(|path| match mig_number.as_deref() {
661 Some(m) => nav.instance_at_has_mig_number(path, m),
662 None => true,
663 })
664 .filter(|path| {
665 variant_entries.iter().all(|(level, entry)| {
666 nav.find_segments_in_instance(
667 &entry.tag,
668 &path[..=*level],
669 )
670 .first()
671 .and_then(|seg| {
672 seg.elements
673 .get(entry.element)
674 .and_then(|e| e.get(entry.component))
675 .cloned()
676 })
677 .map_or(true, |v| entry.codes.contains(v.as_str()))
678 })
679 })
680 .map(|path| {
681 nav.find_segments_in_instance(&segment_id, path)
682 .iter()
683 .filter_map(|seg| {
684 seg.elements
685 .get(group.element_index)
686 .and_then(|e| e.get(group.component_index))
687 .filter(|v| !v.is_empty())
688 .cloned()
689 })
690 .filter(|v| code_set.contains(v.as_str()))
691 .count()
692 })
693 .collect(),
694 )
695 }
696 }
697 (Some(nav), false) => {
698 let instance_count = nav.group_instance_count(&group_path);
699 if instance_count == 0 {
700 None
701 } else {
702 Some(
710 (0..instance_count)
711 .filter(|i| match mig_number.as_deref() {
712 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
713 None => true,
714 })
715 .map(|i| {
716 nav.find_segments_in_group(&segment_id, &group_path, i)
717 .iter()
718 .filter_map(|seg| {
719 seg.elements
720 .get(group.element_index)
721 .and_then(|e| e.get(group.component_index))
722 .filter(|v| !v.is_empty())
723 .cloned()
724 })
725 .filter(|v| code_set.contains(v.as_str()))
726 .count()
727 })
728 .collect(),
729 )
730 }
731 }
732 _ => None,
733 };
734
735 let counts: Vec<usize> = per_instance_counts.unwrap_or_else(|| {
736 let segments = ctx.find_segments(&segment_id);
737 let count = segments
738 .iter()
739 .filter_map(|seg| {
740 seg.elements
741 .get(group.element_index)
742 .and_then(|e| e.get(group.component_index))
743 .filter(|v| !v.is_empty())
744 .map(|s| s.as_str())
745 })
746 .filter(|v| code_set.contains(v))
747 .count();
748 vec![count]
749 });
750
751 counted.insert(
752 (
753 seg_path.clone(),
754 mig_number.clone(),
755 *pkg_id,
756 group.min,
757 group.max,
758 ),
759 (
760 counts,
761 min,
762 max,
763 unique_codes.iter().map(|s| s.to_string()).collect(),
764 ),
765 );
766 }
767
768 let mut keys: Vec<&(String, Option<String>, u32, u32, u32)> = counted.keys().collect();
773 keys.sort();
774 for key in keys {
775 let (counts, min, max, codes) = &counted[key];
776 let (seg_path, mig_number, pkg_id, _, _) = key;
777 let alternative_holds = |i: usize| {
778 alternatives
779 .get(&(seg_path.clone(), mig_number.clone(), *pkg_id))
780 .is_some_and(|alts| {
781 counted.iter().any(|((p, m, id, _, _), (c, lo, hi, _))| {
782 p == seg_path
783 && m == mig_number
784 && alts.contains(id)
785 && c.len() == counts.len()
786 && (*lo..=*hi).contains(&c[i])
787 && c[i] > 0
788 })
789 })
790 };
791 let mut reported_counts: HashSet<usize> = HashSet::new();
792 for (i, &present_count) in counts.iter().enumerate() {
793 if (present_count < *min || present_count > *max)
794 && !alternative_holds(i)
795 && reported_counts.insert(present_count)
796 {
797 report.add_issue(
798 ValidationIssue::new(
799 Severity::Error,
800 IssueKind::PackageCardinality {
801 package_id: pkg_id.to_string(),
802 present: present_count,
803 min: *min,
804 max: *max,
805 codes: codes.clone(),
806 },
807 )
808 .with_field_path(seg_path)
809 .with_expected(format!("{min}..{max}"))
810 .with_actual(present_count.to_string()),
811 );
812 }
813 }
814 }
815 }
816
817 fn validate_codes_cross_field(
829 &self,
830 workflow: &AhbWorkflow,
831 ctx: &EvaluationContext,
832 report: &mut ValidationReport,
833 ) {
834 if ctx.navigator.is_some() {
835 self.validate_codes_group_scoped(workflow, ctx, report);
836 } else {
837 self.validate_codes_tag_scoped(workflow, ctx, report);
838 }
839 }
840
841 fn validate_codes_group_scoped(
844 &self,
845 workflow: &AhbWorkflow,
846 ctx: &EvaluationContext,
847 report: &mut ValidationReport,
848 ) {
849 let by_loc = partition_codes_by_mig(workflow);
850 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
851 let nav = ctx.navigator.unwrap();
852
853 for ((group_key, tag), migs) in &by_loc {
854 let field_path = if group_key.is_empty() {
855 format!("{tag}/qualifier")
856 } else {
857 format!("{group_key}/{tag}/qualifier")
858 };
859
860 let group_path: Vec<&str> = if group_key.is_empty() {
861 Vec::new()
862 } else {
863 group_key.split('/').collect()
864 };
865
866 let tag_qualifiers = known_qualifiers.get(tag);
867
868 if group_path.is_empty() {
869 Self::validate_segments_per_mig(
870 &ctx.find_segments(tag),
871 migs,
872 tag_qualifiers,
873 tag,
874 &field_path,
875 report,
876 );
877 } else {
878 let instance_count = nav.group_instance_count(&group_path);
879 for i in 0..instance_count {
880 let owned = nav.find_segments_in_group(tag, &group_path, i);
881 let refs: Vec<&OwnedSegment> = owned.iter().collect();
882 Self::validate_segments_per_mig(
883 &refs,
884 migs,
885 tag_qualifiers,
886 tag,
887 &field_path,
888 report,
889 );
890 }
891 }
892 }
893 }
894
895 fn validate_codes_tag_scoped(
899 &self,
900 workflow: &AhbWorkflow,
901 ctx: &EvaluationContext,
902 report: &mut ValidationReport,
903 ) {
904 let by_loc = partition_codes_by_mig(workflow);
905 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
906 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
908 for ((_group_key, tag), migs) in by_loc {
909 let merged = by_tag.entry(tag).or_default();
910 for (mig_key, bucket) in migs {
911 let entry = merged.entry(mig_key).or_default();
912 if entry.qualifier_position.is_none() {
913 entry.qualifier_position = bucket.qualifier_position;
914 }
915 if entry.qualifier_position == bucket.qualifier_position {
916 entry.qualifier_values.extend(&bucket.qualifier_values);
917 }
918 for (pos, codes) in bucket.codes {
919 entry.codes.entry(pos).or_default().extend(codes);
920 }
921 }
922 }
923
924 for (tag, migs) in &by_tag {
925 let field_path = format!("{tag}/qualifier");
926 Self::validate_segments_per_mig(
927 &ctx.find_segments(tag),
928 migs,
929 known_qualifiers.get(tag),
930 tag,
931 &field_path,
932 report,
933 );
934 }
935 }
936
937 fn validate_segments_per_mig(
946 segments: &[&OwnedSegment],
947 migs: &HashMap<Option<String>, MigCodeBucket>,
948 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
949 tag: &str,
950 field_path: &str,
951 report: &mut ValidationReport,
952 ) {
953 for seg in segments {
954 match match_segment_to_mig(seg, migs) {
955 Some(bucket) => {
956 for ((el, c), allowed) in &bucket.codes {
957 if allowed.is_empty() {
958 continue;
959 }
960 Self::check_segments_against_codes(
961 vec![*seg],
962 allowed,
963 tag,
964 *el,
965 *c,
966 field_path,
967 report,
968 );
969 }
970 }
971 None => {
972 let Some(qualifiers) = tag_qualifiers else {
973 continue;
974 };
975 for ((el, c), allowed) in qualifiers {
980 let Some(actual) = seg
981 .elements
982 .get(*el)
983 .and_then(|e| e.get(*c))
984 .filter(|v| !v.is_empty())
985 .map(|s| s.as_str())
986 else {
987 continue;
988 };
989 if allowed.iter().any(|v| v == actual) {
990 break;
992 }
993 let bucket_values: HashSet<&str> = migs
996 .values()
997 .filter(|b| b.qualifier_position == Some((*el, *c)))
998 .flat_map(|b| b.qualifier_values.iter().copied())
999 .collect();
1000 if !bucket_values.is_empty() {
1001 Self::check_segments_against_codes(
1002 vec![*seg],
1003 &bucket_values,
1004 tag,
1005 *el,
1006 *c,
1007 field_path,
1008 report,
1009 );
1010 break;
1011 }
1012 }
1013 }
1014 }
1015 }
1016 }
1017
1018 fn validate_answer_lists(
1027 &self,
1028 workflow: &AhbWorkflow,
1029 ctx: &EvaluationContext,
1030 report: &mut ValidationReport,
1031 ) {
1032 let lists = AnswerCodeLists::embedded();
1033 let fv = self.evaluator.format_version();
1034 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1035 for list_rule in &workflow.fields {
1036 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1037 continue;
1038 };
1039 if list_rule.codes.is_empty()
1040 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1041 {
1042 continue;
1043 }
1044 let Some(answer_rule) = workflow
1045 .fields
1046 .iter()
1047 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1048 else {
1049 continue;
1050 };
1051 let at = |rule: &AhbFieldRule| {
1052 (
1053 rule.element_index.unwrap_or(0),
1054 rule.component_index.unwrap_or(0),
1055 )
1056 };
1057 let (list_el, list_c) = at(list_rule);
1058 let (answer_el, answer_c) = at(answer_rule);
1059 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1060 seg.elements
1061 .get(el)
1062 .and_then(|e| e.get(c))
1063 .filter(|v| !v.is_empty())
1064 .cloned()
1065 };
1066 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1067 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1068 continue;
1069 }
1070 let (Some(list), Some(answer)) =
1071 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1072 else {
1073 continue;
1074 };
1075 if !list_rule.codes.iter().any(|c| c.value == list) {
1076 continue;
1077 }
1078 if answer_rule.codes.iter().any(|c| c.value == answer) {
1082 continue;
1083 }
1084 let Some(codes) = lists.codes(fv, &list) else {
1085 continue;
1086 };
1087 if codes.contains(&answer) {
1088 continue;
1089 }
1090 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1091 allowed.sort_unstable();
1092 report.add_issue(
1093 ValidationIssue::new(
1094 Severity::Warning,
1095 IssueKind::AnswerNotInList {
1096 answer: answer.clone(),
1097 list: list.clone(),
1098 allowed: allowed.clone(),
1099 },
1100 )
1101 .with_field_path(&answer_rule.segment_path)
1102 .with_actual(&answer)
1103 .with_expected(allowed.join(", ")),
1104 );
1105 }
1106 }
1107 }
1108
1109 fn check_segments_against_codes(
1111 segments: Vec<&OwnedSegment>,
1112 allowed_codes: &HashSet<&str>,
1113 _tag: &str,
1114 el_idx: usize,
1115 comp_idx: usize,
1116 field_path: &str,
1117 report: &mut ValidationReport,
1118 ) {
1119 for segment in segments {
1120 if let Some(code_value) = segment
1121 .elements
1122 .get(el_idx)
1123 .and_then(|e| e.get(comp_idx))
1124 .filter(|v| !v.is_empty())
1125 {
1126 if !allowed_codes.contains(code_value.as_str()) {
1127 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1128 sorted_codes.sort_unstable();
1129 let kind = IssueKind::CodeNotAllowedForPid {
1130 value: code_value.to_string(),
1131 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1132 };
1133 report.add_issue(
1134 ValidationIssue::new(Severity::Error, kind)
1135 .with_field_path(field_path)
1136 .with_actual(code_value)
1137 .with_expected(sorted_codes.join(", ")),
1138 );
1139 }
1140 }
1141 }
1142 }
1143}
1144
1145fn full_instance_paths<'p>(
1148 nav: &dyn GroupNavigator,
1149 group_path: &[&'p str],
1150) -> Vec<Vec<(&'p str, usize)>> {
1151 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1152 for g in group_path {
1153 paths = paths
1154 .into_iter()
1155 .flat_map(|parent| {
1156 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1157 let mut path = parent.clone();
1158 path.push((*g, i));
1159 path
1160 })
1161 })
1162 .collect();
1163 }
1164 paths
1165}
1166
1167struct VariantEntry<'a> {
1170 tag: String,
1171 element: usize,
1172 component: usize,
1173 codes: HashSet<&'a str>,
1174}
1175
1176fn merged_variant_entry<'a>(
1182 workflow: &'a AhbWorkflow,
1183 group_path: &str,
1184 mig: &str,
1185) -> Option<VariantEntry<'a>> {
1186 let mig: u32 = mig.parse().ok()?;
1187 let prefix = format!("{group_path}/");
1188 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1190 .fields
1191 .iter()
1192 .filter_map(|r| {
1193 let rest = r.segment_path.strip_prefix(&prefix)?;
1194 let tag = rest.split('/').next()?;
1195 if tag.starts_with("SG") {
1196 return None;
1197 }
1198 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1199 })
1200 .collect();
1201 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1203 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1204 .iter()
1205 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1206 .collect();
1207 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1208 if numbers.len() < 2 {
1209 return None;
1210 }
1211 let own = entries
1212 .iter()
1213 .filter(|(_, n, _)| *n <= mig)
1214 .map(|(_, n, _)| *n)
1215 .max()?;
1216 let (rule, _, tag) = entries
1218 .iter()
1219 .filter(|(_, n, _)| *n == own)
1220 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1221 Some(VariantEntry {
1222 tag: tag.to_string(),
1223 element: rule.element_index.unwrap_or(0),
1224 component: rule.component_index.unwrap_or(0),
1225 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1226 })
1227}
1228
1229fn value_length(format: &str, value: &str) -> usize {
1233 let format = format.trim();
1234 if format.starts_with('n') {
1235 value
1236 .chars()
1237 .filter(|c| !matches!(c, '.' | ',' | '-'))
1238 .count()
1239 } else {
1240 value.chars().count()
1241 }
1242}
1243
1244fn max_length(format: &str) -> Option<usize> {
1247 let digits = format
1248 .trim()
1249 .strip_prefix("an")
1250 .or_else(|| format.trim().strip_prefix('a'))
1251 .or_else(|| format.trim().strip_prefix('n'))?;
1252 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1253}
1254
1255fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1265 let derived_from_codes = field
1266 .codes
1267 .first()
1268 .is_some_and(|c| c.ahb_status == field.ahb_status);
1269 if !derived_from_codes {
1270 return &field.ahb_status;
1271 }
1272 value
1273 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1274 .map_or(&field.ahb_status, |c| &c.ahb_status)
1275}
1276
1277fn should_skip_for_parent_group<E: ConditionEvaluator>(
1283 field: &AhbFieldRule,
1284 expr_eval: &ConditionExprEvaluator<E>,
1285 ctx: &EvaluationContext,
1286 ub_definitions: &BTreeMap<String, ConditionExpr>,
1287) -> bool {
1288 if let Some(ref group_status) = field.parent_group_ahb_status {
1289 if group_status.contains('[') {
1290 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
1291 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
1292 }
1293 }
1294 false
1295}
1296
1297fn is_parent_group_required<E: ConditionEvaluator>(
1304 field: &AhbFieldRule,
1305 expr_eval: &ConditionExprEvaluator<E>,
1306 ctx: &EvaluationContext,
1307 ub_definitions: &BTreeMap<String, ConditionExpr>,
1308) -> bool {
1309 field
1310 .parent_group_ahb_status
1311 .as_deref()
1312 .is_some_and(|status| {
1313 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1314 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1315 })
1316}
1317
1318fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1327 let segment_id = extract_segment_id(&field.segment_path);
1328
1329 if !field.codes.is_empty() {
1335 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1336 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1337 let matching = ctx.find_segments(&segment_id);
1338 return matching.iter().any(|seg| {
1339 seg.elements
1340 .get(el_idx)
1341 .and_then(|e| e.get(comp_idx))
1342 .is_some_and(|v| required_codes.contains(&v.as_str()))
1343 });
1344 }
1345 if is_qualifier_field(&field.segment_path) {
1348 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1349 let el_idx = field.element_index.unwrap_or(0);
1350 let comp_idx = field.component_index.unwrap_or(0);
1351 let matching = ctx.find_segments(&segment_id);
1352 return matching.iter().any(|seg| {
1353 seg.elements
1354 .get(el_idx)
1355 .and_then(|e| e.get(comp_idx))
1356 .is_some_and(|v| required_codes.contains(&v.as_str()))
1357 });
1358 }
1359 }
1360
1361 ctx.has_segment(&segment_id)
1362}
1363
1364fn is_group_variant_absent(
1379 ctx: &EvaluationContext,
1380 field: &AhbFieldRule,
1381 is_group_mandatory: bool,
1382) -> bool {
1383 let group_path: Vec<&str> = field
1384 .segment_path
1385 .split('/')
1386 .take_while(|p| p.starts_with("SG"))
1387 .collect();
1388
1389 if group_path.is_empty() {
1390 return false;
1391 }
1392
1393 let nav = match ctx.navigator {
1394 Some(nav) => nav,
1395 None => return false,
1396 };
1397
1398 let instance_count = nav.group_instance_count(&group_path);
1399
1400 if instance_count == 0 {
1405 if !is_group_mandatory {
1406 return true;
1407 }
1408 return false;
1410 }
1411
1412 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1417 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1419 let segment_id = extract_segment_id(&field.segment_path);
1420 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1421
1422 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1423 nav.find_segments_in_group(&segment_id, &group_path, i)
1424 .iter()
1425 .any(|seg| {
1426 seg.elements
1427 .first()
1428 .and_then(|e| e.first())
1429 .is_some_and(|v| required_codes.contains(&v.as_str()))
1430 })
1431 });
1432
1433 if !any_instance_has_qualifier {
1434 return true; }
1436 }
1437 }
1438
1439 let segment_id = extract_segment_id(&field.segment_path);
1448 let segment_absent_from_all = (0..instance_count).all(|i| {
1449 nav.find_segments_in_group(&segment_id, &group_path, i)
1450 .is_empty()
1451 });
1452 if segment_absent_from_all {
1453 let group_has_other_segments =
1454 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1455 if group_has_other_segments {
1456 return true;
1457 }
1458 }
1459
1460 false
1461}
1462
1463fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1465 out.extend(group.fields.iter());
1466 for child in &group.children {
1467 collect_nodes_depth_first(child, out);
1468 }
1469}
1470
1471#[allow(clippy::too_many_arguments)]
1475fn evaluate_node<E: ConditionEvaluator>(
1476 node: &AhbNode,
1477 ctx: &EvaluationContext,
1478 expr_eval: &ConditionExprEvaluator<E>,
1479 evaluator: &E,
1480 ub_definitions: &BTreeMap<String, ConditionExpr>,
1481 tag_migs: &HashMap<String, HashSet<&str>>,
1482 instance_index: Option<usize>,
1483 report: &mut ValidationReport,
1484) {
1485 let field = node.rule;
1486
1487 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1489
1490 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1495 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1496 if length > max {
1497 let mut issue = ValidationIssue::new(
1498 Severity::Error,
1499 IssueKind::ValueTooLong {
1500 field_name: field.name.clone(),
1501 length,
1502 max,
1503 },
1504 )
1505 .with_field_path(&field.segment_path)
1506 .with_actual(value)
1507 .with_expected(field.format.clone().unwrap_or_default());
1508 if let Some(idx) = instance_index {
1509 issue = issue.with_instance_index(idx);
1510 }
1511 if let Some(num) = node.matched_segment_number {
1512 issue = issue.with_position(crate::SegmentPosition {
1513 segment_number: num,
1514 byte_offset: 0,
1515 message_number: 1,
1516 });
1517 }
1518 report.add_issue(attach_field_position(issue, field));
1519 }
1520 }
1521
1522 if should_skip_for_parent_group(field, expr_eval, ctx, ub_definitions) {
1524 return;
1525 }
1526
1527 let status = governing_status(field, node.value);
1530 let StatusResolution {
1531 result: condition_result,
1532 kind,
1533 unknown_ids,
1534 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1535 let mandatory = kind == Some(StatusKind::Muss);
1536
1537 match condition_result {
1538 ConditionResult::True => {
1539 if mandatory && node.value.is_none() {
1548 let tag = extract_segment_id(&field.segment_path);
1549 let single_variant_present = node.segment_elements.is_none()
1550 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1551 && is_field_present(ctx, field);
1552 let segment_optional_and_absent = node.segment_elements.is_none()
1557 && field
1558 .segment_ahb_status
1559 .as_deref()
1560 .is_some_and(is_optional_segment_status);
1561 let segment_not_required_and_absent = node.segment_elements.is_none()
1567 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1568 expr_eval
1569 .resolve_status(st, &node_ctx, ub_definitions)
1570 .result
1571 != ConditionResult::True
1572 });
1573 if !single_variant_present
1574 && !segment_optional_and_absent
1575 && !segment_not_required_and_absent
1576 {
1577 let mut issue = ValidationIssue::new(
1578 Severity::Error,
1579 IssueKind::MissingRequiredField {
1580 field_name: field.name.clone(),
1581 },
1582 )
1583 .with_field_path(&field.segment_path)
1584 .with_rule(&field.ahb_status);
1585 if let Some(first_code) = field.codes.first() {
1586 issue.expected_value = Some(first_code.value.clone());
1587 }
1588 if let Some(idx) = instance_index {
1589 issue = issue.with_instance_index(idx);
1590 }
1591 if let Some(num) = node.matched_segment_number {
1592 issue = issue.with_position(crate::SegmentPosition {
1593 segment_number: num,
1594 byte_offset: 0,
1595 message_number: 1,
1596 });
1597 }
1598 issue = attach_field_position(issue, field);
1599 report.add_issue(issue);
1600 }
1601 }
1602 }
1603 ConditionResult::False => {
1604 if mandatory && node.value.is_some() {
1607 let mut issue = ValidationIssue::new(
1608 Severity::Error,
1609 IssueKind::FieldConditionNotSatisfied {
1610 field_name: field.name.clone(),
1611 },
1612 )
1613 .with_field_path(&field.segment_path)
1614 .with_rule(&field.ahb_status);
1615 if let Some(idx) = instance_index {
1616 issue = issue.with_instance_index(idx);
1617 }
1618 if let Some(num) = node.matched_segment_number {
1619 issue = issue.with_position(crate::SegmentPosition {
1620 segment_number: num,
1621 byte_offset: 0,
1622 message_number: 1,
1623 });
1624 }
1625 issue = attach_field_position(issue, field);
1626 report.add_issue(issue);
1627 }
1628 }
1629 ConditionResult::Unknown => {
1630 let mut external_ids = Vec::new();
1632 let mut undetermined_ids = Vec::new();
1633 let mut missing_ids = Vec::new();
1634 for id in unknown_ids {
1635 if evaluator.is_external(id) {
1636 external_ids.push(id);
1637 } else if evaluator.is_known(id) {
1638 undetermined_ids.push(id);
1639 } else {
1640 missing_ids.push(id);
1641 }
1642 }
1643
1644 let mut issue = ValidationIssue::new(
1645 Severity::Info,
1646 IssueKind::ConditionUnknown {
1647 field_name: field.name.clone(),
1648 unresolved: UnresolvedConditions {
1649 external: external_ids,
1650 undetermined: undetermined_ids,
1651 missing: missing_ids,
1652 },
1653 },
1654 )
1655 .with_field_path(&field.segment_path)
1656 .with_rule(&field.ahb_status);
1657 if let Some(idx) = instance_index {
1658 issue = issue.with_instance_index(idx);
1659 }
1660 if let Some(num) = node.matched_segment_number {
1661 issue = issue.with_position(crate::SegmentPosition {
1662 segment_number: num,
1663 byte_offset: 0,
1664 message_number: 1,
1665 });
1666 }
1667 issue = attach_field_position(issue, field);
1668 report.add_issue(issue);
1669 }
1670 }
1671}
1672
1673fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1678 match field.element_index {
1679 Some(el) => {
1680 let element_pos = (el as u32) + 2;
1681 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1682 issue.with_field_position(element_pos, component_pos)
1683 }
1684 None => issue,
1685 }
1686}
1687
1688fn refuse_segments<E: ConditionEvaluator>(
1694 group: &AhbGroupNode,
1695 path: &[(&str, usize)],
1696 scoped_ctx: &EvaluationContext,
1697 evaluator: &E,
1698 ub_definitions: &BTreeMap<String, ConditionExpr>,
1699 instance_index: usize,
1700 report: &mut ValidationReport,
1701) {
1702 let mut seen: Vec<u32> = Vec::new();
1703 for node in &group.fields {
1704 let rule = node.rule;
1705 let (Some(status), Some(value), Some(number)) = (
1706 rule.segment_ahb_status.as_deref(),
1707 node.value,
1708 node.matched_segment_number,
1709 ) else {
1710 continue;
1711 };
1712 let qualifier =
1715 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
1716 if !qualifier
1717 || seen.contains(&number)
1718 || Some(status) == group.ahb_status
1719 || !rule.codes.iter().any(|c| c.value == value)
1720 {
1721 continue;
1722 }
1723 seen.push(number);
1724 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1725 continue;
1726 }
1727 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1728 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
1729 report.add_issue(
1730 ValidationIssue::new(
1731 Severity::Warning,
1732 IssueKind::SegmentNotAllowed {
1733 segment: segment.join("/"),
1734 },
1735 )
1736 .with_field_path(segment.join("/"))
1737 .with_rule(status)
1738 .with_instance_index(instance_index)
1739 .with_position(crate::SegmentPosition {
1740 segment_number: number,
1741 byte_offset: 0,
1742 message_number: 1,
1743 }),
1744 );
1745 }
1746}
1747
1748#[allow(clippy::too_many_arguments)]
1757fn walk_group_instance<E: ConditionEvaluator>(
1758 group: &AhbGroupNode,
1759 path: &[(&str, usize)],
1760 scoped_ctx: &EvaluationContext,
1761 expr_eval: &ConditionExprEvaluator<E>,
1762 evaluator: &E,
1763 ub_definitions: &BTreeMap<String, ConditionExpr>,
1764 tag_migs: &HashMap<String, HashSet<&str>>,
1765 instance_index: usize,
1766 report_segments: bool,
1767 report: &mut ValidationReport,
1768) {
1769 let mut group_refused = false;
1772 if let Some(status) = group.ahb_status {
1773 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1774 group_refused = true;
1775 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1776 let mut issue = ValidationIssue::new(
1777 Severity::Warning,
1778 IssueKind::GroupNotAllowed {
1779 group: group_path.join("/"),
1780 },
1781 )
1782 .with_field_path(group_path.join("/"))
1783 .with_rule(status)
1784 .with_instance_index(instance_index);
1785 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
1786 issue = issue.with_position(crate::SegmentPosition {
1787 segment_number: num,
1788 byte_offset: 0,
1789 message_number: 1,
1790 });
1791 }
1792 report.add_issue(issue);
1793 }
1794 }
1795 if report_segments && !group_refused {
1796 refuse_segments(
1797 group,
1798 path,
1799 scoped_ctx,
1800 evaluator,
1801 ub_definitions,
1802 instance_index,
1803 report,
1804 );
1805 }
1806 for node in &group.fields {
1807 evaluate_node(
1808 node,
1809 scoped_ctx,
1810 expr_eval,
1811 evaluator,
1812 ub_definitions,
1813 tag_migs,
1814 Some(instance_index),
1815 report,
1816 );
1817 }
1818 for child in &group.children {
1819 let mut child_path = path.to_vec();
1820 child_path.push((child.group_id, child.instance_index));
1821 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
1822 walk_group_instance(
1823 child,
1824 &child_path,
1825 &child_ctx,
1826 expr_eval,
1827 evaluator,
1828 ub_definitions,
1829 tag_migs,
1830 instance_index,
1831 report_segments,
1832 report,
1833 );
1834 }
1835}
1836
1837fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
1839 match expr {
1840 ConditionExpr::Package { id, min, max } => {
1841 out.push((*id, *min, *max));
1842 }
1843 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1844 for e in exprs {
1845 collect_packages(e, out);
1846 }
1847 }
1848 ConditionExpr::Xor(left, right) => {
1849 collect_packages(left, out);
1850 collect_packages(right, out);
1851 }
1852 ConditionExpr::Not(inner) => {
1853 collect_packages(inner, out);
1854 }
1855 ConditionExpr::Ref(_) => {}
1856 }
1857}
1858
1859fn is_optional_segment_status(status: &str) -> bool {
1869 let trimmed = status.trim();
1870 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
1871}
1872
1873fn is_qualifier_field(path: &str) -> bool {
1883 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
1884 matches!(parts.len(), 2 | 3)
1886}
1887
1888#[derive(Default)]
1896struct MigCodeBucket<'a> {
1897 qualifier_position: Option<(usize, usize)>,
1900 qualifier_values: HashSet<&'a str>,
1903 codes: HashMap<(usize, usize), HashSet<&'a str>>,
1906}
1907
1908fn partition_codes_by_mig(
1913 workflow: &AhbWorkflow,
1914) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
1915 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
1916 for field in &workflow.fields {
1917 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
1918 continue;
1919 }
1920 let tag = extract_segment_id(&field.segment_path);
1921 let group_key = extract_group_path_key(&field.segment_path);
1922 let mig = field.mig_number.clone();
1923 let el = field.element_index.unwrap_or(0);
1924 let c = field.component_index.unwrap_or(0);
1925
1926 let bucket = out
1927 .entry((group_key, tag))
1928 .or_default()
1929 .entry(mig)
1930 .or_default();
1931
1932 let required: Vec<&str> = field
1939 .codes
1940 .iter()
1941 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
1942 .map(|code| code.value.as_str())
1943 .collect();
1944
1945 if !required.is_empty() {
1952 if bucket.qualifier_position.is_none() {
1953 bucket.qualifier_position = Some((el, c));
1954 }
1955 if bucket.qualifier_position == Some((el, c)) {
1956 bucket.qualifier_values.extend(required.iter().copied());
1957 }
1958 }
1959
1960 for v in required {
1961 bucket.codes.entry((el, c)).or_default().insert(v);
1962 }
1963 }
1964 out
1965}
1966
1967fn global_qualifiers_by_tag(
1973 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
1974) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
1975 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
1976 for ((_group, tag), migs) in by_loc {
1977 let tag_entry = out.entry(tag.clone()).or_default();
1978 for bucket in migs.values() {
1979 if let Some(pos) = bucket.qualifier_position {
1980 tag_entry
1981 .entry(pos)
1982 .or_default()
1983 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
1984 }
1985 }
1986 }
1987 out
1988}
1989
1990fn match_segment_to_mig<'a, 'b>(
2001 seg: &OwnedSegment,
2002 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
2003) -> Option<&'a MigCodeBucket<'b>> {
2004 let actual_at = |el: usize, c: usize| -> &str {
2005 seg.elements
2006 .get(el)
2007 .and_then(|e| e.get(c))
2008 .map(|s| s.as_str())
2009 .unwrap_or("")
2010 };
2011
2012 let mut best: Option<&MigCodeBucket> = None;
2013 let mut best_matches = 0usize;
2014
2015 for bucket in migs.values() {
2016 let Some((el, c)) = bucket.qualifier_position else {
2017 continue;
2018 };
2019 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2020 continue;
2021 }
2022 let extra_matches = bucket
2023 .codes
2024 .iter()
2025 .filter(|(pos, _)| **pos != (el, c))
2026 .filter(|((e, k), allowed)| {
2027 let v = actual_at(*e, *k);
2028 !v.is_empty() && allowed.contains(v)
2029 })
2030 .count();
2031 if best.is_none() || extra_matches > best_matches {
2032 best = Some(bucket);
2033 best_matches = extra_matches;
2034 }
2035 }
2036 best
2037}
2038
2039fn extract_group_path_key(path: &str) -> String {
2044 let sg_parts: Vec<&str> = path
2045 .split('/')
2046 .take_while(|p| p.starts_with("SG"))
2047 .collect();
2048 sg_parts.join("/")
2049}
2050
2051fn extract_segment_id(path: &str) -> String {
2053 for part in path.split('/') {
2054 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2056 continue;
2057 }
2058 if part.len() >= 3
2060 && part
2061 .chars()
2062 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2063 {
2064 return part.to_string();
2065 }
2066 }
2067 path.split('/').next_back().unwrap_or(path).to_string()
2069}
2070
2071pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2083 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2085 if unh_count > 1 {
2086 return Some(ValidationIssue::new(
2087 Severity::Error,
2088 IssueKind::UntCountNotVerifiable { unh_count },
2089 ));
2090 }
2091
2092 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2094 let declared: usize = unt.get_element(0).parse().ok()?;
2095
2096 let actual = segments
2098 .iter()
2099 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2100 .count();
2101
2102 if declared != actual {
2103 Some(
2104 ValidationIssue::new(
2105 Severity::Error,
2106 IssueKind::UntSegmentCountMismatch { declared, actual },
2107 )
2108 .with_field_path("UNT/0074")
2109 .with_expected(actual.to_string())
2110 .with_actual(declared.to_string()),
2111 )
2112 } else {
2113 None
2114 }
2115}
2116
2117#[cfg(test)]
2118mod tests {
2119
2120 #[test]
2121 fn a_numeric_length_counts_digits_only() {
2122 assert_eq!(value_length("n..15", "-12.50"), 4);
2123 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
2124 assert_eq!(value_length("n5", "55001"), 5);
2125 assert_eq!(value_length("an..3", "Z.1"), 3);
2126 assert_eq!(value_length("an..35", "Müller"), 6);
2127 }
2128 use super::super::codes::ErrorCodes;
2129 use super::super::issue::ValidationCategory;
2130 use super::*;
2131 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2132 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2133 use std::collections::HashMap;
2134
2135 fn narrate(issue: &ValidationIssue) -> String {
2138 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2139 }
2140
2141 struct MockEvaluator {
2143 results: HashMap<u32, CR>,
2144 }
2145
2146 impl MockEvaluator {
2147 fn new(results: Vec<(u32, CR)>) -> Self {
2148 Self {
2149 results: results.into_iter().collect(),
2150 }
2151 }
2152
2153 fn all_true(ids: &[u32]) -> Self {
2154 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2155 }
2156 }
2157
2158 impl ConditionEvaluator for MockEvaluator {
2159 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2160 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2161 }
2162 fn is_external(&self, _condition: u32) -> bool {
2163 false
2164 }
2165 fn message_type(&self) -> &str {
2166 "UTILMD"
2167 }
2168 fn format_version(&self) -> &str {
2169 "FV2510"
2170 }
2171 }
2172
2173 #[test]
2176 fn a_present_code_is_judged_by_its_own_status() {
2177 let code = |value: &str, status: &str| AhbCodeRule {
2179 value: value.to_string(),
2180 description: String::new(),
2181 ahb_status: status.to_string(),
2182 };
2183 let field = AhbFieldRule {
2184 segment_path: "SG4/STS/C556/9013".to_string(),
2185 ahb_status: "X [192]".to_string(),
2186 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2187 ..Default::default()
2188 };
2189 assert_eq!(governing_status(&field, Some("E03")), "X");
2190 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2191 assert_eq!(governing_status(&field, None), "X [192]");
2192 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2193
2194 let own = AhbFieldRule {
2196 ahb_status: "X [931]".to_string(),
2197 codes: vec![code("303", "X")],
2198 ..Default::default()
2199 };
2200 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2201 }
2202
2203 #[test]
2204 fn test_extract_segment_id_simple() {
2205 assert_eq!(extract_segment_id("NAD"), "NAD");
2206 }
2207
2208 #[test]
2209 fn test_extract_segment_id_with_sg_prefix() {
2210 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
2211 }
2212
2213 #[test]
2214 fn test_extract_segment_id_nested_sg() {
2215 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
2216 }
2217
2218 #[test]
2221 fn test_validate_missing_mandatory_field() {
2222 let evaluator = MockEvaluator::all_true(&[182, 152]);
2223 let validator = EdifactValidator::new(evaluator);
2224 let external = NoOpExternalProvider;
2225
2226 let workflow = AhbWorkflow {
2227 pruefidentifikator: "11001".to_string(),
2228 description: "Test".to_string(),
2229 communication_direction: None,
2230 fields: vec![AhbFieldRule {
2231 segment_path: "SG2/NAD/C082/3039".to_string(),
2232 name: "MP-ID des MSB".to_string(),
2233 ahb_status: "Muss [182] ∧ [152]".to_string(),
2234 codes: vec![],
2235 parent_group_ahb_status: None,
2236 segment_ahb_status: None,
2237 ..Default::default()
2238 }],
2239 ub_definitions: BTreeMap::new(),
2240 };
2241
2242 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2244
2245 assert!(!report.is_valid());
2247 let errors: Vec<_> = report.errors().collect();
2248 assert_eq!(errors.len(), 1);
2249 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2250 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
2251 }
2252
2253 #[test]
2254 fn test_validate_condition_false_no_error() {
2255 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
2257 let validator = EdifactValidator::new(evaluator);
2258 let external = NoOpExternalProvider;
2259
2260 let workflow = AhbWorkflow {
2261 pruefidentifikator: "11001".to_string(),
2262 description: "Test".to_string(),
2263 communication_direction: None,
2264 fields: vec![AhbFieldRule {
2265 segment_path: "NAD".to_string(),
2266 name: "Partnerrolle".to_string(),
2267 ahb_status: "Muss [182] ∧ [152]".to_string(),
2268 codes: vec![],
2269 parent_group_ahb_status: None,
2270 segment_ahb_status: None,
2271 ..Default::default()
2272 }],
2273 ub_definitions: BTreeMap::new(),
2274 };
2275
2276 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2277
2278 assert!(report.is_valid());
2280 }
2281
2282 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
2284 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
2285 let workflow = AhbWorkflow {
2286 pruefidentifikator: "11001".to_string(),
2287 description: "Test".to_string(),
2288 communication_direction: None,
2289 fields: vec![AhbFieldRule {
2290 segment_path: "SG2/NAD/C082/3039".to_string(),
2291 name: "MP-ID".to_string(),
2292 ahb_status: status.to_string(),
2293 ..Default::default()
2294 }],
2295 ub_definitions: BTreeMap::new(),
2296 };
2297 validator.validate(
2298 &[],
2299 &workflow,
2300 &NoOpExternalProvider,
2301 ValidationLevel::Conditions,
2302 )
2303 }
2304
2305 #[test]
2306 fn a_later_soll_line_alone_does_not_require_the_field() {
2307 let report = validate_absent_field(
2309 "Muss [2119] \r\nSoll [130]",
2310 vec![(2119, CR::False), (130, CR::True)],
2311 );
2312 assert!(
2313 report.is_valid(),
2314 "{:?}",
2315 report.errors().collect::<Vec<_>>()
2316 );
2317 }
2318
2319 #[test]
2320 fn a_later_muss_line_requires_the_field() {
2321 let report = validate_absent_field(
2323 "Soll [47]\r\nMuss [46]",
2324 vec![(47, CR::False), (46, CR::True)],
2325 );
2326 let errors: Vec<_> = report.errors().collect();
2327 assert_eq!(errors.len(), 1);
2328 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2329 }
2330
2331 #[test]
2332 fn a_weaker_unknown_line_does_not_stop_a_muss() {
2333 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
2335 let errors: Vec<_> = report.errors().collect();
2336 assert_eq!(errors.len(), 1);
2337 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2338 }
2339
2340 #[test]
2341 fn a_stricter_unknown_line_leaves_the_status_undecided() {
2342 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
2343 assert!(report.is_valid());
2344 let infos: Vec<_> = report.infos().collect();
2345 assert_eq!(infos.len(), 1, "{infos:?}");
2346 match &infos[0].kind {
2347 IssueKind::ConditionUnknown { unresolved, .. } => {
2348 assert_eq!(unresolved.missing, vec![2119]);
2349 }
2350 other => panic!("expected ConditionUnknown, got {other:?}"),
2351 }
2352 }
2353
2354 #[test]
2355 fn test_validate_condition_unknown_adds_info() {
2356 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
2358 let validator = EdifactValidator::new(evaluator);
2360 let external = NoOpExternalProvider;
2361
2362 let workflow = AhbWorkflow {
2363 pruefidentifikator: "11001".to_string(),
2364 description: "Test".to_string(),
2365 communication_direction: None,
2366 fields: vec![AhbFieldRule {
2367 segment_path: "NAD".to_string(),
2368 name: "Partnerrolle".to_string(),
2369 ahb_status: "Muss [182] ∧ [152]".to_string(),
2370 codes: vec![],
2371 parent_group_ahb_status: None,
2372 segment_ahb_status: None,
2373 ..Default::default()
2374 }],
2375 ub_definitions: BTreeMap::new(),
2376 };
2377
2378 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2379
2380 assert!(report.is_valid());
2382 let infos: Vec<_> = report.infos().collect();
2383 assert_eq!(infos.len(), 1);
2384 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
2385 }
2386
2387 #[test]
2388 fn test_validate_structure_level_skips_conditions() {
2389 let evaluator = MockEvaluator::all_true(&[182, 152]);
2390 let validator = EdifactValidator::new(evaluator);
2391 let external = NoOpExternalProvider;
2392
2393 let workflow = AhbWorkflow {
2394 pruefidentifikator: "11001".to_string(),
2395 description: "Test".to_string(),
2396 communication_direction: None,
2397 fields: vec![AhbFieldRule {
2398 segment_path: "NAD".to_string(),
2399 name: "Partnerrolle".to_string(),
2400 ahb_status: "Muss [182] ∧ [152]".to_string(),
2401 codes: vec![],
2402 parent_group_ahb_status: None,
2403 segment_ahb_status: None,
2404 ..Default::default()
2405 }],
2406 ub_definitions: BTreeMap::new(),
2407 };
2408
2409 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
2411
2412 assert!(report.is_valid());
2414 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
2415 }
2416
2417 #[test]
2418 fn test_validate_empty_workflow_no_condition_errors() {
2419 let evaluator = MockEvaluator::all_true(&[]);
2420 let validator = EdifactValidator::new(evaluator);
2421 let external = NoOpExternalProvider;
2422
2423 let empty_workflow = AhbWorkflow {
2424 pruefidentifikator: String::new(),
2425 description: String::new(),
2426 communication_direction: None,
2427 fields: vec![],
2428 ub_definitions: BTreeMap::new(),
2429 };
2430
2431 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
2432
2433 assert!(report.is_valid());
2434 }
2435
2436 #[test]
2437 fn test_validate_bare_muss_always_required() {
2438 let evaluator = MockEvaluator::new(vec![]);
2439 let validator = EdifactValidator::new(evaluator);
2440 let external = NoOpExternalProvider;
2441
2442 let workflow = AhbWorkflow {
2443 pruefidentifikator: "55001".to_string(),
2444 description: "Test".to_string(),
2445 communication_direction: Some("NB an LF".to_string()),
2446 fields: vec![AhbFieldRule {
2447 segment_path: "SG2/NAD/3035".to_string(),
2448 name: "Partnerrolle".to_string(),
2449 ahb_status: "Muss".to_string(), codes: vec![],
2451 parent_group_ahb_status: None,
2452 segment_ahb_status: None,
2453 ..Default::default()
2454 }],
2455 ub_definitions: BTreeMap::new(),
2456 };
2457
2458 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2459
2460 assert!(!report.is_valid());
2462 assert_eq!(report.error_count(), 1);
2463 }
2464
2465 #[test]
2466 fn test_validate_x_status_is_mandatory() {
2467 let evaluator = MockEvaluator::new(vec![]);
2468 let validator = EdifactValidator::new(evaluator);
2469 let external = NoOpExternalProvider;
2470
2471 let workflow = AhbWorkflow {
2472 pruefidentifikator: "55001".to_string(),
2473 description: "Test".to_string(),
2474 communication_direction: None,
2475 fields: vec![AhbFieldRule {
2476 segment_path: "DTM".to_string(),
2477 name: "Datum".to_string(),
2478 ahb_status: "X".to_string(),
2479 codes: vec![],
2480 parent_group_ahb_status: None,
2481 segment_ahb_status: None,
2482 ..Default::default()
2483 }],
2484 ub_definitions: BTreeMap::new(),
2485 };
2486
2487 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2488
2489 assert!(!report.is_valid());
2490 let errors: Vec<_> = report.errors().collect();
2491 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2492 }
2493
2494 #[test]
2495 fn test_validate_soll_not_mandatory() {
2496 let evaluator = MockEvaluator::new(vec![]);
2497 let validator = EdifactValidator::new(evaluator);
2498 let external = NoOpExternalProvider;
2499
2500 let workflow = AhbWorkflow {
2501 pruefidentifikator: "55001".to_string(),
2502 description: "Test".to_string(),
2503 communication_direction: None,
2504 fields: vec![AhbFieldRule {
2505 segment_path: "DTM".to_string(),
2506 name: "Datum".to_string(),
2507 ahb_status: "Soll".to_string(),
2508 codes: vec![],
2509 parent_group_ahb_status: None,
2510 segment_ahb_status: None,
2511 ..Default::default()
2512 }],
2513 ub_definitions: BTreeMap::new(),
2514 };
2515
2516 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2517
2518 assert!(report.is_valid());
2520 }
2521
2522 #[test]
2523 fn test_report_includes_metadata() {
2524 let evaluator = MockEvaluator::new(vec![]);
2525 let validator = EdifactValidator::new(evaluator);
2526 let external = NoOpExternalProvider;
2527
2528 let workflow = AhbWorkflow {
2529 pruefidentifikator: "55001".to_string(),
2530 description: String::new(),
2531 communication_direction: None,
2532 fields: vec![],
2533 ub_definitions: BTreeMap::new(),
2534 };
2535
2536 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
2537
2538 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
2539 assert_eq!(report.level, ValidationLevel::Full);
2540 assert_eq!(report.message_type, "UTILMD");
2541 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
2542 }
2543
2544 #[test]
2545 fn test_validate_with_navigator_returns_report() {
2546 let evaluator = MockEvaluator::all_true(&[]);
2547 let validator = EdifactValidator::new(evaluator);
2548 let external = NoOpExternalProvider;
2549 let nav = crate::eval::NoOpGroupNavigator;
2550
2551 let workflow = AhbWorkflow {
2552 pruefidentifikator: "55001".to_string(),
2553 description: "Test".to_string(),
2554 communication_direction: None,
2555 fields: vec![],
2556 ub_definitions: BTreeMap::new(),
2557 };
2558
2559 let report = validator.validate_with_navigator(
2560 &[],
2561 &workflow,
2562 &external,
2563 ValidationLevel::Full,
2564 &nav,
2565 );
2566 assert!(report.is_valid());
2567 }
2568
2569 #[test]
2570 fn test_code_validation_composite_paths_valid_codes() {
2571 let evaluator = MockEvaluator::new(vec![]);
2575 let validator = EdifactValidator::new(evaluator);
2576 let external = NoOpExternalProvider;
2577
2578 let unh_segment = OwnedSegment {
2579 id: "UNH".to_string(),
2580 elements: vec![
2581 vec!["ALEXANDE951842".to_string()],
2582 vec![
2583 "UTILMD".to_string(),
2584 "D".to_string(),
2585 "11A".to_string(),
2586 "UN".to_string(),
2587 "S2.1".to_string(),
2588 ],
2589 ],
2590 segment_number: 1,
2591 };
2592
2593 let workflow = AhbWorkflow {
2594 pruefidentifikator: "55001".to_string(),
2595 description: "Test".to_string(),
2596 communication_direction: None,
2597 fields: vec![
2598 AhbFieldRule {
2599 segment_path: "UNH/S009/0065".to_string(),
2600 name: "Nachrichtentyp".to_string(),
2601 ahb_status: "X".to_string(),
2602 codes: vec![AhbCodeRule {
2603 value: "UTILMD".to_string(),
2604 description: "Stammdaten".to_string(),
2605 ahb_status: "X".to_string(),
2606 }],
2607 parent_group_ahb_status: None,
2608 segment_ahb_status: None,
2609 element_index: Some(1),
2610 component_index: Some(0),
2611 ..Default::default()
2612 },
2613 AhbFieldRule {
2614 segment_path: "UNH/S009/0052".to_string(),
2615 name: "Version".to_string(),
2616 ahb_status: "X".to_string(),
2617 codes: vec![AhbCodeRule {
2618 value: "D".to_string(),
2619 description: "Draft".to_string(),
2620 ahb_status: "X".to_string(),
2621 }],
2622 parent_group_ahb_status: None,
2623 segment_ahb_status: None,
2624 element_index: Some(1),
2625 component_index: Some(1),
2626 ..Default::default()
2627 },
2628 ],
2629 ub_definitions: BTreeMap::new(),
2630 };
2631
2632 let report = validator.validate(
2633 &[unh_segment],
2634 &workflow,
2635 &external,
2636 ValidationLevel::Conditions,
2637 );
2638
2639 let code_errors: Vec<_> = report
2640 .by_category(ValidationCategory::Code)
2641 .filter(|i| i.severity == Severity::Error)
2642 .collect();
2643 assert!(
2644 code_errors.is_empty(),
2645 "Expected no code errors when composite values match allowed codes, got: {:?}",
2646 code_errors
2647 );
2648 }
2649
2650 #[test]
2651 fn test_code_validation_partitions_by_mig_number() {
2652 let evaluator = MockEvaluator::new(vec![]);
2656 let validator = EdifactValidator::new(evaluator);
2657 let external = NoOpExternalProvider;
2658
2659 let sts_7 = OwnedSegment {
2660 id: "STS".to_string(),
2661 elements: vec![
2662 vec!["7".to_string()],
2663 vec![String::new()],
2664 vec!["GH02".to_string()],
2665 vec!["ZW4".to_string()],
2666 ],
2667 segment_number: 1,
2668 };
2669 let sts_e01 = OwnedSegment {
2670 id: "STS".to_string(),
2671 elements: vec![
2672 vec!["E01".to_string()],
2673 vec![String::new()],
2674 vec!["A99".to_string(), "E_0614".to_string()],
2675 ],
2676 segment_number: 2,
2677 };
2678
2679 let workflow = AhbWorkflow {
2680 pruefidentifikator: "55018".to_string(),
2681 description: "Test".to_string(),
2682 communication_direction: None,
2683 fields: vec![
2684 AhbFieldRule {
2686 segment_path: "SG4/STS/C601/9015".to_string(),
2687 name: "Statuskategorie".to_string(),
2688 ahb_status: "X".to_string(),
2689 codes: vec![AhbCodeRule {
2690 value: "7".to_string(),
2691 description: "Transaktionsgrund".to_string(),
2692 ahb_status: "X".to_string(),
2693 }],
2694 parent_group_ahb_status: None,
2695 segment_ahb_status: None,
2696 element_index: Some(0),
2697 component_index: Some(0),
2698 mig_number: Some("00035".to_string()),
2699 format: None,
2700 },
2701 AhbFieldRule {
2702 segment_path: "SG4/STS/C556/9013".to_string(),
2703 name: "Statusanlaß".to_string(),
2704 ahb_status: "X".to_string(),
2705 codes: vec![AhbCodeRule {
2706 value: "E03".to_string(),
2707 description: "Transaktionsgrund".to_string(),
2708 ahb_status: "X".to_string(),
2709 }],
2710 parent_group_ahb_status: None,
2711 segment_ahb_status: None,
2712 element_index: Some(2),
2713 component_index: Some(0),
2714 mig_number: Some("00035".to_string()),
2715 format: None,
2716 },
2717 AhbFieldRule {
2719 segment_path: "SG4/STS/C601/9015".to_string(),
2720 name: "Statuskategorie".to_string(),
2721 ahb_status: "X".to_string(),
2722 codes: vec![AhbCodeRule {
2723 value: "E01".to_string(),
2724 description: "Antwort".to_string(),
2725 ahb_status: "X".to_string(),
2726 }],
2727 parent_group_ahb_status: None,
2728 segment_ahb_status: None,
2729 element_index: Some(0),
2730 component_index: Some(0),
2731 mig_number: Some("00036".to_string()),
2732 format: None,
2733 },
2734 ],
2735 ub_definitions: BTreeMap::new(),
2736 };
2737
2738 let report = validator.validate(
2739 &[sts_7, sts_e01],
2740 &workflow,
2741 &external,
2742 ValidationLevel::Conditions,
2743 );
2744
2745 let code_errors: Vec<_> = report
2746 .by_category(ValidationCategory::Code)
2747 .filter(|i| {
2748 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2749 })
2750 .collect();
2751 assert_eq!(
2752 code_errors.len(),
2753 1,
2754 "Expected one COD002 (for GH02 only), got: {:?}",
2755 code_errors
2756 );
2757 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2758 }
2759
2760 #[test]
2761 fn test_code_validation_composite_paths_detects_invalid_code() {
2762 let evaluator = MockEvaluator::new(vec![]);
2765 let validator = EdifactValidator::new(evaluator);
2766 let external = NoOpExternalProvider;
2767
2768 let sts_segment = OwnedSegment {
2769 id: "STS".to_string(),
2770 elements: vec![
2771 vec!["7".to_string()],
2772 vec![String::new()],
2773 vec!["GH02".to_string()],
2774 vec!["ZW4".to_string()],
2775 ],
2776 segment_number: 1,
2777 };
2778
2779 let workflow = AhbWorkflow {
2780 pruefidentifikator: "55018".to_string(),
2781 description: "Test".to_string(),
2782 communication_direction: None,
2783 fields: vec![AhbFieldRule {
2784 segment_path: "SG4/STS/C556/9013".to_string(),
2785 name: "Statusanlaß".to_string(),
2786 ahb_status: "X".to_string(),
2787 codes: vec![AhbCodeRule {
2788 value: "E03".to_string(),
2789 description: "Transaktionsgrund".to_string(),
2790 ahb_status: "X".to_string(),
2791 }],
2792 parent_group_ahb_status: None,
2793 segment_ahb_status: None,
2794 element_index: Some(2),
2795 component_index: Some(0),
2796 ..Default::default()
2797 }],
2798 ub_definitions: BTreeMap::new(),
2799 };
2800
2801 let report = validator.validate(
2802 &[sts_segment],
2803 &workflow,
2804 &external,
2805 ValidationLevel::Conditions,
2806 );
2807
2808 let code_errors: Vec<_> = report
2809 .by_category(ValidationCategory::Code)
2810 .filter(|i| {
2811 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2812 })
2813 .collect();
2814 assert_eq!(
2815 code_errors.len(),
2816 1,
2817 "Expected COD002 for GH02, got: {:?}",
2818 code_errors
2819 );
2820 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2821 }
2822
2823 #[test]
2824 fn test_cross_field_code_validation_valid_qualifiers() {
2825 let evaluator = MockEvaluator::new(vec![]);
2828 let validator = EdifactValidator::new(evaluator);
2829 let external = NoOpExternalProvider;
2830
2831 let nad_ms = OwnedSegment {
2832 id: "NAD".to_string(),
2833 elements: vec![vec!["MS".to_string()]],
2834 segment_number: 4,
2835 };
2836 let nad_mr = OwnedSegment {
2837 id: "NAD".to_string(),
2838 elements: vec![vec!["MR".to_string()]],
2839 segment_number: 5,
2840 };
2841
2842 let workflow = AhbWorkflow {
2843 pruefidentifikator: "55001".to_string(),
2844 description: "Test".to_string(),
2845 communication_direction: None,
2846 fields: vec![
2847 AhbFieldRule {
2848 segment_path: "SG2/NAD/3035".to_string(),
2849 name: "Absender".to_string(),
2850 ahb_status: "X".to_string(),
2851 codes: vec![AhbCodeRule {
2852 value: "MS".to_string(),
2853 description: "Absender".to_string(),
2854 ahb_status: "X".to_string(),
2855 }],
2856 parent_group_ahb_status: None,
2857 segment_ahb_status: None,
2858 ..Default::default()
2859 },
2860 AhbFieldRule {
2861 segment_path: "SG2/NAD/3035".to_string(),
2862 name: "Empfaenger".to_string(),
2863 ahb_status: "X".to_string(),
2864 codes: vec![AhbCodeRule {
2865 value: "MR".to_string(),
2866 description: "Empfaenger".to_string(),
2867 ahb_status: "X".to_string(),
2868 }],
2869 parent_group_ahb_status: None,
2870 segment_ahb_status: None,
2871 ..Default::default()
2872 },
2873 ],
2874 ub_definitions: BTreeMap::new(),
2875 };
2876
2877 let report = validator.validate(
2878 &[nad_ms, nad_mr],
2879 &workflow,
2880 &external,
2881 ValidationLevel::Conditions,
2882 );
2883
2884 let code_errors: Vec<_> = report
2885 .by_category(ValidationCategory::Code)
2886 .filter(|i| i.severity == Severity::Error)
2887 .collect();
2888 assert!(
2889 code_errors.is_empty(),
2890 "Expected no code errors for valid qualifiers, got: {:?}",
2891 code_errors
2892 );
2893 }
2894
2895 #[test]
2896 fn test_cross_field_code_validation_catches_invalid_qualifier() {
2897 let evaluator = MockEvaluator::new(vec![]);
2899 let validator = EdifactValidator::new(evaluator);
2900 let external = NoOpExternalProvider;
2901
2902 let nad_ms = OwnedSegment {
2903 id: "NAD".to_string(),
2904 elements: vec![vec!["MS".to_string()]],
2905 segment_number: 4,
2906 };
2907 let nad_mt = OwnedSegment {
2908 id: "NAD".to_string(),
2909 elements: vec![vec!["MT".to_string()]], segment_number: 5,
2911 };
2912
2913 let workflow = AhbWorkflow {
2914 pruefidentifikator: "55001".to_string(),
2915 description: "Test".to_string(),
2916 communication_direction: None,
2917 fields: vec![
2918 AhbFieldRule {
2919 segment_path: "SG2/NAD/3035".to_string(),
2920 name: "Absender".to_string(),
2921 ahb_status: "X".to_string(),
2922 codes: vec![AhbCodeRule {
2923 value: "MS".to_string(),
2924 description: "Absender".to_string(),
2925 ahb_status: "X".to_string(),
2926 }],
2927 parent_group_ahb_status: None,
2928 segment_ahb_status: None,
2929 ..Default::default()
2930 },
2931 AhbFieldRule {
2932 segment_path: "SG2/NAD/3035".to_string(),
2933 name: "Empfaenger".to_string(),
2934 ahb_status: "X".to_string(),
2935 codes: vec![AhbCodeRule {
2936 value: "MR".to_string(),
2937 description: "Empfaenger".to_string(),
2938 ahb_status: "X".to_string(),
2939 }],
2940 parent_group_ahb_status: None,
2941 segment_ahb_status: None,
2942 ..Default::default()
2943 },
2944 ],
2945 ub_definitions: BTreeMap::new(),
2946 };
2947
2948 let report = validator.validate(
2949 &[nad_ms, nad_mt],
2950 &workflow,
2951 &external,
2952 ValidationLevel::Conditions,
2953 );
2954
2955 let code_errors: Vec<_> = report
2956 .by_category(ValidationCategory::Code)
2957 .filter(|i| i.severity == Severity::Error)
2958 .collect();
2959 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
2960 assert!(narrate(code_errors[0]).contains("MT"));
2961 assert!(narrate(code_errors[0]).contains("MR"));
2962 assert!(narrate(code_errors[0]).contains("MS"));
2963 }
2964
2965 #[test]
2966 fn test_cross_field_code_validation_unions_across_groups() {
2967 let evaluator = MockEvaluator::new(vec![]);
2971 let validator = EdifactValidator::new(evaluator);
2972 let external = NoOpExternalProvider;
2973
2974 let segments = vec![
2975 OwnedSegment {
2976 id: "NAD".to_string(),
2977 elements: vec![vec!["MS".to_string()]],
2978 segment_number: 3,
2979 },
2980 OwnedSegment {
2981 id: "NAD".to_string(),
2982 elements: vec![vec!["MR".to_string()]],
2983 segment_number: 4,
2984 },
2985 OwnedSegment {
2986 id: "NAD".to_string(),
2987 elements: vec![vec!["Z04".to_string()]],
2988 segment_number: 20,
2989 },
2990 OwnedSegment {
2991 id: "NAD".to_string(),
2992 elements: vec![vec!["Z09".to_string()]],
2993 segment_number: 21,
2994 },
2995 OwnedSegment {
2996 id: "NAD".to_string(),
2997 elements: vec![vec!["MT".to_string()]], segment_number: 22,
2999 },
3000 ];
3001
3002 let workflow = AhbWorkflow {
3003 pruefidentifikator: "55001".to_string(),
3004 description: "Test".to_string(),
3005 communication_direction: None,
3006 fields: vec![
3007 AhbFieldRule {
3008 segment_path: "SG2/NAD/3035".to_string(),
3009 name: "Absender".to_string(),
3010 ahb_status: "X".to_string(),
3011 codes: vec![AhbCodeRule {
3012 value: "MS".to_string(),
3013 description: "Absender".to_string(),
3014 ahb_status: "X".to_string(),
3015 }],
3016 parent_group_ahb_status: None,
3017 segment_ahb_status: None,
3018 ..Default::default()
3019 },
3020 AhbFieldRule {
3021 segment_path: "SG2/NAD/3035".to_string(),
3022 name: "Empfaenger".to_string(),
3023 ahb_status: "X".to_string(),
3024 codes: vec![AhbCodeRule {
3025 value: "MR".to_string(),
3026 description: "Empfaenger".to_string(),
3027 ahb_status: "X".to_string(),
3028 }],
3029 parent_group_ahb_status: None,
3030 segment_ahb_status: None,
3031 ..Default::default()
3032 },
3033 AhbFieldRule {
3034 segment_path: "SG4/SG12/NAD/3035".to_string(),
3035 name: "Anschlussnutzer".to_string(),
3036 ahb_status: "X".to_string(),
3037 codes: vec![AhbCodeRule {
3038 value: "Z04".to_string(),
3039 description: "Anschlussnutzer".to_string(),
3040 ahb_status: "X".to_string(),
3041 }],
3042 parent_group_ahb_status: None,
3043 segment_ahb_status: None,
3044 ..Default::default()
3045 },
3046 AhbFieldRule {
3047 segment_path: "SG4/SG12/NAD/3035".to_string(),
3048 name: "Korrespondenzanschrift".to_string(),
3049 ahb_status: "X".to_string(),
3050 codes: vec![AhbCodeRule {
3051 value: "Z09".to_string(),
3052 description: "Korrespondenzanschrift".to_string(),
3053 ahb_status: "X".to_string(),
3054 }],
3055 parent_group_ahb_status: None,
3056 segment_ahb_status: None,
3057 ..Default::default()
3058 },
3059 ],
3060 ub_definitions: BTreeMap::new(),
3061 };
3062
3063 let report =
3064 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3065
3066 let code_errors: Vec<_> = report
3067 .by_category(ValidationCategory::Code)
3068 .filter(|i| i.severity == Severity::Error)
3069 .collect();
3070 assert_eq!(
3071 code_errors.len(),
3072 1,
3073 "Expected exactly one COD002 error for MT, got: {:?}",
3074 code_errors
3075 );
3076 assert!(narrate(code_errors[0]).contains("MT"));
3077 }
3078
3079 #[test]
3080 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3081 let evaluator = MockEvaluator::new(vec![]);
3090 let validator = EdifactValidator::new(evaluator);
3091 let external = NoOpExternalProvider;
3092
3093 let qty_67 = OwnedSegment {
3094 id: "QTY".to_string(),
3095 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
3096 segment_number: 10,
3097 };
3098
3099 let workflow = AhbWorkflow {
3100 pruefidentifikator: "13025".to_string(),
3101 description: "Test".to_string(),
3102 communication_direction: None,
3103 fields: vec![AhbFieldRule {
3104 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
3105 name: "Menge, Qualifier".to_string(),
3106 ahb_status: "X".to_string(),
3107 codes: vec![
3108 AhbCodeRule {
3109 value: "220".to_string(),
3110 description: "Wahrer Wert".to_string(),
3111 ahb_status: "X".to_string(),
3112 },
3113 AhbCodeRule {
3114 value: "67".to_string(),
3115 description: "Ersatzwert".to_string(),
3116 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
3117 },
3118 AhbCodeRule {
3119 value: "Z18".to_string(),
3120 description: "Vorläufiger Wert".to_string(),
3121 ahb_status: "X [35]".to_string(),
3122 },
3123 ],
3124 parent_group_ahb_status: None,
3125 segment_ahb_status: None,
3126 element_index: Some(0),
3127 component_index: Some(0),
3128 ..Default::default()
3129 }],
3130 ub_definitions: BTreeMap::new(),
3131 };
3132
3133 let report =
3134 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3135
3136 let code_errors: Vec<_> = report
3137 .by_category(ValidationCategory::Code)
3138 .filter(|i| i.severity == Severity::Error)
3139 .collect();
3140 assert!(
3141 code_errors.is_empty(),
3142 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3143 code_errors
3144 );
3145 }
3146
3147 #[test]
3148 fn test_is_qualifier_field_simple_paths() {
3149 assert!(is_qualifier_field("NAD/3035"));
3150 assert!(is_qualifier_field("SG2/NAD/3035"));
3151 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3152 assert!(is_qualifier_field("LOC/3227"));
3153 }
3154
3155 #[test]
3156 fn test_is_qualifier_field_composite_paths() {
3157 assert!(is_qualifier_field("UNH/S009/0065"));
3161 assert!(is_qualifier_field("NAD/C082/3039"));
3162 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3163 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3164 }
3165
3166 #[test]
3167 fn test_is_qualifier_field_bare_segment() {
3168 assert!(!is_qualifier_field("NAD"));
3169 assert!(!is_qualifier_field("SG2/NAD"));
3170 }
3171
3172 #[test]
3173 fn test_is_qualifier_field_rejects_deep_paths() {
3174 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3176 }
3177
3178 #[test]
3179 fn test_missing_qualifier_instance_is_detected() {
3180 let evaluator = MockEvaluator::new(vec![]);
3183 let validator = EdifactValidator::new(evaluator);
3184 let external = NoOpExternalProvider;
3185
3186 let nad_ms = OwnedSegment {
3187 id: "NAD".to_string(),
3188 elements: vec![vec!["MS".to_string()]],
3189 segment_number: 3,
3190 };
3191
3192 let workflow = AhbWorkflow {
3193 pruefidentifikator: "55001".to_string(),
3194 description: "Test".to_string(),
3195 communication_direction: None,
3196 fields: vec![
3197 AhbFieldRule {
3198 segment_path: "SG2/NAD/3035".to_string(),
3199 name: "Absender".to_string(),
3200 ahb_status: "X".to_string(),
3201 codes: vec![AhbCodeRule {
3202 value: "MS".to_string(),
3203 description: "Absender".to_string(),
3204 ahb_status: "X".to_string(),
3205 }],
3206 parent_group_ahb_status: None,
3207 segment_ahb_status: None,
3208 ..Default::default()
3209 },
3210 AhbFieldRule {
3211 segment_path: "SG2/NAD/3035".to_string(),
3212 name: "Empfaenger".to_string(),
3213 ahb_status: "Muss".to_string(),
3214 codes: vec![AhbCodeRule {
3215 value: "MR".to_string(),
3216 description: "Empfaenger".to_string(),
3217 ahb_status: "X".to_string(),
3218 }],
3219 parent_group_ahb_status: None,
3220 segment_ahb_status: None,
3221 ..Default::default()
3222 },
3223 ],
3224 ub_definitions: BTreeMap::new(),
3225 };
3226
3227 let report =
3228 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
3229
3230 let ahb_errors: Vec<_> = report
3231 .by_category(ValidationCategory::Ahb)
3232 .filter(|i| i.severity == Severity::Error)
3233 .collect();
3234 assert_eq!(
3235 ahb_errors.len(),
3236 1,
3237 "Expected AHB001 for missing NAD+MR, got: {:?}",
3238 ahb_errors
3239 );
3240 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3241 }
3242
3243 #[test]
3244 fn test_present_qualifier_instance_no_error() {
3245 let evaluator = MockEvaluator::new(vec![]);
3247 let validator = EdifactValidator::new(evaluator);
3248 let external = NoOpExternalProvider;
3249
3250 let segments = vec![
3251 OwnedSegment {
3252 id: "NAD".to_string(),
3253 elements: vec![vec!["MS".to_string()]],
3254 segment_number: 3,
3255 },
3256 OwnedSegment {
3257 id: "NAD".to_string(),
3258 elements: vec![vec!["MR".to_string()]],
3259 segment_number: 4,
3260 },
3261 ];
3262
3263 let workflow = AhbWorkflow {
3264 pruefidentifikator: "55001".to_string(),
3265 description: "Test".to_string(),
3266 communication_direction: None,
3267 fields: vec![
3268 AhbFieldRule {
3269 segment_path: "SG2/NAD/3035".to_string(),
3270 name: "Absender".to_string(),
3271 ahb_status: "Muss".to_string(),
3272 codes: vec![AhbCodeRule {
3273 value: "MS".to_string(),
3274 description: "Absender".to_string(),
3275 ahb_status: "X".to_string(),
3276 }],
3277 parent_group_ahb_status: None,
3278 segment_ahb_status: None,
3279 ..Default::default()
3280 },
3281 AhbFieldRule {
3282 segment_path: "SG2/NAD/3035".to_string(),
3283 name: "Empfaenger".to_string(),
3284 ahb_status: "Muss".to_string(),
3285 codes: vec![AhbCodeRule {
3286 value: "MR".to_string(),
3287 description: "Empfaenger".to_string(),
3288 ahb_status: "X".to_string(),
3289 }],
3290 parent_group_ahb_status: None,
3291 segment_ahb_status: None,
3292 ..Default::default()
3293 },
3294 ],
3295 ub_definitions: BTreeMap::new(),
3296 };
3297
3298 let report =
3299 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3300
3301 let ahb_errors: Vec<_> = report
3302 .by_category(ValidationCategory::Ahb)
3303 .filter(|i| i.severity == Severity::Error)
3304 .collect();
3305 assert!(
3306 ahb_errors.is_empty(),
3307 "Expected no AHB001 errors, got: {:?}",
3308 ahb_errors
3309 );
3310 }
3311
3312 #[test]
3313 fn test_extract_group_path_key() {
3314 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
3315 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
3316 assert_eq!(extract_group_path_key("NAD/3035"), "");
3317 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
3318 }
3319
3320 fn validate_field_of_absent_group(
3322 group_status: &str,
3323 conditions: Vec<(u32, CR)>,
3324 ) -> ValidationReport {
3325 use mig_types::navigator::GroupNavigator;
3326
3327 struct NoSg9;
3328 impl GroupNavigator for NoSg9 {
3329 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3330 vec![]
3331 }
3332 fn find_segments_with_qualifier_in_group(
3333 &self,
3334 _: &str,
3335 _: usize,
3336 _: &str,
3337 _: &[&str],
3338 _: usize,
3339 ) -> Vec<OwnedSegment> {
3340 vec![]
3341 }
3342 fn group_instance_count(&self, _: &[&str]) -> usize {
3343 0
3344 }
3345 }
3346
3347 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3348 let workflow = AhbWorkflow {
3349 pruefidentifikator: "44001".to_string(),
3350 description: "Test".to_string(),
3351 communication_direction: None,
3352 fields: vec![AhbFieldRule {
3353 segment_path: "SG9/QTY/C186/6060".to_string(),
3354 name: "Menge".to_string(),
3355 ahb_status: "X".to_string(),
3356 parent_group_ahb_status: Some(group_status.to_string()),
3357 ..Default::default()
3358 }],
3359 ub_definitions: BTreeMap::new(),
3360 };
3361 validator.validate_with_navigator(
3362 &[],
3363 &workflow,
3364 &NoOpExternalProvider,
3365 ValidationLevel::Conditions,
3366 &NoSg9,
3367 )
3368 }
3369
3370 #[test]
3371 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
3372 let report = validate_field_of_absent_group(
3374 "Soll [47]\r\nMuss [46]",
3375 vec![(47, CR::True), (46, CR::False)],
3376 );
3377 assert!(
3378 report.is_valid(),
3379 "{:?}",
3380 report.errors().collect::<Vec<_>>()
3381 );
3382 }
3383
3384 #[test]
3385 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
3386 let report = validate_field_of_absent_group(
3387 "Soll [47]\r\nMuss [46]",
3388 vec![(47, CR::False), (46, CR::True)],
3389 );
3390 let errors: Vec<_> = report.errors().collect();
3391 assert_eq!(errors.len(), 1, "{errors:?}");
3392 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3393 }
3394
3395 #[test]
3396 fn test_absent_optional_group_no_missing_field_error() {
3397 use mig_types::navigator::GroupNavigator;
3400
3401 struct NavWithoutSG3;
3402 impl GroupNavigator for NavWithoutSG3 {
3403 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3404 vec![]
3405 }
3406 fn find_segments_with_qualifier_in_group(
3407 &self,
3408 _: &str,
3409 _: usize,
3410 _: &str,
3411 _: &[&str],
3412 _: usize,
3413 ) -> Vec<OwnedSegment> {
3414 vec![]
3415 }
3416 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3417 match group_path {
3418 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
3421 }
3422 }
3423 }
3424
3425 let evaluator = MockEvaluator::new(vec![]);
3426 let validator = EdifactValidator::new(evaluator);
3427 let external = NoOpExternalProvider;
3428 let nav = NavWithoutSG3;
3429
3430 let segments = vec![
3432 OwnedSegment {
3433 id: "NAD".into(),
3434 elements: vec![vec!["MS".into()]],
3435 segment_number: 3,
3436 },
3437 OwnedSegment {
3438 id: "NAD".into(),
3439 elements: vec![vec!["MR".into()]],
3440 segment_number: 4,
3441 },
3442 ];
3443
3444 let workflow = AhbWorkflow {
3445 pruefidentifikator: "55001".to_string(),
3446 description: "Test".to_string(),
3447 communication_direction: None,
3448 fields: vec![
3449 AhbFieldRule {
3450 segment_path: "SG2/SG3/CTA/3139".to_string(),
3451 name: "Funktion des Ansprechpartners, Code".to_string(),
3452 ahb_status: "Muss".to_string(),
3453 codes: vec![],
3454 parent_group_ahb_status: None,
3455 segment_ahb_status: None,
3456 ..Default::default()
3457 },
3458 AhbFieldRule {
3459 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
3460 name: "Name vom Ansprechpartner".to_string(),
3461 ahb_status: "X".to_string(),
3462 codes: vec![],
3463 parent_group_ahb_status: None,
3464 segment_ahb_status: None,
3465 ..Default::default()
3466 },
3467 ],
3468 ub_definitions: BTreeMap::new(),
3469 };
3470
3471 let report = validator.validate_with_navigator(
3472 &segments,
3473 &workflow,
3474 &external,
3475 ValidationLevel::Conditions,
3476 &nav,
3477 );
3478
3479 let ahb_errors: Vec<_> = report
3480 .by_category(ValidationCategory::Ahb)
3481 .filter(|i| i.severity == Severity::Error)
3482 .collect();
3483 assert!(
3484 ahb_errors.is_empty(),
3485 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
3486 ahb_errors
3487 );
3488 }
3489
3490 #[test]
3491 fn test_present_group_still_checks_mandatory_fields() {
3492 use mig_types::navigator::GroupNavigator;
3494
3495 struct NavWithSG3;
3496 impl GroupNavigator for NavWithSG3 {
3497 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3498 vec![]
3499 }
3500 fn find_segments_with_qualifier_in_group(
3501 &self,
3502 _: &str,
3503 _: usize,
3504 _: &str,
3505 _: &[&str],
3506 _: usize,
3507 ) -> Vec<OwnedSegment> {
3508 vec![]
3509 }
3510 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3511 match group_path {
3512 ["SG2"] => 1,
3513 ["SG2", "SG3"] => 1, _ => 0,
3515 }
3516 }
3517 }
3518
3519 let evaluator = MockEvaluator::new(vec![]);
3520 let validator = EdifactValidator::new(evaluator);
3521 let external = NoOpExternalProvider;
3522 let nav = NavWithSG3;
3523
3524 let segments = vec![OwnedSegment {
3526 id: "NAD".into(),
3527 elements: vec![vec!["MS".into()]],
3528 segment_number: 3,
3529 }];
3530
3531 let workflow = AhbWorkflow {
3532 pruefidentifikator: "55001".to_string(),
3533 description: "Test".to_string(),
3534 communication_direction: None,
3535 fields: vec![AhbFieldRule {
3536 segment_path: "SG2/SG3/CTA/3139".to_string(),
3537 name: "Funktion des Ansprechpartners, Code".to_string(),
3538 ahb_status: "Muss".to_string(),
3539 codes: vec![],
3540 parent_group_ahb_status: None,
3541 segment_ahb_status: None,
3542 ..Default::default()
3543 }],
3544 ub_definitions: BTreeMap::new(),
3545 };
3546
3547 let report = validator.validate_with_navigator(
3548 &segments,
3549 &workflow,
3550 &external,
3551 ValidationLevel::Conditions,
3552 &nav,
3553 );
3554
3555 let ahb_errors: Vec<_> = report
3556 .by_category(ValidationCategory::Ahb)
3557 .filter(|i| i.severity == Severity::Error)
3558 .collect();
3559 assert_eq!(
3560 ahb_errors.len(),
3561 1,
3562 "Expected AHB001 error when SG3 is present but CTA missing"
3563 );
3564 assert!(narrate(ahb_errors[0]).contains("CTA"));
3565 }
3566
3567 #[test]
3568 fn test_missing_qualifier_with_navigator_is_detected() {
3569 use mig_types::navigator::GroupNavigator;
3572
3573 struct NavWithSG2;
3574 impl GroupNavigator for NavWithSG2 {
3575 fn find_segments_in_group(
3576 &self,
3577 segment_id: &str,
3578 group_path: &[&str],
3579 instance_index: usize,
3580 ) -> Vec<OwnedSegment> {
3581 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
3582 vec![OwnedSegment {
3583 id: "NAD".into(),
3584 elements: vec![vec!["MS".into()]],
3585 segment_number: 3,
3586 }]
3587 } else {
3588 vec![]
3589 }
3590 }
3591 fn find_segments_with_qualifier_in_group(
3592 &self,
3593 _: &str,
3594 _: usize,
3595 _: &str,
3596 _: &[&str],
3597 _: usize,
3598 ) -> Vec<OwnedSegment> {
3599 vec![]
3600 }
3601 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3602 match group_path {
3603 ["SG2"] => 1,
3604 _ => 0,
3605 }
3606 }
3607 }
3608
3609 let evaluator = MockEvaluator::new(vec![]);
3610 let validator = EdifactValidator::new(evaluator);
3611 let external = NoOpExternalProvider;
3612 let nav = NavWithSG2;
3613
3614 let segments = vec![OwnedSegment {
3615 id: "NAD".into(),
3616 elements: vec![vec!["MS".into()]],
3617 segment_number: 3,
3618 }];
3619
3620 let workflow = AhbWorkflow {
3621 pruefidentifikator: "55001".to_string(),
3622 description: "Test".to_string(),
3623 communication_direction: None,
3624 fields: vec![
3625 AhbFieldRule {
3626 segment_path: "SG2/NAD/3035".to_string(),
3627 name: "Absender".to_string(),
3628 ahb_status: "X".to_string(),
3629 codes: vec![AhbCodeRule {
3630 value: "MS".to_string(),
3631 description: "Absender".to_string(),
3632 ahb_status: "X".to_string(),
3633 }],
3634 parent_group_ahb_status: None,
3635 segment_ahb_status: None,
3636 ..Default::default()
3637 },
3638 AhbFieldRule {
3639 segment_path: "SG2/NAD/3035".to_string(),
3640 name: "Empfaenger".to_string(),
3641 ahb_status: "Muss".to_string(),
3642 codes: vec![AhbCodeRule {
3643 value: "MR".to_string(),
3644 description: "Empfaenger".to_string(),
3645 ahb_status: "X".to_string(),
3646 }],
3647 parent_group_ahb_status: None,
3648 segment_ahb_status: None,
3649 ..Default::default()
3650 },
3651 ],
3652 ub_definitions: BTreeMap::new(),
3653 };
3654
3655 let report = validator.validate_with_navigator(
3656 &segments,
3657 &workflow,
3658 &external,
3659 ValidationLevel::Conditions,
3660 &nav,
3661 );
3662
3663 let ahb_errors: Vec<_> = report
3664 .by_category(ValidationCategory::Ahb)
3665 .filter(|i| i.severity == Severity::Error)
3666 .collect();
3667 assert_eq!(
3668 ahb_errors.len(),
3669 1,
3670 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
3671 ahb_errors
3672 );
3673 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3674 }
3675
3676 #[test]
3677 fn test_optional_group_variant_absent_no_error() {
3678 use mig_types::navigator::GroupNavigator;
3683
3684 struct TestNav;
3685 impl GroupNavigator for TestNav {
3686 fn find_segments_in_group(
3687 &self,
3688 segment_id: &str,
3689 group_path: &[&str],
3690 instance_index: usize,
3691 ) -> Vec<OwnedSegment> {
3692 match (segment_id, group_path, instance_index) {
3693 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
3694 id: "LOC".into(),
3695 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3696 segment_number: 10,
3697 }],
3698 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
3699 id: "NAD".into(),
3700 elements: vec![vec!["MS".into()]],
3701 segment_number: 3,
3702 }],
3703 _ => vec![],
3704 }
3705 }
3706 fn find_segments_with_qualifier_in_group(
3707 &self,
3708 _: &str,
3709 _: usize,
3710 _: &str,
3711 _: &[&str],
3712 _: usize,
3713 ) -> Vec<OwnedSegment> {
3714 vec![]
3715 }
3716 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3717 match group_path {
3718 ["SG2"] => 1,
3719 ["SG4"] => 1,
3720 ["SG4", "SG5"] => 1, _ => 0,
3722 }
3723 }
3724 }
3725
3726 let evaluator = MockEvaluator::new(vec![]);
3727 let validator = EdifactValidator::new(evaluator);
3728 let external = NoOpExternalProvider;
3729 let nav = TestNav;
3730
3731 let segments = vec![
3732 OwnedSegment {
3733 id: "NAD".into(),
3734 elements: vec![vec!["MS".into()]],
3735 segment_number: 3,
3736 },
3737 OwnedSegment {
3738 id: "LOC".into(),
3739 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3740 segment_number: 10,
3741 },
3742 ];
3743
3744 let workflow = AhbWorkflow {
3745 pruefidentifikator: "55001".to_string(),
3746 description: "Test".to_string(),
3747 communication_direction: None,
3748 fields: vec![
3749 AhbFieldRule {
3751 segment_path: "SG2/NAD/3035".to_string(),
3752 name: "Absender".to_string(),
3753 ahb_status: "X".to_string(),
3754 codes: vec![AhbCodeRule {
3755 value: "MS".to_string(),
3756 description: "Absender".to_string(),
3757 ahb_status: "X".to_string(),
3758 }],
3759 parent_group_ahb_status: Some("Muss".to_string()),
3760 segment_ahb_status: None,
3761 ..Default::default()
3762 },
3763 AhbFieldRule {
3764 segment_path: "SG2/NAD/3035".to_string(),
3765 name: "Empfaenger".to_string(),
3766 ahb_status: "Muss".to_string(),
3767 codes: vec![AhbCodeRule {
3768 value: "MR".to_string(),
3769 description: "Empfaenger".to_string(),
3770 ahb_status: "X".to_string(),
3771 }],
3772 parent_group_ahb_status: Some("Muss".to_string()),
3773 segment_ahb_status: None,
3774 ..Default::default()
3775 },
3776 AhbFieldRule {
3778 segment_path: "SG4/SG5/LOC/3227".to_string(),
3779 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3780 ahb_status: "X".to_string(),
3781 codes: vec![AhbCodeRule {
3782 value: "Z16".to_string(),
3783 description: "Marktlokation".to_string(),
3784 ahb_status: "X".to_string(),
3785 }],
3786 parent_group_ahb_status: Some("Kann".to_string()),
3787 segment_ahb_status: None,
3788 ..Default::default()
3789 },
3790 AhbFieldRule {
3791 segment_path: "SG4/SG5/LOC/3227".to_string(),
3792 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3793 ahb_status: "Muss".to_string(),
3794 codes: vec![AhbCodeRule {
3795 value: "Z17".to_string(),
3796 description: "Messlokation".to_string(),
3797 ahb_status: "X".to_string(),
3798 }],
3799 parent_group_ahb_status: Some("Kann".to_string()),
3800 segment_ahb_status: None,
3801 ..Default::default()
3802 },
3803 ],
3804 ub_definitions: BTreeMap::new(),
3805 };
3806
3807 let report = validator.validate_with_navigator(
3808 &segments,
3809 &workflow,
3810 &external,
3811 ValidationLevel::Conditions,
3812 &nav,
3813 );
3814
3815 let ahb_errors: Vec<_> = report
3816 .by_category(ValidationCategory::Ahb)
3817 .filter(|i| i.severity == Severity::Error)
3818 .collect();
3819
3820 assert_eq!(
3823 ahb_errors.len(),
3824 1,
3825 "Expected only AHB001 for missing NAD+MR, got: {:?}",
3826 ahb_errors
3827 );
3828 assert!(
3829 narrate(ahb_errors[0]).contains("Empfaenger"),
3830 "Error should be for missing NAD+MR (Empfaenger)"
3831 );
3832 }
3833
3834 #[test]
3838 fn test_optional_group_variant_with_true_condition_absent_no_error() {
3839 use mig_types::navigator::GroupNavigator;
3840
3841 struct TestNav;
3842 impl GroupNavigator for TestNav {
3843 fn find_segments_in_group(
3844 &self,
3845 segment_id: &str,
3846 group_path: &[&str],
3847 instance_index: usize,
3848 ) -> Vec<OwnedSegment> {
3849 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
3850 vec![OwnedSegment {
3851 id: "SEQ".into(),
3852 elements: vec![vec!["Z03".into()]],
3853 segment_number: 10,
3854 }]
3855 } else {
3856 vec![]
3857 }
3858 }
3859 fn find_segments_with_qualifier_in_group(
3860 &self,
3861 _: &str,
3862 _: usize,
3863 _: &str,
3864 _: &[&str],
3865 _: usize,
3866 ) -> Vec<OwnedSegment> {
3867 vec![]
3868 }
3869 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3870 match group_path {
3871 ["SG4"] => 1,
3872 ["SG4", "SG8"] => 1,
3873 _ => 0,
3874 }
3875 }
3876 }
3877
3878 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
3879 let validator = EdifactValidator::new(evaluator);
3880 let segments = vec![OwnedSegment {
3881 id: "SEQ".into(),
3882 elements: vec![vec!["Z03".into()]],
3883 segment_number: 10,
3884 }];
3885 let seq = |code: &str, group: &str| AhbFieldRule {
3886 segment_path: "SG4/SG8/SEQ/1229".to_string(),
3887 name: format!("Handlung, Code ({code})"),
3888 ahb_status: "X".to_string(),
3889 codes: vec![AhbCodeRule {
3890 value: code.to_string(),
3891 description: String::new(),
3892 ahb_status: "X".to_string(),
3893 }],
3894 parent_group_ahb_status: Some(group.to_string()),
3895 segment_ahb_status: None,
3896 ..Default::default()
3897 };
3898 let workflow = AhbWorkflow {
3899 pruefidentifikator: "44143".to_string(),
3900 description: "Test".to_string(),
3901 communication_direction: None,
3902 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
3903 ub_definitions: BTreeMap::new(),
3904 };
3905
3906 let report = validator.validate_with_navigator(
3907 &segments,
3908 &workflow,
3909 &NoOpExternalProvider,
3910 ValidationLevel::Conditions,
3911 &TestNav,
3912 );
3913 let missing: Vec<_> = report
3914 .issues
3915 .iter()
3916 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
3917 .collect();
3918 assert!(missing.is_empty(), "{missing:#?}");
3919 }
3920
3921 #[test]
3922 fn test_conditional_group_variant_absent_no_error() {
3923 use mig_types::navigator::GroupNavigator;
3928
3929 struct TestNav;
3930 impl GroupNavigator for TestNav {
3931 fn find_segments_in_group(
3932 &self,
3933 segment_id: &str,
3934 group_path: &[&str],
3935 instance_index: usize,
3936 ) -> Vec<OwnedSegment> {
3937 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
3938 vec![OwnedSegment {
3939 id: "LOC".into(),
3940 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3941 segment_number: 10,
3942 }]
3943 } else {
3944 vec![]
3945 }
3946 }
3947 fn find_segments_with_qualifier_in_group(
3948 &self,
3949 _: &str,
3950 _: usize,
3951 _: &str,
3952 _: &[&str],
3953 _: usize,
3954 ) -> Vec<OwnedSegment> {
3955 vec![]
3956 }
3957 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3958 match group_path {
3959 ["SG4"] => 1,
3960 ["SG4", "SG5"] => 1, _ => 0,
3962 }
3963 }
3964 }
3965
3966 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
3969 let validator = EdifactValidator::new(evaluator);
3970 let external = NoOpExternalProvider;
3971 let nav = TestNav;
3972
3973 let segments = vec![OwnedSegment {
3974 id: "LOC".into(),
3975 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3976 segment_number: 10,
3977 }];
3978
3979 let workflow = AhbWorkflow {
3980 pruefidentifikator: "55001".to_string(),
3981 description: "Test".to_string(),
3982 communication_direction: None,
3983 fields: vec![
3984 AhbFieldRule {
3986 segment_path: "SG4/SG5/LOC/3227".to_string(),
3987 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3988 ahb_status: "X".to_string(),
3989 codes: vec![AhbCodeRule {
3990 value: "Z16".to_string(),
3991 description: "Marktlokation".to_string(),
3992 ahb_status: "X".to_string(),
3993 }],
3994 parent_group_ahb_status: Some("Muss [2061]".to_string()),
3995 segment_ahb_status: None,
3996 ..Default::default()
3997 },
3998 AhbFieldRule {
4000 segment_path: "SG4/SG5/LOC/3227".to_string(),
4001 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4002 ahb_status: "X".to_string(),
4003 codes: vec![AhbCodeRule {
4004 value: "Z17".to_string(),
4005 description: "Messlokation".to_string(),
4006 ahb_status: "X".to_string(),
4007 }],
4008 parent_group_ahb_status: Some("Soll [165]".to_string()),
4009 segment_ahb_status: None,
4010 ..Default::default()
4011 },
4012 ],
4013 ub_definitions: BTreeMap::new(),
4014 };
4015
4016 let report = validator.validate_with_navigator(
4017 &segments,
4018 &workflow,
4019 &external,
4020 ValidationLevel::Conditions,
4021 &nav,
4022 );
4023
4024 let ahb_errors: Vec<_> = report
4025 .by_category(ValidationCategory::Ahb)
4026 .filter(|i| i.severity == Severity::Error)
4027 .collect();
4028
4029 assert!(
4031 ahb_errors.is_empty(),
4032 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4033 ahb_errors
4034 );
4035 }
4036
4037 #[test]
4038 fn test_conditional_group_variant_unknown_no_error() {
4039 let evaluator = MockEvaluator::new(vec![]);
4045 let validator = EdifactValidator::new(evaluator);
4046 let external = NoOpExternalProvider;
4047
4048 let workflow = AhbWorkflow {
4049 pruefidentifikator: "55001".to_string(),
4050 description: "Test".to_string(),
4051 communication_direction: None,
4052 fields: vec![AhbFieldRule {
4053 segment_path: "SG4/SG5/LOC/3227".to_string(),
4054 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4055 ahb_status: "X".to_string(),
4056 codes: vec![AhbCodeRule {
4057 value: "Z17".to_string(),
4058 description: "Messlokation".to_string(),
4059 ahb_status: "X".to_string(),
4060 }],
4061 parent_group_ahb_status: Some("Soll [165]".to_string()),
4062 segment_ahb_status: None,
4063 ..Default::default()
4064 }],
4065 ub_definitions: BTreeMap::new(),
4066 };
4067
4068 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4069
4070 let ahb_errors: Vec<_> = report
4071 .by_category(ValidationCategory::Ahb)
4072 .filter(|i| i.severity == Severity::Error)
4073 .collect();
4074
4075 assert!(
4077 ahb_errors.is_empty(),
4078 "Expected no errors when parent group condition is Unknown, got: {:?}",
4079 ahb_errors
4080 );
4081 }
4082
4083 #[test]
4084 fn test_segment_absent_within_present_group_no_error() {
4085 use mig_types::navigator::GroupNavigator;
4089
4090 struct TestNav;
4091 impl GroupNavigator for TestNav {
4092 fn find_segments_in_group(
4093 &self,
4094 segment_id: &str,
4095 group_path: &[&str],
4096 instance_index: usize,
4097 ) -> Vec<OwnedSegment> {
4098 if segment_id == "QTY"
4100 && group_path == ["SG5", "SG6", "SG9", "SG10"]
4101 && instance_index == 0
4102 {
4103 vec![OwnedSegment {
4104 id: "QTY".into(),
4105 elements: vec![vec!["220".into(), "0".into()]],
4106 segment_number: 14,
4107 }]
4108 } else {
4109 vec![]
4110 }
4111 }
4112 fn find_segments_with_qualifier_in_group(
4113 &self,
4114 _: &str,
4115 _: usize,
4116 _: &str,
4117 _: &[&str],
4118 _: usize,
4119 ) -> Vec<OwnedSegment> {
4120 vec![]
4121 }
4122 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4123 match group_path {
4124 ["SG5"] => 1,
4125 ["SG5", "SG6"] => 1,
4126 ["SG5", "SG6", "SG9"] => 1,
4127 ["SG5", "SG6", "SG9", "SG10"] => 1,
4128 _ => 0,
4129 }
4130 }
4131 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4132 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4134 }
4135 }
4136
4137 let evaluator = MockEvaluator::all_true(&[]);
4138 let validator = EdifactValidator::new(evaluator);
4139 let external = NoOpExternalProvider;
4140 let nav = TestNav;
4141
4142 let segments = vec![OwnedSegment {
4143 id: "QTY".into(),
4144 elements: vec![vec!["220".into(), "0".into()]],
4145 segment_number: 14,
4146 }];
4147
4148 let workflow = AhbWorkflow {
4149 pruefidentifikator: "13017".to_string(),
4150 description: "Test".to_string(),
4151 communication_direction: None,
4152 fields: vec![
4153 AhbFieldRule {
4155 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4156 name: "Statuskategorie, Code".to_string(),
4157 ahb_status: "X".to_string(),
4158 codes: vec![],
4159 parent_group_ahb_status: Some("Muss".to_string()),
4160 segment_ahb_status: None,
4161 ..Default::default()
4162 },
4163 AhbFieldRule {
4165 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4166 name: "Statusanlaß, Code".to_string(),
4167 ahb_status: "X [5]".to_string(),
4168 codes: vec![],
4169 parent_group_ahb_status: Some("Muss".to_string()),
4170 segment_ahb_status: None,
4171 ..Default::default()
4172 },
4173 ],
4174 ub_definitions: BTreeMap::new(),
4175 };
4176
4177 let report = validator.validate_with_navigator(
4178 &segments,
4179 &workflow,
4180 &external,
4181 ValidationLevel::Conditions,
4182 &nav,
4183 );
4184
4185 let ahb_errors: Vec<_> = report
4186 .by_category(ValidationCategory::Ahb)
4187 .filter(|i| i.severity == Severity::Error)
4188 .collect();
4189
4190 assert!(
4191 ahb_errors.is_empty(),
4192 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
4193 ahb_errors
4194 );
4195 }
4196
4197 #[test]
4198 fn test_group_scoped_code_validation_with_navigator() {
4199 use mig_types::navigator::GroupNavigator;
4203
4204 struct TestNav;
4205 impl GroupNavigator for TestNav {
4206 fn find_segments_in_group(
4207 &self,
4208 segment_id: &str,
4209 group_path: &[&str],
4210 _instance_index: usize,
4211 ) -> Vec<OwnedSegment> {
4212 if segment_id != "NAD" {
4213 return vec![];
4214 }
4215 match group_path {
4216 ["SG2"] => vec![
4217 OwnedSegment {
4218 id: "NAD".into(),
4219 elements: vec![vec!["MS".into()]],
4220 segment_number: 3,
4221 },
4222 OwnedSegment {
4223 id: "NAD".into(),
4224 elements: vec![vec!["MT".into()]], segment_number: 4,
4226 },
4227 ],
4228 ["SG4", "SG12"] => vec![
4229 OwnedSegment {
4230 id: "NAD".into(),
4231 elements: vec![vec!["Z04".into()]],
4232 segment_number: 20,
4233 },
4234 OwnedSegment {
4235 id: "NAD".into(),
4236 elements: vec![vec!["Z09".into()]],
4237 segment_number: 21,
4238 },
4239 ],
4240 _ => vec![],
4241 }
4242 }
4243 fn find_segments_with_qualifier_in_group(
4244 &self,
4245 _: &str,
4246 _: usize,
4247 _: &str,
4248 _: &[&str],
4249 _: usize,
4250 ) -> Vec<OwnedSegment> {
4251 vec![]
4252 }
4253 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4254 match group_path {
4255 ["SG2"] | ["SG4", "SG12"] => 1,
4256 _ => 0,
4257 }
4258 }
4259 }
4260
4261 let evaluator = MockEvaluator::new(vec![]);
4262 let validator = EdifactValidator::new(evaluator);
4263 let external = NoOpExternalProvider;
4264 let nav = TestNav;
4265
4266 let workflow = AhbWorkflow {
4267 pruefidentifikator: "55001".to_string(),
4268 description: "Test".to_string(),
4269 communication_direction: None,
4270 fields: vec![
4271 AhbFieldRule {
4272 segment_path: "SG2/NAD/3035".to_string(),
4273 name: "Absender".to_string(),
4274 ahb_status: "X".to_string(),
4275 codes: vec![AhbCodeRule {
4276 value: "MS".to_string(),
4277 description: "Absender".to_string(),
4278 ahb_status: "X".to_string(),
4279 }],
4280 parent_group_ahb_status: None,
4281 segment_ahb_status: None,
4282 ..Default::default()
4283 },
4284 AhbFieldRule {
4285 segment_path: "SG2/NAD/3035".to_string(),
4286 name: "Empfaenger".to_string(),
4287 ahb_status: "X".to_string(),
4288 codes: vec![AhbCodeRule {
4289 value: "MR".to_string(),
4290 description: "Empfaenger".to_string(),
4291 ahb_status: "X".to_string(),
4292 }],
4293 parent_group_ahb_status: None,
4294 segment_ahb_status: None,
4295 ..Default::default()
4296 },
4297 AhbFieldRule {
4298 segment_path: "SG4/SG12/NAD/3035".to_string(),
4299 name: "Anschlussnutzer".to_string(),
4300 ahb_status: "X".to_string(),
4301 codes: vec![AhbCodeRule {
4302 value: "Z04".to_string(),
4303 description: "Anschlussnutzer".to_string(),
4304 ahb_status: "X".to_string(),
4305 }],
4306 parent_group_ahb_status: None,
4307 segment_ahb_status: None,
4308 ..Default::default()
4309 },
4310 AhbFieldRule {
4311 segment_path: "SG4/SG12/NAD/3035".to_string(),
4312 name: "Korrespondenzanschrift".to_string(),
4313 ahb_status: "X".to_string(),
4314 codes: vec![AhbCodeRule {
4315 value: "Z09".to_string(),
4316 description: "Korrespondenzanschrift".to_string(),
4317 ahb_status: "X".to_string(),
4318 }],
4319 parent_group_ahb_status: None,
4320 segment_ahb_status: None,
4321 ..Default::default()
4322 },
4323 ],
4324 ub_definitions: BTreeMap::new(),
4325 };
4326
4327 let all_segments = vec![
4329 OwnedSegment {
4330 id: "NAD".into(),
4331 elements: vec![vec!["MS".into()]],
4332 segment_number: 3,
4333 },
4334 OwnedSegment {
4335 id: "NAD".into(),
4336 elements: vec![vec!["MT".into()]],
4337 segment_number: 4,
4338 },
4339 OwnedSegment {
4340 id: "NAD".into(),
4341 elements: vec![vec!["Z04".into()]],
4342 segment_number: 20,
4343 },
4344 OwnedSegment {
4345 id: "NAD".into(),
4346 elements: vec![vec!["Z09".into()]],
4347 segment_number: 21,
4348 },
4349 ];
4350
4351 let report = validator.validate_with_navigator(
4352 &all_segments,
4353 &workflow,
4354 &external,
4355 ValidationLevel::Conditions,
4356 &nav,
4357 );
4358
4359 let code_errors: Vec<_> = report
4360 .by_category(ValidationCategory::Code)
4361 .filter(|i| i.severity == Severity::Error)
4362 .collect();
4363
4364 assert_eq!(
4367 code_errors.len(),
4368 1,
4369 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
4370 code_errors
4371 );
4372 assert!(narrate(code_errors[0]).contains("MT"));
4373 assert!(narrate(code_errors[0]).contains("MR"));
4375 assert!(narrate(code_errors[0]).contains("MS"));
4376 assert!(
4377 !narrate(code_errors[0]).contains("Z04"),
4378 "SG4/SG12 codes should not leak into SG2 error"
4379 );
4380 assert!(
4382 code_errors[0]
4383 .field_path
4384 .as_deref()
4385 .unwrap_or("")
4386 .contains("SG2"),
4387 "Error field_path should reference SG2, got: {:?}",
4388 code_errors[0].field_path
4389 );
4390 }
4391
4392 #[test]
4395 fn test_package_cardinality_within_bounds() {
4396 let evaluator = MockEvaluator::all_true(&[]);
4398 let validator = EdifactValidator::new(evaluator);
4399 let external = NoOpExternalProvider;
4400
4401 let segments = vec![OwnedSegment {
4402 id: "STS".into(),
4403 elements: vec![
4404 vec!["Z33".into()], vec![], vec!["E01".into()], ],
4408 segment_number: 5,
4409 }];
4410
4411 let workflow = AhbWorkflow {
4412 pruefidentifikator: "13017".to_string(),
4413 description: "Test".to_string(),
4414 communication_direction: None,
4415 ub_definitions: BTreeMap::new(),
4416 fields: vec![AhbFieldRule {
4417 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4418 name: "Statusanlaß, Code".to_string(),
4419 ahb_status: "X".to_string(),
4420 element_index: Some(2),
4421 component_index: Some(0),
4422 codes: vec![
4423 AhbCodeRule {
4424 value: "E01".into(),
4425 description: "Code 1".into(),
4426 ahb_status: "X [4P0..1]".into(),
4427 },
4428 AhbCodeRule {
4429 value: "E02".into(),
4430 description: "Code 2".into(),
4431 ahb_status: "X [4P0..1]".into(),
4432 },
4433 ],
4434 parent_group_ahb_status: Some("Muss".to_string()),
4435 segment_ahb_status: None,
4436 mig_number: None,
4437 format: None,
4438 }],
4439 };
4440
4441 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4442 let pkg_errors: Vec<_> = report
4443 .by_category(ValidationCategory::Ahb)
4444 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4445 .collect();
4446 assert!(
4447 pkg_errors.is_empty(),
4448 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
4449 pkg_errors
4450 );
4451 }
4452
4453 #[test]
4454 fn test_package_cardinality_zero_present_min_zero() {
4455 let evaluator = MockEvaluator::all_true(&[]);
4457 let validator = EdifactValidator::new(evaluator);
4458 let external = NoOpExternalProvider;
4459
4460 let segments = vec![OwnedSegment {
4461 id: "STS".into(),
4462 elements: vec![
4463 vec!["Z33".into()],
4464 vec![],
4465 vec!["X99".into()], ],
4467 segment_number: 5,
4468 }];
4469
4470 let workflow = AhbWorkflow {
4471 pruefidentifikator: "13017".to_string(),
4472 description: "Test".to_string(),
4473 communication_direction: None,
4474 ub_definitions: BTreeMap::new(),
4475 fields: vec![AhbFieldRule {
4476 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4477 name: "Statusanlaß, Code".to_string(),
4478 ahb_status: "X".to_string(),
4479 element_index: Some(2),
4480 component_index: Some(0),
4481 codes: vec![
4482 AhbCodeRule {
4483 value: "E01".into(),
4484 description: "Code 1".into(),
4485 ahb_status: "X [4P0..1]".into(),
4486 },
4487 AhbCodeRule {
4488 value: "E02".into(),
4489 description: "Code 2".into(),
4490 ahb_status: "X [4P0..1]".into(),
4491 },
4492 ],
4493 parent_group_ahb_status: Some("Muss".to_string()),
4494 segment_ahb_status: None,
4495 mig_number: None,
4496 format: None,
4497 }],
4498 };
4499
4500 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4501 let pkg_errors: Vec<_> = report
4502 .by_category(ValidationCategory::Ahb)
4503 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4504 .collect();
4505 assert!(
4506 pkg_errors.is_empty(),
4507 "0 codes, min=0 — no error expected, got: {:?}",
4508 pkg_errors
4509 );
4510 }
4511
4512 #[test]
4513 fn test_package_cardinality_too_many() {
4514 let evaluator = MockEvaluator::all_true(&[]);
4516 let validator = EdifactValidator::new(evaluator);
4517 let external = NoOpExternalProvider;
4518
4519 let segments = vec![
4521 OwnedSegment {
4522 id: "STS".into(),
4523 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
4524 segment_number: 5,
4525 },
4526 OwnedSegment {
4527 id: "STS".into(),
4528 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
4529 segment_number: 6,
4530 },
4531 ];
4532
4533 let workflow = AhbWorkflow {
4534 pruefidentifikator: "13017".to_string(),
4535 description: "Test".to_string(),
4536 communication_direction: None,
4537 ub_definitions: BTreeMap::new(),
4538 fields: vec![AhbFieldRule {
4539 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4540 name: "Statusanlaß, Code".to_string(),
4541 ahb_status: "X".to_string(),
4542 element_index: Some(2),
4543 component_index: Some(0),
4544 codes: vec![
4545 AhbCodeRule {
4546 value: "E01".into(),
4547 description: "Code 1".into(),
4548 ahb_status: "X [4P0..1]".into(),
4549 },
4550 AhbCodeRule {
4551 value: "E02".into(),
4552 description: "Code 2".into(),
4553 ahb_status: "X [4P0..1]".into(),
4554 },
4555 ],
4556 parent_group_ahb_status: Some("Muss".to_string()),
4557 segment_ahb_status: None,
4558 mig_number: None,
4559 format: None,
4560 }],
4561 };
4562
4563 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4564 let pkg_errors: Vec<_> = report
4565 .by_category(ValidationCategory::Ahb)
4566 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4567 .collect();
4568 assert_eq!(
4569 pkg_errors.len(),
4570 1,
4571 "2 codes present, max=1 — expected 1 error, got: {:?}",
4572 pkg_errors
4573 );
4574 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
4575 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
4576 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
4577 }
4578
4579 #[test]
4580 fn test_package_cardinality_too_few() {
4581 let evaluator = MockEvaluator::all_true(&[]);
4583 let validator = EdifactValidator::new(evaluator);
4584 let external = NoOpExternalProvider;
4585
4586 let segments = vec![OwnedSegment {
4587 id: "STS".into(),
4588 elements: vec![
4589 vec!["Z33".into()],
4590 vec![],
4591 vec!["X99".into()], ],
4593 segment_number: 5,
4594 }];
4595
4596 let workflow = AhbWorkflow {
4597 pruefidentifikator: "13017".to_string(),
4598 description: "Test".to_string(),
4599 communication_direction: None,
4600 ub_definitions: BTreeMap::new(),
4601 fields: vec![AhbFieldRule {
4602 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4603 name: "Statusanlaß, Code".to_string(),
4604 ahb_status: "X".to_string(),
4605 element_index: Some(2),
4606 component_index: Some(0),
4607 codes: vec![
4608 AhbCodeRule {
4609 value: "E01".into(),
4610 description: "Code 1".into(),
4611 ahb_status: "X [5P1..3]".into(),
4612 },
4613 AhbCodeRule {
4614 value: "E02".into(),
4615 description: "Code 2".into(),
4616 ahb_status: "X [5P1..3]".into(),
4617 },
4618 AhbCodeRule {
4619 value: "E03".into(),
4620 description: "Code 3".into(),
4621 ahb_status: "X [5P1..3]".into(),
4622 },
4623 ],
4624 parent_group_ahb_status: Some("Muss".to_string()),
4625 segment_ahb_status: None,
4626 mig_number: None,
4627 format: None,
4628 }],
4629 };
4630
4631 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4632 let pkg_errors: Vec<_> = report
4633 .by_category(ValidationCategory::Ahb)
4634 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4635 .collect();
4636 assert_eq!(
4637 pkg_errors.len(),
4638 1,
4639 "0 codes present, min=1 — expected 1 error, got: {:?}",
4640 pkg_errors
4641 );
4642 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
4643 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
4644 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
4645 }
4646
4647 #[test]
4648 fn test_package_cardinality_no_packages_in_workflow() {
4649 let evaluator = MockEvaluator::all_true(&[]);
4651 let validator = EdifactValidator::new(evaluator);
4652 let external = NoOpExternalProvider;
4653
4654 let segments = vec![OwnedSegment {
4655 id: "STS".into(),
4656 elements: vec![vec!["E01".into()]],
4657 segment_number: 5,
4658 }];
4659
4660 let workflow = AhbWorkflow {
4661 pruefidentifikator: "13017".to_string(),
4662 description: "Test".to_string(),
4663 communication_direction: None,
4664 ub_definitions: BTreeMap::new(),
4665 fields: vec![AhbFieldRule {
4666 segment_path: "STS/9015".to_string(),
4667 name: "Status Code".to_string(),
4668 ahb_status: "X".to_string(),
4669 codes: vec![AhbCodeRule {
4670 value: "E01".into(),
4671 description: "Code 1".into(),
4672 ahb_status: "X".into(),
4673 }],
4674 parent_group_ahb_status: Some("Muss".to_string()),
4675 segment_ahb_status: None,
4676 ..Default::default()
4677 }],
4678 };
4679
4680 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4681 let pkg_errors: Vec<_> = report
4682 .by_category(ValidationCategory::Ahb)
4683 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4684 .collect();
4685 assert!(
4686 pkg_errors.is_empty(),
4687 "No packages in workflow — no errors expected"
4688 );
4689 }
4690
4691 #[test]
4692 fn test_package_cardinality_with_condition_and_package() {
4693 let evaluator = MockEvaluator::all_true(&[901]);
4695 let validator = EdifactValidator::new(evaluator);
4696 let external = NoOpExternalProvider;
4697
4698 let segments = vec![OwnedSegment {
4699 id: "STS".into(),
4700 elements: vec![vec![], vec![], vec!["E01".into()]],
4701 segment_number: 5,
4702 }];
4703
4704 let workflow = AhbWorkflow {
4705 pruefidentifikator: "13017".to_string(),
4706 description: "Test".to_string(),
4707 communication_direction: None,
4708 ub_definitions: BTreeMap::new(),
4709 fields: vec![AhbFieldRule {
4710 segment_path: "SG10/STS/C556/9013".to_string(),
4711 name: "Code".to_string(),
4712 ahb_status: "X".to_string(),
4713 element_index: Some(2),
4714 component_index: Some(0),
4715 codes: vec![
4716 AhbCodeRule {
4717 value: "E01".into(),
4718 description: "Code 1".into(),
4719 ahb_status: "X [901] [4P0..1]".into(),
4720 },
4721 AhbCodeRule {
4722 value: "E02".into(),
4723 description: "Code 2".into(),
4724 ahb_status: "X [901] [4P0..1]".into(),
4725 },
4726 ],
4727 parent_group_ahb_status: Some("Muss".to_string()),
4728 segment_ahb_status: None,
4729 mig_number: None,
4730 format: None,
4731 }],
4732 };
4733
4734 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4735 let pkg_errors: Vec<_> = report
4736 .by_category(ValidationCategory::Ahb)
4737 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4738 .collect();
4739 assert!(
4740 pkg_errors.is_empty(),
4741 "1 code within [4P0..1] bounds — no error, got: {:?}",
4742 pkg_errors
4743 );
4744 }
4745
4746 #[test]
4747 fn test_package_cardinality_scoped_per_group_instance() {
4748 use mig_types::navigator::GroupNavigator;
4756
4757 struct TwoSg10s {
4758 sts_a: OwnedSegment,
4759 sts_b: OwnedSegment,
4760 }
4761 impl GroupNavigator for TwoSg10s {
4762 fn find_segments_in_group(
4763 &self,
4764 segment_id: &str,
4765 group_path: &[&str],
4766 instance_index: usize,
4767 ) -> Vec<OwnedSegment> {
4768 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
4769 match instance_index {
4770 0 => vec![self.sts_a.clone()],
4771 1 => vec![self.sts_b.clone()],
4772 _ => vec![],
4773 }
4774 } else {
4775 vec![]
4776 }
4777 }
4778 fn find_segments_with_qualifier_in_group(
4779 &self,
4780 _: &str,
4781 _: usize,
4782 _: &str,
4783 _: &[&str],
4784 _: usize,
4785 ) -> Vec<OwnedSegment> {
4786 vec![]
4787 }
4788 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4789 match group_path {
4790 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
4791 ["SG5", "SG6", "SG9", "SG10"] => 2,
4792 _ => 0,
4793 }
4794 }
4795 }
4796
4797 let sts_a = OwnedSegment {
4798 id: "STS".into(),
4799 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4800 segment_number: 10,
4801 };
4802 let sts_b = OwnedSegment {
4803 id: "STS".into(),
4804 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4805 segment_number: 15,
4806 };
4807 let nav = TwoSg10s {
4808 sts_a: sts_a.clone(),
4809 sts_b: sts_b.clone(),
4810 };
4811
4812 let evaluator = MockEvaluator::all_true(&[]);
4813 let validator = EdifactValidator::new(evaluator);
4814 let external = NoOpExternalProvider;
4815
4816 let workflow = AhbWorkflow {
4817 pruefidentifikator: "13025".to_string(),
4818 description: "Test".to_string(),
4819 communication_direction: None,
4820 ub_definitions: BTreeMap::new(),
4821 fields: vec![AhbFieldRule {
4822 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4823 name: "Statusanlaß, Code".to_string(),
4824 ahb_status: "X".to_string(),
4825 element_index: Some(2),
4826 component_index: Some(0),
4827 codes: vec![
4828 AhbCodeRule {
4829 value: "E01".into(),
4830 description: "Code 1".into(),
4831 ahb_status: "X [4P0..1]".into(),
4832 },
4833 AhbCodeRule {
4834 value: "E02".into(),
4835 description: "Code 2".into(),
4836 ahb_status: "X [4P0..1]".into(),
4837 },
4838 ],
4839 parent_group_ahb_status: Some("Muss".to_string()),
4840 segment_ahb_status: None,
4841 mig_number: None,
4842 format: None,
4843 }],
4844 };
4845
4846 let report = validator.validate_with_navigator(
4847 &[sts_a, sts_b],
4848 &workflow,
4849 &external,
4850 ValidationLevel::Full,
4851 &nav,
4852 );
4853 let pkg_errors: Vec<_> = report
4854 .by_category(ValidationCategory::Ahb)
4855 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4856 .collect();
4857 assert!(
4858 pkg_errors.is_empty(),
4859 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
4860 pkg_errors
4861 );
4862 }
4863
4864 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
4865 OwnedSegment {
4866 id: id.to_string(),
4867 elements: elements
4868 .into_iter()
4869 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
4870 .collect(),
4871 segment_number: 0,
4872 }
4873 }
4874
4875 #[test]
4886 fn test_package_cardinality_scoped_to_rule_mig_variant() {
4887 use mig_types::navigator::GroupNavigator;
4888
4889 struct TwoSg8Variants {
4890 seq_z01: OwnedSegment,
4891 seq_z45: OwnedSegment,
4892 }
4893 impl GroupNavigator for TwoSg8Variants {
4894 fn find_segments_in_group(
4895 &self,
4896 segment_id: &str,
4897 group_path: &[&str],
4898 instance_index: usize,
4899 ) -> Vec<OwnedSegment> {
4900 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
4901 match instance_index {
4902 0 => vec![self.seq_z01.clone()],
4903 1 => vec![self.seq_z45.clone()],
4904 _ => vec![],
4905 }
4906 } else {
4907 vec![]
4908 }
4909 }
4910 fn find_segments_with_qualifier_in_group(
4911 &self,
4912 _: &str,
4913 _: usize,
4914 _: &str,
4915 _: &[&str],
4916 _: usize,
4917 ) -> Vec<OwnedSegment> {
4918 vec![]
4919 }
4920 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4921 match group_path {
4922 ["SG4"] => 1,
4923 ["SG4", "SG8"] => 2,
4924 _ => 0,
4925 }
4926 }
4927 fn instance_has_mig_number(
4928 &self,
4929 group_path: &[&str],
4930 instance_index: usize,
4931 mig_number: &str,
4932 ) -> bool {
4933 if group_path != ["SG4", "SG8"] {
4934 return true;
4935 }
4936 match (instance_index, mig_number) {
4937 (0, "00115") => true,
4938 (0, _) => false,
4939 (1, "00171") => true,
4940 (1, _) => false,
4941 _ => false,
4942 }
4943 }
4944 }
4945
4946 let seq_z01 = OwnedSegment {
4947 id: "SEQ".into(),
4948 elements: vec![vec!["Z01".into()], vec!["1".into()]],
4949 segment_number: 10,
4950 };
4951 let seq_z45 = OwnedSegment {
4952 id: "SEQ".into(),
4953 elements: vec![vec!["Z45".into()], vec!["1".into()]],
4954 segment_number: 20,
4955 };
4956 let nav = TwoSg8Variants {
4957 seq_z01: seq_z01.clone(),
4958 seq_z45: seq_z45.clone(),
4959 };
4960
4961 let evaluator = MockEvaluator::all_true(&[]);
4962 let validator = EdifactValidator::new(evaluator);
4963 let external = NoOpExternalProvider;
4964
4965 let workflow = AhbWorkflow {
4966 pruefidentifikator: "55218".to_string(),
4967 description: "Test".to_string(),
4968 communication_direction: None,
4969 ub_definitions: BTreeMap::new(),
4970 fields: vec![AhbFieldRule {
4971 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4972 name: "Handlung, Code".to_string(),
4973 ahb_status: "X".to_string(),
4974 element_index: Some(0),
4975 component_index: Some(0),
4976 codes: vec![
4977 AhbCodeRule {
4978 value: "Z45".into(),
4979 description: "NNA".into(),
4980 ahb_status: "X [1P1..4294967295]".into(),
4981 },
4982 AhbCodeRule {
4983 value: "Z84".into(),
4984 description: "Differenz-NNA".into(),
4985 ahb_status: "X [1P0..4294967295]".into(),
4986 },
4987 ],
4988 parent_group_ahb_status: Some("Muss".to_string()),
4989 segment_ahb_status: None,
4990 mig_number: Some("00171".to_string()),
4991 format: None,
4992 }],
4993 };
4994
4995 let report = validator.validate_with_navigator(
4996 &[seq_z01, seq_z45],
4997 &workflow,
4998 &external,
4999 ValidationLevel::Full,
5000 &nav,
5001 );
5002 let pkg_errors: Vec<_> = report
5003 .by_category(ValidationCategory::Ahb)
5004 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5005 .collect();
5006 assert!(
5007 pkg_errors.is_empty(),
5008 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
5009 pkg_errors
5010 );
5011 }
5012
5013 #[test]
5014 fn test_unt_count_correct() {
5015 let segments = vec![
5017 make_segment("UNH", vec![vec!["001"]]),
5018 make_segment("BGM", vec![vec!["E01"]]),
5019 make_segment("DTM", vec![vec!["137", "20250401"]]),
5020 make_segment("UNT", vec![vec!["4", "001"]]),
5021 ];
5022 assert!(
5023 validate_unt_segment_count(&segments).is_none(),
5024 "Correct count should produce no issue"
5025 );
5026 }
5027
5028 #[test]
5029 fn test_unt_count_mismatch() {
5030 let segments = vec![
5032 make_segment("UNH", vec![vec!["001"]]),
5033 make_segment("BGM", vec![vec!["E01"]]),
5034 make_segment("UNT", vec![vec!["5", "001"]]),
5035 ];
5036 let issue =
5037 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
5038 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5039 assert_eq!(issue.severity, Severity::Error);
5040 assert!(narrate(&issue).contains("declared 5"));
5041 assert!(narrate(&issue).contains("actual 3"));
5042 }
5043
5044 #[test]
5045 fn test_unt_count_excludes_envelope() {
5046 let segments = vec![
5048 make_segment("UNA", vec![]),
5049 make_segment("UNB", vec![vec!["UNOC", "3"]]),
5050 make_segment("UNH", vec![vec!["001"]]),
5051 make_segment("BGM", vec![vec!["E01"]]),
5052 make_segment("UNT", vec![vec!["3", "001"]]),
5053 make_segment("UNZ", vec![vec!["1"]]),
5054 ];
5055 assert!(
5056 validate_unt_segment_count(&segments).is_none(),
5057 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
5058 );
5059 }
5060
5061 #[test]
5062 fn test_unt_count_no_unt_returns_none() {
5063 let segments = vec![
5064 make_segment("UNH", vec![vec!["001"]]),
5065 make_segment("BGM", vec![vec!["E01"]]),
5066 ];
5067 assert!(
5068 validate_unt_segment_count(&segments).is_none(),
5069 "No UNT segment should return None (not our problem)"
5070 );
5071 }
5072
5073 #[test]
5074 fn test_unt_count_rejects_multi_message_input() {
5075 let segments = vec![
5077 make_segment("UNH", vec![vec!["001"]]),
5078 make_segment("BGM", vec![vec!["E01"]]),
5079 make_segment("UNT", vec![vec!["3", "001"]]),
5080 make_segment("UNH", vec![vec!["002"]]),
5081 make_segment("BGM", vec![vec!["E02"]]),
5082 make_segment("UNT", vec![vec!["3", "002"]]),
5083 ];
5084 let issue = validate_unt_segment_count(&segments)
5085 .expect("Multi-message input should produce an error");
5086 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5087 assert!(
5088 narrate(&issue).contains("2 UNH"),
5089 "Should mention UNH count: {}",
5090 narrate(&issue)
5091 );
5092 }
5093
5094 #[test]
5095 fn test_code_validation_accepts_multi_code_variant_qualifier() {
5096 let evaluator = MockEvaluator::new(vec![]);
5103 let validator = EdifactValidator::new(evaluator);
5104 let external = NoOpExternalProvider;
5105
5106 let rff_z39 = OwnedSegment {
5107 id: "RFF".to_string(),
5108 elements: vec![
5109 vec!["RFF".to_string()],
5110 vec!["Z39".to_string(), "REF1".to_string()],
5111 ],
5112 segment_number: 1,
5113 };
5114
5115 let workflow = AhbWorkflow {
5116 pruefidentifikator: "55035".to_string(),
5117 description: "Test".to_string(),
5118 communication_direction: None,
5119 fields: vec![
5120 AhbFieldRule {
5122 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5123 name: "Referenznummer Qualifier".to_string(),
5124 ahb_status: "Muss".to_string(),
5125 codes: vec![
5126 AhbCodeRule {
5127 value: "Z31".to_string(),
5128 description: "".to_string(),
5129 ahb_status: "X".to_string(),
5130 },
5131 AhbCodeRule {
5132 value: "Z39".to_string(),
5133 description: "".to_string(),
5134 ahb_status: "X".to_string(),
5135 },
5136 ],
5137 parent_group_ahb_status: None,
5138 segment_ahb_status: None,
5139 element_index: Some(1),
5140 component_index: Some(0),
5141 mig_number: Some("00075".to_string()),
5142 format: None,
5143 },
5144 AhbFieldRule {
5146 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5147 name: "Referenznummer Qualifier".to_string(),
5148 ahb_status: "Muss".to_string(),
5149 codes: vec![AhbCodeRule {
5150 value: "Z33".to_string(),
5151 description: "".to_string(),
5152 ahb_status: "X".to_string(),
5153 }],
5154 parent_group_ahb_status: None,
5155 segment_ahb_status: None,
5156 element_index: Some(1),
5157 component_index: Some(0),
5158 mig_number: Some("00078".to_string()),
5159 format: None,
5160 },
5161 ],
5162 ub_definitions: BTreeMap::new(),
5163 };
5164
5165 let report = validator.validate(
5166 &[rff_z39],
5167 &workflow,
5168 &external,
5169 ValidationLevel::Conditions,
5170 );
5171
5172 let code_errors: Vec<_> = report
5173 .by_category(ValidationCategory::Code)
5174 .filter(|i| {
5175 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5176 })
5177 .collect();
5178 assert!(
5179 code_errors.is_empty(),
5180 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
5181 code_errors
5182 );
5183 }
5184
5185 #[test]
5186 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
5187 let evaluator = MockEvaluator::new(vec![]);
5193 let validator = EdifactValidator::new(evaluator);
5194 let external = NoOpExternalProvider;
5195
5196 let pia_5_z12 = OwnedSegment {
5200 id: "PIA".to_string(),
5201 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
5202 segment_number: 1,
5203 };
5204 let pia_5_srw = OwnedSegment {
5205 id: "PIA".to_string(),
5206 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
5207 segment_number: 2,
5208 };
5209
5210 let make_rules = |mig: &str, composite_code: &str| {
5211 vec![
5212 AhbFieldRule {
5213 segment_path: "SG4/SG8/PIA/4347".to_string(),
5214 name: "Produkt-ID-Funktion".to_string(),
5215 ahb_status: "Muss".to_string(),
5216 codes: vec![AhbCodeRule {
5217 value: "5".to_string(),
5218 description: "".to_string(),
5219 ahb_status: "X".to_string(),
5220 }],
5221 parent_group_ahb_status: None,
5222 segment_ahb_status: None,
5223 element_index: Some(0),
5224 component_index: Some(0),
5225 mig_number: Some(mig.to_string()),
5226 format: None,
5227 },
5228 AhbFieldRule {
5229 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
5230 name: "Artikel/Dienstleistung-Identifikator".to_string(),
5231 ahb_status: "Muss".to_string(),
5232 codes: vec![AhbCodeRule {
5233 value: composite_code.to_string(),
5234 description: "".to_string(),
5235 ahb_status: "X".to_string(),
5236 }],
5237 parent_group_ahb_status: None,
5238 segment_ahb_status: None,
5239 element_index: Some(1),
5240 component_index: Some(0),
5241 mig_number: Some(mig.to_string()),
5242 format: None,
5243 },
5244 ]
5245 };
5246
5247 let mut fields = make_rules("00108", "Z12");
5248 fields.extend(make_rules("00197", "SRW"));
5249
5250 let workflow = AhbWorkflow {
5251 pruefidentifikator: "55035".to_string(),
5252 description: "Test".to_string(),
5253 communication_direction: None,
5254 fields,
5255 ub_definitions: BTreeMap::new(),
5256 };
5257
5258 let report = validator.validate(
5259 &[pia_5_z12, pia_5_srw],
5260 &workflow,
5261 &external,
5262 ValidationLevel::Conditions,
5263 );
5264
5265 let code_errors: Vec<_> = report
5266 .by_category(ValidationCategory::Code)
5267 .filter(|i| {
5268 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5269 })
5270 .collect();
5271 assert!(
5272 code_errors.is_empty(),
5273 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
5274 code_errors
5275 );
5276 }
5277}