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 true,
225 &mut report,
226 );
227 }
228
229 let mut instance_counter: HashMap<&str, usize> = HashMap::new();
234 for group in &validated_tree.groups {
235 let instance_index = *instance_counter
236 .entry(group.group_id)
237 .and_modify(|c| *c += 1)
238 .or_insert(0);
239
240 let path_storage = [(group.group_id, instance_index)];
241 let scoped_ctx = ctx.with_scope(GroupScope {
244 path: &path_storage,
245 });
246
247 walk_group_instance(
248 group,
249 &path_storage,
250 &scoped_ctx,
251 &expr_eval,
252 &self.evaluator,
253 validated_tree.ub_definitions,
254 &tag_migs,
255 instance_index,
256 self.report_segments,
257 &mut report,
258 );
259 }
260
261 if let Some((once, transaction)) =
263 crate::eval::once_per_transaction(self.evaluator.message_type())
264 {
265 let mut index: HashMap<&str, usize> = HashMap::new();
266 for group in &validated_tree.groups {
267 let i = *index
268 .entry(group.group_id)
269 .and_modify(|c| *c += 1)
270 .or_insert(0);
271 if group.group_id == transaction {
272 report_repeated_in_transaction(group, i, once, &ctx, &mut report);
273 }
274 }
275 }
276
277 for field in &validated_tree.unmatched_rules {
283 if should_skip_for_parent_group(field, &expr_eval, &ctx, validated_tree.ub_definitions)
284 {
285 continue;
286 }
287
288 let status =
289 expr_eval.resolve_status(&field.ahb_status, &ctx, validated_tree.ub_definitions);
290
291 if status.result == ConditionResult::True
292 && status.kind == Some(StatusKind::Muss)
293 && !is_field_present(&ctx, field)
294 && !is_group_variant_absent(
295 &ctx,
296 field,
297 is_parent_group_required(
298 field,
299 &expr_eval,
300 &ctx,
301 validated_tree.ub_definitions,
302 ),
303 )
304 {
305 let mut issue = ValidationIssue::new(
306 Severity::Error,
307 IssueKind::MissingRequiredField {
308 field_name: field.name.clone(),
309 },
310 )
311 .with_field_path(&field.segment_path)
312 .with_rule(&field.ahb_status);
313 if let Some(first_code) = field.codes.first() {
314 issue.expected_value = Some(first_code.value.clone());
315 }
316 issue = attach_field_position(issue, field);
317 report.add_issue(issue);
318 }
319 }
320
321 if matches!(level, ValidationLevel::Full) {
325 let mut all_fields: Vec<AhbFieldRule> = Vec::new();
326 for node in &all_nodes {
327 all_fields.push(node.rule.clone());
328 }
329 for rule in &validated_tree.unmatched_rules {
330 all_fields.push((*rule).clone());
331 }
332 let synthetic_workflow = AhbWorkflow {
333 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
334 description: String::new(),
335 communication_direction: None,
336 fields: all_fields,
337 ub_definitions: validated_tree.ub_definitions.clone(),
338 };
339 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
340 self.validate_answer_lists(&synthetic_workflow, &ctx, &mut report);
341 self.validate_package_cardinality(&synthetic_workflow, &ctx, &mut report);
342 }
343
344 report
345 }
346
347 fn validate_conditions(
349 &self,
350 workflow: &AhbWorkflow,
351 ctx: &EvaluationContext,
352 report: &mut ValidationReport,
353 ) {
354 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
355
356 for field in &workflow.fields {
357 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
359 continue;
360 }
361
362 let StatusResolution {
365 result: condition_result,
366 kind,
367 unknown_ids,
368 } = expr_eval.resolve_status(&field.ahb_status, ctx, &workflow.ub_definitions);
369 let mandatory = kind == Some(StatusKind::Muss);
370
371 match condition_result {
372 ConditionResult::True => {
373 if mandatory
375 && !is_field_present(ctx, field)
376 && !is_group_variant_absent(
377 ctx,
378 field,
379 is_parent_group_required(
380 field,
381 &expr_eval,
382 ctx,
383 &workflow.ub_definitions,
384 ),
385 )
386 {
387 let mut issue = ValidationIssue::new(
388 Severity::Error,
389 IssueKind::MissingRequiredField {
390 field_name: field.name.clone(),
391 },
392 )
393 .with_field_path(&field.segment_path)
394 .with_rule(&field.ahb_status);
395 if let Some(first_code) = field.codes.first() {
399 issue.expected_value = Some(first_code.value.clone());
400 }
401 report.add_issue(issue);
402 }
403 }
404 ConditionResult::False => {
405 if mandatory && is_field_present(ctx, field) {
411 report.add_issue(
412 ValidationIssue::new(
413 Severity::Error,
414 IssueKind::FieldConditionNotSatisfied {
415 field_name: field.name.clone(),
416 },
417 )
418 .with_field_path(&field.segment_path)
419 .with_rule(&field.ahb_status),
420 );
421 }
422 }
423 ConditionResult::Unknown => {
424 let mut external_ids = Vec::new();
429 let mut undetermined_ids = Vec::new();
430 let mut missing_ids = Vec::new();
431 for id in unknown_ids {
432 if self.evaluator.is_external(id) {
433 external_ids.push(id);
434 } else if self.evaluator.is_known(id) {
435 undetermined_ids.push(id);
436 } else {
437 missing_ids.push(id);
438 }
439 }
440
441 report.add_issue(
442 ValidationIssue::new(
443 Severity::Info,
444 IssueKind::ConditionUnknown {
445 field_name: field.name.clone(),
446 unresolved: UnresolvedConditions {
447 external: external_ids,
448 undetermined: undetermined_ids,
449 missing: missing_ids,
450 },
451 },
452 )
453 .with_field_path(&field.segment_path)
454 .with_rule(&field.ahb_status),
455 );
456 }
457 }
458 }
459
460 self.validate_codes_cross_field(workflow, ctx, report);
465 self.validate_answer_lists(workflow, ctx, report);
466
467 self.validate_package_cardinality(workflow, ctx, report);
470 }
471
472 fn validate_package_cardinality(
479 &self,
480 workflow: &AhbWorkflow,
481 ctx: &EvaluationContext,
482 report: &mut ValidationReport,
483 ) {
484 struct PackageGroup {
487 min: u32,
488 max: u32,
489 max_sum: u32,
492 bounds: BTreeSet<(u32, u32)>,
493 code_values: Vec<String>,
494 element_index: usize,
495 component_index: usize,
496 }
497
498 let mut groups: HashMap<(String, Option<String>, u32, u32, u32), PackageGroup> =
507 HashMap::new();
508 let mut alternatives: HashMap<(String, Option<String>, u32), BTreeSet<u32>> =
512 HashMap::new();
513
514 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
515
516 for field in &workflow.fields {
517 let el_idx = field.element_index.unwrap_or(0);
518 let comp_idx = field.component_index.unwrap_or(0);
519
520 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
522 continue;
523 }
524
525 for code in &field.codes {
526 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
528 let mut packages = Vec::new();
530 collect_packages(&expr, &mut packages);
531
532 if packages.len() > 1 {
533 let ids: BTreeSet<u32> = packages.iter().map(|(id, _, _)| *id).collect();
534 for id in &ids {
535 alternatives
536 .entry((field.segment_path.clone(), field.mig_number.clone(), *id))
537 .or_default()
538 .extend(ids.iter().copied().filter(|other| other != id));
539 }
540 }
541
542 for (pkg_id, pkg_min, pkg_max) in packages {
543 let key = (
544 field.segment_path.clone(),
545 field.mig_number.clone(),
546 pkg_id,
547 0,
548 0,
549 );
550 let group = groups.entry(key).or_insert_with(|| PackageGroup {
551 min: pkg_min,
552 max: pkg_max,
553 max_sum: 0,
554 bounds: BTreeSet::new(),
555 code_values: Vec::new(),
556 element_index: el_idx,
557 component_index: comp_idx,
558 });
559 group.bounds.insert((pkg_min, pkg_max));
566 group.min = group.min.min(pkg_min);
567 if !group.code_values.contains(&code.value) {
568 group.max_sum = group.max_sum.saturating_add(pkg_max);
569 }
570 group.max = group.max.max(pkg_max);
571 group.code_values.push(code.value.clone());
572 }
573 }
574 }
575 }
576
577 let mut field_packages: HashMap<(String, Option<String>), BTreeSet<u32>> = HashMap::new();
588 for (seg_path, mig_number, pkg_id, _, _) in groups.keys() {
589 field_packages
590 .entry((seg_path.clone(), mig_number.clone()))
591 .or_default()
592 .insert(*pkg_id);
593 }
594 let with_alternatives: Vec<(String, Option<String>)> = alternatives
595 .keys()
596 .map(|(p, m, _)| (p.clone(), m.clone()))
597 .collect();
598 for field in with_alternatives {
599 let Some(ids) = field_packages.get(&field) else {
600 continue;
601 };
602 for id in ids {
603 alternatives
604 .entry((field.0.clone(), field.1.clone(), *id))
605 .or_default()
606 .extend(ids.iter().copied().filter(|other| other != id));
607 }
608 }
609
610 type Counted = (Vec<usize>, usize, usize, Vec<String>);
612 let mut counted: HashMap<(String, Option<String>, u32, u32, u32), Counted> = HashMap::new();
613 for ((seg_path, mig_number, pkg_id, _, _), group) in &groups {
614 let segment_id = extract_segment_id(seg_path);
615
616 let mut unique_codes: Vec<&str> =
620 group.code_values.iter().map(|s| s.as_str()).collect();
621 unique_codes.sort_unstable();
622 unique_codes.dedup();
623 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
624
625 let group_path_str = extract_group_path_key(seg_path);
626 let group_path: Vec<&str> = if group_path_str.is_empty() {
627 Vec::new()
628 } else {
629 group_path_str.split('/').collect()
630 };
631
632 let min = group.min as usize;
633 let max = if group.bounds.len() > 1 {
634 group.max_sum as usize
635 } else {
636 group.max as usize
637 };
638
639 let per_instance_counts: Option<Vec<usize>> =
640 match (ctx.navigator, group_path.is_empty()) {
641 (Some(nav), false)
649 if group_path.len() > 1
650 && !full_instance_paths(nav, &group_path).is_empty() =>
651 {
652 let paths = full_instance_paths(nav, &group_path);
653 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
661 .as_deref()
662 {
663 Some(m) => (0..group_path.len())
664 .filter_map(|level| {
665 let prefix = group_path[..=level].join("/");
666 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
667 })
668 .collect(),
669 None => Vec::new(),
670 };
671 if paths.is_empty() {
672 None
673 } else {
674 Some(
675 paths
676 .iter()
677 .filter(|path| match mig_number.as_deref() {
678 Some(m) => nav.instance_at_has_mig_number(path, m),
679 None => true,
680 })
681 .filter(|path| {
682 variant_entries.iter().all(|(level, entry)| {
683 nav.find_segments_in_instance(
684 &entry.tag,
685 &path[..=*level],
686 )
687 .first()
688 .and_then(|seg| {
689 seg.elements
690 .get(entry.element)
691 .and_then(|e| e.get(entry.component))
692 .cloned()
693 })
694 .map_or(true, |v| entry.codes.contains(v.as_str()))
695 })
696 })
697 .map(|path| {
698 nav.find_segments_in_instance(&segment_id, path)
699 .iter()
700 .filter_map(|seg| {
701 seg.elements
702 .get(group.element_index)
703 .and_then(|e| e.get(group.component_index))
704 .filter(|v| !v.is_empty())
705 .cloned()
706 })
707 .filter(|v| code_set.contains(v.as_str()))
708 .count()
709 })
710 .collect(),
711 )
712 }
713 }
714 (Some(nav), false) => {
715 let instance_count = nav.group_instance_count(&group_path);
716 if instance_count == 0 {
717 None
718 } else {
719 Some(
727 (0..instance_count)
728 .filter(|i| match mig_number.as_deref() {
729 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
730 None => true,
731 })
732 .map(|i| {
733 nav.find_segments_in_group(&segment_id, &group_path, i)
734 .iter()
735 .filter_map(|seg| {
736 seg.elements
737 .get(group.element_index)
738 .and_then(|e| e.get(group.component_index))
739 .filter(|v| !v.is_empty())
740 .cloned()
741 })
742 .filter(|v| code_set.contains(v.as_str()))
743 .count()
744 })
745 .collect(),
746 )
747 }
748 }
749 _ => None,
750 };
751
752 let counts: Vec<usize> = per_instance_counts.unwrap_or_else(|| {
753 let segments = ctx.find_segments(&segment_id);
754 let count = segments
755 .iter()
756 .filter_map(|seg| {
757 seg.elements
758 .get(group.element_index)
759 .and_then(|e| e.get(group.component_index))
760 .filter(|v| !v.is_empty())
761 .map(|s| s.as_str())
762 })
763 .filter(|v| code_set.contains(v))
764 .count();
765 vec![count]
766 });
767
768 counted.insert(
769 (
770 seg_path.clone(),
771 mig_number.clone(),
772 *pkg_id,
773 group.min,
774 group.max,
775 ),
776 (
777 counts,
778 min,
779 max,
780 unique_codes.iter().map(|s| s.to_string()).collect(),
781 ),
782 );
783 }
784
785 let mut keys: Vec<&(String, Option<String>, u32, u32, u32)> = counted.keys().collect();
790 keys.sort();
791 for key in keys {
792 let (counts, min, max, codes) = &counted[key];
793 let (seg_path, mig_number, pkg_id, _, _) = key;
794 let alternative_holds = |i: usize| {
795 alternatives
796 .get(&(seg_path.clone(), mig_number.clone(), *pkg_id))
797 .is_some_and(|alts| {
798 counted.iter().any(|((p, m, id, _, _), (c, lo, hi, _))| {
799 p == seg_path
800 && m == mig_number
801 && alts.contains(id)
802 && c.len() == counts.len()
803 && (*lo..=*hi).contains(&c[i])
804 && c[i] > 0
805 })
806 })
807 };
808 let mut reported_counts: HashSet<usize> = HashSet::new();
809 for (i, &present_count) in counts.iter().enumerate() {
810 if (present_count < *min || present_count > *max)
811 && !alternative_holds(i)
812 && reported_counts.insert(present_count)
813 {
814 report.add_issue(
815 ValidationIssue::new(
816 Severity::Error,
817 IssueKind::PackageCardinality {
818 package_id: pkg_id.to_string(),
819 present: present_count,
820 min: *min,
821 max: *max,
822 codes: codes.clone(),
823 },
824 )
825 .with_field_path(seg_path)
826 .with_expected(format!("{min}..{max}"))
827 .with_actual(present_count.to_string()),
828 );
829 }
830 }
831 }
832 }
833
834 fn validate_codes_cross_field(
846 &self,
847 workflow: &AhbWorkflow,
848 ctx: &EvaluationContext,
849 report: &mut ValidationReport,
850 ) {
851 if ctx.navigator.is_some() {
852 self.validate_codes_group_scoped(workflow, ctx, report);
853 } else {
854 self.validate_codes_tag_scoped(workflow, ctx, report);
855 }
856 }
857
858 fn validate_codes_group_scoped(
861 &self,
862 workflow: &AhbWorkflow,
863 ctx: &EvaluationContext,
864 report: &mut ValidationReport,
865 ) {
866 let by_loc = partition_codes_by_mig(workflow);
867 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
868 let nav = ctx.navigator.unwrap();
869
870 for ((group_key, tag), migs) in &by_loc {
871 let field_path = if group_key.is_empty() {
872 format!("{tag}/qualifier")
873 } else {
874 format!("{group_key}/{tag}/qualifier")
875 };
876
877 let group_path: Vec<&str> = if group_key.is_empty() {
878 Vec::new()
879 } else {
880 group_key.split('/').collect()
881 };
882
883 let tag_qualifiers = known_qualifiers.get(tag);
884
885 if group_path.is_empty() {
886 Self::validate_segments_per_mig(
887 &ctx.find_segments(tag),
888 migs,
889 tag_qualifiers,
890 tag,
891 &field_path,
892 report,
893 );
894 } else {
895 let instance_count = nav.group_instance_count(&group_path);
896 for i in 0..instance_count {
897 let owned = nav.find_segments_in_group(tag, &group_path, i);
898 let refs: Vec<&OwnedSegment> = owned.iter().collect();
899 Self::validate_segments_per_mig(
900 &refs,
901 migs,
902 tag_qualifiers,
903 tag,
904 &field_path,
905 report,
906 );
907 }
908 }
909 }
910 }
911
912 fn validate_codes_tag_scoped(
916 &self,
917 workflow: &AhbWorkflow,
918 ctx: &EvaluationContext,
919 report: &mut ValidationReport,
920 ) {
921 let by_loc = partition_codes_by_mig(workflow);
922 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
923 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
925 for ((_group_key, tag), migs) in by_loc {
926 let merged = by_tag.entry(tag).or_default();
927 for (mig_key, bucket) in migs {
928 let entry = merged.entry(mig_key).or_default();
929 if entry.qualifier_position.is_none() {
930 entry.qualifier_position = bucket.qualifier_position;
931 }
932 if entry.qualifier_position == bucket.qualifier_position {
933 entry.qualifier_values.extend(&bucket.qualifier_values);
934 }
935 for (pos, codes) in bucket.codes {
936 entry.codes.entry(pos).or_default().extend(codes);
937 }
938 }
939 }
940
941 for (tag, migs) in &by_tag {
942 let field_path = format!("{tag}/qualifier");
943 Self::validate_segments_per_mig(
944 &ctx.find_segments(tag),
945 migs,
946 known_qualifiers.get(tag),
947 tag,
948 &field_path,
949 report,
950 );
951 }
952 }
953
954 fn validate_segments_per_mig(
963 segments: &[&OwnedSegment],
964 migs: &HashMap<Option<String>, MigCodeBucket>,
965 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
966 tag: &str,
967 field_path: &str,
968 report: &mut ValidationReport,
969 ) {
970 for seg in segments {
971 match match_segment_to_mig(seg, migs) {
972 Some(bucket) => {
973 for ((el, c), allowed) in &bucket.codes {
974 if allowed.is_empty() {
975 continue;
976 }
977 Self::check_segments_against_codes(
978 vec![*seg],
979 allowed,
980 tag,
981 *el,
982 *c,
983 field_path,
984 report,
985 );
986 }
987 }
988 None => {
989 let Some(qualifiers) = tag_qualifiers else {
990 continue;
991 };
992 for ((el, c), allowed) in qualifiers {
997 let Some(actual) = seg
998 .elements
999 .get(*el)
1000 .and_then(|e| e.get(*c))
1001 .filter(|v| !v.is_empty())
1002 .map(|s| s.as_str())
1003 else {
1004 continue;
1005 };
1006 if allowed.iter().any(|v| v == actual) {
1007 break;
1009 }
1010 let bucket_values: HashSet<&str> = migs
1013 .values()
1014 .filter(|b| b.qualifier_position == Some((*el, *c)))
1015 .flat_map(|b| b.qualifier_values.iter().copied())
1016 .collect();
1017 if !bucket_values.is_empty() {
1018 Self::check_segments_against_codes(
1019 vec![*seg],
1020 &bucket_values,
1021 tag,
1022 *el,
1023 *c,
1024 field_path,
1025 report,
1026 );
1027 break;
1028 }
1029 }
1030 }
1031 }
1032 }
1033 }
1034
1035 fn validate_answer_lists(
1044 &self,
1045 workflow: &AhbWorkflow,
1046 ctx: &EvaluationContext,
1047 report: &mut ValidationReport,
1048 ) {
1049 let lists = AnswerCodeLists::embedded();
1050 let fv = self.evaluator.format_version();
1051 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1052 for list_rule in &workflow.fields {
1053 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1054 continue;
1055 };
1056 if list_rule.codes.is_empty()
1057 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1058 {
1059 continue;
1060 }
1061 let Some(answer_rule) = workflow
1062 .fields
1063 .iter()
1064 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1065 else {
1066 continue;
1067 };
1068 let at = |rule: &AhbFieldRule| {
1069 (
1070 rule.element_index.unwrap_or(0),
1071 rule.component_index.unwrap_or(0),
1072 )
1073 };
1074 let (list_el, list_c) = at(list_rule);
1075 let (answer_el, answer_c) = at(answer_rule);
1076 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1077 seg.elements
1078 .get(el)
1079 .and_then(|e| e.get(c))
1080 .filter(|v| !v.is_empty())
1081 .cloned()
1082 };
1083 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1084 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1085 continue;
1086 }
1087 let (Some(list), Some(answer)) =
1088 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1089 else {
1090 continue;
1091 };
1092 if !list_rule.codes.iter().any(|c| c.value == list) {
1093 continue;
1094 }
1095 if answer_rule.codes.iter().any(|c| c.value == answer) {
1099 continue;
1100 }
1101 let Some(codes) = lists.codes(fv, &list) else {
1102 continue;
1103 };
1104 if codes.contains(&answer) {
1105 continue;
1106 }
1107 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1108 allowed.sort_unstable();
1109 report.add_issue(
1110 ValidationIssue::new(
1111 Severity::Warning,
1112 IssueKind::AnswerNotInList {
1113 answer: answer.clone(),
1114 list: list.clone(),
1115 allowed: allowed.clone(),
1116 },
1117 )
1118 .with_field_path(&answer_rule.segment_path)
1119 .with_actual(&answer)
1120 .with_expected(allowed.join(", ")),
1121 );
1122 }
1123 }
1124 }
1125
1126 fn check_segments_against_codes(
1128 segments: Vec<&OwnedSegment>,
1129 allowed_codes: &HashSet<&str>,
1130 _tag: &str,
1131 el_idx: usize,
1132 comp_idx: usize,
1133 field_path: &str,
1134 report: &mut ValidationReport,
1135 ) {
1136 for segment in segments {
1137 if let Some(code_value) = segment
1138 .elements
1139 .get(el_idx)
1140 .and_then(|e| e.get(comp_idx))
1141 .filter(|v| !v.is_empty())
1142 {
1143 if !allowed_codes.contains(code_value.as_str()) {
1144 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1145 sorted_codes.sort_unstable();
1146 let kind = IssueKind::CodeNotAllowedForPid {
1147 value: code_value.to_string(),
1148 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1149 };
1150 report.add_issue(
1151 ValidationIssue::new(Severity::Error, kind)
1152 .with_field_path(field_path)
1153 .with_actual(code_value)
1154 .with_expected(sorted_codes.join(", ")),
1155 );
1156 }
1157 }
1158 }
1159 }
1160}
1161
1162fn full_instance_paths<'p>(
1165 nav: &dyn GroupNavigator,
1166 group_path: &[&'p str],
1167) -> Vec<Vec<(&'p str, usize)>> {
1168 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1169 for g in group_path {
1170 paths = paths
1171 .into_iter()
1172 .flat_map(|parent| {
1173 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1174 let mut path = parent.clone();
1175 path.push((*g, i));
1176 path
1177 })
1178 })
1179 .collect();
1180 }
1181 paths
1182}
1183
1184struct VariantEntry<'a> {
1187 tag: String,
1188 element: usize,
1189 component: usize,
1190 codes: HashSet<&'a str>,
1191}
1192
1193fn merged_variant_entry<'a>(
1199 workflow: &'a AhbWorkflow,
1200 group_path: &str,
1201 mig: &str,
1202) -> Option<VariantEntry<'a>> {
1203 let mig: u32 = mig.parse().ok()?;
1204 let prefix = format!("{group_path}/");
1205 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1207 .fields
1208 .iter()
1209 .filter_map(|r| {
1210 let rest = r.segment_path.strip_prefix(&prefix)?;
1211 let tag = rest.split('/').next()?;
1212 if tag.starts_with("SG") {
1213 return None;
1214 }
1215 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1216 })
1217 .collect();
1218 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1220 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1221 .iter()
1222 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1223 .collect();
1224 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1225 if numbers.len() < 2 {
1226 return None;
1227 }
1228 let own = entries
1229 .iter()
1230 .filter(|(_, n, _)| *n <= mig)
1231 .map(|(_, n, _)| *n)
1232 .max()?;
1233 let (rule, _, tag) = entries
1235 .iter()
1236 .filter(|(_, n, _)| *n == own)
1237 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1238 Some(VariantEntry {
1239 tag: tag.to_string(),
1240 element: rule.element_index.unwrap_or(0),
1241 component: rule.component_index.unwrap_or(0),
1242 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1243 })
1244}
1245
1246fn value_length(format: &str, value: &str) -> usize {
1250 let format = format.trim();
1251 if format.starts_with('n') {
1252 value
1253 .chars()
1254 .filter(|c| !matches!(c, '.' | ',' | '-'))
1255 .count()
1256 } else {
1257 value.chars().count()
1258 }
1259}
1260
1261fn max_length(format: &str) -> Option<usize> {
1264 let digits = format
1265 .trim()
1266 .strip_prefix("an")
1267 .or_else(|| format.trim().strip_prefix('a'))
1268 .or_else(|| format.trim().strip_prefix('n'))?;
1269 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1270}
1271
1272fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1282 let derived_from_codes = field
1283 .codes
1284 .first()
1285 .is_some_and(|c| c.ahb_status == field.ahb_status);
1286 if !derived_from_codes {
1287 return &field.ahb_status;
1288 }
1289 value
1290 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1291 .map_or(&field.ahb_status, |c| &c.ahb_status)
1292}
1293
1294fn should_skip_for_parent_group<E: ConditionEvaluator>(
1300 field: &AhbFieldRule,
1301 expr_eval: &ConditionExprEvaluator<E>,
1302 ctx: &EvaluationContext,
1303 ub_definitions: &BTreeMap<String, ConditionExpr>,
1304) -> bool {
1305 if let Some(ref group_status) = field.parent_group_ahb_status {
1306 if group_status.contains('[') {
1307 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
1308 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
1309 }
1310 }
1311 false
1312}
1313
1314fn is_parent_group_required<E: ConditionEvaluator>(
1321 field: &AhbFieldRule,
1322 expr_eval: &ConditionExprEvaluator<E>,
1323 ctx: &EvaluationContext,
1324 ub_definitions: &BTreeMap<String, ConditionExpr>,
1325) -> bool {
1326 field
1327 .parent_group_ahb_status
1328 .as_deref()
1329 .is_some_and(|status| {
1330 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1331 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1332 })
1333}
1334
1335fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1344 let segment_id = extract_segment_id(&field.segment_path);
1345
1346 if !field.codes.is_empty() {
1352 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1353 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1354 let matching = ctx.find_segments(&segment_id);
1355 return matching.iter().any(|seg| {
1356 seg.elements
1357 .get(el_idx)
1358 .and_then(|e| e.get(comp_idx))
1359 .is_some_and(|v| required_codes.contains(&v.as_str()))
1360 });
1361 }
1362 if is_qualifier_field(&field.segment_path) {
1365 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1366 let el_idx = field.element_index.unwrap_or(0);
1367 let comp_idx = field.component_index.unwrap_or(0);
1368 let matching = ctx.find_segments(&segment_id);
1369 return matching.iter().any(|seg| {
1370 seg.elements
1371 .get(el_idx)
1372 .and_then(|e| e.get(comp_idx))
1373 .is_some_and(|v| required_codes.contains(&v.as_str()))
1374 });
1375 }
1376 }
1377
1378 ctx.has_segment(&segment_id)
1379}
1380
1381fn is_group_variant_absent(
1396 ctx: &EvaluationContext,
1397 field: &AhbFieldRule,
1398 is_group_mandatory: bool,
1399) -> bool {
1400 let group_path: Vec<&str> = field
1401 .segment_path
1402 .split('/')
1403 .take_while(|p| p.starts_with("SG"))
1404 .collect();
1405
1406 if group_path.is_empty() {
1407 return false;
1408 }
1409
1410 let nav = match ctx.navigator {
1411 Some(nav) => nav,
1412 None => return false,
1413 };
1414
1415 let instance_count = nav.group_instance_count(&group_path);
1416
1417 if instance_count == 0 {
1422 if !is_group_mandatory {
1423 return true;
1424 }
1425 return false;
1427 }
1428
1429 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1434 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1436 let segment_id = extract_segment_id(&field.segment_path);
1437 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1438
1439 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1440 nav.find_segments_in_group(&segment_id, &group_path, i)
1441 .iter()
1442 .any(|seg| {
1443 seg.elements
1444 .first()
1445 .and_then(|e| e.first())
1446 .is_some_and(|v| required_codes.contains(&v.as_str()))
1447 })
1448 });
1449
1450 if !any_instance_has_qualifier {
1451 return true; }
1453 }
1454 }
1455
1456 let segment_id = extract_segment_id(&field.segment_path);
1465 let segment_absent_from_all = (0..instance_count).all(|i| {
1466 nav.find_segments_in_group(&segment_id, &group_path, i)
1467 .is_empty()
1468 });
1469 if segment_absent_from_all {
1470 let group_has_other_segments =
1471 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1472 if group_has_other_segments {
1473 return true;
1474 }
1475 }
1476
1477 false
1478}
1479
1480fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1482 out.extend(group.fields.iter());
1483 for child in &group.children {
1484 collect_nodes_depth_first(child, out);
1485 }
1486}
1487
1488#[allow(clippy::too_many_arguments)]
1492fn evaluate_node<E: ConditionEvaluator>(
1493 node: &AhbNode,
1494 ctx: &EvaluationContext,
1495 expr_eval: &ConditionExprEvaluator<E>,
1496 evaluator: &E,
1497 ub_definitions: &BTreeMap<String, ConditionExpr>,
1498 tag_migs: &HashMap<String, HashSet<&str>>,
1499 instance_index: Option<usize>,
1500 slot_holds: bool,
1501 report: &mut ValidationReport,
1502) {
1503 let field = node.rule;
1504
1505 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1507
1508 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1513 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1514 if length > max {
1515 let mut issue = ValidationIssue::new(
1516 Severity::Error,
1517 IssueKind::ValueTooLong {
1518 field_name: field.name.clone(),
1519 length,
1520 max,
1521 },
1522 )
1523 .with_field_path(&field.segment_path)
1524 .with_actual(value)
1525 .with_expected(field.format.clone().unwrap_or_default());
1526 if let Some(idx) = instance_index {
1527 issue = issue.with_instance_index(idx);
1528 }
1529 if let Some(num) = node.matched_segment_number {
1530 issue = issue.with_position(crate::SegmentPosition {
1531 segment_number: num,
1532 byte_offset: 0,
1533 message_number: 1,
1534 });
1535 }
1536 report.add_issue(attach_field_position(issue, field));
1537 }
1538 }
1539
1540 if let (Some(value), Some(format)) = (node.value, field.format.as_deref()) {
1543 if slot_holds
1544 && format.trim().starts_with('n')
1545 && field.codes.is_empty()
1546 && !value.is_empty()
1547 && !crate::eval::format_validators::is_edifact_number(value)
1548 {
1549 let mut issue = ValidationIssue::new(
1550 Severity::Error,
1551 IssueKind::ValueNotNumeric {
1552 field_name: field.name.clone(),
1553 },
1554 )
1555 .with_field_path(&field.segment_path)
1556 .with_actual(value)
1557 .with_expected(format);
1558 if let Some(idx) = instance_index {
1559 issue = issue.with_instance_index(idx);
1560 }
1561 if let Some(num) = node.matched_segment_number {
1562 issue = issue.with_position(crate::SegmentPosition {
1563 segment_number: num,
1564 byte_offset: 0,
1565 message_number: 1,
1566 });
1567 }
1568 report.add_issue(attach_field_position(issue, field));
1569 }
1570 }
1571
1572 if should_skip_for_parent_group(field, expr_eval, ctx, ub_definitions) {
1574 return;
1575 }
1576
1577 let status = governing_status(field, node.value);
1580 let StatusResolution {
1581 result: condition_result,
1582 kind,
1583 unknown_ids,
1584 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1585 let mandatory = kind == Some(StatusKind::Muss);
1586
1587 match condition_result {
1588 ConditionResult::True => {
1589 if mandatory && node.value.is_none() {
1598 let tag = extract_segment_id(&field.segment_path);
1599 let single_variant_present = node.segment_elements.is_none()
1600 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1601 && is_field_present(ctx, field);
1602 let segment_optional_and_absent = node.segment_elements.is_none()
1607 && field
1608 .segment_ahb_status
1609 .as_deref()
1610 .is_some_and(is_optional_segment_status);
1611 let segment_not_required_and_absent = node.segment_elements.is_none()
1617 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1618 expr_eval
1619 .resolve_status(st, &node_ctx, ub_definitions)
1620 .result
1621 != ConditionResult::True
1622 });
1623 if !single_variant_present
1624 && !segment_optional_and_absent
1625 && !segment_not_required_and_absent
1626 {
1627 let mut issue = ValidationIssue::new(
1628 Severity::Error,
1629 IssueKind::MissingRequiredField {
1630 field_name: field.name.clone(),
1631 },
1632 )
1633 .with_field_path(&field.segment_path)
1634 .with_rule(&field.ahb_status);
1635 if let Some(first_code) = field.codes.first() {
1636 issue.expected_value = Some(first_code.value.clone());
1637 }
1638 if let Some(idx) = instance_index {
1639 issue = issue.with_instance_index(idx);
1640 }
1641 if let Some(num) = node.matched_segment_number {
1642 issue = issue.with_position(crate::SegmentPosition {
1643 segment_number: num,
1644 byte_offset: 0,
1645 message_number: 1,
1646 });
1647 }
1648 issue = attach_field_position(issue, field);
1649 report.add_issue(issue);
1650 }
1651 }
1652 }
1653 ConditionResult::False => {
1654 if mandatory && node.value.is_some() {
1657 let mut issue = ValidationIssue::new(
1658 Severity::Error,
1659 IssueKind::FieldConditionNotSatisfied {
1660 field_name: field.name.clone(),
1661 },
1662 )
1663 .with_field_path(&field.segment_path)
1664 .with_rule(&field.ahb_status);
1665 if let Some(idx) = instance_index {
1666 issue = issue.with_instance_index(idx);
1667 }
1668 if let Some(num) = node.matched_segment_number {
1669 issue = issue.with_position(crate::SegmentPosition {
1670 segment_number: num,
1671 byte_offset: 0,
1672 message_number: 1,
1673 });
1674 }
1675 issue = attach_field_position(issue, field);
1676 report.add_issue(issue);
1677 }
1678 }
1679 ConditionResult::Unknown => {
1680 let mut external_ids = Vec::new();
1682 let mut undetermined_ids = Vec::new();
1683 let mut missing_ids = Vec::new();
1684 for id in unknown_ids {
1685 if evaluator.is_external(id) {
1686 external_ids.push(id);
1687 } else if evaluator.is_known(id) {
1688 undetermined_ids.push(id);
1689 } else {
1690 missing_ids.push(id);
1691 }
1692 }
1693
1694 let mut issue = ValidationIssue::new(
1695 Severity::Info,
1696 IssueKind::ConditionUnknown {
1697 field_name: field.name.clone(),
1698 unresolved: UnresolvedConditions {
1699 external: external_ids,
1700 undetermined: undetermined_ids,
1701 missing: missing_ids,
1702 },
1703 },
1704 )
1705 .with_field_path(&field.segment_path)
1706 .with_rule(&field.ahb_status);
1707 if let Some(idx) = instance_index {
1708 issue = issue.with_instance_index(idx);
1709 }
1710 if let Some(num) = node.matched_segment_number {
1711 issue = issue.with_position(crate::SegmentPosition {
1712 segment_number: num,
1713 byte_offset: 0,
1714 message_number: 1,
1715 });
1716 }
1717 issue = attach_field_position(issue, field);
1718 report.add_issue(issue);
1719 }
1720 }
1721}
1722
1723fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1728 match field.element_index {
1729 Some(el) => {
1730 let element_pos = (el as u32) + 2;
1731 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1732 issue.with_field_position(element_pos, component_pos)
1733 }
1734 None => issue,
1735 }
1736}
1737
1738fn report_repeated_in_transaction(
1745 transaction: &AhbGroupNode,
1746 transaction_index: usize,
1747 once: u32,
1748 ctx: &EvaluationContext,
1749 report: &mut ValidationReport,
1750) {
1751 let mut counts: BTreeMap<(String, String), (usize, String)> = BTreeMap::new();
1753 fn visit<'a>(
1754 node: &'a AhbGroupNode<'a>,
1755 path: &mut Vec<(&'a str, usize)>,
1756 once: u32,
1757 ctx: &EvaluationContext,
1758 counts: &mut BTreeMap<(String, String), (usize, String)>,
1759 ) {
1760 let ids: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1761 let mut seen: HashSet<Option<&str>> = HashSet::new();
1762 for n in &node.fields {
1763 let rule = n.rule;
1764 let Some(status) = rule.segment_ahb_status.as_deref() else {
1765 continue;
1766 };
1767 if Some(status) == node.ahb_status
1768 || rule.element_index.unwrap_or(0) != 0
1769 || rule.component_index.unwrap_or(0) != 0
1770 || rule.codes.is_empty()
1771 || !crate::eval::binds_once(status, once)
1772 || !seen.insert(rule.mig_number.as_deref())
1773 {
1774 continue;
1775 }
1776 let tag = extract_segment_id(&rule.segment_path);
1777 let sent = ctx.navigator().map_or(0, |nav| {
1778 nav.find_segments_in_instance(&tag, path)
1779 .iter()
1780 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
1781 .count()
1782 });
1783 let entry = counts
1784 .entry((
1785 format!("{}/{tag}", ids.join("/")),
1786 rule.mig_number.clone().unwrap_or_default(),
1787 ))
1788 .or_insert((0, status.to_string()));
1789 entry.0 += sent;
1790 }
1791 for child in &node.children {
1792 if let Some(status) = child
1793 .ahb_status
1794 .filter(|s| crate::eval::binds_once(s, once))
1795 {
1796 let variant = child
1797 .fields
1798 .first()
1799 .and_then(|n| n.rule.mig_number.clone())
1800 .unwrap_or_default();
1801 let entry = counts
1802 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
1803 .or_insert((0, status.to_string()));
1804 entry.0 += 1;
1805 }
1806 path.push((child.group_id, child.instance_index));
1807 visit(child, path, once, ctx, counts);
1808 path.pop();
1809 }
1810 }
1811 let mut path = vec![(transaction.group_id, transaction_index)];
1812 visit(transaction, &mut path, once, ctx, &mut counts);
1813 for ((path, _), (count, status)) in counts {
1814 if count > 1 {
1815 report.add_issue(
1816 ValidationIssue::new(
1817 Severity::Error,
1818 IssueKind::RepeatedInTransaction {
1819 path: path.clone(),
1820 count,
1821 },
1822 )
1823 .with_field_path(path)
1824 .with_rule(status)
1825 .with_instance_index(transaction_index),
1826 );
1827 }
1828 }
1829}
1830
1831fn refuse_segments<E: ConditionEvaluator>(
1837 group: &AhbGroupNode,
1838 path: &[(&str, usize)],
1839 scoped_ctx: &EvaluationContext,
1840 evaluator: &E,
1841 ub_definitions: &BTreeMap<String, ConditionExpr>,
1842 instance_index: usize,
1843 report: &mut ValidationReport,
1844) {
1845 let mut seen: Vec<u32> = Vec::new();
1846 for node in &group.fields {
1847 let rule = node.rule;
1848 let (Some(status), Some(value), Some(number)) = (
1849 rule.segment_ahb_status.as_deref(),
1850 node.value,
1851 node.matched_segment_number,
1852 ) else {
1853 continue;
1854 };
1855 let qualifier =
1858 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
1859 if !qualifier
1860 || seen.contains(&number)
1861 || Some(status) == group.ahb_status
1862 || !rule.codes.iter().any(|c| c.value == value)
1863 {
1864 continue;
1865 }
1866 seen.push(number);
1867 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1868 continue;
1869 }
1870 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1871 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
1872 report.add_issue(
1873 ValidationIssue::new(
1874 Severity::Warning,
1875 IssueKind::SegmentNotAllowed {
1876 segment: segment.join("/"),
1877 },
1878 )
1879 .with_field_path(segment.join("/"))
1880 .with_rule(status)
1881 .with_instance_index(instance_index)
1882 .with_position(crate::SegmentPosition {
1883 segment_number: number,
1884 byte_offset: 0,
1885 message_number: 1,
1886 }),
1887 );
1888 }
1889}
1890
1891#[allow(clippy::too_many_arguments)]
1900fn walk_group_instance<E: ConditionEvaluator>(
1901 group: &AhbGroupNode,
1902 path: &[(&str, usize)],
1903 scoped_ctx: &EvaluationContext,
1904 expr_eval: &ConditionExprEvaluator<E>,
1905 evaluator: &E,
1906 ub_definitions: &BTreeMap<String, ConditionExpr>,
1907 tag_migs: &HashMap<String, HashSet<&str>>,
1908 instance_index: usize,
1909 report_segments: bool,
1910 report: &mut ValidationReport,
1911) {
1912 let mut group_refused = false;
1915 if let Some(status) = group.ahb_status {
1916 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1917 group_refused = true;
1918 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1919 let mut issue = ValidationIssue::new(
1920 Severity::Warning,
1921 IssueKind::GroupNotAllowed {
1922 group: group_path.join("/"),
1923 },
1924 )
1925 .with_field_path(group_path.join("/"))
1926 .with_rule(status)
1927 .with_instance_index(instance_index);
1928 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
1929 issue = issue.with_position(crate::SegmentPosition {
1930 segment_number: num,
1931 byte_offset: 0,
1932 message_number: 1,
1933 });
1934 }
1935 report.add_issue(issue);
1936 }
1937 }
1938 if report_segments && !group_refused {
1939 refuse_segments(
1940 group,
1941 path,
1942 scoped_ctx,
1943 evaluator,
1944 ub_definitions,
1945 instance_index,
1946 report,
1947 );
1948 }
1949 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
1953 .fields
1954 .iter()
1955 .filter(|n| {
1956 n.rule.element_index.unwrap_or(0) == 0
1957 && n.rule.component_index.unwrap_or(0) == 0
1958 && !n.rule.codes.is_empty()
1959 })
1960 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
1961 .collect();
1962 let holds = |node: &AhbNode| {
1963 let (Some(codes), Some(elements)) = (
1964 slot_codes.get(&node.rule.mig_number.as_deref()),
1965 node.segment_elements,
1966 ) else {
1967 return true;
1968 };
1969 let qualifier = elements
1970 .first()
1971 .and_then(|e| e.first())
1972 .map(String::as_str)
1973 .unwrap_or_default();
1974 codes.iter().any(|c| c.value == qualifier)
1975 };
1976 for node in &group.fields {
1977 evaluate_node(
1978 node,
1979 scoped_ctx,
1980 expr_eval,
1981 evaluator,
1982 ub_definitions,
1983 tag_migs,
1984 Some(instance_index),
1985 holds(node),
1986 report,
1987 );
1988 }
1989 for child in &group.children {
1990 let mut child_path = path.to_vec();
1991 child_path.push((child.group_id, child.instance_index));
1992 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
1993 walk_group_instance(
1994 child,
1995 &child_path,
1996 &child_ctx,
1997 expr_eval,
1998 evaluator,
1999 ub_definitions,
2000 tag_migs,
2001 instance_index,
2002 report_segments,
2003 report,
2004 );
2005 }
2006}
2007
2008fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2010 match expr {
2011 ConditionExpr::Package { id, min, max } => {
2012 out.push((*id, *min, *max));
2013 }
2014 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2015 for e in exprs {
2016 collect_packages(e, out);
2017 }
2018 }
2019 ConditionExpr::Xor(left, right) => {
2020 collect_packages(left, out);
2021 collect_packages(right, out);
2022 }
2023 ConditionExpr::Not(inner) => {
2024 collect_packages(inner, out);
2025 }
2026 ConditionExpr::Ref(_) => {}
2027 }
2028}
2029
2030fn is_optional_segment_status(status: &str) -> bool {
2040 let trimmed = status.trim();
2041 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
2042}
2043
2044fn is_qualifier_field(path: &str) -> bool {
2054 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
2055 matches!(parts.len(), 2 | 3)
2057}
2058
2059#[derive(Default)]
2067struct MigCodeBucket<'a> {
2068 qualifier_position: Option<(usize, usize)>,
2071 qualifier_values: HashSet<&'a str>,
2074 codes: HashMap<(usize, usize), HashSet<&'a str>>,
2077}
2078
2079fn partition_codes_by_mig(
2084 workflow: &AhbWorkflow,
2085) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
2086 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
2087 for field in &workflow.fields {
2088 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2089 continue;
2090 }
2091 let tag = extract_segment_id(&field.segment_path);
2092 let group_key = extract_group_path_key(&field.segment_path);
2093 let mig = field.mig_number.clone();
2094 let el = field.element_index.unwrap_or(0);
2095 let c = field.component_index.unwrap_or(0);
2096
2097 let bucket = out
2098 .entry((group_key, tag))
2099 .or_default()
2100 .entry(mig)
2101 .or_default();
2102
2103 let required: Vec<&str> = field
2110 .codes
2111 .iter()
2112 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2113 .map(|code| code.value.as_str())
2114 .collect();
2115
2116 if !required.is_empty() {
2123 if bucket.qualifier_position.is_none() {
2124 bucket.qualifier_position = Some((el, c));
2125 }
2126 if bucket.qualifier_position == Some((el, c)) {
2127 bucket.qualifier_values.extend(required.iter().copied());
2128 }
2129 }
2130
2131 for v in required {
2132 bucket.codes.entry((el, c)).or_default().insert(v);
2133 }
2134 }
2135 out
2136}
2137
2138fn global_qualifiers_by_tag(
2144 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
2145) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
2146 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
2147 for ((_group, tag), migs) in by_loc {
2148 let tag_entry = out.entry(tag.clone()).or_default();
2149 for bucket in migs.values() {
2150 if let Some(pos) = bucket.qualifier_position {
2151 tag_entry
2152 .entry(pos)
2153 .or_default()
2154 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2155 }
2156 }
2157 }
2158 out
2159}
2160
2161fn match_segment_to_mig<'a, 'b>(
2172 seg: &OwnedSegment,
2173 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
2174) -> Option<&'a MigCodeBucket<'b>> {
2175 let actual_at = |el: usize, c: usize| -> &str {
2176 seg.elements
2177 .get(el)
2178 .and_then(|e| e.get(c))
2179 .map(|s| s.as_str())
2180 .unwrap_or("")
2181 };
2182
2183 let mut best: Option<&MigCodeBucket> = None;
2184 let mut best_matches = 0usize;
2185
2186 for bucket in migs.values() {
2187 let Some((el, c)) = bucket.qualifier_position else {
2188 continue;
2189 };
2190 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2191 continue;
2192 }
2193 let extra_matches = bucket
2194 .codes
2195 .iter()
2196 .filter(|(pos, _)| **pos != (el, c))
2197 .filter(|((e, k), allowed)| {
2198 let v = actual_at(*e, *k);
2199 !v.is_empty() && allowed.contains(v)
2200 })
2201 .count();
2202 if best.is_none() || extra_matches > best_matches {
2203 best = Some(bucket);
2204 best_matches = extra_matches;
2205 }
2206 }
2207 best
2208}
2209
2210fn extract_group_path_key(path: &str) -> String {
2215 let sg_parts: Vec<&str> = path
2216 .split('/')
2217 .take_while(|p| p.starts_with("SG"))
2218 .collect();
2219 sg_parts.join("/")
2220}
2221
2222fn extract_segment_id(path: &str) -> String {
2224 for part in path.split('/') {
2225 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2227 continue;
2228 }
2229 if part.len() >= 3
2231 && part
2232 .chars()
2233 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2234 {
2235 return part.to_string();
2236 }
2237 }
2238 path.split('/').next_back().unwrap_or(path).to_string()
2240}
2241
2242pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2254 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2256 if unh_count > 1 {
2257 return Some(ValidationIssue::new(
2258 Severity::Error,
2259 IssueKind::UntCountNotVerifiable { unh_count },
2260 ));
2261 }
2262
2263 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2265 let declared: usize = unt.get_element(0).parse().ok()?;
2266
2267 let actual = segments
2269 .iter()
2270 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2271 .count();
2272
2273 if declared != actual {
2274 Some(
2275 ValidationIssue::new(
2276 Severity::Error,
2277 IssueKind::UntSegmentCountMismatch { declared, actual },
2278 )
2279 .with_field_path("UNT/0074")
2280 .with_expected(actual.to_string())
2281 .with_actual(declared.to_string()),
2282 )
2283 } else {
2284 None
2285 }
2286}
2287
2288#[cfg(test)]
2289mod tests {
2290
2291 #[test]
2292 fn a_numeric_length_counts_digits_only() {
2293 assert_eq!(value_length("n..15", "-12.50"), 4);
2294 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
2295 assert_eq!(value_length("n5", "55001"), 5);
2296 assert_eq!(value_length("an..3", "Z.1"), 3);
2297 assert_eq!(value_length("an..35", "Müller"), 6);
2298 }
2299 use super::super::codes::ErrorCodes;
2300 use super::super::issue::ValidationCategory;
2301 use super::*;
2302 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2303 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2304 use std::collections::HashMap;
2305
2306 fn narrate(issue: &ValidationIssue) -> String {
2309 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2310 }
2311
2312 struct MockEvaluator {
2314 results: HashMap<u32, CR>,
2315 }
2316
2317 impl MockEvaluator {
2318 fn new(results: Vec<(u32, CR)>) -> Self {
2319 Self {
2320 results: results.into_iter().collect(),
2321 }
2322 }
2323
2324 fn all_true(ids: &[u32]) -> Self {
2325 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2326 }
2327 }
2328
2329 impl ConditionEvaluator for MockEvaluator {
2330 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2331 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2332 }
2333 fn is_external(&self, _condition: u32) -> bool {
2334 false
2335 }
2336 fn message_type(&self) -> &str {
2337 "UTILMD"
2338 }
2339 fn format_version(&self) -> &str {
2340 "FV2510"
2341 }
2342 }
2343
2344 #[test]
2347 fn a_present_code_is_judged_by_its_own_status() {
2348 let code = |value: &str, status: &str| AhbCodeRule {
2350 value: value.to_string(),
2351 description: String::new(),
2352 ahb_status: status.to_string(),
2353 };
2354 let field = AhbFieldRule {
2355 segment_path: "SG4/STS/C556/9013".to_string(),
2356 ahb_status: "X [192]".to_string(),
2357 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2358 ..Default::default()
2359 };
2360 assert_eq!(governing_status(&field, Some("E03")), "X");
2361 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2362 assert_eq!(governing_status(&field, None), "X [192]");
2363 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2364
2365 let own = AhbFieldRule {
2367 ahb_status: "X [931]".to_string(),
2368 codes: vec![code("303", "X")],
2369 ..Default::default()
2370 };
2371 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2372 }
2373
2374 #[test]
2375 fn test_extract_segment_id_simple() {
2376 assert_eq!(extract_segment_id("NAD"), "NAD");
2377 }
2378
2379 #[test]
2380 fn test_extract_segment_id_with_sg_prefix() {
2381 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
2382 }
2383
2384 #[test]
2385 fn test_extract_segment_id_nested_sg() {
2386 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
2387 }
2388
2389 #[test]
2392 fn test_validate_missing_mandatory_field() {
2393 let evaluator = MockEvaluator::all_true(&[182, 152]);
2394 let validator = EdifactValidator::new(evaluator);
2395 let external = NoOpExternalProvider;
2396
2397 let workflow = AhbWorkflow {
2398 pruefidentifikator: "11001".to_string(),
2399 description: "Test".to_string(),
2400 communication_direction: None,
2401 fields: vec![AhbFieldRule {
2402 segment_path: "SG2/NAD/C082/3039".to_string(),
2403 name: "MP-ID des MSB".to_string(),
2404 ahb_status: "Muss [182] ∧ [152]".to_string(),
2405 codes: vec![],
2406 parent_group_ahb_status: None,
2407 segment_ahb_status: None,
2408 ..Default::default()
2409 }],
2410 ub_definitions: BTreeMap::new(),
2411 };
2412
2413 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2415
2416 assert!(!report.is_valid());
2418 let errors: Vec<_> = report.errors().collect();
2419 assert_eq!(errors.len(), 1);
2420 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2421 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
2422 }
2423
2424 #[test]
2425 fn test_validate_condition_false_no_error() {
2426 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
2428 let validator = EdifactValidator::new(evaluator);
2429 let external = NoOpExternalProvider;
2430
2431 let workflow = AhbWorkflow {
2432 pruefidentifikator: "11001".to_string(),
2433 description: "Test".to_string(),
2434 communication_direction: None,
2435 fields: vec![AhbFieldRule {
2436 segment_path: "NAD".to_string(),
2437 name: "Partnerrolle".to_string(),
2438 ahb_status: "Muss [182] ∧ [152]".to_string(),
2439 codes: vec![],
2440 parent_group_ahb_status: None,
2441 segment_ahb_status: None,
2442 ..Default::default()
2443 }],
2444 ub_definitions: BTreeMap::new(),
2445 };
2446
2447 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2448
2449 assert!(report.is_valid());
2451 }
2452
2453 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
2455 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
2456 let workflow = AhbWorkflow {
2457 pruefidentifikator: "11001".to_string(),
2458 description: "Test".to_string(),
2459 communication_direction: None,
2460 fields: vec![AhbFieldRule {
2461 segment_path: "SG2/NAD/C082/3039".to_string(),
2462 name: "MP-ID".to_string(),
2463 ahb_status: status.to_string(),
2464 ..Default::default()
2465 }],
2466 ub_definitions: BTreeMap::new(),
2467 };
2468 validator.validate(
2469 &[],
2470 &workflow,
2471 &NoOpExternalProvider,
2472 ValidationLevel::Conditions,
2473 )
2474 }
2475
2476 #[test]
2477 fn a_later_soll_line_alone_does_not_require_the_field() {
2478 let report = validate_absent_field(
2480 "Muss [2119] \r\nSoll [130]",
2481 vec![(2119, CR::False), (130, CR::True)],
2482 );
2483 assert!(
2484 report.is_valid(),
2485 "{:?}",
2486 report.errors().collect::<Vec<_>>()
2487 );
2488 }
2489
2490 #[test]
2491 fn a_later_muss_line_requires_the_field() {
2492 let report = validate_absent_field(
2494 "Soll [47]\r\nMuss [46]",
2495 vec![(47, CR::False), (46, CR::True)],
2496 );
2497 let errors: Vec<_> = report.errors().collect();
2498 assert_eq!(errors.len(), 1);
2499 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2500 }
2501
2502 #[test]
2503 fn a_weaker_unknown_line_does_not_stop_a_muss() {
2504 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
2506 let errors: Vec<_> = report.errors().collect();
2507 assert_eq!(errors.len(), 1);
2508 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2509 }
2510
2511 #[test]
2512 fn a_stricter_unknown_line_leaves_the_status_undecided() {
2513 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
2514 assert!(report.is_valid());
2515 let infos: Vec<_> = report.infos().collect();
2516 assert_eq!(infos.len(), 1, "{infos:?}");
2517 match &infos[0].kind {
2518 IssueKind::ConditionUnknown { unresolved, .. } => {
2519 assert_eq!(unresolved.missing, vec![2119]);
2520 }
2521 other => panic!("expected ConditionUnknown, got {other:?}"),
2522 }
2523 }
2524
2525 #[test]
2526 fn test_validate_condition_unknown_adds_info() {
2527 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
2529 let validator = EdifactValidator::new(evaluator);
2531 let external = NoOpExternalProvider;
2532
2533 let workflow = AhbWorkflow {
2534 pruefidentifikator: "11001".to_string(),
2535 description: "Test".to_string(),
2536 communication_direction: None,
2537 fields: vec![AhbFieldRule {
2538 segment_path: "NAD".to_string(),
2539 name: "Partnerrolle".to_string(),
2540 ahb_status: "Muss [182] ∧ [152]".to_string(),
2541 codes: vec![],
2542 parent_group_ahb_status: None,
2543 segment_ahb_status: None,
2544 ..Default::default()
2545 }],
2546 ub_definitions: BTreeMap::new(),
2547 };
2548
2549 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2550
2551 assert!(report.is_valid());
2553 let infos: Vec<_> = report.infos().collect();
2554 assert_eq!(infos.len(), 1);
2555 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
2556 }
2557
2558 #[test]
2559 fn test_validate_structure_level_skips_conditions() {
2560 let evaluator = MockEvaluator::all_true(&[182, 152]);
2561 let validator = EdifactValidator::new(evaluator);
2562 let external = NoOpExternalProvider;
2563
2564 let workflow = AhbWorkflow {
2565 pruefidentifikator: "11001".to_string(),
2566 description: "Test".to_string(),
2567 communication_direction: None,
2568 fields: vec![AhbFieldRule {
2569 segment_path: "NAD".to_string(),
2570 name: "Partnerrolle".to_string(),
2571 ahb_status: "Muss [182] ∧ [152]".to_string(),
2572 codes: vec![],
2573 parent_group_ahb_status: None,
2574 segment_ahb_status: None,
2575 ..Default::default()
2576 }],
2577 ub_definitions: BTreeMap::new(),
2578 };
2579
2580 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
2582
2583 assert!(report.is_valid());
2585 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
2586 }
2587
2588 #[test]
2589 fn test_validate_empty_workflow_no_condition_errors() {
2590 let evaluator = MockEvaluator::all_true(&[]);
2591 let validator = EdifactValidator::new(evaluator);
2592 let external = NoOpExternalProvider;
2593
2594 let empty_workflow = AhbWorkflow {
2595 pruefidentifikator: String::new(),
2596 description: String::new(),
2597 communication_direction: None,
2598 fields: vec![],
2599 ub_definitions: BTreeMap::new(),
2600 };
2601
2602 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
2603
2604 assert!(report.is_valid());
2605 }
2606
2607 #[test]
2608 fn test_validate_bare_muss_always_required() {
2609 let evaluator = MockEvaluator::new(vec![]);
2610 let validator = EdifactValidator::new(evaluator);
2611 let external = NoOpExternalProvider;
2612
2613 let workflow = AhbWorkflow {
2614 pruefidentifikator: "55001".to_string(),
2615 description: "Test".to_string(),
2616 communication_direction: Some("NB an LF".to_string()),
2617 fields: vec![AhbFieldRule {
2618 segment_path: "SG2/NAD/3035".to_string(),
2619 name: "Partnerrolle".to_string(),
2620 ahb_status: "Muss".to_string(), codes: vec![],
2622 parent_group_ahb_status: None,
2623 segment_ahb_status: None,
2624 ..Default::default()
2625 }],
2626 ub_definitions: BTreeMap::new(),
2627 };
2628
2629 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2630
2631 assert!(!report.is_valid());
2633 assert_eq!(report.error_count(), 1);
2634 }
2635
2636 #[test]
2637 fn test_validate_x_status_is_mandatory() {
2638 let evaluator = MockEvaluator::new(vec![]);
2639 let validator = EdifactValidator::new(evaluator);
2640 let external = NoOpExternalProvider;
2641
2642 let workflow = AhbWorkflow {
2643 pruefidentifikator: "55001".to_string(),
2644 description: "Test".to_string(),
2645 communication_direction: None,
2646 fields: vec![AhbFieldRule {
2647 segment_path: "DTM".to_string(),
2648 name: "Datum".to_string(),
2649 ahb_status: "X".to_string(),
2650 codes: vec![],
2651 parent_group_ahb_status: None,
2652 segment_ahb_status: None,
2653 ..Default::default()
2654 }],
2655 ub_definitions: BTreeMap::new(),
2656 };
2657
2658 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2659
2660 assert!(!report.is_valid());
2661 let errors: Vec<_> = report.errors().collect();
2662 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2663 }
2664
2665 #[test]
2666 fn test_validate_soll_not_mandatory() {
2667 let evaluator = MockEvaluator::new(vec![]);
2668 let validator = EdifactValidator::new(evaluator);
2669 let external = NoOpExternalProvider;
2670
2671 let workflow = AhbWorkflow {
2672 pruefidentifikator: "55001".to_string(),
2673 description: "Test".to_string(),
2674 communication_direction: None,
2675 fields: vec![AhbFieldRule {
2676 segment_path: "DTM".to_string(),
2677 name: "Datum".to_string(),
2678 ahb_status: "Soll".to_string(),
2679 codes: vec![],
2680 parent_group_ahb_status: None,
2681 segment_ahb_status: None,
2682 ..Default::default()
2683 }],
2684 ub_definitions: BTreeMap::new(),
2685 };
2686
2687 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2688
2689 assert!(report.is_valid());
2691 }
2692
2693 #[test]
2694 fn test_report_includes_metadata() {
2695 let evaluator = MockEvaluator::new(vec![]);
2696 let validator = EdifactValidator::new(evaluator);
2697 let external = NoOpExternalProvider;
2698
2699 let workflow = AhbWorkflow {
2700 pruefidentifikator: "55001".to_string(),
2701 description: String::new(),
2702 communication_direction: None,
2703 fields: vec![],
2704 ub_definitions: BTreeMap::new(),
2705 };
2706
2707 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
2708
2709 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
2710 assert_eq!(report.level, ValidationLevel::Full);
2711 assert_eq!(report.message_type, "UTILMD");
2712 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
2713 }
2714
2715 #[test]
2716 fn test_validate_with_navigator_returns_report() {
2717 let evaluator = MockEvaluator::all_true(&[]);
2718 let validator = EdifactValidator::new(evaluator);
2719 let external = NoOpExternalProvider;
2720 let nav = crate::eval::NoOpGroupNavigator;
2721
2722 let workflow = AhbWorkflow {
2723 pruefidentifikator: "55001".to_string(),
2724 description: "Test".to_string(),
2725 communication_direction: None,
2726 fields: vec![],
2727 ub_definitions: BTreeMap::new(),
2728 };
2729
2730 let report = validator.validate_with_navigator(
2731 &[],
2732 &workflow,
2733 &external,
2734 ValidationLevel::Full,
2735 &nav,
2736 );
2737 assert!(report.is_valid());
2738 }
2739
2740 #[test]
2741 fn test_code_validation_composite_paths_valid_codes() {
2742 let evaluator = MockEvaluator::new(vec![]);
2746 let validator = EdifactValidator::new(evaluator);
2747 let external = NoOpExternalProvider;
2748
2749 let unh_segment = OwnedSegment {
2750 id: "UNH".to_string(),
2751 elements: vec![
2752 vec!["ALEXANDE951842".to_string()],
2753 vec![
2754 "UTILMD".to_string(),
2755 "D".to_string(),
2756 "11A".to_string(),
2757 "UN".to_string(),
2758 "S2.1".to_string(),
2759 ],
2760 ],
2761 segment_number: 1,
2762 };
2763
2764 let workflow = AhbWorkflow {
2765 pruefidentifikator: "55001".to_string(),
2766 description: "Test".to_string(),
2767 communication_direction: None,
2768 fields: vec![
2769 AhbFieldRule {
2770 segment_path: "UNH/S009/0065".to_string(),
2771 name: "Nachrichtentyp".to_string(),
2772 ahb_status: "X".to_string(),
2773 codes: vec![AhbCodeRule {
2774 value: "UTILMD".to_string(),
2775 description: "Stammdaten".to_string(),
2776 ahb_status: "X".to_string(),
2777 }],
2778 parent_group_ahb_status: None,
2779 segment_ahb_status: None,
2780 element_index: Some(1),
2781 component_index: Some(0),
2782 ..Default::default()
2783 },
2784 AhbFieldRule {
2785 segment_path: "UNH/S009/0052".to_string(),
2786 name: "Version".to_string(),
2787 ahb_status: "X".to_string(),
2788 codes: vec![AhbCodeRule {
2789 value: "D".to_string(),
2790 description: "Draft".to_string(),
2791 ahb_status: "X".to_string(),
2792 }],
2793 parent_group_ahb_status: None,
2794 segment_ahb_status: None,
2795 element_index: Some(1),
2796 component_index: Some(1),
2797 ..Default::default()
2798 },
2799 ],
2800 ub_definitions: BTreeMap::new(),
2801 };
2802
2803 let report = validator.validate(
2804 &[unh_segment],
2805 &workflow,
2806 &external,
2807 ValidationLevel::Conditions,
2808 );
2809
2810 let code_errors: Vec<_> = report
2811 .by_category(ValidationCategory::Code)
2812 .filter(|i| i.severity == Severity::Error)
2813 .collect();
2814 assert!(
2815 code_errors.is_empty(),
2816 "Expected no code errors when composite values match allowed codes, got: {:?}",
2817 code_errors
2818 );
2819 }
2820
2821 #[test]
2822 fn test_code_validation_partitions_by_mig_number() {
2823 let evaluator = MockEvaluator::new(vec![]);
2827 let validator = EdifactValidator::new(evaluator);
2828 let external = NoOpExternalProvider;
2829
2830 let sts_7 = OwnedSegment {
2831 id: "STS".to_string(),
2832 elements: vec![
2833 vec!["7".to_string()],
2834 vec![String::new()],
2835 vec!["GH02".to_string()],
2836 vec!["ZW4".to_string()],
2837 ],
2838 segment_number: 1,
2839 };
2840 let sts_e01 = OwnedSegment {
2841 id: "STS".to_string(),
2842 elements: vec![
2843 vec!["E01".to_string()],
2844 vec![String::new()],
2845 vec!["A99".to_string(), "E_0614".to_string()],
2846 ],
2847 segment_number: 2,
2848 };
2849
2850 let workflow = AhbWorkflow {
2851 pruefidentifikator: "55018".to_string(),
2852 description: "Test".to_string(),
2853 communication_direction: None,
2854 fields: vec![
2855 AhbFieldRule {
2857 segment_path: "SG4/STS/C601/9015".to_string(),
2858 name: "Statuskategorie".to_string(),
2859 ahb_status: "X".to_string(),
2860 codes: vec![AhbCodeRule {
2861 value: "7".to_string(),
2862 description: "Transaktionsgrund".to_string(),
2863 ahb_status: "X".to_string(),
2864 }],
2865 parent_group_ahb_status: None,
2866 segment_ahb_status: None,
2867 element_index: Some(0),
2868 component_index: Some(0),
2869 mig_number: Some("00035".to_string()),
2870 format: None,
2871 },
2872 AhbFieldRule {
2873 segment_path: "SG4/STS/C556/9013".to_string(),
2874 name: "Statusanlaß".to_string(),
2875 ahb_status: "X".to_string(),
2876 codes: vec![AhbCodeRule {
2877 value: "E03".to_string(),
2878 description: "Transaktionsgrund".to_string(),
2879 ahb_status: "X".to_string(),
2880 }],
2881 parent_group_ahb_status: None,
2882 segment_ahb_status: None,
2883 element_index: Some(2),
2884 component_index: Some(0),
2885 mig_number: Some("00035".to_string()),
2886 format: None,
2887 },
2888 AhbFieldRule {
2890 segment_path: "SG4/STS/C601/9015".to_string(),
2891 name: "Statuskategorie".to_string(),
2892 ahb_status: "X".to_string(),
2893 codes: vec![AhbCodeRule {
2894 value: "E01".to_string(),
2895 description: "Antwort".to_string(),
2896 ahb_status: "X".to_string(),
2897 }],
2898 parent_group_ahb_status: None,
2899 segment_ahb_status: None,
2900 element_index: Some(0),
2901 component_index: Some(0),
2902 mig_number: Some("00036".to_string()),
2903 format: None,
2904 },
2905 ],
2906 ub_definitions: BTreeMap::new(),
2907 };
2908
2909 let report = validator.validate(
2910 &[sts_7, sts_e01],
2911 &workflow,
2912 &external,
2913 ValidationLevel::Conditions,
2914 );
2915
2916 let code_errors: Vec<_> = report
2917 .by_category(ValidationCategory::Code)
2918 .filter(|i| {
2919 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2920 })
2921 .collect();
2922 assert_eq!(
2923 code_errors.len(),
2924 1,
2925 "Expected one COD002 (for GH02 only), got: {:?}",
2926 code_errors
2927 );
2928 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2929 }
2930
2931 #[test]
2932 fn test_code_validation_composite_paths_detects_invalid_code() {
2933 let evaluator = MockEvaluator::new(vec![]);
2936 let validator = EdifactValidator::new(evaluator);
2937 let external = NoOpExternalProvider;
2938
2939 let sts_segment = OwnedSegment {
2940 id: "STS".to_string(),
2941 elements: vec![
2942 vec!["7".to_string()],
2943 vec![String::new()],
2944 vec!["GH02".to_string()],
2945 vec!["ZW4".to_string()],
2946 ],
2947 segment_number: 1,
2948 };
2949
2950 let workflow = AhbWorkflow {
2951 pruefidentifikator: "55018".to_string(),
2952 description: "Test".to_string(),
2953 communication_direction: None,
2954 fields: vec![AhbFieldRule {
2955 segment_path: "SG4/STS/C556/9013".to_string(),
2956 name: "Statusanlaß".to_string(),
2957 ahb_status: "X".to_string(),
2958 codes: vec![AhbCodeRule {
2959 value: "E03".to_string(),
2960 description: "Transaktionsgrund".to_string(),
2961 ahb_status: "X".to_string(),
2962 }],
2963 parent_group_ahb_status: None,
2964 segment_ahb_status: None,
2965 element_index: Some(2),
2966 component_index: Some(0),
2967 ..Default::default()
2968 }],
2969 ub_definitions: BTreeMap::new(),
2970 };
2971
2972 let report = validator.validate(
2973 &[sts_segment],
2974 &workflow,
2975 &external,
2976 ValidationLevel::Conditions,
2977 );
2978
2979 let code_errors: Vec<_> = report
2980 .by_category(ValidationCategory::Code)
2981 .filter(|i| {
2982 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2983 })
2984 .collect();
2985 assert_eq!(
2986 code_errors.len(),
2987 1,
2988 "Expected COD002 for GH02, got: {:?}",
2989 code_errors
2990 );
2991 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2992 }
2993
2994 #[test]
2995 fn test_cross_field_code_validation_valid_qualifiers() {
2996 let evaluator = MockEvaluator::new(vec![]);
2999 let validator = EdifactValidator::new(evaluator);
3000 let external = NoOpExternalProvider;
3001
3002 let nad_ms = OwnedSegment {
3003 id: "NAD".to_string(),
3004 elements: vec![vec!["MS".to_string()]],
3005 segment_number: 4,
3006 };
3007 let nad_mr = OwnedSegment {
3008 id: "NAD".to_string(),
3009 elements: vec![vec!["MR".to_string()]],
3010 segment_number: 5,
3011 };
3012
3013 let workflow = AhbWorkflow {
3014 pruefidentifikator: "55001".to_string(),
3015 description: "Test".to_string(),
3016 communication_direction: None,
3017 fields: vec![
3018 AhbFieldRule {
3019 segment_path: "SG2/NAD/3035".to_string(),
3020 name: "Absender".to_string(),
3021 ahb_status: "X".to_string(),
3022 codes: vec![AhbCodeRule {
3023 value: "MS".to_string(),
3024 description: "Absender".to_string(),
3025 ahb_status: "X".to_string(),
3026 }],
3027 parent_group_ahb_status: None,
3028 segment_ahb_status: None,
3029 ..Default::default()
3030 },
3031 AhbFieldRule {
3032 segment_path: "SG2/NAD/3035".to_string(),
3033 name: "Empfaenger".to_string(),
3034 ahb_status: "X".to_string(),
3035 codes: vec![AhbCodeRule {
3036 value: "MR".to_string(),
3037 description: "Empfaenger".to_string(),
3038 ahb_status: "X".to_string(),
3039 }],
3040 parent_group_ahb_status: None,
3041 segment_ahb_status: None,
3042 ..Default::default()
3043 },
3044 ],
3045 ub_definitions: BTreeMap::new(),
3046 };
3047
3048 let report = validator.validate(
3049 &[nad_ms, nad_mr],
3050 &workflow,
3051 &external,
3052 ValidationLevel::Conditions,
3053 );
3054
3055 let code_errors: Vec<_> = report
3056 .by_category(ValidationCategory::Code)
3057 .filter(|i| i.severity == Severity::Error)
3058 .collect();
3059 assert!(
3060 code_errors.is_empty(),
3061 "Expected no code errors for valid qualifiers, got: {:?}",
3062 code_errors
3063 );
3064 }
3065
3066 #[test]
3067 fn test_cross_field_code_validation_catches_invalid_qualifier() {
3068 let evaluator = MockEvaluator::new(vec![]);
3070 let validator = EdifactValidator::new(evaluator);
3071 let external = NoOpExternalProvider;
3072
3073 let nad_ms = OwnedSegment {
3074 id: "NAD".to_string(),
3075 elements: vec![vec!["MS".to_string()]],
3076 segment_number: 4,
3077 };
3078 let nad_mt = OwnedSegment {
3079 id: "NAD".to_string(),
3080 elements: vec![vec!["MT".to_string()]], segment_number: 5,
3082 };
3083
3084 let workflow = AhbWorkflow {
3085 pruefidentifikator: "55001".to_string(),
3086 description: "Test".to_string(),
3087 communication_direction: None,
3088 fields: vec![
3089 AhbFieldRule {
3090 segment_path: "SG2/NAD/3035".to_string(),
3091 name: "Absender".to_string(),
3092 ahb_status: "X".to_string(),
3093 codes: vec![AhbCodeRule {
3094 value: "MS".to_string(),
3095 description: "Absender".to_string(),
3096 ahb_status: "X".to_string(),
3097 }],
3098 parent_group_ahb_status: None,
3099 segment_ahb_status: None,
3100 ..Default::default()
3101 },
3102 AhbFieldRule {
3103 segment_path: "SG2/NAD/3035".to_string(),
3104 name: "Empfaenger".to_string(),
3105 ahb_status: "X".to_string(),
3106 codes: vec![AhbCodeRule {
3107 value: "MR".to_string(),
3108 description: "Empfaenger".to_string(),
3109 ahb_status: "X".to_string(),
3110 }],
3111 parent_group_ahb_status: None,
3112 segment_ahb_status: None,
3113 ..Default::default()
3114 },
3115 ],
3116 ub_definitions: BTreeMap::new(),
3117 };
3118
3119 let report = validator.validate(
3120 &[nad_ms, nad_mt],
3121 &workflow,
3122 &external,
3123 ValidationLevel::Conditions,
3124 );
3125
3126 let code_errors: Vec<_> = report
3127 .by_category(ValidationCategory::Code)
3128 .filter(|i| i.severity == Severity::Error)
3129 .collect();
3130 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
3131 assert!(narrate(code_errors[0]).contains("MT"));
3132 assert!(narrate(code_errors[0]).contains("MR"));
3133 assert!(narrate(code_errors[0]).contains("MS"));
3134 }
3135
3136 #[test]
3137 fn test_cross_field_code_validation_unions_across_groups() {
3138 let evaluator = MockEvaluator::new(vec![]);
3142 let validator = EdifactValidator::new(evaluator);
3143 let external = NoOpExternalProvider;
3144
3145 let segments = vec![
3146 OwnedSegment {
3147 id: "NAD".to_string(),
3148 elements: vec![vec!["MS".to_string()]],
3149 segment_number: 3,
3150 },
3151 OwnedSegment {
3152 id: "NAD".to_string(),
3153 elements: vec![vec!["MR".to_string()]],
3154 segment_number: 4,
3155 },
3156 OwnedSegment {
3157 id: "NAD".to_string(),
3158 elements: vec![vec!["Z04".to_string()]],
3159 segment_number: 20,
3160 },
3161 OwnedSegment {
3162 id: "NAD".to_string(),
3163 elements: vec![vec!["Z09".to_string()]],
3164 segment_number: 21,
3165 },
3166 OwnedSegment {
3167 id: "NAD".to_string(),
3168 elements: vec![vec!["MT".to_string()]], segment_number: 22,
3170 },
3171 ];
3172
3173 let workflow = AhbWorkflow {
3174 pruefidentifikator: "55001".to_string(),
3175 description: "Test".to_string(),
3176 communication_direction: None,
3177 fields: vec![
3178 AhbFieldRule {
3179 segment_path: "SG2/NAD/3035".to_string(),
3180 name: "Absender".to_string(),
3181 ahb_status: "X".to_string(),
3182 codes: vec![AhbCodeRule {
3183 value: "MS".to_string(),
3184 description: "Absender".to_string(),
3185 ahb_status: "X".to_string(),
3186 }],
3187 parent_group_ahb_status: None,
3188 segment_ahb_status: None,
3189 ..Default::default()
3190 },
3191 AhbFieldRule {
3192 segment_path: "SG2/NAD/3035".to_string(),
3193 name: "Empfaenger".to_string(),
3194 ahb_status: "X".to_string(),
3195 codes: vec![AhbCodeRule {
3196 value: "MR".to_string(),
3197 description: "Empfaenger".to_string(),
3198 ahb_status: "X".to_string(),
3199 }],
3200 parent_group_ahb_status: None,
3201 segment_ahb_status: None,
3202 ..Default::default()
3203 },
3204 AhbFieldRule {
3205 segment_path: "SG4/SG12/NAD/3035".to_string(),
3206 name: "Anschlussnutzer".to_string(),
3207 ahb_status: "X".to_string(),
3208 codes: vec![AhbCodeRule {
3209 value: "Z04".to_string(),
3210 description: "Anschlussnutzer".to_string(),
3211 ahb_status: "X".to_string(),
3212 }],
3213 parent_group_ahb_status: None,
3214 segment_ahb_status: None,
3215 ..Default::default()
3216 },
3217 AhbFieldRule {
3218 segment_path: "SG4/SG12/NAD/3035".to_string(),
3219 name: "Korrespondenzanschrift".to_string(),
3220 ahb_status: "X".to_string(),
3221 codes: vec![AhbCodeRule {
3222 value: "Z09".to_string(),
3223 description: "Korrespondenzanschrift".to_string(),
3224 ahb_status: "X".to_string(),
3225 }],
3226 parent_group_ahb_status: None,
3227 segment_ahb_status: None,
3228 ..Default::default()
3229 },
3230 ],
3231 ub_definitions: BTreeMap::new(),
3232 };
3233
3234 let report =
3235 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3236
3237 let code_errors: Vec<_> = report
3238 .by_category(ValidationCategory::Code)
3239 .filter(|i| i.severity == Severity::Error)
3240 .collect();
3241 assert_eq!(
3242 code_errors.len(),
3243 1,
3244 "Expected exactly one COD002 error for MT, got: {:?}",
3245 code_errors
3246 );
3247 assert!(narrate(code_errors[0]).contains("MT"));
3248 }
3249
3250 #[test]
3251 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3252 let evaluator = MockEvaluator::new(vec![]);
3261 let validator = EdifactValidator::new(evaluator);
3262 let external = NoOpExternalProvider;
3263
3264 let qty_67 = OwnedSegment {
3265 id: "QTY".to_string(),
3266 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
3267 segment_number: 10,
3268 };
3269
3270 let workflow = AhbWorkflow {
3271 pruefidentifikator: "13025".to_string(),
3272 description: "Test".to_string(),
3273 communication_direction: None,
3274 fields: vec![AhbFieldRule {
3275 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
3276 name: "Menge, Qualifier".to_string(),
3277 ahb_status: "X".to_string(),
3278 codes: vec![
3279 AhbCodeRule {
3280 value: "220".to_string(),
3281 description: "Wahrer Wert".to_string(),
3282 ahb_status: "X".to_string(),
3283 },
3284 AhbCodeRule {
3285 value: "67".to_string(),
3286 description: "Ersatzwert".to_string(),
3287 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
3288 },
3289 AhbCodeRule {
3290 value: "Z18".to_string(),
3291 description: "Vorläufiger Wert".to_string(),
3292 ahb_status: "X [35]".to_string(),
3293 },
3294 ],
3295 parent_group_ahb_status: None,
3296 segment_ahb_status: None,
3297 element_index: Some(0),
3298 component_index: Some(0),
3299 ..Default::default()
3300 }],
3301 ub_definitions: BTreeMap::new(),
3302 };
3303
3304 let report =
3305 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3306
3307 let code_errors: Vec<_> = report
3308 .by_category(ValidationCategory::Code)
3309 .filter(|i| i.severity == Severity::Error)
3310 .collect();
3311 assert!(
3312 code_errors.is_empty(),
3313 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3314 code_errors
3315 );
3316 }
3317
3318 #[test]
3319 fn test_is_qualifier_field_simple_paths() {
3320 assert!(is_qualifier_field("NAD/3035"));
3321 assert!(is_qualifier_field("SG2/NAD/3035"));
3322 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3323 assert!(is_qualifier_field("LOC/3227"));
3324 }
3325
3326 #[test]
3327 fn test_is_qualifier_field_composite_paths() {
3328 assert!(is_qualifier_field("UNH/S009/0065"));
3332 assert!(is_qualifier_field("NAD/C082/3039"));
3333 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3334 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3335 }
3336
3337 #[test]
3338 fn test_is_qualifier_field_bare_segment() {
3339 assert!(!is_qualifier_field("NAD"));
3340 assert!(!is_qualifier_field("SG2/NAD"));
3341 }
3342
3343 #[test]
3344 fn test_is_qualifier_field_rejects_deep_paths() {
3345 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3347 }
3348
3349 #[test]
3350 fn test_missing_qualifier_instance_is_detected() {
3351 let evaluator = MockEvaluator::new(vec![]);
3354 let validator = EdifactValidator::new(evaluator);
3355 let external = NoOpExternalProvider;
3356
3357 let nad_ms = OwnedSegment {
3358 id: "NAD".to_string(),
3359 elements: vec![vec!["MS".to_string()]],
3360 segment_number: 3,
3361 };
3362
3363 let workflow = AhbWorkflow {
3364 pruefidentifikator: "55001".to_string(),
3365 description: "Test".to_string(),
3366 communication_direction: None,
3367 fields: vec![
3368 AhbFieldRule {
3369 segment_path: "SG2/NAD/3035".to_string(),
3370 name: "Absender".to_string(),
3371 ahb_status: "X".to_string(),
3372 codes: vec![AhbCodeRule {
3373 value: "MS".to_string(),
3374 description: "Absender".to_string(),
3375 ahb_status: "X".to_string(),
3376 }],
3377 parent_group_ahb_status: None,
3378 segment_ahb_status: None,
3379 ..Default::default()
3380 },
3381 AhbFieldRule {
3382 segment_path: "SG2/NAD/3035".to_string(),
3383 name: "Empfaenger".to_string(),
3384 ahb_status: "Muss".to_string(),
3385 codes: vec![AhbCodeRule {
3386 value: "MR".to_string(),
3387 description: "Empfaenger".to_string(),
3388 ahb_status: "X".to_string(),
3389 }],
3390 parent_group_ahb_status: None,
3391 segment_ahb_status: None,
3392 ..Default::default()
3393 },
3394 ],
3395 ub_definitions: BTreeMap::new(),
3396 };
3397
3398 let report =
3399 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
3400
3401 let ahb_errors: Vec<_> = report
3402 .by_category(ValidationCategory::Ahb)
3403 .filter(|i| i.severity == Severity::Error)
3404 .collect();
3405 assert_eq!(
3406 ahb_errors.len(),
3407 1,
3408 "Expected AHB001 for missing NAD+MR, got: {:?}",
3409 ahb_errors
3410 );
3411 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3412 }
3413
3414 #[test]
3415 fn test_present_qualifier_instance_no_error() {
3416 let evaluator = MockEvaluator::new(vec![]);
3418 let validator = EdifactValidator::new(evaluator);
3419 let external = NoOpExternalProvider;
3420
3421 let segments = vec![
3422 OwnedSegment {
3423 id: "NAD".to_string(),
3424 elements: vec![vec!["MS".to_string()]],
3425 segment_number: 3,
3426 },
3427 OwnedSegment {
3428 id: "NAD".to_string(),
3429 elements: vec![vec!["MR".to_string()]],
3430 segment_number: 4,
3431 },
3432 ];
3433
3434 let workflow = AhbWorkflow {
3435 pruefidentifikator: "55001".to_string(),
3436 description: "Test".to_string(),
3437 communication_direction: None,
3438 fields: vec![
3439 AhbFieldRule {
3440 segment_path: "SG2/NAD/3035".to_string(),
3441 name: "Absender".to_string(),
3442 ahb_status: "Muss".to_string(),
3443 codes: vec![AhbCodeRule {
3444 value: "MS".to_string(),
3445 description: "Absender".to_string(),
3446 ahb_status: "X".to_string(),
3447 }],
3448 parent_group_ahb_status: None,
3449 segment_ahb_status: None,
3450 ..Default::default()
3451 },
3452 AhbFieldRule {
3453 segment_path: "SG2/NAD/3035".to_string(),
3454 name: "Empfaenger".to_string(),
3455 ahb_status: "Muss".to_string(),
3456 codes: vec![AhbCodeRule {
3457 value: "MR".to_string(),
3458 description: "Empfaenger".to_string(),
3459 ahb_status: "X".to_string(),
3460 }],
3461 parent_group_ahb_status: None,
3462 segment_ahb_status: None,
3463 ..Default::default()
3464 },
3465 ],
3466 ub_definitions: BTreeMap::new(),
3467 };
3468
3469 let report =
3470 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3471
3472 let ahb_errors: Vec<_> = report
3473 .by_category(ValidationCategory::Ahb)
3474 .filter(|i| i.severity == Severity::Error)
3475 .collect();
3476 assert!(
3477 ahb_errors.is_empty(),
3478 "Expected no AHB001 errors, got: {:?}",
3479 ahb_errors
3480 );
3481 }
3482
3483 #[test]
3484 fn test_extract_group_path_key() {
3485 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
3486 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
3487 assert_eq!(extract_group_path_key("NAD/3035"), "");
3488 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
3489 }
3490
3491 fn validate_field_of_absent_group(
3493 group_status: &str,
3494 conditions: Vec<(u32, CR)>,
3495 ) -> ValidationReport {
3496 use mig_types::navigator::GroupNavigator;
3497
3498 struct NoSg9;
3499 impl GroupNavigator for NoSg9 {
3500 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3501 vec![]
3502 }
3503 fn find_segments_with_qualifier_in_group(
3504 &self,
3505 _: &str,
3506 _: usize,
3507 _: &str,
3508 _: &[&str],
3509 _: usize,
3510 ) -> Vec<OwnedSegment> {
3511 vec![]
3512 }
3513 fn group_instance_count(&self, _: &[&str]) -> usize {
3514 0
3515 }
3516 }
3517
3518 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3519 let workflow = AhbWorkflow {
3520 pruefidentifikator: "44001".to_string(),
3521 description: "Test".to_string(),
3522 communication_direction: None,
3523 fields: vec![AhbFieldRule {
3524 segment_path: "SG9/QTY/C186/6060".to_string(),
3525 name: "Menge".to_string(),
3526 ahb_status: "X".to_string(),
3527 parent_group_ahb_status: Some(group_status.to_string()),
3528 ..Default::default()
3529 }],
3530 ub_definitions: BTreeMap::new(),
3531 };
3532 validator.validate_with_navigator(
3533 &[],
3534 &workflow,
3535 &NoOpExternalProvider,
3536 ValidationLevel::Conditions,
3537 &NoSg9,
3538 )
3539 }
3540
3541 #[test]
3542 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
3543 let report = validate_field_of_absent_group(
3545 "Soll [47]\r\nMuss [46]",
3546 vec![(47, CR::True), (46, CR::False)],
3547 );
3548 assert!(
3549 report.is_valid(),
3550 "{:?}",
3551 report.errors().collect::<Vec<_>>()
3552 );
3553 }
3554
3555 #[test]
3556 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
3557 let report = validate_field_of_absent_group(
3558 "Soll [47]\r\nMuss [46]",
3559 vec![(47, CR::False), (46, CR::True)],
3560 );
3561 let errors: Vec<_> = report.errors().collect();
3562 assert_eq!(errors.len(), 1, "{errors:?}");
3563 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3564 }
3565
3566 #[test]
3567 fn test_absent_optional_group_no_missing_field_error() {
3568 use mig_types::navigator::GroupNavigator;
3571
3572 struct NavWithoutSG3;
3573 impl GroupNavigator for NavWithoutSG3 {
3574 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3575 vec![]
3576 }
3577 fn find_segments_with_qualifier_in_group(
3578 &self,
3579 _: &str,
3580 _: usize,
3581 _: &str,
3582 _: &[&str],
3583 _: usize,
3584 ) -> Vec<OwnedSegment> {
3585 vec![]
3586 }
3587 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3588 match group_path {
3589 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
3592 }
3593 }
3594 }
3595
3596 let evaluator = MockEvaluator::new(vec![]);
3597 let validator = EdifactValidator::new(evaluator);
3598 let external = NoOpExternalProvider;
3599 let nav = NavWithoutSG3;
3600
3601 let segments = vec![
3603 OwnedSegment {
3604 id: "NAD".into(),
3605 elements: vec![vec!["MS".into()]],
3606 segment_number: 3,
3607 },
3608 OwnedSegment {
3609 id: "NAD".into(),
3610 elements: vec![vec!["MR".into()]],
3611 segment_number: 4,
3612 },
3613 ];
3614
3615 let workflow = AhbWorkflow {
3616 pruefidentifikator: "55001".to_string(),
3617 description: "Test".to_string(),
3618 communication_direction: None,
3619 fields: vec![
3620 AhbFieldRule {
3621 segment_path: "SG2/SG3/CTA/3139".to_string(),
3622 name: "Funktion des Ansprechpartners, Code".to_string(),
3623 ahb_status: "Muss".to_string(),
3624 codes: vec![],
3625 parent_group_ahb_status: None,
3626 segment_ahb_status: None,
3627 ..Default::default()
3628 },
3629 AhbFieldRule {
3630 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
3631 name: "Name vom Ansprechpartner".to_string(),
3632 ahb_status: "X".to_string(),
3633 codes: vec![],
3634 parent_group_ahb_status: None,
3635 segment_ahb_status: None,
3636 ..Default::default()
3637 },
3638 ],
3639 ub_definitions: BTreeMap::new(),
3640 };
3641
3642 let report = validator.validate_with_navigator(
3643 &segments,
3644 &workflow,
3645 &external,
3646 ValidationLevel::Conditions,
3647 &nav,
3648 );
3649
3650 let ahb_errors: Vec<_> = report
3651 .by_category(ValidationCategory::Ahb)
3652 .filter(|i| i.severity == Severity::Error)
3653 .collect();
3654 assert!(
3655 ahb_errors.is_empty(),
3656 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
3657 ahb_errors
3658 );
3659 }
3660
3661 #[test]
3662 fn test_present_group_still_checks_mandatory_fields() {
3663 use mig_types::navigator::GroupNavigator;
3665
3666 struct NavWithSG3;
3667 impl GroupNavigator for NavWithSG3 {
3668 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3669 vec![]
3670 }
3671 fn find_segments_with_qualifier_in_group(
3672 &self,
3673 _: &str,
3674 _: usize,
3675 _: &str,
3676 _: &[&str],
3677 _: usize,
3678 ) -> Vec<OwnedSegment> {
3679 vec![]
3680 }
3681 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3682 match group_path {
3683 ["SG2"] => 1,
3684 ["SG2", "SG3"] => 1, _ => 0,
3686 }
3687 }
3688 }
3689
3690 let evaluator = MockEvaluator::new(vec![]);
3691 let validator = EdifactValidator::new(evaluator);
3692 let external = NoOpExternalProvider;
3693 let nav = NavWithSG3;
3694
3695 let segments = vec![OwnedSegment {
3697 id: "NAD".into(),
3698 elements: vec![vec!["MS".into()]],
3699 segment_number: 3,
3700 }];
3701
3702 let workflow = AhbWorkflow {
3703 pruefidentifikator: "55001".to_string(),
3704 description: "Test".to_string(),
3705 communication_direction: None,
3706 fields: vec![AhbFieldRule {
3707 segment_path: "SG2/SG3/CTA/3139".to_string(),
3708 name: "Funktion des Ansprechpartners, Code".to_string(),
3709 ahb_status: "Muss".to_string(),
3710 codes: vec![],
3711 parent_group_ahb_status: None,
3712 segment_ahb_status: None,
3713 ..Default::default()
3714 }],
3715 ub_definitions: BTreeMap::new(),
3716 };
3717
3718 let report = validator.validate_with_navigator(
3719 &segments,
3720 &workflow,
3721 &external,
3722 ValidationLevel::Conditions,
3723 &nav,
3724 );
3725
3726 let ahb_errors: Vec<_> = report
3727 .by_category(ValidationCategory::Ahb)
3728 .filter(|i| i.severity == Severity::Error)
3729 .collect();
3730 assert_eq!(
3731 ahb_errors.len(),
3732 1,
3733 "Expected AHB001 error when SG3 is present but CTA missing"
3734 );
3735 assert!(narrate(ahb_errors[0]).contains("CTA"));
3736 }
3737
3738 #[test]
3739 fn test_missing_qualifier_with_navigator_is_detected() {
3740 use mig_types::navigator::GroupNavigator;
3743
3744 struct NavWithSG2;
3745 impl GroupNavigator for NavWithSG2 {
3746 fn find_segments_in_group(
3747 &self,
3748 segment_id: &str,
3749 group_path: &[&str],
3750 instance_index: usize,
3751 ) -> Vec<OwnedSegment> {
3752 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
3753 vec![OwnedSegment {
3754 id: "NAD".into(),
3755 elements: vec![vec!["MS".into()]],
3756 segment_number: 3,
3757 }]
3758 } else {
3759 vec![]
3760 }
3761 }
3762 fn find_segments_with_qualifier_in_group(
3763 &self,
3764 _: &str,
3765 _: usize,
3766 _: &str,
3767 _: &[&str],
3768 _: usize,
3769 ) -> Vec<OwnedSegment> {
3770 vec![]
3771 }
3772 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3773 match group_path {
3774 ["SG2"] => 1,
3775 _ => 0,
3776 }
3777 }
3778 }
3779
3780 let evaluator = MockEvaluator::new(vec![]);
3781 let validator = EdifactValidator::new(evaluator);
3782 let external = NoOpExternalProvider;
3783 let nav = NavWithSG2;
3784
3785 let segments = vec![OwnedSegment {
3786 id: "NAD".into(),
3787 elements: vec![vec!["MS".into()]],
3788 segment_number: 3,
3789 }];
3790
3791 let workflow = AhbWorkflow {
3792 pruefidentifikator: "55001".to_string(),
3793 description: "Test".to_string(),
3794 communication_direction: None,
3795 fields: vec![
3796 AhbFieldRule {
3797 segment_path: "SG2/NAD/3035".to_string(),
3798 name: "Absender".to_string(),
3799 ahb_status: "X".to_string(),
3800 codes: vec![AhbCodeRule {
3801 value: "MS".to_string(),
3802 description: "Absender".to_string(),
3803 ahb_status: "X".to_string(),
3804 }],
3805 parent_group_ahb_status: None,
3806 segment_ahb_status: None,
3807 ..Default::default()
3808 },
3809 AhbFieldRule {
3810 segment_path: "SG2/NAD/3035".to_string(),
3811 name: "Empfaenger".to_string(),
3812 ahb_status: "Muss".to_string(),
3813 codes: vec![AhbCodeRule {
3814 value: "MR".to_string(),
3815 description: "Empfaenger".to_string(),
3816 ahb_status: "X".to_string(),
3817 }],
3818 parent_group_ahb_status: None,
3819 segment_ahb_status: None,
3820 ..Default::default()
3821 },
3822 ],
3823 ub_definitions: BTreeMap::new(),
3824 };
3825
3826 let report = validator.validate_with_navigator(
3827 &segments,
3828 &workflow,
3829 &external,
3830 ValidationLevel::Conditions,
3831 &nav,
3832 );
3833
3834 let ahb_errors: Vec<_> = report
3835 .by_category(ValidationCategory::Ahb)
3836 .filter(|i| i.severity == Severity::Error)
3837 .collect();
3838 assert_eq!(
3839 ahb_errors.len(),
3840 1,
3841 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
3842 ahb_errors
3843 );
3844 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3845 }
3846
3847 #[test]
3848 fn test_optional_group_variant_absent_no_error() {
3849 use mig_types::navigator::GroupNavigator;
3854
3855 struct TestNav;
3856 impl GroupNavigator for TestNav {
3857 fn find_segments_in_group(
3858 &self,
3859 segment_id: &str,
3860 group_path: &[&str],
3861 instance_index: usize,
3862 ) -> Vec<OwnedSegment> {
3863 match (segment_id, group_path, instance_index) {
3864 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
3865 id: "LOC".into(),
3866 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3867 segment_number: 10,
3868 }],
3869 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
3870 id: "NAD".into(),
3871 elements: vec![vec!["MS".into()]],
3872 segment_number: 3,
3873 }],
3874 _ => vec![],
3875 }
3876 }
3877 fn find_segments_with_qualifier_in_group(
3878 &self,
3879 _: &str,
3880 _: usize,
3881 _: &str,
3882 _: &[&str],
3883 _: usize,
3884 ) -> Vec<OwnedSegment> {
3885 vec![]
3886 }
3887 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3888 match group_path {
3889 ["SG2"] => 1,
3890 ["SG4"] => 1,
3891 ["SG4", "SG5"] => 1, _ => 0,
3893 }
3894 }
3895 }
3896
3897 let evaluator = MockEvaluator::new(vec![]);
3898 let validator = EdifactValidator::new(evaluator);
3899 let external = NoOpExternalProvider;
3900 let nav = TestNav;
3901
3902 let segments = vec![
3903 OwnedSegment {
3904 id: "NAD".into(),
3905 elements: vec![vec!["MS".into()]],
3906 segment_number: 3,
3907 },
3908 OwnedSegment {
3909 id: "LOC".into(),
3910 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3911 segment_number: 10,
3912 },
3913 ];
3914
3915 let workflow = AhbWorkflow {
3916 pruefidentifikator: "55001".to_string(),
3917 description: "Test".to_string(),
3918 communication_direction: None,
3919 fields: vec![
3920 AhbFieldRule {
3922 segment_path: "SG2/NAD/3035".to_string(),
3923 name: "Absender".to_string(),
3924 ahb_status: "X".to_string(),
3925 codes: vec![AhbCodeRule {
3926 value: "MS".to_string(),
3927 description: "Absender".to_string(),
3928 ahb_status: "X".to_string(),
3929 }],
3930 parent_group_ahb_status: Some("Muss".to_string()),
3931 segment_ahb_status: None,
3932 ..Default::default()
3933 },
3934 AhbFieldRule {
3935 segment_path: "SG2/NAD/3035".to_string(),
3936 name: "Empfaenger".to_string(),
3937 ahb_status: "Muss".to_string(),
3938 codes: vec![AhbCodeRule {
3939 value: "MR".to_string(),
3940 description: "Empfaenger".to_string(),
3941 ahb_status: "X".to_string(),
3942 }],
3943 parent_group_ahb_status: Some("Muss".to_string()),
3944 segment_ahb_status: None,
3945 ..Default::default()
3946 },
3947 AhbFieldRule {
3949 segment_path: "SG4/SG5/LOC/3227".to_string(),
3950 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3951 ahb_status: "X".to_string(),
3952 codes: vec![AhbCodeRule {
3953 value: "Z16".to_string(),
3954 description: "Marktlokation".to_string(),
3955 ahb_status: "X".to_string(),
3956 }],
3957 parent_group_ahb_status: Some("Kann".to_string()),
3958 segment_ahb_status: None,
3959 ..Default::default()
3960 },
3961 AhbFieldRule {
3962 segment_path: "SG4/SG5/LOC/3227".to_string(),
3963 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3964 ahb_status: "Muss".to_string(),
3965 codes: vec![AhbCodeRule {
3966 value: "Z17".to_string(),
3967 description: "Messlokation".to_string(),
3968 ahb_status: "X".to_string(),
3969 }],
3970 parent_group_ahb_status: Some("Kann".to_string()),
3971 segment_ahb_status: None,
3972 ..Default::default()
3973 },
3974 ],
3975 ub_definitions: BTreeMap::new(),
3976 };
3977
3978 let report = validator.validate_with_navigator(
3979 &segments,
3980 &workflow,
3981 &external,
3982 ValidationLevel::Conditions,
3983 &nav,
3984 );
3985
3986 let ahb_errors: Vec<_> = report
3987 .by_category(ValidationCategory::Ahb)
3988 .filter(|i| i.severity == Severity::Error)
3989 .collect();
3990
3991 assert_eq!(
3994 ahb_errors.len(),
3995 1,
3996 "Expected only AHB001 for missing NAD+MR, got: {:?}",
3997 ahb_errors
3998 );
3999 assert!(
4000 narrate(ahb_errors[0]).contains("Empfaenger"),
4001 "Error should be for missing NAD+MR (Empfaenger)"
4002 );
4003 }
4004
4005 #[test]
4009 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4010 use mig_types::navigator::GroupNavigator;
4011
4012 struct TestNav;
4013 impl GroupNavigator for TestNav {
4014 fn find_segments_in_group(
4015 &self,
4016 segment_id: &str,
4017 group_path: &[&str],
4018 instance_index: usize,
4019 ) -> Vec<OwnedSegment> {
4020 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4021 vec![OwnedSegment {
4022 id: "SEQ".into(),
4023 elements: vec![vec!["Z03".into()]],
4024 segment_number: 10,
4025 }]
4026 } else {
4027 vec![]
4028 }
4029 }
4030 fn find_segments_with_qualifier_in_group(
4031 &self,
4032 _: &str,
4033 _: usize,
4034 _: &str,
4035 _: &[&str],
4036 _: usize,
4037 ) -> Vec<OwnedSegment> {
4038 vec![]
4039 }
4040 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4041 match group_path {
4042 ["SG4"] => 1,
4043 ["SG4", "SG8"] => 1,
4044 _ => 0,
4045 }
4046 }
4047 }
4048
4049 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
4050 let validator = EdifactValidator::new(evaluator);
4051 let segments = vec![OwnedSegment {
4052 id: "SEQ".into(),
4053 elements: vec![vec!["Z03".into()]],
4054 segment_number: 10,
4055 }];
4056 let seq = |code: &str, group: &str| AhbFieldRule {
4057 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4058 name: format!("Handlung, Code ({code})"),
4059 ahb_status: "X".to_string(),
4060 codes: vec![AhbCodeRule {
4061 value: code.to_string(),
4062 description: String::new(),
4063 ahb_status: "X".to_string(),
4064 }],
4065 parent_group_ahb_status: Some(group.to_string()),
4066 segment_ahb_status: None,
4067 ..Default::default()
4068 };
4069 let workflow = AhbWorkflow {
4070 pruefidentifikator: "44143".to_string(),
4071 description: "Test".to_string(),
4072 communication_direction: None,
4073 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
4074 ub_definitions: BTreeMap::new(),
4075 };
4076
4077 let report = validator.validate_with_navigator(
4078 &segments,
4079 &workflow,
4080 &NoOpExternalProvider,
4081 ValidationLevel::Conditions,
4082 &TestNav,
4083 );
4084 let missing: Vec<_> = report
4085 .issues
4086 .iter()
4087 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
4088 .collect();
4089 assert!(missing.is_empty(), "{missing:#?}");
4090 }
4091
4092 #[test]
4093 fn test_conditional_group_variant_absent_no_error() {
4094 use mig_types::navigator::GroupNavigator;
4099
4100 struct TestNav;
4101 impl GroupNavigator for TestNav {
4102 fn find_segments_in_group(
4103 &self,
4104 segment_id: &str,
4105 group_path: &[&str],
4106 instance_index: usize,
4107 ) -> Vec<OwnedSegment> {
4108 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
4109 vec![OwnedSegment {
4110 id: "LOC".into(),
4111 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4112 segment_number: 10,
4113 }]
4114 } else {
4115 vec![]
4116 }
4117 }
4118 fn find_segments_with_qualifier_in_group(
4119 &self,
4120 _: &str,
4121 _: usize,
4122 _: &str,
4123 _: &[&str],
4124 _: usize,
4125 ) -> Vec<OwnedSegment> {
4126 vec![]
4127 }
4128 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4129 match group_path {
4130 ["SG4"] => 1,
4131 ["SG4", "SG5"] => 1, _ => 0,
4133 }
4134 }
4135 }
4136
4137 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
4140 let validator = EdifactValidator::new(evaluator);
4141 let external = NoOpExternalProvider;
4142 let nav = TestNav;
4143
4144 let segments = vec![OwnedSegment {
4145 id: "LOC".into(),
4146 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4147 segment_number: 10,
4148 }];
4149
4150 let workflow = AhbWorkflow {
4151 pruefidentifikator: "55001".to_string(),
4152 description: "Test".to_string(),
4153 communication_direction: None,
4154 fields: vec![
4155 AhbFieldRule {
4157 segment_path: "SG4/SG5/LOC/3227".to_string(),
4158 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4159 ahb_status: "X".to_string(),
4160 codes: vec![AhbCodeRule {
4161 value: "Z16".to_string(),
4162 description: "Marktlokation".to_string(),
4163 ahb_status: "X".to_string(),
4164 }],
4165 parent_group_ahb_status: Some("Muss [2061]".to_string()),
4166 segment_ahb_status: None,
4167 ..Default::default()
4168 },
4169 AhbFieldRule {
4171 segment_path: "SG4/SG5/LOC/3227".to_string(),
4172 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4173 ahb_status: "X".to_string(),
4174 codes: vec![AhbCodeRule {
4175 value: "Z17".to_string(),
4176 description: "Messlokation".to_string(),
4177 ahb_status: "X".to_string(),
4178 }],
4179 parent_group_ahb_status: Some("Soll [165]".to_string()),
4180 segment_ahb_status: None,
4181 ..Default::default()
4182 },
4183 ],
4184 ub_definitions: BTreeMap::new(),
4185 };
4186
4187 let report = validator.validate_with_navigator(
4188 &segments,
4189 &workflow,
4190 &external,
4191 ValidationLevel::Conditions,
4192 &nav,
4193 );
4194
4195 let ahb_errors: Vec<_> = report
4196 .by_category(ValidationCategory::Ahb)
4197 .filter(|i| i.severity == Severity::Error)
4198 .collect();
4199
4200 assert!(
4202 ahb_errors.is_empty(),
4203 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4204 ahb_errors
4205 );
4206 }
4207
4208 #[test]
4209 fn test_conditional_group_variant_unknown_no_error() {
4210 let evaluator = MockEvaluator::new(vec![]);
4216 let validator = EdifactValidator::new(evaluator);
4217 let external = NoOpExternalProvider;
4218
4219 let workflow = AhbWorkflow {
4220 pruefidentifikator: "55001".to_string(),
4221 description: "Test".to_string(),
4222 communication_direction: None,
4223 fields: vec![AhbFieldRule {
4224 segment_path: "SG4/SG5/LOC/3227".to_string(),
4225 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4226 ahb_status: "X".to_string(),
4227 codes: vec![AhbCodeRule {
4228 value: "Z17".to_string(),
4229 description: "Messlokation".to_string(),
4230 ahb_status: "X".to_string(),
4231 }],
4232 parent_group_ahb_status: Some("Soll [165]".to_string()),
4233 segment_ahb_status: None,
4234 ..Default::default()
4235 }],
4236 ub_definitions: BTreeMap::new(),
4237 };
4238
4239 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4240
4241 let ahb_errors: Vec<_> = report
4242 .by_category(ValidationCategory::Ahb)
4243 .filter(|i| i.severity == Severity::Error)
4244 .collect();
4245
4246 assert!(
4248 ahb_errors.is_empty(),
4249 "Expected no errors when parent group condition is Unknown, got: {:?}",
4250 ahb_errors
4251 );
4252 }
4253
4254 #[test]
4255 fn test_segment_absent_within_present_group_no_error() {
4256 use mig_types::navigator::GroupNavigator;
4260
4261 struct TestNav;
4262 impl GroupNavigator for TestNav {
4263 fn find_segments_in_group(
4264 &self,
4265 segment_id: &str,
4266 group_path: &[&str],
4267 instance_index: usize,
4268 ) -> Vec<OwnedSegment> {
4269 if segment_id == "QTY"
4271 && group_path == ["SG5", "SG6", "SG9", "SG10"]
4272 && instance_index == 0
4273 {
4274 vec![OwnedSegment {
4275 id: "QTY".into(),
4276 elements: vec![vec!["220".into(), "0".into()]],
4277 segment_number: 14,
4278 }]
4279 } else {
4280 vec![]
4281 }
4282 }
4283 fn find_segments_with_qualifier_in_group(
4284 &self,
4285 _: &str,
4286 _: usize,
4287 _: &str,
4288 _: &[&str],
4289 _: usize,
4290 ) -> Vec<OwnedSegment> {
4291 vec![]
4292 }
4293 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4294 match group_path {
4295 ["SG5"] => 1,
4296 ["SG5", "SG6"] => 1,
4297 ["SG5", "SG6", "SG9"] => 1,
4298 ["SG5", "SG6", "SG9", "SG10"] => 1,
4299 _ => 0,
4300 }
4301 }
4302 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4303 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4305 }
4306 }
4307
4308 let evaluator = MockEvaluator::all_true(&[]);
4309 let validator = EdifactValidator::new(evaluator);
4310 let external = NoOpExternalProvider;
4311 let nav = TestNav;
4312
4313 let segments = vec![OwnedSegment {
4314 id: "QTY".into(),
4315 elements: vec![vec!["220".into(), "0".into()]],
4316 segment_number: 14,
4317 }];
4318
4319 let workflow = AhbWorkflow {
4320 pruefidentifikator: "13017".to_string(),
4321 description: "Test".to_string(),
4322 communication_direction: None,
4323 fields: vec![
4324 AhbFieldRule {
4326 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4327 name: "Statuskategorie, Code".to_string(),
4328 ahb_status: "X".to_string(),
4329 codes: vec![],
4330 parent_group_ahb_status: Some("Muss".to_string()),
4331 segment_ahb_status: None,
4332 ..Default::default()
4333 },
4334 AhbFieldRule {
4336 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4337 name: "Statusanlaß, Code".to_string(),
4338 ahb_status: "X [5]".to_string(),
4339 codes: vec![],
4340 parent_group_ahb_status: Some("Muss".to_string()),
4341 segment_ahb_status: None,
4342 ..Default::default()
4343 },
4344 ],
4345 ub_definitions: BTreeMap::new(),
4346 };
4347
4348 let report = validator.validate_with_navigator(
4349 &segments,
4350 &workflow,
4351 &external,
4352 ValidationLevel::Conditions,
4353 &nav,
4354 );
4355
4356 let ahb_errors: Vec<_> = report
4357 .by_category(ValidationCategory::Ahb)
4358 .filter(|i| i.severity == Severity::Error)
4359 .collect();
4360
4361 assert!(
4362 ahb_errors.is_empty(),
4363 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
4364 ahb_errors
4365 );
4366 }
4367
4368 #[test]
4369 fn test_group_scoped_code_validation_with_navigator() {
4370 use mig_types::navigator::GroupNavigator;
4374
4375 struct TestNav;
4376 impl GroupNavigator for TestNav {
4377 fn find_segments_in_group(
4378 &self,
4379 segment_id: &str,
4380 group_path: &[&str],
4381 _instance_index: usize,
4382 ) -> Vec<OwnedSegment> {
4383 if segment_id != "NAD" {
4384 return vec![];
4385 }
4386 match group_path {
4387 ["SG2"] => vec![
4388 OwnedSegment {
4389 id: "NAD".into(),
4390 elements: vec![vec!["MS".into()]],
4391 segment_number: 3,
4392 },
4393 OwnedSegment {
4394 id: "NAD".into(),
4395 elements: vec![vec!["MT".into()]], segment_number: 4,
4397 },
4398 ],
4399 ["SG4", "SG12"] => vec![
4400 OwnedSegment {
4401 id: "NAD".into(),
4402 elements: vec![vec!["Z04".into()]],
4403 segment_number: 20,
4404 },
4405 OwnedSegment {
4406 id: "NAD".into(),
4407 elements: vec![vec!["Z09".into()]],
4408 segment_number: 21,
4409 },
4410 ],
4411 _ => vec![],
4412 }
4413 }
4414 fn find_segments_with_qualifier_in_group(
4415 &self,
4416 _: &str,
4417 _: usize,
4418 _: &str,
4419 _: &[&str],
4420 _: usize,
4421 ) -> Vec<OwnedSegment> {
4422 vec![]
4423 }
4424 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4425 match group_path {
4426 ["SG2"] | ["SG4", "SG12"] => 1,
4427 _ => 0,
4428 }
4429 }
4430 }
4431
4432 let evaluator = MockEvaluator::new(vec![]);
4433 let validator = EdifactValidator::new(evaluator);
4434 let external = NoOpExternalProvider;
4435 let nav = TestNav;
4436
4437 let workflow = AhbWorkflow {
4438 pruefidentifikator: "55001".to_string(),
4439 description: "Test".to_string(),
4440 communication_direction: None,
4441 fields: vec![
4442 AhbFieldRule {
4443 segment_path: "SG2/NAD/3035".to_string(),
4444 name: "Absender".to_string(),
4445 ahb_status: "X".to_string(),
4446 codes: vec![AhbCodeRule {
4447 value: "MS".to_string(),
4448 description: "Absender".to_string(),
4449 ahb_status: "X".to_string(),
4450 }],
4451 parent_group_ahb_status: None,
4452 segment_ahb_status: None,
4453 ..Default::default()
4454 },
4455 AhbFieldRule {
4456 segment_path: "SG2/NAD/3035".to_string(),
4457 name: "Empfaenger".to_string(),
4458 ahb_status: "X".to_string(),
4459 codes: vec![AhbCodeRule {
4460 value: "MR".to_string(),
4461 description: "Empfaenger".to_string(),
4462 ahb_status: "X".to_string(),
4463 }],
4464 parent_group_ahb_status: None,
4465 segment_ahb_status: None,
4466 ..Default::default()
4467 },
4468 AhbFieldRule {
4469 segment_path: "SG4/SG12/NAD/3035".to_string(),
4470 name: "Anschlussnutzer".to_string(),
4471 ahb_status: "X".to_string(),
4472 codes: vec![AhbCodeRule {
4473 value: "Z04".to_string(),
4474 description: "Anschlussnutzer".to_string(),
4475 ahb_status: "X".to_string(),
4476 }],
4477 parent_group_ahb_status: None,
4478 segment_ahb_status: None,
4479 ..Default::default()
4480 },
4481 AhbFieldRule {
4482 segment_path: "SG4/SG12/NAD/3035".to_string(),
4483 name: "Korrespondenzanschrift".to_string(),
4484 ahb_status: "X".to_string(),
4485 codes: vec![AhbCodeRule {
4486 value: "Z09".to_string(),
4487 description: "Korrespondenzanschrift".to_string(),
4488 ahb_status: "X".to_string(),
4489 }],
4490 parent_group_ahb_status: None,
4491 segment_ahb_status: None,
4492 ..Default::default()
4493 },
4494 ],
4495 ub_definitions: BTreeMap::new(),
4496 };
4497
4498 let all_segments = vec![
4500 OwnedSegment {
4501 id: "NAD".into(),
4502 elements: vec![vec!["MS".into()]],
4503 segment_number: 3,
4504 },
4505 OwnedSegment {
4506 id: "NAD".into(),
4507 elements: vec![vec!["MT".into()]],
4508 segment_number: 4,
4509 },
4510 OwnedSegment {
4511 id: "NAD".into(),
4512 elements: vec![vec!["Z04".into()]],
4513 segment_number: 20,
4514 },
4515 OwnedSegment {
4516 id: "NAD".into(),
4517 elements: vec![vec!["Z09".into()]],
4518 segment_number: 21,
4519 },
4520 ];
4521
4522 let report = validator.validate_with_navigator(
4523 &all_segments,
4524 &workflow,
4525 &external,
4526 ValidationLevel::Conditions,
4527 &nav,
4528 );
4529
4530 let code_errors: Vec<_> = report
4531 .by_category(ValidationCategory::Code)
4532 .filter(|i| i.severity == Severity::Error)
4533 .collect();
4534
4535 assert_eq!(
4538 code_errors.len(),
4539 1,
4540 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
4541 code_errors
4542 );
4543 assert!(narrate(code_errors[0]).contains("MT"));
4544 assert!(narrate(code_errors[0]).contains("MR"));
4546 assert!(narrate(code_errors[0]).contains("MS"));
4547 assert!(
4548 !narrate(code_errors[0]).contains("Z04"),
4549 "SG4/SG12 codes should not leak into SG2 error"
4550 );
4551 assert!(
4553 code_errors[0]
4554 .field_path
4555 .as_deref()
4556 .unwrap_or("")
4557 .contains("SG2"),
4558 "Error field_path should reference SG2, got: {:?}",
4559 code_errors[0].field_path
4560 );
4561 }
4562
4563 #[test]
4566 fn test_package_cardinality_within_bounds() {
4567 let evaluator = MockEvaluator::all_true(&[]);
4569 let validator = EdifactValidator::new(evaluator);
4570 let external = NoOpExternalProvider;
4571
4572 let segments = vec![OwnedSegment {
4573 id: "STS".into(),
4574 elements: vec![
4575 vec!["Z33".into()], vec![], vec!["E01".into()], ],
4579 segment_number: 5,
4580 }];
4581
4582 let workflow = AhbWorkflow {
4583 pruefidentifikator: "13017".to_string(),
4584 description: "Test".to_string(),
4585 communication_direction: None,
4586 ub_definitions: BTreeMap::new(),
4587 fields: vec![AhbFieldRule {
4588 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4589 name: "Statusanlaß, Code".to_string(),
4590 ahb_status: "X".to_string(),
4591 element_index: Some(2),
4592 component_index: Some(0),
4593 codes: vec![
4594 AhbCodeRule {
4595 value: "E01".into(),
4596 description: "Code 1".into(),
4597 ahb_status: "X [4P0..1]".into(),
4598 },
4599 AhbCodeRule {
4600 value: "E02".into(),
4601 description: "Code 2".into(),
4602 ahb_status: "X [4P0..1]".into(),
4603 },
4604 ],
4605 parent_group_ahb_status: Some("Muss".to_string()),
4606 segment_ahb_status: None,
4607 mig_number: None,
4608 format: None,
4609 }],
4610 };
4611
4612 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4613 let pkg_errors: Vec<_> = report
4614 .by_category(ValidationCategory::Ahb)
4615 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4616 .collect();
4617 assert!(
4618 pkg_errors.is_empty(),
4619 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
4620 pkg_errors
4621 );
4622 }
4623
4624 #[test]
4625 fn test_package_cardinality_zero_present_min_zero() {
4626 let evaluator = MockEvaluator::all_true(&[]);
4628 let validator = EdifactValidator::new(evaluator);
4629 let external = NoOpExternalProvider;
4630
4631 let segments = vec![OwnedSegment {
4632 id: "STS".into(),
4633 elements: vec![
4634 vec!["Z33".into()],
4635 vec![],
4636 vec!["X99".into()], ],
4638 segment_number: 5,
4639 }];
4640
4641 let workflow = AhbWorkflow {
4642 pruefidentifikator: "13017".to_string(),
4643 description: "Test".to_string(),
4644 communication_direction: None,
4645 ub_definitions: BTreeMap::new(),
4646 fields: vec![AhbFieldRule {
4647 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4648 name: "Statusanlaß, Code".to_string(),
4649 ahb_status: "X".to_string(),
4650 element_index: Some(2),
4651 component_index: Some(0),
4652 codes: vec![
4653 AhbCodeRule {
4654 value: "E01".into(),
4655 description: "Code 1".into(),
4656 ahb_status: "X [4P0..1]".into(),
4657 },
4658 AhbCodeRule {
4659 value: "E02".into(),
4660 description: "Code 2".into(),
4661 ahb_status: "X [4P0..1]".into(),
4662 },
4663 ],
4664 parent_group_ahb_status: Some("Muss".to_string()),
4665 segment_ahb_status: None,
4666 mig_number: None,
4667 format: None,
4668 }],
4669 };
4670
4671 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4672 let pkg_errors: Vec<_> = report
4673 .by_category(ValidationCategory::Ahb)
4674 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4675 .collect();
4676 assert!(
4677 pkg_errors.is_empty(),
4678 "0 codes, min=0 — no error expected, got: {:?}",
4679 pkg_errors
4680 );
4681 }
4682
4683 #[test]
4684 fn test_package_cardinality_too_many() {
4685 let evaluator = MockEvaluator::all_true(&[]);
4687 let validator = EdifactValidator::new(evaluator);
4688 let external = NoOpExternalProvider;
4689
4690 let segments = vec![
4692 OwnedSegment {
4693 id: "STS".into(),
4694 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
4695 segment_number: 5,
4696 },
4697 OwnedSegment {
4698 id: "STS".into(),
4699 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
4700 segment_number: 6,
4701 },
4702 ];
4703
4704 let workflow = AhbWorkflow {
4705 pruefidentifikator: "13017".to_string(),
4706 description: "Test".to_string(),
4707 communication_direction: None,
4708 ub_definitions: BTreeMap::new(),
4709 fields: vec![AhbFieldRule {
4710 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4711 name: "Statusanlaß, Code".to_string(),
4712 ahb_status: "X".to_string(),
4713 element_index: Some(2),
4714 component_index: Some(0),
4715 codes: vec![
4716 AhbCodeRule {
4717 value: "E01".into(),
4718 description: "Code 1".into(),
4719 ahb_status: "X [4P0..1]".into(),
4720 },
4721 AhbCodeRule {
4722 value: "E02".into(),
4723 description: "Code 2".into(),
4724 ahb_status: "X [4P0..1]".into(),
4725 },
4726 ],
4727 parent_group_ahb_status: Some("Muss".to_string()),
4728 segment_ahb_status: None,
4729 mig_number: None,
4730 format: None,
4731 }],
4732 };
4733
4734 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4735 let pkg_errors: Vec<_> = report
4736 .by_category(ValidationCategory::Ahb)
4737 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4738 .collect();
4739 assert_eq!(
4740 pkg_errors.len(),
4741 1,
4742 "2 codes present, max=1 — expected 1 error, got: {:?}",
4743 pkg_errors
4744 );
4745 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
4746 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
4747 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
4748 }
4749
4750 #[test]
4751 fn test_package_cardinality_too_few() {
4752 let evaluator = MockEvaluator::all_true(&[]);
4754 let validator = EdifactValidator::new(evaluator);
4755 let external = NoOpExternalProvider;
4756
4757 let segments = vec![OwnedSegment {
4758 id: "STS".into(),
4759 elements: vec![
4760 vec!["Z33".into()],
4761 vec![],
4762 vec!["X99".into()], ],
4764 segment_number: 5,
4765 }];
4766
4767 let workflow = AhbWorkflow {
4768 pruefidentifikator: "13017".to_string(),
4769 description: "Test".to_string(),
4770 communication_direction: None,
4771 ub_definitions: BTreeMap::new(),
4772 fields: vec![AhbFieldRule {
4773 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4774 name: "Statusanlaß, Code".to_string(),
4775 ahb_status: "X".to_string(),
4776 element_index: Some(2),
4777 component_index: Some(0),
4778 codes: vec![
4779 AhbCodeRule {
4780 value: "E01".into(),
4781 description: "Code 1".into(),
4782 ahb_status: "X [5P1..3]".into(),
4783 },
4784 AhbCodeRule {
4785 value: "E02".into(),
4786 description: "Code 2".into(),
4787 ahb_status: "X [5P1..3]".into(),
4788 },
4789 AhbCodeRule {
4790 value: "E03".into(),
4791 description: "Code 3".into(),
4792 ahb_status: "X [5P1..3]".into(),
4793 },
4794 ],
4795 parent_group_ahb_status: Some("Muss".to_string()),
4796 segment_ahb_status: None,
4797 mig_number: None,
4798 format: None,
4799 }],
4800 };
4801
4802 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4803 let pkg_errors: Vec<_> = report
4804 .by_category(ValidationCategory::Ahb)
4805 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4806 .collect();
4807 assert_eq!(
4808 pkg_errors.len(),
4809 1,
4810 "0 codes present, min=1 — expected 1 error, got: {:?}",
4811 pkg_errors
4812 );
4813 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
4814 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
4815 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
4816 }
4817
4818 #[test]
4819 fn test_package_cardinality_no_packages_in_workflow() {
4820 let evaluator = MockEvaluator::all_true(&[]);
4822 let validator = EdifactValidator::new(evaluator);
4823 let external = NoOpExternalProvider;
4824
4825 let segments = vec![OwnedSegment {
4826 id: "STS".into(),
4827 elements: vec![vec!["E01".into()]],
4828 segment_number: 5,
4829 }];
4830
4831 let workflow = AhbWorkflow {
4832 pruefidentifikator: "13017".to_string(),
4833 description: "Test".to_string(),
4834 communication_direction: None,
4835 ub_definitions: BTreeMap::new(),
4836 fields: vec![AhbFieldRule {
4837 segment_path: "STS/9015".to_string(),
4838 name: "Status Code".to_string(),
4839 ahb_status: "X".to_string(),
4840 codes: vec![AhbCodeRule {
4841 value: "E01".into(),
4842 description: "Code 1".into(),
4843 ahb_status: "X".into(),
4844 }],
4845 parent_group_ahb_status: Some("Muss".to_string()),
4846 segment_ahb_status: None,
4847 ..Default::default()
4848 }],
4849 };
4850
4851 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4852 let pkg_errors: Vec<_> = report
4853 .by_category(ValidationCategory::Ahb)
4854 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4855 .collect();
4856 assert!(
4857 pkg_errors.is_empty(),
4858 "No packages in workflow — no errors expected"
4859 );
4860 }
4861
4862 #[test]
4863 fn test_package_cardinality_with_condition_and_package() {
4864 let evaluator = MockEvaluator::all_true(&[901]);
4866 let validator = EdifactValidator::new(evaluator);
4867 let external = NoOpExternalProvider;
4868
4869 let segments = vec![OwnedSegment {
4870 id: "STS".into(),
4871 elements: vec![vec![], vec![], vec!["E01".into()]],
4872 segment_number: 5,
4873 }];
4874
4875 let workflow = AhbWorkflow {
4876 pruefidentifikator: "13017".to_string(),
4877 description: "Test".to_string(),
4878 communication_direction: None,
4879 ub_definitions: BTreeMap::new(),
4880 fields: vec![AhbFieldRule {
4881 segment_path: "SG10/STS/C556/9013".to_string(),
4882 name: "Code".to_string(),
4883 ahb_status: "X".to_string(),
4884 element_index: Some(2),
4885 component_index: Some(0),
4886 codes: vec![
4887 AhbCodeRule {
4888 value: "E01".into(),
4889 description: "Code 1".into(),
4890 ahb_status: "X [901] [4P0..1]".into(),
4891 },
4892 AhbCodeRule {
4893 value: "E02".into(),
4894 description: "Code 2".into(),
4895 ahb_status: "X [901] [4P0..1]".into(),
4896 },
4897 ],
4898 parent_group_ahb_status: Some("Muss".to_string()),
4899 segment_ahb_status: None,
4900 mig_number: None,
4901 format: None,
4902 }],
4903 };
4904
4905 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4906 let pkg_errors: Vec<_> = report
4907 .by_category(ValidationCategory::Ahb)
4908 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4909 .collect();
4910 assert!(
4911 pkg_errors.is_empty(),
4912 "1 code within [4P0..1] bounds — no error, got: {:?}",
4913 pkg_errors
4914 );
4915 }
4916
4917 #[test]
4918 fn test_package_cardinality_scoped_per_group_instance() {
4919 use mig_types::navigator::GroupNavigator;
4927
4928 struct TwoSg10s {
4929 sts_a: OwnedSegment,
4930 sts_b: OwnedSegment,
4931 }
4932 impl GroupNavigator for TwoSg10s {
4933 fn find_segments_in_group(
4934 &self,
4935 segment_id: &str,
4936 group_path: &[&str],
4937 instance_index: usize,
4938 ) -> Vec<OwnedSegment> {
4939 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
4940 match instance_index {
4941 0 => vec![self.sts_a.clone()],
4942 1 => vec![self.sts_b.clone()],
4943 _ => vec![],
4944 }
4945 } else {
4946 vec![]
4947 }
4948 }
4949 fn find_segments_with_qualifier_in_group(
4950 &self,
4951 _: &str,
4952 _: usize,
4953 _: &str,
4954 _: &[&str],
4955 _: usize,
4956 ) -> Vec<OwnedSegment> {
4957 vec![]
4958 }
4959 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4960 match group_path {
4961 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
4962 ["SG5", "SG6", "SG9", "SG10"] => 2,
4963 _ => 0,
4964 }
4965 }
4966 }
4967
4968 let sts_a = OwnedSegment {
4969 id: "STS".into(),
4970 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4971 segment_number: 10,
4972 };
4973 let sts_b = OwnedSegment {
4974 id: "STS".into(),
4975 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4976 segment_number: 15,
4977 };
4978 let nav = TwoSg10s {
4979 sts_a: sts_a.clone(),
4980 sts_b: sts_b.clone(),
4981 };
4982
4983 let evaluator = MockEvaluator::all_true(&[]);
4984 let validator = EdifactValidator::new(evaluator);
4985 let external = NoOpExternalProvider;
4986
4987 let workflow = AhbWorkflow {
4988 pruefidentifikator: "13025".to_string(),
4989 description: "Test".to_string(),
4990 communication_direction: None,
4991 ub_definitions: BTreeMap::new(),
4992 fields: vec![AhbFieldRule {
4993 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4994 name: "Statusanlaß, Code".to_string(),
4995 ahb_status: "X".to_string(),
4996 element_index: Some(2),
4997 component_index: Some(0),
4998 codes: vec![
4999 AhbCodeRule {
5000 value: "E01".into(),
5001 description: "Code 1".into(),
5002 ahb_status: "X [4P0..1]".into(),
5003 },
5004 AhbCodeRule {
5005 value: "E02".into(),
5006 description: "Code 2".into(),
5007 ahb_status: "X [4P0..1]".into(),
5008 },
5009 ],
5010 parent_group_ahb_status: Some("Muss".to_string()),
5011 segment_ahb_status: None,
5012 mig_number: None,
5013 format: None,
5014 }],
5015 };
5016
5017 let report = validator.validate_with_navigator(
5018 &[sts_a, sts_b],
5019 &workflow,
5020 &external,
5021 ValidationLevel::Full,
5022 &nav,
5023 );
5024 let pkg_errors: Vec<_> = report
5025 .by_category(ValidationCategory::Ahb)
5026 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5027 .collect();
5028 assert!(
5029 pkg_errors.is_empty(),
5030 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
5031 pkg_errors
5032 );
5033 }
5034
5035 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
5036 OwnedSegment {
5037 id: id.to_string(),
5038 elements: elements
5039 .into_iter()
5040 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
5041 .collect(),
5042 segment_number: 0,
5043 }
5044 }
5045
5046 #[test]
5057 fn test_package_cardinality_scoped_to_rule_mig_variant() {
5058 use mig_types::navigator::GroupNavigator;
5059
5060 struct TwoSg8Variants {
5061 seq_z01: OwnedSegment,
5062 seq_z45: OwnedSegment,
5063 }
5064 impl GroupNavigator for TwoSg8Variants {
5065 fn find_segments_in_group(
5066 &self,
5067 segment_id: &str,
5068 group_path: &[&str],
5069 instance_index: usize,
5070 ) -> Vec<OwnedSegment> {
5071 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
5072 match instance_index {
5073 0 => vec![self.seq_z01.clone()],
5074 1 => vec![self.seq_z45.clone()],
5075 _ => vec![],
5076 }
5077 } else {
5078 vec![]
5079 }
5080 }
5081 fn find_segments_with_qualifier_in_group(
5082 &self,
5083 _: &str,
5084 _: usize,
5085 _: &str,
5086 _: &[&str],
5087 _: usize,
5088 ) -> Vec<OwnedSegment> {
5089 vec![]
5090 }
5091 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5092 match group_path {
5093 ["SG4"] => 1,
5094 ["SG4", "SG8"] => 2,
5095 _ => 0,
5096 }
5097 }
5098 fn instance_has_mig_number(
5099 &self,
5100 group_path: &[&str],
5101 instance_index: usize,
5102 mig_number: &str,
5103 ) -> bool {
5104 if group_path != ["SG4", "SG8"] {
5105 return true;
5106 }
5107 match (instance_index, mig_number) {
5108 (0, "00115") => true,
5109 (0, _) => false,
5110 (1, "00171") => true,
5111 (1, _) => false,
5112 _ => false,
5113 }
5114 }
5115 }
5116
5117 let seq_z01 = OwnedSegment {
5118 id: "SEQ".into(),
5119 elements: vec![vec!["Z01".into()], vec!["1".into()]],
5120 segment_number: 10,
5121 };
5122 let seq_z45 = OwnedSegment {
5123 id: "SEQ".into(),
5124 elements: vec![vec!["Z45".into()], vec!["1".into()]],
5125 segment_number: 20,
5126 };
5127 let nav = TwoSg8Variants {
5128 seq_z01: seq_z01.clone(),
5129 seq_z45: seq_z45.clone(),
5130 };
5131
5132 let evaluator = MockEvaluator::all_true(&[]);
5133 let validator = EdifactValidator::new(evaluator);
5134 let external = NoOpExternalProvider;
5135
5136 let workflow = AhbWorkflow {
5137 pruefidentifikator: "55218".to_string(),
5138 description: "Test".to_string(),
5139 communication_direction: None,
5140 ub_definitions: BTreeMap::new(),
5141 fields: vec![AhbFieldRule {
5142 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5143 name: "Handlung, Code".to_string(),
5144 ahb_status: "X".to_string(),
5145 element_index: Some(0),
5146 component_index: Some(0),
5147 codes: vec![
5148 AhbCodeRule {
5149 value: "Z45".into(),
5150 description: "NNA".into(),
5151 ahb_status: "X [1P1..4294967295]".into(),
5152 },
5153 AhbCodeRule {
5154 value: "Z84".into(),
5155 description: "Differenz-NNA".into(),
5156 ahb_status: "X [1P0..4294967295]".into(),
5157 },
5158 ],
5159 parent_group_ahb_status: Some("Muss".to_string()),
5160 segment_ahb_status: None,
5161 mig_number: Some("00171".to_string()),
5162 format: None,
5163 }],
5164 };
5165
5166 let report = validator.validate_with_navigator(
5167 &[seq_z01, seq_z45],
5168 &workflow,
5169 &external,
5170 ValidationLevel::Full,
5171 &nav,
5172 );
5173 let pkg_errors: Vec<_> = report
5174 .by_category(ValidationCategory::Ahb)
5175 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5176 .collect();
5177 assert!(
5178 pkg_errors.is_empty(),
5179 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
5180 pkg_errors
5181 );
5182 }
5183
5184 #[test]
5185 fn test_unt_count_correct() {
5186 let segments = vec![
5188 make_segment("UNH", vec![vec!["001"]]),
5189 make_segment("BGM", vec![vec!["E01"]]),
5190 make_segment("DTM", vec![vec!["137", "20250401"]]),
5191 make_segment("UNT", vec![vec!["4", "001"]]),
5192 ];
5193 assert!(
5194 validate_unt_segment_count(&segments).is_none(),
5195 "Correct count should produce no issue"
5196 );
5197 }
5198
5199 #[test]
5200 fn test_unt_count_mismatch() {
5201 let segments = vec![
5203 make_segment("UNH", vec![vec!["001"]]),
5204 make_segment("BGM", vec![vec!["E01"]]),
5205 make_segment("UNT", vec![vec!["5", "001"]]),
5206 ];
5207 let issue =
5208 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
5209 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5210 assert_eq!(issue.severity, Severity::Error);
5211 assert!(narrate(&issue).contains("declared 5"));
5212 assert!(narrate(&issue).contains("actual 3"));
5213 }
5214
5215 #[test]
5216 fn test_unt_count_excludes_envelope() {
5217 let segments = vec![
5219 make_segment("UNA", vec![]),
5220 make_segment("UNB", vec![vec!["UNOC", "3"]]),
5221 make_segment("UNH", vec![vec!["001"]]),
5222 make_segment("BGM", vec![vec!["E01"]]),
5223 make_segment("UNT", vec![vec!["3", "001"]]),
5224 make_segment("UNZ", vec![vec!["1"]]),
5225 ];
5226 assert!(
5227 validate_unt_segment_count(&segments).is_none(),
5228 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
5229 );
5230 }
5231
5232 #[test]
5233 fn test_unt_count_no_unt_returns_none() {
5234 let segments = vec![
5235 make_segment("UNH", vec![vec!["001"]]),
5236 make_segment("BGM", vec![vec!["E01"]]),
5237 ];
5238 assert!(
5239 validate_unt_segment_count(&segments).is_none(),
5240 "No UNT segment should return None (not our problem)"
5241 );
5242 }
5243
5244 #[test]
5245 fn test_unt_count_rejects_multi_message_input() {
5246 let segments = vec![
5248 make_segment("UNH", vec![vec!["001"]]),
5249 make_segment("BGM", vec![vec!["E01"]]),
5250 make_segment("UNT", vec![vec!["3", "001"]]),
5251 make_segment("UNH", vec![vec!["002"]]),
5252 make_segment("BGM", vec![vec!["E02"]]),
5253 make_segment("UNT", vec![vec!["3", "002"]]),
5254 ];
5255 let issue = validate_unt_segment_count(&segments)
5256 .expect("Multi-message input should produce an error");
5257 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5258 assert!(
5259 narrate(&issue).contains("2 UNH"),
5260 "Should mention UNH count: {}",
5261 narrate(&issue)
5262 );
5263 }
5264
5265 #[test]
5266 fn test_code_validation_accepts_multi_code_variant_qualifier() {
5267 let evaluator = MockEvaluator::new(vec![]);
5274 let validator = EdifactValidator::new(evaluator);
5275 let external = NoOpExternalProvider;
5276
5277 let rff_z39 = OwnedSegment {
5278 id: "RFF".to_string(),
5279 elements: vec![
5280 vec!["RFF".to_string()],
5281 vec!["Z39".to_string(), "REF1".to_string()],
5282 ],
5283 segment_number: 1,
5284 };
5285
5286 let workflow = AhbWorkflow {
5287 pruefidentifikator: "55035".to_string(),
5288 description: "Test".to_string(),
5289 communication_direction: None,
5290 fields: vec![
5291 AhbFieldRule {
5293 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5294 name: "Referenznummer Qualifier".to_string(),
5295 ahb_status: "Muss".to_string(),
5296 codes: vec![
5297 AhbCodeRule {
5298 value: "Z31".to_string(),
5299 description: "".to_string(),
5300 ahb_status: "X".to_string(),
5301 },
5302 AhbCodeRule {
5303 value: "Z39".to_string(),
5304 description: "".to_string(),
5305 ahb_status: "X".to_string(),
5306 },
5307 ],
5308 parent_group_ahb_status: None,
5309 segment_ahb_status: None,
5310 element_index: Some(1),
5311 component_index: Some(0),
5312 mig_number: Some("00075".to_string()),
5313 format: None,
5314 },
5315 AhbFieldRule {
5317 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5318 name: "Referenznummer Qualifier".to_string(),
5319 ahb_status: "Muss".to_string(),
5320 codes: vec![AhbCodeRule {
5321 value: "Z33".to_string(),
5322 description: "".to_string(),
5323 ahb_status: "X".to_string(),
5324 }],
5325 parent_group_ahb_status: None,
5326 segment_ahb_status: None,
5327 element_index: Some(1),
5328 component_index: Some(0),
5329 mig_number: Some("00078".to_string()),
5330 format: None,
5331 },
5332 ],
5333 ub_definitions: BTreeMap::new(),
5334 };
5335
5336 let report = validator.validate(
5337 &[rff_z39],
5338 &workflow,
5339 &external,
5340 ValidationLevel::Conditions,
5341 );
5342
5343 let code_errors: Vec<_> = report
5344 .by_category(ValidationCategory::Code)
5345 .filter(|i| {
5346 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5347 })
5348 .collect();
5349 assert!(
5350 code_errors.is_empty(),
5351 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
5352 code_errors
5353 );
5354 }
5355
5356 #[test]
5357 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
5358 let evaluator = MockEvaluator::new(vec![]);
5364 let validator = EdifactValidator::new(evaluator);
5365 let external = NoOpExternalProvider;
5366
5367 let pia_5_z12 = OwnedSegment {
5371 id: "PIA".to_string(),
5372 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
5373 segment_number: 1,
5374 };
5375 let pia_5_srw = OwnedSegment {
5376 id: "PIA".to_string(),
5377 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
5378 segment_number: 2,
5379 };
5380
5381 let make_rules = |mig: &str, composite_code: &str| {
5382 vec![
5383 AhbFieldRule {
5384 segment_path: "SG4/SG8/PIA/4347".to_string(),
5385 name: "Produkt-ID-Funktion".to_string(),
5386 ahb_status: "Muss".to_string(),
5387 codes: vec![AhbCodeRule {
5388 value: "5".to_string(),
5389 description: "".to_string(),
5390 ahb_status: "X".to_string(),
5391 }],
5392 parent_group_ahb_status: None,
5393 segment_ahb_status: None,
5394 element_index: Some(0),
5395 component_index: Some(0),
5396 mig_number: Some(mig.to_string()),
5397 format: None,
5398 },
5399 AhbFieldRule {
5400 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
5401 name: "Artikel/Dienstleistung-Identifikator".to_string(),
5402 ahb_status: "Muss".to_string(),
5403 codes: vec![AhbCodeRule {
5404 value: composite_code.to_string(),
5405 description: "".to_string(),
5406 ahb_status: "X".to_string(),
5407 }],
5408 parent_group_ahb_status: None,
5409 segment_ahb_status: None,
5410 element_index: Some(1),
5411 component_index: Some(0),
5412 mig_number: Some(mig.to_string()),
5413 format: None,
5414 },
5415 ]
5416 };
5417
5418 let mut fields = make_rules("00108", "Z12");
5419 fields.extend(make_rules("00197", "SRW"));
5420
5421 let workflow = AhbWorkflow {
5422 pruefidentifikator: "55035".to_string(),
5423 description: "Test".to_string(),
5424 communication_direction: None,
5425 fields,
5426 ub_definitions: BTreeMap::new(),
5427 };
5428
5429 let report = validator.validate(
5430 &[pia_5_z12, pia_5_srw],
5431 &workflow,
5432 &external,
5433 ValidationLevel::Conditions,
5434 );
5435
5436 let code_errors: Vec<_> = report
5437 .by_category(ValidationCategory::Code)
5438 .filter(|i| {
5439 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5440 })
5441 .collect();
5442 assert!(
5443 code_errors.is_empty(),
5444 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
5445 code_errors
5446 );
5447 }
5448}