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 max_length(format: &str) -> Option<usize> {
1232 let digits = format
1233 .trim()
1234 .strip_prefix("an")
1235 .or_else(|| format.trim().strip_prefix('a'))
1236 .or_else(|| format.trim().strip_prefix('n'))?;
1237 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1238}
1239
1240fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1250 let derived_from_codes = field
1251 .codes
1252 .first()
1253 .is_some_and(|c| c.ahb_status == field.ahb_status);
1254 if !derived_from_codes {
1255 return &field.ahb_status;
1256 }
1257 value
1258 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1259 .map_or(&field.ahb_status, |c| &c.ahb_status)
1260}
1261
1262fn should_skip_for_parent_group<E: ConditionEvaluator>(
1268 field: &AhbFieldRule,
1269 expr_eval: &ConditionExprEvaluator<E>,
1270 ctx: &EvaluationContext,
1271 ub_definitions: &BTreeMap<String, ConditionExpr>,
1272) -> bool {
1273 if let Some(ref group_status) = field.parent_group_ahb_status {
1274 if group_status.contains('[') {
1275 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
1276 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
1277 }
1278 }
1279 false
1280}
1281
1282fn is_parent_group_required<E: ConditionEvaluator>(
1289 field: &AhbFieldRule,
1290 expr_eval: &ConditionExprEvaluator<E>,
1291 ctx: &EvaluationContext,
1292 ub_definitions: &BTreeMap<String, ConditionExpr>,
1293) -> bool {
1294 field
1295 .parent_group_ahb_status
1296 .as_deref()
1297 .is_some_and(|status| {
1298 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1299 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1300 })
1301}
1302
1303fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1312 let segment_id = extract_segment_id(&field.segment_path);
1313
1314 if !field.codes.is_empty() {
1320 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1321 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1322 let matching = ctx.find_segments(&segment_id);
1323 return matching.iter().any(|seg| {
1324 seg.elements
1325 .get(el_idx)
1326 .and_then(|e| e.get(comp_idx))
1327 .is_some_and(|v| required_codes.contains(&v.as_str()))
1328 });
1329 }
1330 if is_qualifier_field(&field.segment_path) {
1333 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1334 let el_idx = field.element_index.unwrap_or(0);
1335 let comp_idx = field.component_index.unwrap_or(0);
1336 let matching = ctx.find_segments(&segment_id);
1337 return matching.iter().any(|seg| {
1338 seg.elements
1339 .get(el_idx)
1340 .and_then(|e| e.get(comp_idx))
1341 .is_some_and(|v| required_codes.contains(&v.as_str()))
1342 });
1343 }
1344 }
1345
1346 ctx.has_segment(&segment_id)
1347}
1348
1349fn is_group_variant_absent(
1364 ctx: &EvaluationContext,
1365 field: &AhbFieldRule,
1366 is_group_mandatory: bool,
1367) -> bool {
1368 let group_path: Vec<&str> = field
1369 .segment_path
1370 .split('/')
1371 .take_while(|p| p.starts_with("SG"))
1372 .collect();
1373
1374 if group_path.is_empty() {
1375 return false;
1376 }
1377
1378 let nav = match ctx.navigator {
1379 Some(nav) => nav,
1380 None => return false,
1381 };
1382
1383 let instance_count = nav.group_instance_count(&group_path);
1384
1385 if instance_count == 0 {
1390 if !is_group_mandatory {
1391 return true;
1392 }
1393 return false;
1395 }
1396
1397 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1402 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1404 let segment_id = extract_segment_id(&field.segment_path);
1405 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1406
1407 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1408 nav.find_segments_in_group(&segment_id, &group_path, i)
1409 .iter()
1410 .any(|seg| {
1411 seg.elements
1412 .first()
1413 .and_then(|e| e.first())
1414 .is_some_and(|v| required_codes.contains(&v.as_str()))
1415 })
1416 });
1417
1418 if !any_instance_has_qualifier {
1419 return true; }
1421 }
1422 }
1423
1424 let segment_id = extract_segment_id(&field.segment_path);
1433 let segment_absent_from_all = (0..instance_count).all(|i| {
1434 nav.find_segments_in_group(&segment_id, &group_path, i)
1435 .is_empty()
1436 });
1437 if segment_absent_from_all {
1438 let group_has_other_segments =
1439 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1440 if group_has_other_segments {
1441 return true;
1442 }
1443 }
1444
1445 false
1446}
1447
1448fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1450 out.extend(group.fields.iter());
1451 for child in &group.children {
1452 collect_nodes_depth_first(child, out);
1453 }
1454}
1455
1456#[allow(clippy::too_many_arguments)]
1460fn evaluate_node<E: ConditionEvaluator>(
1461 node: &AhbNode,
1462 ctx: &EvaluationContext,
1463 expr_eval: &ConditionExprEvaluator<E>,
1464 evaluator: &E,
1465 ub_definitions: &BTreeMap<String, ConditionExpr>,
1466 tag_migs: &HashMap<String, HashSet<&str>>,
1467 instance_index: Option<usize>,
1468 report: &mut ValidationReport,
1469) {
1470 let field = node.rule;
1471
1472 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1474
1475 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1480 let length = value.chars().count();
1481 if length > max {
1482 let mut issue = ValidationIssue::new(
1483 Severity::Error,
1484 IssueKind::ValueTooLong {
1485 field_name: field.name.clone(),
1486 length,
1487 max,
1488 },
1489 )
1490 .with_field_path(&field.segment_path)
1491 .with_actual(value)
1492 .with_expected(field.format.clone().unwrap_or_default());
1493 if let Some(idx) = instance_index {
1494 issue = issue.with_instance_index(idx);
1495 }
1496 if let Some(num) = node.matched_segment_number {
1497 issue = issue.with_position(crate::SegmentPosition {
1498 segment_number: num,
1499 byte_offset: 0,
1500 message_number: 1,
1501 });
1502 }
1503 report.add_issue(attach_field_position(issue, field));
1504 }
1505 }
1506
1507 if should_skip_for_parent_group(field, expr_eval, ctx, ub_definitions) {
1509 return;
1510 }
1511
1512 let status = governing_status(field, node.value);
1515 let StatusResolution {
1516 result: condition_result,
1517 kind,
1518 unknown_ids,
1519 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1520 let mandatory = kind == Some(StatusKind::Muss);
1521
1522 match condition_result {
1523 ConditionResult::True => {
1524 if mandatory && node.value.is_none() {
1533 let tag = extract_segment_id(&field.segment_path);
1534 let single_variant_present = node.segment_elements.is_none()
1535 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1536 && is_field_present(ctx, field);
1537 let segment_optional_and_absent = node.segment_elements.is_none()
1542 && field
1543 .segment_ahb_status
1544 .as_deref()
1545 .is_some_and(is_optional_segment_status);
1546 let segment_not_required_and_absent = node.segment_elements.is_none()
1552 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1553 expr_eval
1554 .resolve_status(st, &node_ctx, ub_definitions)
1555 .result
1556 != ConditionResult::True
1557 });
1558 if !single_variant_present
1559 && !segment_optional_and_absent
1560 && !segment_not_required_and_absent
1561 {
1562 let mut issue = ValidationIssue::new(
1563 Severity::Error,
1564 IssueKind::MissingRequiredField {
1565 field_name: field.name.clone(),
1566 },
1567 )
1568 .with_field_path(&field.segment_path)
1569 .with_rule(&field.ahb_status);
1570 if let Some(first_code) = field.codes.first() {
1571 issue.expected_value = Some(first_code.value.clone());
1572 }
1573 if let Some(idx) = instance_index {
1574 issue = issue.with_instance_index(idx);
1575 }
1576 if let Some(num) = node.matched_segment_number {
1577 issue = issue.with_position(crate::SegmentPosition {
1578 segment_number: num,
1579 byte_offset: 0,
1580 message_number: 1,
1581 });
1582 }
1583 issue = attach_field_position(issue, field);
1584 report.add_issue(issue);
1585 }
1586 }
1587 }
1588 ConditionResult::False => {
1589 if mandatory && node.value.is_some() {
1592 let mut issue = ValidationIssue::new(
1593 Severity::Error,
1594 IssueKind::FieldConditionNotSatisfied {
1595 field_name: field.name.clone(),
1596 },
1597 )
1598 .with_field_path(&field.segment_path)
1599 .with_rule(&field.ahb_status);
1600 if let Some(idx) = instance_index {
1601 issue = issue.with_instance_index(idx);
1602 }
1603 if let Some(num) = node.matched_segment_number {
1604 issue = issue.with_position(crate::SegmentPosition {
1605 segment_number: num,
1606 byte_offset: 0,
1607 message_number: 1,
1608 });
1609 }
1610 issue = attach_field_position(issue, field);
1611 report.add_issue(issue);
1612 }
1613 }
1614 ConditionResult::Unknown => {
1615 let mut external_ids = Vec::new();
1617 let mut undetermined_ids = Vec::new();
1618 let mut missing_ids = Vec::new();
1619 for id in unknown_ids {
1620 if evaluator.is_external(id) {
1621 external_ids.push(id);
1622 } else if evaluator.is_known(id) {
1623 undetermined_ids.push(id);
1624 } else {
1625 missing_ids.push(id);
1626 }
1627 }
1628
1629 let mut issue = ValidationIssue::new(
1630 Severity::Info,
1631 IssueKind::ConditionUnknown {
1632 field_name: field.name.clone(),
1633 unresolved: UnresolvedConditions {
1634 external: external_ids,
1635 undetermined: undetermined_ids,
1636 missing: missing_ids,
1637 },
1638 },
1639 )
1640 .with_field_path(&field.segment_path)
1641 .with_rule(&field.ahb_status);
1642 if let Some(idx) = instance_index {
1643 issue = issue.with_instance_index(idx);
1644 }
1645 if let Some(num) = node.matched_segment_number {
1646 issue = issue.with_position(crate::SegmentPosition {
1647 segment_number: num,
1648 byte_offset: 0,
1649 message_number: 1,
1650 });
1651 }
1652 issue = attach_field_position(issue, field);
1653 report.add_issue(issue);
1654 }
1655 }
1656}
1657
1658fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1663 match field.element_index {
1664 Some(el) => {
1665 let element_pos = (el as u32) + 2;
1666 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1667 issue.with_field_position(element_pos, component_pos)
1668 }
1669 None => issue,
1670 }
1671}
1672
1673fn refuse_segments<E: ConditionEvaluator>(
1679 group: &AhbGroupNode,
1680 path: &[(&str, usize)],
1681 scoped_ctx: &EvaluationContext,
1682 evaluator: &E,
1683 ub_definitions: &BTreeMap<String, ConditionExpr>,
1684 instance_index: usize,
1685 report: &mut ValidationReport,
1686) {
1687 let mut seen: Vec<u32> = Vec::new();
1688 for node in &group.fields {
1689 let rule = node.rule;
1690 let (Some(status), Some(value), Some(number)) = (
1691 rule.segment_ahb_status.as_deref(),
1692 node.value,
1693 node.matched_segment_number,
1694 ) else {
1695 continue;
1696 };
1697 let qualifier =
1700 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
1701 if !qualifier
1702 || seen.contains(&number)
1703 || Some(status) == group.ahb_status
1704 || !rule.codes.iter().any(|c| c.value == value)
1705 {
1706 continue;
1707 }
1708 seen.push(number);
1709 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1710 continue;
1711 }
1712 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1713 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
1714 report.add_issue(
1715 ValidationIssue::new(
1716 Severity::Warning,
1717 IssueKind::SegmentNotAllowed {
1718 segment: segment.join("/"),
1719 },
1720 )
1721 .with_field_path(segment.join("/"))
1722 .with_rule(status)
1723 .with_instance_index(instance_index)
1724 .with_position(crate::SegmentPosition {
1725 segment_number: number,
1726 byte_offset: 0,
1727 message_number: 1,
1728 }),
1729 );
1730 }
1731}
1732
1733#[allow(clippy::too_many_arguments)]
1742fn walk_group_instance<E: ConditionEvaluator>(
1743 group: &AhbGroupNode,
1744 path: &[(&str, usize)],
1745 scoped_ctx: &EvaluationContext,
1746 expr_eval: &ConditionExprEvaluator<E>,
1747 evaluator: &E,
1748 ub_definitions: &BTreeMap<String, ConditionExpr>,
1749 tag_migs: &HashMap<String, HashSet<&str>>,
1750 instance_index: usize,
1751 report_segments: bool,
1752 report: &mut ValidationReport,
1753) {
1754 let mut group_refused = false;
1757 if let Some(status) = group.ahb_status {
1758 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1759 group_refused = true;
1760 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1761 let mut issue = ValidationIssue::new(
1762 Severity::Warning,
1763 IssueKind::GroupNotAllowed {
1764 group: group_path.join("/"),
1765 },
1766 )
1767 .with_field_path(group_path.join("/"))
1768 .with_rule(status)
1769 .with_instance_index(instance_index);
1770 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
1771 issue = issue.with_position(crate::SegmentPosition {
1772 segment_number: num,
1773 byte_offset: 0,
1774 message_number: 1,
1775 });
1776 }
1777 report.add_issue(issue);
1778 }
1779 }
1780 if report_segments && !group_refused {
1781 refuse_segments(
1782 group,
1783 path,
1784 scoped_ctx,
1785 evaluator,
1786 ub_definitions,
1787 instance_index,
1788 report,
1789 );
1790 }
1791 for node in &group.fields {
1792 evaluate_node(
1793 node,
1794 scoped_ctx,
1795 expr_eval,
1796 evaluator,
1797 ub_definitions,
1798 tag_migs,
1799 Some(instance_index),
1800 report,
1801 );
1802 }
1803 for child in &group.children {
1804 let mut child_path = path.to_vec();
1805 child_path.push((child.group_id, child.instance_index));
1806 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
1807 walk_group_instance(
1808 child,
1809 &child_path,
1810 &child_ctx,
1811 expr_eval,
1812 evaluator,
1813 ub_definitions,
1814 tag_migs,
1815 instance_index,
1816 report_segments,
1817 report,
1818 );
1819 }
1820}
1821
1822fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
1824 match expr {
1825 ConditionExpr::Package { id, min, max } => {
1826 out.push((*id, *min, *max));
1827 }
1828 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1829 for e in exprs {
1830 collect_packages(e, out);
1831 }
1832 }
1833 ConditionExpr::Xor(left, right) => {
1834 collect_packages(left, out);
1835 collect_packages(right, out);
1836 }
1837 ConditionExpr::Not(inner) => {
1838 collect_packages(inner, out);
1839 }
1840 ConditionExpr::Ref(_) => {}
1841 }
1842}
1843
1844fn is_optional_segment_status(status: &str) -> bool {
1854 let trimmed = status.trim();
1855 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
1856}
1857
1858fn is_qualifier_field(path: &str) -> bool {
1868 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
1869 matches!(parts.len(), 2 | 3)
1871}
1872
1873#[derive(Default)]
1881struct MigCodeBucket<'a> {
1882 qualifier_position: Option<(usize, usize)>,
1885 qualifier_values: HashSet<&'a str>,
1888 codes: HashMap<(usize, usize), HashSet<&'a str>>,
1891}
1892
1893fn partition_codes_by_mig(
1898 workflow: &AhbWorkflow,
1899) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
1900 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
1901 for field in &workflow.fields {
1902 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
1903 continue;
1904 }
1905 let tag = extract_segment_id(&field.segment_path);
1906 let group_key = extract_group_path_key(&field.segment_path);
1907 let mig = field.mig_number.clone();
1908 let el = field.element_index.unwrap_or(0);
1909 let c = field.component_index.unwrap_or(0);
1910
1911 let bucket = out
1912 .entry((group_key, tag))
1913 .or_default()
1914 .entry(mig)
1915 .or_default();
1916
1917 let required: Vec<&str> = field
1924 .codes
1925 .iter()
1926 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
1927 .map(|code| code.value.as_str())
1928 .collect();
1929
1930 if !required.is_empty() {
1937 if bucket.qualifier_position.is_none() {
1938 bucket.qualifier_position = Some((el, c));
1939 }
1940 if bucket.qualifier_position == Some((el, c)) {
1941 bucket.qualifier_values.extend(required.iter().copied());
1942 }
1943 }
1944
1945 for v in required {
1946 bucket.codes.entry((el, c)).or_default().insert(v);
1947 }
1948 }
1949 out
1950}
1951
1952fn global_qualifiers_by_tag(
1958 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
1959) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
1960 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
1961 for ((_group, tag), migs) in by_loc {
1962 let tag_entry = out.entry(tag.clone()).or_default();
1963 for bucket in migs.values() {
1964 if let Some(pos) = bucket.qualifier_position {
1965 tag_entry
1966 .entry(pos)
1967 .or_default()
1968 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
1969 }
1970 }
1971 }
1972 out
1973}
1974
1975fn match_segment_to_mig<'a, 'b>(
1986 seg: &OwnedSegment,
1987 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
1988) -> Option<&'a MigCodeBucket<'b>> {
1989 let actual_at = |el: usize, c: usize| -> &str {
1990 seg.elements
1991 .get(el)
1992 .and_then(|e| e.get(c))
1993 .map(|s| s.as_str())
1994 .unwrap_or("")
1995 };
1996
1997 let mut best: Option<&MigCodeBucket> = None;
1998 let mut best_matches = 0usize;
1999
2000 for bucket in migs.values() {
2001 let Some((el, c)) = bucket.qualifier_position else {
2002 continue;
2003 };
2004 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2005 continue;
2006 }
2007 let extra_matches = bucket
2008 .codes
2009 .iter()
2010 .filter(|(pos, _)| **pos != (el, c))
2011 .filter(|((e, k), allowed)| {
2012 let v = actual_at(*e, *k);
2013 !v.is_empty() && allowed.contains(v)
2014 })
2015 .count();
2016 if best.is_none() || extra_matches > best_matches {
2017 best = Some(bucket);
2018 best_matches = extra_matches;
2019 }
2020 }
2021 best
2022}
2023
2024fn extract_group_path_key(path: &str) -> String {
2029 let sg_parts: Vec<&str> = path
2030 .split('/')
2031 .take_while(|p| p.starts_with("SG"))
2032 .collect();
2033 sg_parts.join("/")
2034}
2035
2036fn extract_segment_id(path: &str) -> String {
2038 for part in path.split('/') {
2039 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2041 continue;
2042 }
2043 if part.len() >= 3
2045 && part
2046 .chars()
2047 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2048 {
2049 return part.to_string();
2050 }
2051 }
2052 path.split('/').next_back().unwrap_or(path).to_string()
2054}
2055
2056pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2068 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2070 if unh_count > 1 {
2071 return Some(ValidationIssue::new(
2072 Severity::Error,
2073 IssueKind::UntCountNotVerifiable { unh_count },
2074 ));
2075 }
2076
2077 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2079 let declared: usize = unt.get_element(0).parse().ok()?;
2080
2081 let actual = segments
2083 .iter()
2084 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2085 .count();
2086
2087 if declared != actual {
2088 Some(
2089 ValidationIssue::new(
2090 Severity::Error,
2091 IssueKind::UntSegmentCountMismatch { declared, actual },
2092 )
2093 .with_field_path("UNT/0074")
2094 .with_expected(actual.to_string())
2095 .with_actual(declared.to_string()),
2096 )
2097 } else {
2098 None
2099 }
2100}
2101
2102#[cfg(test)]
2103mod tests {
2104 use super::super::codes::ErrorCodes;
2105 use super::super::issue::ValidationCategory;
2106 use super::*;
2107 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2108 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2109 use std::collections::HashMap;
2110
2111 fn narrate(issue: &ValidationIssue) -> String {
2114 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2115 }
2116
2117 struct MockEvaluator {
2119 results: HashMap<u32, CR>,
2120 }
2121
2122 impl MockEvaluator {
2123 fn new(results: Vec<(u32, CR)>) -> Self {
2124 Self {
2125 results: results.into_iter().collect(),
2126 }
2127 }
2128
2129 fn all_true(ids: &[u32]) -> Self {
2130 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2131 }
2132 }
2133
2134 impl ConditionEvaluator for MockEvaluator {
2135 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2136 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2137 }
2138 fn is_external(&self, _condition: u32) -> bool {
2139 false
2140 }
2141 fn message_type(&self) -> &str {
2142 "UTILMD"
2143 }
2144 fn format_version(&self) -> &str {
2145 "FV2510"
2146 }
2147 }
2148
2149 #[test]
2152 fn a_present_code_is_judged_by_its_own_status() {
2153 let code = |value: &str, status: &str| AhbCodeRule {
2155 value: value.to_string(),
2156 description: String::new(),
2157 ahb_status: status.to_string(),
2158 };
2159 let field = AhbFieldRule {
2160 segment_path: "SG4/STS/C556/9013".to_string(),
2161 ahb_status: "X [192]".to_string(),
2162 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2163 ..Default::default()
2164 };
2165 assert_eq!(governing_status(&field, Some("E03")), "X");
2166 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2167 assert_eq!(governing_status(&field, None), "X [192]");
2168 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2169
2170 let own = AhbFieldRule {
2172 ahb_status: "X [931]".to_string(),
2173 codes: vec![code("303", "X")],
2174 ..Default::default()
2175 };
2176 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2177 }
2178
2179 #[test]
2180 fn test_extract_segment_id_simple() {
2181 assert_eq!(extract_segment_id("NAD"), "NAD");
2182 }
2183
2184 #[test]
2185 fn test_extract_segment_id_with_sg_prefix() {
2186 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
2187 }
2188
2189 #[test]
2190 fn test_extract_segment_id_nested_sg() {
2191 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
2192 }
2193
2194 #[test]
2197 fn test_validate_missing_mandatory_field() {
2198 let evaluator = MockEvaluator::all_true(&[182, 152]);
2199 let validator = EdifactValidator::new(evaluator);
2200 let external = NoOpExternalProvider;
2201
2202 let workflow = AhbWorkflow {
2203 pruefidentifikator: "11001".to_string(),
2204 description: "Test".to_string(),
2205 communication_direction: None,
2206 fields: vec![AhbFieldRule {
2207 segment_path: "SG2/NAD/C082/3039".to_string(),
2208 name: "MP-ID des MSB".to_string(),
2209 ahb_status: "Muss [182] ∧ [152]".to_string(),
2210 codes: vec![],
2211 parent_group_ahb_status: None,
2212 segment_ahb_status: None,
2213 ..Default::default()
2214 }],
2215 ub_definitions: BTreeMap::new(),
2216 };
2217
2218 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2220
2221 assert!(!report.is_valid());
2223 let errors: Vec<_> = report.errors().collect();
2224 assert_eq!(errors.len(), 1);
2225 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2226 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
2227 }
2228
2229 #[test]
2230 fn test_validate_condition_false_no_error() {
2231 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
2233 let validator = EdifactValidator::new(evaluator);
2234 let external = NoOpExternalProvider;
2235
2236 let workflow = AhbWorkflow {
2237 pruefidentifikator: "11001".to_string(),
2238 description: "Test".to_string(),
2239 communication_direction: None,
2240 fields: vec![AhbFieldRule {
2241 segment_path: "NAD".to_string(),
2242 name: "Partnerrolle".to_string(),
2243 ahb_status: "Muss [182] ∧ [152]".to_string(),
2244 codes: vec![],
2245 parent_group_ahb_status: None,
2246 segment_ahb_status: None,
2247 ..Default::default()
2248 }],
2249 ub_definitions: BTreeMap::new(),
2250 };
2251
2252 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2253
2254 assert!(report.is_valid());
2256 }
2257
2258 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
2260 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
2261 let workflow = AhbWorkflow {
2262 pruefidentifikator: "11001".to_string(),
2263 description: "Test".to_string(),
2264 communication_direction: None,
2265 fields: vec![AhbFieldRule {
2266 segment_path: "SG2/NAD/C082/3039".to_string(),
2267 name: "MP-ID".to_string(),
2268 ahb_status: status.to_string(),
2269 ..Default::default()
2270 }],
2271 ub_definitions: BTreeMap::new(),
2272 };
2273 validator.validate(
2274 &[],
2275 &workflow,
2276 &NoOpExternalProvider,
2277 ValidationLevel::Conditions,
2278 )
2279 }
2280
2281 #[test]
2282 fn a_later_soll_line_alone_does_not_require_the_field() {
2283 let report = validate_absent_field(
2285 "Muss [2119] \r\nSoll [130]",
2286 vec![(2119, CR::False), (130, CR::True)],
2287 );
2288 assert!(
2289 report.is_valid(),
2290 "{:?}",
2291 report.errors().collect::<Vec<_>>()
2292 );
2293 }
2294
2295 #[test]
2296 fn a_later_muss_line_requires_the_field() {
2297 let report = validate_absent_field(
2299 "Soll [47]\r\nMuss [46]",
2300 vec![(47, CR::False), (46, CR::True)],
2301 );
2302 let errors: Vec<_> = report.errors().collect();
2303 assert_eq!(errors.len(), 1);
2304 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2305 }
2306
2307 #[test]
2308 fn a_weaker_unknown_line_does_not_stop_a_muss() {
2309 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
2311 let errors: Vec<_> = report.errors().collect();
2312 assert_eq!(errors.len(), 1);
2313 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2314 }
2315
2316 #[test]
2317 fn a_stricter_unknown_line_leaves_the_status_undecided() {
2318 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
2319 assert!(report.is_valid());
2320 let infos: Vec<_> = report.infos().collect();
2321 assert_eq!(infos.len(), 1, "{infos:?}");
2322 match &infos[0].kind {
2323 IssueKind::ConditionUnknown { unresolved, .. } => {
2324 assert_eq!(unresolved.missing, vec![2119]);
2325 }
2326 other => panic!("expected ConditionUnknown, got {other:?}"),
2327 }
2328 }
2329
2330 #[test]
2331 fn test_validate_condition_unknown_adds_info() {
2332 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
2334 let validator = EdifactValidator::new(evaluator);
2336 let external = NoOpExternalProvider;
2337
2338 let workflow = AhbWorkflow {
2339 pruefidentifikator: "11001".to_string(),
2340 description: "Test".to_string(),
2341 communication_direction: None,
2342 fields: vec![AhbFieldRule {
2343 segment_path: "NAD".to_string(),
2344 name: "Partnerrolle".to_string(),
2345 ahb_status: "Muss [182] ∧ [152]".to_string(),
2346 codes: vec![],
2347 parent_group_ahb_status: None,
2348 segment_ahb_status: None,
2349 ..Default::default()
2350 }],
2351 ub_definitions: BTreeMap::new(),
2352 };
2353
2354 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2355
2356 assert!(report.is_valid());
2358 let infos: Vec<_> = report.infos().collect();
2359 assert_eq!(infos.len(), 1);
2360 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
2361 }
2362
2363 #[test]
2364 fn test_validate_structure_level_skips_conditions() {
2365 let evaluator = MockEvaluator::all_true(&[182, 152]);
2366 let validator = EdifactValidator::new(evaluator);
2367 let external = NoOpExternalProvider;
2368
2369 let workflow = AhbWorkflow {
2370 pruefidentifikator: "11001".to_string(),
2371 description: "Test".to_string(),
2372 communication_direction: None,
2373 fields: vec![AhbFieldRule {
2374 segment_path: "NAD".to_string(),
2375 name: "Partnerrolle".to_string(),
2376 ahb_status: "Muss [182] ∧ [152]".to_string(),
2377 codes: vec![],
2378 parent_group_ahb_status: None,
2379 segment_ahb_status: None,
2380 ..Default::default()
2381 }],
2382 ub_definitions: BTreeMap::new(),
2383 };
2384
2385 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
2387
2388 assert!(report.is_valid());
2390 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
2391 }
2392
2393 #[test]
2394 fn test_validate_empty_workflow_no_condition_errors() {
2395 let evaluator = MockEvaluator::all_true(&[]);
2396 let validator = EdifactValidator::new(evaluator);
2397 let external = NoOpExternalProvider;
2398
2399 let empty_workflow = AhbWorkflow {
2400 pruefidentifikator: String::new(),
2401 description: String::new(),
2402 communication_direction: None,
2403 fields: vec![],
2404 ub_definitions: BTreeMap::new(),
2405 };
2406
2407 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
2408
2409 assert!(report.is_valid());
2410 }
2411
2412 #[test]
2413 fn test_validate_bare_muss_always_required() {
2414 let evaluator = MockEvaluator::new(vec![]);
2415 let validator = EdifactValidator::new(evaluator);
2416 let external = NoOpExternalProvider;
2417
2418 let workflow = AhbWorkflow {
2419 pruefidentifikator: "55001".to_string(),
2420 description: "Test".to_string(),
2421 communication_direction: Some("NB an LF".to_string()),
2422 fields: vec![AhbFieldRule {
2423 segment_path: "SG2/NAD/3035".to_string(),
2424 name: "Partnerrolle".to_string(),
2425 ahb_status: "Muss".to_string(), codes: vec![],
2427 parent_group_ahb_status: None,
2428 segment_ahb_status: None,
2429 ..Default::default()
2430 }],
2431 ub_definitions: BTreeMap::new(),
2432 };
2433
2434 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2435
2436 assert!(!report.is_valid());
2438 assert_eq!(report.error_count(), 1);
2439 }
2440
2441 #[test]
2442 fn test_validate_x_status_is_mandatory() {
2443 let evaluator = MockEvaluator::new(vec![]);
2444 let validator = EdifactValidator::new(evaluator);
2445 let external = NoOpExternalProvider;
2446
2447 let workflow = AhbWorkflow {
2448 pruefidentifikator: "55001".to_string(),
2449 description: "Test".to_string(),
2450 communication_direction: None,
2451 fields: vec![AhbFieldRule {
2452 segment_path: "DTM".to_string(),
2453 name: "Datum".to_string(),
2454 ahb_status: "X".to_string(),
2455 codes: vec![],
2456 parent_group_ahb_status: None,
2457 segment_ahb_status: None,
2458 ..Default::default()
2459 }],
2460 ub_definitions: BTreeMap::new(),
2461 };
2462
2463 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2464
2465 assert!(!report.is_valid());
2466 let errors: Vec<_> = report.errors().collect();
2467 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2468 }
2469
2470 #[test]
2471 fn test_validate_soll_not_mandatory() {
2472 let evaluator = MockEvaluator::new(vec![]);
2473 let validator = EdifactValidator::new(evaluator);
2474 let external = NoOpExternalProvider;
2475
2476 let workflow = AhbWorkflow {
2477 pruefidentifikator: "55001".to_string(),
2478 description: "Test".to_string(),
2479 communication_direction: None,
2480 fields: vec![AhbFieldRule {
2481 segment_path: "DTM".to_string(),
2482 name: "Datum".to_string(),
2483 ahb_status: "Soll".to_string(),
2484 codes: vec![],
2485 parent_group_ahb_status: None,
2486 segment_ahb_status: None,
2487 ..Default::default()
2488 }],
2489 ub_definitions: BTreeMap::new(),
2490 };
2491
2492 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2493
2494 assert!(report.is_valid());
2496 }
2497
2498 #[test]
2499 fn test_report_includes_metadata() {
2500 let evaluator = MockEvaluator::new(vec![]);
2501 let validator = EdifactValidator::new(evaluator);
2502 let external = NoOpExternalProvider;
2503
2504 let workflow = AhbWorkflow {
2505 pruefidentifikator: "55001".to_string(),
2506 description: String::new(),
2507 communication_direction: None,
2508 fields: vec![],
2509 ub_definitions: BTreeMap::new(),
2510 };
2511
2512 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
2513
2514 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
2515 assert_eq!(report.level, ValidationLevel::Full);
2516 assert_eq!(report.message_type, "UTILMD");
2517 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
2518 }
2519
2520 #[test]
2521 fn test_validate_with_navigator_returns_report() {
2522 let evaluator = MockEvaluator::all_true(&[]);
2523 let validator = EdifactValidator::new(evaluator);
2524 let external = NoOpExternalProvider;
2525 let nav = crate::eval::NoOpGroupNavigator;
2526
2527 let workflow = AhbWorkflow {
2528 pruefidentifikator: "55001".to_string(),
2529 description: "Test".to_string(),
2530 communication_direction: None,
2531 fields: vec![],
2532 ub_definitions: BTreeMap::new(),
2533 };
2534
2535 let report = validator.validate_with_navigator(
2536 &[],
2537 &workflow,
2538 &external,
2539 ValidationLevel::Full,
2540 &nav,
2541 );
2542 assert!(report.is_valid());
2543 }
2544
2545 #[test]
2546 fn test_code_validation_composite_paths_valid_codes() {
2547 let evaluator = MockEvaluator::new(vec![]);
2551 let validator = EdifactValidator::new(evaluator);
2552 let external = NoOpExternalProvider;
2553
2554 let unh_segment = OwnedSegment {
2555 id: "UNH".to_string(),
2556 elements: vec![
2557 vec!["ALEXANDE951842".to_string()],
2558 vec![
2559 "UTILMD".to_string(),
2560 "D".to_string(),
2561 "11A".to_string(),
2562 "UN".to_string(),
2563 "S2.1".to_string(),
2564 ],
2565 ],
2566 segment_number: 1,
2567 };
2568
2569 let workflow = AhbWorkflow {
2570 pruefidentifikator: "55001".to_string(),
2571 description: "Test".to_string(),
2572 communication_direction: None,
2573 fields: vec![
2574 AhbFieldRule {
2575 segment_path: "UNH/S009/0065".to_string(),
2576 name: "Nachrichtentyp".to_string(),
2577 ahb_status: "X".to_string(),
2578 codes: vec![AhbCodeRule {
2579 value: "UTILMD".to_string(),
2580 description: "Stammdaten".to_string(),
2581 ahb_status: "X".to_string(),
2582 }],
2583 parent_group_ahb_status: None,
2584 segment_ahb_status: None,
2585 element_index: Some(1),
2586 component_index: Some(0),
2587 ..Default::default()
2588 },
2589 AhbFieldRule {
2590 segment_path: "UNH/S009/0052".to_string(),
2591 name: "Version".to_string(),
2592 ahb_status: "X".to_string(),
2593 codes: vec![AhbCodeRule {
2594 value: "D".to_string(),
2595 description: "Draft".to_string(),
2596 ahb_status: "X".to_string(),
2597 }],
2598 parent_group_ahb_status: None,
2599 segment_ahb_status: None,
2600 element_index: Some(1),
2601 component_index: Some(1),
2602 ..Default::default()
2603 },
2604 ],
2605 ub_definitions: BTreeMap::new(),
2606 };
2607
2608 let report = validator.validate(
2609 &[unh_segment],
2610 &workflow,
2611 &external,
2612 ValidationLevel::Conditions,
2613 );
2614
2615 let code_errors: Vec<_> = report
2616 .by_category(ValidationCategory::Code)
2617 .filter(|i| i.severity == Severity::Error)
2618 .collect();
2619 assert!(
2620 code_errors.is_empty(),
2621 "Expected no code errors when composite values match allowed codes, got: {:?}",
2622 code_errors
2623 );
2624 }
2625
2626 #[test]
2627 fn test_code_validation_partitions_by_mig_number() {
2628 let evaluator = MockEvaluator::new(vec![]);
2632 let validator = EdifactValidator::new(evaluator);
2633 let external = NoOpExternalProvider;
2634
2635 let sts_7 = OwnedSegment {
2636 id: "STS".to_string(),
2637 elements: vec![
2638 vec!["7".to_string()],
2639 vec![String::new()],
2640 vec!["GH02".to_string()],
2641 vec!["ZW4".to_string()],
2642 ],
2643 segment_number: 1,
2644 };
2645 let sts_e01 = OwnedSegment {
2646 id: "STS".to_string(),
2647 elements: vec![
2648 vec!["E01".to_string()],
2649 vec![String::new()],
2650 vec!["A99".to_string(), "E_0614".to_string()],
2651 ],
2652 segment_number: 2,
2653 };
2654
2655 let workflow = AhbWorkflow {
2656 pruefidentifikator: "55018".to_string(),
2657 description: "Test".to_string(),
2658 communication_direction: None,
2659 fields: vec![
2660 AhbFieldRule {
2662 segment_path: "SG4/STS/C601/9015".to_string(),
2663 name: "Statuskategorie".to_string(),
2664 ahb_status: "X".to_string(),
2665 codes: vec![AhbCodeRule {
2666 value: "7".to_string(),
2667 description: "Transaktionsgrund".to_string(),
2668 ahb_status: "X".to_string(),
2669 }],
2670 parent_group_ahb_status: None,
2671 segment_ahb_status: None,
2672 element_index: Some(0),
2673 component_index: Some(0),
2674 mig_number: Some("00035".to_string()),
2675 format: None,
2676 },
2677 AhbFieldRule {
2678 segment_path: "SG4/STS/C556/9013".to_string(),
2679 name: "Statusanlaß".to_string(),
2680 ahb_status: "X".to_string(),
2681 codes: vec![AhbCodeRule {
2682 value: "E03".to_string(),
2683 description: "Transaktionsgrund".to_string(),
2684 ahb_status: "X".to_string(),
2685 }],
2686 parent_group_ahb_status: None,
2687 segment_ahb_status: None,
2688 element_index: Some(2),
2689 component_index: Some(0),
2690 mig_number: Some("00035".to_string()),
2691 format: None,
2692 },
2693 AhbFieldRule {
2695 segment_path: "SG4/STS/C601/9015".to_string(),
2696 name: "Statuskategorie".to_string(),
2697 ahb_status: "X".to_string(),
2698 codes: vec![AhbCodeRule {
2699 value: "E01".to_string(),
2700 description: "Antwort".to_string(),
2701 ahb_status: "X".to_string(),
2702 }],
2703 parent_group_ahb_status: None,
2704 segment_ahb_status: None,
2705 element_index: Some(0),
2706 component_index: Some(0),
2707 mig_number: Some("00036".to_string()),
2708 format: None,
2709 },
2710 ],
2711 ub_definitions: BTreeMap::new(),
2712 };
2713
2714 let report = validator.validate(
2715 &[sts_7, sts_e01],
2716 &workflow,
2717 &external,
2718 ValidationLevel::Conditions,
2719 );
2720
2721 let code_errors: Vec<_> = report
2722 .by_category(ValidationCategory::Code)
2723 .filter(|i| {
2724 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2725 })
2726 .collect();
2727 assert_eq!(
2728 code_errors.len(),
2729 1,
2730 "Expected one COD002 (for GH02 only), got: {:?}",
2731 code_errors
2732 );
2733 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2734 }
2735
2736 #[test]
2737 fn test_code_validation_composite_paths_detects_invalid_code() {
2738 let evaluator = MockEvaluator::new(vec![]);
2741 let validator = EdifactValidator::new(evaluator);
2742 let external = NoOpExternalProvider;
2743
2744 let sts_segment = OwnedSegment {
2745 id: "STS".to_string(),
2746 elements: vec![
2747 vec!["7".to_string()],
2748 vec![String::new()],
2749 vec!["GH02".to_string()],
2750 vec!["ZW4".to_string()],
2751 ],
2752 segment_number: 1,
2753 };
2754
2755 let workflow = AhbWorkflow {
2756 pruefidentifikator: "55018".to_string(),
2757 description: "Test".to_string(),
2758 communication_direction: None,
2759 fields: vec![AhbFieldRule {
2760 segment_path: "SG4/STS/C556/9013".to_string(),
2761 name: "Statusanlaß".to_string(),
2762 ahb_status: "X".to_string(),
2763 codes: vec![AhbCodeRule {
2764 value: "E03".to_string(),
2765 description: "Transaktionsgrund".to_string(),
2766 ahb_status: "X".to_string(),
2767 }],
2768 parent_group_ahb_status: None,
2769 segment_ahb_status: None,
2770 element_index: Some(2),
2771 component_index: Some(0),
2772 ..Default::default()
2773 }],
2774 ub_definitions: BTreeMap::new(),
2775 };
2776
2777 let report = validator.validate(
2778 &[sts_segment],
2779 &workflow,
2780 &external,
2781 ValidationLevel::Conditions,
2782 );
2783
2784 let code_errors: Vec<_> = report
2785 .by_category(ValidationCategory::Code)
2786 .filter(|i| {
2787 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2788 })
2789 .collect();
2790 assert_eq!(
2791 code_errors.len(),
2792 1,
2793 "Expected COD002 for GH02, got: {:?}",
2794 code_errors
2795 );
2796 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2797 }
2798
2799 #[test]
2800 fn test_cross_field_code_validation_valid_qualifiers() {
2801 let evaluator = MockEvaluator::new(vec![]);
2804 let validator = EdifactValidator::new(evaluator);
2805 let external = NoOpExternalProvider;
2806
2807 let nad_ms = OwnedSegment {
2808 id: "NAD".to_string(),
2809 elements: vec![vec!["MS".to_string()]],
2810 segment_number: 4,
2811 };
2812 let nad_mr = OwnedSegment {
2813 id: "NAD".to_string(),
2814 elements: vec![vec!["MR".to_string()]],
2815 segment_number: 5,
2816 };
2817
2818 let workflow = AhbWorkflow {
2819 pruefidentifikator: "55001".to_string(),
2820 description: "Test".to_string(),
2821 communication_direction: None,
2822 fields: vec![
2823 AhbFieldRule {
2824 segment_path: "SG2/NAD/3035".to_string(),
2825 name: "Absender".to_string(),
2826 ahb_status: "X".to_string(),
2827 codes: vec![AhbCodeRule {
2828 value: "MS".to_string(),
2829 description: "Absender".to_string(),
2830 ahb_status: "X".to_string(),
2831 }],
2832 parent_group_ahb_status: None,
2833 segment_ahb_status: None,
2834 ..Default::default()
2835 },
2836 AhbFieldRule {
2837 segment_path: "SG2/NAD/3035".to_string(),
2838 name: "Empfaenger".to_string(),
2839 ahb_status: "X".to_string(),
2840 codes: vec![AhbCodeRule {
2841 value: "MR".to_string(),
2842 description: "Empfaenger".to_string(),
2843 ahb_status: "X".to_string(),
2844 }],
2845 parent_group_ahb_status: None,
2846 segment_ahb_status: None,
2847 ..Default::default()
2848 },
2849 ],
2850 ub_definitions: BTreeMap::new(),
2851 };
2852
2853 let report = validator.validate(
2854 &[nad_ms, nad_mr],
2855 &workflow,
2856 &external,
2857 ValidationLevel::Conditions,
2858 );
2859
2860 let code_errors: Vec<_> = report
2861 .by_category(ValidationCategory::Code)
2862 .filter(|i| i.severity == Severity::Error)
2863 .collect();
2864 assert!(
2865 code_errors.is_empty(),
2866 "Expected no code errors for valid qualifiers, got: {:?}",
2867 code_errors
2868 );
2869 }
2870
2871 #[test]
2872 fn test_cross_field_code_validation_catches_invalid_qualifier() {
2873 let evaluator = MockEvaluator::new(vec![]);
2875 let validator = EdifactValidator::new(evaluator);
2876 let external = NoOpExternalProvider;
2877
2878 let nad_ms = OwnedSegment {
2879 id: "NAD".to_string(),
2880 elements: vec![vec!["MS".to_string()]],
2881 segment_number: 4,
2882 };
2883 let nad_mt = OwnedSegment {
2884 id: "NAD".to_string(),
2885 elements: vec![vec!["MT".to_string()]], segment_number: 5,
2887 };
2888
2889 let workflow = AhbWorkflow {
2890 pruefidentifikator: "55001".to_string(),
2891 description: "Test".to_string(),
2892 communication_direction: None,
2893 fields: vec![
2894 AhbFieldRule {
2895 segment_path: "SG2/NAD/3035".to_string(),
2896 name: "Absender".to_string(),
2897 ahb_status: "X".to_string(),
2898 codes: vec![AhbCodeRule {
2899 value: "MS".to_string(),
2900 description: "Absender".to_string(),
2901 ahb_status: "X".to_string(),
2902 }],
2903 parent_group_ahb_status: None,
2904 segment_ahb_status: None,
2905 ..Default::default()
2906 },
2907 AhbFieldRule {
2908 segment_path: "SG2/NAD/3035".to_string(),
2909 name: "Empfaenger".to_string(),
2910 ahb_status: "X".to_string(),
2911 codes: vec![AhbCodeRule {
2912 value: "MR".to_string(),
2913 description: "Empfaenger".to_string(),
2914 ahb_status: "X".to_string(),
2915 }],
2916 parent_group_ahb_status: None,
2917 segment_ahb_status: None,
2918 ..Default::default()
2919 },
2920 ],
2921 ub_definitions: BTreeMap::new(),
2922 };
2923
2924 let report = validator.validate(
2925 &[nad_ms, nad_mt],
2926 &workflow,
2927 &external,
2928 ValidationLevel::Conditions,
2929 );
2930
2931 let code_errors: Vec<_> = report
2932 .by_category(ValidationCategory::Code)
2933 .filter(|i| i.severity == Severity::Error)
2934 .collect();
2935 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
2936 assert!(narrate(code_errors[0]).contains("MT"));
2937 assert!(narrate(code_errors[0]).contains("MR"));
2938 assert!(narrate(code_errors[0]).contains("MS"));
2939 }
2940
2941 #[test]
2942 fn test_cross_field_code_validation_unions_across_groups() {
2943 let evaluator = MockEvaluator::new(vec![]);
2947 let validator = EdifactValidator::new(evaluator);
2948 let external = NoOpExternalProvider;
2949
2950 let segments = vec![
2951 OwnedSegment {
2952 id: "NAD".to_string(),
2953 elements: vec![vec!["MS".to_string()]],
2954 segment_number: 3,
2955 },
2956 OwnedSegment {
2957 id: "NAD".to_string(),
2958 elements: vec![vec!["MR".to_string()]],
2959 segment_number: 4,
2960 },
2961 OwnedSegment {
2962 id: "NAD".to_string(),
2963 elements: vec![vec!["Z04".to_string()]],
2964 segment_number: 20,
2965 },
2966 OwnedSegment {
2967 id: "NAD".to_string(),
2968 elements: vec![vec!["Z09".to_string()]],
2969 segment_number: 21,
2970 },
2971 OwnedSegment {
2972 id: "NAD".to_string(),
2973 elements: vec![vec!["MT".to_string()]], segment_number: 22,
2975 },
2976 ];
2977
2978 let workflow = AhbWorkflow {
2979 pruefidentifikator: "55001".to_string(),
2980 description: "Test".to_string(),
2981 communication_direction: None,
2982 fields: vec![
2983 AhbFieldRule {
2984 segment_path: "SG2/NAD/3035".to_string(),
2985 name: "Absender".to_string(),
2986 ahb_status: "X".to_string(),
2987 codes: vec![AhbCodeRule {
2988 value: "MS".to_string(),
2989 description: "Absender".to_string(),
2990 ahb_status: "X".to_string(),
2991 }],
2992 parent_group_ahb_status: None,
2993 segment_ahb_status: None,
2994 ..Default::default()
2995 },
2996 AhbFieldRule {
2997 segment_path: "SG2/NAD/3035".to_string(),
2998 name: "Empfaenger".to_string(),
2999 ahb_status: "X".to_string(),
3000 codes: vec![AhbCodeRule {
3001 value: "MR".to_string(),
3002 description: "Empfaenger".to_string(),
3003 ahb_status: "X".to_string(),
3004 }],
3005 parent_group_ahb_status: None,
3006 segment_ahb_status: None,
3007 ..Default::default()
3008 },
3009 AhbFieldRule {
3010 segment_path: "SG4/SG12/NAD/3035".to_string(),
3011 name: "Anschlussnutzer".to_string(),
3012 ahb_status: "X".to_string(),
3013 codes: vec![AhbCodeRule {
3014 value: "Z04".to_string(),
3015 description: "Anschlussnutzer".to_string(),
3016 ahb_status: "X".to_string(),
3017 }],
3018 parent_group_ahb_status: None,
3019 segment_ahb_status: None,
3020 ..Default::default()
3021 },
3022 AhbFieldRule {
3023 segment_path: "SG4/SG12/NAD/3035".to_string(),
3024 name: "Korrespondenzanschrift".to_string(),
3025 ahb_status: "X".to_string(),
3026 codes: vec![AhbCodeRule {
3027 value: "Z09".to_string(),
3028 description: "Korrespondenzanschrift".to_string(),
3029 ahb_status: "X".to_string(),
3030 }],
3031 parent_group_ahb_status: None,
3032 segment_ahb_status: None,
3033 ..Default::default()
3034 },
3035 ],
3036 ub_definitions: BTreeMap::new(),
3037 };
3038
3039 let report =
3040 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3041
3042 let code_errors: Vec<_> = report
3043 .by_category(ValidationCategory::Code)
3044 .filter(|i| i.severity == Severity::Error)
3045 .collect();
3046 assert_eq!(
3047 code_errors.len(),
3048 1,
3049 "Expected exactly one COD002 error for MT, got: {:?}",
3050 code_errors
3051 );
3052 assert!(narrate(code_errors[0]).contains("MT"));
3053 }
3054
3055 #[test]
3056 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3057 let evaluator = MockEvaluator::new(vec![]);
3066 let validator = EdifactValidator::new(evaluator);
3067 let external = NoOpExternalProvider;
3068
3069 let qty_67 = OwnedSegment {
3070 id: "QTY".to_string(),
3071 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
3072 segment_number: 10,
3073 };
3074
3075 let workflow = AhbWorkflow {
3076 pruefidentifikator: "13025".to_string(),
3077 description: "Test".to_string(),
3078 communication_direction: None,
3079 fields: vec![AhbFieldRule {
3080 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
3081 name: "Menge, Qualifier".to_string(),
3082 ahb_status: "X".to_string(),
3083 codes: vec![
3084 AhbCodeRule {
3085 value: "220".to_string(),
3086 description: "Wahrer Wert".to_string(),
3087 ahb_status: "X".to_string(),
3088 },
3089 AhbCodeRule {
3090 value: "67".to_string(),
3091 description: "Ersatzwert".to_string(),
3092 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
3093 },
3094 AhbCodeRule {
3095 value: "Z18".to_string(),
3096 description: "Vorläufiger Wert".to_string(),
3097 ahb_status: "X [35]".to_string(),
3098 },
3099 ],
3100 parent_group_ahb_status: None,
3101 segment_ahb_status: None,
3102 element_index: Some(0),
3103 component_index: Some(0),
3104 ..Default::default()
3105 }],
3106 ub_definitions: BTreeMap::new(),
3107 };
3108
3109 let report =
3110 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3111
3112 let code_errors: Vec<_> = report
3113 .by_category(ValidationCategory::Code)
3114 .filter(|i| i.severity == Severity::Error)
3115 .collect();
3116 assert!(
3117 code_errors.is_empty(),
3118 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3119 code_errors
3120 );
3121 }
3122
3123 #[test]
3124 fn test_is_qualifier_field_simple_paths() {
3125 assert!(is_qualifier_field("NAD/3035"));
3126 assert!(is_qualifier_field("SG2/NAD/3035"));
3127 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3128 assert!(is_qualifier_field("LOC/3227"));
3129 }
3130
3131 #[test]
3132 fn test_is_qualifier_field_composite_paths() {
3133 assert!(is_qualifier_field("UNH/S009/0065"));
3137 assert!(is_qualifier_field("NAD/C082/3039"));
3138 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3139 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3140 }
3141
3142 #[test]
3143 fn test_is_qualifier_field_bare_segment() {
3144 assert!(!is_qualifier_field("NAD"));
3145 assert!(!is_qualifier_field("SG2/NAD"));
3146 }
3147
3148 #[test]
3149 fn test_is_qualifier_field_rejects_deep_paths() {
3150 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3152 }
3153
3154 #[test]
3155 fn test_missing_qualifier_instance_is_detected() {
3156 let evaluator = MockEvaluator::new(vec![]);
3159 let validator = EdifactValidator::new(evaluator);
3160 let external = NoOpExternalProvider;
3161
3162 let nad_ms = OwnedSegment {
3163 id: "NAD".to_string(),
3164 elements: vec![vec!["MS".to_string()]],
3165 segment_number: 3,
3166 };
3167
3168 let workflow = AhbWorkflow {
3169 pruefidentifikator: "55001".to_string(),
3170 description: "Test".to_string(),
3171 communication_direction: None,
3172 fields: vec![
3173 AhbFieldRule {
3174 segment_path: "SG2/NAD/3035".to_string(),
3175 name: "Absender".to_string(),
3176 ahb_status: "X".to_string(),
3177 codes: vec![AhbCodeRule {
3178 value: "MS".to_string(),
3179 description: "Absender".to_string(),
3180 ahb_status: "X".to_string(),
3181 }],
3182 parent_group_ahb_status: None,
3183 segment_ahb_status: None,
3184 ..Default::default()
3185 },
3186 AhbFieldRule {
3187 segment_path: "SG2/NAD/3035".to_string(),
3188 name: "Empfaenger".to_string(),
3189 ahb_status: "Muss".to_string(),
3190 codes: vec![AhbCodeRule {
3191 value: "MR".to_string(),
3192 description: "Empfaenger".to_string(),
3193 ahb_status: "X".to_string(),
3194 }],
3195 parent_group_ahb_status: None,
3196 segment_ahb_status: None,
3197 ..Default::default()
3198 },
3199 ],
3200 ub_definitions: BTreeMap::new(),
3201 };
3202
3203 let report =
3204 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
3205
3206 let ahb_errors: Vec<_> = report
3207 .by_category(ValidationCategory::Ahb)
3208 .filter(|i| i.severity == Severity::Error)
3209 .collect();
3210 assert_eq!(
3211 ahb_errors.len(),
3212 1,
3213 "Expected AHB001 for missing NAD+MR, got: {:?}",
3214 ahb_errors
3215 );
3216 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3217 }
3218
3219 #[test]
3220 fn test_present_qualifier_instance_no_error() {
3221 let evaluator = MockEvaluator::new(vec![]);
3223 let validator = EdifactValidator::new(evaluator);
3224 let external = NoOpExternalProvider;
3225
3226 let segments = vec![
3227 OwnedSegment {
3228 id: "NAD".to_string(),
3229 elements: vec![vec!["MS".to_string()]],
3230 segment_number: 3,
3231 },
3232 OwnedSegment {
3233 id: "NAD".to_string(),
3234 elements: vec![vec!["MR".to_string()]],
3235 segment_number: 4,
3236 },
3237 ];
3238
3239 let workflow = AhbWorkflow {
3240 pruefidentifikator: "55001".to_string(),
3241 description: "Test".to_string(),
3242 communication_direction: None,
3243 fields: vec![
3244 AhbFieldRule {
3245 segment_path: "SG2/NAD/3035".to_string(),
3246 name: "Absender".to_string(),
3247 ahb_status: "Muss".to_string(),
3248 codes: vec![AhbCodeRule {
3249 value: "MS".to_string(),
3250 description: "Absender".to_string(),
3251 ahb_status: "X".to_string(),
3252 }],
3253 parent_group_ahb_status: None,
3254 segment_ahb_status: None,
3255 ..Default::default()
3256 },
3257 AhbFieldRule {
3258 segment_path: "SG2/NAD/3035".to_string(),
3259 name: "Empfaenger".to_string(),
3260 ahb_status: "Muss".to_string(),
3261 codes: vec![AhbCodeRule {
3262 value: "MR".to_string(),
3263 description: "Empfaenger".to_string(),
3264 ahb_status: "X".to_string(),
3265 }],
3266 parent_group_ahb_status: None,
3267 segment_ahb_status: None,
3268 ..Default::default()
3269 },
3270 ],
3271 ub_definitions: BTreeMap::new(),
3272 };
3273
3274 let report =
3275 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3276
3277 let ahb_errors: Vec<_> = report
3278 .by_category(ValidationCategory::Ahb)
3279 .filter(|i| i.severity == Severity::Error)
3280 .collect();
3281 assert!(
3282 ahb_errors.is_empty(),
3283 "Expected no AHB001 errors, got: {:?}",
3284 ahb_errors
3285 );
3286 }
3287
3288 #[test]
3289 fn test_extract_group_path_key() {
3290 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
3291 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
3292 assert_eq!(extract_group_path_key("NAD/3035"), "");
3293 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
3294 }
3295
3296 fn validate_field_of_absent_group(
3298 group_status: &str,
3299 conditions: Vec<(u32, CR)>,
3300 ) -> ValidationReport {
3301 use mig_types::navigator::GroupNavigator;
3302
3303 struct NoSg9;
3304 impl GroupNavigator for NoSg9 {
3305 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3306 vec![]
3307 }
3308 fn find_segments_with_qualifier_in_group(
3309 &self,
3310 _: &str,
3311 _: usize,
3312 _: &str,
3313 _: &[&str],
3314 _: usize,
3315 ) -> Vec<OwnedSegment> {
3316 vec![]
3317 }
3318 fn group_instance_count(&self, _: &[&str]) -> usize {
3319 0
3320 }
3321 }
3322
3323 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3324 let workflow = AhbWorkflow {
3325 pruefidentifikator: "44001".to_string(),
3326 description: "Test".to_string(),
3327 communication_direction: None,
3328 fields: vec![AhbFieldRule {
3329 segment_path: "SG9/QTY/C186/6060".to_string(),
3330 name: "Menge".to_string(),
3331 ahb_status: "X".to_string(),
3332 parent_group_ahb_status: Some(group_status.to_string()),
3333 ..Default::default()
3334 }],
3335 ub_definitions: BTreeMap::new(),
3336 };
3337 validator.validate_with_navigator(
3338 &[],
3339 &workflow,
3340 &NoOpExternalProvider,
3341 ValidationLevel::Conditions,
3342 &NoSg9,
3343 )
3344 }
3345
3346 #[test]
3347 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
3348 let report = validate_field_of_absent_group(
3350 "Soll [47]\r\nMuss [46]",
3351 vec![(47, CR::True), (46, CR::False)],
3352 );
3353 assert!(
3354 report.is_valid(),
3355 "{:?}",
3356 report.errors().collect::<Vec<_>>()
3357 );
3358 }
3359
3360 #[test]
3361 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
3362 let report = validate_field_of_absent_group(
3363 "Soll [47]\r\nMuss [46]",
3364 vec![(47, CR::False), (46, CR::True)],
3365 );
3366 let errors: Vec<_> = report.errors().collect();
3367 assert_eq!(errors.len(), 1, "{errors:?}");
3368 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3369 }
3370
3371 #[test]
3372 fn test_absent_optional_group_no_missing_field_error() {
3373 use mig_types::navigator::GroupNavigator;
3376
3377 struct NavWithoutSG3;
3378 impl GroupNavigator for NavWithoutSG3 {
3379 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3380 vec![]
3381 }
3382 fn find_segments_with_qualifier_in_group(
3383 &self,
3384 _: &str,
3385 _: usize,
3386 _: &str,
3387 _: &[&str],
3388 _: usize,
3389 ) -> Vec<OwnedSegment> {
3390 vec![]
3391 }
3392 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3393 match group_path {
3394 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
3397 }
3398 }
3399 }
3400
3401 let evaluator = MockEvaluator::new(vec![]);
3402 let validator = EdifactValidator::new(evaluator);
3403 let external = NoOpExternalProvider;
3404 let nav = NavWithoutSG3;
3405
3406 let segments = vec![
3408 OwnedSegment {
3409 id: "NAD".into(),
3410 elements: vec![vec!["MS".into()]],
3411 segment_number: 3,
3412 },
3413 OwnedSegment {
3414 id: "NAD".into(),
3415 elements: vec![vec!["MR".into()]],
3416 segment_number: 4,
3417 },
3418 ];
3419
3420 let workflow = AhbWorkflow {
3421 pruefidentifikator: "55001".to_string(),
3422 description: "Test".to_string(),
3423 communication_direction: None,
3424 fields: vec![
3425 AhbFieldRule {
3426 segment_path: "SG2/SG3/CTA/3139".to_string(),
3427 name: "Funktion des Ansprechpartners, Code".to_string(),
3428 ahb_status: "Muss".to_string(),
3429 codes: vec![],
3430 parent_group_ahb_status: None,
3431 segment_ahb_status: None,
3432 ..Default::default()
3433 },
3434 AhbFieldRule {
3435 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
3436 name: "Name vom Ansprechpartner".to_string(),
3437 ahb_status: "X".to_string(),
3438 codes: vec![],
3439 parent_group_ahb_status: None,
3440 segment_ahb_status: None,
3441 ..Default::default()
3442 },
3443 ],
3444 ub_definitions: BTreeMap::new(),
3445 };
3446
3447 let report = validator.validate_with_navigator(
3448 &segments,
3449 &workflow,
3450 &external,
3451 ValidationLevel::Conditions,
3452 &nav,
3453 );
3454
3455 let ahb_errors: Vec<_> = report
3456 .by_category(ValidationCategory::Ahb)
3457 .filter(|i| i.severity == Severity::Error)
3458 .collect();
3459 assert!(
3460 ahb_errors.is_empty(),
3461 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
3462 ahb_errors
3463 );
3464 }
3465
3466 #[test]
3467 fn test_present_group_still_checks_mandatory_fields() {
3468 use mig_types::navigator::GroupNavigator;
3470
3471 struct NavWithSG3;
3472 impl GroupNavigator for NavWithSG3 {
3473 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3474 vec![]
3475 }
3476 fn find_segments_with_qualifier_in_group(
3477 &self,
3478 _: &str,
3479 _: usize,
3480 _: &str,
3481 _: &[&str],
3482 _: usize,
3483 ) -> Vec<OwnedSegment> {
3484 vec![]
3485 }
3486 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3487 match group_path {
3488 ["SG2"] => 1,
3489 ["SG2", "SG3"] => 1, _ => 0,
3491 }
3492 }
3493 }
3494
3495 let evaluator = MockEvaluator::new(vec![]);
3496 let validator = EdifactValidator::new(evaluator);
3497 let external = NoOpExternalProvider;
3498 let nav = NavWithSG3;
3499
3500 let segments = vec![OwnedSegment {
3502 id: "NAD".into(),
3503 elements: vec![vec!["MS".into()]],
3504 segment_number: 3,
3505 }];
3506
3507 let workflow = AhbWorkflow {
3508 pruefidentifikator: "55001".to_string(),
3509 description: "Test".to_string(),
3510 communication_direction: None,
3511 fields: vec![AhbFieldRule {
3512 segment_path: "SG2/SG3/CTA/3139".to_string(),
3513 name: "Funktion des Ansprechpartners, Code".to_string(),
3514 ahb_status: "Muss".to_string(),
3515 codes: vec![],
3516 parent_group_ahb_status: None,
3517 segment_ahb_status: None,
3518 ..Default::default()
3519 }],
3520 ub_definitions: BTreeMap::new(),
3521 };
3522
3523 let report = validator.validate_with_navigator(
3524 &segments,
3525 &workflow,
3526 &external,
3527 ValidationLevel::Conditions,
3528 &nav,
3529 );
3530
3531 let ahb_errors: Vec<_> = report
3532 .by_category(ValidationCategory::Ahb)
3533 .filter(|i| i.severity == Severity::Error)
3534 .collect();
3535 assert_eq!(
3536 ahb_errors.len(),
3537 1,
3538 "Expected AHB001 error when SG3 is present but CTA missing"
3539 );
3540 assert!(narrate(ahb_errors[0]).contains("CTA"));
3541 }
3542
3543 #[test]
3544 fn test_missing_qualifier_with_navigator_is_detected() {
3545 use mig_types::navigator::GroupNavigator;
3548
3549 struct NavWithSG2;
3550 impl GroupNavigator for NavWithSG2 {
3551 fn find_segments_in_group(
3552 &self,
3553 segment_id: &str,
3554 group_path: &[&str],
3555 instance_index: usize,
3556 ) -> Vec<OwnedSegment> {
3557 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
3558 vec![OwnedSegment {
3559 id: "NAD".into(),
3560 elements: vec![vec!["MS".into()]],
3561 segment_number: 3,
3562 }]
3563 } else {
3564 vec![]
3565 }
3566 }
3567 fn find_segments_with_qualifier_in_group(
3568 &self,
3569 _: &str,
3570 _: usize,
3571 _: &str,
3572 _: &[&str],
3573 _: usize,
3574 ) -> Vec<OwnedSegment> {
3575 vec![]
3576 }
3577 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3578 match group_path {
3579 ["SG2"] => 1,
3580 _ => 0,
3581 }
3582 }
3583 }
3584
3585 let evaluator = MockEvaluator::new(vec![]);
3586 let validator = EdifactValidator::new(evaluator);
3587 let external = NoOpExternalProvider;
3588 let nav = NavWithSG2;
3589
3590 let segments = vec![OwnedSegment {
3591 id: "NAD".into(),
3592 elements: vec![vec!["MS".into()]],
3593 segment_number: 3,
3594 }];
3595
3596 let workflow = AhbWorkflow {
3597 pruefidentifikator: "55001".to_string(),
3598 description: "Test".to_string(),
3599 communication_direction: None,
3600 fields: vec![
3601 AhbFieldRule {
3602 segment_path: "SG2/NAD/3035".to_string(),
3603 name: "Absender".to_string(),
3604 ahb_status: "X".to_string(),
3605 codes: vec![AhbCodeRule {
3606 value: "MS".to_string(),
3607 description: "Absender".to_string(),
3608 ahb_status: "X".to_string(),
3609 }],
3610 parent_group_ahb_status: None,
3611 segment_ahb_status: None,
3612 ..Default::default()
3613 },
3614 AhbFieldRule {
3615 segment_path: "SG2/NAD/3035".to_string(),
3616 name: "Empfaenger".to_string(),
3617 ahb_status: "Muss".to_string(),
3618 codes: vec![AhbCodeRule {
3619 value: "MR".to_string(),
3620 description: "Empfaenger".to_string(),
3621 ahb_status: "X".to_string(),
3622 }],
3623 parent_group_ahb_status: None,
3624 segment_ahb_status: None,
3625 ..Default::default()
3626 },
3627 ],
3628 ub_definitions: BTreeMap::new(),
3629 };
3630
3631 let report = validator.validate_with_navigator(
3632 &segments,
3633 &workflow,
3634 &external,
3635 ValidationLevel::Conditions,
3636 &nav,
3637 );
3638
3639 let ahb_errors: Vec<_> = report
3640 .by_category(ValidationCategory::Ahb)
3641 .filter(|i| i.severity == Severity::Error)
3642 .collect();
3643 assert_eq!(
3644 ahb_errors.len(),
3645 1,
3646 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
3647 ahb_errors
3648 );
3649 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3650 }
3651
3652 #[test]
3653 fn test_optional_group_variant_absent_no_error() {
3654 use mig_types::navigator::GroupNavigator;
3659
3660 struct TestNav;
3661 impl GroupNavigator for TestNav {
3662 fn find_segments_in_group(
3663 &self,
3664 segment_id: &str,
3665 group_path: &[&str],
3666 instance_index: usize,
3667 ) -> Vec<OwnedSegment> {
3668 match (segment_id, group_path, instance_index) {
3669 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
3670 id: "LOC".into(),
3671 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3672 segment_number: 10,
3673 }],
3674 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
3675 id: "NAD".into(),
3676 elements: vec![vec!["MS".into()]],
3677 segment_number: 3,
3678 }],
3679 _ => vec![],
3680 }
3681 }
3682 fn find_segments_with_qualifier_in_group(
3683 &self,
3684 _: &str,
3685 _: usize,
3686 _: &str,
3687 _: &[&str],
3688 _: usize,
3689 ) -> Vec<OwnedSegment> {
3690 vec![]
3691 }
3692 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3693 match group_path {
3694 ["SG2"] => 1,
3695 ["SG4"] => 1,
3696 ["SG4", "SG5"] => 1, _ => 0,
3698 }
3699 }
3700 }
3701
3702 let evaluator = MockEvaluator::new(vec![]);
3703 let validator = EdifactValidator::new(evaluator);
3704 let external = NoOpExternalProvider;
3705 let nav = TestNav;
3706
3707 let segments = vec![
3708 OwnedSegment {
3709 id: "NAD".into(),
3710 elements: vec![vec!["MS".into()]],
3711 segment_number: 3,
3712 },
3713 OwnedSegment {
3714 id: "LOC".into(),
3715 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3716 segment_number: 10,
3717 },
3718 ];
3719
3720 let workflow = AhbWorkflow {
3721 pruefidentifikator: "55001".to_string(),
3722 description: "Test".to_string(),
3723 communication_direction: None,
3724 fields: vec![
3725 AhbFieldRule {
3727 segment_path: "SG2/NAD/3035".to_string(),
3728 name: "Absender".to_string(),
3729 ahb_status: "X".to_string(),
3730 codes: vec![AhbCodeRule {
3731 value: "MS".to_string(),
3732 description: "Absender".to_string(),
3733 ahb_status: "X".to_string(),
3734 }],
3735 parent_group_ahb_status: Some("Muss".to_string()),
3736 segment_ahb_status: None,
3737 ..Default::default()
3738 },
3739 AhbFieldRule {
3740 segment_path: "SG2/NAD/3035".to_string(),
3741 name: "Empfaenger".to_string(),
3742 ahb_status: "Muss".to_string(),
3743 codes: vec![AhbCodeRule {
3744 value: "MR".to_string(),
3745 description: "Empfaenger".to_string(),
3746 ahb_status: "X".to_string(),
3747 }],
3748 parent_group_ahb_status: Some("Muss".to_string()),
3749 segment_ahb_status: None,
3750 ..Default::default()
3751 },
3752 AhbFieldRule {
3754 segment_path: "SG4/SG5/LOC/3227".to_string(),
3755 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3756 ahb_status: "X".to_string(),
3757 codes: vec![AhbCodeRule {
3758 value: "Z16".to_string(),
3759 description: "Marktlokation".to_string(),
3760 ahb_status: "X".to_string(),
3761 }],
3762 parent_group_ahb_status: Some("Kann".to_string()),
3763 segment_ahb_status: None,
3764 ..Default::default()
3765 },
3766 AhbFieldRule {
3767 segment_path: "SG4/SG5/LOC/3227".to_string(),
3768 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3769 ahb_status: "Muss".to_string(),
3770 codes: vec![AhbCodeRule {
3771 value: "Z17".to_string(),
3772 description: "Messlokation".to_string(),
3773 ahb_status: "X".to_string(),
3774 }],
3775 parent_group_ahb_status: Some("Kann".to_string()),
3776 segment_ahb_status: None,
3777 ..Default::default()
3778 },
3779 ],
3780 ub_definitions: BTreeMap::new(),
3781 };
3782
3783 let report = validator.validate_with_navigator(
3784 &segments,
3785 &workflow,
3786 &external,
3787 ValidationLevel::Conditions,
3788 &nav,
3789 );
3790
3791 let ahb_errors: Vec<_> = report
3792 .by_category(ValidationCategory::Ahb)
3793 .filter(|i| i.severity == Severity::Error)
3794 .collect();
3795
3796 assert_eq!(
3799 ahb_errors.len(),
3800 1,
3801 "Expected only AHB001 for missing NAD+MR, got: {:?}",
3802 ahb_errors
3803 );
3804 assert!(
3805 narrate(ahb_errors[0]).contains("Empfaenger"),
3806 "Error should be for missing NAD+MR (Empfaenger)"
3807 );
3808 }
3809
3810 #[test]
3814 fn test_optional_group_variant_with_true_condition_absent_no_error() {
3815 use mig_types::navigator::GroupNavigator;
3816
3817 struct TestNav;
3818 impl GroupNavigator for TestNav {
3819 fn find_segments_in_group(
3820 &self,
3821 segment_id: &str,
3822 group_path: &[&str],
3823 instance_index: usize,
3824 ) -> Vec<OwnedSegment> {
3825 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
3826 vec![OwnedSegment {
3827 id: "SEQ".into(),
3828 elements: vec![vec!["Z03".into()]],
3829 segment_number: 10,
3830 }]
3831 } else {
3832 vec![]
3833 }
3834 }
3835 fn find_segments_with_qualifier_in_group(
3836 &self,
3837 _: &str,
3838 _: usize,
3839 _: &str,
3840 _: &[&str],
3841 _: usize,
3842 ) -> Vec<OwnedSegment> {
3843 vec![]
3844 }
3845 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3846 match group_path {
3847 ["SG4"] => 1,
3848 ["SG4", "SG8"] => 1,
3849 _ => 0,
3850 }
3851 }
3852 }
3853
3854 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
3855 let validator = EdifactValidator::new(evaluator);
3856 let segments = vec![OwnedSegment {
3857 id: "SEQ".into(),
3858 elements: vec![vec!["Z03".into()]],
3859 segment_number: 10,
3860 }];
3861 let seq = |code: &str, group: &str| AhbFieldRule {
3862 segment_path: "SG4/SG8/SEQ/1229".to_string(),
3863 name: format!("Handlung, Code ({code})"),
3864 ahb_status: "X".to_string(),
3865 codes: vec![AhbCodeRule {
3866 value: code.to_string(),
3867 description: String::new(),
3868 ahb_status: "X".to_string(),
3869 }],
3870 parent_group_ahb_status: Some(group.to_string()),
3871 segment_ahb_status: None,
3872 ..Default::default()
3873 };
3874 let workflow = AhbWorkflow {
3875 pruefidentifikator: "44143".to_string(),
3876 description: "Test".to_string(),
3877 communication_direction: None,
3878 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
3879 ub_definitions: BTreeMap::new(),
3880 };
3881
3882 let report = validator.validate_with_navigator(
3883 &segments,
3884 &workflow,
3885 &NoOpExternalProvider,
3886 ValidationLevel::Conditions,
3887 &TestNav,
3888 );
3889 let missing: Vec<_> = report
3890 .issues
3891 .iter()
3892 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
3893 .collect();
3894 assert!(missing.is_empty(), "{missing:#?}");
3895 }
3896
3897 #[test]
3898 fn test_conditional_group_variant_absent_no_error() {
3899 use mig_types::navigator::GroupNavigator;
3904
3905 struct TestNav;
3906 impl GroupNavigator for TestNav {
3907 fn find_segments_in_group(
3908 &self,
3909 segment_id: &str,
3910 group_path: &[&str],
3911 instance_index: usize,
3912 ) -> Vec<OwnedSegment> {
3913 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
3914 vec![OwnedSegment {
3915 id: "LOC".into(),
3916 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3917 segment_number: 10,
3918 }]
3919 } else {
3920 vec![]
3921 }
3922 }
3923 fn find_segments_with_qualifier_in_group(
3924 &self,
3925 _: &str,
3926 _: usize,
3927 _: &str,
3928 _: &[&str],
3929 _: usize,
3930 ) -> Vec<OwnedSegment> {
3931 vec![]
3932 }
3933 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3934 match group_path {
3935 ["SG4"] => 1,
3936 ["SG4", "SG5"] => 1, _ => 0,
3938 }
3939 }
3940 }
3941
3942 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
3945 let validator = EdifactValidator::new(evaluator);
3946 let external = NoOpExternalProvider;
3947 let nav = TestNav;
3948
3949 let segments = vec![OwnedSegment {
3950 id: "LOC".into(),
3951 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3952 segment_number: 10,
3953 }];
3954
3955 let workflow = AhbWorkflow {
3956 pruefidentifikator: "55001".to_string(),
3957 description: "Test".to_string(),
3958 communication_direction: None,
3959 fields: vec![
3960 AhbFieldRule {
3962 segment_path: "SG4/SG5/LOC/3227".to_string(),
3963 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3964 ahb_status: "X".to_string(),
3965 codes: vec![AhbCodeRule {
3966 value: "Z16".to_string(),
3967 description: "Marktlokation".to_string(),
3968 ahb_status: "X".to_string(),
3969 }],
3970 parent_group_ahb_status: Some("Muss [2061]".to_string()),
3971 segment_ahb_status: None,
3972 ..Default::default()
3973 },
3974 AhbFieldRule {
3976 segment_path: "SG4/SG5/LOC/3227".to_string(),
3977 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3978 ahb_status: "X".to_string(),
3979 codes: vec![AhbCodeRule {
3980 value: "Z17".to_string(),
3981 description: "Messlokation".to_string(),
3982 ahb_status: "X".to_string(),
3983 }],
3984 parent_group_ahb_status: Some("Soll [165]".to_string()),
3985 segment_ahb_status: None,
3986 ..Default::default()
3987 },
3988 ],
3989 ub_definitions: BTreeMap::new(),
3990 };
3991
3992 let report = validator.validate_with_navigator(
3993 &segments,
3994 &workflow,
3995 &external,
3996 ValidationLevel::Conditions,
3997 &nav,
3998 );
3999
4000 let ahb_errors: Vec<_> = report
4001 .by_category(ValidationCategory::Ahb)
4002 .filter(|i| i.severity == Severity::Error)
4003 .collect();
4004
4005 assert!(
4007 ahb_errors.is_empty(),
4008 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4009 ahb_errors
4010 );
4011 }
4012
4013 #[test]
4014 fn test_conditional_group_variant_unknown_no_error() {
4015 let evaluator = MockEvaluator::new(vec![]);
4021 let validator = EdifactValidator::new(evaluator);
4022 let external = NoOpExternalProvider;
4023
4024 let workflow = AhbWorkflow {
4025 pruefidentifikator: "55001".to_string(),
4026 description: "Test".to_string(),
4027 communication_direction: None,
4028 fields: vec![AhbFieldRule {
4029 segment_path: "SG4/SG5/LOC/3227".to_string(),
4030 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4031 ahb_status: "X".to_string(),
4032 codes: vec![AhbCodeRule {
4033 value: "Z17".to_string(),
4034 description: "Messlokation".to_string(),
4035 ahb_status: "X".to_string(),
4036 }],
4037 parent_group_ahb_status: Some("Soll [165]".to_string()),
4038 segment_ahb_status: None,
4039 ..Default::default()
4040 }],
4041 ub_definitions: BTreeMap::new(),
4042 };
4043
4044 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4045
4046 let ahb_errors: Vec<_> = report
4047 .by_category(ValidationCategory::Ahb)
4048 .filter(|i| i.severity == Severity::Error)
4049 .collect();
4050
4051 assert!(
4053 ahb_errors.is_empty(),
4054 "Expected no errors when parent group condition is Unknown, got: {:?}",
4055 ahb_errors
4056 );
4057 }
4058
4059 #[test]
4060 fn test_segment_absent_within_present_group_no_error() {
4061 use mig_types::navigator::GroupNavigator;
4065
4066 struct TestNav;
4067 impl GroupNavigator for TestNav {
4068 fn find_segments_in_group(
4069 &self,
4070 segment_id: &str,
4071 group_path: &[&str],
4072 instance_index: usize,
4073 ) -> Vec<OwnedSegment> {
4074 if segment_id == "QTY"
4076 && group_path == ["SG5", "SG6", "SG9", "SG10"]
4077 && instance_index == 0
4078 {
4079 vec![OwnedSegment {
4080 id: "QTY".into(),
4081 elements: vec![vec!["220".into(), "0".into()]],
4082 segment_number: 14,
4083 }]
4084 } else {
4085 vec![]
4086 }
4087 }
4088 fn find_segments_with_qualifier_in_group(
4089 &self,
4090 _: &str,
4091 _: usize,
4092 _: &str,
4093 _: &[&str],
4094 _: usize,
4095 ) -> Vec<OwnedSegment> {
4096 vec![]
4097 }
4098 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4099 match group_path {
4100 ["SG5"] => 1,
4101 ["SG5", "SG6"] => 1,
4102 ["SG5", "SG6", "SG9"] => 1,
4103 ["SG5", "SG6", "SG9", "SG10"] => 1,
4104 _ => 0,
4105 }
4106 }
4107 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4108 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4110 }
4111 }
4112
4113 let evaluator = MockEvaluator::all_true(&[]);
4114 let validator = EdifactValidator::new(evaluator);
4115 let external = NoOpExternalProvider;
4116 let nav = TestNav;
4117
4118 let segments = vec![OwnedSegment {
4119 id: "QTY".into(),
4120 elements: vec![vec!["220".into(), "0".into()]],
4121 segment_number: 14,
4122 }];
4123
4124 let workflow = AhbWorkflow {
4125 pruefidentifikator: "13017".to_string(),
4126 description: "Test".to_string(),
4127 communication_direction: None,
4128 fields: vec![
4129 AhbFieldRule {
4131 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4132 name: "Statuskategorie, Code".to_string(),
4133 ahb_status: "X".to_string(),
4134 codes: vec![],
4135 parent_group_ahb_status: Some("Muss".to_string()),
4136 segment_ahb_status: None,
4137 ..Default::default()
4138 },
4139 AhbFieldRule {
4141 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4142 name: "Statusanlaß, Code".to_string(),
4143 ahb_status: "X [5]".to_string(),
4144 codes: vec![],
4145 parent_group_ahb_status: Some("Muss".to_string()),
4146 segment_ahb_status: None,
4147 ..Default::default()
4148 },
4149 ],
4150 ub_definitions: BTreeMap::new(),
4151 };
4152
4153 let report = validator.validate_with_navigator(
4154 &segments,
4155 &workflow,
4156 &external,
4157 ValidationLevel::Conditions,
4158 &nav,
4159 );
4160
4161 let ahb_errors: Vec<_> = report
4162 .by_category(ValidationCategory::Ahb)
4163 .filter(|i| i.severity == Severity::Error)
4164 .collect();
4165
4166 assert!(
4167 ahb_errors.is_empty(),
4168 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
4169 ahb_errors
4170 );
4171 }
4172
4173 #[test]
4174 fn test_group_scoped_code_validation_with_navigator() {
4175 use mig_types::navigator::GroupNavigator;
4179
4180 struct TestNav;
4181 impl GroupNavigator for TestNav {
4182 fn find_segments_in_group(
4183 &self,
4184 segment_id: &str,
4185 group_path: &[&str],
4186 _instance_index: usize,
4187 ) -> Vec<OwnedSegment> {
4188 if segment_id != "NAD" {
4189 return vec![];
4190 }
4191 match group_path {
4192 ["SG2"] => vec![
4193 OwnedSegment {
4194 id: "NAD".into(),
4195 elements: vec![vec!["MS".into()]],
4196 segment_number: 3,
4197 },
4198 OwnedSegment {
4199 id: "NAD".into(),
4200 elements: vec![vec!["MT".into()]], segment_number: 4,
4202 },
4203 ],
4204 ["SG4", "SG12"] => vec![
4205 OwnedSegment {
4206 id: "NAD".into(),
4207 elements: vec![vec!["Z04".into()]],
4208 segment_number: 20,
4209 },
4210 OwnedSegment {
4211 id: "NAD".into(),
4212 elements: vec![vec!["Z09".into()]],
4213 segment_number: 21,
4214 },
4215 ],
4216 _ => vec![],
4217 }
4218 }
4219 fn find_segments_with_qualifier_in_group(
4220 &self,
4221 _: &str,
4222 _: usize,
4223 _: &str,
4224 _: &[&str],
4225 _: usize,
4226 ) -> Vec<OwnedSegment> {
4227 vec![]
4228 }
4229 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4230 match group_path {
4231 ["SG2"] | ["SG4", "SG12"] => 1,
4232 _ => 0,
4233 }
4234 }
4235 }
4236
4237 let evaluator = MockEvaluator::new(vec![]);
4238 let validator = EdifactValidator::new(evaluator);
4239 let external = NoOpExternalProvider;
4240 let nav = TestNav;
4241
4242 let workflow = AhbWorkflow {
4243 pruefidentifikator: "55001".to_string(),
4244 description: "Test".to_string(),
4245 communication_direction: None,
4246 fields: vec![
4247 AhbFieldRule {
4248 segment_path: "SG2/NAD/3035".to_string(),
4249 name: "Absender".to_string(),
4250 ahb_status: "X".to_string(),
4251 codes: vec![AhbCodeRule {
4252 value: "MS".to_string(),
4253 description: "Absender".to_string(),
4254 ahb_status: "X".to_string(),
4255 }],
4256 parent_group_ahb_status: None,
4257 segment_ahb_status: None,
4258 ..Default::default()
4259 },
4260 AhbFieldRule {
4261 segment_path: "SG2/NAD/3035".to_string(),
4262 name: "Empfaenger".to_string(),
4263 ahb_status: "X".to_string(),
4264 codes: vec![AhbCodeRule {
4265 value: "MR".to_string(),
4266 description: "Empfaenger".to_string(),
4267 ahb_status: "X".to_string(),
4268 }],
4269 parent_group_ahb_status: None,
4270 segment_ahb_status: None,
4271 ..Default::default()
4272 },
4273 AhbFieldRule {
4274 segment_path: "SG4/SG12/NAD/3035".to_string(),
4275 name: "Anschlussnutzer".to_string(),
4276 ahb_status: "X".to_string(),
4277 codes: vec![AhbCodeRule {
4278 value: "Z04".to_string(),
4279 description: "Anschlussnutzer".to_string(),
4280 ahb_status: "X".to_string(),
4281 }],
4282 parent_group_ahb_status: None,
4283 segment_ahb_status: None,
4284 ..Default::default()
4285 },
4286 AhbFieldRule {
4287 segment_path: "SG4/SG12/NAD/3035".to_string(),
4288 name: "Korrespondenzanschrift".to_string(),
4289 ahb_status: "X".to_string(),
4290 codes: vec![AhbCodeRule {
4291 value: "Z09".to_string(),
4292 description: "Korrespondenzanschrift".to_string(),
4293 ahb_status: "X".to_string(),
4294 }],
4295 parent_group_ahb_status: None,
4296 segment_ahb_status: None,
4297 ..Default::default()
4298 },
4299 ],
4300 ub_definitions: BTreeMap::new(),
4301 };
4302
4303 let all_segments = vec![
4305 OwnedSegment {
4306 id: "NAD".into(),
4307 elements: vec![vec!["MS".into()]],
4308 segment_number: 3,
4309 },
4310 OwnedSegment {
4311 id: "NAD".into(),
4312 elements: vec![vec!["MT".into()]],
4313 segment_number: 4,
4314 },
4315 OwnedSegment {
4316 id: "NAD".into(),
4317 elements: vec![vec!["Z04".into()]],
4318 segment_number: 20,
4319 },
4320 OwnedSegment {
4321 id: "NAD".into(),
4322 elements: vec![vec!["Z09".into()]],
4323 segment_number: 21,
4324 },
4325 ];
4326
4327 let report = validator.validate_with_navigator(
4328 &all_segments,
4329 &workflow,
4330 &external,
4331 ValidationLevel::Conditions,
4332 &nav,
4333 );
4334
4335 let code_errors: Vec<_> = report
4336 .by_category(ValidationCategory::Code)
4337 .filter(|i| i.severity == Severity::Error)
4338 .collect();
4339
4340 assert_eq!(
4343 code_errors.len(),
4344 1,
4345 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
4346 code_errors
4347 );
4348 assert!(narrate(code_errors[0]).contains("MT"));
4349 assert!(narrate(code_errors[0]).contains("MR"));
4351 assert!(narrate(code_errors[0]).contains("MS"));
4352 assert!(
4353 !narrate(code_errors[0]).contains("Z04"),
4354 "SG4/SG12 codes should not leak into SG2 error"
4355 );
4356 assert!(
4358 code_errors[0]
4359 .field_path
4360 .as_deref()
4361 .unwrap_or("")
4362 .contains("SG2"),
4363 "Error field_path should reference SG2, got: {:?}",
4364 code_errors[0].field_path
4365 );
4366 }
4367
4368 #[test]
4371 fn test_package_cardinality_within_bounds() {
4372 let evaluator = MockEvaluator::all_true(&[]);
4374 let validator = EdifactValidator::new(evaluator);
4375 let external = NoOpExternalProvider;
4376
4377 let segments = vec![OwnedSegment {
4378 id: "STS".into(),
4379 elements: vec![
4380 vec!["Z33".into()], vec![], vec!["E01".into()], ],
4384 segment_number: 5,
4385 }];
4386
4387 let workflow = AhbWorkflow {
4388 pruefidentifikator: "13017".to_string(),
4389 description: "Test".to_string(),
4390 communication_direction: None,
4391 ub_definitions: BTreeMap::new(),
4392 fields: vec![AhbFieldRule {
4393 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4394 name: "Statusanlaß, Code".to_string(),
4395 ahb_status: "X".to_string(),
4396 element_index: Some(2),
4397 component_index: Some(0),
4398 codes: vec![
4399 AhbCodeRule {
4400 value: "E01".into(),
4401 description: "Code 1".into(),
4402 ahb_status: "X [4P0..1]".into(),
4403 },
4404 AhbCodeRule {
4405 value: "E02".into(),
4406 description: "Code 2".into(),
4407 ahb_status: "X [4P0..1]".into(),
4408 },
4409 ],
4410 parent_group_ahb_status: Some("Muss".to_string()),
4411 segment_ahb_status: None,
4412 mig_number: None,
4413 format: None,
4414 }],
4415 };
4416
4417 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4418 let pkg_errors: Vec<_> = report
4419 .by_category(ValidationCategory::Ahb)
4420 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4421 .collect();
4422 assert!(
4423 pkg_errors.is_empty(),
4424 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
4425 pkg_errors
4426 );
4427 }
4428
4429 #[test]
4430 fn test_package_cardinality_zero_present_min_zero() {
4431 let evaluator = MockEvaluator::all_true(&[]);
4433 let validator = EdifactValidator::new(evaluator);
4434 let external = NoOpExternalProvider;
4435
4436 let segments = vec![OwnedSegment {
4437 id: "STS".into(),
4438 elements: vec![
4439 vec!["Z33".into()],
4440 vec![],
4441 vec!["X99".into()], ],
4443 segment_number: 5,
4444 }];
4445
4446 let workflow = AhbWorkflow {
4447 pruefidentifikator: "13017".to_string(),
4448 description: "Test".to_string(),
4449 communication_direction: None,
4450 ub_definitions: BTreeMap::new(),
4451 fields: vec![AhbFieldRule {
4452 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4453 name: "Statusanlaß, Code".to_string(),
4454 ahb_status: "X".to_string(),
4455 element_index: Some(2),
4456 component_index: Some(0),
4457 codes: vec![
4458 AhbCodeRule {
4459 value: "E01".into(),
4460 description: "Code 1".into(),
4461 ahb_status: "X [4P0..1]".into(),
4462 },
4463 AhbCodeRule {
4464 value: "E02".into(),
4465 description: "Code 2".into(),
4466 ahb_status: "X [4P0..1]".into(),
4467 },
4468 ],
4469 parent_group_ahb_status: Some("Muss".to_string()),
4470 segment_ahb_status: None,
4471 mig_number: None,
4472 format: None,
4473 }],
4474 };
4475
4476 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4477 let pkg_errors: Vec<_> = report
4478 .by_category(ValidationCategory::Ahb)
4479 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4480 .collect();
4481 assert!(
4482 pkg_errors.is_empty(),
4483 "0 codes, min=0 — no error expected, got: {:?}",
4484 pkg_errors
4485 );
4486 }
4487
4488 #[test]
4489 fn test_package_cardinality_too_many() {
4490 let evaluator = MockEvaluator::all_true(&[]);
4492 let validator = EdifactValidator::new(evaluator);
4493 let external = NoOpExternalProvider;
4494
4495 let segments = vec![
4497 OwnedSegment {
4498 id: "STS".into(),
4499 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
4500 segment_number: 5,
4501 },
4502 OwnedSegment {
4503 id: "STS".into(),
4504 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
4505 segment_number: 6,
4506 },
4507 ];
4508
4509 let workflow = AhbWorkflow {
4510 pruefidentifikator: "13017".to_string(),
4511 description: "Test".to_string(),
4512 communication_direction: None,
4513 ub_definitions: BTreeMap::new(),
4514 fields: vec![AhbFieldRule {
4515 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4516 name: "Statusanlaß, Code".to_string(),
4517 ahb_status: "X".to_string(),
4518 element_index: Some(2),
4519 component_index: Some(0),
4520 codes: vec![
4521 AhbCodeRule {
4522 value: "E01".into(),
4523 description: "Code 1".into(),
4524 ahb_status: "X [4P0..1]".into(),
4525 },
4526 AhbCodeRule {
4527 value: "E02".into(),
4528 description: "Code 2".into(),
4529 ahb_status: "X [4P0..1]".into(),
4530 },
4531 ],
4532 parent_group_ahb_status: Some("Muss".to_string()),
4533 segment_ahb_status: None,
4534 mig_number: None,
4535 format: None,
4536 }],
4537 };
4538
4539 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4540 let pkg_errors: Vec<_> = report
4541 .by_category(ValidationCategory::Ahb)
4542 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4543 .collect();
4544 assert_eq!(
4545 pkg_errors.len(),
4546 1,
4547 "2 codes present, max=1 — expected 1 error, got: {:?}",
4548 pkg_errors
4549 );
4550 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
4551 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
4552 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
4553 }
4554
4555 #[test]
4556 fn test_package_cardinality_too_few() {
4557 let evaluator = MockEvaluator::all_true(&[]);
4559 let validator = EdifactValidator::new(evaluator);
4560 let external = NoOpExternalProvider;
4561
4562 let segments = vec![OwnedSegment {
4563 id: "STS".into(),
4564 elements: vec![
4565 vec!["Z33".into()],
4566 vec![],
4567 vec!["X99".into()], ],
4569 segment_number: 5,
4570 }];
4571
4572 let workflow = AhbWorkflow {
4573 pruefidentifikator: "13017".to_string(),
4574 description: "Test".to_string(),
4575 communication_direction: None,
4576 ub_definitions: BTreeMap::new(),
4577 fields: vec![AhbFieldRule {
4578 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4579 name: "Statusanlaß, Code".to_string(),
4580 ahb_status: "X".to_string(),
4581 element_index: Some(2),
4582 component_index: Some(0),
4583 codes: vec![
4584 AhbCodeRule {
4585 value: "E01".into(),
4586 description: "Code 1".into(),
4587 ahb_status: "X [5P1..3]".into(),
4588 },
4589 AhbCodeRule {
4590 value: "E02".into(),
4591 description: "Code 2".into(),
4592 ahb_status: "X [5P1..3]".into(),
4593 },
4594 AhbCodeRule {
4595 value: "E03".into(),
4596 description: "Code 3".into(),
4597 ahb_status: "X [5P1..3]".into(),
4598 },
4599 ],
4600 parent_group_ahb_status: Some("Muss".to_string()),
4601 segment_ahb_status: None,
4602 mig_number: None,
4603 format: None,
4604 }],
4605 };
4606
4607 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4608 let pkg_errors: Vec<_> = report
4609 .by_category(ValidationCategory::Ahb)
4610 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4611 .collect();
4612 assert_eq!(
4613 pkg_errors.len(),
4614 1,
4615 "0 codes present, min=1 — expected 1 error, got: {:?}",
4616 pkg_errors
4617 );
4618 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
4619 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
4620 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
4621 }
4622
4623 #[test]
4624 fn test_package_cardinality_no_packages_in_workflow() {
4625 let evaluator = MockEvaluator::all_true(&[]);
4627 let validator = EdifactValidator::new(evaluator);
4628 let external = NoOpExternalProvider;
4629
4630 let segments = vec![OwnedSegment {
4631 id: "STS".into(),
4632 elements: vec![vec!["E01".into()]],
4633 segment_number: 5,
4634 }];
4635
4636 let workflow = AhbWorkflow {
4637 pruefidentifikator: "13017".to_string(),
4638 description: "Test".to_string(),
4639 communication_direction: None,
4640 ub_definitions: BTreeMap::new(),
4641 fields: vec![AhbFieldRule {
4642 segment_path: "STS/9015".to_string(),
4643 name: "Status Code".to_string(),
4644 ahb_status: "X".to_string(),
4645 codes: vec![AhbCodeRule {
4646 value: "E01".into(),
4647 description: "Code 1".into(),
4648 ahb_status: "X".into(),
4649 }],
4650 parent_group_ahb_status: Some("Muss".to_string()),
4651 segment_ahb_status: None,
4652 ..Default::default()
4653 }],
4654 };
4655
4656 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4657 let pkg_errors: Vec<_> = report
4658 .by_category(ValidationCategory::Ahb)
4659 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4660 .collect();
4661 assert!(
4662 pkg_errors.is_empty(),
4663 "No packages in workflow — no errors expected"
4664 );
4665 }
4666
4667 #[test]
4668 fn test_package_cardinality_with_condition_and_package() {
4669 let evaluator = MockEvaluator::all_true(&[901]);
4671 let validator = EdifactValidator::new(evaluator);
4672 let external = NoOpExternalProvider;
4673
4674 let segments = vec![OwnedSegment {
4675 id: "STS".into(),
4676 elements: vec![vec![], vec![], vec!["E01".into()]],
4677 segment_number: 5,
4678 }];
4679
4680 let workflow = AhbWorkflow {
4681 pruefidentifikator: "13017".to_string(),
4682 description: "Test".to_string(),
4683 communication_direction: None,
4684 ub_definitions: BTreeMap::new(),
4685 fields: vec![AhbFieldRule {
4686 segment_path: "SG10/STS/C556/9013".to_string(),
4687 name: "Code".to_string(),
4688 ahb_status: "X".to_string(),
4689 element_index: Some(2),
4690 component_index: Some(0),
4691 codes: vec![
4692 AhbCodeRule {
4693 value: "E01".into(),
4694 description: "Code 1".into(),
4695 ahb_status: "X [901] [4P0..1]".into(),
4696 },
4697 AhbCodeRule {
4698 value: "E02".into(),
4699 description: "Code 2".into(),
4700 ahb_status: "X [901] [4P0..1]".into(),
4701 },
4702 ],
4703 parent_group_ahb_status: Some("Muss".to_string()),
4704 segment_ahb_status: None,
4705 mig_number: None,
4706 format: None,
4707 }],
4708 };
4709
4710 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4711 let pkg_errors: Vec<_> = report
4712 .by_category(ValidationCategory::Ahb)
4713 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4714 .collect();
4715 assert!(
4716 pkg_errors.is_empty(),
4717 "1 code within [4P0..1] bounds — no error, got: {:?}",
4718 pkg_errors
4719 );
4720 }
4721
4722 #[test]
4723 fn test_package_cardinality_scoped_per_group_instance() {
4724 use mig_types::navigator::GroupNavigator;
4732
4733 struct TwoSg10s {
4734 sts_a: OwnedSegment,
4735 sts_b: OwnedSegment,
4736 }
4737 impl GroupNavigator for TwoSg10s {
4738 fn find_segments_in_group(
4739 &self,
4740 segment_id: &str,
4741 group_path: &[&str],
4742 instance_index: usize,
4743 ) -> Vec<OwnedSegment> {
4744 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
4745 match instance_index {
4746 0 => vec![self.sts_a.clone()],
4747 1 => vec![self.sts_b.clone()],
4748 _ => vec![],
4749 }
4750 } else {
4751 vec![]
4752 }
4753 }
4754 fn find_segments_with_qualifier_in_group(
4755 &self,
4756 _: &str,
4757 _: usize,
4758 _: &str,
4759 _: &[&str],
4760 _: usize,
4761 ) -> Vec<OwnedSegment> {
4762 vec![]
4763 }
4764 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4765 match group_path {
4766 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
4767 ["SG5", "SG6", "SG9", "SG10"] => 2,
4768 _ => 0,
4769 }
4770 }
4771 }
4772
4773 let sts_a = OwnedSegment {
4774 id: "STS".into(),
4775 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4776 segment_number: 10,
4777 };
4778 let sts_b = OwnedSegment {
4779 id: "STS".into(),
4780 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4781 segment_number: 15,
4782 };
4783 let nav = TwoSg10s {
4784 sts_a: sts_a.clone(),
4785 sts_b: sts_b.clone(),
4786 };
4787
4788 let evaluator = MockEvaluator::all_true(&[]);
4789 let validator = EdifactValidator::new(evaluator);
4790 let external = NoOpExternalProvider;
4791
4792 let workflow = AhbWorkflow {
4793 pruefidentifikator: "13025".to_string(),
4794 description: "Test".to_string(),
4795 communication_direction: None,
4796 ub_definitions: BTreeMap::new(),
4797 fields: vec![AhbFieldRule {
4798 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4799 name: "Statusanlaß, Code".to_string(),
4800 ahb_status: "X".to_string(),
4801 element_index: Some(2),
4802 component_index: Some(0),
4803 codes: vec![
4804 AhbCodeRule {
4805 value: "E01".into(),
4806 description: "Code 1".into(),
4807 ahb_status: "X [4P0..1]".into(),
4808 },
4809 AhbCodeRule {
4810 value: "E02".into(),
4811 description: "Code 2".into(),
4812 ahb_status: "X [4P0..1]".into(),
4813 },
4814 ],
4815 parent_group_ahb_status: Some("Muss".to_string()),
4816 segment_ahb_status: None,
4817 mig_number: None,
4818 format: None,
4819 }],
4820 };
4821
4822 let report = validator.validate_with_navigator(
4823 &[sts_a, sts_b],
4824 &workflow,
4825 &external,
4826 ValidationLevel::Full,
4827 &nav,
4828 );
4829 let pkg_errors: Vec<_> = report
4830 .by_category(ValidationCategory::Ahb)
4831 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4832 .collect();
4833 assert!(
4834 pkg_errors.is_empty(),
4835 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
4836 pkg_errors
4837 );
4838 }
4839
4840 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
4841 OwnedSegment {
4842 id: id.to_string(),
4843 elements: elements
4844 .into_iter()
4845 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
4846 .collect(),
4847 segment_number: 0,
4848 }
4849 }
4850
4851 #[test]
4862 fn test_package_cardinality_scoped_to_rule_mig_variant() {
4863 use mig_types::navigator::GroupNavigator;
4864
4865 struct TwoSg8Variants {
4866 seq_z01: OwnedSegment,
4867 seq_z45: OwnedSegment,
4868 }
4869 impl GroupNavigator for TwoSg8Variants {
4870 fn find_segments_in_group(
4871 &self,
4872 segment_id: &str,
4873 group_path: &[&str],
4874 instance_index: usize,
4875 ) -> Vec<OwnedSegment> {
4876 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
4877 match instance_index {
4878 0 => vec![self.seq_z01.clone()],
4879 1 => vec![self.seq_z45.clone()],
4880 _ => vec![],
4881 }
4882 } else {
4883 vec![]
4884 }
4885 }
4886 fn find_segments_with_qualifier_in_group(
4887 &self,
4888 _: &str,
4889 _: usize,
4890 _: &str,
4891 _: &[&str],
4892 _: usize,
4893 ) -> Vec<OwnedSegment> {
4894 vec![]
4895 }
4896 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4897 match group_path {
4898 ["SG4"] => 1,
4899 ["SG4", "SG8"] => 2,
4900 _ => 0,
4901 }
4902 }
4903 fn instance_has_mig_number(
4904 &self,
4905 group_path: &[&str],
4906 instance_index: usize,
4907 mig_number: &str,
4908 ) -> bool {
4909 if group_path != ["SG4", "SG8"] {
4910 return true;
4911 }
4912 match (instance_index, mig_number) {
4913 (0, "00115") => true,
4914 (0, _) => false,
4915 (1, "00171") => true,
4916 (1, _) => false,
4917 _ => false,
4918 }
4919 }
4920 }
4921
4922 let seq_z01 = OwnedSegment {
4923 id: "SEQ".into(),
4924 elements: vec![vec!["Z01".into()], vec!["1".into()]],
4925 segment_number: 10,
4926 };
4927 let seq_z45 = OwnedSegment {
4928 id: "SEQ".into(),
4929 elements: vec![vec!["Z45".into()], vec!["1".into()]],
4930 segment_number: 20,
4931 };
4932 let nav = TwoSg8Variants {
4933 seq_z01: seq_z01.clone(),
4934 seq_z45: seq_z45.clone(),
4935 };
4936
4937 let evaluator = MockEvaluator::all_true(&[]);
4938 let validator = EdifactValidator::new(evaluator);
4939 let external = NoOpExternalProvider;
4940
4941 let workflow = AhbWorkflow {
4942 pruefidentifikator: "55218".to_string(),
4943 description: "Test".to_string(),
4944 communication_direction: None,
4945 ub_definitions: BTreeMap::new(),
4946 fields: vec![AhbFieldRule {
4947 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4948 name: "Handlung, Code".to_string(),
4949 ahb_status: "X".to_string(),
4950 element_index: Some(0),
4951 component_index: Some(0),
4952 codes: vec![
4953 AhbCodeRule {
4954 value: "Z45".into(),
4955 description: "NNA".into(),
4956 ahb_status: "X [1P1..4294967295]".into(),
4957 },
4958 AhbCodeRule {
4959 value: "Z84".into(),
4960 description: "Differenz-NNA".into(),
4961 ahb_status: "X [1P0..4294967295]".into(),
4962 },
4963 ],
4964 parent_group_ahb_status: Some("Muss".to_string()),
4965 segment_ahb_status: None,
4966 mig_number: Some("00171".to_string()),
4967 format: None,
4968 }],
4969 };
4970
4971 let report = validator.validate_with_navigator(
4972 &[seq_z01, seq_z45],
4973 &workflow,
4974 &external,
4975 ValidationLevel::Full,
4976 &nav,
4977 );
4978 let pkg_errors: Vec<_> = report
4979 .by_category(ValidationCategory::Ahb)
4980 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4981 .collect();
4982 assert!(
4983 pkg_errors.is_empty(),
4984 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
4985 pkg_errors
4986 );
4987 }
4988
4989 #[test]
4990 fn test_unt_count_correct() {
4991 let segments = vec![
4993 make_segment("UNH", vec![vec!["001"]]),
4994 make_segment("BGM", vec![vec!["E01"]]),
4995 make_segment("DTM", vec![vec!["137", "20250401"]]),
4996 make_segment("UNT", vec![vec!["4", "001"]]),
4997 ];
4998 assert!(
4999 validate_unt_segment_count(&segments).is_none(),
5000 "Correct count should produce no issue"
5001 );
5002 }
5003
5004 #[test]
5005 fn test_unt_count_mismatch() {
5006 let segments = vec![
5008 make_segment("UNH", vec![vec!["001"]]),
5009 make_segment("BGM", vec![vec!["E01"]]),
5010 make_segment("UNT", vec![vec!["5", "001"]]),
5011 ];
5012 let issue =
5013 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
5014 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5015 assert_eq!(issue.severity, Severity::Error);
5016 assert!(narrate(&issue).contains("declared 5"));
5017 assert!(narrate(&issue).contains("actual 3"));
5018 }
5019
5020 #[test]
5021 fn test_unt_count_excludes_envelope() {
5022 let segments = vec![
5024 make_segment("UNA", vec![]),
5025 make_segment("UNB", vec![vec!["UNOC", "3"]]),
5026 make_segment("UNH", vec![vec!["001"]]),
5027 make_segment("BGM", vec![vec!["E01"]]),
5028 make_segment("UNT", vec![vec!["3", "001"]]),
5029 make_segment("UNZ", vec![vec!["1"]]),
5030 ];
5031 assert!(
5032 validate_unt_segment_count(&segments).is_none(),
5033 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
5034 );
5035 }
5036
5037 #[test]
5038 fn test_unt_count_no_unt_returns_none() {
5039 let segments = vec![
5040 make_segment("UNH", vec![vec!["001"]]),
5041 make_segment("BGM", vec![vec!["E01"]]),
5042 ];
5043 assert!(
5044 validate_unt_segment_count(&segments).is_none(),
5045 "No UNT segment should return None (not our problem)"
5046 );
5047 }
5048
5049 #[test]
5050 fn test_unt_count_rejects_multi_message_input() {
5051 let segments = vec![
5053 make_segment("UNH", vec![vec!["001"]]),
5054 make_segment("BGM", vec![vec!["E01"]]),
5055 make_segment("UNT", vec![vec!["3", "001"]]),
5056 make_segment("UNH", vec![vec!["002"]]),
5057 make_segment("BGM", vec![vec!["E02"]]),
5058 make_segment("UNT", vec![vec!["3", "002"]]),
5059 ];
5060 let issue = validate_unt_segment_count(&segments)
5061 .expect("Multi-message input should produce an error");
5062 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5063 assert!(
5064 narrate(&issue).contains("2 UNH"),
5065 "Should mention UNH count: {}",
5066 narrate(&issue)
5067 );
5068 }
5069
5070 #[test]
5071 fn test_code_validation_accepts_multi_code_variant_qualifier() {
5072 let evaluator = MockEvaluator::new(vec![]);
5079 let validator = EdifactValidator::new(evaluator);
5080 let external = NoOpExternalProvider;
5081
5082 let rff_z39 = OwnedSegment {
5083 id: "RFF".to_string(),
5084 elements: vec![
5085 vec!["RFF".to_string()],
5086 vec!["Z39".to_string(), "REF1".to_string()],
5087 ],
5088 segment_number: 1,
5089 };
5090
5091 let workflow = AhbWorkflow {
5092 pruefidentifikator: "55035".to_string(),
5093 description: "Test".to_string(),
5094 communication_direction: None,
5095 fields: vec![
5096 AhbFieldRule {
5098 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5099 name: "Referenznummer Qualifier".to_string(),
5100 ahb_status: "Muss".to_string(),
5101 codes: vec![
5102 AhbCodeRule {
5103 value: "Z31".to_string(),
5104 description: "".to_string(),
5105 ahb_status: "X".to_string(),
5106 },
5107 AhbCodeRule {
5108 value: "Z39".to_string(),
5109 description: "".to_string(),
5110 ahb_status: "X".to_string(),
5111 },
5112 ],
5113 parent_group_ahb_status: None,
5114 segment_ahb_status: None,
5115 element_index: Some(1),
5116 component_index: Some(0),
5117 mig_number: Some("00075".to_string()),
5118 format: None,
5119 },
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![AhbCodeRule {
5126 value: "Z33".to_string(),
5127 description: "".to_string(),
5128 ahb_status: "X".to_string(),
5129 }],
5130 parent_group_ahb_status: None,
5131 segment_ahb_status: None,
5132 element_index: Some(1),
5133 component_index: Some(0),
5134 mig_number: Some("00078".to_string()),
5135 format: None,
5136 },
5137 ],
5138 ub_definitions: BTreeMap::new(),
5139 };
5140
5141 let report = validator.validate(
5142 &[rff_z39],
5143 &workflow,
5144 &external,
5145 ValidationLevel::Conditions,
5146 );
5147
5148 let code_errors: Vec<_> = report
5149 .by_category(ValidationCategory::Code)
5150 .filter(|i| {
5151 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5152 })
5153 .collect();
5154 assert!(
5155 code_errors.is_empty(),
5156 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
5157 code_errors
5158 );
5159 }
5160
5161 #[test]
5162 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
5163 let evaluator = MockEvaluator::new(vec![]);
5169 let validator = EdifactValidator::new(evaluator);
5170 let external = NoOpExternalProvider;
5171
5172 let pia_5_z12 = OwnedSegment {
5176 id: "PIA".to_string(),
5177 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
5178 segment_number: 1,
5179 };
5180 let pia_5_srw = OwnedSegment {
5181 id: "PIA".to_string(),
5182 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
5183 segment_number: 2,
5184 };
5185
5186 let make_rules = |mig: &str, composite_code: &str| {
5187 vec![
5188 AhbFieldRule {
5189 segment_path: "SG4/SG8/PIA/4347".to_string(),
5190 name: "Produkt-ID-Funktion".to_string(),
5191 ahb_status: "Muss".to_string(),
5192 codes: vec![AhbCodeRule {
5193 value: "5".to_string(),
5194 description: "".to_string(),
5195 ahb_status: "X".to_string(),
5196 }],
5197 parent_group_ahb_status: None,
5198 segment_ahb_status: None,
5199 element_index: Some(0),
5200 component_index: Some(0),
5201 mig_number: Some(mig.to_string()),
5202 format: None,
5203 },
5204 AhbFieldRule {
5205 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
5206 name: "Artikel/Dienstleistung-Identifikator".to_string(),
5207 ahb_status: "Muss".to_string(),
5208 codes: vec![AhbCodeRule {
5209 value: composite_code.to_string(),
5210 description: "".to_string(),
5211 ahb_status: "X".to_string(),
5212 }],
5213 parent_group_ahb_status: None,
5214 segment_ahb_status: None,
5215 element_index: Some(1),
5216 component_index: Some(0),
5217 mig_number: Some(mig.to_string()),
5218 format: None,
5219 },
5220 ]
5221 };
5222
5223 let mut fields = make_rules("00108", "Z12");
5224 fields.extend(make_rules("00197", "SRW"));
5225
5226 let workflow = AhbWorkflow {
5227 pruefidentifikator: "55035".to_string(),
5228 description: "Test".to_string(),
5229 communication_direction: None,
5230 fields,
5231 ub_definitions: BTreeMap::new(),
5232 };
5233
5234 let report = validator.validate(
5235 &[pia_5_z12, pia_5_srw],
5236 &workflow,
5237 &external,
5238 ValidationLevel::Conditions,
5239 );
5240
5241 let code_errors: Vec<_> = report
5242 .by_category(ValidationCategory::Code)
5243 .filter(|i| {
5244 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5245 })
5246 .collect();
5247 assert!(
5248 code_errors.is_empty(),
5249 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
5250 code_errors
5251 );
5252 }
5253}