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