1use std::collections::{HashMap, HashSet};
4
5use crate::expr::{ConditionExpr, ConditionParser};
6
7use crate::eval::{
8 ConditionEvaluator, ConditionExprEvaluator, ConditionResult, EvaluationContext, GroupScope,
9 ExternalConditionProvider,
10};
11use mig_types::navigator::GroupNavigator;
12use mig_types::segment::OwnedSegment;
13
14use super::tree::{AhbGroupNode, AhbNode, ValidatedTree};
15
16use super::codes::ErrorCodes;
17use super::issue::{Severity, ValidationCategory, ValidationIssue};
18use super::level::ValidationLevel;
19use super::report::ValidationReport;
20
21#[derive(Debug, Clone, Default)]
26pub struct AhbFieldRule {
27 pub segment_path: String,
29
30 pub name: String,
32
33 pub ahb_status: String,
35
36 pub codes: Vec<AhbCodeRule>,
38
39 pub parent_group_ahb_status: Option<String>,
44
45 pub segment_ahb_status: Option<String>,
51
52 pub element_index: Option<usize>,
55
56 pub component_index: Option<usize>,
59
60 pub mig_number: Option<String>,
63}
64
65#[derive(Debug, Clone, Default)]
67pub struct AhbCodeRule {
68 pub value: String,
70
71 pub description: String,
73
74 pub ahb_status: String,
76}
77
78#[derive(Debug, Clone)]
80pub struct AhbWorkflow {
81 pub pruefidentifikator: String,
83
84 pub description: String,
86
87 pub communication_direction: Option<String>,
89
90 pub fields: Vec<AhbFieldRule>,
92
93 pub ub_definitions: HashMap<String, ConditionExpr>,
99}
100
101pub struct EdifactValidator<E: ConditionEvaluator> {
134 evaluator: E,
135}
136
137impl<E: ConditionEvaluator> EdifactValidator<E> {
138 pub fn new(evaluator: E) -> Self {
140 Self { evaluator }
141 }
142
143 pub fn validate(
156 &self,
157 segments: &[OwnedSegment],
158 workflow: &AhbWorkflow,
159 external: &dyn ExternalConditionProvider,
160 level: ValidationLevel,
161 ) -> ValidationReport {
162 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
163 .with_format_version(self.evaluator.format_version())
164 .with_pruefidentifikator(&workflow.pruefidentifikator);
165
166 let ctx = EvaluationContext::new(&workflow.pruefidentifikator, external, segments);
167
168 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
169 self.validate_conditions(workflow, &ctx, &mut report);
170 }
171
172 report
173 }
174
175 pub fn validate_with_navigator(
181 &self,
182 segments: &[OwnedSegment],
183 workflow: &AhbWorkflow,
184 external: &dyn ExternalConditionProvider,
185 level: ValidationLevel,
186 navigator: &dyn GroupNavigator,
187 ) -> ValidationReport {
188 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
189 .with_format_version(self.evaluator.format_version())
190 .with_pruefidentifikator(&workflow.pruefidentifikator);
191
192 let ctx = EvaluationContext::with_navigator(
193 &workflow.pruefidentifikator,
194 external,
195 segments,
196 navigator,
197 );
198
199 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
200 self.validate_conditions(workflow, &ctx, &mut report);
201 }
202
203 report
204 }
205
206 pub fn validate_tree(
213 &self,
214 validated_tree: &ValidatedTree,
215 segments: &[OwnedSegment],
216 external: &dyn ExternalConditionProvider,
217 level: ValidationLevel,
218 navigator: Option<&dyn GroupNavigator>,
219 ) -> ValidationReport {
220 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
221 .with_format_version(self.evaluator.format_version())
222 .with_pruefidentifikator(validated_tree.pruefidentifikator);
223
224 if !matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
225 return report;
226 }
227
228 let ctx = match navigator {
229 Some(nav) => EvaluationContext::with_navigator(
230 validated_tree.pruefidentifikator,
231 external,
232 segments,
233 nav,
234 ),
235 None => EvaluationContext::new(validated_tree.pruefidentifikator, external, segments),
236 };
237
238 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
239
240 let mut all_nodes: Vec<&AhbNode> = Vec::new();
243 all_nodes.extend(validated_tree.root_fields.iter());
244 for group in &validated_tree.groups {
245 collect_nodes_depth_first(group, &mut all_nodes);
246 }
247
248 let mut tag_migs: HashMap<String, HashSet<&str>> = HashMap::new();
255 for node in &all_nodes {
256 if let Some(ref m) = node.rule.mig_number {
257 tag_migs
258 .entry(extract_segment_id(&node.rule.segment_path))
259 .or_default()
260 .insert(m.as_str());
261 }
262 }
263 for rule in &validated_tree.unmatched_rules {
264 if let Some(ref m) = rule.mig_number {
265 tag_migs
266 .entry(extract_segment_id(&rule.segment_path))
267 .or_default()
268 .insert(m.as_str());
269 }
270 }
271
272 for node in &validated_tree.root_fields {
274 evaluate_node(
275 node,
276 &ctx,
277 &expr_eval,
278 &self.evaluator,
279 validated_tree.ub_definitions,
280 &tag_migs,
281 None,
282 &mut report,
283 );
284 }
285
286 let mut instance_counter: HashMap<&str, usize> = HashMap::new();
291 for group in &validated_tree.groups {
292 let instance_index = *instance_counter
293 .entry(group.group_id)
294 .and_modify(|c| *c += 1)
295 .or_insert(0);
296
297 let group_path_storage = [group.group_id];
298 let scoped_ctx = ctx.with_scope(GroupScope {
301 group_path: &group_path_storage,
302 instance_index,
303 });
304
305 walk_group_instance(
306 group,
307 &scoped_ctx,
308 &expr_eval,
309 &self.evaluator,
310 validated_tree.ub_definitions,
311 &tag_migs,
312 instance_index,
313 &mut report,
314 );
315 }
316
317 for field in &validated_tree.unmatched_rules {
323 if should_skip_for_parent_group(field, &expr_eval, &ctx, validated_tree.ub_definitions)
324 {
325 continue;
326 }
327
328 let (condition_result, _) = expr_eval.evaluate_status_detailed_with_ub(
329 &field.ahb_status,
330 &ctx,
331 validated_tree.ub_definitions,
332 );
333
334 if matches!(condition_result, ConditionResult::True)
335 && is_mandatory_status(&field.ahb_status)
336 && !is_field_present(&ctx, field)
337 && !is_group_variant_absent(&ctx, field)
338 {
339 let mut issue = ValidationIssue::new(
340 Severity::Error,
341 ValidationCategory::Ahb,
342 ErrorCodes::MISSING_REQUIRED_FIELD,
343 format!(
344 "Required field '{}' at {} is missing",
345 field.name, field.segment_path
346 ),
347 )
348 .with_field_path(&field.segment_path)
349 .with_rule(&field.ahb_status);
350 if let Some(first_code) = field.codes.first() {
351 issue.expected_value = Some(first_code.value.clone());
352 }
353 issue = attach_field_position(issue, field);
354 report.add_issue(issue);
355 }
356 }
357
358 if matches!(level, ValidationLevel::Full) {
362 let mut all_fields: Vec<AhbFieldRule> = Vec::new();
363 for node in &all_nodes {
364 all_fields.push(node.rule.clone());
365 }
366 for rule in &validated_tree.unmatched_rules {
367 all_fields.push((*rule).clone());
368 }
369 let synthetic_workflow = AhbWorkflow {
370 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
371 description: String::new(),
372 communication_direction: None,
373 fields: all_fields,
374 ub_definitions: validated_tree.ub_definitions.clone(),
375 };
376 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
377 self.validate_package_cardinality(&synthetic_workflow, &ctx, &mut report);
378 }
379
380 report
381 }
382
383 fn validate_conditions(
385 &self,
386 workflow: &AhbWorkflow,
387 ctx: &EvaluationContext,
388 report: &mut ValidationReport,
389 ) {
390 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
391
392 for field in &workflow.fields {
393 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
395 continue;
396 }
397
398 let (condition_result, unknown_ids) = expr_eval.evaluate_status_detailed_with_ub(
401 &field.ahb_status,
402 ctx,
403 &workflow.ub_definitions,
404 );
405
406 match condition_result {
407 ConditionResult::True => {
408 if is_mandatory_status(&field.ahb_status)
410 && !is_field_present(ctx, field)
411 && !is_group_variant_absent(ctx, field)
412 {
413 let mut issue = ValidationIssue::new(
414 Severity::Error,
415 ValidationCategory::Ahb,
416 ErrorCodes::MISSING_REQUIRED_FIELD,
417 format!(
418 "Required field '{}' at {} is missing",
419 field.name, field.segment_path
420 ),
421 )
422 .with_field_path(&field.segment_path)
423 .with_rule(&field.ahb_status);
424 if let Some(first_code) = field.codes.first() {
428 issue.expected_value = Some(first_code.value.clone());
429 }
430 report.add_issue(issue);
431 }
432 }
433 ConditionResult::False => {
434 if is_mandatory_status(&field.ahb_status) && is_field_present(ctx, field) {
440 report.add_issue(
441 ValidationIssue::new(
442 Severity::Error,
443 ValidationCategory::Ahb,
444 ErrorCodes::CONDITIONAL_RULE_VIOLATION,
445 format!(
446 "Field '{}' at {} is present but does not satisfy condition: {}",
447 field.name, field.segment_path, field.ahb_status
448 ),
449 )
450 .with_field_path(&field.segment_path)
451 .with_rule(&field.ahb_status),
452 );
453 }
454 }
455 ConditionResult::Unknown => {
456 let mut external_ids = Vec::new();
461 let mut undetermined_ids = Vec::new();
462 let mut missing_ids = Vec::new();
463 for id in unknown_ids {
464 if self.evaluator.is_external(id) {
465 external_ids.push(id);
466 } else if self.evaluator.is_known(id) {
467 undetermined_ids.push(id);
468 } else {
469 missing_ids.push(id);
470 }
471 }
472
473 let mut parts = Vec::new();
474 if !external_ids.is_empty() {
475 let ids: Vec<String> =
476 external_ids.iter().map(|id| format!("[{id}]")).collect();
477 parts.push(format!(
478 "external conditions require provider: {}",
479 ids.join(", ")
480 ));
481 }
482 if !undetermined_ids.is_empty() {
483 let ids: Vec<String> = undetermined_ids
484 .iter()
485 .map(|id| format!("[{id}]"))
486 .collect();
487 parts.push(format!(
488 "conditions could not be determined from message data: {}",
489 ids.join(", ")
490 ));
491 }
492 if !missing_ids.is_empty() {
493 let ids: Vec<String> =
494 missing_ids.iter().map(|id| format!("[{id}]")).collect();
495 parts.push(format!("missing conditions: {}", ids.join(", ")));
496 }
497 let detail = if parts.is_empty() {
498 String::new()
499 } else {
500 format!(" ({})", parts.join("; "))
501 };
502 report.add_issue(
503 ValidationIssue::new(
504 Severity::Info,
505 ValidationCategory::Ahb,
506 ErrorCodes::CONDITION_UNKNOWN,
507 format!(
508 "Condition for field '{}' could not be fully evaluated{}",
509 field.name, detail
510 ),
511 )
512 .with_field_path(&field.segment_path)
513 .with_rule(&field.ahb_status),
514 );
515 }
516 }
517 }
518
519 self.validate_codes_cross_field(workflow, ctx, report);
524
525 self.validate_package_cardinality(workflow, ctx, report);
528 }
529
530 fn validate_package_cardinality(
537 &self,
538 workflow: &AhbWorkflow,
539 ctx: &EvaluationContext,
540 report: &mut ValidationReport,
541 ) {
542 struct PackageGroup {
545 min: u32,
546 max: u32,
547 code_values: Vec<String>,
548 element_index: usize,
549 component_index: usize,
550 }
551
552 let mut groups: HashMap<(String, Option<String>, u32), PackageGroup> = HashMap::new();
561
562 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
563
564 for field in &workflow.fields {
565 let el_idx = field.element_index.unwrap_or(0);
566 let comp_idx = field.component_index.unwrap_or(0);
567
568 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
570 continue;
571 }
572
573 for code in &field.codes {
574 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
576 let mut packages = Vec::new();
578 collect_packages(&expr, &mut packages);
579
580 for (pkg_id, pkg_min, pkg_max) in packages {
581 let key = (field.segment_path.clone(), field.mig_number.clone(), pkg_id);
582 let group = groups.entry(key).or_insert_with(|| PackageGroup {
583 min: pkg_min,
584 max: pkg_max,
585 code_values: Vec::new(),
586 element_index: el_idx,
587 component_index: comp_idx,
588 });
589 group.min = group.min.max(pkg_min);
592 group.max = group.max.min(pkg_max);
593 group.code_values.push(code.value.clone());
594 }
595 }
596 }
597 }
598
599 for ((seg_path, mig_number, pkg_id), group) in &groups {
607 let segment_id = extract_segment_id(seg_path);
608
609 let mut unique_codes: Vec<&str> =
613 group.code_values.iter().map(|s| s.as_str()).collect();
614 unique_codes.sort_unstable();
615 unique_codes.dedup();
616 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
617
618 let group_path_str = extract_group_path_key(seg_path);
619 let group_path: Vec<&str> = if group_path_str.is_empty() {
620 Vec::new()
621 } else {
622 group_path_str.split('/').collect()
623 };
624
625 let min = group.min as usize;
626 let max = group.max as usize;
627
628 let per_instance_counts: Option<Vec<usize>> =
629 match (ctx.navigator, group_path.is_empty()) {
630 (Some(nav), false) => {
631 let instance_count = nav.group_instance_count(&group_path);
632 if instance_count == 0 {
633 None
634 } else {
635 Some(
643 (0..instance_count)
644 .filter(|i| match mig_number.as_deref() {
645 Some(m) => {
646 nav.instance_has_mig_number(&group_path, *i, m)
647 }
648 None => true,
649 })
650 .map(|i| {
651 nav.find_segments_in_group(&segment_id, &group_path, i)
652 .iter()
653 .filter_map(|seg| {
654 seg.elements
655 .get(group.element_index)
656 .and_then(|e| e.get(group.component_index))
657 .filter(|v| !v.is_empty())
658 .cloned()
659 })
660 .filter(|v| code_set.contains(v.as_str()))
661 .count()
662 })
663 .collect(),
664 )
665 }
666 }
667 _ => None,
668 };
669
670 let counts: Vec<usize> = per_instance_counts.unwrap_or_else(|| {
671 let segments = ctx.find_segments(&segment_id);
672 let count = segments
673 .iter()
674 .filter_map(|seg| {
675 seg.elements
676 .get(group.element_index)
677 .and_then(|e| e.get(group.component_index))
678 .filter(|v| !v.is_empty())
679 .map(|s| s.as_str())
680 })
681 .filter(|v| code_set.contains(v))
682 .count();
683 vec![count]
684 });
685
686 let mut reported_counts: HashSet<usize> = HashSet::new();
690 for present_count in counts {
691 if (present_count < min || present_count > max)
692 && reported_counts.insert(present_count)
693 {
694 let code_list = unique_codes.join(", ");
695 report.add_issue(
696 ValidationIssue::new(
697 Severity::Error,
698 ValidationCategory::Ahb,
699 ErrorCodes::PACKAGE_CARDINALITY_VIOLATION,
700 format!(
701 "Package [{}P{}..{}] at {}: {} code(s) present (allowed {}..{}). Codes in package: [{}]",
702 pkg_id, group.min, group.max, seg_path, present_count, group.min, group.max, code_list
703 ),
704 )
705 .with_field_path(seg_path)
706 .with_expected(format!("{}..{}", group.min, group.max))
707 .with_actual(present_count.to_string()),
708 );
709 }
710 }
711 }
712 }
713
714 fn validate_codes_cross_field(
726 &self,
727 workflow: &AhbWorkflow,
728 ctx: &EvaluationContext,
729 report: &mut ValidationReport,
730 ) {
731 if ctx.navigator.is_some() {
732 self.validate_codes_group_scoped(workflow, ctx, report);
733 } else {
734 self.validate_codes_tag_scoped(workflow, ctx, report);
735 }
736 }
737
738 fn validate_codes_group_scoped(
741 &self,
742 workflow: &AhbWorkflow,
743 ctx: &EvaluationContext,
744 report: &mut ValidationReport,
745 ) {
746 let by_loc = partition_codes_by_mig(workflow);
747 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
748 let nav = ctx.navigator.unwrap();
749
750 for ((group_key, tag), migs) in &by_loc {
751 let field_path = if group_key.is_empty() {
752 format!("{tag}/qualifier")
753 } else {
754 format!("{group_key}/{tag}/qualifier")
755 };
756
757 let group_path: Vec<&str> = if group_key.is_empty() {
758 Vec::new()
759 } else {
760 group_key.split('/').collect()
761 };
762
763 let tag_qualifiers = known_qualifiers.get(tag);
764
765 if group_path.is_empty() {
766 Self::validate_segments_per_mig(
767 &ctx.find_segments(tag),
768 migs,
769 tag_qualifiers,
770 tag,
771 &field_path,
772 report,
773 );
774 } else {
775 let instance_count = nav.group_instance_count(&group_path);
776 for i in 0..instance_count {
777 let owned = nav.find_segments_in_group(tag, &group_path, i);
778 let refs: Vec<&OwnedSegment> = owned.iter().collect();
779 Self::validate_segments_per_mig(
780 &refs,
781 migs,
782 tag_qualifiers,
783 tag,
784 &field_path,
785 report,
786 );
787 }
788 }
789 }
790 }
791
792 fn validate_codes_tag_scoped(
796 &self,
797 workflow: &AhbWorkflow,
798 ctx: &EvaluationContext,
799 report: &mut ValidationReport,
800 ) {
801 let by_loc = partition_codes_by_mig(workflow);
802 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
803 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
805 for ((_group_key, tag), migs) in by_loc {
806 let merged = by_tag.entry(tag).or_default();
807 for (mig_key, bucket) in migs {
808 let entry = merged.entry(mig_key).or_default();
809 if entry.qualifier_position.is_none() {
810 entry.qualifier_position = bucket.qualifier_position;
811 }
812 if entry.qualifier_position == bucket.qualifier_position {
813 entry.qualifier_values.extend(&bucket.qualifier_values);
814 }
815 for (pos, codes) in bucket.codes {
816 entry.codes.entry(pos).or_default().extend(codes);
817 }
818 }
819 }
820
821 for (tag, migs) in &by_tag {
822 let field_path = format!("{tag}/qualifier");
823 Self::validate_segments_per_mig(
824 &ctx.find_segments(tag),
825 migs,
826 known_qualifiers.get(tag),
827 tag,
828 &field_path,
829 report,
830 );
831 }
832 }
833
834 fn validate_segments_per_mig(
843 segments: &[&OwnedSegment],
844 migs: &HashMap<Option<String>, MigCodeBucket>,
845 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
846 tag: &str,
847 field_path: &str,
848 report: &mut ValidationReport,
849 ) {
850 for seg in segments {
851 match match_segment_to_mig(seg, migs) {
852 Some(bucket) => {
853 for ((el, c), allowed) in &bucket.codes {
854 if allowed.is_empty() {
855 continue;
856 }
857 Self::check_segments_against_codes(
858 vec![*seg],
859 allowed,
860 tag,
861 *el,
862 *c,
863 field_path,
864 report,
865 );
866 }
867 }
868 None => {
869 let Some(qualifiers) = tag_qualifiers else {
870 continue;
871 };
872 for ((el, c), allowed) in qualifiers {
877 let Some(actual) = seg
878 .elements
879 .get(*el)
880 .and_then(|e| e.get(*c))
881 .filter(|v| !v.is_empty())
882 .map(|s| s.as_str())
883 else {
884 continue;
885 };
886 if allowed.iter().any(|v| v == actual) {
887 break;
889 }
890 let bucket_values: HashSet<&str> = migs
893 .values()
894 .filter(|b| b.qualifier_position == Some((*el, *c)))
895 .flat_map(|b| b.qualifier_values.iter().copied())
896 .collect();
897 if !bucket_values.is_empty() {
898 Self::check_segments_against_codes(
899 vec![*seg],
900 &bucket_values,
901 tag,
902 *el,
903 *c,
904 field_path,
905 report,
906 );
907 break;
908 }
909 }
910 }
911 }
912 }
913 }
914
915 fn check_segments_against_codes(
917 segments: Vec<&OwnedSegment>,
918 allowed_codes: &HashSet<&str>,
919 _tag: &str,
920 el_idx: usize,
921 comp_idx: usize,
922 field_path: &str,
923 report: &mut ValidationReport,
924 ) {
925 for segment in segments {
926 if let Some(code_value) = segment
927 .elements
928 .get(el_idx)
929 .and_then(|e| e.get(comp_idx))
930 .filter(|v| !v.is_empty())
931 {
932 if !allowed_codes.contains(code_value.as_str()) {
933 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
934 sorted_codes.sort_unstable();
935 report.add_issue(
936 ValidationIssue::new(
937 Severity::Error,
938 ValidationCategory::Code,
939 ErrorCodes::CODE_NOT_ALLOWED_FOR_PID,
940 format!(
941 "Code '{}' is not allowed for this PID. Allowed: [{}]",
942 code_value,
943 sorted_codes.join(", ")
944 ),
945 )
946 .with_field_path(field_path)
947 .with_actual(code_value)
948 .with_expected(sorted_codes.join(", ")),
949 );
950 }
951 }
952 }
953 }
954}
955
956fn should_skip_for_parent_group<E: ConditionEvaluator>(
962 field: &AhbFieldRule,
963 expr_eval: &ConditionExprEvaluator<E>,
964 ctx: &EvaluationContext,
965 ub_definitions: &HashMap<String, ConditionExpr>,
966) -> bool {
967 if let Some(ref group_status) = field.parent_group_ahb_status {
968 if group_status.contains('[') {
969 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
970 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
971 }
972 }
973 false
974}
975
976fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
985 let segment_id = extract_segment_id(&field.segment_path);
986
987 if !field.codes.is_empty() {
993 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
994 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
995 let matching = ctx.find_segments(&segment_id);
996 return matching.iter().any(|seg| {
997 seg.elements
998 .get(el_idx)
999 .and_then(|e| e.get(comp_idx))
1000 .is_some_and(|v| required_codes.contains(&v.as_str()))
1001 });
1002 }
1003 if is_qualifier_field(&field.segment_path) {
1006 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1007 let el_idx = field.element_index.unwrap_or(0);
1008 let comp_idx = field.component_index.unwrap_or(0);
1009 let matching = ctx.find_segments(&segment_id);
1010 return matching.iter().any(|seg| {
1011 seg.elements
1012 .get(el_idx)
1013 .and_then(|e| e.get(comp_idx))
1014 .is_some_and(|v| required_codes.contains(&v.as_str()))
1015 });
1016 }
1017 }
1018
1019 ctx.has_segment(&segment_id)
1020}
1021
1022fn is_group_variant_absent(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1037 let group_path: Vec<&str> = field
1038 .segment_path
1039 .split('/')
1040 .take_while(|p| p.starts_with("SG"))
1041 .collect();
1042
1043 if group_path.is_empty() {
1044 return false;
1045 }
1046
1047 let nav = match ctx.navigator {
1048 Some(nav) => nav,
1049 None => return false,
1050 };
1051
1052 let instance_count = nav.group_instance_count(&group_path);
1053
1054 if instance_count == 0 {
1059 let is_group_mandatory = field
1060 .parent_group_ahb_status
1061 .as_deref()
1062 .is_some_and(is_mandatory_status);
1063 if !is_group_mandatory {
1064 return true;
1065 }
1066 return false;
1068 }
1069
1070 if let Some(ref group_status) = field.parent_group_ahb_status {
1074 if !is_mandatory_status(group_status) && !group_status.contains('[') {
1075 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1078 let segment_id = extract_segment_id(&field.segment_path);
1079 let required_codes: Vec<&str> =
1080 field.codes.iter().map(|c| c.value.as_str()).collect();
1081
1082 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1083 nav.find_segments_in_group(&segment_id, &group_path, i)
1084 .iter()
1085 .any(|seg| {
1086 seg.elements
1087 .first()
1088 .and_then(|e| e.first())
1089 .is_some_and(|v| required_codes.contains(&v.as_str()))
1090 })
1091 });
1092
1093 if !any_instance_has_qualifier {
1094 return true; }
1096 }
1097 }
1098 }
1099
1100 let segment_id = extract_segment_id(&field.segment_path);
1109 let segment_absent_from_all = (0..instance_count).all(|i| {
1110 nav.find_segments_in_group(&segment_id, &group_path, i)
1111 .is_empty()
1112 });
1113 if segment_absent_from_all {
1114 let group_has_other_segments =
1115 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1116 if group_has_other_segments {
1117 return true;
1118 }
1119 }
1120
1121 false
1122}
1123
1124fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1126 out.extend(group.fields.iter());
1127 for child in &group.children {
1128 collect_nodes_depth_first(child, out);
1129 }
1130}
1131
1132#[allow(clippy::too_many_arguments)]
1136fn evaluate_node<E: ConditionEvaluator>(
1137 node: &AhbNode,
1138 ctx: &EvaluationContext,
1139 expr_eval: &ConditionExprEvaluator<E>,
1140 evaluator: &E,
1141 ub_definitions: &HashMap<String, ConditionExpr>,
1142 tag_migs: &HashMap<String, HashSet<&str>>,
1143 instance_index: Option<usize>,
1144 report: &mut ValidationReport,
1145) {
1146 let field = node.rule;
1147
1148 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1150
1151 if should_skip_for_parent_group(field, expr_eval, ctx, ub_definitions) {
1153 return;
1154 }
1155
1156 let (condition_result, unknown_ids) =
1159 expr_eval.evaluate_status_detailed_with_ub(&field.ahb_status, &node_ctx, ub_definitions);
1160
1161 match condition_result {
1162 ConditionResult::True => {
1163 if is_mandatory_status(&field.ahb_status) && node.value.is_none() {
1172 let tag = extract_segment_id(&field.segment_path);
1173 let single_variant_present = node.segment_elements.is_none()
1174 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1175 && is_field_present(ctx, field);
1176 let segment_optional_and_absent = node.segment_elements.is_none()
1181 && field
1182 .segment_ahb_status
1183 .as_deref()
1184 .is_some_and(is_optional_segment_status);
1185 if !single_variant_present && !segment_optional_and_absent {
1186 let mut issue = ValidationIssue::new(
1187 Severity::Error,
1188 ValidationCategory::Ahb,
1189 ErrorCodes::MISSING_REQUIRED_FIELD,
1190 format!(
1191 "Required field '{}' at {} is missing",
1192 field.name, field.segment_path
1193 ),
1194 )
1195 .with_field_path(&field.segment_path)
1196 .with_rule(&field.ahb_status);
1197 if let Some(first_code) = field.codes.first() {
1198 issue.expected_value = Some(first_code.value.clone());
1199 }
1200 if let Some(idx) = instance_index {
1201 issue = issue.with_instance_index(idx);
1202 }
1203 if let Some(num) = node.matched_segment_number {
1204 issue = issue.with_position(crate::SegmentPosition {
1205 segment_number: num,
1206 byte_offset: 0,
1207 message_number: 1,
1208 });
1209 }
1210 issue = attach_field_position(issue, field);
1211 report.add_issue(issue);
1212 }
1213 }
1214 }
1215 ConditionResult::False => {
1216 if is_mandatory_status(&field.ahb_status) && node.value.is_some() {
1219 let mut issue = ValidationIssue::new(
1220 Severity::Error,
1221 ValidationCategory::Ahb,
1222 ErrorCodes::CONDITIONAL_RULE_VIOLATION,
1223 format!(
1224 "Field '{}' at {} is present but does not satisfy condition: {}",
1225 field.name, field.segment_path, field.ahb_status
1226 ),
1227 )
1228 .with_field_path(&field.segment_path)
1229 .with_rule(&field.ahb_status);
1230 if let Some(idx) = instance_index {
1231 issue = issue.with_instance_index(idx);
1232 }
1233 if let Some(num) = node.matched_segment_number {
1234 issue = issue.with_position(crate::SegmentPosition {
1235 segment_number: num,
1236 byte_offset: 0,
1237 message_number: 1,
1238 });
1239 }
1240 issue = attach_field_position(issue, field);
1241 report.add_issue(issue);
1242 }
1243 }
1244 ConditionResult::Unknown => {
1245 let mut external_ids = Vec::new();
1247 let mut undetermined_ids = Vec::new();
1248 let mut missing_ids = Vec::new();
1249 for id in unknown_ids {
1250 if evaluator.is_external(id) {
1251 external_ids.push(id);
1252 } else if evaluator.is_known(id) {
1253 undetermined_ids.push(id);
1254 } else {
1255 missing_ids.push(id);
1256 }
1257 }
1258
1259 let mut parts = Vec::new();
1260 if !external_ids.is_empty() {
1261 let ids: Vec<String> = external_ids.iter().map(|id| format!("[{id}]")).collect();
1262 parts.push(format!(
1263 "external conditions require provider: {}",
1264 ids.join(", ")
1265 ));
1266 }
1267 if !undetermined_ids.is_empty() {
1268 let ids: Vec<String> = undetermined_ids
1269 .iter()
1270 .map(|id| format!("[{id}]"))
1271 .collect();
1272 parts.push(format!(
1273 "conditions could not be determined from message data: {}",
1274 ids.join(", ")
1275 ));
1276 }
1277 if !missing_ids.is_empty() {
1278 let ids: Vec<String> = missing_ids.iter().map(|id| format!("[{id}]")).collect();
1279 parts.push(format!("missing conditions: {}", ids.join(", ")));
1280 }
1281 let detail = if parts.is_empty() {
1282 String::new()
1283 } else {
1284 format!(" ({})", parts.join("; "))
1285 };
1286 let mut issue = ValidationIssue::new(
1287 Severity::Info,
1288 ValidationCategory::Ahb,
1289 ErrorCodes::CONDITION_UNKNOWN,
1290 format!(
1291 "Condition for field '{}' could not be fully evaluated{}",
1292 field.name, detail
1293 ),
1294 )
1295 .with_field_path(&field.segment_path)
1296 .with_rule(&field.ahb_status);
1297 if let Some(idx) = instance_index {
1298 issue = issue.with_instance_index(idx);
1299 }
1300 if let Some(num) = node.matched_segment_number {
1301 issue = issue.with_position(crate::SegmentPosition {
1302 segment_number: num,
1303 byte_offset: 0,
1304 message_number: 1,
1305 });
1306 }
1307 issue = attach_field_position(issue, field);
1308 report.add_issue(issue);
1309 }
1310 }
1311}
1312
1313fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1318 match field.element_index {
1319 Some(el) => {
1320 let element_pos = (el as u32) + 2;
1321 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1322 issue.with_field_position(element_pos, component_pos)
1323 }
1324 None => issue,
1325 }
1326}
1327
1328#[allow(clippy::too_many_arguments)]
1339fn walk_group_instance<E: ConditionEvaluator>(
1340 group: &AhbGroupNode,
1341 scoped_ctx: &EvaluationContext,
1342 expr_eval: &ConditionExprEvaluator<E>,
1343 evaluator: &E,
1344 ub_definitions: &HashMap<String, ConditionExpr>,
1345 tag_migs: &HashMap<String, HashSet<&str>>,
1346 instance_index: usize,
1347 report: &mut ValidationReport,
1348) {
1349 for node in &group.fields {
1350 evaluate_node(
1351 node,
1352 scoped_ctx,
1353 expr_eval,
1354 evaluator,
1355 ub_definitions,
1356 tag_migs,
1357 Some(instance_index),
1358 report,
1359 );
1360 }
1361 for child in &group.children {
1362 walk_group_instance(
1363 child,
1364 scoped_ctx,
1365 expr_eval,
1366 evaluator,
1367 ub_definitions,
1368 tag_migs,
1369 instance_index,
1370 report,
1371 );
1372 }
1373}
1374
1375fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
1377 match expr {
1378 ConditionExpr::Package { id, min, max } => {
1379 out.push((*id, *min, *max));
1380 }
1381 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1382 for e in exprs {
1383 collect_packages(e, out);
1384 }
1385 }
1386 ConditionExpr::Xor(left, right) => {
1387 collect_packages(left, out);
1388 collect_packages(right, out);
1389 }
1390 ConditionExpr::Not(inner) => {
1391 collect_packages(inner, out);
1392 }
1393 ConditionExpr::Ref(_) => {}
1394 }
1395}
1396
1397fn is_mandatory_status(status: &str) -> bool {
1399 let trimmed = status.trim();
1400 trimmed.starts_with("Muss") || trimmed.starts_with('X')
1401}
1402
1403fn is_optional_segment_status(status: &str) -> bool {
1413 let trimmed = status.trim();
1414 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
1415}
1416
1417fn is_qualifier_field(path: &str) -> bool {
1427 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
1428 matches!(parts.len(), 2 | 3)
1430}
1431
1432#[derive(Default)]
1440struct MigCodeBucket<'a> {
1441 qualifier_position: Option<(usize, usize)>,
1444 qualifier_values: HashSet<&'a str>,
1447 codes: HashMap<(usize, usize), HashSet<&'a str>>,
1450}
1451
1452fn partition_codes_by_mig(
1457 workflow: &AhbWorkflow,
1458) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
1459 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
1460 for field in &workflow.fields {
1461 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
1462 continue;
1463 }
1464 let tag = extract_segment_id(&field.segment_path);
1465 let group_key = extract_group_path_key(&field.segment_path);
1466 let mig = field.mig_number.clone();
1467 let el = field.element_index.unwrap_or(0);
1468 let c = field.component_index.unwrap_or(0);
1469
1470 let bucket = out
1471 .entry((group_key, tag))
1472 .or_default()
1473 .entry(mig)
1474 .or_default();
1475
1476 let required: Vec<&str> = field
1483 .codes
1484 .iter()
1485 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
1486 .map(|code| code.value.as_str())
1487 .collect();
1488
1489 if !required.is_empty() {
1496 if bucket.qualifier_position.is_none() {
1497 bucket.qualifier_position = Some((el, c));
1498 }
1499 if bucket.qualifier_position == Some((el, c)) {
1500 bucket.qualifier_values.extend(required.iter().copied());
1501 }
1502 }
1503
1504 for v in required {
1505 bucket.codes.entry((el, c)).or_default().insert(v);
1506 }
1507 }
1508 out
1509}
1510
1511fn global_qualifiers_by_tag(
1517 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
1518) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
1519 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
1520 for ((_group, tag), migs) in by_loc {
1521 let tag_entry = out.entry(tag.clone()).or_default();
1522 for bucket in migs.values() {
1523 if let Some(pos) = bucket.qualifier_position {
1524 tag_entry
1525 .entry(pos)
1526 .or_default()
1527 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
1528 }
1529 }
1530 }
1531 out
1532}
1533
1534fn match_segment_to_mig<'a, 'b>(
1545 seg: &OwnedSegment,
1546 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
1547) -> Option<&'a MigCodeBucket<'b>> {
1548 let actual_at = |el: usize, c: usize| -> &str {
1549 seg.elements
1550 .get(el)
1551 .and_then(|e| e.get(c))
1552 .map(|s| s.as_str())
1553 .unwrap_or("")
1554 };
1555
1556 let mut best: Option<&MigCodeBucket> = None;
1557 let mut best_matches = 0usize;
1558
1559 for bucket in migs.values() {
1560 let Some((el, c)) = bucket.qualifier_position else {
1561 continue;
1562 };
1563 if !bucket.qualifier_values.contains(actual_at(el, c)) {
1564 continue;
1565 }
1566 let extra_matches = bucket
1567 .codes
1568 .iter()
1569 .filter(|(pos, _)| **pos != (el, c))
1570 .filter(|((e, k), allowed)| {
1571 let v = actual_at(*e, *k);
1572 !v.is_empty() && allowed.contains(v)
1573 })
1574 .count();
1575 if best.is_none() || extra_matches > best_matches {
1576 best = Some(bucket);
1577 best_matches = extra_matches;
1578 }
1579 }
1580 best
1581}
1582
1583fn extract_group_path_key(path: &str) -> String {
1588 let sg_parts: Vec<&str> = path
1589 .split('/')
1590 .take_while(|p| p.starts_with("SG"))
1591 .collect();
1592 sg_parts.join("/")
1593}
1594
1595fn extract_segment_id(path: &str) -> String {
1597 for part in path.split('/') {
1598 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
1600 continue;
1601 }
1602 if part.len() >= 3
1604 && part
1605 .chars()
1606 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
1607 {
1608 return part.to_string();
1609 }
1610 }
1611 path.split('/').next_back().unwrap_or(path).to_string()
1613}
1614
1615pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
1627 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
1629 if unh_count > 1 {
1630 return Some(ValidationIssue::new(
1631 Severity::Error,
1632 ValidationCategory::Structure,
1633 ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH,
1634 format!("UNT validation requires per-message segments, found {unh_count} UNH segments"),
1635 ));
1636 }
1637
1638 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
1640 let declared: usize = unt.get_element(0).parse().ok()?;
1641
1642 let actual = segments
1644 .iter()
1645 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
1646 .count();
1647
1648 if declared != actual {
1649 Some(
1650 ValidationIssue::new(
1651 Severity::Error,
1652 ValidationCategory::Structure,
1653 ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH,
1654 format!("UNT segment count mismatch: declared {declared}, actual {actual}"),
1655 )
1656 .with_field_path("UNT/0074")
1657 .with_expected(actual.to_string())
1658 .with_actual(declared.to_string()),
1659 )
1660 } else {
1661 None
1662 }
1663}
1664
1665#[cfg(test)]
1666mod tests {
1667 use super::*;
1668 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
1669 use std::collections::HashMap;
1670
1671 struct MockEvaluator {
1673 results: HashMap<u32, CR>,
1674 }
1675
1676 impl MockEvaluator {
1677 fn new(results: Vec<(u32, CR)>) -> Self {
1678 Self {
1679 results: results.into_iter().collect(),
1680 }
1681 }
1682
1683 fn all_true(ids: &[u32]) -> Self {
1684 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
1685 }
1686 }
1687
1688 impl ConditionEvaluator for MockEvaluator {
1689 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
1690 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
1691 }
1692 fn is_external(&self, _condition: u32) -> bool {
1693 false
1694 }
1695 fn message_type(&self) -> &str {
1696 "UTILMD"
1697 }
1698 fn format_version(&self) -> &str {
1699 "FV2510"
1700 }
1701 }
1702
1703 #[test]
1706 fn test_is_mandatory_status() {
1707 assert!(is_mandatory_status("Muss"));
1708 assert!(is_mandatory_status("Muss [182] ∧ [152]"));
1709 assert!(is_mandatory_status("X"));
1710 assert!(is_mandatory_status("X [567]"));
1711 assert!(!is_mandatory_status("Soll [1]"));
1712 assert!(!is_mandatory_status("Kann [1]"));
1713 assert!(!is_mandatory_status(""));
1714 }
1715
1716 #[test]
1717 fn test_extract_segment_id_simple() {
1718 assert_eq!(extract_segment_id("NAD"), "NAD");
1719 }
1720
1721 #[test]
1722 fn test_extract_segment_id_with_sg_prefix() {
1723 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
1724 }
1725
1726 #[test]
1727 fn test_extract_segment_id_nested_sg() {
1728 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
1729 }
1730
1731 #[test]
1734 fn test_validate_missing_mandatory_field() {
1735 let evaluator = MockEvaluator::all_true(&[182, 152]);
1736 let validator = EdifactValidator::new(evaluator);
1737 let external = NoOpExternalProvider;
1738
1739 let workflow = AhbWorkflow {
1740 pruefidentifikator: "11001".to_string(),
1741 description: "Test".to_string(),
1742 communication_direction: None,
1743 fields: vec![AhbFieldRule {
1744 segment_path: "SG2/NAD/C082/3039".to_string(),
1745 name: "MP-ID des MSB".to_string(),
1746 ahb_status: "Muss [182] ∧ [152]".to_string(),
1747 codes: vec![],
1748 parent_group_ahb_status: None,
1749 segment_ahb_status: None,
1750 ..Default::default()
1751 }],
1752 ub_definitions: HashMap::new(),
1753 };
1754
1755 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1757
1758 assert!(!report.is_valid());
1760 let errors: Vec<_> = report.errors().collect();
1761 assert_eq!(errors.len(), 1);
1762 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1763 assert!(errors[0].message.contains("MP-ID des MSB"));
1764 }
1765
1766 #[test]
1767 fn test_validate_condition_false_no_error() {
1768 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
1770 let validator = EdifactValidator::new(evaluator);
1771 let external = NoOpExternalProvider;
1772
1773 let workflow = AhbWorkflow {
1774 pruefidentifikator: "11001".to_string(),
1775 description: "Test".to_string(),
1776 communication_direction: None,
1777 fields: vec![AhbFieldRule {
1778 segment_path: "NAD".to_string(),
1779 name: "Partnerrolle".to_string(),
1780 ahb_status: "Muss [182] ∧ [152]".to_string(),
1781 codes: vec![],
1782 parent_group_ahb_status: None,
1783 segment_ahb_status: None,
1784 ..Default::default()
1785 }],
1786 ub_definitions: HashMap::new(),
1787 };
1788
1789 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1790
1791 assert!(report.is_valid());
1793 }
1794
1795 #[test]
1796 fn test_validate_condition_unknown_adds_info() {
1797 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
1799 let validator = EdifactValidator::new(evaluator);
1801 let external = NoOpExternalProvider;
1802
1803 let workflow = AhbWorkflow {
1804 pruefidentifikator: "11001".to_string(),
1805 description: "Test".to_string(),
1806 communication_direction: None,
1807 fields: vec![AhbFieldRule {
1808 segment_path: "NAD".to_string(),
1809 name: "Partnerrolle".to_string(),
1810 ahb_status: "Muss [182] ∧ [152]".to_string(),
1811 codes: vec![],
1812 parent_group_ahb_status: None,
1813 segment_ahb_status: None,
1814 ..Default::default()
1815 }],
1816 ub_definitions: HashMap::new(),
1817 };
1818
1819 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1820
1821 assert!(report.is_valid());
1823 let infos: Vec<_> = report.infos().collect();
1824 assert_eq!(infos.len(), 1);
1825 assert_eq!(infos[0].code, ErrorCodes::CONDITION_UNKNOWN);
1826 }
1827
1828 #[test]
1829 fn test_validate_structure_level_skips_conditions() {
1830 let evaluator = MockEvaluator::all_true(&[182, 152]);
1831 let validator = EdifactValidator::new(evaluator);
1832 let external = NoOpExternalProvider;
1833
1834 let workflow = AhbWorkflow {
1835 pruefidentifikator: "11001".to_string(),
1836 description: "Test".to_string(),
1837 communication_direction: None,
1838 fields: vec![AhbFieldRule {
1839 segment_path: "NAD".to_string(),
1840 name: "Partnerrolle".to_string(),
1841 ahb_status: "Muss [182] ∧ [152]".to_string(),
1842 codes: vec![],
1843 parent_group_ahb_status: None,
1844 segment_ahb_status: None,
1845 ..Default::default()
1846 }],
1847 ub_definitions: HashMap::new(),
1848 };
1849
1850 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
1852
1853 assert!(report.is_valid());
1855 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
1856 }
1857
1858 #[test]
1859 fn test_validate_empty_workflow_no_condition_errors() {
1860 let evaluator = MockEvaluator::all_true(&[]);
1861 let validator = EdifactValidator::new(evaluator);
1862 let external = NoOpExternalProvider;
1863
1864 let empty_workflow = AhbWorkflow {
1865 pruefidentifikator: String::new(),
1866 description: String::new(),
1867 communication_direction: None,
1868 fields: vec![],
1869 ub_definitions: HashMap::new(),
1870 };
1871
1872 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
1873
1874 assert!(report.is_valid());
1875 }
1876
1877 #[test]
1878 fn test_validate_bare_muss_always_required() {
1879 let evaluator = MockEvaluator::new(vec![]);
1880 let validator = EdifactValidator::new(evaluator);
1881 let external = NoOpExternalProvider;
1882
1883 let workflow = AhbWorkflow {
1884 pruefidentifikator: "55001".to_string(),
1885 description: "Test".to_string(),
1886 communication_direction: Some("NB an LF".to_string()),
1887 fields: vec![AhbFieldRule {
1888 segment_path: "SG2/NAD/3035".to_string(),
1889 name: "Partnerrolle".to_string(),
1890 ahb_status: "Muss".to_string(), codes: vec![],
1892 parent_group_ahb_status: None,
1893 segment_ahb_status: None,
1894 ..Default::default()
1895 }],
1896 ub_definitions: HashMap::new(),
1897 };
1898
1899 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1900
1901 assert!(!report.is_valid());
1903 assert_eq!(report.error_count(), 1);
1904 }
1905
1906 #[test]
1907 fn test_validate_x_status_is_mandatory() {
1908 let evaluator = MockEvaluator::new(vec![]);
1909 let validator = EdifactValidator::new(evaluator);
1910 let external = NoOpExternalProvider;
1911
1912 let workflow = AhbWorkflow {
1913 pruefidentifikator: "55001".to_string(),
1914 description: "Test".to_string(),
1915 communication_direction: None,
1916 fields: vec![AhbFieldRule {
1917 segment_path: "DTM".to_string(),
1918 name: "Datum".to_string(),
1919 ahb_status: "X".to_string(),
1920 codes: vec![],
1921 parent_group_ahb_status: None,
1922 segment_ahb_status: None,
1923 ..Default::default()
1924 }],
1925 ub_definitions: HashMap::new(),
1926 };
1927
1928 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1929
1930 assert!(!report.is_valid());
1931 let errors: Vec<_> = report.errors().collect();
1932 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1933 }
1934
1935 #[test]
1936 fn test_validate_soll_not_mandatory() {
1937 let evaluator = MockEvaluator::new(vec![]);
1938 let validator = EdifactValidator::new(evaluator);
1939 let external = NoOpExternalProvider;
1940
1941 let workflow = AhbWorkflow {
1942 pruefidentifikator: "55001".to_string(),
1943 description: "Test".to_string(),
1944 communication_direction: None,
1945 fields: vec![AhbFieldRule {
1946 segment_path: "DTM".to_string(),
1947 name: "Datum".to_string(),
1948 ahb_status: "Soll".to_string(),
1949 codes: vec![],
1950 parent_group_ahb_status: None,
1951 segment_ahb_status: None,
1952 ..Default::default()
1953 }],
1954 ub_definitions: HashMap::new(),
1955 };
1956
1957 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1958
1959 assert!(report.is_valid());
1961 }
1962
1963 #[test]
1964 fn test_report_includes_metadata() {
1965 let evaluator = MockEvaluator::new(vec![]);
1966 let validator = EdifactValidator::new(evaluator);
1967 let external = NoOpExternalProvider;
1968
1969 let workflow = AhbWorkflow {
1970 pruefidentifikator: "55001".to_string(),
1971 description: String::new(),
1972 communication_direction: None,
1973 fields: vec![],
1974 ub_definitions: HashMap::new(),
1975 };
1976
1977 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
1978
1979 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
1980 assert_eq!(report.level, ValidationLevel::Full);
1981 assert_eq!(report.message_type, "UTILMD");
1982 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
1983 }
1984
1985 #[test]
1986 fn test_validate_with_navigator_returns_report() {
1987 let evaluator = MockEvaluator::all_true(&[]);
1988 let validator = EdifactValidator::new(evaluator);
1989 let external = NoOpExternalProvider;
1990 let nav = crate::eval::NoOpGroupNavigator;
1991
1992 let workflow = AhbWorkflow {
1993 pruefidentifikator: "55001".to_string(),
1994 description: "Test".to_string(),
1995 communication_direction: None,
1996 fields: vec![],
1997 ub_definitions: HashMap::new(),
1998 };
1999
2000 let report = validator.validate_with_navigator(
2001 &[],
2002 &workflow,
2003 &external,
2004 ValidationLevel::Full,
2005 &nav,
2006 );
2007 assert!(report.is_valid());
2008 }
2009
2010 #[test]
2011 fn test_code_validation_composite_paths_valid_codes() {
2012 let evaluator = MockEvaluator::new(vec![]);
2016 let validator = EdifactValidator::new(evaluator);
2017 let external = NoOpExternalProvider;
2018
2019 let unh_segment = OwnedSegment {
2020 id: "UNH".to_string(),
2021 elements: vec![
2022 vec!["ALEXANDE951842".to_string()],
2023 vec![
2024 "UTILMD".to_string(),
2025 "D".to_string(),
2026 "11A".to_string(),
2027 "UN".to_string(),
2028 "S2.1".to_string(),
2029 ],
2030 ],
2031 segment_number: 1,
2032 };
2033
2034 let workflow = AhbWorkflow {
2035 pruefidentifikator: "55001".to_string(),
2036 description: "Test".to_string(),
2037 communication_direction: None,
2038 fields: vec![
2039 AhbFieldRule {
2040 segment_path: "UNH/S009/0065".to_string(),
2041 name: "Nachrichtentyp".to_string(),
2042 ahb_status: "X".to_string(),
2043 codes: vec![AhbCodeRule {
2044 value: "UTILMD".to_string(),
2045 description: "Stammdaten".to_string(),
2046 ahb_status: "X".to_string(),
2047 }],
2048 parent_group_ahb_status: None,
2049 segment_ahb_status: None,
2050 element_index: Some(1),
2051 component_index: Some(0),
2052 ..Default::default()
2053 },
2054 AhbFieldRule {
2055 segment_path: "UNH/S009/0052".to_string(),
2056 name: "Version".to_string(),
2057 ahb_status: "X".to_string(),
2058 codes: vec![AhbCodeRule {
2059 value: "D".to_string(),
2060 description: "Draft".to_string(),
2061 ahb_status: "X".to_string(),
2062 }],
2063 parent_group_ahb_status: None,
2064 segment_ahb_status: None,
2065 element_index: Some(1),
2066 component_index: Some(1),
2067 ..Default::default()
2068 },
2069 ],
2070 ub_definitions: HashMap::new(),
2071 };
2072
2073 let report = validator.validate(
2074 &[unh_segment],
2075 &workflow,
2076 &external,
2077 ValidationLevel::Conditions,
2078 );
2079
2080 let code_errors: Vec<_> = report
2081 .by_category(ValidationCategory::Code)
2082 .filter(|i| i.severity == Severity::Error)
2083 .collect();
2084 assert!(
2085 code_errors.is_empty(),
2086 "Expected no code errors when composite values match allowed codes, got: {:?}",
2087 code_errors
2088 );
2089 }
2090
2091 #[test]
2092 fn test_code_validation_partitions_by_mig_number() {
2093 let evaluator = MockEvaluator::new(vec![]);
2097 let validator = EdifactValidator::new(evaluator);
2098 let external = NoOpExternalProvider;
2099
2100 let sts_7 = OwnedSegment {
2101 id: "STS".to_string(),
2102 elements: vec![
2103 vec!["7".to_string()],
2104 vec![String::new()],
2105 vec!["GH02".to_string()],
2106 vec!["ZW4".to_string()],
2107 ],
2108 segment_number: 1,
2109 };
2110 let sts_e01 = OwnedSegment {
2111 id: "STS".to_string(),
2112 elements: vec![
2113 vec!["E01".to_string()],
2114 vec![String::new()],
2115 vec!["A99".to_string(), "E_0614".to_string()],
2116 ],
2117 segment_number: 2,
2118 };
2119
2120 let workflow = AhbWorkflow {
2121 pruefidentifikator: "55018".to_string(),
2122 description: "Test".to_string(),
2123 communication_direction: None,
2124 fields: vec![
2125 AhbFieldRule {
2127 segment_path: "SG4/STS/C601/9015".to_string(),
2128 name: "Statuskategorie".to_string(),
2129 ahb_status: "X".to_string(),
2130 codes: vec![AhbCodeRule {
2131 value: "7".to_string(),
2132 description: "Transaktionsgrund".to_string(),
2133 ahb_status: "X".to_string(),
2134 }],
2135 parent_group_ahb_status: None,
2136 segment_ahb_status: None,
2137 element_index: Some(0),
2138 component_index: Some(0),
2139 mig_number: Some("00035".to_string()),
2140 },
2141 AhbFieldRule {
2142 segment_path: "SG4/STS/C556/9013".to_string(),
2143 name: "Statusanlaß".to_string(),
2144 ahb_status: "X".to_string(),
2145 codes: vec![AhbCodeRule {
2146 value: "E03".to_string(),
2147 description: "Transaktionsgrund".to_string(),
2148 ahb_status: "X".to_string(),
2149 }],
2150 parent_group_ahb_status: None,
2151 segment_ahb_status: None,
2152 element_index: Some(2),
2153 component_index: Some(0),
2154 mig_number: Some("00035".to_string()),
2155 },
2156 AhbFieldRule {
2158 segment_path: "SG4/STS/C601/9015".to_string(),
2159 name: "Statuskategorie".to_string(),
2160 ahb_status: "X".to_string(),
2161 codes: vec![AhbCodeRule {
2162 value: "E01".to_string(),
2163 description: "Antwort".to_string(),
2164 ahb_status: "X".to_string(),
2165 }],
2166 parent_group_ahb_status: None,
2167 segment_ahb_status: None,
2168 element_index: Some(0),
2169 component_index: Some(0),
2170 mig_number: Some("00036".to_string()),
2171 },
2172 ],
2173 ub_definitions: HashMap::new(),
2174 };
2175
2176 let report = validator.validate(
2177 &[sts_7, sts_e01],
2178 &workflow,
2179 &external,
2180 ValidationLevel::Conditions,
2181 );
2182
2183 let code_errors: Vec<_> = report
2184 .by_category(ValidationCategory::Code)
2185 .filter(|i| {
2186 i.severity == Severity::Error && i.code == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2187 })
2188 .collect();
2189 assert_eq!(
2190 code_errors.len(),
2191 1,
2192 "Expected one COD002 (for GH02 only), got: {:?}",
2193 code_errors
2194 );
2195 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2196 }
2197
2198 #[test]
2199 fn test_code_validation_composite_paths_detects_invalid_code() {
2200 let evaluator = MockEvaluator::new(vec![]);
2203 let validator = EdifactValidator::new(evaluator);
2204 let external = NoOpExternalProvider;
2205
2206 let sts_segment = OwnedSegment {
2207 id: "STS".to_string(),
2208 elements: vec![
2209 vec!["7".to_string()],
2210 vec![String::new()],
2211 vec!["GH02".to_string()],
2212 vec!["ZW4".to_string()],
2213 ],
2214 segment_number: 1,
2215 };
2216
2217 let workflow = AhbWorkflow {
2218 pruefidentifikator: "55018".to_string(),
2219 description: "Test".to_string(),
2220 communication_direction: None,
2221 fields: vec![AhbFieldRule {
2222 segment_path: "SG4/STS/C556/9013".to_string(),
2223 name: "Statusanlaß".to_string(),
2224 ahb_status: "X".to_string(),
2225 codes: vec![AhbCodeRule {
2226 value: "E03".to_string(),
2227 description: "Transaktionsgrund".to_string(),
2228 ahb_status: "X".to_string(),
2229 }],
2230 parent_group_ahb_status: None,
2231 segment_ahb_status: None,
2232 element_index: Some(2),
2233 component_index: Some(0),
2234 ..Default::default()
2235 }],
2236 ub_definitions: HashMap::new(),
2237 };
2238
2239 let report = validator.validate(
2240 &[sts_segment],
2241 &workflow,
2242 &external,
2243 ValidationLevel::Conditions,
2244 );
2245
2246 let code_errors: Vec<_> = report
2247 .by_category(ValidationCategory::Code)
2248 .filter(|i| {
2249 i.severity == Severity::Error && i.code == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2250 })
2251 .collect();
2252 assert_eq!(
2253 code_errors.len(),
2254 1,
2255 "Expected COD002 for GH02, got: {:?}",
2256 code_errors
2257 );
2258 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2259 }
2260
2261 #[test]
2262 fn test_cross_field_code_validation_valid_qualifiers() {
2263 let evaluator = MockEvaluator::new(vec![]);
2266 let validator = EdifactValidator::new(evaluator);
2267 let external = NoOpExternalProvider;
2268
2269 let nad_ms = OwnedSegment {
2270 id: "NAD".to_string(),
2271 elements: vec![vec!["MS".to_string()]],
2272 segment_number: 4,
2273 };
2274 let nad_mr = OwnedSegment {
2275 id: "NAD".to_string(),
2276 elements: vec![vec!["MR".to_string()]],
2277 segment_number: 5,
2278 };
2279
2280 let workflow = AhbWorkflow {
2281 pruefidentifikator: "55001".to_string(),
2282 description: "Test".to_string(),
2283 communication_direction: None,
2284 fields: vec![
2285 AhbFieldRule {
2286 segment_path: "SG2/NAD/3035".to_string(),
2287 name: "Absender".to_string(),
2288 ahb_status: "X".to_string(),
2289 codes: vec![AhbCodeRule {
2290 value: "MS".to_string(),
2291 description: "Absender".to_string(),
2292 ahb_status: "X".to_string(),
2293 }],
2294 parent_group_ahb_status: None,
2295 segment_ahb_status: None,
2296 ..Default::default()
2297 },
2298 AhbFieldRule {
2299 segment_path: "SG2/NAD/3035".to_string(),
2300 name: "Empfaenger".to_string(),
2301 ahb_status: "X".to_string(),
2302 codes: vec![AhbCodeRule {
2303 value: "MR".to_string(),
2304 description: "Empfaenger".to_string(),
2305 ahb_status: "X".to_string(),
2306 }],
2307 parent_group_ahb_status: None,
2308 segment_ahb_status: None,
2309 ..Default::default()
2310 },
2311 ],
2312 ub_definitions: HashMap::new(),
2313 };
2314
2315 let report = validator.validate(
2316 &[nad_ms, nad_mr],
2317 &workflow,
2318 &external,
2319 ValidationLevel::Conditions,
2320 );
2321
2322 let code_errors: Vec<_> = report
2323 .by_category(ValidationCategory::Code)
2324 .filter(|i| i.severity == Severity::Error)
2325 .collect();
2326 assert!(
2327 code_errors.is_empty(),
2328 "Expected no code errors for valid qualifiers, got: {:?}",
2329 code_errors
2330 );
2331 }
2332
2333 #[test]
2334 fn test_cross_field_code_validation_catches_invalid_qualifier() {
2335 let evaluator = MockEvaluator::new(vec![]);
2337 let validator = EdifactValidator::new(evaluator);
2338 let external = NoOpExternalProvider;
2339
2340 let nad_ms = OwnedSegment {
2341 id: "NAD".to_string(),
2342 elements: vec![vec!["MS".to_string()]],
2343 segment_number: 4,
2344 };
2345 let nad_mt = OwnedSegment {
2346 id: "NAD".to_string(),
2347 elements: vec![vec!["MT".to_string()]], segment_number: 5,
2349 };
2350
2351 let workflow = AhbWorkflow {
2352 pruefidentifikator: "55001".to_string(),
2353 description: "Test".to_string(),
2354 communication_direction: None,
2355 fields: vec![
2356 AhbFieldRule {
2357 segment_path: "SG2/NAD/3035".to_string(),
2358 name: "Absender".to_string(),
2359 ahb_status: "X".to_string(),
2360 codes: vec![AhbCodeRule {
2361 value: "MS".to_string(),
2362 description: "Absender".to_string(),
2363 ahb_status: "X".to_string(),
2364 }],
2365 parent_group_ahb_status: None,
2366 segment_ahb_status: None,
2367 ..Default::default()
2368 },
2369 AhbFieldRule {
2370 segment_path: "SG2/NAD/3035".to_string(),
2371 name: "Empfaenger".to_string(),
2372 ahb_status: "X".to_string(),
2373 codes: vec![AhbCodeRule {
2374 value: "MR".to_string(),
2375 description: "Empfaenger".to_string(),
2376 ahb_status: "X".to_string(),
2377 }],
2378 parent_group_ahb_status: None,
2379 segment_ahb_status: None,
2380 ..Default::default()
2381 },
2382 ],
2383 ub_definitions: HashMap::new(),
2384 };
2385
2386 let report = validator.validate(
2387 &[nad_ms, nad_mt],
2388 &workflow,
2389 &external,
2390 ValidationLevel::Conditions,
2391 );
2392
2393 let code_errors: Vec<_> = report
2394 .by_category(ValidationCategory::Code)
2395 .filter(|i| i.severity == Severity::Error)
2396 .collect();
2397 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
2398 assert!(code_errors[0].message.contains("MT"));
2399 assert!(code_errors[0].message.contains("MR"));
2400 assert!(code_errors[0].message.contains("MS"));
2401 }
2402
2403 #[test]
2404 fn test_cross_field_code_validation_unions_across_groups() {
2405 let evaluator = MockEvaluator::new(vec![]);
2409 let validator = EdifactValidator::new(evaluator);
2410 let external = NoOpExternalProvider;
2411
2412 let segments = vec![
2413 OwnedSegment {
2414 id: "NAD".to_string(),
2415 elements: vec![vec!["MS".to_string()]],
2416 segment_number: 3,
2417 },
2418 OwnedSegment {
2419 id: "NAD".to_string(),
2420 elements: vec![vec!["MR".to_string()]],
2421 segment_number: 4,
2422 },
2423 OwnedSegment {
2424 id: "NAD".to_string(),
2425 elements: vec![vec!["Z04".to_string()]],
2426 segment_number: 20,
2427 },
2428 OwnedSegment {
2429 id: "NAD".to_string(),
2430 elements: vec![vec!["Z09".to_string()]],
2431 segment_number: 21,
2432 },
2433 OwnedSegment {
2434 id: "NAD".to_string(),
2435 elements: vec![vec!["MT".to_string()]], segment_number: 22,
2437 },
2438 ];
2439
2440 let workflow = AhbWorkflow {
2441 pruefidentifikator: "55001".to_string(),
2442 description: "Test".to_string(),
2443 communication_direction: None,
2444 fields: vec![
2445 AhbFieldRule {
2446 segment_path: "SG2/NAD/3035".to_string(),
2447 name: "Absender".to_string(),
2448 ahb_status: "X".to_string(),
2449 codes: vec![AhbCodeRule {
2450 value: "MS".to_string(),
2451 description: "Absender".to_string(),
2452 ahb_status: "X".to_string(),
2453 }],
2454 parent_group_ahb_status: None,
2455 segment_ahb_status: None,
2456 ..Default::default()
2457 },
2458 AhbFieldRule {
2459 segment_path: "SG2/NAD/3035".to_string(),
2460 name: "Empfaenger".to_string(),
2461 ahb_status: "X".to_string(),
2462 codes: vec![AhbCodeRule {
2463 value: "MR".to_string(),
2464 description: "Empfaenger".to_string(),
2465 ahb_status: "X".to_string(),
2466 }],
2467 parent_group_ahb_status: None,
2468 segment_ahb_status: None,
2469 ..Default::default()
2470 },
2471 AhbFieldRule {
2472 segment_path: "SG4/SG12/NAD/3035".to_string(),
2473 name: "Anschlussnutzer".to_string(),
2474 ahb_status: "X".to_string(),
2475 codes: vec![AhbCodeRule {
2476 value: "Z04".to_string(),
2477 description: "Anschlussnutzer".to_string(),
2478 ahb_status: "X".to_string(),
2479 }],
2480 parent_group_ahb_status: None,
2481 segment_ahb_status: None,
2482 ..Default::default()
2483 },
2484 AhbFieldRule {
2485 segment_path: "SG4/SG12/NAD/3035".to_string(),
2486 name: "Korrespondenzanschrift".to_string(),
2487 ahb_status: "X".to_string(),
2488 codes: vec![AhbCodeRule {
2489 value: "Z09".to_string(),
2490 description: "Korrespondenzanschrift".to_string(),
2491 ahb_status: "X".to_string(),
2492 }],
2493 parent_group_ahb_status: None,
2494 segment_ahb_status: None,
2495 ..Default::default()
2496 },
2497 ],
2498 ub_definitions: HashMap::new(),
2499 };
2500
2501 let report =
2502 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
2503
2504 let code_errors: Vec<_> = report
2505 .by_category(ValidationCategory::Code)
2506 .filter(|i| i.severity == Severity::Error)
2507 .collect();
2508 assert_eq!(
2509 code_errors.len(),
2510 1,
2511 "Expected exactly one COD002 error for MT, got: {:?}",
2512 code_errors
2513 );
2514 assert!(code_errors[0].message.contains("MT"));
2515 }
2516
2517 #[test]
2518 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
2519 let evaluator = MockEvaluator::new(vec![]);
2528 let validator = EdifactValidator::new(evaluator);
2529 let external = NoOpExternalProvider;
2530
2531 let qty_67 = OwnedSegment {
2532 id: "QTY".to_string(),
2533 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
2534 segment_number: 10,
2535 };
2536
2537 let workflow = AhbWorkflow {
2538 pruefidentifikator: "13025".to_string(),
2539 description: "Test".to_string(),
2540 communication_direction: None,
2541 fields: vec![AhbFieldRule {
2542 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
2543 name: "Menge, Qualifier".to_string(),
2544 ahb_status: "X".to_string(),
2545 codes: vec![
2546 AhbCodeRule {
2547 value: "220".to_string(),
2548 description: "Wahrer Wert".to_string(),
2549 ahb_status: "X".to_string(),
2550 },
2551 AhbCodeRule {
2552 value: "67".to_string(),
2553 description: "Ersatzwert".to_string(),
2554 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
2555 },
2556 AhbCodeRule {
2557 value: "Z18".to_string(),
2558 description: "Vorläufiger Wert".to_string(),
2559 ahb_status: "X [35]".to_string(),
2560 },
2561 ],
2562 parent_group_ahb_status: None,
2563 segment_ahb_status: None,
2564 element_index: Some(0),
2565 component_index: Some(0),
2566 ..Default::default()
2567 }],
2568 ub_definitions: HashMap::new(),
2569 };
2570
2571 let report =
2572 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
2573
2574 let code_errors: Vec<_> = report
2575 .by_category(ValidationCategory::Code)
2576 .filter(|i| i.severity == Severity::Error)
2577 .collect();
2578 assert!(
2579 code_errors.is_empty(),
2580 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
2581 code_errors
2582 );
2583 }
2584
2585 #[test]
2586 fn test_is_qualifier_field_simple_paths() {
2587 assert!(is_qualifier_field("NAD/3035"));
2588 assert!(is_qualifier_field("SG2/NAD/3035"));
2589 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
2590 assert!(is_qualifier_field("LOC/3227"));
2591 }
2592
2593 #[test]
2594 fn test_is_qualifier_field_composite_paths() {
2595 assert!(is_qualifier_field("UNH/S009/0065"));
2599 assert!(is_qualifier_field("NAD/C082/3039"));
2600 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
2601 assert!(is_qualifier_field("SG4/STS/C556/9013"));
2602 }
2603
2604 #[test]
2605 fn test_is_qualifier_field_bare_segment() {
2606 assert!(!is_qualifier_field("NAD"));
2607 assert!(!is_qualifier_field("SG2/NAD"));
2608 }
2609
2610 #[test]
2611 fn test_is_qualifier_field_rejects_deep_paths() {
2612 assert!(!is_qualifier_field("SEG/A/B/C/D"));
2614 }
2615
2616 #[test]
2617 fn test_missing_qualifier_instance_is_detected() {
2618 let evaluator = MockEvaluator::new(vec![]);
2621 let validator = EdifactValidator::new(evaluator);
2622 let external = NoOpExternalProvider;
2623
2624 let nad_ms = OwnedSegment {
2625 id: "NAD".to_string(),
2626 elements: vec![vec!["MS".to_string()]],
2627 segment_number: 3,
2628 };
2629
2630 let workflow = AhbWorkflow {
2631 pruefidentifikator: "55001".to_string(),
2632 description: "Test".to_string(),
2633 communication_direction: None,
2634 fields: vec![
2635 AhbFieldRule {
2636 segment_path: "SG2/NAD/3035".to_string(),
2637 name: "Absender".to_string(),
2638 ahb_status: "X".to_string(),
2639 codes: vec![AhbCodeRule {
2640 value: "MS".to_string(),
2641 description: "Absender".to_string(),
2642 ahb_status: "X".to_string(),
2643 }],
2644 parent_group_ahb_status: None,
2645 segment_ahb_status: None,
2646 ..Default::default()
2647 },
2648 AhbFieldRule {
2649 segment_path: "SG2/NAD/3035".to_string(),
2650 name: "Empfaenger".to_string(),
2651 ahb_status: "Muss".to_string(),
2652 codes: vec![AhbCodeRule {
2653 value: "MR".to_string(),
2654 description: "Empfaenger".to_string(),
2655 ahb_status: "X".to_string(),
2656 }],
2657 parent_group_ahb_status: None,
2658 segment_ahb_status: None,
2659 ..Default::default()
2660 },
2661 ],
2662 ub_definitions: HashMap::new(),
2663 };
2664
2665 let report =
2666 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
2667
2668 let ahb_errors: Vec<_> = report
2669 .by_category(ValidationCategory::Ahb)
2670 .filter(|i| i.severity == Severity::Error)
2671 .collect();
2672 assert_eq!(
2673 ahb_errors.len(),
2674 1,
2675 "Expected AHB001 for missing NAD+MR, got: {:?}",
2676 ahb_errors
2677 );
2678 assert!(ahb_errors[0].message.contains("Empfaenger"));
2679 }
2680
2681 #[test]
2682 fn test_present_qualifier_instance_no_error() {
2683 let evaluator = MockEvaluator::new(vec![]);
2685 let validator = EdifactValidator::new(evaluator);
2686 let external = NoOpExternalProvider;
2687
2688 let segments = vec![
2689 OwnedSegment {
2690 id: "NAD".to_string(),
2691 elements: vec![vec!["MS".to_string()]],
2692 segment_number: 3,
2693 },
2694 OwnedSegment {
2695 id: "NAD".to_string(),
2696 elements: vec![vec!["MR".to_string()]],
2697 segment_number: 4,
2698 },
2699 ];
2700
2701 let workflow = AhbWorkflow {
2702 pruefidentifikator: "55001".to_string(),
2703 description: "Test".to_string(),
2704 communication_direction: None,
2705 fields: vec![
2706 AhbFieldRule {
2707 segment_path: "SG2/NAD/3035".to_string(),
2708 name: "Absender".to_string(),
2709 ahb_status: "Muss".to_string(),
2710 codes: vec![AhbCodeRule {
2711 value: "MS".to_string(),
2712 description: "Absender".to_string(),
2713 ahb_status: "X".to_string(),
2714 }],
2715 parent_group_ahb_status: None,
2716 segment_ahb_status: None,
2717 ..Default::default()
2718 },
2719 AhbFieldRule {
2720 segment_path: "SG2/NAD/3035".to_string(),
2721 name: "Empfaenger".to_string(),
2722 ahb_status: "Muss".to_string(),
2723 codes: vec![AhbCodeRule {
2724 value: "MR".to_string(),
2725 description: "Empfaenger".to_string(),
2726 ahb_status: "X".to_string(),
2727 }],
2728 parent_group_ahb_status: None,
2729 segment_ahb_status: None,
2730 ..Default::default()
2731 },
2732 ],
2733 ub_definitions: HashMap::new(),
2734 };
2735
2736 let report =
2737 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
2738
2739 let ahb_errors: Vec<_> = report
2740 .by_category(ValidationCategory::Ahb)
2741 .filter(|i| i.severity == Severity::Error)
2742 .collect();
2743 assert!(
2744 ahb_errors.is_empty(),
2745 "Expected no AHB001 errors, got: {:?}",
2746 ahb_errors
2747 );
2748 }
2749
2750 #[test]
2751 fn test_extract_group_path_key() {
2752 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
2753 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
2754 assert_eq!(extract_group_path_key("NAD/3035"), "");
2755 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
2756 }
2757
2758 #[test]
2759 fn test_absent_optional_group_no_missing_field_error() {
2760 use mig_types::navigator::GroupNavigator;
2763
2764 struct NavWithoutSG3;
2765 impl GroupNavigator for NavWithoutSG3 {
2766 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2767 vec![]
2768 }
2769 fn find_segments_with_qualifier_in_group(
2770 &self,
2771 _: &str,
2772 _: usize,
2773 _: &str,
2774 _: &[&str],
2775 _: usize,
2776 ) -> Vec<OwnedSegment> {
2777 vec![]
2778 }
2779 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2780 match group_path {
2781 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
2784 }
2785 }
2786 }
2787
2788 let evaluator = MockEvaluator::new(vec![]);
2789 let validator = EdifactValidator::new(evaluator);
2790 let external = NoOpExternalProvider;
2791 let nav = NavWithoutSG3;
2792
2793 let segments = vec![
2795 OwnedSegment {
2796 id: "NAD".into(),
2797 elements: vec![vec!["MS".into()]],
2798 segment_number: 3,
2799 },
2800 OwnedSegment {
2801 id: "NAD".into(),
2802 elements: vec![vec!["MR".into()]],
2803 segment_number: 4,
2804 },
2805 ];
2806
2807 let workflow = AhbWorkflow {
2808 pruefidentifikator: "55001".to_string(),
2809 description: "Test".to_string(),
2810 communication_direction: None,
2811 fields: vec![
2812 AhbFieldRule {
2813 segment_path: "SG2/SG3/CTA/3139".to_string(),
2814 name: "Funktion des Ansprechpartners, Code".to_string(),
2815 ahb_status: "Muss".to_string(),
2816 codes: vec![],
2817 parent_group_ahb_status: None,
2818 segment_ahb_status: None,
2819 ..Default::default()
2820 },
2821 AhbFieldRule {
2822 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
2823 name: "Name vom Ansprechpartner".to_string(),
2824 ahb_status: "X".to_string(),
2825 codes: vec![],
2826 parent_group_ahb_status: None,
2827 segment_ahb_status: None,
2828 ..Default::default()
2829 },
2830 ],
2831 ub_definitions: HashMap::new(),
2832 };
2833
2834 let report = validator.validate_with_navigator(
2835 &segments,
2836 &workflow,
2837 &external,
2838 ValidationLevel::Conditions,
2839 &nav,
2840 );
2841
2842 let ahb_errors: Vec<_> = report
2843 .by_category(ValidationCategory::Ahb)
2844 .filter(|i| i.severity == Severity::Error)
2845 .collect();
2846 assert!(
2847 ahb_errors.is_empty(),
2848 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
2849 ahb_errors
2850 );
2851 }
2852
2853 #[test]
2854 fn test_present_group_still_checks_mandatory_fields() {
2855 use mig_types::navigator::GroupNavigator;
2857
2858 struct NavWithSG3;
2859 impl GroupNavigator for NavWithSG3 {
2860 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2861 vec![]
2862 }
2863 fn find_segments_with_qualifier_in_group(
2864 &self,
2865 _: &str,
2866 _: usize,
2867 _: &str,
2868 _: &[&str],
2869 _: usize,
2870 ) -> Vec<OwnedSegment> {
2871 vec![]
2872 }
2873 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2874 match group_path {
2875 ["SG2"] => 1,
2876 ["SG2", "SG3"] => 1, _ => 0,
2878 }
2879 }
2880 }
2881
2882 let evaluator = MockEvaluator::new(vec![]);
2883 let validator = EdifactValidator::new(evaluator);
2884 let external = NoOpExternalProvider;
2885 let nav = NavWithSG3;
2886
2887 let segments = vec![OwnedSegment {
2889 id: "NAD".into(),
2890 elements: vec![vec!["MS".into()]],
2891 segment_number: 3,
2892 }];
2893
2894 let workflow = AhbWorkflow {
2895 pruefidentifikator: "55001".to_string(),
2896 description: "Test".to_string(),
2897 communication_direction: None,
2898 fields: vec![AhbFieldRule {
2899 segment_path: "SG2/SG3/CTA/3139".to_string(),
2900 name: "Funktion des Ansprechpartners, Code".to_string(),
2901 ahb_status: "Muss".to_string(),
2902 codes: vec![],
2903 parent_group_ahb_status: None,
2904 segment_ahb_status: None,
2905 ..Default::default()
2906 }],
2907 ub_definitions: HashMap::new(),
2908 };
2909
2910 let report = validator.validate_with_navigator(
2911 &segments,
2912 &workflow,
2913 &external,
2914 ValidationLevel::Conditions,
2915 &nav,
2916 );
2917
2918 let ahb_errors: Vec<_> = report
2919 .by_category(ValidationCategory::Ahb)
2920 .filter(|i| i.severity == Severity::Error)
2921 .collect();
2922 assert_eq!(
2923 ahb_errors.len(),
2924 1,
2925 "Expected AHB001 error when SG3 is present but CTA missing"
2926 );
2927 assert!(ahb_errors[0].message.contains("CTA"));
2928 }
2929
2930 #[test]
2931 fn test_missing_qualifier_with_navigator_is_detected() {
2932 use mig_types::navigator::GroupNavigator;
2935
2936 struct NavWithSG2;
2937 impl GroupNavigator for NavWithSG2 {
2938 fn find_segments_in_group(
2939 &self,
2940 segment_id: &str,
2941 group_path: &[&str],
2942 instance_index: usize,
2943 ) -> Vec<OwnedSegment> {
2944 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
2945 vec![OwnedSegment {
2946 id: "NAD".into(),
2947 elements: vec![vec!["MS".into()]],
2948 segment_number: 3,
2949 }]
2950 } else {
2951 vec![]
2952 }
2953 }
2954 fn find_segments_with_qualifier_in_group(
2955 &self,
2956 _: &str,
2957 _: usize,
2958 _: &str,
2959 _: &[&str],
2960 _: usize,
2961 ) -> Vec<OwnedSegment> {
2962 vec![]
2963 }
2964 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2965 match group_path {
2966 ["SG2"] => 1,
2967 _ => 0,
2968 }
2969 }
2970 }
2971
2972 let evaluator = MockEvaluator::new(vec![]);
2973 let validator = EdifactValidator::new(evaluator);
2974 let external = NoOpExternalProvider;
2975 let nav = NavWithSG2;
2976
2977 let segments = vec![OwnedSegment {
2978 id: "NAD".into(),
2979 elements: vec![vec!["MS".into()]],
2980 segment_number: 3,
2981 }];
2982
2983 let workflow = AhbWorkflow {
2984 pruefidentifikator: "55001".to_string(),
2985 description: "Test".to_string(),
2986 communication_direction: None,
2987 fields: vec![
2988 AhbFieldRule {
2989 segment_path: "SG2/NAD/3035".to_string(),
2990 name: "Absender".to_string(),
2991 ahb_status: "X".to_string(),
2992 codes: vec![AhbCodeRule {
2993 value: "MS".to_string(),
2994 description: "Absender".to_string(),
2995 ahb_status: "X".to_string(),
2996 }],
2997 parent_group_ahb_status: None,
2998 segment_ahb_status: None,
2999 ..Default::default()
3000 },
3001 AhbFieldRule {
3002 segment_path: "SG2/NAD/3035".to_string(),
3003 name: "Empfaenger".to_string(),
3004 ahb_status: "Muss".to_string(),
3005 codes: vec![AhbCodeRule {
3006 value: "MR".to_string(),
3007 description: "Empfaenger".to_string(),
3008 ahb_status: "X".to_string(),
3009 }],
3010 parent_group_ahb_status: None,
3011 segment_ahb_status: None,
3012 ..Default::default()
3013 },
3014 ],
3015 ub_definitions: HashMap::new(),
3016 };
3017
3018 let report = validator.validate_with_navigator(
3019 &segments,
3020 &workflow,
3021 &external,
3022 ValidationLevel::Conditions,
3023 &nav,
3024 );
3025
3026 let ahb_errors: Vec<_> = report
3027 .by_category(ValidationCategory::Ahb)
3028 .filter(|i| i.severity == Severity::Error)
3029 .collect();
3030 assert_eq!(
3031 ahb_errors.len(),
3032 1,
3033 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
3034 ahb_errors
3035 );
3036 assert!(ahb_errors[0].message.contains("Empfaenger"));
3037 }
3038
3039 #[test]
3040 fn test_optional_group_variant_absent_no_error() {
3041 use mig_types::navigator::GroupNavigator;
3046
3047 struct TestNav;
3048 impl GroupNavigator for TestNav {
3049 fn find_segments_in_group(
3050 &self,
3051 segment_id: &str,
3052 group_path: &[&str],
3053 instance_index: usize,
3054 ) -> Vec<OwnedSegment> {
3055 match (segment_id, group_path, instance_index) {
3056 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
3057 id: "LOC".into(),
3058 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3059 segment_number: 10,
3060 }],
3061 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
3062 id: "NAD".into(),
3063 elements: vec![vec!["MS".into()]],
3064 segment_number: 3,
3065 }],
3066 _ => vec![],
3067 }
3068 }
3069 fn find_segments_with_qualifier_in_group(
3070 &self,
3071 _: &str,
3072 _: usize,
3073 _: &str,
3074 _: &[&str],
3075 _: usize,
3076 ) -> Vec<OwnedSegment> {
3077 vec![]
3078 }
3079 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3080 match group_path {
3081 ["SG2"] => 1,
3082 ["SG4"] => 1,
3083 ["SG4", "SG5"] => 1, _ => 0,
3085 }
3086 }
3087 }
3088
3089 let evaluator = MockEvaluator::new(vec![]);
3090 let validator = EdifactValidator::new(evaluator);
3091 let external = NoOpExternalProvider;
3092 let nav = TestNav;
3093
3094 let segments = vec![
3095 OwnedSegment {
3096 id: "NAD".into(),
3097 elements: vec![vec!["MS".into()]],
3098 segment_number: 3,
3099 },
3100 OwnedSegment {
3101 id: "LOC".into(),
3102 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3103 segment_number: 10,
3104 },
3105 ];
3106
3107 let workflow = AhbWorkflow {
3108 pruefidentifikator: "55001".to_string(),
3109 description: "Test".to_string(),
3110 communication_direction: None,
3111 fields: vec![
3112 AhbFieldRule {
3114 segment_path: "SG2/NAD/3035".to_string(),
3115 name: "Absender".to_string(),
3116 ahb_status: "X".to_string(),
3117 codes: vec![AhbCodeRule {
3118 value: "MS".to_string(),
3119 description: "Absender".to_string(),
3120 ahb_status: "X".to_string(),
3121 }],
3122 parent_group_ahb_status: Some("Muss".to_string()),
3123 segment_ahb_status: None,
3124 ..Default::default()
3125 },
3126 AhbFieldRule {
3127 segment_path: "SG2/NAD/3035".to_string(),
3128 name: "Empfaenger".to_string(),
3129 ahb_status: "Muss".to_string(),
3130 codes: vec![AhbCodeRule {
3131 value: "MR".to_string(),
3132 description: "Empfaenger".to_string(),
3133 ahb_status: "X".to_string(),
3134 }],
3135 parent_group_ahb_status: Some("Muss".to_string()),
3136 segment_ahb_status: None,
3137 ..Default::default()
3138 },
3139 AhbFieldRule {
3141 segment_path: "SG4/SG5/LOC/3227".to_string(),
3142 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3143 ahb_status: "X".to_string(),
3144 codes: vec![AhbCodeRule {
3145 value: "Z16".to_string(),
3146 description: "Marktlokation".to_string(),
3147 ahb_status: "X".to_string(),
3148 }],
3149 parent_group_ahb_status: Some("Kann".to_string()),
3150 segment_ahb_status: None,
3151 ..Default::default()
3152 },
3153 AhbFieldRule {
3154 segment_path: "SG4/SG5/LOC/3227".to_string(),
3155 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3156 ahb_status: "Muss".to_string(),
3157 codes: vec![AhbCodeRule {
3158 value: "Z17".to_string(),
3159 description: "Messlokation".to_string(),
3160 ahb_status: "X".to_string(),
3161 }],
3162 parent_group_ahb_status: Some("Kann".to_string()),
3163 segment_ahb_status: None,
3164 ..Default::default()
3165 },
3166 ],
3167 ub_definitions: HashMap::new(),
3168 };
3169
3170 let report = validator.validate_with_navigator(
3171 &segments,
3172 &workflow,
3173 &external,
3174 ValidationLevel::Conditions,
3175 &nav,
3176 );
3177
3178 let ahb_errors: Vec<_> = report
3179 .by_category(ValidationCategory::Ahb)
3180 .filter(|i| i.severity == Severity::Error)
3181 .collect();
3182
3183 assert_eq!(
3186 ahb_errors.len(),
3187 1,
3188 "Expected only AHB001 for missing NAD+MR, got: {:?}",
3189 ahb_errors
3190 );
3191 assert!(
3192 ahb_errors[0].message.contains("Empfaenger"),
3193 "Error should be for missing NAD+MR (Empfaenger)"
3194 );
3195 }
3196
3197 #[test]
3198 fn test_conditional_group_variant_absent_no_error() {
3199 use mig_types::navigator::GroupNavigator;
3204
3205 struct TestNav;
3206 impl GroupNavigator for TestNav {
3207 fn find_segments_in_group(
3208 &self,
3209 segment_id: &str,
3210 group_path: &[&str],
3211 instance_index: usize,
3212 ) -> Vec<OwnedSegment> {
3213 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
3214 vec![OwnedSegment {
3215 id: "LOC".into(),
3216 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3217 segment_number: 10,
3218 }]
3219 } else {
3220 vec![]
3221 }
3222 }
3223 fn find_segments_with_qualifier_in_group(
3224 &self,
3225 _: &str,
3226 _: usize,
3227 _: &str,
3228 _: &[&str],
3229 _: usize,
3230 ) -> Vec<OwnedSegment> {
3231 vec![]
3232 }
3233 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3234 match group_path {
3235 ["SG4"] => 1,
3236 ["SG4", "SG5"] => 1, _ => 0,
3238 }
3239 }
3240 }
3241
3242 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
3245 let validator = EdifactValidator::new(evaluator);
3246 let external = NoOpExternalProvider;
3247 let nav = TestNav;
3248
3249 let segments = vec![OwnedSegment {
3250 id: "LOC".into(),
3251 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3252 segment_number: 10,
3253 }];
3254
3255 let workflow = AhbWorkflow {
3256 pruefidentifikator: "55001".to_string(),
3257 description: "Test".to_string(),
3258 communication_direction: None,
3259 fields: vec![
3260 AhbFieldRule {
3262 segment_path: "SG4/SG5/LOC/3227".to_string(),
3263 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3264 ahb_status: "X".to_string(),
3265 codes: vec![AhbCodeRule {
3266 value: "Z16".to_string(),
3267 description: "Marktlokation".to_string(),
3268 ahb_status: "X".to_string(),
3269 }],
3270 parent_group_ahb_status: Some("Muss [2061]".to_string()),
3271 segment_ahb_status: None,
3272 ..Default::default()
3273 },
3274 AhbFieldRule {
3276 segment_path: "SG4/SG5/LOC/3227".to_string(),
3277 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3278 ahb_status: "X".to_string(),
3279 codes: vec![AhbCodeRule {
3280 value: "Z17".to_string(),
3281 description: "Messlokation".to_string(),
3282 ahb_status: "X".to_string(),
3283 }],
3284 parent_group_ahb_status: Some("Soll [165]".to_string()),
3285 segment_ahb_status: None,
3286 ..Default::default()
3287 },
3288 ],
3289 ub_definitions: HashMap::new(),
3290 };
3291
3292 let report = validator.validate_with_navigator(
3293 &segments,
3294 &workflow,
3295 &external,
3296 ValidationLevel::Conditions,
3297 &nav,
3298 );
3299
3300 let ahb_errors: Vec<_> = report
3301 .by_category(ValidationCategory::Ahb)
3302 .filter(|i| i.severity == Severity::Error)
3303 .collect();
3304
3305 assert!(
3307 ahb_errors.is_empty(),
3308 "Expected no errors when conditional group variant [165]=False, got: {:?}",
3309 ahb_errors
3310 );
3311 }
3312
3313 #[test]
3314 fn test_conditional_group_variant_unknown_no_error() {
3315 let evaluator = MockEvaluator::new(vec![]);
3321 let validator = EdifactValidator::new(evaluator);
3322 let external = NoOpExternalProvider;
3323
3324 let workflow = AhbWorkflow {
3325 pruefidentifikator: "55001".to_string(),
3326 description: "Test".to_string(),
3327 communication_direction: None,
3328 fields: vec![AhbFieldRule {
3329 segment_path: "SG4/SG5/LOC/3227".to_string(),
3330 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3331 ahb_status: "X".to_string(),
3332 codes: vec![AhbCodeRule {
3333 value: "Z17".to_string(),
3334 description: "Messlokation".to_string(),
3335 ahb_status: "X".to_string(),
3336 }],
3337 parent_group_ahb_status: Some("Soll [165]".to_string()),
3338 segment_ahb_status: None,
3339 ..Default::default()
3340 }],
3341 ub_definitions: HashMap::new(),
3342 };
3343
3344 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3345
3346 let ahb_errors: Vec<_> = report
3347 .by_category(ValidationCategory::Ahb)
3348 .filter(|i| i.severity == Severity::Error)
3349 .collect();
3350
3351 assert!(
3353 ahb_errors.is_empty(),
3354 "Expected no errors when parent group condition is Unknown, got: {:?}",
3355 ahb_errors
3356 );
3357 }
3358
3359 #[test]
3360 fn test_segment_absent_within_present_group_no_error() {
3361 use mig_types::navigator::GroupNavigator;
3365
3366 struct TestNav;
3367 impl GroupNavigator for TestNav {
3368 fn find_segments_in_group(
3369 &self,
3370 segment_id: &str,
3371 group_path: &[&str],
3372 instance_index: usize,
3373 ) -> Vec<OwnedSegment> {
3374 if segment_id == "QTY"
3376 && group_path == ["SG5", "SG6", "SG9", "SG10"]
3377 && instance_index == 0
3378 {
3379 vec![OwnedSegment {
3380 id: "QTY".into(),
3381 elements: vec![vec!["220".into(), "0".into()]],
3382 segment_number: 14,
3383 }]
3384 } else {
3385 vec![]
3386 }
3387 }
3388 fn find_segments_with_qualifier_in_group(
3389 &self,
3390 _: &str,
3391 _: usize,
3392 _: &str,
3393 _: &[&str],
3394 _: usize,
3395 ) -> Vec<OwnedSegment> {
3396 vec![]
3397 }
3398 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3399 match group_path {
3400 ["SG5"] => 1,
3401 ["SG5", "SG6"] => 1,
3402 ["SG5", "SG6", "SG9"] => 1,
3403 ["SG5", "SG6", "SG9", "SG10"] => 1,
3404 _ => 0,
3405 }
3406 }
3407 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
3408 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
3410 }
3411 }
3412
3413 let evaluator = MockEvaluator::all_true(&[]);
3414 let validator = EdifactValidator::new(evaluator);
3415 let external = NoOpExternalProvider;
3416 let nav = TestNav;
3417
3418 let segments = vec![OwnedSegment {
3419 id: "QTY".into(),
3420 elements: vec![vec!["220".into(), "0".into()]],
3421 segment_number: 14,
3422 }];
3423
3424 let workflow = AhbWorkflow {
3425 pruefidentifikator: "13017".to_string(),
3426 description: "Test".to_string(),
3427 communication_direction: None,
3428 fields: vec![
3429 AhbFieldRule {
3431 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
3432 name: "Statuskategorie, Code".to_string(),
3433 ahb_status: "X".to_string(),
3434 codes: vec![],
3435 parent_group_ahb_status: Some("Muss".to_string()),
3436 segment_ahb_status: None,
3437 ..Default::default()
3438 },
3439 AhbFieldRule {
3441 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3442 name: "Statusanlaß, Code".to_string(),
3443 ahb_status: "X [5]".to_string(),
3444 codes: vec![],
3445 parent_group_ahb_status: Some("Muss".to_string()),
3446 segment_ahb_status: None,
3447 ..Default::default()
3448 },
3449 ],
3450 ub_definitions: HashMap::new(),
3451 };
3452
3453 let report = validator.validate_with_navigator(
3454 &segments,
3455 &workflow,
3456 &external,
3457 ValidationLevel::Conditions,
3458 &nav,
3459 );
3460
3461 let ahb_errors: Vec<_> = report
3462 .by_category(ValidationCategory::Ahb)
3463 .filter(|i| i.severity == Severity::Error)
3464 .collect();
3465
3466 assert!(
3467 ahb_errors.is_empty(),
3468 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
3469 ahb_errors
3470 );
3471 }
3472
3473 #[test]
3474 fn test_group_scoped_code_validation_with_navigator() {
3475 use mig_types::navigator::GroupNavigator;
3479
3480 struct TestNav;
3481 impl GroupNavigator for TestNav {
3482 fn find_segments_in_group(
3483 &self,
3484 segment_id: &str,
3485 group_path: &[&str],
3486 _instance_index: usize,
3487 ) -> Vec<OwnedSegment> {
3488 if segment_id != "NAD" {
3489 return vec![];
3490 }
3491 match group_path {
3492 ["SG2"] => vec![
3493 OwnedSegment {
3494 id: "NAD".into(),
3495 elements: vec![vec!["MS".into()]],
3496 segment_number: 3,
3497 },
3498 OwnedSegment {
3499 id: "NAD".into(),
3500 elements: vec![vec!["MT".into()]], segment_number: 4,
3502 },
3503 ],
3504 ["SG4", "SG12"] => vec![
3505 OwnedSegment {
3506 id: "NAD".into(),
3507 elements: vec![vec!["Z04".into()]],
3508 segment_number: 20,
3509 },
3510 OwnedSegment {
3511 id: "NAD".into(),
3512 elements: vec![vec!["Z09".into()]],
3513 segment_number: 21,
3514 },
3515 ],
3516 _ => vec![],
3517 }
3518 }
3519 fn find_segments_with_qualifier_in_group(
3520 &self,
3521 _: &str,
3522 _: usize,
3523 _: &str,
3524 _: &[&str],
3525 _: usize,
3526 ) -> Vec<OwnedSegment> {
3527 vec![]
3528 }
3529 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3530 match group_path {
3531 ["SG2"] | ["SG4", "SG12"] => 1,
3532 _ => 0,
3533 }
3534 }
3535 }
3536
3537 let evaluator = MockEvaluator::new(vec![]);
3538 let validator = EdifactValidator::new(evaluator);
3539 let external = NoOpExternalProvider;
3540 let nav = TestNav;
3541
3542 let workflow = AhbWorkflow {
3543 pruefidentifikator: "55001".to_string(),
3544 description: "Test".to_string(),
3545 communication_direction: None,
3546 fields: vec![
3547 AhbFieldRule {
3548 segment_path: "SG2/NAD/3035".to_string(),
3549 name: "Absender".to_string(),
3550 ahb_status: "X".to_string(),
3551 codes: vec![AhbCodeRule {
3552 value: "MS".to_string(),
3553 description: "Absender".to_string(),
3554 ahb_status: "X".to_string(),
3555 }],
3556 parent_group_ahb_status: None,
3557 segment_ahb_status: None,
3558 ..Default::default()
3559 },
3560 AhbFieldRule {
3561 segment_path: "SG2/NAD/3035".to_string(),
3562 name: "Empfaenger".to_string(),
3563 ahb_status: "X".to_string(),
3564 codes: vec![AhbCodeRule {
3565 value: "MR".to_string(),
3566 description: "Empfaenger".to_string(),
3567 ahb_status: "X".to_string(),
3568 }],
3569 parent_group_ahb_status: None,
3570 segment_ahb_status: None,
3571 ..Default::default()
3572 },
3573 AhbFieldRule {
3574 segment_path: "SG4/SG12/NAD/3035".to_string(),
3575 name: "Anschlussnutzer".to_string(),
3576 ahb_status: "X".to_string(),
3577 codes: vec![AhbCodeRule {
3578 value: "Z04".to_string(),
3579 description: "Anschlussnutzer".to_string(),
3580 ahb_status: "X".to_string(),
3581 }],
3582 parent_group_ahb_status: None,
3583 segment_ahb_status: None,
3584 ..Default::default()
3585 },
3586 AhbFieldRule {
3587 segment_path: "SG4/SG12/NAD/3035".to_string(),
3588 name: "Korrespondenzanschrift".to_string(),
3589 ahb_status: "X".to_string(),
3590 codes: vec![AhbCodeRule {
3591 value: "Z09".to_string(),
3592 description: "Korrespondenzanschrift".to_string(),
3593 ahb_status: "X".to_string(),
3594 }],
3595 parent_group_ahb_status: None,
3596 segment_ahb_status: None,
3597 ..Default::default()
3598 },
3599 ],
3600 ub_definitions: HashMap::new(),
3601 };
3602
3603 let all_segments = vec![
3605 OwnedSegment {
3606 id: "NAD".into(),
3607 elements: vec![vec!["MS".into()]],
3608 segment_number: 3,
3609 },
3610 OwnedSegment {
3611 id: "NAD".into(),
3612 elements: vec![vec!["MT".into()]],
3613 segment_number: 4,
3614 },
3615 OwnedSegment {
3616 id: "NAD".into(),
3617 elements: vec![vec!["Z04".into()]],
3618 segment_number: 20,
3619 },
3620 OwnedSegment {
3621 id: "NAD".into(),
3622 elements: vec![vec!["Z09".into()]],
3623 segment_number: 21,
3624 },
3625 ];
3626
3627 let report = validator.validate_with_navigator(
3628 &all_segments,
3629 &workflow,
3630 &external,
3631 ValidationLevel::Conditions,
3632 &nav,
3633 );
3634
3635 let code_errors: Vec<_> = report
3636 .by_category(ValidationCategory::Code)
3637 .filter(|i| i.severity == Severity::Error)
3638 .collect();
3639
3640 assert_eq!(
3643 code_errors.len(),
3644 1,
3645 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
3646 code_errors
3647 );
3648 assert!(code_errors[0].message.contains("MT"));
3649 assert!(code_errors[0].message.contains("MR"));
3651 assert!(code_errors[0].message.contains("MS"));
3652 assert!(
3653 !code_errors[0].message.contains("Z04"),
3654 "SG4/SG12 codes should not leak into SG2 error"
3655 );
3656 assert!(
3658 code_errors[0]
3659 .field_path
3660 .as_deref()
3661 .unwrap_or("")
3662 .contains("SG2"),
3663 "Error field_path should reference SG2, got: {:?}",
3664 code_errors[0].field_path
3665 );
3666 }
3667
3668 #[test]
3671 fn test_package_cardinality_within_bounds() {
3672 let evaluator = MockEvaluator::all_true(&[]);
3674 let validator = EdifactValidator::new(evaluator);
3675 let external = NoOpExternalProvider;
3676
3677 let segments = vec![OwnedSegment {
3678 id: "STS".into(),
3679 elements: vec![
3680 vec!["Z33".into()], vec![], vec!["E01".into()], ],
3684 segment_number: 5,
3685 }];
3686
3687 let workflow = AhbWorkflow {
3688 pruefidentifikator: "13017".to_string(),
3689 description: "Test".to_string(),
3690 communication_direction: None,
3691 ub_definitions: HashMap::new(),
3692 fields: vec![AhbFieldRule {
3693 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3694 name: "Statusanlaß, Code".to_string(),
3695 ahb_status: "X".to_string(),
3696 element_index: Some(2),
3697 component_index: Some(0),
3698 codes: vec![
3699 AhbCodeRule {
3700 value: "E01".into(),
3701 description: "Code 1".into(),
3702 ahb_status: "X [4P0..1]".into(),
3703 },
3704 AhbCodeRule {
3705 value: "E02".into(),
3706 description: "Code 2".into(),
3707 ahb_status: "X [4P0..1]".into(),
3708 },
3709 ],
3710 parent_group_ahb_status: Some("Muss".to_string()),
3711 segment_ahb_status: None,
3712 mig_number: None,
3713 }],
3714 };
3715
3716 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3717 let pkg_errors: Vec<_> = report
3718 .by_category(ValidationCategory::Ahb)
3719 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3720 .collect();
3721 assert!(
3722 pkg_errors.is_empty(),
3723 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
3724 pkg_errors
3725 );
3726 }
3727
3728 #[test]
3729 fn test_package_cardinality_zero_present_min_zero() {
3730 let evaluator = MockEvaluator::all_true(&[]);
3732 let validator = EdifactValidator::new(evaluator);
3733 let external = NoOpExternalProvider;
3734
3735 let segments = vec![OwnedSegment {
3736 id: "STS".into(),
3737 elements: vec![
3738 vec!["Z33".into()],
3739 vec![],
3740 vec!["X99".into()], ],
3742 segment_number: 5,
3743 }];
3744
3745 let workflow = AhbWorkflow {
3746 pruefidentifikator: "13017".to_string(),
3747 description: "Test".to_string(),
3748 communication_direction: None,
3749 ub_definitions: HashMap::new(),
3750 fields: vec![AhbFieldRule {
3751 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3752 name: "Statusanlaß, Code".to_string(),
3753 ahb_status: "X".to_string(),
3754 element_index: Some(2),
3755 component_index: Some(0),
3756 codes: vec![
3757 AhbCodeRule {
3758 value: "E01".into(),
3759 description: "Code 1".into(),
3760 ahb_status: "X [4P0..1]".into(),
3761 },
3762 AhbCodeRule {
3763 value: "E02".into(),
3764 description: "Code 2".into(),
3765 ahb_status: "X [4P0..1]".into(),
3766 },
3767 ],
3768 parent_group_ahb_status: Some("Muss".to_string()),
3769 segment_ahb_status: None,
3770 mig_number: None,
3771 }],
3772 };
3773
3774 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3775 let pkg_errors: Vec<_> = report
3776 .by_category(ValidationCategory::Ahb)
3777 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3778 .collect();
3779 assert!(
3780 pkg_errors.is_empty(),
3781 "0 codes, min=0 — no error expected, got: {:?}",
3782 pkg_errors
3783 );
3784 }
3785
3786 #[test]
3787 fn test_package_cardinality_too_many() {
3788 let evaluator = MockEvaluator::all_true(&[]);
3790 let validator = EdifactValidator::new(evaluator);
3791 let external = NoOpExternalProvider;
3792
3793 let segments = vec![
3795 OwnedSegment {
3796 id: "STS".into(),
3797 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
3798 segment_number: 5,
3799 },
3800 OwnedSegment {
3801 id: "STS".into(),
3802 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
3803 segment_number: 6,
3804 },
3805 ];
3806
3807 let workflow = AhbWorkflow {
3808 pruefidentifikator: "13017".to_string(),
3809 description: "Test".to_string(),
3810 communication_direction: None,
3811 ub_definitions: HashMap::new(),
3812 fields: vec![AhbFieldRule {
3813 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3814 name: "Statusanlaß, Code".to_string(),
3815 ahb_status: "X".to_string(),
3816 element_index: Some(2),
3817 component_index: Some(0),
3818 codes: vec![
3819 AhbCodeRule {
3820 value: "E01".into(),
3821 description: "Code 1".into(),
3822 ahb_status: "X [4P0..1]".into(),
3823 },
3824 AhbCodeRule {
3825 value: "E02".into(),
3826 description: "Code 2".into(),
3827 ahb_status: "X [4P0..1]".into(),
3828 },
3829 ],
3830 parent_group_ahb_status: Some("Muss".to_string()),
3831 segment_ahb_status: None,
3832 mig_number: None,
3833 }],
3834 };
3835
3836 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3837 let pkg_errors: Vec<_> = report
3838 .by_category(ValidationCategory::Ahb)
3839 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3840 .collect();
3841 assert_eq!(
3842 pkg_errors.len(),
3843 1,
3844 "2 codes present, max=1 — expected 1 error, got: {:?}",
3845 pkg_errors
3846 );
3847 assert!(pkg_errors[0].message.contains("[4P0..1]"));
3848 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
3849 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
3850 }
3851
3852 #[test]
3853 fn test_package_cardinality_too_few() {
3854 let evaluator = MockEvaluator::all_true(&[]);
3856 let validator = EdifactValidator::new(evaluator);
3857 let external = NoOpExternalProvider;
3858
3859 let segments = vec![OwnedSegment {
3860 id: "STS".into(),
3861 elements: vec![
3862 vec!["Z33".into()],
3863 vec![],
3864 vec!["X99".into()], ],
3866 segment_number: 5,
3867 }];
3868
3869 let workflow = AhbWorkflow {
3870 pruefidentifikator: "13017".to_string(),
3871 description: "Test".to_string(),
3872 communication_direction: None,
3873 ub_definitions: HashMap::new(),
3874 fields: vec![AhbFieldRule {
3875 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3876 name: "Statusanlaß, Code".to_string(),
3877 ahb_status: "X".to_string(),
3878 element_index: Some(2),
3879 component_index: Some(0),
3880 codes: vec![
3881 AhbCodeRule {
3882 value: "E01".into(),
3883 description: "Code 1".into(),
3884 ahb_status: "X [5P1..3]".into(),
3885 },
3886 AhbCodeRule {
3887 value: "E02".into(),
3888 description: "Code 2".into(),
3889 ahb_status: "X [5P1..3]".into(),
3890 },
3891 AhbCodeRule {
3892 value: "E03".into(),
3893 description: "Code 3".into(),
3894 ahb_status: "X [5P1..3]".into(),
3895 },
3896 ],
3897 parent_group_ahb_status: Some("Muss".to_string()),
3898 segment_ahb_status: None,
3899 mig_number: None,
3900 }],
3901 };
3902
3903 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3904 let pkg_errors: Vec<_> = report
3905 .by_category(ValidationCategory::Ahb)
3906 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3907 .collect();
3908 assert_eq!(
3909 pkg_errors.len(),
3910 1,
3911 "0 codes present, min=1 — expected 1 error, got: {:?}",
3912 pkg_errors
3913 );
3914 assert!(pkg_errors[0].message.contains("[5P1..3]"));
3915 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
3916 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
3917 }
3918
3919 #[test]
3920 fn test_package_cardinality_no_packages_in_workflow() {
3921 let evaluator = MockEvaluator::all_true(&[]);
3923 let validator = EdifactValidator::new(evaluator);
3924 let external = NoOpExternalProvider;
3925
3926 let segments = vec![OwnedSegment {
3927 id: "STS".into(),
3928 elements: vec![vec!["E01".into()]],
3929 segment_number: 5,
3930 }];
3931
3932 let workflow = AhbWorkflow {
3933 pruefidentifikator: "13017".to_string(),
3934 description: "Test".to_string(),
3935 communication_direction: None,
3936 ub_definitions: HashMap::new(),
3937 fields: vec![AhbFieldRule {
3938 segment_path: "STS/9015".to_string(),
3939 name: "Status Code".to_string(),
3940 ahb_status: "X".to_string(),
3941 codes: vec![AhbCodeRule {
3942 value: "E01".into(),
3943 description: "Code 1".into(),
3944 ahb_status: "X".into(),
3945 }],
3946 parent_group_ahb_status: Some("Muss".to_string()),
3947 segment_ahb_status: None,
3948 ..Default::default()
3949 }],
3950 };
3951
3952 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3953 let pkg_errors: Vec<_> = report
3954 .by_category(ValidationCategory::Ahb)
3955 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3956 .collect();
3957 assert!(
3958 pkg_errors.is_empty(),
3959 "No packages in workflow — no errors expected"
3960 );
3961 }
3962
3963 #[test]
3964 fn test_package_cardinality_with_condition_and_package() {
3965 let evaluator = MockEvaluator::all_true(&[901]);
3967 let validator = EdifactValidator::new(evaluator);
3968 let external = NoOpExternalProvider;
3969
3970 let segments = vec![OwnedSegment {
3971 id: "STS".into(),
3972 elements: vec![vec![], vec![], vec!["E01".into()]],
3973 segment_number: 5,
3974 }];
3975
3976 let workflow = AhbWorkflow {
3977 pruefidentifikator: "13017".to_string(),
3978 description: "Test".to_string(),
3979 communication_direction: None,
3980 ub_definitions: HashMap::new(),
3981 fields: vec![AhbFieldRule {
3982 segment_path: "SG10/STS/C556/9013".to_string(),
3983 name: "Code".to_string(),
3984 ahb_status: "X".to_string(),
3985 element_index: Some(2),
3986 component_index: Some(0),
3987 codes: vec![
3988 AhbCodeRule {
3989 value: "E01".into(),
3990 description: "Code 1".into(),
3991 ahb_status: "X [901] [4P0..1]".into(),
3992 },
3993 AhbCodeRule {
3994 value: "E02".into(),
3995 description: "Code 2".into(),
3996 ahb_status: "X [901] [4P0..1]".into(),
3997 },
3998 ],
3999 parent_group_ahb_status: Some("Muss".to_string()),
4000 segment_ahb_status: None,
4001 mig_number: None,
4002 }],
4003 };
4004
4005 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4006 let pkg_errors: Vec<_> = report
4007 .by_category(ValidationCategory::Ahb)
4008 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4009 .collect();
4010 assert!(
4011 pkg_errors.is_empty(),
4012 "1 code within [4P0..1] bounds — no error, got: {:?}",
4013 pkg_errors
4014 );
4015 }
4016
4017 #[test]
4018 fn test_package_cardinality_scoped_per_group_instance() {
4019 use mig_types::navigator::GroupNavigator;
4027
4028 struct TwoSg10s {
4029 sts_a: OwnedSegment,
4030 sts_b: OwnedSegment,
4031 }
4032 impl GroupNavigator for TwoSg10s {
4033 fn find_segments_in_group(
4034 &self,
4035 segment_id: &str,
4036 group_path: &[&str],
4037 instance_index: usize,
4038 ) -> Vec<OwnedSegment> {
4039 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
4040 match instance_index {
4041 0 => vec![self.sts_a.clone()],
4042 1 => vec![self.sts_b.clone()],
4043 _ => vec![],
4044 }
4045 } else {
4046 vec![]
4047 }
4048 }
4049 fn find_segments_with_qualifier_in_group(
4050 &self,
4051 _: &str,
4052 _: usize,
4053 _: &str,
4054 _: &[&str],
4055 _: usize,
4056 ) -> Vec<OwnedSegment> {
4057 vec![]
4058 }
4059 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4060 match group_path {
4061 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
4062 ["SG5", "SG6", "SG9", "SG10"] => 2,
4063 _ => 0,
4064 }
4065 }
4066 }
4067
4068 let sts_a = OwnedSegment {
4069 id: "STS".into(),
4070 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4071 segment_number: 10,
4072 };
4073 let sts_b = OwnedSegment {
4074 id: "STS".into(),
4075 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4076 segment_number: 15,
4077 };
4078 let nav = TwoSg10s {
4079 sts_a: sts_a.clone(),
4080 sts_b: sts_b.clone(),
4081 };
4082
4083 let evaluator = MockEvaluator::all_true(&[]);
4084 let validator = EdifactValidator::new(evaluator);
4085 let external = NoOpExternalProvider;
4086
4087 let workflow = AhbWorkflow {
4088 pruefidentifikator: "13025".to_string(),
4089 description: "Test".to_string(),
4090 communication_direction: None,
4091 ub_definitions: HashMap::new(),
4092 fields: vec![AhbFieldRule {
4093 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4094 name: "Statusanlaß, Code".to_string(),
4095 ahb_status: "X".to_string(),
4096 element_index: Some(2),
4097 component_index: Some(0),
4098 codes: vec![
4099 AhbCodeRule {
4100 value: "E01".into(),
4101 description: "Code 1".into(),
4102 ahb_status: "X [4P0..1]".into(),
4103 },
4104 AhbCodeRule {
4105 value: "E02".into(),
4106 description: "Code 2".into(),
4107 ahb_status: "X [4P0..1]".into(),
4108 },
4109 ],
4110 parent_group_ahb_status: Some("Muss".to_string()),
4111 segment_ahb_status: None,
4112 mig_number: None,
4113 }],
4114 };
4115
4116 let report = validator.validate_with_navigator(
4117 &[sts_a, sts_b],
4118 &workflow,
4119 &external,
4120 ValidationLevel::Full,
4121 &nav,
4122 );
4123 let pkg_errors: Vec<_> = report
4124 .by_category(ValidationCategory::Ahb)
4125 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4126 .collect();
4127 assert!(
4128 pkg_errors.is_empty(),
4129 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
4130 pkg_errors
4131 );
4132 }
4133
4134 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
4135 OwnedSegment {
4136 id: id.to_string(),
4137 elements: elements
4138 .into_iter()
4139 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
4140 .collect(),
4141 segment_number: 0,
4142 }
4143 }
4144
4145 #[test]
4156 fn test_package_cardinality_scoped_to_rule_mig_variant() {
4157 use mig_types::navigator::GroupNavigator;
4158
4159 struct TwoSg8Variants {
4160 seq_z01: OwnedSegment,
4161 seq_z45: OwnedSegment,
4162 }
4163 impl GroupNavigator for TwoSg8Variants {
4164 fn find_segments_in_group(
4165 &self,
4166 segment_id: &str,
4167 group_path: &[&str],
4168 instance_index: usize,
4169 ) -> Vec<OwnedSegment> {
4170 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
4171 match instance_index {
4172 0 => vec![self.seq_z01.clone()],
4173 1 => vec![self.seq_z45.clone()],
4174 _ => vec![],
4175 }
4176 } else {
4177 vec![]
4178 }
4179 }
4180 fn find_segments_with_qualifier_in_group(
4181 &self,
4182 _: &str,
4183 _: usize,
4184 _: &str,
4185 _: &[&str],
4186 _: usize,
4187 ) -> Vec<OwnedSegment> {
4188 vec![]
4189 }
4190 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4191 match group_path {
4192 ["SG4"] => 1,
4193 ["SG4", "SG8"] => 2,
4194 _ => 0,
4195 }
4196 }
4197 fn instance_has_mig_number(
4198 &self,
4199 group_path: &[&str],
4200 instance_index: usize,
4201 mig_number: &str,
4202 ) -> bool {
4203 if group_path != ["SG4", "SG8"] {
4204 return true;
4205 }
4206 match (instance_index, mig_number) {
4207 (0, "00115") => true,
4208 (0, _) => false,
4209 (1, "00171") => true,
4210 (1, _) => false,
4211 _ => false,
4212 }
4213 }
4214 }
4215
4216 let seq_z01 = OwnedSegment {
4217 id: "SEQ".into(),
4218 elements: vec![vec!["Z01".into()], vec!["1".into()]],
4219 segment_number: 10,
4220 };
4221 let seq_z45 = OwnedSegment {
4222 id: "SEQ".into(),
4223 elements: vec![vec!["Z45".into()], vec!["1".into()]],
4224 segment_number: 20,
4225 };
4226 let nav = TwoSg8Variants {
4227 seq_z01: seq_z01.clone(),
4228 seq_z45: seq_z45.clone(),
4229 };
4230
4231 let evaluator = MockEvaluator::all_true(&[]);
4232 let validator = EdifactValidator::new(evaluator);
4233 let external = NoOpExternalProvider;
4234
4235 let workflow = AhbWorkflow {
4236 pruefidentifikator: "55218".to_string(),
4237 description: "Test".to_string(),
4238 communication_direction: None,
4239 ub_definitions: HashMap::new(),
4240 fields: vec![AhbFieldRule {
4241 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4242 name: "Handlung, Code".to_string(),
4243 ahb_status: "X".to_string(),
4244 element_index: Some(0),
4245 component_index: Some(0),
4246 codes: vec![
4247 AhbCodeRule {
4248 value: "Z45".into(),
4249 description: "NNA".into(),
4250 ahb_status: "X [1P1..4294967295]".into(),
4251 },
4252 AhbCodeRule {
4253 value: "Z84".into(),
4254 description: "Differenz-NNA".into(),
4255 ahb_status: "X [1P0..4294967295]".into(),
4256 },
4257 ],
4258 parent_group_ahb_status: Some("Muss".to_string()),
4259 segment_ahb_status: None,
4260 mig_number: Some("00171".to_string()),
4261 }],
4262 };
4263
4264 let report = validator.validate_with_navigator(
4265 &[seq_z01, seq_z45],
4266 &workflow,
4267 &external,
4268 ValidationLevel::Full,
4269 &nav,
4270 );
4271 let pkg_errors: Vec<_> = report
4272 .by_category(ValidationCategory::Ahb)
4273 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4274 .collect();
4275 assert!(
4276 pkg_errors.is_empty(),
4277 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
4278 pkg_errors
4279 );
4280 }
4281
4282 #[test]
4283 fn test_unt_count_correct() {
4284 let segments = vec![
4286 make_segment("UNH", vec![vec!["001"]]),
4287 make_segment("BGM", vec![vec!["E01"]]),
4288 make_segment("DTM", vec![vec!["137", "20250401"]]),
4289 make_segment("UNT", vec![vec!["4", "001"]]),
4290 ];
4291 assert!(
4292 validate_unt_segment_count(&segments).is_none(),
4293 "Correct count should produce no issue"
4294 );
4295 }
4296
4297 #[test]
4298 fn test_unt_count_mismatch() {
4299 let segments = vec![
4301 make_segment("UNH", vec![vec!["001"]]),
4302 make_segment("BGM", vec![vec!["E01"]]),
4303 make_segment("UNT", vec![vec!["5", "001"]]),
4304 ];
4305 let issue =
4306 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
4307 assert_eq!(issue.code, ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
4308 assert_eq!(issue.severity, Severity::Error);
4309 assert!(issue.message.contains("declared 5"));
4310 assert!(issue.message.contains("actual 3"));
4311 }
4312
4313 #[test]
4314 fn test_unt_count_excludes_envelope() {
4315 let segments = vec![
4317 make_segment("UNA", vec![]),
4318 make_segment("UNB", vec![vec!["UNOC", "3"]]),
4319 make_segment("UNH", vec![vec!["001"]]),
4320 make_segment("BGM", vec![vec!["E01"]]),
4321 make_segment("UNT", vec![vec!["3", "001"]]),
4322 make_segment("UNZ", vec![vec!["1"]]),
4323 ];
4324 assert!(
4325 validate_unt_segment_count(&segments).is_none(),
4326 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
4327 );
4328 }
4329
4330 #[test]
4331 fn test_unt_count_no_unt_returns_none() {
4332 let segments = vec![
4333 make_segment("UNH", vec![vec!["001"]]),
4334 make_segment("BGM", vec![vec!["E01"]]),
4335 ];
4336 assert!(
4337 validate_unt_segment_count(&segments).is_none(),
4338 "No UNT segment should return None (not our problem)"
4339 );
4340 }
4341
4342 #[test]
4343 fn test_unt_count_rejects_multi_message_input() {
4344 let segments = vec![
4346 make_segment("UNH", vec![vec!["001"]]),
4347 make_segment("BGM", vec![vec!["E01"]]),
4348 make_segment("UNT", vec![vec!["3", "001"]]),
4349 make_segment("UNH", vec![vec!["002"]]),
4350 make_segment("BGM", vec![vec!["E02"]]),
4351 make_segment("UNT", vec![vec!["3", "002"]]),
4352 ];
4353 let issue = validate_unt_segment_count(&segments)
4354 .expect("Multi-message input should produce an error");
4355 assert_eq!(issue.code, ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
4356 assert!(
4357 issue.message.contains("2 UNH"),
4358 "Should mention UNH count: {}",
4359 issue.message
4360 );
4361 }
4362
4363 #[test]
4364 fn test_code_validation_accepts_multi_code_variant_qualifier() {
4365 let evaluator = MockEvaluator::new(vec![]);
4372 let validator = EdifactValidator::new(evaluator);
4373 let external = NoOpExternalProvider;
4374
4375 let rff_z39 = OwnedSegment {
4376 id: "RFF".to_string(),
4377 elements: vec![
4378 vec!["RFF".to_string()],
4379 vec!["Z39".to_string(), "REF1".to_string()],
4380 ],
4381 segment_number: 1,
4382 };
4383
4384 let workflow = AhbWorkflow {
4385 pruefidentifikator: "55035".to_string(),
4386 description: "Test".to_string(),
4387 communication_direction: None,
4388 fields: vec![
4389 AhbFieldRule {
4391 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
4392 name: "Referenznummer Qualifier".to_string(),
4393 ahb_status: "Muss".to_string(),
4394 codes: vec![
4395 AhbCodeRule {
4396 value: "Z31".to_string(),
4397 description: "".to_string(),
4398 ahb_status: "X".to_string(),
4399 },
4400 AhbCodeRule {
4401 value: "Z39".to_string(),
4402 description: "".to_string(),
4403 ahb_status: "X".to_string(),
4404 },
4405 ],
4406 parent_group_ahb_status: None,
4407 segment_ahb_status: None,
4408 element_index: Some(1),
4409 component_index: Some(0),
4410 mig_number: Some("00075".to_string()),
4411 },
4412 AhbFieldRule {
4414 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
4415 name: "Referenznummer Qualifier".to_string(),
4416 ahb_status: "Muss".to_string(),
4417 codes: vec![AhbCodeRule {
4418 value: "Z33".to_string(),
4419 description: "".to_string(),
4420 ahb_status: "X".to_string(),
4421 }],
4422 parent_group_ahb_status: None,
4423 segment_ahb_status: None,
4424 element_index: Some(1),
4425 component_index: Some(0),
4426 mig_number: Some("00078".to_string()),
4427 },
4428 ],
4429 ub_definitions: HashMap::new(),
4430 };
4431
4432 let report = validator.validate(
4433 &[rff_z39],
4434 &workflow,
4435 &external,
4436 ValidationLevel::Conditions,
4437 );
4438
4439 let code_errors: Vec<_> = report
4440 .by_category(ValidationCategory::Code)
4441 .filter(|i| {
4442 i.severity == Severity::Error && i.code == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4443 })
4444 .collect();
4445 assert!(
4446 code_errors.is_empty(),
4447 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
4448 code_errors
4449 );
4450 }
4451
4452 #[test]
4453 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
4454 let evaluator = MockEvaluator::new(vec![]);
4460 let validator = EdifactValidator::new(evaluator);
4461 let external = NoOpExternalProvider;
4462
4463 let pia_5_z12 = OwnedSegment {
4467 id: "PIA".to_string(),
4468 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
4469 segment_number: 1,
4470 };
4471 let pia_5_srw = OwnedSegment {
4472 id: "PIA".to_string(),
4473 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
4474 segment_number: 2,
4475 };
4476
4477 let make_rules = |mig: &str, composite_code: &str| {
4478 vec![
4479 AhbFieldRule {
4480 segment_path: "SG4/SG8/PIA/4347".to_string(),
4481 name: "Produkt-ID-Funktion".to_string(),
4482 ahb_status: "Muss".to_string(),
4483 codes: vec![AhbCodeRule {
4484 value: "5".to_string(),
4485 description: "".to_string(),
4486 ahb_status: "X".to_string(),
4487 }],
4488 parent_group_ahb_status: None,
4489 segment_ahb_status: None,
4490 element_index: Some(0),
4491 component_index: Some(0),
4492 mig_number: Some(mig.to_string()),
4493 },
4494 AhbFieldRule {
4495 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
4496 name: "Artikel/Dienstleistung-Identifikator".to_string(),
4497 ahb_status: "Muss".to_string(),
4498 codes: vec![AhbCodeRule {
4499 value: composite_code.to_string(),
4500 description: "".to_string(),
4501 ahb_status: "X".to_string(),
4502 }],
4503 parent_group_ahb_status: None,
4504 segment_ahb_status: None,
4505 element_index: Some(1),
4506 component_index: Some(0),
4507 mig_number: Some(mig.to_string()),
4508 },
4509 ]
4510 };
4511
4512 let mut fields = make_rules("00108", "Z12");
4513 fields.extend(make_rules("00197", "SRW"));
4514
4515 let workflow = AhbWorkflow {
4516 pruefidentifikator: "55035".to_string(),
4517 description: "Test".to_string(),
4518 communication_direction: None,
4519 fields,
4520 ub_definitions: HashMap::new(),
4521 };
4522
4523 let report = validator.validate(
4524 &[pia_5_z12, pia_5_srw],
4525 &workflow,
4526 &external,
4527 ValidationLevel::Conditions,
4528 );
4529
4530 let code_errors: Vec<_> = report
4531 .by_category(ValidationCategory::Code)
4532 .filter(|i| {
4533 i.severity == Severity::Error && i.code == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4534 })
4535 .collect();
4536 assert!(
4537 code_errors.is_empty(),
4538 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
4539 code_errors
4540 );
4541 }
4542}