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