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