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 governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
850 let derived_from_codes = field
851 .codes
852 .first()
853 .is_some_and(|c| c.ahb_status == field.ahb_status);
854 if !derived_from_codes {
855 return &field.ahb_status;
856 }
857 value
858 .and_then(|v| field.codes.iter().find(|c| c.value == v))
859 .map_or(&field.ahb_status, |c| &c.ahb_status)
860}
861
862fn should_skip_for_parent_group<E: ConditionEvaluator>(
868 field: &AhbFieldRule,
869 expr_eval: &ConditionExprEvaluator<E>,
870 ctx: &EvaluationContext,
871 ub_definitions: &BTreeMap<String, ConditionExpr>,
872) -> bool {
873 if let Some(ref group_status) = field.parent_group_ahb_status {
874 if group_status.contains('[') {
875 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
876 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
877 }
878 }
879 false
880}
881
882fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
891 let segment_id = extract_segment_id(&field.segment_path);
892
893 if !field.codes.is_empty() {
899 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
900 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
901 let matching = ctx.find_segments(&segment_id);
902 return matching.iter().any(|seg| {
903 seg.elements
904 .get(el_idx)
905 .and_then(|e| e.get(comp_idx))
906 .is_some_and(|v| required_codes.contains(&v.as_str()))
907 });
908 }
909 if is_qualifier_field(&field.segment_path) {
912 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
913 let el_idx = field.element_index.unwrap_or(0);
914 let comp_idx = field.component_index.unwrap_or(0);
915 let matching = ctx.find_segments(&segment_id);
916 return matching.iter().any(|seg| {
917 seg.elements
918 .get(el_idx)
919 .and_then(|e| e.get(comp_idx))
920 .is_some_and(|v| required_codes.contains(&v.as_str()))
921 });
922 }
923 }
924
925 ctx.has_segment(&segment_id)
926}
927
928fn is_group_variant_absent(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
943 let group_path: Vec<&str> = field
944 .segment_path
945 .split('/')
946 .take_while(|p| p.starts_with("SG"))
947 .collect();
948
949 if group_path.is_empty() {
950 return false;
951 }
952
953 let nav = match ctx.navigator {
954 Some(nav) => nav,
955 None => return false,
956 };
957
958 let instance_count = nav.group_instance_count(&group_path);
959
960 if instance_count == 0 {
965 let is_group_mandatory = field
966 .parent_group_ahb_status
967 .as_deref()
968 .is_some_and(is_mandatory_status);
969 if !is_group_mandatory {
970 return true;
971 }
972 return false;
974 }
975
976 if let Some(ref group_status) = field.parent_group_ahb_status {
980 if !is_mandatory_status(group_status) && !group_status.contains('[') {
981 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
984 let segment_id = extract_segment_id(&field.segment_path);
985 let required_codes: Vec<&str> =
986 field.codes.iter().map(|c| c.value.as_str()).collect();
987
988 let any_instance_has_qualifier = (0..instance_count).any(|i| {
989 nav.find_segments_in_group(&segment_id, &group_path, i)
990 .iter()
991 .any(|seg| {
992 seg.elements
993 .first()
994 .and_then(|e| e.first())
995 .is_some_and(|v| required_codes.contains(&v.as_str()))
996 })
997 });
998
999 if !any_instance_has_qualifier {
1000 return true; }
1002 }
1003 }
1004 }
1005
1006 let segment_id = extract_segment_id(&field.segment_path);
1015 let segment_absent_from_all = (0..instance_count).all(|i| {
1016 nav.find_segments_in_group(&segment_id, &group_path, i)
1017 .is_empty()
1018 });
1019 if segment_absent_from_all {
1020 let group_has_other_segments =
1021 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1022 if group_has_other_segments {
1023 return true;
1024 }
1025 }
1026
1027 false
1028}
1029
1030fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1032 out.extend(group.fields.iter());
1033 for child in &group.children {
1034 collect_nodes_depth_first(child, out);
1035 }
1036}
1037
1038#[allow(clippy::too_many_arguments)]
1042fn evaluate_node<E: ConditionEvaluator>(
1043 node: &AhbNode,
1044 ctx: &EvaluationContext,
1045 expr_eval: &ConditionExprEvaluator<E>,
1046 evaluator: &E,
1047 ub_definitions: &BTreeMap<String, ConditionExpr>,
1048 tag_migs: &HashMap<String, HashSet<&str>>,
1049 instance_index: Option<usize>,
1050 report: &mut ValidationReport,
1051) {
1052 let field = node.rule;
1053
1054 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1056
1057 if should_skip_for_parent_group(field, expr_eval, ctx, ub_definitions) {
1059 return;
1060 }
1061
1062 let status = governing_status(field, node.value);
1065 let (condition_result, unknown_ids) =
1066 expr_eval.evaluate_status_detailed_with_ub(status, &node_ctx, ub_definitions);
1067
1068 match condition_result {
1069 ConditionResult::True => {
1070 if is_mandatory_status(&field.ahb_status) && node.value.is_none() {
1079 let tag = extract_segment_id(&field.segment_path);
1080 let single_variant_present = node.segment_elements.is_none()
1081 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1082 && is_field_present(ctx, field);
1083 let segment_optional_and_absent = node.segment_elements.is_none()
1088 && field
1089 .segment_ahb_status
1090 .as_deref()
1091 .is_some_and(is_optional_segment_status);
1092 if !single_variant_present && !segment_optional_and_absent {
1093 let mut issue = ValidationIssue::new(
1094 Severity::Error,
1095 IssueKind::MissingRequiredField {
1096 field_name: field.name.clone(),
1097 },
1098 )
1099 .with_field_path(&field.segment_path)
1100 .with_rule(&field.ahb_status);
1101 if let Some(first_code) = field.codes.first() {
1102 issue.expected_value = Some(first_code.value.clone());
1103 }
1104 if let Some(idx) = instance_index {
1105 issue = issue.with_instance_index(idx);
1106 }
1107 if let Some(num) = node.matched_segment_number {
1108 issue = issue.with_position(crate::SegmentPosition {
1109 segment_number: num,
1110 byte_offset: 0,
1111 message_number: 1,
1112 });
1113 }
1114 issue = attach_field_position(issue, field);
1115 report.add_issue(issue);
1116 }
1117 }
1118 }
1119 ConditionResult::False => {
1120 if is_mandatory_status(status) && node.value.is_some() {
1123 let mut issue = ValidationIssue::new(
1124 Severity::Error,
1125 IssueKind::FieldConditionNotSatisfied {
1126 field_name: field.name.clone(),
1127 },
1128 )
1129 .with_field_path(&field.segment_path)
1130 .with_rule(&field.ahb_status);
1131 if let Some(idx) = instance_index {
1132 issue = issue.with_instance_index(idx);
1133 }
1134 if let Some(num) = node.matched_segment_number {
1135 issue = issue.with_position(crate::SegmentPosition {
1136 segment_number: num,
1137 byte_offset: 0,
1138 message_number: 1,
1139 });
1140 }
1141 issue = attach_field_position(issue, field);
1142 report.add_issue(issue);
1143 }
1144 }
1145 ConditionResult::Unknown => {
1146 let mut external_ids = Vec::new();
1148 let mut undetermined_ids = Vec::new();
1149 let mut missing_ids = Vec::new();
1150 for id in unknown_ids {
1151 if evaluator.is_external(id) {
1152 external_ids.push(id);
1153 } else if evaluator.is_known(id) {
1154 undetermined_ids.push(id);
1155 } else {
1156 missing_ids.push(id);
1157 }
1158 }
1159
1160 let mut issue = ValidationIssue::new(
1161 Severity::Info,
1162 IssueKind::ConditionUnknown {
1163 field_name: field.name.clone(),
1164 unresolved: UnresolvedConditions {
1165 external: external_ids,
1166 undetermined: undetermined_ids,
1167 missing: missing_ids,
1168 },
1169 },
1170 )
1171 .with_field_path(&field.segment_path)
1172 .with_rule(&field.ahb_status);
1173 if let Some(idx) = instance_index {
1174 issue = issue.with_instance_index(idx);
1175 }
1176 if let Some(num) = node.matched_segment_number {
1177 issue = issue.with_position(crate::SegmentPosition {
1178 segment_number: num,
1179 byte_offset: 0,
1180 message_number: 1,
1181 });
1182 }
1183 issue = attach_field_position(issue, field);
1184 report.add_issue(issue);
1185 }
1186 }
1187}
1188
1189fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1194 match field.element_index {
1195 Some(el) => {
1196 let element_pos = (el as u32) + 2;
1197 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1198 issue.with_field_position(element_pos, component_pos)
1199 }
1200 None => issue,
1201 }
1202}
1203
1204#[allow(clippy::too_many_arguments)]
1215fn walk_group_instance<E: ConditionEvaluator>(
1216 group: &AhbGroupNode,
1217 scoped_ctx: &EvaluationContext,
1218 expr_eval: &ConditionExprEvaluator<E>,
1219 evaluator: &E,
1220 ub_definitions: &BTreeMap<String, ConditionExpr>,
1221 tag_migs: &HashMap<String, HashSet<&str>>,
1222 instance_index: usize,
1223 report: &mut ValidationReport,
1224) {
1225 for node in &group.fields {
1226 evaluate_node(
1227 node,
1228 scoped_ctx,
1229 expr_eval,
1230 evaluator,
1231 ub_definitions,
1232 tag_migs,
1233 Some(instance_index),
1234 report,
1235 );
1236 }
1237 for child in &group.children {
1238 walk_group_instance(
1239 child,
1240 scoped_ctx,
1241 expr_eval,
1242 evaluator,
1243 ub_definitions,
1244 tag_migs,
1245 instance_index,
1246 report,
1247 );
1248 }
1249}
1250
1251fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
1253 match expr {
1254 ConditionExpr::Package { id, min, max } => {
1255 out.push((*id, *min, *max));
1256 }
1257 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1258 for e in exprs {
1259 collect_packages(e, out);
1260 }
1261 }
1262 ConditionExpr::Xor(left, right) => {
1263 collect_packages(left, out);
1264 collect_packages(right, out);
1265 }
1266 ConditionExpr::Not(inner) => {
1267 collect_packages(inner, out);
1268 }
1269 ConditionExpr::Ref(_) => {}
1270 }
1271}
1272
1273fn is_mandatory_status(status: &str) -> bool {
1275 let trimmed = status.trim();
1276 trimmed.starts_with("Muss") || trimmed.starts_with('X')
1277}
1278
1279fn is_optional_segment_status(status: &str) -> bool {
1289 let trimmed = status.trim();
1290 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
1291}
1292
1293fn is_qualifier_field(path: &str) -> bool {
1303 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
1304 matches!(parts.len(), 2 | 3)
1306}
1307
1308#[derive(Default)]
1316struct MigCodeBucket<'a> {
1317 qualifier_position: Option<(usize, usize)>,
1320 qualifier_values: HashSet<&'a str>,
1323 codes: HashMap<(usize, usize), HashSet<&'a str>>,
1326}
1327
1328fn partition_codes_by_mig(
1333 workflow: &AhbWorkflow,
1334) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
1335 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
1336 for field in &workflow.fields {
1337 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
1338 continue;
1339 }
1340 let tag = extract_segment_id(&field.segment_path);
1341 let group_key = extract_group_path_key(&field.segment_path);
1342 let mig = field.mig_number.clone();
1343 let el = field.element_index.unwrap_or(0);
1344 let c = field.component_index.unwrap_or(0);
1345
1346 let bucket = out
1347 .entry((group_key, tag))
1348 .or_default()
1349 .entry(mig)
1350 .or_default();
1351
1352 let required: Vec<&str> = field
1359 .codes
1360 .iter()
1361 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
1362 .map(|code| code.value.as_str())
1363 .collect();
1364
1365 if !required.is_empty() {
1372 if bucket.qualifier_position.is_none() {
1373 bucket.qualifier_position = Some((el, c));
1374 }
1375 if bucket.qualifier_position == Some((el, c)) {
1376 bucket.qualifier_values.extend(required.iter().copied());
1377 }
1378 }
1379
1380 for v in required {
1381 bucket.codes.entry((el, c)).or_default().insert(v);
1382 }
1383 }
1384 out
1385}
1386
1387fn global_qualifiers_by_tag(
1393 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
1394) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
1395 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
1396 for ((_group, tag), migs) in by_loc {
1397 let tag_entry = out.entry(tag.clone()).or_default();
1398 for bucket in migs.values() {
1399 if let Some(pos) = bucket.qualifier_position {
1400 tag_entry
1401 .entry(pos)
1402 .or_default()
1403 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
1404 }
1405 }
1406 }
1407 out
1408}
1409
1410fn match_segment_to_mig<'a, 'b>(
1421 seg: &OwnedSegment,
1422 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
1423) -> Option<&'a MigCodeBucket<'b>> {
1424 let actual_at = |el: usize, c: usize| -> &str {
1425 seg.elements
1426 .get(el)
1427 .and_then(|e| e.get(c))
1428 .map(|s| s.as_str())
1429 .unwrap_or("")
1430 };
1431
1432 let mut best: Option<&MigCodeBucket> = None;
1433 let mut best_matches = 0usize;
1434
1435 for bucket in migs.values() {
1436 let Some((el, c)) = bucket.qualifier_position else {
1437 continue;
1438 };
1439 if !bucket.qualifier_values.contains(actual_at(el, c)) {
1440 continue;
1441 }
1442 let extra_matches = bucket
1443 .codes
1444 .iter()
1445 .filter(|(pos, _)| **pos != (el, c))
1446 .filter(|((e, k), allowed)| {
1447 let v = actual_at(*e, *k);
1448 !v.is_empty() && allowed.contains(v)
1449 })
1450 .count();
1451 if best.is_none() || extra_matches > best_matches {
1452 best = Some(bucket);
1453 best_matches = extra_matches;
1454 }
1455 }
1456 best
1457}
1458
1459fn extract_group_path_key(path: &str) -> String {
1464 let sg_parts: Vec<&str> = path
1465 .split('/')
1466 .take_while(|p| p.starts_with("SG"))
1467 .collect();
1468 sg_parts.join("/")
1469}
1470
1471fn extract_segment_id(path: &str) -> String {
1473 for part in path.split('/') {
1474 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
1476 continue;
1477 }
1478 if part.len() >= 3
1480 && part
1481 .chars()
1482 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
1483 {
1484 return part.to_string();
1485 }
1486 }
1487 path.split('/').next_back().unwrap_or(path).to_string()
1489}
1490
1491pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
1503 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
1505 if unh_count > 1 {
1506 return Some(ValidationIssue::new(
1507 Severity::Error,
1508 IssueKind::UntCountNotVerifiable { unh_count },
1509 ));
1510 }
1511
1512 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
1514 let declared: usize = unt.get_element(0).parse().ok()?;
1515
1516 let actual = segments
1518 .iter()
1519 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
1520 .count();
1521
1522 if declared != actual {
1523 Some(
1524 ValidationIssue::new(
1525 Severity::Error,
1526 IssueKind::UntSegmentCountMismatch { declared, actual },
1527 )
1528 .with_field_path("UNT/0074")
1529 .with_expected(actual.to_string())
1530 .with_actual(declared.to_string()),
1531 )
1532 } else {
1533 None
1534 }
1535}
1536
1537#[cfg(test)]
1538mod tests {
1539 use super::super::codes::ErrorCodes;
1540 use super::super::issue::ValidationCategory;
1541 use super::*;
1542 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
1543 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
1544 use std::collections::HashMap;
1545
1546 fn narrate(issue: &ValidationIssue) -> String {
1549 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
1550 }
1551
1552 struct MockEvaluator {
1554 results: HashMap<u32, CR>,
1555 }
1556
1557 impl MockEvaluator {
1558 fn new(results: Vec<(u32, CR)>) -> Self {
1559 Self {
1560 results: results.into_iter().collect(),
1561 }
1562 }
1563
1564 fn all_true(ids: &[u32]) -> Self {
1565 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
1566 }
1567 }
1568
1569 impl ConditionEvaluator for MockEvaluator {
1570 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
1571 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
1572 }
1573 fn is_external(&self, _condition: u32) -> bool {
1574 false
1575 }
1576 fn message_type(&self) -> &str {
1577 "UTILMD"
1578 }
1579 fn format_version(&self) -> &str {
1580 "FV2510"
1581 }
1582 }
1583
1584 #[test]
1587 fn test_is_mandatory_status() {
1588 assert!(is_mandatory_status("Muss"));
1589 assert!(is_mandatory_status("Muss [182] ∧ [152]"));
1590 assert!(is_mandatory_status("X"));
1591 assert!(is_mandatory_status("X [567]"));
1592 assert!(!is_mandatory_status("Soll [1]"));
1593 assert!(!is_mandatory_status("Kann [1]"));
1594 assert!(!is_mandatory_status(""));
1595 }
1596
1597 #[test]
1598 fn a_present_code_is_judged_by_its_own_status() {
1599 let code = |value: &str, status: &str| AhbCodeRule {
1601 value: value.to_string(),
1602 description: String::new(),
1603 ahb_status: status.to_string(),
1604 };
1605 let field = AhbFieldRule {
1606 segment_path: "SG4/STS/C556/9013".to_string(),
1607 ahb_status: "X [192]".to_string(),
1608 codes: vec![code("E01", "X [192]"), code("E03", "X")],
1609 ..Default::default()
1610 };
1611 assert_eq!(governing_status(&field, Some("E03")), "X");
1612 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
1613 assert_eq!(governing_status(&field, None), "X [192]");
1614 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
1615
1616 let own = AhbFieldRule {
1618 ahb_status: "X [931]".to_string(),
1619 codes: vec![code("303", "X")],
1620 ..Default::default()
1621 };
1622 assert_eq!(governing_status(&own, Some("303")), "X [931]");
1623 }
1624
1625 #[test]
1626 fn test_extract_segment_id_simple() {
1627 assert_eq!(extract_segment_id("NAD"), "NAD");
1628 }
1629
1630 #[test]
1631 fn test_extract_segment_id_with_sg_prefix() {
1632 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
1633 }
1634
1635 #[test]
1636 fn test_extract_segment_id_nested_sg() {
1637 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
1638 }
1639
1640 #[test]
1643 fn test_validate_missing_mandatory_field() {
1644 let evaluator = MockEvaluator::all_true(&[182, 152]);
1645 let validator = EdifactValidator::new(evaluator);
1646 let external = NoOpExternalProvider;
1647
1648 let workflow = AhbWorkflow {
1649 pruefidentifikator: "11001".to_string(),
1650 description: "Test".to_string(),
1651 communication_direction: None,
1652 fields: vec![AhbFieldRule {
1653 segment_path: "SG2/NAD/C082/3039".to_string(),
1654 name: "MP-ID des MSB".to_string(),
1655 ahb_status: "Muss [182] ∧ [152]".to_string(),
1656 codes: vec![],
1657 parent_group_ahb_status: None,
1658 segment_ahb_status: None,
1659 ..Default::default()
1660 }],
1661 ub_definitions: BTreeMap::new(),
1662 };
1663
1664 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1666
1667 assert!(!report.is_valid());
1669 let errors: Vec<_> = report.errors().collect();
1670 assert_eq!(errors.len(), 1);
1671 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
1672 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
1673 }
1674
1675 #[test]
1676 fn test_validate_condition_false_no_error() {
1677 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
1679 let validator = EdifactValidator::new(evaluator);
1680 let external = NoOpExternalProvider;
1681
1682 let workflow = AhbWorkflow {
1683 pruefidentifikator: "11001".to_string(),
1684 description: "Test".to_string(),
1685 communication_direction: None,
1686 fields: vec![AhbFieldRule {
1687 segment_path: "NAD".to_string(),
1688 name: "Partnerrolle".to_string(),
1689 ahb_status: "Muss [182] ∧ [152]".to_string(),
1690 codes: vec![],
1691 parent_group_ahb_status: None,
1692 segment_ahb_status: None,
1693 ..Default::default()
1694 }],
1695 ub_definitions: BTreeMap::new(),
1696 };
1697
1698 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1699
1700 assert!(report.is_valid());
1702 }
1703
1704 #[test]
1705 fn test_validate_condition_unknown_adds_info() {
1706 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
1708 let validator = EdifactValidator::new(evaluator);
1710 let external = NoOpExternalProvider;
1711
1712 let workflow = AhbWorkflow {
1713 pruefidentifikator: "11001".to_string(),
1714 description: "Test".to_string(),
1715 communication_direction: None,
1716 fields: vec![AhbFieldRule {
1717 segment_path: "NAD".to_string(),
1718 name: "Partnerrolle".to_string(),
1719 ahb_status: "Muss [182] ∧ [152]".to_string(),
1720 codes: vec![],
1721 parent_group_ahb_status: None,
1722 segment_ahb_status: None,
1723 ..Default::default()
1724 }],
1725 ub_definitions: BTreeMap::new(),
1726 };
1727
1728 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1729
1730 assert!(report.is_valid());
1732 let infos: Vec<_> = report.infos().collect();
1733 assert_eq!(infos.len(), 1);
1734 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
1735 }
1736
1737 #[test]
1738 fn test_validate_structure_level_skips_conditions() {
1739 let evaluator = MockEvaluator::all_true(&[182, 152]);
1740 let validator = EdifactValidator::new(evaluator);
1741 let external = NoOpExternalProvider;
1742
1743 let workflow = AhbWorkflow {
1744 pruefidentifikator: "11001".to_string(),
1745 description: "Test".to_string(),
1746 communication_direction: None,
1747 fields: vec![AhbFieldRule {
1748 segment_path: "NAD".to_string(),
1749 name: "Partnerrolle".to_string(),
1750 ahb_status: "Muss [182] ∧ [152]".to_string(),
1751 codes: vec![],
1752 parent_group_ahb_status: None,
1753 segment_ahb_status: None,
1754 ..Default::default()
1755 }],
1756 ub_definitions: BTreeMap::new(),
1757 };
1758
1759 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
1761
1762 assert!(report.is_valid());
1764 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
1765 }
1766
1767 #[test]
1768 fn test_validate_empty_workflow_no_condition_errors() {
1769 let evaluator = MockEvaluator::all_true(&[]);
1770 let validator = EdifactValidator::new(evaluator);
1771 let external = NoOpExternalProvider;
1772
1773 let empty_workflow = AhbWorkflow {
1774 pruefidentifikator: String::new(),
1775 description: String::new(),
1776 communication_direction: None,
1777 fields: vec![],
1778 ub_definitions: BTreeMap::new(),
1779 };
1780
1781 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
1782
1783 assert!(report.is_valid());
1784 }
1785
1786 #[test]
1787 fn test_validate_bare_muss_always_required() {
1788 let evaluator = MockEvaluator::new(vec![]);
1789 let validator = EdifactValidator::new(evaluator);
1790 let external = NoOpExternalProvider;
1791
1792 let workflow = AhbWorkflow {
1793 pruefidentifikator: "55001".to_string(),
1794 description: "Test".to_string(),
1795 communication_direction: Some("NB an LF".to_string()),
1796 fields: vec![AhbFieldRule {
1797 segment_path: "SG2/NAD/3035".to_string(),
1798 name: "Partnerrolle".to_string(),
1799 ahb_status: "Muss".to_string(), codes: vec![],
1801 parent_group_ahb_status: None,
1802 segment_ahb_status: None,
1803 ..Default::default()
1804 }],
1805 ub_definitions: BTreeMap::new(),
1806 };
1807
1808 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1809
1810 assert!(!report.is_valid());
1812 assert_eq!(report.error_count(), 1);
1813 }
1814
1815 #[test]
1816 fn test_validate_x_status_is_mandatory() {
1817 let evaluator = MockEvaluator::new(vec![]);
1818 let validator = EdifactValidator::new(evaluator);
1819 let external = NoOpExternalProvider;
1820
1821 let workflow = AhbWorkflow {
1822 pruefidentifikator: "55001".to_string(),
1823 description: "Test".to_string(),
1824 communication_direction: None,
1825 fields: vec![AhbFieldRule {
1826 segment_path: "DTM".to_string(),
1827 name: "Datum".to_string(),
1828 ahb_status: "X".to_string(),
1829 codes: vec![],
1830 parent_group_ahb_status: None,
1831 segment_ahb_status: None,
1832 ..Default::default()
1833 }],
1834 ub_definitions: BTreeMap::new(),
1835 };
1836
1837 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1838
1839 assert!(!report.is_valid());
1840 let errors: Vec<_> = report.errors().collect();
1841 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
1842 }
1843
1844 #[test]
1845 fn test_validate_soll_not_mandatory() {
1846 let evaluator = MockEvaluator::new(vec![]);
1847 let validator = EdifactValidator::new(evaluator);
1848 let external = NoOpExternalProvider;
1849
1850 let workflow = AhbWorkflow {
1851 pruefidentifikator: "55001".to_string(),
1852 description: "Test".to_string(),
1853 communication_direction: None,
1854 fields: vec![AhbFieldRule {
1855 segment_path: "DTM".to_string(),
1856 name: "Datum".to_string(),
1857 ahb_status: "Soll".to_string(),
1858 codes: vec![],
1859 parent_group_ahb_status: None,
1860 segment_ahb_status: None,
1861 ..Default::default()
1862 }],
1863 ub_definitions: BTreeMap::new(),
1864 };
1865
1866 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1867
1868 assert!(report.is_valid());
1870 }
1871
1872 #[test]
1873 fn test_report_includes_metadata() {
1874 let evaluator = MockEvaluator::new(vec![]);
1875 let validator = EdifactValidator::new(evaluator);
1876 let external = NoOpExternalProvider;
1877
1878 let workflow = AhbWorkflow {
1879 pruefidentifikator: "55001".to_string(),
1880 description: String::new(),
1881 communication_direction: None,
1882 fields: vec![],
1883 ub_definitions: BTreeMap::new(),
1884 };
1885
1886 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
1887
1888 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
1889 assert_eq!(report.level, ValidationLevel::Full);
1890 assert_eq!(report.message_type, "UTILMD");
1891 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
1892 }
1893
1894 #[test]
1895 fn test_validate_with_navigator_returns_report() {
1896 let evaluator = MockEvaluator::all_true(&[]);
1897 let validator = EdifactValidator::new(evaluator);
1898 let external = NoOpExternalProvider;
1899 let nav = crate::eval::NoOpGroupNavigator;
1900
1901 let workflow = AhbWorkflow {
1902 pruefidentifikator: "55001".to_string(),
1903 description: "Test".to_string(),
1904 communication_direction: None,
1905 fields: vec![],
1906 ub_definitions: BTreeMap::new(),
1907 };
1908
1909 let report = validator.validate_with_navigator(
1910 &[],
1911 &workflow,
1912 &external,
1913 ValidationLevel::Full,
1914 &nav,
1915 );
1916 assert!(report.is_valid());
1917 }
1918
1919 #[test]
1920 fn test_code_validation_composite_paths_valid_codes() {
1921 let evaluator = MockEvaluator::new(vec![]);
1925 let validator = EdifactValidator::new(evaluator);
1926 let external = NoOpExternalProvider;
1927
1928 let unh_segment = OwnedSegment {
1929 id: "UNH".to_string(),
1930 elements: vec![
1931 vec!["ALEXANDE951842".to_string()],
1932 vec![
1933 "UTILMD".to_string(),
1934 "D".to_string(),
1935 "11A".to_string(),
1936 "UN".to_string(),
1937 "S2.1".to_string(),
1938 ],
1939 ],
1940 segment_number: 1,
1941 };
1942
1943 let workflow = AhbWorkflow {
1944 pruefidentifikator: "55001".to_string(),
1945 description: "Test".to_string(),
1946 communication_direction: None,
1947 fields: vec![
1948 AhbFieldRule {
1949 segment_path: "UNH/S009/0065".to_string(),
1950 name: "Nachrichtentyp".to_string(),
1951 ahb_status: "X".to_string(),
1952 codes: vec![AhbCodeRule {
1953 value: "UTILMD".to_string(),
1954 description: "Stammdaten".to_string(),
1955 ahb_status: "X".to_string(),
1956 }],
1957 parent_group_ahb_status: None,
1958 segment_ahb_status: None,
1959 element_index: Some(1),
1960 component_index: Some(0),
1961 ..Default::default()
1962 },
1963 AhbFieldRule {
1964 segment_path: "UNH/S009/0052".to_string(),
1965 name: "Version".to_string(),
1966 ahb_status: "X".to_string(),
1967 codes: vec![AhbCodeRule {
1968 value: "D".to_string(),
1969 description: "Draft".to_string(),
1970 ahb_status: "X".to_string(),
1971 }],
1972 parent_group_ahb_status: None,
1973 segment_ahb_status: None,
1974 element_index: Some(1),
1975 component_index: Some(1),
1976 ..Default::default()
1977 },
1978 ],
1979 ub_definitions: BTreeMap::new(),
1980 };
1981
1982 let report = validator.validate(
1983 &[unh_segment],
1984 &workflow,
1985 &external,
1986 ValidationLevel::Conditions,
1987 );
1988
1989 let code_errors: Vec<_> = report
1990 .by_category(ValidationCategory::Code)
1991 .filter(|i| i.severity == Severity::Error)
1992 .collect();
1993 assert!(
1994 code_errors.is_empty(),
1995 "Expected no code errors when composite values match allowed codes, got: {:?}",
1996 code_errors
1997 );
1998 }
1999
2000 #[test]
2001 fn test_code_validation_partitions_by_mig_number() {
2002 let evaluator = MockEvaluator::new(vec![]);
2006 let validator = EdifactValidator::new(evaluator);
2007 let external = NoOpExternalProvider;
2008
2009 let sts_7 = OwnedSegment {
2010 id: "STS".to_string(),
2011 elements: vec![
2012 vec!["7".to_string()],
2013 vec![String::new()],
2014 vec!["GH02".to_string()],
2015 vec!["ZW4".to_string()],
2016 ],
2017 segment_number: 1,
2018 };
2019 let sts_e01 = OwnedSegment {
2020 id: "STS".to_string(),
2021 elements: vec![
2022 vec!["E01".to_string()],
2023 vec![String::new()],
2024 vec!["A99".to_string(), "E_0614".to_string()],
2025 ],
2026 segment_number: 2,
2027 };
2028
2029 let workflow = AhbWorkflow {
2030 pruefidentifikator: "55018".to_string(),
2031 description: "Test".to_string(),
2032 communication_direction: None,
2033 fields: vec![
2034 AhbFieldRule {
2036 segment_path: "SG4/STS/C601/9015".to_string(),
2037 name: "Statuskategorie".to_string(),
2038 ahb_status: "X".to_string(),
2039 codes: vec![AhbCodeRule {
2040 value: "7".to_string(),
2041 description: "Transaktionsgrund".to_string(),
2042 ahb_status: "X".to_string(),
2043 }],
2044 parent_group_ahb_status: None,
2045 segment_ahb_status: None,
2046 element_index: Some(0),
2047 component_index: Some(0),
2048 mig_number: Some("00035".to_string()),
2049 },
2050 AhbFieldRule {
2051 segment_path: "SG4/STS/C556/9013".to_string(),
2052 name: "Statusanlaß".to_string(),
2053 ahb_status: "X".to_string(),
2054 codes: vec![AhbCodeRule {
2055 value: "E03".to_string(),
2056 description: "Transaktionsgrund".to_string(),
2057 ahb_status: "X".to_string(),
2058 }],
2059 parent_group_ahb_status: None,
2060 segment_ahb_status: None,
2061 element_index: Some(2),
2062 component_index: Some(0),
2063 mig_number: Some("00035".to_string()),
2064 },
2065 AhbFieldRule {
2067 segment_path: "SG4/STS/C601/9015".to_string(),
2068 name: "Statuskategorie".to_string(),
2069 ahb_status: "X".to_string(),
2070 codes: vec![AhbCodeRule {
2071 value: "E01".to_string(),
2072 description: "Antwort".to_string(),
2073 ahb_status: "X".to_string(),
2074 }],
2075 parent_group_ahb_status: None,
2076 segment_ahb_status: None,
2077 element_index: Some(0),
2078 component_index: Some(0),
2079 mig_number: Some("00036".to_string()),
2080 },
2081 ],
2082 ub_definitions: BTreeMap::new(),
2083 };
2084
2085 let report = validator.validate(
2086 &[sts_7, sts_e01],
2087 &workflow,
2088 &external,
2089 ValidationLevel::Conditions,
2090 );
2091
2092 let code_errors: Vec<_> = report
2093 .by_category(ValidationCategory::Code)
2094 .filter(|i| {
2095 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2096 })
2097 .collect();
2098 assert_eq!(
2099 code_errors.len(),
2100 1,
2101 "Expected one COD002 (for GH02 only), got: {:?}",
2102 code_errors
2103 );
2104 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2105 }
2106
2107 #[test]
2108 fn test_code_validation_composite_paths_detects_invalid_code() {
2109 let evaluator = MockEvaluator::new(vec![]);
2112 let validator = EdifactValidator::new(evaluator);
2113 let external = NoOpExternalProvider;
2114
2115 let sts_segment = OwnedSegment {
2116 id: "STS".to_string(),
2117 elements: vec![
2118 vec!["7".to_string()],
2119 vec![String::new()],
2120 vec!["GH02".to_string()],
2121 vec!["ZW4".to_string()],
2122 ],
2123 segment_number: 1,
2124 };
2125
2126 let workflow = AhbWorkflow {
2127 pruefidentifikator: "55018".to_string(),
2128 description: "Test".to_string(),
2129 communication_direction: None,
2130 fields: vec![AhbFieldRule {
2131 segment_path: "SG4/STS/C556/9013".to_string(),
2132 name: "Statusanlaß".to_string(),
2133 ahb_status: "X".to_string(),
2134 codes: vec![AhbCodeRule {
2135 value: "E03".to_string(),
2136 description: "Transaktionsgrund".to_string(),
2137 ahb_status: "X".to_string(),
2138 }],
2139 parent_group_ahb_status: None,
2140 segment_ahb_status: None,
2141 element_index: Some(2),
2142 component_index: Some(0),
2143 ..Default::default()
2144 }],
2145 ub_definitions: BTreeMap::new(),
2146 };
2147
2148 let report = validator.validate(
2149 &[sts_segment],
2150 &workflow,
2151 &external,
2152 ValidationLevel::Conditions,
2153 );
2154
2155 let code_errors: Vec<_> = report
2156 .by_category(ValidationCategory::Code)
2157 .filter(|i| {
2158 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2159 })
2160 .collect();
2161 assert_eq!(
2162 code_errors.len(),
2163 1,
2164 "Expected COD002 for GH02, got: {:?}",
2165 code_errors
2166 );
2167 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2168 }
2169
2170 #[test]
2171 fn test_cross_field_code_validation_valid_qualifiers() {
2172 let evaluator = MockEvaluator::new(vec![]);
2175 let validator = EdifactValidator::new(evaluator);
2176 let external = NoOpExternalProvider;
2177
2178 let nad_ms = OwnedSegment {
2179 id: "NAD".to_string(),
2180 elements: vec![vec!["MS".to_string()]],
2181 segment_number: 4,
2182 };
2183 let nad_mr = OwnedSegment {
2184 id: "NAD".to_string(),
2185 elements: vec![vec!["MR".to_string()]],
2186 segment_number: 5,
2187 };
2188
2189 let workflow = AhbWorkflow {
2190 pruefidentifikator: "55001".to_string(),
2191 description: "Test".to_string(),
2192 communication_direction: None,
2193 fields: vec![
2194 AhbFieldRule {
2195 segment_path: "SG2/NAD/3035".to_string(),
2196 name: "Absender".to_string(),
2197 ahb_status: "X".to_string(),
2198 codes: vec![AhbCodeRule {
2199 value: "MS".to_string(),
2200 description: "Absender".to_string(),
2201 ahb_status: "X".to_string(),
2202 }],
2203 parent_group_ahb_status: None,
2204 segment_ahb_status: None,
2205 ..Default::default()
2206 },
2207 AhbFieldRule {
2208 segment_path: "SG2/NAD/3035".to_string(),
2209 name: "Empfaenger".to_string(),
2210 ahb_status: "X".to_string(),
2211 codes: vec![AhbCodeRule {
2212 value: "MR".to_string(),
2213 description: "Empfaenger".to_string(),
2214 ahb_status: "X".to_string(),
2215 }],
2216 parent_group_ahb_status: None,
2217 segment_ahb_status: None,
2218 ..Default::default()
2219 },
2220 ],
2221 ub_definitions: BTreeMap::new(),
2222 };
2223
2224 let report = validator.validate(
2225 &[nad_ms, nad_mr],
2226 &workflow,
2227 &external,
2228 ValidationLevel::Conditions,
2229 );
2230
2231 let code_errors: Vec<_> = report
2232 .by_category(ValidationCategory::Code)
2233 .filter(|i| i.severity == Severity::Error)
2234 .collect();
2235 assert!(
2236 code_errors.is_empty(),
2237 "Expected no code errors for valid qualifiers, got: {:?}",
2238 code_errors
2239 );
2240 }
2241
2242 #[test]
2243 fn test_cross_field_code_validation_catches_invalid_qualifier() {
2244 let evaluator = MockEvaluator::new(vec![]);
2246 let validator = EdifactValidator::new(evaluator);
2247 let external = NoOpExternalProvider;
2248
2249 let nad_ms = OwnedSegment {
2250 id: "NAD".to_string(),
2251 elements: vec![vec!["MS".to_string()]],
2252 segment_number: 4,
2253 };
2254 let nad_mt = OwnedSegment {
2255 id: "NAD".to_string(),
2256 elements: vec![vec!["MT".to_string()]], segment_number: 5,
2258 };
2259
2260 let workflow = AhbWorkflow {
2261 pruefidentifikator: "55001".to_string(),
2262 description: "Test".to_string(),
2263 communication_direction: None,
2264 fields: vec![
2265 AhbFieldRule {
2266 segment_path: "SG2/NAD/3035".to_string(),
2267 name: "Absender".to_string(),
2268 ahb_status: "X".to_string(),
2269 codes: vec![AhbCodeRule {
2270 value: "MS".to_string(),
2271 description: "Absender".to_string(),
2272 ahb_status: "X".to_string(),
2273 }],
2274 parent_group_ahb_status: None,
2275 segment_ahb_status: None,
2276 ..Default::default()
2277 },
2278 AhbFieldRule {
2279 segment_path: "SG2/NAD/3035".to_string(),
2280 name: "Empfaenger".to_string(),
2281 ahb_status: "X".to_string(),
2282 codes: vec![AhbCodeRule {
2283 value: "MR".to_string(),
2284 description: "Empfaenger".to_string(),
2285 ahb_status: "X".to_string(),
2286 }],
2287 parent_group_ahb_status: None,
2288 segment_ahb_status: None,
2289 ..Default::default()
2290 },
2291 ],
2292 ub_definitions: BTreeMap::new(),
2293 };
2294
2295 let report = validator.validate(
2296 &[nad_ms, nad_mt],
2297 &workflow,
2298 &external,
2299 ValidationLevel::Conditions,
2300 );
2301
2302 let code_errors: Vec<_> = report
2303 .by_category(ValidationCategory::Code)
2304 .filter(|i| i.severity == Severity::Error)
2305 .collect();
2306 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
2307 assert!(narrate(code_errors[0]).contains("MT"));
2308 assert!(narrate(code_errors[0]).contains("MR"));
2309 assert!(narrate(code_errors[0]).contains("MS"));
2310 }
2311
2312 #[test]
2313 fn test_cross_field_code_validation_unions_across_groups() {
2314 let evaluator = MockEvaluator::new(vec![]);
2318 let validator = EdifactValidator::new(evaluator);
2319 let external = NoOpExternalProvider;
2320
2321 let segments = vec![
2322 OwnedSegment {
2323 id: "NAD".to_string(),
2324 elements: vec![vec!["MS".to_string()]],
2325 segment_number: 3,
2326 },
2327 OwnedSegment {
2328 id: "NAD".to_string(),
2329 elements: vec![vec!["MR".to_string()]],
2330 segment_number: 4,
2331 },
2332 OwnedSegment {
2333 id: "NAD".to_string(),
2334 elements: vec![vec!["Z04".to_string()]],
2335 segment_number: 20,
2336 },
2337 OwnedSegment {
2338 id: "NAD".to_string(),
2339 elements: vec![vec!["Z09".to_string()]],
2340 segment_number: 21,
2341 },
2342 OwnedSegment {
2343 id: "NAD".to_string(),
2344 elements: vec![vec!["MT".to_string()]], segment_number: 22,
2346 },
2347 ];
2348
2349 let workflow = AhbWorkflow {
2350 pruefidentifikator: "55001".to_string(),
2351 description: "Test".to_string(),
2352 communication_direction: None,
2353 fields: vec![
2354 AhbFieldRule {
2355 segment_path: "SG2/NAD/3035".to_string(),
2356 name: "Absender".to_string(),
2357 ahb_status: "X".to_string(),
2358 codes: vec![AhbCodeRule {
2359 value: "MS".to_string(),
2360 description: "Absender".to_string(),
2361 ahb_status: "X".to_string(),
2362 }],
2363 parent_group_ahb_status: None,
2364 segment_ahb_status: None,
2365 ..Default::default()
2366 },
2367 AhbFieldRule {
2368 segment_path: "SG2/NAD/3035".to_string(),
2369 name: "Empfaenger".to_string(),
2370 ahb_status: "X".to_string(),
2371 codes: vec![AhbCodeRule {
2372 value: "MR".to_string(),
2373 description: "Empfaenger".to_string(),
2374 ahb_status: "X".to_string(),
2375 }],
2376 parent_group_ahb_status: None,
2377 segment_ahb_status: None,
2378 ..Default::default()
2379 },
2380 AhbFieldRule {
2381 segment_path: "SG4/SG12/NAD/3035".to_string(),
2382 name: "Anschlussnutzer".to_string(),
2383 ahb_status: "X".to_string(),
2384 codes: vec![AhbCodeRule {
2385 value: "Z04".to_string(),
2386 description: "Anschlussnutzer".to_string(),
2387 ahb_status: "X".to_string(),
2388 }],
2389 parent_group_ahb_status: None,
2390 segment_ahb_status: None,
2391 ..Default::default()
2392 },
2393 AhbFieldRule {
2394 segment_path: "SG4/SG12/NAD/3035".to_string(),
2395 name: "Korrespondenzanschrift".to_string(),
2396 ahb_status: "X".to_string(),
2397 codes: vec![AhbCodeRule {
2398 value: "Z09".to_string(),
2399 description: "Korrespondenzanschrift".to_string(),
2400 ahb_status: "X".to_string(),
2401 }],
2402 parent_group_ahb_status: None,
2403 segment_ahb_status: None,
2404 ..Default::default()
2405 },
2406 ],
2407 ub_definitions: BTreeMap::new(),
2408 };
2409
2410 let report =
2411 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
2412
2413 let code_errors: Vec<_> = report
2414 .by_category(ValidationCategory::Code)
2415 .filter(|i| i.severity == Severity::Error)
2416 .collect();
2417 assert_eq!(
2418 code_errors.len(),
2419 1,
2420 "Expected exactly one COD002 error for MT, got: {:?}",
2421 code_errors
2422 );
2423 assert!(narrate(code_errors[0]).contains("MT"));
2424 }
2425
2426 #[test]
2427 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
2428 let evaluator = MockEvaluator::new(vec![]);
2437 let validator = EdifactValidator::new(evaluator);
2438 let external = NoOpExternalProvider;
2439
2440 let qty_67 = OwnedSegment {
2441 id: "QTY".to_string(),
2442 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
2443 segment_number: 10,
2444 };
2445
2446 let workflow = AhbWorkflow {
2447 pruefidentifikator: "13025".to_string(),
2448 description: "Test".to_string(),
2449 communication_direction: None,
2450 fields: vec![AhbFieldRule {
2451 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
2452 name: "Menge, Qualifier".to_string(),
2453 ahb_status: "X".to_string(),
2454 codes: vec![
2455 AhbCodeRule {
2456 value: "220".to_string(),
2457 description: "Wahrer Wert".to_string(),
2458 ahb_status: "X".to_string(),
2459 },
2460 AhbCodeRule {
2461 value: "67".to_string(),
2462 description: "Ersatzwert".to_string(),
2463 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
2464 },
2465 AhbCodeRule {
2466 value: "Z18".to_string(),
2467 description: "Vorläufiger Wert".to_string(),
2468 ahb_status: "X [35]".to_string(),
2469 },
2470 ],
2471 parent_group_ahb_status: None,
2472 segment_ahb_status: None,
2473 element_index: Some(0),
2474 component_index: Some(0),
2475 ..Default::default()
2476 }],
2477 ub_definitions: BTreeMap::new(),
2478 };
2479
2480 let report =
2481 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
2482
2483 let code_errors: Vec<_> = report
2484 .by_category(ValidationCategory::Code)
2485 .filter(|i| i.severity == Severity::Error)
2486 .collect();
2487 assert!(
2488 code_errors.is_empty(),
2489 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
2490 code_errors
2491 );
2492 }
2493
2494 #[test]
2495 fn test_is_qualifier_field_simple_paths() {
2496 assert!(is_qualifier_field("NAD/3035"));
2497 assert!(is_qualifier_field("SG2/NAD/3035"));
2498 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
2499 assert!(is_qualifier_field("LOC/3227"));
2500 }
2501
2502 #[test]
2503 fn test_is_qualifier_field_composite_paths() {
2504 assert!(is_qualifier_field("UNH/S009/0065"));
2508 assert!(is_qualifier_field("NAD/C082/3039"));
2509 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
2510 assert!(is_qualifier_field("SG4/STS/C556/9013"));
2511 }
2512
2513 #[test]
2514 fn test_is_qualifier_field_bare_segment() {
2515 assert!(!is_qualifier_field("NAD"));
2516 assert!(!is_qualifier_field("SG2/NAD"));
2517 }
2518
2519 #[test]
2520 fn test_is_qualifier_field_rejects_deep_paths() {
2521 assert!(!is_qualifier_field("SEG/A/B/C/D"));
2523 }
2524
2525 #[test]
2526 fn test_missing_qualifier_instance_is_detected() {
2527 let evaluator = MockEvaluator::new(vec![]);
2530 let validator = EdifactValidator::new(evaluator);
2531 let external = NoOpExternalProvider;
2532
2533 let nad_ms = OwnedSegment {
2534 id: "NAD".to_string(),
2535 elements: vec![vec!["MS".to_string()]],
2536 segment_number: 3,
2537 };
2538
2539 let workflow = AhbWorkflow {
2540 pruefidentifikator: "55001".to_string(),
2541 description: "Test".to_string(),
2542 communication_direction: None,
2543 fields: vec![
2544 AhbFieldRule {
2545 segment_path: "SG2/NAD/3035".to_string(),
2546 name: "Absender".to_string(),
2547 ahb_status: "X".to_string(),
2548 codes: vec![AhbCodeRule {
2549 value: "MS".to_string(),
2550 description: "Absender".to_string(),
2551 ahb_status: "X".to_string(),
2552 }],
2553 parent_group_ahb_status: None,
2554 segment_ahb_status: None,
2555 ..Default::default()
2556 },
2557 AhbFieldRule {
2558 segment_path: "SG2/NAD/3035".to_string(),
2559 name: "Empfaenger".to_string(),
2560 ahb_status: "Muss".to_string(),
2561 codes: vec![AhbCodeRule {
2562 value: "MR".to_string(),
2563 description: "Empfaenger".to_string(),
2564 ahb_status: "X".to_string(),
2565 }],
2566 parent_group_ahb_status: None,
2567 segment_ahb_status: None,
2568 ..Default::default()
2569 },
2570 ],
2571 ub_definitions: BTreeMap::new(),
2572 };
2573
2574 let report =
2575 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
2576
2577 let ahb_errors: Vec<_> = report
2578 .by_category(ValidationCategory::Ahb)
2579 .filter(|i| i.severity == Severity::Error)
2580 .collect();
2581 assert_eq!(
2582 ahb_errors.len(),
2583 1,
2584 "Expected AHB001 for missing NAD+MR, got: {:?}",
2585 ahb_errors
2586 );
2587 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
2588 }
2589
2590 #[test]
2591 fn test_present_qualifier_instance_no_error() {
2592 let evaluator = MockEvaluator::new(vec![]);
2594 let validator = EdifactValidator::new(evaluator);
2595 let external = NoOpExternalProvider;
2596
2597 let segments = vec![
2598 OwnedSegment {
2599 id: "NAD".to_string(),
2600 elements: vec![vec!["MS".to_string()]],
2601 segment_number: 3,
2602 },
2603 OwnedSegment {
2604 id: "NAD".to_string(),
2605 elements: vec![vec!["MR".to_string()]],
2606 segment_number: 4,
2607 },
2608 ];
2609
2610 let workflow = AhbWorkflow {
2611 pruefidentifikator: "55001".to_string(),
2612 description: "Test".to_string(),
2613 communication_direction: None,
2614 fields: vec![
2615 AhbFieldRule {
2616 segment_path: "SG2/NAD/3035".to_string(),
2617 name: "Absender".to_string(),
2618 ahb_status: "Muss".to_string(),
2619 codes: vec![AhbCodeRule {
2620 value: "MS".to_string(),
2621 description: "Absender".to_string(),
2622 ahb_status: "X".to_string(),
2623 }],
2624 parent_group_ahb_status: None,
2625 segment_ahb_status: None,
2626 ..Default::default()
2627 },
2628 AhbFieldRule {
2629 segment_path: "SG2/NAD/3035".to_string(),
2630 name: "Empfaenger".to_string(),
2631 ahb_status: "Muss".to_string(),
2632 codes: vec![AhbCodeRule {
2633 value: "MR".to_string(),
2634 description: "Empfaenger".to_string(),
2635 ahb_status: "X".to_string(),
2636 }],
2637 parent_group_ahb_status: None,
2638 segment_ahb_status: None,
2639 ..Default::default()
2640 },
2641 ],
2642 ub_definitions: BTreeMap::new(),
2643 };
2644
2645 let report =
2646 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
2647
2648 let ahb_errors: Vec<_> = report
2649 .by_category(ValidationCategory::Ahb)
2650 .filter(|i| i.severity == Severity::Error)
2651 .collect();
2652 assert!(
2653 ahb_errors.is_empty(),
2654 "Expected no AHB001 errors, got: {:?}",
2655 ahb_errors
2656 );
2657 }
2658
2659 #[test]
2660 fn test_extract_group_path_key() {
2661 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
2662 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
2663 assert_eq!(extract_group_path_key("NAD/3035"), "");
2664 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
2665 }
2666
2667 #[test]
2668 fn test_absent_optional_group_no_missing_field_error() {
2669 use mig_types::navigator::GroupNavigator;
2672
2673 struct NavWithoutSG3;
2674 impl GroupNavigator for NavWithoutSG3 {
2675 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2676 vec![]
2677 }
2678 fn find_segments_with_qualifier_in_group(
2679 &self,
2680 _: &str,
2681 _: usize,
2682 _: &str,
2683 _: &[&str],
2684 _: usize,
2685 ) -> Vec<OwnedSegment> {
2686 vec![]
2687 }
2688 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2689 match group_path {
2690 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
2693 }
2694 }
2695 }
2696
2697 let evaluator = MockEvaluator::new(vec![]);
2698 let validator = EdifactValidator::new(evaluator);
2699 let external = NoOpExternalProvider;
2700 let nav = NavWithoutSG3;
2701
2702 let segments = vec![
2704 OwnedSegment {
2705 id: "NAD".into(),
2706 elements: vec![vec!["MS".into()]],
2707 segment_number: 3,
2708 },
2709 OwnedSegment {
2710 id: "NAD".into(),
2711 elements: vec![vec!["MR".into()]],
2712 segment_number: 4,
2713 },
2714 ];
2715
2716 let workflow = AhbWorkflow {
2717 pruefidentifikator: "55001".to_string(),
2718 description: "Test".to_string(),
2719 communication_direction: None,
2720 fields: vec![
2721 AhbFieldRule {
2722 segment_path: "SG2/SG3/CTA/3139".to_string(),
2723 name: "Funktion des Ansprechpartners, Code".to_string(),
2724 ahb_status: "Muss".to_string(),
2725 codes: vec![],
2726 parent_group_ahb_status: None,
2727 segment_ahb_status: None,
2728 ..Default::default()
2729 },
2730 AhbFieldRule {
2731 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
2732 name: "Name vom Ansprechpartner".to_string(),
2733 ahb_status: "X".to_string(),
2734 codes: vec![],
2735 parent_group_ahb_status: None,
2736 segment_ahb_status: None,
2737 ..Default::default()
2738 },
2739 ],
2740 ub_definitions: BTreeMap::new(),
2741 };
2742
2743 let report = validator.validate_with_navigator(
2744 &segments,
2745 &workflow,
2746 &external,
2747 ValidationLevel::Conditions,
2748 &nav,
2749 );
2750
2751 let ahb_errors: Vec<_> = report
2752 .by_category(ValidationCategory::Ahb)
2753 .filter(|i| i.severity == Severity::Error)
2754 .collect();
2755 assert!(
2756 ahb_errors.is_empty(),
2757 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
2758 ahb_errors
2759 );
2760 }
2761
2762 #[test]
2763 fn test_present_group_still_checks_mandatory_fields() {
2764 use mig_types::navigator::GroupNavigator;
2766
2767 struct NavWithSG3;
2768 impl GroupNavigator for NavWithSG3 {
2769 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2770 vec![]
2771 }
2772 fn find_segments_with_qualifier_in_group(
2773 &self,
2774 _: &str,
2775 _: usize,
2776 _: &str,
2777 _: &[&str],
2778 _: usize,
2779 ) -> Vec<OwnedSegment> {
2780 vec![]
2781 }
2782 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2783 match group_path {
2784 ["SG2"] => 1,
2785 ["SG2", "SG3"] => 1, _ => 0,
2787 }
2788 }
2789 }
2790
2791 let evaluator = MockEvaluator::new(vec![]);
2792 let validator = EdifactValidator::new(evaluator);
2793 let external = NoOpExternalProvider;
2794 let nav = NavWithSG3;
2795
2796 let segments = vec![OwnedSegment {
2798 id: "NAD".into(),
2799 elements: vec![vec!["MS".into()]],
2800 segment_number: 3,
2801 }];
2802
2803 let workflow = AhbWorkflow {
2804 pruefidentifikator: "55001".to_string(),
2805 description: "Test".to_string(),
2806 communication_direction: None,
2807 fields: vec![AhbFieldRule {
2808 segment_path: "SG2/SG3/CTA/3139".to_string(),
2809 name: "Funktion des Ansprechpartners, Code".to_string(),
2810 ahb_status: "Muss".to_string(),
2811 codes: vec![],
2812 parent_group_ahb_status: None,
2813 segment_ahb_status: None,
2814 ..Default::default()
2815 }],
2816 ub_definitions: BTreeMap::new(),
2817 };
2818
2819 let report = validator.validate_with_navigator(
2820 &segments,
2821 &workflow,
2822 &external,
2823 ValidationLevel::Conditions,
2824 &nav,
2825 );
2826
2827 let ahb_errors: Vec<_> = report
2828 .by_category(ValidationCategory::Ahb)
2829 .filter(|i| i.severity == Severity::Error)
2830 .collect();
2831 assert_eq!(
2832 ahb_errors.len(),
2833 1,
2834 "Expected AHB001 error when SG3 is present but CTA missing"
2835 );
2836 assert!(narrate(ahb_errors[0]).contains("CTA"));
2837 }
2838
2839 #[test]
2840 fn test_missing_qualifier_with_navigator_is_detected() {
2841 use mig_types::navigator::GroupNavigator;
2844
2845 struct NavWithSG2;
2846 impl GroupNavigator for NavWithSG2 {
2847 fn find_segments_in_group(
2848 &self,
2849 segment_id: &str,
2850 group_path: &[&str],
2851 instance_index: usize,
2852 ) -> Vec<OwnedSegment> {
2853 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
2854 vec![OwnedSegment {
2855 id: "NAD".into(),
2856 elements: vec![vec!["MS".into()]],
2857 segment_number: 3,
2858 }]
2859 } else {
2860 vec![]
2861 }
2862 }
2863 fn find_segments_with_qualifier_in_group(
2864 &self,
2865 _: &str,
2866 _: usize,
2867 _: &str,
2868 _: &[&str],
2869 _: usize,
2870 ) -> Vec<OwnedSegment> {
2871 vec![]
2872 }
2873 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2874 match group_path {
2875 ["SG2"] => 1,
2876 _ => 0,
2877 }
2878 }
2879 }
2880
2881 let evaluator = MockEvaluator::new(vec![]);
2882 let validator = EdifactValidator::new(evaluator);
2883 let external = NoOpExternalProvider;
2884 let nav = NavWithSG2;
2885
2886 let segments = vec![OwnedSegment {
2887 id: "NAD".into(),
2888 elements: vec![vec!["MS".into()]],
2889 segment_number: 3,
2890 }];
2891
2892 let workflow = AhbWorkflow {
2893 pruefidentifikator: "55001".to_string(),
2894 description: "Test".to_string(),
2895 communication_direction: None,
2896 fields: vec![
2897 AhbFieldRule {
2898 segment_path: "SG2/NAD/3035".to_string(),
2899 name: "Absender".to_string(),
2900 ahb_status: "X".to_string(),
2901 codes: vec![AhbCodeRule {
2902 value: "MS".to_string(),
2903 description: "Absender".to_string(),
2904 ahb_status: "X".to_string(),
2905 }],
2906 parent_group_ahb_status: None,
2907 segment_ahb_status: None,
2908 ..Default::default()
2909 },
2910 AhbFieldRule {
2911 segment_path: "SG2/NAD/3035".to_string(),
2912 name: "Empfaenger".to_string(),
2913 ahb_status: "Muss".to_string(),
2914 codes: vec![AhbCodeRule {
2915 value: "MR".to_string(),
2916 description: "Empfaenger".to_string(),
2917 ahb_status: "X".to_string(),
2918 }],
2919 parent_group_ahb_status: None,
2920 segment_ahb_status: None,
2921 ..Default::default()
2922 },
2923 ],
2924 ub_definitions: BTreeMap::new(),
2925 };
2926
2927 let report = validator.validate_with_navigator(
2928 &segments,
2929 &workflow,
2930 &external,
2931 ValidationLevel::Conditions,
2932 &nav,
2933 );
2934
2935 let ahb_errors: Vec<_> = report
2936 .by_category(ValidationCategory::Ahb)
2937 .filter(|i| i.severity == Severity::Error)
2938 .collect();
2939 assert_eq!(
2940 ahb_errors.len(),
2941 1,
2942 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
2943 ahb_errors
2944 );
2945 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
2946 }
2947
2948 #[test]
2949 fn test_optional_group_variant_absent_no_error() {
2950 use mig_types::navigator::GroupNavigator;
2955
2956 struct TestNav;
2957 impl GroupNavigator for TestNav {
2958 fn find_segments_in_group(
2959 &self,
2960 segment_id: &str,
2961 group_path: &[&str],
2962 instance_index: usize,
2963 ) -> Vec<OwnedSegment> {
2964 match (segment_id, group_path, instance_index) {
2965 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
2966 id: "LOC".into(),
2967 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2968 segment_number: 10,
2969 }],
2970 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
2971 id: "NAD".into(),
2972 elements: vec![vec!["MS".into()]],
2973 segment_number: 3,
2974 }],
2975 _ => vec![],
2976 }
2977 }
2978 fn find_segments_with_qualifier_in_group(
2979 &self,
2980 _: &str,
2981 _: usize,
2982 _: &str,
2983 _: &[&str],
2984 _: usize,
2985 ) -> Vec<OwnedSegment> {
2986 vec![]
2987 }
2988 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2989 match group_path {
2990 ["SG2"] => 1,
2991 ["SG4"] => 1,
2992 ["SG4", "SG5"] => 1, _ => 0,
2994 }
2995 }
2996 }
2997
2998 let evaluator = MockEvaluator::new(vec![]);
2999 let validator = EdifactValidator::new(evaluator);
3000 let external = NoOpExternalProvider;
3001 let nav = TestNav;
3002
3003 let segments = vec![
3004 OwnedSegment {
3005 id: "NAD".into(),
3006 elements: vec![vec!["MS".into()]],
3007 segment_number: 3,
3008 },
3009 OwnedSegment {
3010 id: "LOC".into(),
3011 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3012 segment_number: 10,
3013 },
3014 ];
3015
3016 let workflow = AhbWorkflow {
3017 pruefidentifikator: "55001".to_string(),
3018 description: "Test".to_string(),
3019 communication_direction: None,
3020 fields: vec![
3021 AhbFieldRule {
3023 segment_path: "SG2/NAD/3035".to_string(),
3024 name: "Absender".to_string(),
3025 ahb_status: "X".to_string(),
3026 codes: vec![AhbCodeRule {
3027 value: "MS".to_string(),
3028 description: "Absender".to_string(),
3029 ahb_status: "X".to_string(),
3030 }],
3031 parent_group_ahb_status: Some("Muss".to_string()),
3032 segment_ahb_status: None,
3033 ..Default::default()
3034 },
3035 AhbFieldRule {
3036 segment_path: "SG2/NAD/3035".to_string(),
3037 name: "Empfaenger".to_string(),
3038 ahb_status: "Muss".to_string(),
3039 codes: vec![AhbCodeRule {
3040 value: "MR".to_string(),
3041 description: "Empfaenger".to_string(),
3042 ahb_status: "X".to_string(),
3043 }],
3044 parent_group_ahb_status: Some("Muss".to_string()),
3045 segment_ahb_status: None,
3046 ..Default::default()
3047 },
3048 AhbFieldRule {
3050 segment_path: "SG4/SG5/LOC/3227".to_string(),
3051 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3052 ahb_status: "X".to_string(),
3053 codes: vec![AhbCodeRule {
3054 value: "Z16".to_string(),
3055 description: "Marktlokation".to_string(),
3056 ahb_status: "X".to_string(),
3057 }],
3058 parent_group_ahb_status: Some("Kann".to_string()),
3059 segment_ahb_status: None,
3060 ..Default::default()
3061 },
3062 AhbFieldRule {
3063 segment_path: "SG4/SG5/LOC/3227".to_string(),
3064 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3065 ahb_status: "Muss".to_string(),
3066 codes: vec![AhbCodeRule {
3067 value: "Z17".to_string(),
3068 description: "Messlokation".to_string(),
3069 ahb_status: "X".to_string(),
3070 }],
3071 parent_group_ahb_status: Some("Kann".to_string()),
3072 segment_ahb_status: None,
3073 ..Default::default()
3074 },
3075 ],
3076 ub_definitions: BTreeMap::new(),
3077 };
3078
3079 let report = validator.validate_with_navigator(
3080 &segments,
3081 &workflow,
3082 &external,
3083 ValidationLevel::Conditions,
3084 &nav,
3085 );
3086
3087 let ahb_errors: Vec<_> = report
3088 .by_category(ValidationCategory::Ahb)
3089 .filter(|i| i.severity == Severity::Error)
3090 .collect();
3091
3092 assert_eq!(
3095 ahb_errors.len(),
3096 1,
3097 "Expected only AHB001 for missing NAD+MR, got: {:?}",
3098 ahb_errors
3099 );
3100 assert!(
3101 narrate(ahb_errors[0]).contains("Empfaenger"),
3102 "Error should be for missing NAD+MR (Empfaenger)"
3103 );
3104 }
3105
3106 #[test]
3107 fn test_conditional_group_variant_absent_no_error() {
3108 use mig_types::navigator::GroupNavigator;
3113
3114 struct TestNav;
3115 impl GroupNavigator for TestNav {
3116 fn find_segments_in_group(
3117 &self,
3118 segment_id: &str,
3119 group_path: &[&str],
3120 instance_index: usize,
3121 ) -> Vec<OwnedSegment> {
3122 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
3123 vec![OwnedSegment {
3124 id: "LOC".into(),
3125 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3126 segment_number: 10,
3127 }]
3128 } else {
3129 vec![]
3130 }
3131 }
3132 fn find_segments_with_qualifier_in_group(
3133 &self,
3134 _: &str,
3135 _: usize,
3136 _: &str,
3137 _: &[&str],
3138 _: usize,
3139 ) -> Vec<OwnedSegment> {
3140 vec![]
3141 }
3142 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3143 match group_path {
3144 ["SG4"] => 1,
3145 ["SG4", "SG5"] => 1, _ => 0,
3147 }
3148 }
3149 }
3150
3151 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
3154 let validator = EdifactValidator::new(evaluator);
3155 let external = NoOpExternalProvider;
3156 let nav = TestNav;
3157
3158 let segments = vec![OwnedSegment {
3159 id: "LOC".into(),
3160 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3161 segment_number: 10,
3162 }];
3163
3164 let workflow = AhbWorkflow {
3165 pruefidentifikator: "55001".to_string(),
3166 description: "Test".to_string(),
3167 communication_direction: None,
3168 fields: vec![
3169 AhbFieldRule {
3171 segment_path: "SG4/SG5/LOC/3227".to_string(),
3172 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3173 ahb_status: "X".to_string(),
3174 codes: vec![AhbCodeRule {
3175 value: "Z16".to_string(),
3176 description: "Marktlokation".to_string(),
3177 ahb_status: "X".to_string(),
3178 }],
3179 parent_group_ahb_status: Some("Muss [2061]".to_string()),
3180 segment_ahb_status: None,
3181 ..Default::default()
3182 },
3183 AhbFieldRule {
3185 segment_path: "SG4/SG5/LOC/3227".to_string(),
3186 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3187 ahb_status: "X".to_string(),
3188 codes: vec![AhbCodeRule {
3189 value: "Z17".to_string(),
3190 description: "Messlokation".to_string(),
3191 ahb_status: "X".to_string(),
3192 }],
3193 parent_group_ahb_status: Some("Soll [165]".to_string()),
3194 segment_ahb_status: None,
3195 ..Default::default()
3196 },
3197 ],
3198 ub_definitions: BTreeMap::new(),
3199 };
3200
3201 let report = validator.validate_with_navigator(
3202 &segments,
3203 &workflow,
3204 &external,
3205 ValidationLevel::Conditions,
3206 &nav,
3207 );
3208
3209 let ahb_errors: Vec<_> = report
3210 .by_category(ValidationCategory::Ahb)
3211 .filter(|i| i.severity == Severity::Error)
3212 .collect();
3213
3214 assert!(
3216 ahb_errors.is_empty(),
3217 "Expected no errors when conditional group variant [165]=False, got: {:?}",
3218 ahb_errors
3219 );
3220 }
3221
3222 #[test]
3223 fn test_conditional_group_variant_unknown_no_error() {
3224 let evaluator = MockEvaluator::new(vec![]);
3230 let validator = EdifactValidator::new(evaluator);
3231 let external = NoOpExternalProvider;
3232
3233 let workflow = AhbWorkflow {
3234 pruefidentifikator: "55001".to_string(),
3235 description: "Test".to_string(),
3236 communication_direction: None,
3237 fields: vec![AhbFieldRule {
3238 segment_path: "SG4/SG5/LOC/3227".to_string(),
3239 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3240 ahb_status: "X".to_string(),
3241 codes: vec![AhbCodeRule {
3242 value: "Z17".to_string(),
3243 description: "Messlokation".to_string(),
3244 ahb_status: "X".to_string(),
3245 }],
3246 parent_group_ahb_status: Some("Soll [165]".to_string()),
3247 segment_ahb_status: None,
3248 ..Default::default()
3249 }],
3250 ub_definitions: BTreeMap::new(),
3251 };
3252
3253 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3254
3255 let ahb_errors: Vec<_> = report
3256 .by_category(ValidationCategory::Ahb)
3257 .filter(|i| i.severity == Severity::Error)
3258 .collect();
3259
3260 assert!(
3262 ahb_errors.is_empty(),
3263 "Expected no errors when parent group condition is Unknown, got: {:?}",
3264 ahb_errors
3265 );
3266 }
3267
3268 #[test]
3269 fn test_segment_absent_within_present_group_no_error() {
3270 use mig_types::navigator::GroupNavigator;
3274
3275 struct TestNav;
3276 impl GroupNavigator for TestNav {
3277 fn find_segments_in_group(
3278 &self,
3279 segment_id: &str,
3280 group_path: &[&str],
3281 instance_index: usize,
3282 ) -> Vec<OwnedSegment> {
3283 if segment_id == "QTY"
3285 && group_path == ["SG5", "SG6", "SG9", "SG10"]
3286 && instance_index == 0
3287 {
3288 vec![OwnedSegment {
3289 id: "QTY".into(),
3290 elements: vec![vec!["220".into(), "0".into()]],
3291 segment_number: 14,
3292 }]
3293 } else {
3294 vec![]
3295 }
3296 }
3297 fn find_segments_with_qualifier_in_group(
3298 &self,
3299 _: &str,
3300 _: usize,
3301 _: &str,
3302 _: &[&str],
3303 _: usize,
3304 ) -> Vec<OwnedSegment> {
3305 vec![]
3306 }
3307 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3308 match group_path {
3309 ["SG5"] => 1,
3310 ["SG5", "SG6"] => 1,
3311 ["SG5", "SG6", "SG9"] => 1,
3312 ["SG5", "SG6", "SG9", "SG10"] => 1,
3313 _ => 0,
3314 }
3315 }
3316 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
3317 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
3319 }
3320 }
3321
3322 let evaluator = MockEvaluator::all_true(&[]);
3323 let validator = EdifactValidator::new(evaluator);
3324 let external = NoOpExternalProvider;
3325 let nav = TestNav;
3326
3327 let segments = vec![OwnedSegment {
3328 id: "QTY".into(),
3329 elements: vec![vec!["220".into(), "0".into()]],
3330 segment_number: 14,
3331 }];
3332
3333 let workflow = AhbWorkflow {
3334 pruefidentifikator: "13017".to_string(),
3335 description: "Test".to_string(),
3336 communication_direction: None,
3337 fields: vec![
3338 AhbFieldRule {
3340 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
3341 name: "Statuskategorie, Code".to_string(),
3342 ahb_status: "X".to_string(),
3343 codes: vec![],
3344 parent_group_ahb_status: Some("Muss".to_string()),
3345 segment_ahb_status: None,
3346 ..Default::default()
3347 },
3348 AhbFieldRule {
3350 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3351 name: "Statusanlaß, Code".to_string(),
3352 ahb_status: "X [5]".to_string(),
3353 codes: vec![],
3354 parent_group_ahb_status: Some("Muss".to_string()),
3355 segment_ahb_status: None,
3356 ..Default::default()
3357 },
3358 ],
3359 ub_definitions: BTreeMap::new(),
3360 };
3361
3362 let report = validator.validate_with_navigator(
3363 &segments,
3364 &workflow,
3365 &external,
3366 ValidationLevel::Conditions,
3367 &nav,
3368 );
3369
3370 let ahb_errors: Vec<_> = report
3371 .by_category(ValidationCategory::Ahb)
3372 .filter(|i| i.severity == Severity::Error)
3373 .collect();
3374
3375 assert!(
3376 ahb_errors.is_empty(),
3377 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
3378 ahb_errors
3379 );
3380 }
3381
3382 #[test]
3383 fn test_group_scoped_code_validation_with_navigator() {
3384 use mig_types::navigator::GroupNavigator;
3388
3389 struct TestNav;
3390 impl GroupNavigator for TestNav {
3391 fn find_segments_in_group(
3392 &self,
3393 segment_id: &str,
3394 group_path: &[&str],
3395 _instance_index: usize,
3396 ) -> Vec<OwnedSegment> {
3397 if segment_id != "NAD" {
3398 return vec![];
3399 }
3400 match group_path {
3401 ["SG2"] => vec![
3402 OwnedSegment {
3403 id: "NAD".into(),
3404 elements: vec![vec!["MS".into()]],
3405 segment_number: 3,
3406 },
3407 OwnedSegment {
3408 id: "NAD".into(),
3409 elements: vec![vec!["MT".into()]], segment_number: 4,
3411 },
3412 ],
3413 ["SG4", "SG12"] => vec![
3414 OwnedSegment {
3415 id: "NAD".into(),
3416 elements: vec![vec!["Z04".into()]],
3417 segment_number: 20,
3418 },
3419 OwnedSegment {
3420 id: "NAD".into(),
3421 elements: vec![vec!["Z09".into()]],
3422 segment_number: 21,
3423 },
3424 ],
3425 _ => vec![],
3426 }
3427 }
3428 fn find_segments_with_qualifier_in_group(
3429 &self,
3430 _: &str,
3431 _: usize,
3432 _: &str,
3433 _: &[&str],
3434 _: usize,
3435 ) -> Vec<OwnedSegment> {
3436 vec![]
3437 }
3438 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3439 match group_path {
3440 ["SG2"] | ["SG4", "SG12"] => 1,
3441 _ => 0,
3442 }
3443 }
3444 }
3445
3446 let evaluator = MockEvaluator::new(vec![]);
3447 let validator = EdifactValidator::new(evaluator);
3448 let external = NoOpExternalProvider;
3449 let nav = TestNav;
3450
3451 let workflow = AhbWorkflow {
3452 pruefidentifikator: "55001".to_string(),
3453 description: "Test".to_string(),
3454 communication_direction: None,
3455 fields: vec![
3456 AhbFieldRule {
3457 segment_path: "SG2/NAD/3035".to_string(),
3458 name: "Absender".to_string(),
3459 ahb_status: "X".to_string(),
3460 codes: vec![AhbCodeRule {
3461 value: "MS".to_string(),
3462 description: "Absender".to_string(),
3463 ahb_status: "X".to_string(),
3464 }],
3465 parent_group_ahb_status: None,
3466 segment_ahb_status: None,
3467 ..Default::default()
3468 },
3469 AhbFieldRule {
3470 segment_path: "SG2/NAD/3035".to_string(),
3471 name: "Empfaenger".to_string(),
3472 ahb_status: "X".to_string(),
3473 codes: vec![AhbCodeRule {
3474 value: "MR".to_string(),
3475 description: "Empfaenger".to_string(),
3476 ahb_status: "X".to_string(),
3477 }],
3478 parent_group_ahb_status: None,
3479 segment_ahb_status: None,
3480 ..Default::default()
3481 },
3482 AhbFieldRule {
3483 segment_path: "SG4/SG12/NAD/3035".to_string(),
3484 name: "Anschlussnutzer".to_string(),
3485 ahb_status: "X".to_string(),
3486 codes: vec![AhbCodeRule {
3487 value: "Z04".to_string(),
3488 description: "Anschlussnutzer".to_string(),
3489 ahb_status: "X".to_string(),
3490 }],
3491 parent_group_ahb_status: None,
3492 segment_ahb_status: None,
3493 ..Default::default()
3494 },
3495 AhbFieldRule {
3496 segment_path: "SG4/SG12/NAD/3035".to_string(),
3497 name: "Korrespondenzanschrift".to_string(),
3498 ahb_status: "X".to_string(),
3499 codes: vec![AhbCodeRule {
3500 value: "Z09".to_string(),
3501 description: "Korrespondenzanschrift".to_string(),
3502 ahb_status: "X".to_string(),
3503 }],
3504 parent_group_ahb_status: None,
3505 segment_ahb_status: None,
3506 ..Default::default()
3507 },
3508 ],
3509 ub_definitions: BTreeMap::new(),
3510 };
3511
3512 let all_segments = vec![
3514 OwnedSegment {
3515 id: "NAD".into(),
3516 elements: vec![vec!["MS".into()]],
3517 segment_number: 3,
3518 },
3519 OwnedSegment {
3520 id: "NAD".into(),
3521 elements: vec![vec!["MT".into()]],
3522 segment_number: 4,
3523 },
3524 OwnedSegment {
3525 id: "NAD".into(),
3526 elements: vec![vec!["Z04".into()]],
3527 segment_number: 20,
3528 },
3529 OwnedSegment {
3530 id: "NAD".into(),
3531 elements: vec![vec!["Z09".into()]],
3532 segment_number: 21,
3533 },
3534 ];
3535
3536 let report = validator.validate_with_navigator(
3537 &all_segments,
3538 &workflow,
3539 &external,
3540 ValidationLevel::Conditions,
3541 &nav,
3542 );
3543
3544 let code_errors: Vec<_> = report
3545 .by_category(ValidationCategory::Code)
3546 .filter(|i| i.severity == Severity::Error)
3547 .collect();
3548
3549 assert_eq!(
3552 code_errors.len(),
3553 1,
3554 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
3555 code_errors
3556 );
3557 assert!(narrate(code_errors[0]).contains("MT"));
3558 assert!(narrate(code_errors[0]).contains("MR"));
3560 assert!(narrate(code_errors[0]).contains("MS"));
3561 assert!(
3562 !narrate(code_errors[0]).contains("Z04"),
3563 "SG4/SG12 codes should not leak into SG2 error"
3564 );
3565 assert!(
3567 code_errors[0]
3568 .field_path
3569 .as_deref()
3570 .unwrap_or("")
3571 .contains("SG2"),
3572 "Error field_path should reference SG2, got: {:?}",
3573 code_errors[0].field_path
3574 );
3575 }
3576
3577 #[test]
3580 fn test_package_cardinality_within_bounds() {
3581 let evaluator = MockEvaluator::all_true(&[]);
3583 let validator = EdifactValidator::new(evaluator);
3584 let external = NoOpExternalProvider;
3585
3586 let segments = vec![OwnedSegment {
3587 id: "STS".into(),
3588 elements: vec![
3589 vec!["Z33".into()], vec![], vec!["E01".into()], ],
3593 segment_number: 5,
3594 }];
3595
3596 let workflow = AhbWorkflow {
3597 pruefidentifikator: "13017".to_string(),
3598 description: "Test".to_string(),
3599 communication_direction: None,
3600 ub_definitions: BTreeMap::new(),
3601 fields: vec![AhbFieldRule {
3602 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3603 name: "Statusanlaß, Code".to_string(),
3604 ahb_status: "X".to_string(),
3605 element_index: Some(2),
3606 component_index: Some(0),
3607 codes: vec![
3608 AhbCodeRule {
3609 value: "E01".into(),
3610 description: "Code 1".into(),
3611 ahb_status: "X [4P0..1]".into(),
3612 },
3613 AhbCodeRule {
3614 value: "E02".into(),
3615 description: "Code 2".into(),
3616 ahb_status: "X [4P0..1]".into(),
3617 },
3618 ],
3619 parent_group_ahb_status: Some("Muss".to_string()),
3620 segment_ahb_status: None,
3621 mig_number: None,
3622 }],
3623 };
3624
3625 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3626 let pkg_errors: Vec<_> = report
3627 .by_category(ValidationCategory::Ahb)
3628 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3629 .collect();
3630 assert!(
3631 pkg_errors.is_empty(),
3632 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
3633 pkg_errors
3634 );
3635 }
3636
3637 #[test]
3638 fn test_package_cardinality_zero_present_min_zero() {
3639 let evaluator = MockEvaluator::all_true(&[]);
3641 let validator = EdifactValidator::new(evaluator);
3642 let external = NoOpExternalProvider;
3643
3644 let segments = vec![OwnedSegment {
3645 id: "STS".into(),
3646 elements: vec![
3647 vec!["Z33".into()],
3648 vec![],
3649 vec!["X99".into()], ],
3651 segment_number: 5,
3652 }];
3653
3654 let workflow = AhbWorkflow {
3655 pruefidentifikator: "13017".to_string(),
3656 description: "Test".to_string(),
3657 communication_direction: None,
3658 ub_definitions: BTreeMap::new(),
3659 fields: vec![AhbFieldRule {
3660 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3661 name: "Statusanlaß, Code".to_string(),
3662 ahb_status: "X".to_string(),
3663 element_index: Some(2),
3664 component_index: Some(0),
3665 codes: vec![
3666 AhbCodeRule {
3667 value: "E01".into(),
3668 description: "Code 1".into(),
3669 ahb_status: "X [4P0..1]".into(),
3670 },
3671 AhbCodeRule {
3672 value: "E02".into(),
3673 description: "Code 2".into(),
3674 ahb_status: "X [4P0..1]".into(),
3675 },
3676 ],
3677 parent_group_ahb_status: Some("Muss".to_string()),
3678 segment_ahb_status: None,
3679 mig_number: None,
3680 }],
3681 };
3682
3683 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3684 let pkg_errors: Vec<_> = report
3685 .by_category(ValidationCategory::Ahb)
3686 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3687 .collect();
3688 assert!(
3689 pkg_errors.is_empty(),
3690 "0 codes, min=0 — no error expected, got: {:?}",
3691 pkg_errors
3692 );
3693 }
3694
3695 #[test]
3696 fn test_package_cardinality_too_many() {
3697 let evaluator = MockEvaluator::all_true(&[]);
3699 let validator = EdifactValidator::new(evaluator);
3700 let external = NoOpExternalProvider;
3701
3702 let segments = vec![
3704 OwnedSegment {
3705 id: "STS".into(),
3706 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
3707 segment_number: 5,
3708 },
3709 OwnedSegment {
3710 id: "STS".into(),
3711 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
3712 segment_number: 6,
3713 },
3714 ];
3715
3716 let workflow = AhbWorkflow {
3717 pruefidentifikator: "13017".to_string(),
3718 description: "Test".to_string(),
3719 communication_direction: None,
3720 ub_definitions: BTreeMap::new(),
3721 fields: vec![AhbFieldRule {
3722 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3723 name: "Statusanlaß, Code".to_string(),
3724 ahb_status: "X".to_string(),
3725 element_index: Some(2),
3726 component_index: Some(0),
3727 codes: vec![
3728 AhbCodeRule {
3729 value: "E01".into(),
3730 description: "Code 1".into(),
3731 ahb_status: "X [4P0..1]".into(),
3732 },
3733 AhbCodeRule {
3734 value: "E02".into(),
3735 description: "Code 2".into(),
3736 ahb_status: "X [4P0..1]".into(),
3737 },
3738 ],
3739 parent_group_ahb_status: Some("Muss".to_string()),
3740 segment_ahb_status: None,
3741 mig_number: None,
3742 }],
3743 };
3744
3745 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3746 let pkg_errors: Vec<_> = report
3747 .by_category(ValidationCategory::Ahb)
3748 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3749 .collect();
3750 assert_eq!(
3751 pkg_errors.len(),
3752 1,
3753 "2 codes present, max=1 — expected 1 error, got: {:?}",
3754 pkg_errors
3755 );
3756 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
3757 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
3758 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
3759 }
3760
3761 #[test]
3762 fn test_package_cardinality_too_few() {
3763 let evaluator = MockEvaluator::all_true(&[]);
3765 let validator = EdifactValidator::new(evaluator);
3766 let external = NoOpExternalProvider;
3767
3768 let segments = vec![OwnedSegment {
3769 id: "STS".into(),
3770 elements: vec![
3771 vec!["Z33".into()],
3772 vec![],
3773 vec!["X99".into()], ],
3775 segment_number: 5,
3776 }];
3777
3778 let workflow = AhbWorkflow {
3779 pruefidentifikator: "13017".to_string(),
3780 description: "Test".to_string(),
3781 communication_direction: None,
3782 ub_definitions: BTreeMap::new(),
3783 fields: vec![AhbFieldRule {
3784 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3785 name: "Statusanlaß, Code".to_string(),
3786 ahb_status: "X".to_string(),
3787 element_index: Some(2),
3788 component_index: Some(0),
3789 codes: vec![
3790 AhbCodeRule {
3791 value: "E01".into(),
3792 description: "Code 1".into(),
3793 ahb_status: "X [5P1..3]".into(),
3794 },
3795 AhbCodeRule {
3796 value: "E02".into(),
3797 description: "Code 2".into(),
3798 ahb_status: "X [5P1..3]".into(),
3799 },
3800 AhbCodeRule {
3801 value: "E03".into(),
3802 description: "Code 3".into(),
3803 ahb_status: "X [5P1..3]".into(),
3804 },
3805 ],
3806 parent_group_ahb_status: Some("Muss".to_string()),
3807 segment_ahb_status: None,
3808 mig_number: None,
3809 }],
3810 };
3811
3812 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3813 let pkg_errors: Vec<_> = report
3814 .by_category(ValidationCategory::Ahb)
3815 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3816 .collect();
3817 assert_eq!(
3818 pkg_errors.len(),
3819 1,
3820 "0 codes present, min=1 — expected 1 error, got: {:?}",
3821 pkg_errors
3822 );
3823 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
3824 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
3825 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
3826 }
3827
3828 #[test]
3829 fn test_package_cardinality_no_packages_in_workflow() {
3830 let evaluator = MockEvaluator::all_true(&[]);
3832 let validator = EdifactValidator::new(evaluator);
3833 let external = NoOpExternalProvider;
3834
3835 let segments = vec![OwnedSegment {
3836 id: "STS".into(),
3837 elements: vec![vec!["E01".into()]],
3838 segment_number: 5,
3839 }];
3840
3841 let workflow = AhbWorkflow {
3842 pruefidentifikator: "13017".to_string(),
3843 description: "Test".to_string(),
3844 communication_direction: None,
3845 ub_definitions: BTreeMap::new(),
3846 fields: vec![AhbFieldRule {
3847 segment_path: "STS/9015".to_string(),
3848 name: "Status Code".to_string(),
3849 ahb_status: "X".to_string(),
3850 codes: vec![AhbCodeRule {
3851 value: "E01".into(),
3852 description: "Code 1".into(),
3853 ahb_status: "X".into(),
3854 }],
3855 parent_group_ahb_status: Some("Muss".to_string()),
3856 segment_ahb_status: None,
3857 ..Default::default()
3858 }],
3859 };
3860
3861 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3862 let pkg_errors: Vec<_> = report
3863 .by_category(ValidationCategory::Ahb)
3864 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3865 .collect();
3866 assert!(
3867 pkg_errors.is_empty(),
3868 "No packages in workflow — no errors expected"
3869 );
3870 }
3871
3872 #[test]
3873 fn test_package_cardinality_with_condition_and_package() {
3874 let evaluator = MockEvaluator::all_true(&[901]);
3876 let validator = EdifactValidator::new(evaluator);
3877 let external = NoOpExternalProvider;
3878
3879 let segments = vec![OwnedSegment {
3880 id: "STS".into(),
3881 elements: vec![vec![], vec![], vec!["E01".into()]],
3882 segment_number: 5,
3883 }];
3884
3885 let workflow = AhbWorkflow {
3886 pruefidentifikator: "13017".to_string(),
3887 description: "Test".to_string(),
3888 communication_direction: None,
3889 ub_definitions: BTreeMap::new(),
3890 fields: vec![AhbFieldRule {
3891 segment_path: "SG10/STS/C556/9013".to_string(),
3892 name: "Code".to_string(),
3893 ahb_status: "X".to_string(),
3894 element_index: Some(2),
3895 component_index: Some(0),
3896 codes: vec![
3897 AhbCodeRule {
3898 value: "E01".into(),
3899 description: "Code 1".into(),
3900 ahb_status: "X [901] [4P0..1]".into(),
3901 },
3902 AhbCodeRule {
3903 value: "E02".into(),
3904 description: "Code 2".into(),
3905 ahb_status: "X [901] [4P0..1]".into(),
3906 },
3907 ],
3908 parent_group_ahb_status: Some("Muss".to_string()),
3909 segment_ahb_status: None,
3910 mig_number: None,
3911 }],
3912 };
3913
3914 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3915 let pkg_errors: Vec<_> = report
3916 .by_category(ValidationCategory::Ahb)
3917 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3918 .collect();
3919 assert!(
3920 pkg_errors.is_empty(),
3921 "1 code within [4P0..1] bounds — no error, got: {:?}",
3922 pkg_errors
3923 );
3924 }
3925
3926 #[test]
3927 fn test_package_cardinality_scoped_per_group_instance() {
3928 use mig_types::navigator::GroupNavigator;
3936
3937 struct TwoSg10s {
3938 sts_a: OwnedSegment,
3939 sts_b: OwnedSegment,
3940 }
3941 impl GroupNavigator for TwoSg10s {
3942 fn find_segments_in_group(
3943 &self,
3944 segment_id: &str,
3945 group_path: &[&str],
3946 instance_index: usize,
3947 ) -> Vec<OwnedSegment> {
3948 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
3949 match instance_index {
3950 0 => vec![self.sts_a.clone()],
3951 1 => vec![self.sts_b.clone()],
3952 _ => vec![],
3953 }
3954 } else {
3955 vec![]
3956 }
3957 }
3958 fn find_segments_with_qualifier_in_group(
3959 &self,
3960 _: &str,
3961 _: usize,
3962 _: &str,
3963 _: &[&str],
3964 _: usize,
3965 ) -> Vec<OwnedSegment> {
3966 vec![]
3967 }
3968 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3969 match group_path {
3970 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
3971 ["SG5", "SG6", "SG9", "SG10"] => 2,
3972 _ => 0,
3973 }
3974 }
3975 }
3976
3977 let sts_a = OwnedSegment {
3978 id: "STS".into(),
3979 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
3980 segment_number: 10,
3981 };
3982 let sts_b = OwnedSegment {
3983 id: "STS".into(),
3984 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
3985 segment_number: 15,
3986 };
3987 let nav = TwoSg10s {
3988 sts_a: sts_a.clone(),
3989 sts_b: sts_b.clone(),
3990 };
3991
3992 let evaluator = MockEvaluator::all_true(&[]);
3993 let validator = EdifactValidator::new(evaluator);
3994 let external = NoOpExternalProvider;
3995
3996 let workflow = AhbWorkflow {
3997 pruefidentifikator: "13025".to_string(),
3998 description: "Test".to_string(),
3999 communication_direction: None,
4000 ub_definitions: BTreeMap::new(),
4001 fields: vec![AhbFieldRule {
4002 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4003 name: "Statusanlaß, Code".to_string(),
4004 ahb_status: "X".to_string(),
4005 element_index: Some(2),
4006 component_index: Some(0),
4007 codes: vec![
4008 AhbCodeRule {
4009 value: "E01".into(),
4010 description: "Code 1".into(),
4011 ahb_status: "X [4P0..1]".into(),
4012 },
4013 AhbCodeRule {
4014 value: "E02".into(),
4015 description: "Code 2".into(),
4016 ahb_status: "X [4P0..1]".into(),
4017 },
4018 ],
4019 parent_group_ahb_status: Some("Muss".to_string()),
4020 segment_ahb_status: None,
4021 mig_number: None,
4022 }],
4023 };
4024
4025 let report = validator.validate_with_navigator(
4026 &[sts_a, sts_b],
4027 &workflow,
4028 &external,
4029 ValidationLevel::Full,
4030 &nav,
4031 );
4032 let pkg_errors: Vec<_> = report
4033 .by_category(ValidationCategory::Ahb)
4034 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4035 .collect();
4036 assert!(
4037 pkg_errors.is_empty(),
4038 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
4039 pkg_errors
4040 );
4041 }
4042
4043 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
4044 OwnedSegment {
4045 id: id.to_string(),
4046 elements: elements
4047 .into_iter()
4048 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
4049 .collect(),
4050 segment_number: 0,
4051 }
4052 }
4053
4054 #[test]
4065 fn test_package_cardinality_scoped_to_rule_mig_variant() {
4066 use mig_types::navigator::GroupNavigator;
4067
4068 struct TwoSg8Variants {
4069 seq_z01: OwnedSegment,
4070 seq_z45: OwnedSegment,
4071 }
4072 impl GroupNavigator for TwoSg8Variants {
4073 fn find_segments_in_group(
4074 &self,
4075 segment_id: &str,
4076 group_path: &[&str],
4077 instance_index: usize,
4078 ) -> Vec<OwnedSegment> {
4079 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
4080 match instance_index {
4081 0 => vec![self.seq_z01.clone()],
4082 1 => vec![self.seq_z45.clone()],
4083 _ => vec![],
4084 }
4085 } else {
4086 vec![]
4087 }
4088 }
4089 fn find_segments_with_qualifier_in_group(
4090 &self,
4091 _: &str,
4092 _: usize,
4093 _: &str,
4094 _: &[&str],
4095 _: usize,
4096 ) -> Vec<OwnedSegment> {
4097 vec![]
4098 }
4099 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4100 match group_path {
4101 ["SG4"] => 1,
4102 ["SG4", "SG8"] => 2,
4103 _ => 0,
4104 }
4105 }
4106 fn instance_has_mig_number(
4107 &self,
4108 group_path: &[&str],
4109 instance_index: usize,
4110 mig_number: &str,
4111 ) -> bool {
4112 if group_path != ["SG4", "SG8"] {
4113 return true;
4114 }
4115 match (instance_index, mig_number) {
4116 (0, "00115") => true,
4117 (0, _) => false,
4118 (1, "00171") => true,
4119 (1, _) => false,
4120 _ => false,
4121 }
4122 }
4123 }
4124
4125 let seq_z01 = OwnedSegment {
4126 id: "SEQ".into(),
4127 elements: vec![vec!["Z01".into()], vec!["1".into()]],
4128 segment_number: 10,
4129 };
4130 let seq_z45 = OwnedSegment {
4131 id: "SEQ".into(),
4132 elements: vec![vec!["Z45".into()], vec!["1".into()]],
4133 segment_number: 20,
4134 };
4135 let nav = TwoSg8Variants {
4136 seq_z01: seq_z01.clone(),
4137 seq_z45: seq_z45.clone(),
4138 };
4139
4140 let evaluator = MockEvaluator::all_true(&[]);
4141 let validator = EdifactValidator::new(evaluator);
4142 let external = NoOpExternalProvider;
4143
4144 let workflow = AhbWorkflow {
4145 pruefidentifikator: "55218".to_string(),
4146 description: "Test".to_string(),
4147 communication_direction: None,
4148 ub_definitions: BTreeMap::new(),
4149 fields: vec![AhbFieldRule {
4150 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4151 name: "Handlung, Code".to_string(),
4152 ahb_status: "X".to_string(),
4153 element_index: Some(0),
4154 component_index: Some(0),
4155 codes: vec![
4156 AhbCodeRule {
4157 value: "Z45".into(),
4158 description: "NNA".into(),
4159 ahb_status: "X [1P1..4294967295]".into(),
4160 },
4161 AhbCodeRule {
4162 value: "Z84".into(),
4163 description: "Differenz-NNA".into(),
4164 ahb_status: "X [1P0..4294967295]".into(),
4165 },
4166 ],
4167 parent_group_ahb_status: Some("Muss".to_string()),
4168 segment_ahb_status: None,
4169 mig_number: Some("00171".to_string()),
4170 }],
4171 };
4172
4173 let report = validator.validate_with_navigator(
4174 &[seq_z01, seq_z45],
4175 &workflow,
4176 &external,
4177 ValidationLevel::Full,
4178 &nav,
4179 );
4180 let pkg_errors: Vec<_> = report
4181 .by_category(ValidationCategory::Ahb)
4182 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4183 .collect();
4184 assert!(
4185 pkg_errors.is_empty(),
4186 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
4187 pkg_errors
4188 );
4189 }
4190
4191 #[test]
4192 fn test_unt_count_correct() {
4193 let segments = vec![
4195 make_segment("UNH", vec![vec!["001"]]),
4196 make_segment("BGM", vec![vec!["E01"]]),
4197 make_segment("DTM", vec![vec!["137", "20250401"]]),
4198 make_segment("UNT", vec![vec!["4", "001"]]),
4199 ];
4200 assert!(
4201 validate_unt_segment_count(&segments).is_none(),
4202 "Correct count should produce no issue"
4203 );
4204 }
4205
4206 #[test]
4207 fn test_unt_count_mismatch() {
4208 let segments = vec![
4210 make_segment("UNH", vec![vec!["001"]]),
4211 make_segment("BGM", vec![vec!["E01"]]),
4212 make_segment("UNT", vec![vec!["5", "001"]]),
4213 ];
4214 let issue =
4215 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
4216 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
4217 assert_eq!(issue.severity, Severity::Error);
4218 assert!(narrate(&issue).contains("declared 5"));
4219 assert!(narrate(&issue).contains("actual 3"));
4220 }
4221
4222 #[test]
4223 fn test_unt_count_excludes_envelope() {
4224 let segments = vec![
4226 make_segment("UNA", vec![]),
4227 make_segment("UNB", vec![vec!["UNOC", "3"]]),
4228 make_segment("UNH", vec![vec!["001"]]),
4229 make_segment("BGM", vec![vec!["E01"]]),
4230 make_segment("UNT", vec![vec!["3", "001"]]),
4231 make_segment("UNZ", vec![vec!["1"]]),
4232 ];
4233 assert!(
4234 validate_unt_segment_count(&segments).is_none(),
4235 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
4236 );
4237 }
4238
4239 #[test]
4240 fn test_unt_count_no_unt_returns_none() {
4241 let segments = vec![
4242 make_segment("UNH", vec![vec!["001"]]),
4243 make_segment("BGM", vec![vec!["E01"]]),
4244 ];
4245 assert!(
4246 validate_unt_segment_count(&segments).is_none(),
4247 "No UNT segment should return None (not our problem)"
4248 );
4249 }
4250
4251 #[test]
4252 fn test_unt_count_rejects_multi_message_input() {
4253 let segments = vec![
4255 make_segment("UNH", vec![vec!["001"]]),
4256 make_segment("BGM", vec![vec!["E01"]]),
4257 make_segment("UNT", vec![vec!["3", "001"]]),
4258 make_segment("UNH", vec![vec!["002"]]),
4259 make_segment("BGM", vec![vec!["E02"]]),
4260 make_segment("UNT", vec![vec!["3", "002"]]),
4261 ];
4262 let issue = validate_unt_segment_count(&segments)
4263 .expect("Multi-message input should produce an error");
4264 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
4265 assert!(
4266 narrate(&issue).contains("2 UNH"),
4267 "Should mention UNH count: {}",
4268 narrate(&issue)
4269 );
4270 }
4271
4272 #[test]
4273 fn test_code_validation_accepts_multi_code_variant_qualifier() {
4274 let evaluator = MockEvaluator::new(vec![]);
4281 let validator = EdifactValidator::new(evaluator);
4282 let external = NoOpExternalProvider;
4283
4284 let rff_z39 = OwnedSegment {
4285 id: "RFF".to_string(),
4286 elements: vec![
4287 vec!["RFF".to_string()],
4288 vec!["Z39".to_string(), "REF1".to_string()],
4289 ],
4290 segment_number: 1,
4291 };
4292
4293 let workflow = AhbWorkflow {
4294 pruefidentifikator: "55035".to_string(),
4295 description: "Test".to_string(),
4296 communication_direction: None,
4297 fields: vec![
4298 AhbFieldRule {
4300 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
4301 name: "Referenznummer Qualifier".to_string(),
4302 ahb_status: "Muss".to_string(),
4303 codes: vec![
4304 AhbCodeRule {
4305 value: "Z31".to_string(),
4306 description: "".to_string(),
4307 ahb_status: "X".to_string(),
4308 },
4309 AhbCodeRule {
4310 value: "Z39".to_string(),
4311 description: "".to_string(),
4312 ahb_status: "X".to_string(),
4313 },
4314 ],
4315 parent_group_ahb_status: None,
4316 segment_ahb_status: None,
4317 element_index: Some(1),
4318 component_index: Some(0),
4319 mig_number: Some("00075".to_string()),
4320 },
4321 AhbFieldRule {
4323 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
4324 name: "Referenznummer Qualifier".to_string(),
4325 ahb_status: "Muss".to_string(),
4326 codes: vec![AhbCodeRule {
4327 value: "Z33".to_string(),
4328 description: "".to_string(),
4329 ahb_status: "X".to_string(),
4330 }],
4331 parent_group_ahb_status: None,
4332 segment_ahb_status: None,
4333 element_index: Some(1),
4334 component_index: Some(0),
4335 mig_number: Some("00078".to_string()),
4336 },
4337 ],
4338 ub_definitions: BTreeMap::new(),
4339 };
4340
4341 let report = validator.validate(
4342 &[rff_z39],
4343 &workflow,
4344 &external,
4345 ValidationLevel::Conditions,
4346 );
4347
4348 let code_errors: Vec<_> = report
4349 .by_category(ValidationCategory::Code)
4350 .filter(|i| {
4351 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4352 })
4353 .collect();
4354 assert!(
4355 code_errors.is_empty(),
4356 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
4357 code_errors
4358 );
4359 }
4360
4361 #[test]
4362 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
4363 let evaluator = MockEvaluator::new(vec![]);
4369 let validator = EdifactValidator::new(evaluator);
4370 let external = NoOpExternalProvider;
4371
4372 let pia_5_z12 = OwnedSegment {
4376 id: "PIA".to_string(),
4377 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
4378 segment_number: 1,
4379 };
4380 let pia_5_srw = OwnedSegment {
4381 id: "PIA".to_string(),
4382 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
4383 segment_number: 2,
4384 };
4385
4386 let make_rules = |mig: &str, composite_code: &str| {
4387 vec![
4388 AhbFieldRule {
4389 segment_path: "SG4/SG8/PIA/4347".to_string(),
4390 name: "Produkt-ID-Funktion".to_string(),
4391 ahb_status: "Muss".to_string(),
4392 codes: vec![AhbCodeRule {
4393 value: "5".to_string(),
4394 description: "".to_string(),
4395 ahb_status: "X".to_string(),
4396 }],
4397 parent_group_ahb_status: None,
4398 segment_ahb_status: None,
4399 element_index: Some(0),
4400 component_index: Some(0),
4401 mig_number: Some(mig.to_string()),
4402 },
4403 AhbFieldRule {
4404 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
4405 name: "Artikel/Dienstleistung-Identifikator".to_string(),
4406 ahb_status: "Muss".to_string(),
4407 codes: vec![AhbCodeRule {
4408 value: composite_code.to_string(),
4409 description: "".to_string(),
4410 ahb_status: "X".to_string(),
4411 }],
4412 parent_group_ahb_status: None,
4413 segment_ahb_status: None,
4414 element_index: Some(1),
4415 component_index: Some(0),
4416 mig_number: Some(mig.to_string()),
4417 },
4418 ]
4419 };
4420
4421 let mut fields = make_rules("00108", "Z12");
4422 fields.extend(make_rules("00197", "SRW"));
4423
4424 let workflow = AhbWorkflow {
4425 pruefidentifikator: "55035".to_string(),
4426 description: "Test".to_string(),
4427 communication_direction: None,
4428 fields,
4429 ub_definitions: BTreeMap::new(),
4430 };
4431
4432 let report = validator.validate(
4433 &[pia_5_z12, pia_5_srw],
4434 &workflow,
4435 &external,
4436 ValidationLevel::Conditions,
4437 );
4438
4439 let code_errors: Vec<_> = report
4440 .by_category(ValidationCategory::Code)
4441 .filter(|i| {
4442 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4443 })
4444 .collect();
4445 assert!(
4446 code_errors.is_empty(),
4447 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
4448 code_errors
4449 );
4450 }
4451}