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