1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
4
5use crate::expr::{ConditionExpr, ConditionParser, StatusKind};
6
7use crate::eval::answer_lists::{answer_path, is_answer_list_id};
8use crate::eval::{
9 AnswerCodeLists, ConditionEvaluator, ConditionExprEvaluator, ConditionResult,
10 EvaluationContext, ExternalConditionProvider, GroupScope, StatusResolution,
11};
12use mig_assembly::assembler::AssembledTree;
13use mig_assembly::navigator::AssembledTreeNavigator;
14use mig_types::navigator::GroupNavigator;
15use mig_types::segment::OwnedSegment;
16
17use super::tree::{AhbGroupNode, AhbNode, ValidatedTree};
18
19use super::issue::{Severity, ValidationIssue};
20use super::level::ValidationLevel;
21use super::report::ValidationReport;
22use crate::{IssueKind, UnresolvedConditions};
23
24pub use ahb_types::{AhbCodeRule, AhbFieldRule, AhbWorkflow};
30
31pub struct EdifactValidator<E: ConditionEvaluator> {
64 evaluator: E,
65 report_segments: bool,
66}
67
68impl<E: ConditionEvaluator> EdifactValidator<E> {
69 pub fn new(evaluator: E) -> Self {
71 Self {
72 evaluator,
73 report_segments: false,
74 }
75 }
76
77 pub fn reporting_segments(mut self) -> Self {
83 self.report_segments = true;
84 self
85 }
86
87 pub fn validate(
100 &self,
101 segments: &[OwnedSegment],
102 workflow: &AhbWorkflow,
103 external: &dyn ExternalConditionProvider,
104 level: ValidationLevel,
105 ) -> ValidationReport {
106 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
107 .with_format_version(self.evaluator.format_version())
108 .with_pruefidentifikator(&workflow.pruefidentifikator);
109
110 let ctx = EvaluationContext::new(&workflow.pruefidentifikator, external, segments);
111
112 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
113 self.validate_conditions(workflow, &ctx, &mut report);
114 }
115
116 report
117 }
118
119 pub fn validate_with_navigator(
125 &self,
126 segments: &[OwnedSegment],
127 workflow: &AhbWorkflow,
128 external: &dyn ExternalConditionProvider,
129 level: ValidationLevel,
130 navigator: &dyn GroupNavigator,
131 ) -> ValidationReport {
132 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
133 .with_format_version(self.evaluator.format_version())
134 .with_pruefidentifikator(&workflow.pruefidentifikator);
135
136 let ctx = EvaluationContext::with_navigator(
137 &workflow.pruefidentifikator,
138 external,
139 segments,
140 navigator,
141 );
142
143 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
144 self.validate_conditions(workflow, &ctx, &mut report);
145 }
146
147 report
148 }
149
150 pub fn validate_tree(
157 &self,
158 validated_tree: &ValidatedTree,
159 segments: &[OwnedSegment],
160 external: &dyn ExternalConditionProvider,
161 level: ValidationLevel,
162 navigator: Option<&dyn GroupNavigator>,
163 ) -> ValidationReport {
164 self.validate_tree_in(validated_tree, segments, external, level, navigator, None)
165 }
166
167 pub fn validate_assembled_tree(
174 &self,
175 validated_tree: &ValidatedTree,
176 segments: &[OwnedSegment],
177 external: &dyn ExternalConditionProvider,
178 level: ValidationLevel,
179 tree: &AssembledTree,
180 ) -> ValidationReport {
181 let navigator = AssembledTreeNavigator::new(tree);
182 self.validate_tree_in(
183 validated_tree,
184 segments,
185 external,
186 level,
187 Some(&navigator),
188 Some(tree),
189 )
190 }
191
192 fn validate_tree_in(
193 &self,
194 validated_tree: &ValidatedTree,
195 segments: &[OwnedSegment],
196 external: &dyn ExternalConditionProvider,
197 level: ValidationLevel,
198 navigator: Option<&dyn GroupNavigator>,
199 tree: Option<&AssembledTree>,
200 ) -> ValidationReport {
201 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
202 .with_format_version(self.evaluator.format_version())
203 .with_pruefidentifikator(validated_tree.pruefidentifikator);
204
205 if !matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
206 return report;
207 }
208
209 let ctx = match navigator {
210 Some(nav) => EvaluationContext::with_navigator(
211 validated_tree.pruefidentifikator,
212 external,
213 segments,
214 nav,
215 ),
216 None => EvaluationContext::new(validated_tree.pruefidentifikator, external, segments),
217 };
218
219 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
220
221 let mut all_nodes: Vec<&AhbNode> = Vec::new();
224 all_nodes.extend(validated_tree.root_fields.iter());
225 for group in &validated_tree.groups {
226 collect_nodes_depth_first(group, &mut all_nodes);
227 }
228
229 let mut tag_migs: HashMap<String, HashSet<&str>> = HashMap::new();
236 for node in &all_nodes {
237 if let Some(ref m) = node.rule.mig_number {
238 tag_migs
239 .entry(extract_segment_id(&node.rule.segment_path))
240 .or_default()
241 .insert(m.as_str());
242 }
243 }
244 for rule in &validated_tree.unmatched_rules {
245 if let Some(ref m) = rule.mig_number {
246 tag_migs
247 .entry(extract_segment_id(&rule.segment_path))
248 .or_default()
249 .insert(m.as_str());
250 }
251 }
252
253 for node in &validated_tree.root_fields {
255 evaluate_node(
256 node,
257 &ctx,
258 &expr_eval,
259 &self.evaluator,
260 validated_tree.ub_definitions,
261 &tag_migs,
262 None,
263 true,
264 &mut report,
265 );
266 }
267
268 if self.report_segments {
269 refuse_segments(
270 &validated_tree.root_fields,
271 None,
272 &[],
273 &ctx,
274 &self.evaluator,
275 validated_tree.ub_definitions,
276 0,
277 &mut report,
278 );
279 }
280
281 let transaction = crate::eval::transaction_group(self.evaluator.message_type());
286 let absent = crate::eval::AbsentTarget(&self.evaluator);
287 let absent_eval = ConditionExprEvaluator::new(&absent);
288 let mut per_transaction: HashSet<&str> = HashSet::new();
292 let mut instance_counter: HashMap<&str, usize> = HashMap::new();
293 for group in &validated_tree.groups {
294 let instance_index = *instance_counter
295 .entry(group.group_id)
296 .and_modify(|c| *c += 1)
297 .or_insert(0);
298
299 let view = tree
302 .filter(|_| transaction == Some(group.group_id))
303 .and_then(|t| t.transaction_view(group.group_id, instance_index));
304 if let Some(view) = view {
305 let first_new = report.issues.len();
306 let view_segments: Vec<OwnedSegment> = segments
307 .iter()
308 .filter(|s| view.left_out.binary_search(&s.segment_number).is_err())
309 .cloned()
310 .collect();
311 let view_navigator = AssembledTreeNavigator::new(&view.tree);
312 let view_ctx = ctx.with_view(&view_segments, &view_navigator);
313 let path_storage = [(group.group_id, 0)];
314 let scoped_ctx = view_ctx.with_scope(GroupScope {
315 path: &path_storage,
316 });
317 walk_group_instance(
318 group,
319 &path_storage,
320 &scoped_ctx,
321 &expr_eval,
322 &self.evaluator,
323 validated_tree.ub_definitions,
324 &tag_migs,
325 instance_index,
326 self.report_segments,
327 &mut report,
328 );
329 report_absent_children(
330 group,
331 &path_storage,
332 &scoped_ctx,
333 &absent_eval,
334 &self.evaluator,
335 validated_tree.ub_definitions,
336 instance_index,
337 &mut report,
338 );
339 for issue in &mut report.issues[first_new..] {
340 issue.transaction_index = Some(instance_index);
341 }
342 per_transaction.extend(
343 group
344 .variant_rules
345 .iter()
346 .filter_map(|r| r.mig_number.as_deref()),
347 );
348 continue;
349 }
350
351 let path_storage = [(group.group_id, instance_index)];
352 let scoped_ctx = ctx.with_scope(GroupScope {
355 path: &path_storage,
356 });
357
358 walk_group_instance(
359 group,
360 &path_storage,
361 &scoped_ctx,
362 &expr_eval,
363 &self.evaluator,
364 validated_tree.ub_definitions,
365 &tag_migs,
366 instance_index,
367 self.report_segments,
368 &mut report,
369 );
370 }
371
372 if let Some((once, transaction)) =
374 crate::eval::once_per_transaction(self.evaluator.message_type())
375 {
376 let mut index: HashMap<&str, usize> = HashMap::new();
377 for group in &validated_tree.groups {
378 let i = *index
379 .entry(group.group_id)
380 .and_modify(|c| *c += 1)
381 .or_insert(0);
382 if group.group_id == transaction {
383 let first_new = report.issues.len();
384 report_repeated_in_transaction(group, i, once, &ctx, &mut report);
385 for issue in &mut report.issues[first_new..] {
386 issue.transaction_index = Some(i);
387 }
388 }
389 }
390 }
391
392 for field in &validated_tree.unmatched_rules {
402 if field
403 .mig_number
404 .as_deref()
405 .is_some_and(|n| per_transaction.contains(n))
406 {
407 continue;
408 }
409 if should_skip_for_parent_group(
410 field,
411 &absent_eval,
412 &ctx,
413 validated_tree.ub_definitions,
414 ) {
415 continue;
416 }
417
418 let status =
419 absent_eval.resolve_status(&field.ahb_status, &ctx, validated_tree.ub_definitions);
420
421 if status.result == ConditionResult::True
422 && status.kind == Some(StatusKind::Muss)
423 && !is_field_present(&ctx, field)
424 && !is_group_variant_absent(
425 &ctx,
426 field,
427 is_parent_group_required(
428 field,
429 &absent_eval,
430 &ctx,
431 validated_tree.ub_definitions,
432 ),
433 )
434 {
435 let mut issue = ValidationIssue::new(
436 Severity::Error,
437 IssueKind::MissingRequiredField {
438 field_name: field.name.clone(),
439 },
440 )
441 .with_field_path(&field.segment_path)
442 .with_rule(&field.ahb_status);
443 if let Some(first_code) = field.codes.first() {
444 issue.expected_value = Some(first_code.value.clone());
445 }
446 issue = attach_field_position(issue, field);
447 report.add_issue(issue);
448 }
449 }
450
451 if matches!(level, ValidationLevel::Full) {
455 let mut all_fields: Vec<AhbFieldRule> = Vec::new();
456 for node in &all_nodes {
457 all_fields.push(node.rule.clone());
458 }
459 for rule in &validated_tree.unmatched_rules {
460 all_fields.push((*rule).clone());
461 }
462 let synthetic_workflow = AhbWorkflow {
463 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
464 description: String::new(),
465 communication_direction: None,
466 fields: all_fields,
467 ub_definitions: validated_tree.ub_definitions.clone(),
468 package_conditions: validated_tree.package_conditions.clone(),
469 };
470 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
471 self.validate_answer_lists(&synthetic_workflow, &ctx, &mut report);
472 self.validate_package_cardinality(&synthetic_workflow, &ctx, &mut report);
473 }
474
475 report
476 }
477
478 fn validate_conditions(
480 &self,
481 workflow: &AhbWorkflow,
482 ctx: &EvaluationContext,
483 report: &mut ValidationReport,
484 ) {
485 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
486
487 for field in &workflow.fields {
488 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
490 continue;
491 }
492
493 let StatusResolution {
496 result: condition_result,
497 kind,
498 unknown_ids,
499 } = expr_eval.resolve_status(&field.ahb_status, ctx, &workflow.ub_definitions);
500 let mandatory = kind == Some(StatusKind::Muss);
501
502 match condition_result {
503 ConditionResult::True => {
504 if mandatory
506 && !is_field_present(ctx, field)
507 && !is_group_variant_absent(
508 ctx,
509 field,
510 is_parent_group_required(
511 field,
512 &expr_eval,
513 ctx,
514 &workflow.ub_definitions,
515 ),
516 )
517 {
518 let mut issue = ValidationIssue::new(
519 Severity::Error,
520 IssueKind::MissingRequiredField {
521 field_name: field.name.clone(),
522 },
523 )
524 .with_field_path(&field.segment_path)
525 .with_rule(&field.ahb_status)
526 .with_mig_number(field.mig_number.as_deref());
527 if let Some(first_code) = field.codes.first() {
531 issue.expected_value = Some(first_code.value.clone());
532 }
533 report.add_issue(issue);
534 }
535 }
536 ConditionResult::False => {
537 if mandatory && is_field_present(ctx, field) {
543 let failed = expr_eval.false_ids_with_ub(
544 &field.ahb_status,
545 ctx,
546 &workflow.ub_definitions,
547 );
548 report.add_issue(
549 ValidationIssue::new(
550 Severity::Error,
551 IssueKind::FieldConditionNotSatisfied {
552 field_name: field.name.clone(),
553 failed,
554 },
555 )
556 .with_field_path(&field.segment_path)
557 .with_rule(&field.ahb_status)
558 .with_mig_number(field.mig_number.as_deref()),
559 );
560 }
561 }
562 ConditionResult::Unknown => {
563 let mut external_ids = Vec::new();
568 let mut undetermined_ids = Vec::new();
569 let mut missing_ids = Vec::new();
570 for id in unknown_ids {
571 if self.evaluator.is_external(id) {
572 external_ids.push(id);
573 } else if self.evaluator.is_known(id) {
574 undetermined_ids.push(id);
575 } else {
576 missing_ids.push(id);
577 }
578 }
579
580 report.add_issue(
581 ValidationIssue::new(
582 Severity::Info,
583 IssueKind::ConditionUnknown {
584 field_name: field.name.clone(),
585 unresolved: UnresolvedConditions {
586 external: external_ids,
587 undetermined: undetermined_ids,
588 missing: missing_ids,
589 },
590 },
591 )
592 .with_field_path(&field.segment_path)
593 .with_rule(&field.ahb_status),
594 );
595 }
596 }
597 }
598
599 self.validate_codes_cross_field(workflow, ctx, report);
604 self.validate_answer_lists(workflow, ctx, report);
605
606 self.validate_package_cardinality(workflow, ctx, report);
609 }
610
611 fn validate_package_cardinality(
618 &self,
619 workflow: &AhbWorkflow,
620 ctx: &EvaluationContext,
621 report: &mut ValidationReport,
622 ) {
623 struct PackageGroup {
626 min: u32,
627 max: u32,
628 max_sum: u32,
631 bounds: BTreeSet<(u32, u32)>,
632 code_values: Vec<String>,
633 element_index: usize,
634 component_index: usize,
635 }
636
637 let mut groups: HashMap<(String, Option<String>, u32, u32, u32), PackageGroup> =
646 HashMap::new();
647 let mut alternatives: HashMap<(String, Option<String>, u32), BTreeSet<u32>> =
651 HashMap::new();
652
653 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
654
655 for field in &workflow.fields {
656 let el_idx = field.element_index.unwrap_or(0);
657 let comp_idx = field.component_index.unwrap_or(0);
658
659 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
661 continue;
662 }
663
664 for code in &field.codes {
665 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
667 let mut packages = Vec::new();
669 collect_packages(&expr, &mut packages);
670
671 if packages.len() > 1 {
672 let ids: BTreeSet<u32> = packages.iter().map(|(id, _, _)| *id).collect();
673 for id in &ids {
674 alternatives
675 .entry((field.segment_path.clone(), field.mig_number.clone(), *id))
676 .or_default()
677 .extend(ids.iter().copied().filter(|other| other != id));
678 }
679 }
680
681 for (pkg_id, pkg_min, pkg_max) in packages {
682 let key = (
683 field.segment_path.clone(),
684 field.mig_number.clone(),
685 pkg_id,
686 0,
687 0,
688 );
689 let group = groups.entry(key).or_insert_with(|| PackageGroup {
690 min: pkg_min,
691 max: pkg_max,
692 max_sum: 0,
693 bounds: BTreeSet::new(),
694 code_values: Vec::new(),
695 element_index: el_idx,
696 component_index: comp_idx,
697 });
698 group.bounds.insert((pkg_min, pkg_max));
705 group.min = group.min.min(pkg_min);
706 if !group.code_values.contains(&code.value) {
707 group.max_sum = group.max_sum.saturating_add(pkg_max);
708 }
709 group.max = group.max.max(pkg_max);
710 group.code_values.push(code.value.clone());
711 }
712 }
713 }
714 }
715
716 let mut field_packages: HashMap<(String, Option<String>), BTreeSet<u32>> = HashMap::new();
727 for (seg_path, mig_number, pkg_id, _, _) in groups.keys() {
728 field_packages
729 .entry((seg_path.clone(), mig_number.clone()))
730 .or_default()
731 .insert(*pkg_id);
732 }
733 let with_alternatives: Vec<(String, Option<String>)> = alternatives
734 .keys()
735 .map(|(p, m, _)| (p.clone(), m.clone()))
736 .collect();
737 for field in with_alternatives {
738 let Some(ids) = field_packages.get(&field) else {
739 continue;
740 };
741 for id in ids {
742 alternatives
743 .entry((field.0.clone(), field.1.clone(), *id))
744 .or_default()
745 .extend(ids.iter().copied().filter(|other| other != id));
746 }
747 }
748
749 type Counted = (Vec<usize>, Vec<bool>, usize, usize, Vec<String>);
751 let mut counted: HashMap<(String, Option<String>, u32, u32, u32), Counted> = HashMap::new();
752 for ((seg_path, mig_number, pkg_id, _, _), group) in &groups {
753 let segment_id = extract_segment_id(seg_path);
754
755 let mut unique_codes: Vec<&str> =
759 group.code_values.iter().map(|s| s.as_str()).collect();
760 unique_codes.sort_unstable();
761 unique_codes.dedup();
762 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
763
764 let group_path_str = extract_group_path_key(seg_path);
765 let group_path: Vec<&str> = if group_path_str.is_empty() {
766 Vec::new()
767 } else {
768 group_path_str.split('/').collect()
769 };
770
771 let min = group.min as usize;
772 let max = if group.bounds.len() > 1 {
773 group.max_sum as usize
774 } else {
775 group.max as usize
776 };
777
778 type Instance<'p> = (usize, Option<Vec<(&'p str, usize)>>);
781 let per_instance_counts: Option<Vec<Instance>> =
782 match (ctx.navigator, group_path.is_empty()) {
783 (Some(nav), false)
791 if group_path.len() > 1
792 && !full_instance_paths(nav, &group_path).is_empty() =>
793 {
794 let paths = full_instance_paths(nav, &group_path);
795 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
803 .as_deref()
804 {
805 Some(m) => (0..group_path.len())
806 .filter_map(|level| {
807 let prefix = group_path[..=level].join("/");
808 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
809 })
810 .collect(),
811 None => Vec::new(),
812 };
813 if paths.is_empty() {
814 None
815 } else {
816 Some(
817 paths
818 .into_iter()
819 .filter(|path| match mig_number.as_deref() {
820 Some(m) => nav.instance_at_has_mig_number(path, m),
821 None => true,
822 })
823 .filter(|path| {
824 variant_entries.iter().all(|(level, entry)| {
825 nav.find_segments_in_instance(
826 &entry.tag,
827 &path[..=*level],
828 )
829 .first()
830 .and_then(|seg| {
831 seg.elements
832 .get(entry.element)
833 .and_then(|e| e.get(entry.component))
834 .cloned()
835 })
836 .map_or(true, |v| entry.codes.contains(v.as_str()))
837 })
838 })
839 .map(|path| {
840 let count = nav
841 .find_segments_in_instance(&segment_id, &path)
842 .iter()
843 .filter_map(|seg| {
844 seg.elements
845 .get(group.element_index)
846 .and_then(|e| e.get(group.component_index))
847 .filter(|v| !v.is_empty())
848 .cloned()
849 })
850 .filter(|v| code_set.contains(v.as_str()))
851 .count();
852 (count, Some(path))
853 })
854 .collect(),
855 )
856 }
857 }
858 (Some(nav), false) => {
859 let instance_count = nav.group_instance_count(&group_path);
860 if instance_count == 0 {
861 None
862 } else {
863 Some(
871 (0..instance_count)
872 .filter(|i| match mig_number.as_deref() {
873 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
874 None => true,
875 })
876 .map(|i| {
877 let count = nav
878 .find_segments_in_group(&segment_id, &group_path, i)
879 .iter()
880 .filter_map(|seg| {
881 seg.elements
882 .get(group.element_index)
883 .and_then(|e| e.get(group.component_index))
884 .filter(|v| !v.is_empty())
885 .cloned()
886 })
887 .filter(|v| code_set.contains(v.as_str()))
888 .count();
889 let path = (group_path.len() == 1)
891 .then(|| vec![(group_path[0], i)]);
892 (count, path)
893 })
894 .collect(),
895 )
896 }
897 }
898 _ => None,
899 };
900
901 let instances = per_instance_counts.unwrap_or_else(|| {
902 let segments = ctx.find_segments(&segment_id);
903 let count = segments
904 .iter()
905 .filter_map(|seg| {
906 seg.elements
907 .get(group.element_index)
908 .and_then(|e| e.get(group.component_index))
909 .filter(|v| !v.is_empty())
910 .map(|s| s.as_str())
911 })
912 .filter(|v| code_set.contains(v))
913 .count();
914 vec![(count, None)]
915 });
916 let applies: Vec<bool> = instances
919 .iter()
920 .map(|(_, path)| match workflow.package_conditions.get(pkg_id) {
921 None => true,
922 Some(condition) => {
923 let result = match path {
924 Some(path) => expr_eval.evaluate(
925 condition,
926 &ctx.with_scope(crate::eval::GroupScope { path }),
927 ),
928 None => expr_eval.evaluate(condition, ctx),
929 };
930 result == ConditionResult::True
931 }
932 })
933 .collect();
934 let counts: Vec<usize> = instances.iter().map(|(c, _)| *c).collect();
935
936 counted.insert(
937 (
938 seg_path.clone(),
939 mig_number.clone(),
940 *pkg_id,
941 group.min,
942 group.max,
943 ),
944 (
945 counts,
946 applies,
947 min,
948 max,
949 unique_codes.iter().map(|s| s.to_string()).collect(),
950 ),
951 );
952 }
953
954 let mut keys: Vec<&(String, Option<String>, u32, u32, u32)> = counted.keys().collect();
959 keys.sort();
960 for key in keys {
961 let (counts, applies, min, max, codes) = &counted[key];
962 let (seg_path, mig_number, pkg_id, _, _) = key;
963 let holds = |c: &[usize], a: &[bool], lo: usize, hi: usize, i: usize| {
964 c.len() == counts.len() && a[i] && (lo..=hi).contains(&c[i]) && c[i] > 0
965 };
966 let alternative_holds = |i: usize| {
967 alternatives
968 .get(&(seg_path.clone(), mig_number.clone(), *pkg_id))
969 .is_some_and(|alts| {
970 counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
971 p == seg_path
972 && m == mig_number
973 && alts.contains(id)
974 && holds(c, a, *lo, *hi, i)
975 })
976 })
977 };
978 let applicable_alternative_holds = |i: usize| {
983 *min > 0
984 && counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
985 p == seg_path
986 && m == mig_number
987 && id != pkg_id
988 && *lo > 0
989 && holds(c, a, *lo, *hi, i)
990 })
991 };
992 let mut reported_counts: HashSet<usize> = HashSet::new();
993 for (i, &present_count) in counts.iter().enumerate() {
994 if applies[i]
995 && (present_count < *min || present_count > *max)
996 && !alternative_holds(i)
997 && !(present_count < *min && applicable_alternative_holds(i))
998 && reported_counts.insert(present_count)
999 {
1000 report.add_issue(
1001 ValidationIssue::new(
1002 Severity::Error,
1003 IssueKind::PackageCardinality {
1004 package_id: pkg_id.to_string(),
1005 present: present_count,
1006 min: *min,
1007 max: *max,
1008 codes: codes.clone(),
1009 },
1010 )
1011 .with_field_path(seg_path)
1012 .with_expected(format!("{min}..{max}"))
1013 .with_actual(present_count.to_string()),
1014 );
1015 }
1016 }
1017 }
1018 }
1019
1020 fn validate_codes_cross_field(
1032 &self,
1033 workflow: &AhbWorkflow,
1034 ctx: &EvaluationContext,
1035 report: &mut ValidationReport,
1036 ) {
1037 if ctx.navigator.is_some() {
1038 self.validate_codes_group_scoped(workflow, ctx, report);
1039 } else {
1040 self.validate_codes_tag_scoped(workflow, ctx, report);
1041 }
1042 }
1043
1044 fn validate_codes_group_scoped(
1047 &self,
1048 workflow: &AhbWorkflow,
1049 ctx: &EvaluationContext,
1050 report: &mut ValidationReport,
1051 ) {
1052 let by_loc = partition_codes_by_mig(workflow);
1053 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
1054 let data_slots = data_slot_entries(workflow);
1055 let nav = ctx.navigator.unwrap();
1056
1057 for ((group_key, tag), migs) in &by_loc {
1058 let field_path = if group_key.is_empty() {
1059 format!("{tag}/qualifier")
1060 } else {
1061 format!("{group_key}/{tag}/qualifier")
1062 };
1063
1064 let group_path: Vec<&str> = if group_key.is_empty() {
1065 Vec::new()
1066 } else {
1067 group_key.split('/').collect()
1068 };
1069
1070 let tag_qualifiers = known_qualifiers.get(tag);
1071
1072 if group_path.is_empty() {
1073 Self::validate_segments_per_mig(
1074 &ctx.find_segments(tag),
1075 migs,
1076 tag_qualifiers,
1077 false,
1078 tag,
1079 &field_path,
1080 report,
1081 );
1082 } else {
1083 let instance_count = nav.group_instance_count(&group_path);
1084 let slots = data_slots.get(&(group_key.clone(), tag.clone()));
1085 for i in 0..instance_count {
1086 let owned = nav.find_segments_in_group(tag, &group_path, i);
1087 let refs: Vec<&OwnedSegment> = owned.iter().collect();
1088 let holds_data_slot = slots.is_some_and(|entries| {
1091 entries.iter().any(|(entry_tag, entry_mig)| {
1092 let Some(bucket) = by_loc
1093 .get(&(group_key.clone(), entry_tag.clone()))
1094 .and_then(|m| m.get(entry_mig))
1095 else {
1096 return false;
1097 };
1098 nav.find_segments_in_group(entry_tag, &group_path, i)
1099 .iter()
1100 .any(|s| matches_qualifier(s, bucket))
1101 })
1102 });
1103 Self::validate_segments_per_mig(
1104 &refs,
1105 migs,
1106 tag_qualifiers,
1107 holds_data_slot,
1108 tag,
1109 &field_path,
1110 report,
1111 );
1112 }
1113 }
1114 }
1115 }
1116
1117 fn validate_codes_tag_scoped(
1121 &self,
1122 workflow: &AhbWorkflow,
1123 ctx: &EvaluationContext,
1124 report: &mut ValidationReport,
1125 ) {
1126 let by_loc = partition_codes_by_mig(workflow);
1127 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
1128 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
1130 for ((_group_key, tag), migs) in by_loc {
1131 let merged = by_tag.entry(tag).or_default();
1132 for (mig_key, bucket) in migs {
1133 let entry = merged.entry(mig_key).or_default();
1134 if entry.qualifier_position.is_none() {
1135 entry.qualifier_position = bucket.qualifier_position;
1136 }
1137 if entry.qualifier_position == bucket.qualifier_position {
1138 entry.qualifier_values.extend(&bucket.qualifier_values);
1139 }
1140 for (pos, codes) in bucket.codes {
1141 entry.codes.entry(pos).or_default().extend(codes);
1142 }
1143 }
1144 }
1145
1146 for (tag, migs) in &by_tag {
1147 let field_path = format!("{tag}/qualifier");
1148 Self::validate_segments_per_mig(
1149 &ctx.find_segments(tag),
1150 migs,
1151 known_qualifiers.get(tag),
1152 false,
1153 tag,
1154 &field_path,
1155 report,
1156 );
1157 }
1158 }
1159
1160 fn validate_segments_per_mig(
1173 segments: &[&OwnedSegment],
1174 migs: &HashMap<Option<String>, MigCodeBucket>,
1175 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
1176 holds_data_slot: bool,
1177 tag: &str,
1178 field_path: &str,
1179 report: &mut ValidationReport,
1180 ) {
1181 for seg in segments {
1182 match match_segment_to_mig(seg, migs) {
1183 Some(bucket) => {
1184 for ((el, c), allowed) in &bucket.codes {
1185 if allowed.is_empty() {
1186 continue;
1187 }
1188 Self::check_segments_against_codes(
1189 vec![*seg],
1190 allowed,
1191 tag,
1192 *el,
1193 *c,
1194 field_path,
1195 report,
1196 );
1197 }
1198 }
1199 None => {
1200 let Some(qualifiers) = tag_qualifiers.filter(|_| !holds_data_slot) else {
1201 continue;
1202 };
1203 for ((el, c), allowed) in qualifiers {
1208 let Some(actual) = seg
1209 .elements
1210 .get(*el)
1211 .and_then(|e| e.get(*c))
1212 .filter(|v| !v.is_empty())
1213 .map(|s| s.as_str())
1214 else {
1215 continue;
1216 };
1217 if allowed.iter().any(|v| v == actual) {
1218 break;
1220 }
1221 let bucket_values: HashSet<&str> = migs
1224 .values()
1225 .filter(|b| b.qualifier_position == Some((*el, *c)))
1226 .flat_map(|b| b.qualifier_values.iter().copied())
1227 .collect();
1228 if !bucket_values.is_empty() {
1229 Self::check_segments_against_codes(
1230 vec![*seg],
1231 &bucket_values,
1232 tag,
1233 *el,
1234 *c,
1235 field_path,
1236 report,
1237 );
1238 break;
1239 }
1240 }
1241 }
1242 }
1243 }
1244 }
1245
1246 fn validate_answer_lists(
1255 &self,
1256 workflow: &AhbWorkflow,
1257 ctx: &EvaluationContext,
1258 report: &mut ValidationReport,
1259 ) {
1260 let lists = AnswerCodeLists::embedded();
1261 let fv = self.evaluator.format_version();
1262 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1263 for list_rule in &workflow.fields {
1264 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1265 continue;
1266 };
1267 if list_rule.codes.is_empty()
1268 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1269 {
1270 continue;
1271 }
1272 let Some(answer_rule) = workflow
1273 .fields
1274 .iter()
1275 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1276 else {
1277 continue;
1278 };
1279 let at = |rule: &AhbFieldRule| {
1280 (
1281 rule.element_index.unwrap_or(0),
1282 rule.component_index.unwrap_or(0),
1283 )
1284 };
1285 let (list_el, list_c) = at(list_rule);
1286 let (answer_el, answer_c) = at(answer_rule);
1287 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1288 seg.elements
1289 .get(el)
1290 .and_then(|e| e.get(c))
1291 .filter(|v| !v.is_empty())
1292 .cloned()
1293 };
1294 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1295 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1296 continue;
1297 }
1298 let (Some(list), Some(answer)) =
1299 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1300 else {
1301 continue;
1302 };
1303 if !list_rule.codes.iter().any(|c| c.value == list) {
1304 continue;
1305 }
1306 if answer_rule.codes.iter().any(|c| c.value == answer) {
1310 continue;
1311 }
1312 let Some(codes) = lists.codes(fv, &list) else {
1313 continue;
1314 };
1315 if codes.contains(&answer) {
1316 continue;
1317 }
1318 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1319 allowed.sort_unstable();
1320 report.add_issue(
1321 ValidationIssue::new(
1322 Severity::Warning,
1323 IssueKind::AnswerNotInList {
1324 answer: answer.clone(),
1325 list: list.clone(),
1326 allowed: allowed.clone(),
1327 },
1328 )
1329 .with_field_path(&answer_rule.segment_path)
1330 .with_actual(&answer)
1331 .with_expected(allowed.join(", ")),
1332 );
1333 }
1334 }
1335 }
1336
1337 fn check_segments_against_codes(
1339 segments: Vec<&OwnedSegment>,
1340 allowed_codes: &HashSet<&str>,
1341 _tag: &str,
1342 el_idx: usize,
1343 comp_idx: usize,
1344 field_path: &str,
1345 report: &mut ValidationReport,
1346 ) {
1347 for segment in segments {
1348 if let Some(code_value) = segment
1349 .elements
1350 .get(el_idx)
1351 .and_then(|e| e.get(comp_idx))
1352 .filter(|v| !v.is_empty())
1353 {
1354 if !allowed_codes.contains(code_value.as_str()) {
1355 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1356 sorted_codes.sort_unstable();
1357 let kind = IssueKind::CodeNotAllowedForPid {
1358 value: code_value.to_string(),
1359 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1360 };
1361 let mut issue = ValidationIssue::new(Severity::Error, kind)
1362 .with_field_path(field_path)
1363 .with_actual(code_value)
1364 .with_expected(sorted_codes.join(", "))
1365 .with_field_position(el_idx as u32 + 2, Some(comp_idx as u32 + 1));
1366 if segment.segment_number > 0 {
1367 issue =
1368 issue.with_position(crate::SegmentPosition::at(segment.segment_number));
1369 }
1370 report.add_issue(issue);
1371 }
1372 }
1373 }
1374 }
1375}
1376
1377fn full_instance_paths<'p>(
1380 nav: &dyn GroupNavigator,
1381 group_path: &[&'p str],
1382) -> Vec<Vec<(&'p str, usize)>> {
1383 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1384 for g in group_path {
1385 paths = paths
1386 .into_iter()
1387 .flat_map(|parent| {
1388 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1389 let mut path = parent.clone();
1390 path.push((*g, i));
1391 path
1392 })
1393 })
1394 .collect();
1395 }
1396 paths
1397}
1398
1399struct VariantEntry<'a> {
1402 tag: String,
1403 element: usize,
1404 component: usize,
1405 codes: HashSet<&'a str>,
1406}
1407
1408fn merged_variant_entry<'a>(
1414 workflow: &'a AhbWorkflow,
1415 group_path: &str,
1416 mig: &str,
1417) -> Option<VariantEntry<'a>> {
1418 let mig: u32 = mig.parse().ok()?;
1419 let prefix = format!("{group_path}/");
1420 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1422 .fields
1423 .iter()
1424 .filter_map(|r| {
1425 let rest = r.segment_path.strip_prefix(&prefix)?;
1426 let tag = rest.split('/').next()?;
1427 if tag.starts_with("SG") {
1428 return None;
1429 }
1430 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1431 })
1432 .collect();
1433 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1435 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1436 .iter()
1437 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1438 .collect();
1439 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1440 if numbers.len() < 2 {
1441 return None;
1442 }
1443 let own = entries
1444 .iter()
1445 .filter(|(_, n, _)| *n <= mig)
1446 .map(|(_, n, _)| *n)
1447 .max()?;
1448 let (rule, _, tag) = entries
1450 .iter()
1451 .filter(|(_, n, _)| *n == own)
1452 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1453 Some(VariantEntry {
1454 tag: tag.to_string(),
1455 element: rule.element_index.unwrap_or(0),
1456 component: rule.component_index.unwrap_or(0),
1457 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1458 })
1459}
1460
1461fn value_length(format: &str, value: &str) -> usize {
1465 let format = format.trim();
1466 if format.starts_with('n') {
1467 value
1468 .chars()
1469 .filter(|c| !matches!(c, '.' | ',' | '-'))
1470 .count()
1471 } else {
1472 value.chars().count()
1473 }
1474}
1475
1476fn max_length(format: &str) -> Option<usize> {
1479 let digits = format
1480 .trim()
1481 .strip_prefix("an")
1482 .or_else(|| format.trim().strip_prefix('a'))
1483 .or_else(|| format.trim().strip_prefix('n'))?;
1484 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1485}
1486
1487fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1498 let derived_from_codes = field
1499 .codes
1500 .first()
1501 .is_some_and(|c| c.ahb_status == field.ahb_status);
1502 if !derived_from_codes && !codes_are_alternatives(field) {
1503 return &field.ahb_status;
1504 }
1505 value
1506 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1507 .map_or(&field.ahb_status, |c| &c.ahb_status)
1508}
1509
1510fn codes_are_alternatives(field: &AhbFieldRule) -> bool {
1521 !field.codes.is_empty()
1522 && field.ahb_status.contains('[')
1523 && field
1524 .codes
1525 .iter()
1526 .all(|c| c.ahb_status.contains('[') && c.ahb_status != field.ahb_status)
1527}
1528
1529fn should_skip_for_parent_group<E: ConditionEvaluator>(
1539 field: &AhbFieldRule,
1540 expr_eval: &ConditionExprEvaluator<E>,
1541 ctx: &EvaluationContext,
1542 ub_definitions: &BTreeMap<String, ConditionExpr>,
1543) -> bool {
1544 let Some(group_status) = field.parent_group_ahb_status.as_deref() else {
1545 return false;
1546 };
1547 if !group_status.contains('[') {
1548 return false;
1549 }
1550 match ConditionParser::parse_status_branches(group_status, ub_definitions) {
1551 Ok(branches) => !branches.iter().any(|branch| {
1552 branch.condition.as_ref().map_or(true, |c| {
1553 expr_eval.evaluate(c, ctx) == ConditionResult::True
1554 })
1555 }),
1556 Err(_) => true,
1557 }
1558}
1559
1560fn parent_group_refused<E: ConditionEvaluator>(
1564 field: &AhbFieldRule,
1565 expr_eval: &ConditionExprEvaluator<E>,
1566 ctx: &EvaluationContext,
1567 ub_definitions: &BTreeMap<String, ConditionExpr>,
1568) -> bool {
1569 let Some(group_status) = field.parent_group_ahb_status.as_deref() else {
1570 return false;
1571 };
1572 if !group_status.contains('[') {
1573 return false;
1574 }
1575 match ConditionParser::parse_status_branches(group_status, ub_definitions) {
1576 Ok(branches) => branches.iter().all(|branch| {
1577 branch
1578 .condition
1579 .as_ref()
1580 .is_some_and(|c| expr_eval.evaluate(c, ctx) == ConditionResult::False)
1581 }),
1582 Err(_) => true,
1583 }
1584}
1585
1586fn is_parent_group_required<E: ConditionEvaluator>(
1593 field: &AhbFieldRule,
1594 expr_eval: &ConditionExprEvaluator<E>,
1595 ctx: &EvaluationContext,
1596 ub_definitions: &BTreeMap<String, ConditionExpr>,
1597) -> bool {
1598 field
1599 .parent_group_ahb_status
1600 .as_deref()
1601 .is_some_and(|status| {
1602 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1603 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1604 })
1605}
1606
1607fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1616 let segment_id = extract_segment_id(&field.segment_path);
1617
1618 if !field.codes.is_empty() {
1624 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1625 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1626 let matching = ctx.find_segments(&segment_id);
1627 return matching.iter().any(|seg| {
1628 seg.elements
1629 .get(el_idx)
1630 .and_then(|e| e.get(comp_idx))
1631 .is_some_and(|v| required_codes.contains(&v.as_str()))
1632 });
1633 }
1634 if is_qualifier_field(&field.segment_path) {
1637 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1638 let el_idx = field.element_index.unwrap_or(0);
1639 let comp_idx = field.component_index.unwrap_or(0);
1640 let matching = ctx.find_segments(&segment_id);
1641 return matching.iter().any(|seg| {
1642 seg.elements
1643 .get(el_idx)
1644 .and_then(|e| e.get(comp_idx))
1645 .is_some_and(|v| required_codes.contains(&v.as_str()))
1646 });
1647 }
1648 }
1649
1650 ctx.has_segment(&segment_id)
1651}
1652
1653fn is_group_variant_absent(
1668 ctx: &EvaluationContext,
1669 field: &AhbFieldRule,
1670 is_group_mandatory: bool,
1671) -> bool {
1672 let group_path: Vec<&str> = field
1673 .segment_path
1674 .split('/')
1675 .take_while(|p| p.starts_with("SG"))
1676 .collect();
1677
1678 if group_path.is_empty() {
1679 return false;
1680 }
1681
1682 let nav = match ctx.navigator {
1683 Some(nav) => nav,
1684 None => return false,
1685 };
1686
1687 let instance_count = nav.group_instance_count(&group_path);
1688
1689 if instance_count == 0 {
1694 if !is_group_mandatory {
1695 return true;
1696 }
1697 return false;
1699 }
1700
1701 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1706 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1708 let segment_id = extract_segment_id(&field.segment_path);
1709 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1710
1711 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1712 nav.find_segments_in_group(&segment_id, &group_path, i)
1713 .iter()
1714 .any(|seg| {
1715 seg.elements
1716 .first()
1717 .and_then(|e| e.first())
1718 .is_some_and(|v| required_codes.contains(&v.as_str()))
1719 })
1720 });
1721
1722 if !any_instance_has_qualifier {
1723 return true; }
1725 }
1726 }
1727
1728 let segment_id = extract_segment_id(&field.segment_path);
1737 let segment_absent_from_all = (0..instance_count).all(|i| {
1738 nav.find_segments_in_group(&segment_id, &group_path, i)
1739 .is_empty()
1740 });
1741 if segment_absent_from_all {
1742 let group_has_other_segments =
1743 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1744 if group_has_other_segments {
1745 return true;
1746 }
1747 }
1748
1749 false
1750}
1751
1752fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1754 out.extend(group.fields.iter());
1755 for child in &group.children {
1756 collect_nodes_depth_first(child, out);
1757 }
1758}
1759
1760#[allow(clippy::too_many_arguments)]
1764fn evaluate_node<E: ConditionEvaluator>(
1765 node: &AhbNode,
1766 ctx: &EvaluationContext,
1767 expr_eval: &ConditionExprEvaluator<E>,
1768 evaluator: &E,
1769 ub_definitions: &BTreeMap<String, ConditionExpr>,
1770 tag_migs: &HashMap<String, HashSet<&str>>,
1771 instance_index: Option<usize>,
1772 slot_holds: bool,
1773 report: &mut ValidationReport,
1774) {
1775 let field = node.rule;
1776
1777 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1779
1780 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1785 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1786 if length > max {
1787 let mut issue = ValidationIssue::new(
1788 Severity::Error,
1789 IssueKind::ValueTooLong {
1790 field_name: field.name.clone(),
1791 length,
1792 max,
1793 },
1794 )
1795 .with_field_path(&field.segment_path)
1796 .with_actual(value)
1797 .with_expected(field.format.clone().unwrap_or_default());
1798 if let Some(idx) = instance_index {
1799 issue = issue.with_instance_index(idx);
1800 }
1801 if let Some(num) = node.matched_segment_number {
1802 issue = issue.with_position(crate::SegmentPosition::at(num));
1803 }
1804 report.add_issue(attach_field_position(issue, field));
1805 }
1806 }
1807
1808 if let (Some(value), Some(format)) = (node.value, field.format.as_deref()) {
1811 if slot_holds
1812 && format.trim().starts_with('n')
1813 && field.codes.is_empty()
1814 && !value.is_empty()
1815 && !crate::eval::format_validators::is_edifact_number(value)
1816 {
1817 let mut issue = ValidationIssue::new(
1818 Severity::Error,
1819 IssueKind::ValueNotNumeric {
1820 field_name: field.name.clone(),
1821 },
1822 )
1823 .with_field_path(&field.segment_path)
1824 .with_actual(value)
1825 .with_expected(format);
1826 if let Some(idx) = instance_index {
1827 issue = issue.with_instance_index(idx);
1828 }
1829 if let Some(num) = node.matched_segment_number {
1830 issue = issue.with_position(crate::SegmentPosition::at(num));
1831 }
1832 report.add_issue(attach_field_position(issue, field));
1833 }
1834 }
1835
1836 if parent_group_refused(field, expr_eval, ctx, ub_definitions) {
1840 return;
1841 }
1842
1843 let status = governing_status(field, node.value);
1846 let StatusResolution {
1847 result: condition_result,
1848 kind,
1849 unknown_ids,
1850 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1851 let mandatory = kind == Some(StatusKind::Muss);
1852
1853 match condition_result {
1854 ConditionResult::True => {
1855 if mandatory && node.value.is_none() {
1864 let tag = extract_segment_id(&field.segment_path);
1865 let single_variant_present = node.segment_elements.is_none()
1866 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1867 && is_field_present(ctx, field);
1868 let segment_optional_and_absent = node.segment_elements.is_none()
1873 && field
1874 .segment_ahb_status
1875 .as_deref()
1876 .is_some_and(is_optional_segment_status);
1877 let segment_not_required_and_absent = node.segment_elements.is_none()
1884 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1885 let status = expr_eval.resolve_status(st, &node_ctx, ub_definitions);
1886 status.result != ConditionResult::True
1887 || status.kind != Some(StatusKind::Muss)
1888 });
1889 if !single_variant_present
1890 && !segment_optional_and_absent
1891 && !segment_not_required_and_absent
1892 {
1893 let mut issue = ValidationIssue::new(
1894 Severity::Error,
1895 IssueKind::MissingRequiredField {
1896 field_name: field.name.clone(),
1897 },
1898 )
1899 .with_field_path(&field.segment_path)
1900 .with_rule(&field.ahb_status);
1901 if let Some(first_code) = field.codes.first() {
1902 issue.expected_value = Some(first_code.value.clone());
1903 }
1904 if let Some(idx) = instance_index {
1905 issue = issue.with_instance_index(idx);
1906 }
1907 if let Some(num) = node.matched_segment_number {
1908 issue = issue.with_position(crate::SegmentPosition::at(num));
1909 }
1910 issue = attach_field_position(issue, field);
1911 report.add_issue(issue);
1912 }
1913 }
1914 }
1915 ConditionResult::False => {
1916 if mandatory && node.value.is_some() {
1919 let failed = expr_eval.false_ids_with_ub(status, &node_ctx, ub_definitions);
1920 let mut issue = ValidationIssue::new(
1921 Severity::Error,
1922 IssueKind::FieldConditionNotSatisfied {
1923 field_name: field.name.clone(),
1924 failed,
1925 },
1926 )
1927 .with_field_path(&field.segment_path)
1928 .with_rule(&field.ahb_status);
1929 if let Some(idx) = instance_index {
1930 issue = issue.with_instance_index(idx);
1931 }
1932 if let Some(num) = node.matched_segment_number {
1933 issue = issue.with_position(crate::SegmentPosition::at(num));
1934 }
1935 issue = attach_field_position(issue, field);
1936 report.add_issue(issue);
1937 }
1938 }
1939 ConditionResult::Unknown => {
1940 let mut external_ids = Vec::new();
1942 let mut undetermined_ids = Vec::new();
1943 let mut missing_ids = Vec::new();
1944 for id in unknown_ids {
1945 if evaluator.is_external(id) {
1946 external_ids.push(id);
1947 } else if evaluator.is_known(id) {
1948 undetermined_ids.push(id);
1949 } else {
1950 missing_ids.push(id);
1951 }
1952 }
1953
1954 let mut issue = ValidationIssue::new(
1955 Severity::Info,
1956 IssueKind::ConditionUnknown {
1957 field_name: field.name.clone(),
1958 unresolved: UnresolvedConditions {
1959 external: external_ids,
1960 undetermined: undetermined_ids,
1961 missing: missing_ids,
1962 },
1963 },
1964 )
1965 .with_field_path(&field.segment_path)
1966 .with_rule(&field.ahb_status);
1967 if let Some(idx) = instance_index {
1968 issue = issue.with_instance_index(idx);
1969 }
1970 if let Some(num) = node.matched_segment_number {
1971 issue = issue.with_position(crate::SegmentPosition::at(num));
1972 }
1973 issue = attach_field_position(issue, field);
1974 report.add_issue(issue);
1975 }
1976 }
1977}
1978
1979fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1984 let issue = issue.with_mig_number(field.mig_number.as_deref());
1985 match field.element_index {
1986 Some(el) => {
1987 let element_pos = (el as u32) + 2;
1988 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1989 issue.with_field_position(element_pos, component_pos)
1990 }
1991 None => issue,
1992 }
1993}
1994
1995fn report_repeated_in_transaction(
2002 transaction: &AhbGroupNode,
2003 transaction_index: usize,
2004 once: u32,
2005 ctx: &EvaluationContext,
2006 report: &mut ValidationReport,
2007) {
2008 let mut counts: BTreeMap<(String, String), (usize, String)> = BTreeMap::new();
2010 fn visit<'a>(
2011 node: &'a AhbGroupNode<'a>,
2012 path: &mut Vec<(&'a str, usize)>,
2013 once: u32,
2014 ctx: &EvaluationContext,
2015 counts: &mut BTreeMap<(String, String), (usize, String)>,
2016 ) {
2017 let ids: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2018 let mut seen: HashSet<Option<&str>> = HashSet::new();
2019 for n in &node.fields {
2020 let rule = n.rule;
2021 let Some(status) = rule.segment_ahb_status.as_deref() else {
2022 continue;
2023 };
2024 if Some(status) == node.ahb_status
2025 || rule.element_index.unwrap_or(0) != 0
2026 || rule.component_index.unwrap_or(0) != 0
2027 || rule.codes.is_empty()
2028 || !crate::eval::binds_once(status, once)
2029 || !seen.insert(rule.mig_number.as_deref())
2030 {
2031 continue;
2032 }
2033 let tag = extract_segment_id(&rule.segment_path);
2034 let sent = ctx.navigator().map_or(0, |nav| {
2035 nav.find_segments_in_instance(&tag, path)
2036 .iter()
2037 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2038 .count()
2039 });
2040 let entry = counts
2041 .entry((
2042 format!("{}/{tag}", ids.join("/")),
2043 rule.mig_number.clone().unwrap_or_default(),
2044 ))
2045 .or_insert((0, status.to_string()));
2046 entry.0 += sent;
2047 }
2048 for child in &node.children {
2049 if let Some(status) = child
2050 .ahb_status
2051 .filter(|s| crate::eval::binds_once(s, once))
2052 {
2053 let variant = child
2054 .fields
2055 .first()
2056 .and_then(|n| n.rule.mig_number.clone())
2057 .unwrap_or_default();
2058 let entry = counts
2059 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
2060 .or_insert((0, status.to_string()));
2061 entry.0 += 1;
2062 }
2063 path.push((child.group_id, child.instance_index));
2064 visit(child, path, once, ctx, counts);
2065 path.pop();
2066 }
2067 }
2068 let mut path = vec![(transaction.group_id, transaction_index)];
2069 visit(transaction, &mut path, once, ctx, &mut counts);
2070 for ((path, _), (count, status)) in counts {
2071 if count > 1 {
2072 report.add_issue(
2073 ValidationIssue::new(
2074 Severity::Error,
2075 IssueKind::RepeatedInTransaction {
2076 path: path.clone(),
2077 count,
2078 },
2079 )
2080 .with_field_path(path)
2081 .with_rule(status)
2082 .with_instance_index(transaction_index),
2083 );
2084 }
2085 }
2086}
2087
2088#[allow(clippy::too_many_arguments)]
2095fn refuse_segments<E: ConditionEvaluator>(
2096 fields: &[AhbNode],
2097 group_status: Option<&str>,
2098 path: &[(&str, usize)],
2099 scoped_ctx: &EvaluationContext,
2100 evaluator: &E,
2101 ub_definitions: &BTreeMap<String, ConditionExpr>,
2102 instance_index: usize,
2103 report: &mut ValidationReport,
2104) {
2105 let mut seen: Vec<u32> = Vec::new();
2106 for node in fields {
2107 let rule = node.rule;
2108 let (Some(status), Some(value), Some(number)) = (
2109 rule.segment_ahb_status.as_deref(),
2110 node.value,
2111 node.matched_segment_number,
2112 ) else {
2113 continue;
2114 };
2115 let qualifier =
2118 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
2119 if !qualifier
2120 || seen.contains(&number)
2121 || Some(status) == group_status
2122 || !rule.codes.iter().any(|c| c.value == value)
2123 {
2124 continue;
2125 }
2126 seen.push(number);
2127 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2128 continue;
2129 }
2130 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2131 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
2132 report.add_issue(
2133 ValidationIssue::new(
2134 Severity::Warning,
2135 IssueKind::SegmentNotAllowed {
2136 segment: segment.join("/"),
2137 },
2138 )
2139 .with_field_path(segment.join("/"))
2140 .with_rule(status)
2141 .with_instance_index(instance_index)
2142 .with_position(crate::SegmentPosition::at(number)),
2143 );
2144 }
2145}
2146
2147fn report_segment_counts<E: ConditionEvaluator>(
2154 group: &AhbGroupNode,
2155 path: &[(&str, usize)],
2156 scoped_ctx: &EvaluationContext,
2157 evaluator: &E,
2158 instance_index: usize,
2159 report: &mut ValidationReport,
2160) {
2161 if crate::eval::repetition_limits(evaluator.message_type()).is_empty() {
2162 return;
2163 }
2164 let mut seen: HashSet<Option<&str>> = HashSet::new();
2165 for node in &group.fields {
2166 let rule = node.rule;
2167 let Some(status) = rule.segment_ahb_status.as_deref() else {
2168 continue;
2169 };
2170 if rule.element_index.unwrap_or(0) != 0
2171 || rule.component_index.unwrap_or(0) != 0
2172 || rule.codes.is_empty()
2173 || !seen.insert(rule.mig_number.as_deref())
2174 {
2175 continue;
2176 }
2177 let tag = extract_segment_id(&rule.segment_path);
2178 let sent: Vec<_> = scoped_ctx
2179 .find_segments_at(path, &tag)
2180 .into_iter()
2181 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2182 .collect();
2183 if sent.is_empty()
2184 || !crate::eval::breaks_repetition_limit(status, evaluator, scoped_ctx, sent.len())
2185 {
2186 continue;
2187 }
2188 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2189 segment.push(&tag);
2190 let mut issue = ValidationIssue::new(
2191 Severity::Error,
2192 IssueKind::RepetitionCount {
2193 path: segment.join("/"),
2194 count: sent.len(),
2195 },
2196 )
2197 .with_field_path(segment.join("/"))
2198 .with_rule(status)
2199 .with_instance_index(instance_index);
2200 if let Some(num) = node.matched_segment_number {
2201 issue = issue.with_position(crate::SegmentPosition::at(num));
2202 }
2203 report.add_issue(issue);
2204 }
2205}
2206
2207#[allow(clippy::too_many_arguments)]
2213fn report_repeated_in_group<E: ConditionEvaluator>(
2214 group: &AhbGroupNode,
2215 path: &[(&str, usize)],
2216 scoped_ctx: &EvaluationContext,
2217 evaluator: &E,
2218 ub_definitions: &BTreeMap<String, ConditionExpr>,
2219 instance_index: usize,
2220 report: &mut ValidationReport,
2221) {
2222 if crate::eval::counted_repetitions(evaluator.message_type()).is_empty() {
2223 return;
2224 }
2225 let mut reported: HashSet<&str> = HashSet::new();
2226 for child in &group.children {
2227 let Some(status) = child.ahb_status else {
2228 continue;
2229 };
2230 let count = group
2231 .children
2232 .iter()
2233 .filter(|c| c.group_id == child.group_id)
2234 .count();
2235 if count < 2 || reported.contains(child.group_id) {
2238 continue;
2239 }
2240 let mut child_path = path.to_vec();
2241 child_path.push((child.group_id, child.instance_index));
2242 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2243 if !crate::eval::refuses_count(status, evaluator, &child_ctx, ub_definitions) {
2244 continue;
2245 }
2246 reported.insert(child.group_id);
2247 let group_path: Vec<&str> = child_path.iter().map(|(g, _)| *g).collect();
2248 let mut issue = ValidationIssue::new(
2249 Severity::Error,
2250 IssueKind::RepetitionCount {
2251 path: group_path.join("/"),
2252 count,
2253 },
2254 )
2255 .with_field_path(group_path.join("/"))
2256 .with_rule(status)
2257 .with_instance_index(instance_index);
2258 if let Some(num) = child.fields.iter().find_map(|n| n.matched_segment_number) {
2259 issue = issue.with_position(crate::SegmentPosition::at(num));
2260 }
2261 report.add_issue(issue);
2262 }
2263}
2264
2265#[allow(clippy::too_many_arguments)]
2274fn walk_group_instance<E: ConditionEvaluator>(
2275 group: &AhbGroupNode,
2276 path: &[(&str, usize)],
2277 scoped_ctx: &EvaluationContext,
2278 expr_eval: &ConditionExprEvaluator<E>,
2279 evaluator: &E,
2280 ub_definitions: &BTreeMap<String, ConditionExpr>,
2281 tag_migs: &HashMap<String, HashSet<&str>>,
2282 instance_index: usize,
2283 report_segments: bool,
2284 report: &mut ValidationReport,
2285) {
2286 let mut group_refused = false;
2289 if let Some(status) = group.ahb_status {
2290 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2291 group_refused = true;
2292 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2293 let mut issue = ValidationIssue::new(
2294 Severity::Warning,
2295 IssueKind::GroupNotAllowed {
2296 group: group_path.join("/"),
2297 },
2298 )
2299 .with_field_path(group_path.join("/"))
2300 .with_rule(status)
2301 .with_instance_index(instance_index);
2302 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
2303 issue = issue.with_position(crate::SegmentPosition::at(num));
2304 }
2305 report.add_issue(issue);
2306 }
2307 }
2308 if report_segments && !group_refused {
2309 refuse_segments(
2310 &group.fields,
2311 group.ahb_status,
2312 path,
2313 scoped_ctx,
2314 evaluator,
2315 ub_definitions,
2316 instance_index,
2317 report,
2318 );
2319 }
2320 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
2324 .fields
2325 .iter()
2326 .filter(|n| {
2327 n.rule.element_index.unwrap_or(0) == 0
2328 && n.rule.component_index.unwrap_or(0) == 0
2329 && !n.rule.codes.is_empty()
2330 })
2331 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
2332 .collect();
2333 let holds = |node: &AhbNode| {
2334 let (Some(codes), Some(elements)) = (
2335 slot_codes.get(&node.rule.mig_number.as_deref()),
2336 node.segment_elements,
2337 ) else {
2338 return true;
2339 };
2340 let qualifier = elements
2341 .first()
2342 .and_then(|e| e.first())
2343 .map(String::as_str)
2344 .unwrap_or_default();
2345 codes.iter().any(|c| c.value == qualifier)
2346 };
2347 for node in &group.fields {
2348 evaluate_node(
2349 node,
2350 scoped_ctx,
2351 expr_eval,
2352 evaluator,
2353 ub_definitions,
2354 tag_migs,
2355 Some(instance_index),
2356 holds(node),
2357 report,
2358 );
2359 }
2360 report_repeated_in_group(
2361 group,
2362 path,
2363 scoped_ctx,
2364 evaluator,
2365 ub_definitions,
2366 instance_index,
2367 report,
2368 );
2369 report_segment_counts(group, path, scoped_ctx, evaluator, instance_index, report);
2370 for child in &group.children {
2371 let mut child_path = path.to_vec();
2372 child_path.push((child.group_id, child.instance_index));
2373 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2374 walk_group_instance(
2375 child,
2376 &child_path,
2377 &child_ctx,
2378 expr_eval,
2379 evaluator,
2380 ub_definitions,
2381 tag_migs,
2382 instance_index,
2383 report_segments,
2384 report,
2385 );
2386 }
2387}
2388
2389#[allow(clippy::too_many_arguments)]
2399fn report_absent_children<E: ConditionEvaluator>(
2400 group: &AhbGroupNode,
2401 path: &[(&str, usize)],
2402 scoped_ctx: &EvaluationContext,
2403 absent_eval: &ConditionExprEvaluator<crate::eval::AbsentTarget<'_, E>>,
2404 evaluator: &E,
2405 ub_definitions: &BTreeMap<String, ConditionExpr>,
2406 instance_index: usize,
2407 report: &mut ValidationReport,
2408) {
2409 if group.ahb_status.is_some_and(|status| {
2410 crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions)
2411 }) {
2412 return;
2413 }
2414 for rule in &group.absent_children {
2415 if should_skip_for_parent_group(rule, absent_eval, scoped_ctx, ub_definitions)
2416 || !is_parent_group_required(rule, absent_eval, scoped_ctx, ub_definitions)
2417 {
2418 continue;
2419 }
2420 let required = |status: &str| {
2421 let resolved = absent_eval.resolve_status(status, scoped_ctx, ub_definitions);
2422 resolved.result == ConditionResult::True && resolved.kind == Some(StatusKind::Muss)
2423 };
2424 if !required(&rule.ahb_status)
2425 || rule
2426 .segment_ahb_status
2427 .as_deref()
2428 .is_some_and(|status| !required(status))
2429 {
2430 continue;
2431 }
2432 let mut issue = ValidationIssue::new(
2433 Severity::Error,
2434 IssueKind::MissingRequiredField {
2435 field_name: rule.name.clone(),
2436 },
2437 )
2438 .with_field_path(&rule.segment_path)
2439 .with_rule(&rule.ahb_status)
2440 .with_instance_index(instance_index);
2441 if let Some(first_code) = rule.codes.first() {
2442 issue.expected_value = Some(first_code.value.clone());
2443 }
2444 report.add_issue(attach_field_position(issue, rule));
2445 }
2446 for child in &group.children {
2447 let mut child_path = path.to_vec();
2448 child_path.push((child.group_id, child.instance_index));
2449 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2450 report_absent_children(
2451 child,
2452 &child_path,
2453 &child_ctx,
2454 absent_eval,
2455 evaluator,
2456 ub_definitions,
2457 instance_index,
2458 report,
2459 );
2460 }
2461}
2462
2463fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2464 match expr {
2465 ConditionExpr::Package { id, min, max } => {
2466 out.push((*id, *min, *max));
2467 }
2468 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2469 for e in exprs {
2470 collect_packages(e, out);
2471 }
2472 }
2473 ConditionExpr::Xor(left, right) => {
2474 collect_packages(left, out);
2475 collect_packages(right, out);
2476 }
2477 ConditionExpr::Not(inner) => {
2478 collect_packages(inner, out);
2479 }
2480 ConditionExpr::Ref(_) => {}
2481 }
2482}
2483
2484fn is_optional_segment_status(status: &str) -> bool {
2494 let trimmed = status.trim();
2495 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
2496}
2497
2498fn is_qualifier_field(path: &str) -> bool {
2508 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
2509 matches!(parts.len(), 2 | 3)
2511}
2512
2513#[derive(Default)]
2521struct MigCodeBucket<'a> {
2522 qualifier_position: Option<(usize, usize)>,
2525 qualifier_values: HashSet<&'a str>,
2528 codes: HashMap<(usize, usize), HashSet<&'a str>>,
2531}
2532
2533fn partition_codes_by_mig(
2538 workflow: &AhbWorkflow,
2539) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
2540 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
2541 for field in &workflow.fields {
2542 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2543 continue;
2544 }
2545 if codes_are_alternatives(field) {
2548 continue;
2549 }
2550 let tag = extract_segment_id(&field.segment_path);
2551 let group_key = extract_group_path_key(&field.segment_path);
2552 let mig = field.mig_number.clone();
2553 let el = field.element_index.unwrap_or(0);
2554 let c = field.component_index.unwrap_or(0);
2555
2556 let bucket = out
2557 .entry((group_key, tag))
2558 .or_default()
2559 .entry(mig)
2560 .or_default();
2561
2562 let required: Vec<&str> = field
2569 .codes
2570 .iter()
2571 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2572 .map(|code| code.value.as_str())
2573 .collect();
2574
2575 if !required.is_empty() {
2582 if bucket.qualifier_position.is_none() {
2583 bucket.qualifier_position = Some((el, c));
2584 }
2585 if bucket.qualifier_position == Some((el, c)) {
2586 bucket.qualifier_values.extend(required.iter().copied());
2587 }
2588 }
2589
2590 for v in required {
2591 bucket.codes.entry((el, c)).or_default().insert(v);
2592 }
2593 }
2594 out
2595}
2596
2597fn global_qualifiers_by_tag(
2603 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
2604) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
2605 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
2606 for ((_group, tag), migs) in by_loc {
2607 let tag_entry = out.entry(tag.clone()).or_default();
2608 for bucket in migs.values() {
2609 if let Some(pos) = bucket.qualifier_position {
2610 tag_entry
2611 .entry(pos)
2612 .or_default()
2613 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2614 }
2615 }
2616 }
2617 out
2618}
2619
2620fn matches_qualifier(seg: &OwnedSegment, bucket: &MigCodeBucket<'_>) -> bool {
2622 bucket.qualifier_position.is_some_and(|(el, c)| {
2623 seg.elements
2624 .get(el)
2625 .and_then(|e| e.get(c))
2626 .is_some_and(|v| bucket.qualifier_values.contains(v.as_str()))
2627 })
2628}
2629
2630fn data_slot_entries(
2636 workflow: &AhbWorkflow,
2637) -> HashMap<(String, String), Vec<(String, Option<String>)>> {
2638 let mut out: HashMap<(String, String), Vec<(String, Option<String>)>> = HashMap::new();
2639 for field in &workflow.fields {
2640 let leading =
2641 field.element_index.unwrap_or(0) == 0 && field.component_index.unwrap_or(0) == 0;
2642 let Some(mig) = field.mig_number.as_deref() else {
2643 continue;
2644 };
2645 if !field.codes.is_empty() || !leading || !is_qualifier_field(&field.segment_path) {
2646 continue;
2647 }
2648 let tag = extract_segment_id(&field.segment_path);
2649 let group_key = extract_group_path_key(&field.segment_path);
2650 if group_key.is_empty() {
2651 continue;
2652 }
2653 let entry = workflow
2654 .fields
2655 .iter()
2656 .filter(|f| extract_group_path_key(&f.segment_path) == group_key)
2657 .filter(|f| extract_segment_id(&f.segment_path) != tag)
2658 .filter_map(|f| Some((f.mig_number.as_deref()?, f)))
2659 .filter(|(n, _)| *n < mig)
2660 .max_by_key(|(n, _)| *n);
2661 if let Some((n, f)) = entry {
2662 let slot = (extract_segment_id(&f.segment_path), Some(n.to_string()));
2663 let entries = out.entry((group_key, tag)).or_default();
2664 if !entries.contains(&slot) {
2665 entries.push(slot);
2666 }
2667 }
2668 }
2669 out
2670}
2671
2672fn match_segment_to_mig<'a, 'b>(
2683 seg: &OwnedSegment,
2684 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
2685) -> Option<&'a MigCodeBucket<'b>> {
2686 let actual_at = |el: usize, c: usize| -> &str {
2687 seg.elements
2688 .get(el)
2689 .and_then(|e| e.get(c))
2690 .map(|s| s.as_str())
2691 .unwrap_or("")
2692 };
2693
2694 let mut best: Option<&MigCodeBucket> = None;
2695 let mut best_matches = 0usize;
2696
2697 for bucket in migs.values() {
2698 let Some((el, c)) = bucket.qualifier_position else {
2699 continue;
2700 };
2701 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2702 continue;
2703 }
2704 let extra_matches = bucket
2705 .codes
2706 .iter()
2707 .filter(|(pos, _)| **pos != (el, c))
2708 .filter(|((e, k), allowed)| {
2709 let v = actual_at(*e, *k);
2710 !v.is_empty() && allowed.contains(v)
2711 })
2712 .count();
2713 if best.is_none() || extra_matches > best_matches {
2714 best = Some(bucket);
2715 best_matches = extra_matches;
2716 }
2717 }
2718 best
2719}
2720
2721fn extract_group_path_key(path: &str) -> String {
2726 let sg_parts: Vec<&str> = path
2727 .split('/')
2728 .take_while(|p| p.starts_with("SG"))
2729 .collect();
2730 sg_parts.join("/")
2731}
2732
2733fn extract_segment_id(path: &str) -> String {
2735 for part in path.split('/') {
2736 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2738 continue;
2739 }
2740 if part.len() >= 3
2742 && part
2743 .chars()
2744 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2745 {
2746 return part.to_string();
2747 }
2748 }
2749 path.split('/').next_back().unwrap_or(path).to_string()
2751}
2752
2753pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2765 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2767 if unh_count > 1 {
2768 return Some(ValidationIssue::new(
2769 Severity::Error,
2770 IssueKind::UntCountNotVerifiable { unh_count },
2771 ));
2772 }
2773
2774 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2776 let declared: usize = unt.get_element(0).parse().ok()?;
2777
2778 let actual = segments
2780 .iter()
2781 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2782 .count();
2783
2784 if declared != actual {
2785 Some(
2786 ValidationIssue::new(
2787 Severity::Error,
2788 IssueKind::UntSegmentCountMismatch { declared, actual },
2789 )
2790 .with_field_path("UNT/0074")
2791 .with_expected(actual.to_string())
2792 .with_actual(declared.to_string()),
2793 )
2794 } else {
2795 None
2796 }
2797}
2798
2799#[cfg(test)]
2800mod tests {
2801
2802 #[test]
2803 fn a_numeric_length_counts_digits_only() {
2804 assert_eq!(value_length("n..15", "-12.50"), 4);
2805 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
2806 assert_eq!(value_length("n5", "55001"), 5);
2807 assert_eq!(value_length("an..3", "Z.1"), 3);
2808 assert_eq!(value_length("an..35", "Müller"), 6);
2809 }
2810 use super::super::codes::ErrorCodes;
2811 use super::super::issue::ValidationCategory;
2812 use super::*;
2813 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2814 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2815 use std::collections::HashMap;
2816
2817 fn narrate(issue: &ValidationIssue) -> String {
2820 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2821 }
2822
2823 struct MockEvaluator {
2825 results: HashMap<u32, CR>,
2826 }
2827
2828 impl MockEvaluator {
2829 fn new(results: Vec<(u32, CR)>) -> Self {
2830 Self {
2831 results: results.into_iter().collect(),
2832 }
2833 }
2834
2835 fn all_true(ids: &[u32]) -> Self {
2836 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2837 }
2838 }
2839
2840 impl ConditionEvaluator for MockEvaluator {
2841 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2842 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2843 }
2844 fn is_external(&self, _condition: u32) -> bool {
2845 false
2846 }
2847 fn message_type(&self) -> &str {
2848 "UTILMD"
2849 }
2850 fn format_version(&self) -> &str {
2851 "FV2510"
2852 }
2853 }
2854
2855 #[test]
2858 fn a_present_code_is_judged_by_its_own_status() {
2859 let code = |value: &str, status: &str| AhbCodeRule {
2861 value: value.to_string(),
2862 description: String::new(),
2863 ahb_status: status.to_string(),
2864 };
2865 let field = AhbFieldRule {
2866 segment_path: "SG4/STS/C556/9013".to_string(),
2867 ahb_status: "X [192]".to_string(),
2868 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2869 ..Default::default()
2870 };
2871 assert_eq!(governing_status(&field, Some("E03")), "X");
2872 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2873 assert_eq!(governing_status(&field, None), "X [192]");
2874 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2875
2876 let own = AhbFieldRule {
2878 ahb_status: "X [931]".to_string(),
2879 codes: vec![code("303", "X")],
2880 ..Default::default()
2881 };
2882 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2883 assert!(!codes_are_alternatives(&own));
2884
2885 let alternatives = AhbFieldRule {
2888 ahb_status: "X [153] ∧ [538]".to_string(),
2889 codes: vec![
2890 code("Z74", "X [103] ∧ [537]"),
2891 code("Z75", "X [103] ∧ [537]"),
2892 ],
2893 ..Default::default()
2894 };
2895 assert!(codes_are_alternatives(&alternatives));
2896 assert_eq!(
2897 governing_status(&alternatives, Some("Z74")),
2898 "X [103] ∧ [537]"
2899 );
2900 assert_eq!(
2901 governing_status(&alternatives, Some("A01")),
2902 "X [153] ∧ [538]"
2903 );
2904 assert_eq!(governing_status(&alternatives, None), "X [153] ∧ [538]");
2905
2906 let restricting = AhbFieldRule {
2908 ahb_status: "X [2] ∨ [3]".to_string(),
2909 codes: vec![code("UNH", "X"), code("UNT", "X")],
2910 ..Default::default()
2911 };
2912 assert!(!codes_are_alternatives(&restricting));
2913 assert_eq!(governing_status(&restricting, Some("UNH")), "X [2] ∨ [3]");
2914 }
2915
2916 #[test]
2917 fn test_extract_segment_id_simple() {
2918 assert_eq!(extract_segment_id("NAD"), "NAD");
2919 }
2920
2921 #[test]
2922 fn test_extract_segment_id_with_sg_prefix() {
2923 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
2924 }
2925
2926 #[test]
2927 fn test_extract_segment_id_nested_sg() {
2928 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
2929 }
2930
2931 #[test]
2934 fn test_validate_missing_mandatory_field() {
2935 let evaluator = MockEvaluator::all_true(&[182, 152]);
2936 let validator = EdifactValidator::new(evaluator);
2937 let external = NoOpExternalProvider;
2938
2939 let workflow = AhbWorkflow {
2940 pruefidentifikator: "11001".to_string(),
2941 description: "Test".to_string(),
2942 communication_direction: None,
2943 fields: vec![AhbFieldRule {
2944 segment_path: "SG2/NAD/C082/3039".to_string(),
2945 name: "MP-ID des MSB".to_string(),
2946 ahb_status: "Muss [182] ∧ [152]".to_string(),
2947 codes: vec![],
2948 parent_group_ahb_status: None,
2949 segment_ahb_status: None,
2950 ..Default::default()
2951 }],
2952 ub_definitions: BTreeMap::new(),
2953 package_conditions: Default::default(),
2954 };
2955
2956 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2958
2959 assert!(!report.is_valid());
2961 let errors: Vec<_> = report.errors().collect();
2962 assert_eq!(errors.len(), 1);
2963 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2964 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
2965 }
2966
2967 #[test]
2968 fn test_validate_condition_false_no_error() {
2969 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
2971 let validator = EdifactValidator::new(evaluator);
2972 let external = NoOpExternalProvider;
2973
2974 let workflow = AhbWorkflow {
2975 pruefidentifikator: "11001".to_string(),
2976 description: "Test".to_string(),
2977 communication_direction: None,
2978 fields: vec![AhbFieldRule {
2979 segment_path: "NAD".to_string(),
2980 name: "Partnerrolle".to_string(),
2981 ahb_status: "Muss [182] ∧ [152]".to_string(),
2982 codes: vec![],
2983 parent_group_ahb_status: None,
2984 segment_ahb_status: None,
2985 ..Default::default()
2986 }],
2987 ub_definitions: BTreeMap::new(),
2988 package_conditions: Default::default(),
2989 };
2990
2991 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2992
2993 assert!(report.is_valid());
2995 }
2996
2997 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
2999 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3000 let workflow = AhbWorkflow {
3001 pruefidentifikator: "11001".to_string(),
3002 description: "Test".to_string(),
3003 communication_direction: None,
3004 fields: vec![AhbFieldRule {
3005 segment_path: "SG2/NAD/C082/3039".to_string(),
3006 name: "MP-ID".to_string(),
3007 ahb_status: status.to_string(),
3008 ..Default::default()
3009 }],
3010 ub_definitions: BTreeMap::new(),
3011 package_conditions: Default::default(),
3012 };
3013 validator.validate(
3014 &[],
3015 &workflow,
3016 &NoOpExternalProvider,
3017 ValidationLevel::Conditions,
3018 )
3019 }
3020
3021 #[test]
3022 fn a_later_soll_line_alone_does_not_require_the_field() {
3023 let report = validate_absent_field(
3025 "Muss [2119] \r\nSoll [130]",
3026 vec![(2119, CR::False), (130, CR::True)],
3027 );
3028 assert!(
3029 report.is_valid(),
3030 "{:?}",
3031 report.errors().collect::<Vec<_>>()
3032 );
3033 }
3034
3035 #[test]
3036 fn a_later_muss_line_requires_the_field() {
3037 let report = validate_absent_field(
3039 "Soll [47]\r\nMuss [46]",
3040 vec![(47, CR::False), (46, CR::True)],
3041 );
3042 let errors: Vec<_> = report.errors().collect();
3043 assert_eq!(errors.len(), 1);
3044 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3045 }
3046
3047 #[test]
3048 fn a_weaker_unknown_line_does_not_stop_a_muss() {
3049 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
3051 let errors: Vec<_> = report.errors().collect();
3052 assert_eq!(errors.len(), 1);
3053 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3054 }
3055
3056 #[test]
3057 fn a_stricter_unknown_line_leaves_the_status_undecided() {
3058 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
3059 assert!(report.is_valid());
3060 let infos: Vec<_> = report.infos().collect();
3061 assert_eq!(infos.len(), 1, "{infos:?}");
3062 match &infos[0].kind {
3063 IssueKind::ConditionUnknown { unresolved, .. } => {
3064 assert_eq!(unresolved.missing, vec![2119]);
3065 }
3066 other => panic!("expected ConditionUnknown, got {other:?}"),
3067 }
3068 }
3069
3070 #[test]
3071 fn test_validate_condition_unknown_adds_info() {
3072 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
3074 let validator = EdifactValidator::new(evaluator);
3076 let external = NoOpExternalProvider;
3077
3078 let workflow = AhbWorkflow {
3079 pruefidentifikator: "11001".to_string(),
3080 description: "Test".to_string(),
3081 communication_direction: None,
3082 fields: vec![AhbFieldRule {
3083 segment_path: "NAD".to_string(),
3084 name: "Partnerrolle".to_string(),
3085 ahb_status: "Muss [182] ∧ [152]".to_string(),
3086 codes: vec![],
3087 parent_group_ahb_status: None,
3088 segment_ahb_status: None,
3089 ..Default::default()
3090 }],
3091 ub_definitions: BTreeMap::new(),
3092 package_conditions: Default::default(),
3093 };
3094
3095 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3096
3097 assert!(report.is_valid());
3099 let infos: Vec<_> = report.infos().collect();
3100 assert_eq!(infos.len(), 1);
3101 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
3102 }
3103
3104 #[test]
3105 fn test_validate_structure_level_skips_conditions() {
3106 let evaluator = MockEvaluator::all_true(&[182, 152]);
3107 let validator = EdifactValidator::new(evaluator);
3108 let external = NoOpExternalProvider;
3109
3110 let workflow = AhbWorkflow {
3111 pruefidentifikator: "11001".to_string(),
3112 description: "Test".to_string(),
3113 communication_direction: None,
3114 fields: vec![AhbFieldRule {
3115 segment_path: "NAD".to_string(),
3116 name: "Partnerrolle".to_string(),
3117 ahb_status: "Muss [182] ∧ [152]".to_string(),
3118 codes: vec![],
3119 parent_group_ahb_status: None,
3120 segment_ahb_status: None,
3121 ..Default::default()
3122 }],
3123 ub_definitions: BTreeMap::new(),
3124 package_conditions: Default::default(),
3125 };
3126
3127 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
3129
3130 assert!(report.is_valid());
3132 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
3133 }
3134
3135 #[test]
3136 fn test_validate_empty_workflow_no_condition_errors() {
3137 let evaluator = MockEvaluator::all_true(&[]);
3138 let validator = EdifactValidator::new(evaluator);
3139 let external = NoOpExternalProvider;
3140
3141 let empty_workflow = AhbWorkflow {
3142 pruefidentifikator: String::new(),
3143 description: String::new(),
3144 communication_direction: None,
3145 fields: vec![],
3146 ub_definitions: BTreeMap::new(),
3147 package_conditions: Default::default(),
3148 };
3149
3150 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
3151
3152 assert!(report.is_valid());
3153 }
3154
3155 #[test]
3156 fn test_validate_bare_muss_always_required() {
3157 let evaluator = MockEvaluator::new(vec![]);
3158 let validator = EdifactValidator::new(evaluator);
3159 let external = NoOpExternalProvider;
3160
3161 let workflow = AhbWorkflow {
3162 pruefidentifikator: "55001".to_string(),
3163 description: "Test".to_string(),
3164 communication_direction: Some("NB an LF".to_string()),
3165 fields: vec![AhbFieldRule {
3166 segment_path: "SG2/NAD/3035".to_string(),
3167 name: "Partnerrolle".to_string(),
3168 ahb_status: "Muss".to_string(), codes: vec![],
3170 parent_group_ahb_status: None,
3171 segment_ahb_status: None,
3172 ..Default::default()
3173 }],
3174 ub_definitions: BTreeMap::new(),
3175 package_conditions: Default::default(),
3176 };
3177
3178 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3179
3180 assert!(!report.is_valid());
3182 assert_eq!(report.error_count(), 1);
3183 }
3184
3185 #[test]
3186 fn test_validate_x_status_is_mandatory() {
3187 let evaluator = MockEvaluator::new(vec![]);
3188 let validator = EdifactValidator::new(evaluator);
3189 let external = NoOpExternalProvider;
3190
3191 let workflow = AhbWorkflow {
3192 pruefidentifikator: "55001".to_string(),
3193 description: "Test".to_string(),
3194 communication_direction: None,
3195 fields: vec![AhbFieldRule {
3196 segment_path: "DTM".to_string(),
3197 name: "Datum".to_string(),
3198 ahb_status: "X".to_string(),
3199 codes: vec![],
3200 parent_group_ahb_status: None,
3201 segment_ahb_status: None,
3202 ..Default::default()
3203 }],
3204 ub_definitions: BTreeMap::new(),
3205 package_conditions: Default::default(),
3206 };
3207
3208 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3209
3210 assert!(!report.is_valid());
3211 let errors: Vec<_> = report.errors().collect();
3212 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3213 }
3214
3215 #[test]
3216 fn test_validate_soll_not_mandatory() {
3217 let evaluator = MockEvaluator::new(vec![]);
3218 let validator = EdifactValidator::new(evaluator);
3219 let external = NoOpExternalProvider;
3220
3221 let workflow = AhbWorkflow {
3222 pruefidentifikator: "55001".to_string(),
3223 description: "Test".to_string(),
3224 communication_direction: None,
3225 fields: vec![AhbFieldRule {
3226 segment_path: "DTM".to_string(),
3227 name: "Datum".to_string(),
3228 ahb_status: "Soll".to_string(),
3229 codes: vec![],
3230 parent_group_ahb_status: None,
3231 segment_ahb_status: None,
3232 ..Default::default()
3233 }],
3234 ub_definitions: BTreeMap::new(),
3235 package_conditions: Default::default(),
3236 };
3237
3238 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3239
3240 assert!(report.is_valid());
3242 }
3243
3244 #[test]
3245 fn test_report_includes_metadata() {
3246 let evaluator = MockEvaluator::new(vec![]);
3247 let validator = EdifactValidator::new(evaluator);
3248 let external = NoOpExternalProvider;
3249
3250 let workflow = AhbWorkflow {
3251 pruefidentifikator: "55001".to_string(),
3252 description: String::new(),
3253 communication_direction: None,
3254 fields: vec![],
3255 ub_definitions: BTreeMap::new(),
3256 package_conditions: Default::default(),
3257 };
3258
3259 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
3260
3261 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
3262 assert_eq!(report.level, ValidationLevel::Full);
3263 assert_eq!(report.message_type, "UTILMD");
3264 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
3265 }
3266
3267 #[test]
3268 fn test_validate_with_navigator_returns_report() {
3269 let evaluator = MockEvaluator::all_true(&[]);
3270 let validator = EdifactValidator::new(evaluator);
3271 let external = NoOpExternalProvider;
3272 let nav = crate::eval::NoOpGroupNavigator;
3273
3274 let workflow = AhbWorkflow {
3275 pruefidentifikator: "55001".to_string(),
3276 description: "Test".to_string(),
3277 communication_direction: None,
3278 fields: vec![],
3279 ub_definitions: BTreeMap::new(),
3280 package_conditions: Default::default(),
3281 };
3282
3283 let report = validator.validate_with_navigator(
3284 &[],
3285 &workflow,
3286 &external,
3287 ValidationLevel::Full,
3288 &nav,
3289 );
3290 assert!(report.is_valid());
3291 }
3292
3293 #[test]
3294 fn test_code_validation_composite_paths_valid_codes() {
3295 let evaluator = MockEvaluator::new(vec![]);
3299 let validator = EdifactValidator::new(evaluator);
3300 let external = NoOpExternalProvider;
3301
3302 let unh_segment = OwnedSegment {
3303 id: "UNH".to_string(),
3304 elements: vec![
3305 vec!["ALEXANDE951842".to_string()],
3306 vec![
3307 "UTILMD".to_string(),
3308 "D".to_string(),
3309 "11A".to_string(),
3310 "UN".to_string(),
3311 "S2.1".to_string(),
3312 ],
3313 ],
3314 segment_number: 1,
3315 };
3316
3317 let workflow = AhbWorkflow {
3318 pruefidentifikator: "55001".to_string(),
3319 description: "Test".to_string(),
3320 communication_direction: None,
3321 fields: vec![
3322 AhbFieldRule {
3323 segment_path: "UNH/S009/0065".to_string(),
3324 name: "Nachrichtentyp".to_string(),
3325 ahb_status: "X".to_string(),
3326 codes: vec![AhbCodeRule {
3327 value: "UTILMD".to_string(),
3328 description: "Stammdaten".to_string(),
3329 ahb_status: "X".to_string(),
3330 }],
3331 parent_group_ahb_status: None,
3332 segment_ahb_status: None,
3333 element_index: Some(1),
3334 component_index: Some(0),
3335 ..Default::default()
3336 },
3337 AhbFieldRule {
3338 segment_path: "UNH/S009/0052".to_string(),
3339 name: "Version".to_string(),
3340 ahb_status: "X".to_string(),
3341 codes: vec![AhbCodeRule {
3342 value: "D".to_string(),
3343 description: "Draft".to_string(),
3344 ahb_status: "X".to_string(),
3345 }],
3346 parent_group_ahb_status: None,
3347 segment_ahb_status: None,
3348 element_index: Some(1),
3349 component_index: Some(1),
3350 ..Default::default()
3351 },
3352 ],
3353 ub_definitions: BTreeMap::new(),
3354 package_conditions: Default::default(),
3355 };
3356
3357 let report = validator.validate(
3358 &[unh_segment],
3359 &workflow,
3360 &external,
3361 ValidationLevel::Conditions,
3362 );
3363
3364 let code_errors: Vec<_> = report
3365 .by_category(ValidationCategory::Code)
3366 .filter(|i| i.severity == Severity::Error)
3367 .collect();
3368 assert!(
3369 code_errors.is_empty(),
3370 "Expected no code errors when composite values match allowed codes, got: {:?}",
3371 code_errors
3372 );
3373 }
3374
3375 #[test]
3376 fn test_code_validation_partitions_by_mig_number() {
3377 let evaluator = MockEvaluator::new(vec![]);
3381 let validator = EdifactValidator::new(evaluator);
3382 let external = NoOpExternalProvider;
3383
3384 let sts_7 = OwnedSegment {
3385 id: "STS".to_string(),
3386 elements: vec![
3387 vec!["7".to_string()],
3388 vec![String::new()],
3389 vec!["GH02".to_string()],
3390 vec!["ZW4".to_string()],
3391 ],
3392 segment_number: 1,
3393 };
3394 let sts_e01 = OwnedSegment {
3395 id: "STS".to_string(),
3396 elements: vec![
3397 vec!["E01".to_string()],
3398 vec![String::new()],
3399 vec!["A99".to_string(), "E_0614".to_string()],
3400 ],
3401 segment_number: 2,
3402 };
3403
3404 let workflow = AhbWorkflow {
3405 pruefidentifikator: "55018".to_string(),
3406 description: "Test".to_string(),
3407 communication_direction: None,
3408 fields: vec![
3409 AhbFieldRule {
3411 segment_path: "SG4/STS/C601/9015".to_string(),
3412 name: "Statuskategorie".to_string(),
3413 ahb_status: "X".to_string(),
3414 codes: vec![AhbCodeRule {
3415 value: "7".to_string(),
3416 description: "Transaktionsgrund".to_string(),
3417 ahb_status: "X".to_string(),
3418 }],
3419 parent_group_ahb_status: None,
3420 segment_ahb_status: None,
3421 element_index: Some(0),
3422 component_index: Some(0),
3423 mig_number: Some("00035".to_string()),
3424 format: None,
3425 },
3426 AhbFieldRule {
3427 segment_path: "SG4/STS/C556/9013".to_string(),
3428 name: "Statusanlaß".to_string(),
3429 ahb_status: "X".to_string(),
3430 codes: vec![AhbCodeRule {
3431 value: "E03".to_string(),
3432 description: "Transaktionsgrund".to_string(),
3433 ahb_status: "X".to_string(),
3434 }],
3435 parent_group_ahb_status: None,
3436 segment_ahb_status: None,
3437 element_index: Some(2),
3438 component_index: Some(0),
3439 mig_number: Some("00035".to_string()),
3440 format: None,
3441 },
3442 AhbFieldRule {
3444 segment_path: "SG4/STS/C601/9015".to_string(),
3445 name: "Statuskategorie".to_string(),
3446 ahb_status: "X".to_string(),
3447 codes: vec![AhbCodeRule {
3448 value: "E01".to_string(),
3449 description: "Antwort".to_string(),
3450 ahb_status: "X".to_string(),
3451 }],
3452 parent_group_ahb_status: None,
3453 segment_ahb_status: None,
3454 element_index: Some(0),
3455 component_index: Some(0),
3456 mig_number: Some("00036".to_string()),
3457 format: None,
3458 },
3459 ],
3460 ub_definitions: BTreeMap::new(),
3461 package_conditions: Default::default(),
3462 };
3463
3464 let report = validator.validate(
3465 &[sts_7, sts_e01],
3466 &workflow,
3467 &external,
3468 ValidationLevel::Conditions,
3469 );
3470
3471 let code_errors: Vec<_> = report
3472 .by_category(ValidationCategory::Code)
3473 .filter(|i| {
3474 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3475 })
3476 .collect();
3477 assert_eq!(
3478 code_errors.len(),
3479 1,
3480 "Expected one COD002 (for GH02 only), got: {:?}",
3481 code_errors
3482 );
3483 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3484 }
3485
3486 #[test]
3487 fn test_code_validation_composite_paths_detects_invalid_code() {
3488 let evaluator = MockEvaluator::new(vec![]);
3491 let validator = EdifactValidator::new(evaluator);
3492 let external = NoOpExternalProvider;
3493
3494 let sts_segment = OwnedSegment {
3495 id: "STS".to_string(),
3496 elements: vec![
3497 vec!["7".to_string()],
3498 vec![String::new()],
3499 vec!["GH02".to_string()],
3500 vec!["ZW4".to_string()],
3501 ],
3502 segment_number: 1,
3503 };
3504
3505 let workflow = AhbWorkflow {
3506 pruefidentifikator: "55018".to_string(),
3507 description: "Test".to_string(),
3508 communication_direction: None,
3509 fields: vec![AhbFieldRule {
3510 segment_path: "SG4/STS/C556/9013".to_string(),
3511 name: "Statusanlaß".to_string(),
3512 ahb_status: "X".to_string(),
3513 codes: vec![AhbCodeRule {
3514 value: "E03".to_string(),
3515 description: "Transaktionsgrund".to_string(),
3516 ahb_status: "X".to_string(),
3517 }],
3518 parent_group_ahb_status: None,
3519 segment_ahb_status: None,
3520 element_index: Some(2),
3521 component_index: Some(0),
3522 ..Default::default()
3523 }],
3524 ub_definitions: BTreeMap::new(),
3525 package_conditions: Default::default(),
3526 };
3527
3528 let report = validator.validate(
3529 &[sts_segment],
3530 &workflow,
3531 &external,
3532 ValidationLevel::Conditions,
3533 );
3534
3535 let code_errors: Vec<_> = report
3536 .by_category(ValidationCategory::Code)
3537 .filter(|i| {
3538 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3539 })
3540 .collect();
3541 assert_eq!(
3542 code_errors.len(),
3543 1,
3544 "Expected COD002 for GH02, got: {:?}",
3545 code_errors
3546 );
3547 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3548 }
3549
3550 #[test]
3551 fn test_cross_field_code_validation_valid_qualifiers() {
3552 let evaluator = MockEvaluator::new(vec![]);
3555 let validator = EdifactValidator::new(evaluator);
3556 let external = NoOpExternalProvider;
3557
3558 let nad_ms = OwnedSegment {
3559 id: "NAD".to_string(),
3560 elements: vec![vec!["MS".to_string()]],
3561 segment_number: 4,
3562 };
3563 let nad_mr = OwnedSegment {
3564 id: "NAD".to_string(),
3565 elements: vec![vec!["MR".to_string()]],
3566 segment_number: 5,
3567 };
3568
3569 let workflow = AhbWorkflow {
3570 pruefidentifikator: "55001".to_string(),
3571 description: "Test".to_string(),
3572 communication_direction: None,
3573 fields: vec![
3574 AhbFieldRule {
3575 segment_path: "SG2/NAD/3035".to_string(),
3576 name: "Absender".to_string(),
3577 ahb_status: "X".to_string(),
3578 codes: vec![AhbCodeRule {
3579 value: "MS".to_string(),
3580 description: "Absender".to_string(),
3581 ahb_status: "X".to_string(),
3582 }],
3583 parent_group_ahb_status: None,
3584 segment_ahb_status: None,
3585 ..Default::default()
3586 },
3587 AhbFieldRule {
3588 segment_path: "SG2/NAD/3035".to_string(),
3589 name: "Empfaenger".to_string(),
3590 ahb_status: "X".to_string(),
3591 codes: vec![AhbCodeRule {
3592 value: "MR".to_string(),
3593 description: "Empfaenger".to_string(),
3594 ahb_status: "X".to_string(),
3595 }],
3596 parent_group_ahb_status: None,
3597 segment_ahb_status: None,
3598 ..Default::default()
3599 },
3600 ],
3601 ub_definitions: BTreeMap::new(),
3602 package_conditions: Default::default(),
3603 };
3604
3605 let report = validator.validate(
3606 &[nad_ms, nad_mr],
3607 &workflow,
3608 &external,
3609 ValidationLevel::Conditions,
3610 );
3611
3612 let code_errors: Vec<_> = report
3613 .by_category(ValidationCategory::Code)
3614 .filter(|i| i.severity == Severity::Error)
3615 .collect();
3616 assert!(
3617 code_errors.is_empty(),
3618 "Expected no code errors for valid qualifiers, got: {:?}",
3619 code_errors
3620 );
3621 }
3622
3623 #[test]
3624 fn test_cross_field_code_validation_catches_invalid_qualifier() {
3625 let evaluator = MockEvaluator::new(vec![]);
3627 let validator = EdifactValidator::new(evaluator);
3628 let external = NoOpExternalProvider;
3629
3630 let nad_ms = OwnedSegment {
3631 id: "NAD".to_string(),
3632 elements: vec![vec!["MS".to_string()]],
3633 segment_number: 4,
3634 };
3635 let nad_mt = OwnedSegment {
3636 id: "NAD".to_string(),
3637 elements: vec![vec!["MT".to_string()]], segment_number: 5,
3639 };
3640
3641 let workflow = AhbWorkflow {
3642 pruefidentifikator: "55001".to_string(),
3643 description: "Test".to_string(),
3644 communication_direction: None,
3645 fields: vec![
3646 AhbFieldRule {
3647 segment_path: "SG2/NAD/3035".to_string(),
3648 name: "Absender".to_string(),
3649 ahb_status: "X".to_string(),
3650 codes: vec![AhbCodeRule {
3651 value: "MS".to_string(),
3652 description: "Absender".to_string(),
3653 ahb_status: "X".to_string(),
3654 }],
3655 parent_group_ahb_status: None,
3656 segment_ahb_status: None,
3657 ..Default::default()
3658 },
3659 AhbFieldRule {
3660 segment_path: "SG2/NAD/3035".to_string(),
3661 name: "Empfaenger".to_string(),
3662 ahb_status: "X".to_string(),
3663 codes: vec![AhbCodeRule {
3664 value: "MR".to_string(),
3665 description: "Empfaenger".to_string(),
3666 ahb_status: "X".to_string(),
3667 }],
3668 parent_group_ahb_status: None,
3669 segment_ahb_status: None,
3670 ..Default::default()
3671 },
3672 ],
3673 ub_definitions: BTreeMap::new(),
3674 package_conditions: Default::default(),
3675 };
3676
3677 let report = validator.validate(
3678 &[nad_ms, nad_mt],
3679 &workflow,
3680 &external,
3681 ValidationLevel::Conditions,
3682 );
3683
3684 let code_errors: Vec<_> = report
3685 .by_category(ValidationCategory::Code)
3686 .filter(|i| i.severity == Severity::Error)
3687 .collect();
3688 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
3689 assert!(narrate(code_errors[0]).contains("MT"));
3690 assert!(narrate(code_errors[0]).contains("MR"));
3691 assert!(narrate(code_errors[0]).contains("MS"));
3692 }
3693
3694 #[test]
3695 fn test_cross_field_code_validation_unions_across_groups() {
3696 let evaluator = MockEvaluator::new(vec![]);
3700 let validator = EdifactValidator::new(evaluator);
3701 let external = NoOpExternalProvider;
3702
3703 let segments = vec![
3704 OwnedSegment {
3705 id: "NAD".to_string(),
3706 elements: vec![vec!["MS".to_string()]],
3707 segment_number: 3,
3708 },
3709 OwnedSegment {
3710 id: "NAD".to_string(),
3711 elements: vec![vec!["MR".to_string()]],
3712 segment_number: 4,
3713 },
3714 OwnedSegment {
3715 id: "NAD".to_string(),
3716 elements: vec![vec!["Z04".to_string()]],
3717 segment_number: 20,
3718 },
3719 OwnedSegment {
3720 id: "NAD".to_string(),
3721 elements: vec![vec!["Z09".to_string()]],
3722 segment_number: 21,
3723 },
3724 OwnedSegment {
3725 id: "NAD".to_string(),
3726 elements: vec![vec!["MT".to_string()]], segment_number: 22,
3728 },
3729 ];
3730
3731 let workflow = AhbWorkflow {
3732 pruefidentifikator: "55001".to_string(),
3733 description: "Test".to_string(),
3734 communication_direction: None,
3735 fields: vec![
3736 AhbFieldRule {
3737 segment_path: "SG2/NAD/3035".to_string(),
3738 name: "Absender".to_string(),
3739 ahb_status: "X".to_string(),
3740 codes: vec![AhbCodeRule {
3741 value: "MS".to_string(),
3742 description: "Absender".to_string(),
3743 ahb_status: "X".to_string(),
3744 }],
3745 parent_group_ahb_status: None,
3746 segment_ahb_status: None,
3747 ..Default::default()
3748 },
3749 AhbFieldRule {
3750 segment_path: "SG2/NAD/3035".to_string(),
3751 name: "Empfaenger".to_string(),
3752 ahb_status: "X".to_string(),
3753 codes: vec![AhbCodeRule {
3754 value: "MR".to_string(),
3755 description: "Empfaenger".to_string(),
3756 ahb_status: "X".to_string(),
3757 }],
3758 parent_group_ahb_status: None,
3759 segment_ahb_status: None,
3760 ..Default::default()
3761 },
3762 AhbFieldRule {
3763 segment_path: "SG4/SG12/NAD/3035".to_string(),
3764 name: "Anschlussnutzer".to_string(),
3765 ahb_status: "X".to_string(),
3766 codes: vec![AhbCodeRule {
3767 value: "Z04".to_string(),
3768 description: "Anschlussnutzer".to_string(),
3769 ahb_status: "X".to_string(),
3770 }],
3771 parent_group_ahb_status: None,
3772 segment_ahb_status: None,
3773 ..Default::default()
3774 },
3775 AhbFieldRule {
3776 segment_path: "SG4/SG12/NAD/3035".to_string(),
3777 name: "Korrespondenzanschrift".to_string(),
3778 ahb_status: "X".to_string(),
3779 codes: vec![AhbCodeRule {
3780 value: "Z09".to_string(),
3781 description: "Korrespondenzanschrift".to_string(),
3782 ahb_status: "X".to_string(),
3783 }],
3784 parent_group_ahb_status: None,
3785 segment_ahb_status: None,
3786 ..Default::default()
3787 },
3788 ],
3789 ub_definitions: BTreeMap::new(),
3790 package_conditions: Default::default(),
3791 };
3792
3793 let report =
3794 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3795
3796 let code_errors: Vec<_> = report
3797 .by_category(ValidationCategory::Code)
3798 .filter(|i| i.severity == Severity::Error)
3799 .collect();
3800 assert_eq!(
3801 code_errors.len(),
3802 1,
3803 "Expected exactly one COD002 error for MT, got: {:?}",
3804 code_errors
3805 );
3806 assert!(narrate(code_errors[0]).contains("MT"));
3807 }
3808
3809 #[test]
3810 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3811 let evaluator = MockEvaluator::new(vec![]);
3820 let validator = EdifactValidator::new(evaluator);
3821 let external = NoOpExternalProvider;
3822
3823 let qty_67 = OwnedSegment {
3824 id: "QTY".to_string(),
3825 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
3826 segment_number: 10,
3827 };
3828
3829 let workflow = AhbWorkflow {
3830 pruefidentifikator: "13025".to_string(),
3831 description: "Test".to_string(),
3832 communication_direction: None,
3833 fields: vec![AhbFieldRule {
3834 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
3835 name: "Menge, Qualifier".to_string(),
3836 ahb_status: "X".to_string(),
3837 codes: vec![
3838 AhbCodeRule {
3839 value: "220".to_string(),
3840 description: "Wahrer Wert".to_string(),
3841 ahb_status: "X".to_string(),
3842 },
3843 AhbCodeRule {
3844 value: "67".to_string(),
3845 description: "Ersatzwert".to_string(),
3846 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
3847 },
3848 AhbCodeRule {
3849 value: "Z18".to_string(),
3850 description: "Vorläufiger Wert".to_string(),
3851 ahb_status: "X [35]".to_string(),
3852 },
3853 ],
3854 parent_group_ahb_status: None,
3855 segment_ahb_status: None,
3856 element_index: Some(0),
3857 component_index: Some(0),
3858 ..Default::default()
3859 }],
3860 ub_definitions: BTreeMap::new(),
3861 package_conditions: Default::default(),
3862 };
3863
3864 let report =
3865 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3866
3867 let code_errors: Vec<_> = report
3868 .by_category(ValidationCategory::Code)
3869 .filter(|i| i.severity == Severity::Error)
3870 .collect();
3871 assert!(
3872 code_errors.is_empty(),
3873 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3874 code_errors
3875 );
3876 }
3877
3878 #[test]
3879 fn test_is_qualifier_field_simple_paths() {
3880 assert!(is_qualifier_field("NAD/3035"));
3881 assert!(is_qualifier_field("SG2/NAD/3035"));
3882 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3883 assert!(is_qualifier_field("LOC/3227"));
3884 }
3885
3886 #[test]
3887 fn test_is_qualifier_field_composite_paths() {
3888 assert!(is_qualifier_field("UNH/S009/0065"));
3892 assert!(is_qualifier_field("NAD/C082/3039"));
3893 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3894 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3895 }
3896
3897 #[test]
3898 fn test_is_qualifier_field_bare_segment() {
3899 assert!(!is_qualifier_field("NAD"));
3900 assert!(!is_qualifier_field("SG2/NAD"));
3901 }
3902
3903 #[test]
3904 fn test_is_qualifier_field_rejects_deep_paths() {
3905 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3907 }
3908
3909 #[test]
3910 fn test_missing_qualifier_instance_is_detected() {
3911 let evaluator = MockEvaluator::new(vec![]);
3914 let validator = EdifactValidator::new(evaluator);
3915 let external = NoOpExternalProvider;
3916
3917 let nad_ms = OwnedSegment {
3918 id: "NAD".to_string(),
3919 elements: vec![vec!["MS".to_string()]],
3920 segment_number: 3,
3921 };
3922
3923 let workflow = AhbWorkflow {
3924 pruefidentifikator: "55001".to_string(),
3925 description: "Test".to_string(),
3926 communication_direction: None,
3927 fields: vec![
3928 AhbFieldRule {
3929 segment_path: "SG2/NAD/3035".to_string(),
3930 name: "Absender".to_string(),
3931 ahb_status: "X".to_string(),
3932 codes: vec![AhbCodeRule {
3933 value: "MS".to_string(),
3934 description: "Absender".to_string(),
3935 ahb_status: "X".to_string(),
3936 }],
3937 parent_group_ahb_status: None,
3938 segment_ahb_status: None,
3939 ..Default::default()
3940 },
3941 AhbFieldRule {
3942 segment_path: "SG2/NAD/3035".to_string(),
3943 name: "Empfaenger".to_string(),
3944 ahb_status: "Muss".to_string(),
3945 codes: vec![AhbCodeRule {
3946 value: "MR".to_string(),
3947 description: "Empfaenger".to_string(),
3948 ahb_status: "X".to_string(),
3949 }],
3950 parent_group_ahb_status: None,
3951 segment_ahb_status: None,
3952 ..Default::default()
3953 },
3954 ],
3955 ub_definitions: BTreeMap::new(),
3956 package_conditions: Default::default(),
3957 };
3958
3959 let report =
3960 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
3961
3962 let ahb_errors: Vec<_> = report
3963 .by_category(ValidationCategory::Ahb)
3964 .filter(|i| i.severity == Severity::Error)
3965 .collect();
3966 assert_eq!(
3967 ahb_errors.len(),
3968 1,
3969 "Expected AHB001 for missing NAD+MR, got: {:?}",
3970 ahb_errors
3971 );
3972 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3973 }
3974
3975 #[test]
3976 fn test_present_qualifier_instance_no_error() {
3977 let evaluator = MockEvaluator::new(vec![]);
3979 let validator = EdifactValidator::new(evaluator);
3980 let external = NoOpExternalProvider;
3981
3982 let segments = vec![
3983 OwnedSegment {
3984 id: "NAD".to_string(),
3985 elements: vec![vec!["MS".to_string()]],
3986 segment_number: 3,
3987 },
3988 OwnedSegment {
3989 id: "NAD".to_string(),
3990 elements: vec![vec!["MR".to_string()]],
3991 segment_number: 4,
3992 },
3993 ];
3994
3995 let workflow = AhbWorkflow {
3996 pruefidentifikator: "55001".to_string(),
3997 description: "Test".to_string(),
3998 communication_direction: None,
3999 fields: vec![
4000 AhbFieldRule {
4001 segment_path: "SG2/NAD/3035".to_string(),
4002 name: "Absender".to_string(),
4003 ahb_status: "Muss".to_string(),
4004 codes: vec![AhbCodeRule {
4005 value: "MS".to_string(),
4006 description: "Absender".to_string(),
4007 ahb_status: "X".to_string(),
4008 }],
4009 parent_group_ahb_status: None,
4010 segment_ahb_status: None,
4011 ..Default::default()
4012 },
4013 AhbFieldRule {
4014 segment_path: "SG2/NAD/3035".to_string(),
4015 name: "Empfaenger".to_string(),
4016 ahb_status: "Muss".to_string(),
4017 codes: vec![AhbCodeRule {
4018 value: "MR".to_string(),
4019 description: "Empfaenger".to_string(),
4020 ahb_status: "X".to_string(),
4021 }],
4022 parent_group_ahb_status: None,
4023 segment_ahb_status: None,
4024 ..Default::default()
4025 },
4026 ],
4027 ub_definitions: BTreeMap::new(),
4028 package_conditions: Default::default(),
4029 };
4030
4031 let report =
4032 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
4033
4034 let ahb_errors: Vec<_> = report
4035 .by_category(ValidationCategory::Ahb)
4036 .filter(|i| i.severity == Severity::Error)
4037 .collect();
4038 assert!(
4039 ahb_errors.is_empty(),
4040 "Expected no AHB001 errors, got: {:?}",
4041 ahb_errors
4042 );
4043 }
4044
4045 #[test]
4046 fn test_extract_group_path_key() {
4047 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
4048 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
4049 assert_eq!(extract_group_path_key("NAD/3035"), "");
4050 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
4051 }
4052
4053 fn validate_field_of_absent_group(
4055 group_status: &str,
4056 conditions: Vec<(u32, CR)>,
4057 ) -> ValidationReport {
4058 use mig_types::navigator::GroupNavigator;
4059
4060 struct NoSg9;
4061 impl GroupNavigator for NoSg9 {
4062 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4063 vec![]
4064 }
4065 fn find_segments_with_qualifier_in_group(
4066 &self,
4067 _: &str,
4068 _: usize,
4069 _: &str,
4070 _: &[&str],
4071 _: usize,
4072 ) -> Vec<OwnedSegment> {
4073 vec![]
4074 }
4075 fn group_instance_count(&self, _: &[&str]) -> usize {
4076 0
4077 }
4078 }
4079
4080 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
4081 let workflow = AhbWorkflow {
4082 pruefidentifikator: "44001".to_string(),
4083 description: "Test".to_string(),
4084 communication_direction: None,
4085 fields: vec![AhbFieldRule {
4086 segment_path: "SG9/QTY/C186/6060".to_string(),
4087 name: "Menge".to_string(),
4088 ahb_status: "X".to_string(),
4089 parent_group_ahb_status: Some(group_status.to_string()),
4090 ..Default::default()
4091 }],
4092 ub_definitions: BTreeMap::new(),
4093 package_conditions: Default::default(),
4094 };
4095 validator.validate_with_navigator(
4096 &[],
4097 &workflow,
4098 &NoOpExternalProvider,
4099 ValidationLevel::Conditions,
4100 &NoSg9,
4101 )
4102 }
4103
4104 #[test]
4105 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
4106 let report = validate_field_of_absent_group(
4108 "Soll [47]\r\nMuss [46]",
4109 vec![(47, CR::True), (46, CR::False)],
4110 );
4111 assert!(
4112 report.is_valid(),
4113 "{:?}",
4114 report.errors().collect::<Vec<_>>()
4115 );
4116 }
4117
4118 #[test]
4119 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
4120 let report = validate_field_of_absent_group(
4121 "Soll [47]\r\nMuss [46]",
4122 vec![(47, CR::False), (46, CR::True)],
4123 );
4124 let errors: Vec<_> = report.errors().collect();
4125 assert_eq!(errors.len(), 1, "{errors:?}");
4126 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
4127 }
4128
4129 #[test]
4130 fn test_absent_optional_group_no_missing_field_error() {
4131 use mig_types::navigator::GroupNavigator;
4134
4135 struct NavWithoutSG3;
4136 impl GroupNavigator for NavWithoutSG3 {
4137 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4138 vec![]
4139 }
4140 fn find_segments_with_qualifier_in_group(
4141 &self,
4142 _: &str,
4143 _: usize,
4144 _: &str,
4145 _: &[&str],
4146 _: usize,
4147 ) -> Vec<OwnedSegment> {
4148 vec![]
4149 }
4150 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4151 match group_path {
4152 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
4155 }
4156 }
4157 }
4158
4159 let evaluator = MockEvaluator::new(vec![]);
4160 let validator = EdifactValidator::new(evaluator);
4161 let external = NoOpExternalProvider;
4162 let nav = NavWithoutSG3;
4163
4164 let segments = vec![
4166 OwnedSegment {
4167 id: "NAD".into(),
4168 elements: vec![vec!["MS".into()]],
4169 segment_number: 3,
4170 },
4171 OwnedSegment {
4172 id: "NAD".into(),
4173 elements: vec![vec!["MR".into()]],
4174 segment_number: 4,
4175 },
4176 ];
4177
4178 let workflow = AhbWorkflow {
4179 pruefidentifikator: "55001".to_string(),
4180 description: "Test".to_string(),
4181 communication_direction: None,
4182 fields: vec![
4183 AhbFieldRule {
4184 segment_path: "SG2/SG3/CTA/3139".to_string(),
4185 name: "Funktion des Ansprechpartners, Code".to_string(),
4186 ahb_status: "Muss".to_string(),
4187 codes: vec![],
4188 parent_group_ahb_status: None,
4189 segment_ahb_status: None,
4190 ..Default::default()
4191 },
4192 AhbFieldRule {
4193 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
4194 name: "Name vom Ansprechpartner".to_string(),
4195 ahb_status: "X".to_string(),
4196 codes: vec![],
4197 parent_group_ahb_status: None,
4198 segment_ahb_status: None,
4199 ..Default::default()
4200 },
4201 ],
4202 ub_definitions: BTreeMap::new(),
4203 package_conditions: Default::default(),
4204 };
4205
4206 let report = validator.validate_with_navigator(
4207 &segments,
4208 &workflow,
4209 &external,
4210 ValidationLevel::Conditions,
4211 &nav,
4212 );
4213
4214 let ahb_errors: Vec<_> = report
4215 .by_category(ValidationCategory::Ahb)
4216 .filter(|i| i.severity == Severity::Error)
4217 .collect();
4218 assert!(
4219 ahb_errors.is_empty(),
4220 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
4221 ahb_errors
4222 );
4223 }
4224
4225 #[test]
4226 fn test_present_group_still_checks_mandatory_fields() {
4227 use mig_types::navigator::GroupNavigator;
4229
4230 struct NavWithSG3;
4231 impl GroupNavigator for NavWithSG3 {
4232 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4233 vec![]
4234 }
4235 fn find_segments_with_qualifier_in_group(
4236 &self,
4237 _: &str,
4238 _: usize,
4239 _: &str,
4240 _: &[&str],
4241 _: usize,
4242 ) -> Vec<OwnedSegment> {
4243 vec![]
4244 }
4245 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4246 match group_path {
4247 ["SG2"] => 1,
4248 ["SG2", "SG3"] => 1, _ => 0,
4250 }
4251 }
4252 }
4253
4254 let evaluator = MockEvaluator::new(vec![]);
4255 let validator = EdifactValidator::new(evaluator);
4256 let external = NoOpExternalProvider;
4257 let nav = NavWithSG3;
4258
4259 let segments = vec![OwnedSegment {
4261 id: "NAD".into(),
4262 elements: vec![vec!["MS".into()]],
4263 segment_number: 3,
4264 }];
4265
4266 let workflow = AhbWorkflow {
4267 pruefidentifikator: "55001".to_string(),
4268 description: "Test".to_string(),
4269 communication_direction: None,
4270 fields: vec![AhbFieldRule {
4271 segment_path: "SG2/SG3/CTA/3139".to_string(),
4272 name: "Funktion des Ansprechpartners, Code".to_string(),
4273 ahb_status: "Muss".to_string(),
4274 codes: vec![],
4275 parent_group_ahb_status: None,
4276 segment_ahb_status: None,
4277 ..Default::default()
4278 }],
4279 ub_definitions: BTreeMap::new(),
4280 package_conditions: Default::default(),
4281 };
4282
4283 let report = validator.validate_with_navigator(
4284 &segments,
4285 &workflow,
4286 &external,
4287 ValidationLevel::Conditions,
4288 &nav,
4289 );
4290
4291 let ahb_errors: Vec<_> = report
4292 .by_category(ValidationCategory::Ahb)
4293 .filter(|i| i.severity == Severity::Error)
4294 .collect();
4295 assert_eq!(
4296 ahb_errors.len(),
4297 1,
4298 "Expected AHB001 error when SG3 is present but CTA missing"
4299 );
4300 assert!(narrate(ahb_errors[0]).contains("CTA"));
4301 }
4302
4303 #[test]
4304 fn test_missing_qualifier_with_navigator_is_detected() {
4305 use mig_types::navigator::GroupNavigator;
4308
4309 struct NavWithSG2;
4310 impl GroupNavigator for NavWithSG2 {
4311 fn find_segments_in_group(
4312 &self,
4313 segment_id: &str,
4314 group_path: &[&str],
4315 instance_index: usize,
4316 ) -> Vec<OwnedSegment> {
4317 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
4318 vec![OwnedSegment {
4319 id: "NAD".into(),
4320 elements: vec![vec!["MS".into()]],
4321 segment_number: 3,
4322 }]
4323 } else {
4324 vec![]
4325 }
4326 }
4327 fn find_segments_with_qualifier_in_group(
4328 &self,
4329 _: &str,
4330 _: usize,
4331 _: &str,
4332 _: &[&str],
4333 _: usize,
4334 ) -> Vec<OwnedSegment> {
4335 vec![]
4336 }
4337 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4338 match group_path {
4339 ["SG2"] => 1,
4340 _ => 0,
4341 }
4342 }
4343 }
4344
4345 let evaluator = MockEvaluator::new(vec![]);
4346 let validator = EdifactValidator::new(evaluator);
4347 let external = NoOpExternalProvider;
4348 let nav = NavWithSG2;
4349
4350 let segments = vec![OwnedSegment {
4351 id: "NAD".into(),
4352 elements: vec![vec!["MS".into()]],
4353 segment_number: 3,
4354 }];
4355
4356 let workflow = AhbWorkflow {
4357 pruefidentifikator: "55001".to_string(),
4358 description: "Test".to_string(),
4359 communication_direction: None,
4360 fields: vec![
4361 AhbFieldRule {
4362 segment_path: "SG2/NAD/3035".to_string(),
4363 name: "Absender".to_string(),
4364 ahb_status: "X".to_string(),
4365 codes: vec![AhbCodeRule {
4366 value: "MS".to_string(),
4367 description: "Absender".to_string(),
4368 ahb_status: "X".to_string(),
4369 }],
4370 parent_group_ahb_status: None,
4371 segment_ahb_status: None,
4372 ..Default::default()
4373 },
4374 AhbFieldRule {
4375 segment_path: "SG2/NAD/3035".to_string(),
4376 name: "Empfaenger".to_string(),
4377 ahb_status: "Muss".to_string(),
4378 codes: vec![AhbCodeRule {
4379 value: "MR".to_string(),
4380 description: "Empfaenger".to_string(),
4381 ahb_status: "X".to_string(),
4382 }],
4383 parent_group_ahb_status: None,
4384 segment_ahb_status: None,
4385 ..Default::default()
4386 },
4387 ],
4388 ub_definitions: BTreeMap::new(),
4389 package_conditions: Default::default(),
4390 };
4391
4392 let report = validator.validate_with_navigator(
4393 &segments,
4394 &workflow,
4395 &external,
4396 ValidationLevel::Conditions,
4397 &nav,
4398 );
4399
4400 let ahb_errors: Vec<_> = report
4401 .by_category(ValidationCategory::Ahb)
4402 .filter(|i| i.severity == Severity::Error)
4403 .collect();
4404 assert_eq!(
4405 ahb_errors.len(),
4406 1,
4407 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
4408 ahb_errors
4409 );
4410 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4411 }
4412
4413 #[test]
4414 fn test_optional_group_variant_absent_no_error() {
4415 use mig_types::navigator::GroupNavigator;
4420
4421 struct TestNav;
4422 impl GroupNavigator for TestNav {
4423 fn find_segments_in_group(
4424 &self,
4425 segment_id: &str,
4426 group_path: &[&str],
4427 instance_index: usize,
4428 ) -> Vec<OwnedSegment> {
4429 match (segment_id, group_path, instance_index) {
4430 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
4431 id: "LOC".into(),
4432 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4433 segment_number: 10,
4434 }],
4435 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
4436 id: "NAD".into(),
4437 elements: vec![vec!["MS".into()]],
4438 segment_number: 3,
4439 }],
4440 _ => vec![],
4441 }
4442 }
4443 fn find_segments_with_qualifier_in_group(
4444 &self,
4445 _: &str,
4446 _: usize,
4447 _: &str,
4448 _: &[&str],
4449 _: usize,
4450 ) -> Vec<OwnedSegment> {
4451 vec![]
4452 }
4453 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4454 match group_path {
4455 ["SG2"] => 1,
4456 ["SG4"] => 1,
4457 ["SG4", "SG5"] => 1, _ => 0,
4459 }
4460 }
4461 }
4462
4463 let evaluator = MockEvaluator::new(vec![]);
4464 let validator = EdifactValidator::new(evaluator);
4465 let external = NoOpExternalProvider;
4466 let nav = TestNav;
4467
4468 let segments = vec![
4469 OwnedSegment {
4470 id: "NAD".into(),
4471 elements: vec![vec!["MS".into()]],
4472 segment_number: 3,
4473 },
4474 OwnedSegment {
4475 id: "LOC".into(),
4476 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4477 segment_number: 10,
4478 },
4479 ];
4480
4481 let workflow = AhbWorkflow {
4482 pruefidentifikator: "55001".to_string(),
4483 description: "Test".to_string(),
4484 communication_direction: None,
4485 fields: vec![
4486 AhbFieldRule {
4488 segment_path: "SG2/NAD/3035".to_string(),
4489 name: "Absender".to_string(),
4490 ahb_status: "X".to_string(),
4491 codes: vec![AhbCodeRule {
4492 value: "MS".to_string(),
4493 description: "Absender".to_string(),
4494 ahb_status: "X".to_string(),
4495 }],
4496 parent_group_ahb_status: Some("Muss".to_string()),
4497 segment_ahb_status: None,
4498 ..Default::default()
4499 },
4500 AhbFieldRule {
4501 segment_path: "SG2/NAD/3035".to_string(),
4502 name: "Empfaenger".to_string(),
4503 ahb_status: "Muss".to_string(),
4504 codes: vec![AhbCodeRule {
4505 value: "MR".to_string(),
4506 description: "Empfaenger".to_string(),
4507 ahb_status: "X".to_string(),
4508 }],
4509 parent_group_ahb_status: Some("Muss".to_string()),
4510 segment_ahb_status: None,
4511 ..Default::default()
4512 },
4513 AhbFieldRule {
4515 segment_path: "SG4/SG5/LOC/3227".to_string(),
4516 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4517 ahb_status: "X".to_string(),
4518 codes: vec![AhbCodeRule {
4519 value: "Z16".to_string(),
4520 description: "Marktlokation".to_string(),
4521 ahb_status: "X".to_string(),
4522 }],
4523 parent_group_ahb_status: Some("Kann".to_string()),
4524 segment_ahb_status: None,
4525 ..Default::default()
4526 },
4527 AhbFieldRule {
4528 segment_path: "SG4/SG5/LOC/3227".to_string(),
4529 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4530 ahb_status: "Muss".to_string(),
4531 codes: vec![AhbCodeRule {
4532 value: "Z17".to_string(),
4533 description: "Messlokation".to_string(),
4534 ahb_status: "X".to_string(),
4535 }],
4536 parent_group_ahb_status: Some("Kann".to_string()),
4537 segment_ahb_status: None,
4538 ..Default::default()
4539 },
4540 ],
4541 ub_definitions: BTreeMap::new(),
4542 package_conditions: Default::default(),
4543 };
4544
4545 let report = validator.validate_with_navigator(
4546 &segments,
4547 &workflow,
4548 &external,
4549 ValidationLevel::Conditions,
4550 &nav,
4551 );
4552
4553 let ahb_errors: Vec<_> = report
4554 .by_category(ValidationCategory::Ahb)
4555 .filter(|i| i.severity == Severity::Error)
4556 .collect();
4557
4558 assert_eq!(
4561 ahb_errors.len(),
4562 1,
4563 "Expected only AHB001 for missing NAD+MR, got: {:?}",
4564 ahb_errors
4565 );
4566 assert!(
4567 narrate(ahb_errors[0]).contains("Empfaenger"),
4568 "Error should be for missing NAD+MR (Empfaenger)"
4569 );
4570 }
4571
4572 #[test]
4576 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4577 use mig_types::navigator::GroupNavigator;
4578
4579 struct TestNav;
4580 impl GroupNavigator for TestNav {
4581 fn find_segments_in_group(
4582 &self,
4583 segment_id: &str,
4584 group_path: &[&str],
4585 instance_index: usize,
4586 ) -> Vec<OwnedSegment> {
4587 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4588 vec![OwnedSegment {
4589 id: "SEQ".into(),
4590 elements: vec![vec!["Z03".into()]],
4591 segment_number: 10,
4592 }]
4593 } else {
4594 vec![]
4595 }
4596 }
4597 fn find_segments_with_qualifier_in_group(
4598 &self,
4599 _: &str,
4600 _: usize,
4601 _: &str,
4602 _: &[&str],
4603 _: usize,
4604 ) -> Vec<OwnedSegment> {
4605 vec![]
4606 }
4607 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4608 match group_path {
4609 ["SG4"] => 1,
4610 ["SG4", "SG8"] => 1,
4611 _ => 0,
4612 }
4613 }
4614 }
4615
4616 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
4617 let validator = EdifactValidator::new(evaluator);
4618 let segments = vec![OwnedSegment {
4619 id: "SEQ".into(),
4620 elements: vec![vec!["Z03".into()]],
4621 segment_number: 10,
4622 }];
4623 let seq = |code: &str, group: &str| AhbFieldRule {
4624 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4625 name: format!("Handlung, Code ({code})"),
4626 ahb_status: "X".to_string(),
4627 codes: vec![AhbCodeRule {
4628 value: code.to_string(),
4629 description: String::new(),
4630 ahb_status: "X".to_string(),
4631 }],
4632 parent_group_ahb_status: Some(group.to_string()),
4633 segment_ahb_status: None,
4634 ..Default::default()
4635 };
4636 let workflow = AhbWorkflow {
4637 pruefidentifikator: "44143".to_string(),
4638 description: "Test".to_string(),
4639 communication_direction: None,
4640 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
4641 ub_definitions: BTreeMap::new(),
4642 package_conditions: Default::default(),
4643 };
4644
4645 let report = validator.validate_with_navigator(
4646 &segments,
4647 &workflow,
4648 &NoOpExternalProvider,
4649 ValidationLevel::Conditions,
4650 &TestNav,
4651 );
4652 let missing: Vec<_> = report
4653 .issues
4654 .iter()
4655 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
4656 .collect();
4657 assert!(missing.is_empty(), "{missing:#?}");
4658 }
4659
4660 #[test]
4661 fn test_conditional_group_variant_absent_no_error() {
4662 use mig_types::navigator::GroupNavigator;
4667
4668 struct TestNav;
4669 impl GroupNavigator for TestNav {
4670 fn find_segments_in_group(
4671 &self,
4672 segment_id: &str,
4673 group_path: &[&str],
4674 instance_index: usize,
4675 ) -> Vec<OwnedSegment> {
4676 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
4677 vec![OwnedSegment {
4678 id: "LOC".into(),
4679 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4680 segment_number: 10,
4681 }]
4682 } else {
4683 vec![]
4684 }
4685 }
4686 fn find_segments_with_qualifier_in_group(
4687 &self,
4688 _: &str,
4689 _: usize,
4690 _: &str,
4691 _: &[&str],
4692 _: usize,
4693 ) -> Vec<OwnedSegment> {
4694 vec![]
4695 }
4696 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4697 match group_path {
4698 ["SG4"] => 1,
4699 ["SG4", "SG5"] => 1, _ => 0,
4701 }
4702 }
4703 }
4704
4705 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
4708 let validator = EdifactValidator::new(evaluator);
4709 let external = NoOpExternalProvider;
4710 let nav = TestNav;
4711
4712 let segments = vec![OwnedSegment {
4713 id: "LOC".into(),
4714 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4715 segment_number: 10,
4716 }];
4717
4718 let workflow = AhbWorkflow {
4719 pruefidentifikator: "55001".to_string(),
4720 description: "Test".to_string(),
4721 communication_direction: None,
4722 fields: vec![
4723 AhbFieldRule {
4725 segment_path: "SG4/SG5/LOC/3227".to_string(),
4726 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4727 ahb_status: "X".to_string(),
4728 codes: vec![AhbCodeRule {
4729 value: "Z16".to_string(),
4730 description: "Marktlokation".to_string(),
4731 ahb_status: "X".to_string(),
4732 }],
4733 parent_group_ahb_status: Some("Muss [2061]".to_string()),
4734 segment_ahb_status: None,
4735 ..Default::default()
4736 },
4737 AhbFieldRule {
4739 segment_path: "SG4/SG5/LOC/3227".to_string(),
4740 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4741 ahb_status: "X".to_string(),
4742 codes: vec![AhbCodeRule {
4743 value: "Z17".to_string(),
4744 description: "Messlokation".to_string(),
4745 ahb_status: "X".to_string(),
4746 }],
4747 parent_group_ahb_status: Some("Soll [165]".to_string()),
4748 segment_ahb_status: None,
4749 ..Default::default()
4750 },
4751 ],
4752 ub_definitions: BTreeMap::new(),
4753 package_conditions: Default::default(),
4754 };
4755
4756 let report = validator.validate_with_navigator(
4757 &segments,
4758 &workflow,
4759 &external,
4760 ValidationLevel::Conditions,
4761 &nav,
4762 );
4763
4764 let ahb_errors: Vec<_> = report
4765 .by_category(ValidationCategory::Ahb)
4766 .filter(|i| i.severity == Severity::Error)
4767 .collect();
4768
4769 assert!(
4771 ahb_errors.is_empty(),
4772 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4773 ahb_errors
4774 );
4775 }
4776
4777 #[test]
4778 fn test_conditional_group_variant_unknown_no_error() {
4779 let evaluator = MockEvaluator::new(vec![]);
4785 let validator = EdifactValidator::new(evaluator);
4786 let external = NoOpExternalProvider;
4787
4788 let workflow = AhbWorkflow {
4789 pruefidentifikator: "55001".to_string(),
4790 description: "Test".to_string(),
4791 communication_direction: None,
4792 fields: vec![AhbFieldRule {
4793 segment_path: "SG4/SG5/LOC/3227".to_string(),
4794 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4795 ahb_status: "X".to_string(),
4796 codes: vec![AhbCodeRule {
4797 value: "Z17".to_string(),
4798 description: "Messlokation".to_string(),
4799 ahb_status: "X".to_string(),
4800 }],
4801 parent_group_ahb_status: Some("Soll [165]".to_string()),
4802 segment_ahb_status: None,
4803 ..Default::default()
4804 }],
4805 ub_definitions: BTreeMap::new(),
4806 package_conditions: Default::default(),
4807 };
4808
4809 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4810
4811 let ahb_errors: Vec<_> = report
4812 .by_category(ValidationCategory::Ahb)
4813 .filter(|i| i.severity == Severity::Error)
4814 .collect();
4815
4816 assert!(
4818 ahb_errors.is_empty(),
4819 "Expected no errors when parent group condition is Unknown, got: {:?}",
4820 ahb_errors
4821 );
4822 }
4823
4824 #[test]
4825 fn test_segment_absent_within_present_group_no_error() {
4826 use mig_types::navigator::GroupNavigator;
4830
4831 struct TestNav;
4832 impl GroupNavigator for TestNav {
4833 fn find_segments_in_group(
4834 &self,
4835 segment_id: &str,
4836 group_path: &[&str],
4837 instance_index: usize,
4838 ) -> Vec<OwnedSegment> {
4839 if segment_id == "QTY"
4841 && group_path == ["SG5", "SG6", "SG9", "SG10"]
4842 && instance_index == 0
4843 {
4844 vec![OwnedSegment {
4845 id: "QTY".into(),
4846 elements: vec![vec!["220".into(), "0".into()]],
4847 segment_number: 14,
4848 }]
4849 } else {
4850 vec![]
4851 }
4852 }
4853 fn find_segments_with_qualifier_in_group(
4854 &self,
4855 _: &str,
4856 _: usize,
4857 _: &str,
4858 _: &[&str],
4859 _: usize,
4860 ) -> Vec<OwnedSegment> {
4861 vec![]
4862 }
4863 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4864 match group_path {
4865 ["SG5"] => 1,
4866 ["SG5", "SG6"] => 1,
4867 ["SG5", "SG6", "SG9"] => 1,
4868 ["SG5", "SG6", "SG9", "SG10"] => 1,
4869 _ => 0,
4870 }
4871 }
4872 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4873 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4875 }
4876 }
4877
4878 let evaluator = MockEvaluator::all_true(&[]);
4879 let validator = EdifactValidator::new(evaluator);
4880 let external = NoOpExternalProvider;
4881 let nav = TestNav;
4882
4883 let segments = vec![OwnedSegment {
4884 id: "QTY".into(),
4885 elements: vec![vec!["220".into(), "0".into()]],
4886 segment_number: 14,
4887 }];
4888
4889 let workflow = AhbWorkflow {
4890 pruefidentifikator: "13017".to_string(),
4891 description: "Test".to_string(),
4892 communication_direction: None,
4893 fields: vec![
4894 AhbFieldRule {
4896 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4897 name: "Statuskategorie, Code".to_string(),
4898 ahb_status: "X".to_string(),
4899 codes: vec![],
4900 parent_group_ahb_status: Some("Muss".to_string()),
4901 segment_ahb_status: None,
4902 ..Default::default()
4903 },
4904 AhbFieldRule {
4906 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4907 name: "Statusanlaß, Code".to_string(),
4908 ahb_status: "X [5]".to_string(),
4909 codes: vec![],
4910 parent_group_ahb_status: Some("Muss".to_string()),
4911 segment_ahb_status: None,
4912 ..Default::default()
4913 },
4914 ],
4915 ub_definitions: BTreeMap::new(),
4916 package_conditions: Default::default(),
4917 };
4918
4919 let report = validator.validate_with_navigator(
4920 &segments,
4921 &workflow,
4922 &external,
4923 ValidationLevel::Conditions,
4924 &nav,
4925 );
4926
4927 let ahb_errors: Vec<_> = report
4928 .by_category(ValidationCategory::Ahb)
4929 .filter(|i| i.severity == Severity::Error)
4930 .collect();
4931
4932 assert!(
4933 ahb_errors.is_empty(),
4934 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
4935 ahb_errors
4936 );
4937 }
4938
4939 #[test]
4940 fn test_group_scoped_code_validation_with_navigator() {
4941 use mig_types::navigator::GroupNavigator;
4945
4946 struct TestNav;
4947 impl GroupNavigator for TestNav {
4948 fn find_segments_in_group(
4949 &self,
4950 segment_id: &str,
4951 group_path: &[&str],
4952 _instance_index: usize,
4953 ) -> Vec<OwnedSegment> {
4954 if segment_id != "NAD" {
4955 return vec![];
4956 }
4957 match group_path {
4958 ["SG2"] => vec![
4959 OwnedSegment {
4960 id: "NAD".into(),
4961 elements: vec![vec!["MS".into()]],
4962 segment_number: 3,
4963 },
4964 OwnedSegment {
4965 id: "NAD".into(),
4966 elements: vec![vec!["MT".into()]], segment_number: 4,
4968 },
4969 ],
4970 ["SG4", "SG12"] => vec![
4971 OwnedSegment {
4972 id: "NAD".into(),
4973 elements: vec![vec!["Z04".into()]],
4974 segment_number: 20,
4975 },
4976 OwnedSegment {
4977 id: "NAD".into(),
4978 elements: vec![vec!["Z09".into()]],
4979 segment_number: 21,
4980 },
4981 ],
4982 _ => vec![],
4983 }
4984 }
4985 fn find_segments_with_qualifier_in_group(
4986 &self,
4987 _: &str,
4988 _: usize,
4989 _: &str,
4990 _: &[&str],
4991 _: usize,
4992 ) -> Vec<OwnedSegment> {
4993 vec![]
4994 }
4995 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4996 match group_path {
4997 ["SG2"] | ["SG4", "SG12"] => 1,
4998 _ => 0,
4999 }
5000 }
5001 }
5002
5003 let evaluator = MockEvaluator::new(vec![]);
5004 let validator = EdifactValidator::new(evaluator);
5005 let external = NoOpExternalProvider;
5006 let nav = TestNav;
5007
5008 let workflow = AhbWorkflow {
5009 pruefidentifikator: "55001".to_string(),
5010 description: "Test".to_string(),
5011 communication_direction: None,
5012 fields: vec![
5013 AhbFieldRule {
5014 segment_path: "SG2/NAD/3035".to_string(),
5015 name: "Absender".to_string(),
5016 ahb_status: "X".to_string(),
5017 codes: vec![AhbCodeRule {
5018 value: "MS".to_string(),
5019 description: "Absender".to_string(),
5020 ahb_status: "X".to_string(),
5021 }],
5022 parent_group_ahb_status: None,
5023 segment_ahb_status: None,
5024 ..Default::default()
5025 },
5026 AhbFieldRule {
5027 segment_path: "SG2/NAD/3035".to_string(),
5028 name: "Empfaenger".to_string(),
5029 ahb_status: "X".to_string(),
5030 codes: vec![AhbCodeRule {
5031 value: "MR".to_string(),
5032 description: "Empfaenger".to_string(),
5033 ahb_status: "X".to_string(),
5034 }],
5035 parent_group_ahb_status: None,
5036 segment_ahb_status: None,
5037 ..Default::default()
5038 },
5039 AhbFieldRule {
5040 segment_path: "SG4/SG12/NAD/3035".to_string(),
5041 name: "Anschlussnutzer".to_string(),
5042 ahb_status: "X".to_string(),
5043 codes: vec![AhbCodeRule {
5044 value: "Z04".to_string(),
5045 description: "Anschlussnutzer".to_string(),
5046 ahb_status: "X".to_string(),
5047 }],
5048 parent_group_ahb_status: None,
5049 segment_ahb_status: None,
5050 ..Default::default()
5051 },
5052 AhbFieldRule {
5053 segment_path: "SG4/SG12/NAD/3035".to_string(),
5054 name: "Korrespondenzanschrift".to_string(),
5055 ahb_status: "X".to_string(),
5056 codes: vec![AhbCodeRule {
5057 value: "Z09".to_string(),
5058 description: "Korrespondenzanschrift".to_string(),
5059 ahb_status: "X".to_string(),
5060 }],
5061 parent_group_ahb_status: None,
5062 segment_ahb_status: None,
5063 ..Default::default()
5064 },
5065 ],
5066 ub_definitions: BTreeMap::new(),
5067 package_conditions: Default::default(),
5068 };
5069
5070 let all_segments = vec![
5072 OwnedSegment {
5073 id: "NAD".into(),
5074 elements: vec![vec!["MS".into()]],
5075 segment_number: 3,
5076 },
5077 OwnedSegment {
5078 id: "NAD".into(),
5079 elements: vec![vec!["MT".into()]],
5080 segment_number: 4,
5081 },
5082 OwnedSegment {
5083 id: "NAD".into(),
5084 elements: vec![vec!["Z04".into()]],
5085 segment_number: 20,
5086 },
5087 OwnedSegment {
5088 id: "NAD".into(),
5089 elements: vec![vec!["Z09".into()]],
5090 segment_number: 21,
5091 },
5092 ];
5093
5094 let report = validator.validate_with_navigator(
5095 &all_segments,
5096 &workflow,
5097 &external,
5098 ValidationLevel::Conditions,
5099 &nav,
5100 );
5101
5102 let code_errors: Vec<_> = report
5103 .by_category(ValidationCategory::Code)
5104 .filter(|i| i.severity == Severity::Error)
5105 .collect();
5106
5107 assert_eq!(
5110 code_errors.len(),
5111 1,
5112 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
5113 code_errors
5114 );
5115 assert!(narrate(code_errors[0]).contains("MT"));
5116 assert!(narrate(code_errors[0]).contains("MR"));
5118 assert!(narrate(code_errors[0]).contains("MS"));
5119 assert!(
5120 !narrate(code_errors[0]).contains("Z04"),
5121 "SG4/SG12 codes should not leak into SG2 error"
5122 );
5123 assert!(
5125 code_errors[0]
5126 .field_path
5127 .as_deref()
5128 .unwrap_or("")
5129 .contains("SG2"),
5130 "Error field_path should reference SG2, got: {:?}",
5131 code_errors[0].field_path
5132 );
5133 }
5134
5135 #[test]
5138 fn test_package_cardinality_within_bounds() {
5139 let evaluator = MockEvaluator::all_true(&[]);
5141 let validator = EdifactValidator::new(evaluator);
5142 let external = NoOpExternalProvider;
5143
5144 let segments = vec![OwnedSegment {
5145 id: "STS".into(),
5146 elements: vec![
5147 vec!["Z33".into()], vec![], vec!["E01".into()], ],
5151 segment_number: 5,
5152 }];
5153
5154 let workflow = AhbWorkflow {
5155 pruefidentifikator: "13017".to_string(),
5156 description: "Test".to_string(),
5157 communication_direction: None,
5158 ub_definitions: BTreeMap::new(),
5159 package_conditions: Default::default(),
5160 fields: vec![AhbFieldRule {
5161 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5162 name: "Statusanlaß, Code".to_string(),
5163 ahb_status: "X".to_string(),
5164 element_index: Some(2),
5165 component_index: Some(0),
5166 codes: vec![
5167 AhbCodeRule {
5168 value: "E01".into(),
5169 description: "Code 1".into(),
5170 ahb_status: "X [4P0..1]".into(),
5171 },
5172 AhbCodeRule {
5173 value: "E02".into(),
5174 description: "Code 2".into(),
5175 ahb_status: "X [4P0..1]".into(),
5176 },
5177 ],
5178 parent_group_ahb_status: Some("Muss".to_string()),
5179 segment_ahb_status: None,
5180 mig_number: None,
5181 format: None,
5182 }],
5183 };
5184
5185 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5186 let pkg_errors: Vec<_> = report
5187 .by_category(ValidationCategory::Ahb)
5188 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5189 .collect();
5190 assert!(
5191 pkg_errors.is_empty(),
5192 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
5193 pkg_errors
5194 );
5195 }
5196
5197 #[test]
5198 fn test_package_cardinality_zero_present_min_zero() {
5199 let evaluator = MockEvaluator::all_true(&[]);
5201 let validator = EdifactValidator::new(evaluator);
5202 let external = NoOpExternalProvider;
5203
5204 let segments = vec![OwnedSegment {
5205 id: "STS".into(),
5206 elements: vec![
5207 vec!["Z33".into()],
5208 vec![],
5209 vec!["X99".into()], ],
5211 segment_number: 5,
5212 }];
5213
5214 let workflow = AhbWorkflow {
5215 pruefidentifikator: "13017".to_string(),
5216 description: "Test".to_string(),
5217 communication_direction: None,
5218 ub_definitions: BTreeMap::new(),
5219 package_conditions: Default::default(),
5220 fields: vec![AhbFieldRule {
5221 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5222 name: "Statusanlaß, Code".to_string(),
5223 ahb_status: "X".to_string(),
5224 element_index: Some(2),
5225 component_index: Some(0),
5226 codes: vec![
5227 AhbCodeRule {
5228 value: "E01".into(),
5229 description: "Code 1".into(),
5230 ahb_status: "X [4P0..1]".into(),
5231 },
5232 AhbCodeRule {
5233 value: "E02".into(),
5234 description: "Code 2".into(),
5235 ahb_status: "X [4P0..1]".into(),
5236 },
5237 ],
5238 parent_group_ahb_status: Some("Muss".to_string()),
5239 segment_ahb_status: None,
5240 mig_number: None,
5241 format: None,
5242 }],
5243 };
5244
5245 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5246 let pkg_errors: Vec<_> = report
5247 .by_category(ValidationCategory::Ahb)
5248 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5249 .collect();
5250 assert!(
5251 pkg_errors.is_empty(),
5252 "0 codes, min=0 — no error expected, got: {:?}",
5253 pkg_errors
5254 );
5255 }
5256
5257 #[test]
5258 fn test_package_cardinality_too_many() {
5259 let evaluator = MockEvaluator::all_true(&[]);
5261 let validator = EdifactValidator::new(evaluator);
5262 let external = NoOpExternalProvider;
5263
5264 let segments = vec![
5266 OwnedSegment {
5267 id: "STS".into(),
5268 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
5269 segment_number: 5,
5270 },
5271 OwnedSegment {
5272 id: "STS".into(),
5273 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
5274 segment_number: 6,
5275 },
5276 ];
5277
5278 let workflow = AhbWorkflow {
5279 pruefidentifikator: "13017".to_string(),
5280 description: "Test".to_string(),
5281 communication_direction: None,
5282 ub_definitions: BTreeMap::new(),
5283 package_conditions: Default::default(),
5284 fields: vec![AhbFieldRule {
5285 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5286 name: "Statusanlaß, Code".to_string(),
5287 ahb_status: "X".to_string(),
5288 element_index: Some(2),
5289 component_index: Some(0),
5290 codes: vec![
5291 AhbCodeRule {
5292 value: "E01".into(),
5293 description: "Code 1".into(),
5294 ahb_status: "X [4P0..1]".into(),
5295 },
5296 AhbCodeRule {
5297 value: "E02".into(),
5298 description: "Code 2".into(),
5299 ahb_status: "X [4P0..1]".into(),
5300 },
5301 ],
5302 parent_group_ahb_status: Some("Muss".to_string()),
5303 segment_ahb_status: None,
5304 mig_number: None,
5305 format: None,
5306 }],
5307 };
5308
5309 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5310 let pkg_errors: Vec<_> = report
5311 .by_category(ValidationCategory::Ahb)
5312 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5313 .collect();
5314 assert_eq!(
5315 pkg_errors.len(),
5316 1,
5317 "2 codes present, max=1 — expected 1 error, got: {:?}",
5318 pkg_errors
5319 );
5320 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
5321 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
5322 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
5323 }
5324
5325 #[test]
5326 fn test_package_cardinality_too_few() {
5327 let evaluator = MockEvaluator::all_true(&[]);
5329 let validator = EdifactValidator::new(evaluator);
5330 let external = NoOpExternalProvider;
5331
5332 let segments = vec![OwnedSegment {
5333 id: "STS".into(),
5334 elements: vec![
5335 vec!["Z33".into()],
5336 vec![],
5337 vec!["X99".into()], ],
5339 segment_number: 5,
5340 }];
5341
5342 let workflow = AhbWorkflow {
5343 pruefidentifikator: "13017".to_string(),
5344 description: "Test".to_string(),
5345 communication_direction: None,
5346 ub_definitions: BTreeMap::new(),
5347 package_conditions: Default::default(),
5348 fields: vec![AhbFieldRule {
5349 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5350 name: "Statusanlaß, Code".to_string(),
5351 ahb_status: "X".to_string(),
5352 element_index: Some(2),
5353 component_index: Some(0),
5354 codes: vec![
5355 AhbCodeRule {
5356 value: "E01".into(),
5357 description: "Code 1".into(),
5358 ahb_status: "X [5P1..3]".into(),
5359 },
5360 AhbCodeRule {
5361 value: "E02".into(),
5362 description: "Code 2".into(),
5363 ahb_status: "X [5P1..3]".into(),
5364 },
5365 AhbCodeRule {
5366 value: "E03".into(),
5367 description: "Code 3".into(),
5368 ahb_status: "X [5P1..3]".into(),
5369 },
5370 ],
5371 parent_group_ahb_status: Some("Muss".to_string()),
5372 segment_ahb_status: None,
5373 mig_number: None,
5374 format: None,
5375 }],
5376 };
5377
5378 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5379 let pkg_errors: Vec<_> = report
5380 .by_category(ValidationCategory::Ahb)
5381 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5382 .collect();
5383 assert_eq!(
5384 pkg_errors.len(),
5385 1,
5386 "0 codes present, min=1 — expected 1 error, got: {:?}",
5387 pkg_errors
5388 );
5389 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
5390 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
5391 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
5392 }
5393
5394 #[test]
5395 fn test_package_cardinality_no_packages_in_workflow() {
5396 let evaluator = MockEvaluator::all_true(&[]);
5398 let validator = EdifactValidator::new(evaluator);
5399 let external = NoOpExternalProvider;
5400
5401 let segments = vec![OwnedSegment {
5402 id: "STS".into(),
5403 elements: vec![vec!["E01".into()]],
5404 segment_number: 5,
5405 }];
5406
5407 let workflow = AhbWorkflow {
5408 pruefidentifikator: "13017".to_string(),
5409 description: "Test".to_string(),
5410 communication_direction: None,
5411 ub_definitions: BTreeMap::new(),
5412 package_conditions: Default::default(),
5413 fields: vec![AhbFieldRule {
5414 segment_path: "STS/9015".to_string(),
5415 name: "Status Code".to_string(),
5416 ahb_status: "X".to_string(),
5417 codes: vec![AhbCodeRule {
5418 value: "E01".into(),
5419 description: "Code 1".into(),
5420 ahb_status: "X".into(),
5421 }],
5422 parent_group_ahb_status: Some("Muss".to_string()),
5423 segment_ahb_status: None,
5424 ..Default::default()
5425 }],
5426 };
5427
5428 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5429 let pkg_errors: Vec<_> = report
5430 .by_category(ValidationCategory::Ahb)
5431 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5432 .collect();
5433 assert!(
5434 pkg_errors.is_empty(),
5435 "No packages in workflow — no errors expected"
5436 );
5437 }
5438
5439 #[test]
5440 fn test_package_cardinality_with_condition_and_package() {
5441 let evaluator = MockEvaluator::all_true(&[901]);
5443 let validator = EdifactValidator::new(evaluator);
5444 let external = NoOpExternalProvider;
5445
5446 let segments = vec![OwnedSegment {
5447 id: "STS".into(),
5448 elements: vec![vec![], vec![], vec!["E01".into()]],
5449 segment_number: 5,
5450 }];
5451
5452 let workflow = AhbWorkflow {
5453 pruefidentifikator: "13017".to_string(),
5454 description: "Test".to_string(),
5455 communication_direction: None,
5456 ub_definitions: BTreeMap::new(),
5457 package_conditions: Default::default(),
5458 fields: vec![AhbFieldRule {
5459 segment_path: "SG10/STS/C556/9013".to_string(),
5460 name: "Code".to_string(),
5461 ahb_status: "X".to_string(),
5462 element_index: Some(2),
5463 component_index: Some(0),
5464 codes: vec![
5465 AhbCodeRule {
5466 value: "E01".into(),
5467 description: "Code 1".into(),
5468 ahb_status: "X [901] [4P0..1]".into(),
5469 },
5470 AhbCodeRule {
5471 value: "E02".into(),
5472 description: "Code 2".into(),
5473 ahb_status: "X [901] [4P0..1]".into(),
5474 },
5475 ],
5476 parent_group_ahb_status: Some("Muss".to_string()),
5477 segment_ahb_status: None,
5478 mig_number: None,
5479 format: None,
5480 }],
5481 };
5482
5483 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5484 let pkg_errors: Vec<_> = report
5485 .by_category(ValidationCategory::Ahb)
5486 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5487 .collect();
5488 assert!(
5489 pkg_errors.is_empty(),
5490 "1 code within [4P0..1] bounds — no error, got: {:?}",
5491 pkg_errors
5492 );
5493 }
5494
5495 #[test]
5496 fn test_package_binds_only_where_its_condition_holds() {
5497 let report_for = |package_condition: CR| {
5501 let validator =
5502 EdifactValidator::new(MockEvaluator::new(vec![(902, package_condition)]));
5503 let segments = vec![OwnedSegment {
5504 id: "STS".into(),
5505 elements: vec![vec![], vec![], vec!["E09".into()]],
5506 segment_number: 5,
5507 }];
5508 let workflow = AhbWorkflow {
5509 pruefidentifikator: "13017".to_string(),
5510 description: "Test".to_string(),
5511 communication_direction: None,
5512 ub_definitions: BTreeMap::new(),
5513 package_conditions: BTreeMap::from([(5, ConditionExpr::Ref(902))]),
5514 fields: vec![AhbFieldRule {
5515 segment_path: "SG10/STS/C556/9013".to_string(),
5516 name: "Code".to_string(),
5517 ahb_status: "X".to_string(),
5518 element_index: Some(2),
5519 component_index: Some(0),
5520 codes: vec![
5521 AhbCodeRule {
5522 value: "E01".into(),
5523 description: "Code 1".into(),
5524 ahb_status: "X [5P1..1]".into(),
5525 },
5526 AhbCodeRule {
5527 value: "E09".into(),
5528 description: "Code 9".into(),
5529 ahb_status: "X".into(),
5530 },
5531 ],
5532 parent_group_ahb_status: Some("Muss".to_string()),
5533 segment_ahb_status: None,
5534 mig_number: None,
5535 format: None,
5536 }],
5537 };
5538 validator
5539 .validate(
5540 &segments,
5541 &workflow,
5542 &NoOpExternalProvider,
5543 ValidationLevel::Full,
5544 )
5545 .by_category(ValidationCategory::Ahb)
5546 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5547 .count()
5548 };
5549 assert_eq!(report_for(CR::False), 0, "[902] false: [5P] does not bind");
5550 assert_eq!(report_for(CR::True), 1, "[902] true: [5P1..1] binds");
5551 }
5552
5553 #[test]
5554 fn test_package_cardinality_scoped_per_group_instance() {
5555 use mig_types::navigator::GroupNavigator;
5563
5564 struct TwoSg10s {
5565 sts_a: OwnedSegment,
5566 sts_b: OwnedSegment,
5567 }
5568 impl GroupNavigator for TwoSg10s {
5569 fn find_segments_in_group(
5570 &self,
5571 segment_id: &str,
5572 group_path: &[&str],
5573 instance_index: usize,
5574 ) -> Vec<OwnedSegment> {
5575 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
5576 match instance_index {
5577 0 => vec![self.sts_a.clone()],
5578 1 => vec![self.sts_b.clone()],
5579 _ => vec![],
5580 }
5581 } else {
5582 vec![]
5583 }
5584 }
5585 fn find_segments_with_qualifier_in_group(
5586 &self,
5587 _: &str,
5588 _: usize,
5589 _: &str,
5590 _: &[&str],
5591 _: usize,
5592 ) -> Vec<OwnedSegment> {
5593 vec![]
5594 }
5595 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5596 match group_path {
5597 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
5598 ["SG5", "SG6", "SG9", "SG10"] => 2,
5599 _ => 0,
5600 }
5601 }
5602 }
5603
5604 let sts_a = OwnedSegment {
5605 id: "STS".into(),
5606 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5607 segment_number: 10,
5608 };
5609 let sts_b = OwnedSegment {
5610 id: "STS".into(),
5611 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5612 segment_number: 15,
5613 };
5614 let nav = TwoSg10s {
5615 sts_a: sts_a.clone(),
5616 sts_b: sts_b.clone(),
5617 };
5618
5619 let evaluator = MockEvaluator::all_true(&[]);
5620 let validator = EdifactValidator::new(evaluator);
5621 let external = NoOpExternalProvider;
5622
5623 let workflow = AhbWorkflow {
5624 pruefidentifikator: "13025".to_string(),
5625 description: "Test".to_string(),
5626 communication_direction: None,
5627 ub_definitions: BTreeMap::new(),
5628 package_conditions: Default::default(),
5629 fields: vec![AhbFieldRule {
5630 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5631 name: "Statusanlaß, Code".to_string(),
5632 ahb_status: "X".to_string(),
5633 element_index: Some(2),
5634 component_index: Some(0),
5635 codes: vec![
5636 AhbCodeRule {
5637 value: "E01".into(),
5638 description: "Code 1".into(),
5639 ahb_status: "X [4P0..1]".into(),
5640 },
5641 AhbCodeRule {
5642 value: "E02".into(),
5643 description: "Code 2".into(),
5644 ahb_status: "X [4P0..1]".into(),
5645 },
5646 ],
5647 parent_group_ahb_status: Some("Muss".to_string()),
5648 segment_ahb_status: None,
5649 mig_number: None,
5650 format: None,
5651 }],
5652 };
5653
5654 let report = validator.validate_with_navigator(
5655 &[sts_a, sts_b],
5656 &workflow,
5657 &external,
5658 ValidationLevel::Full,
5659 &nav,
5660 );
5661 let pkg_errors: Vec<_> = report
5662 .by_category(ValidationCategory::Ahb)
5663 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5664 .collect();
5665 assert!(
5666 pkg_errors.is_empty(),
5667 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
5668 pkg_errors
5669 );
5670 }
5671
5672 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
5673 OwnedSegment {
5674 id: id.to_string(),
5675 elements: elements
5676 .into_iter()
5677 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
5678 .collect(),
5679 segment_number: 0,
5680 }
5681 }
5682
5683 #[test]
5694 fn test_package_cardinality_scoped_to_rule_mig_variant() {
5695 use mig_types::navigator::GroupNavigator;
5696
5697 struct TwoSg8Variants {
5698 seq_z01: OwnedSegment,
5699 seq_z45: OwnedSegment,
5700 }
5701 impl GroupNavigator for TwoSg8Variants {
5702 fn find_segments_in_group(
5703 &self,
5704 segment_id: &str,
5705 group_path: &[&str],
5706 instance_index: usize,
5707 ) -> Vec<OwnedSegment> {
5708 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
5709 match instance_index {
5710 0 => vec![self.seq_z01.clone()],
5711 1 => vec![self.seq_z45.clone()],
5712 _ => vec![],
5713 }
5714 } else {
5715 vec![]
5716 }
5717 }
5718 fn find_segments_with_qualifier_in_group(
5719 &self,
5720 _: &str,
5721 _: usize,
5722 _: &str,
5723 _: &[&str],
5724 _: usize,
5725 ) -> Vec<OwnedSegment> {
5726 vec![]
5727 }
5728 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5729 match group_path {
5730 ["SG4"] => 1,
5731 ["SG4", "SG8"] => 2,
5732 _ => 0,
5733 }
5734 }
5735 fn instance_has_mig_number(
5736 &self,
5737 group_path: &[&str],
5738 instance_index: usize,
5739 mig_number: &str,
5740 ) -> bool {
5741 if group_path != ["SG4", "SG8"] {
5742 return true;
5743 }
5744 match (instance_index, mig_number) {
5745 (0, "00115") => true,
5746 (0, _) => false,
5747 (1, "00171") => true,
5748 (1, _) => false,
5749 _ => false,
5750 }
5751 }
5752 }
5753
5754 let seq_z01 = OwnedSegment {
5755 id: "SEQ".into(),
5756 elements: vec![vec!["Z01".into()], vec!["1".into()]],
5757 segment_number: 10,
5758 };
5759 let seq_z45 = OwnedSegment {
5760 id: "SEQ".into(),
5761 elements: vec![vec!["Z45".into()], vec!["1".into()]],
5762 segment_number: 20,
5763 };
5764 let nav = TwoSg8Variants {
5765 seq_z01: seq_z01.clone(),
5766 seq_z45: seq_z45.clone(),
5767 };
5768
5769 let evaluator = MockEvaluator::all_true(&[]);
5770 let validator = EdifactValidator::new(evaluator);
5771 let external = NoOpExternalProvider;
5772
5773 let workflow = AhbWorkflow {
5774 pruefidentifikator: "55218".to_string(),
5775 description: "Test".to_string(),
5776 communication_direction: None,
5777 ub_definitions: BTreeMap::new(),
5778 package_conditions: Default::default(),
5779 fields: vec![AhbFieldRule {
5780 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5781 name: "Handlung, Code".to_string(),
5782 ahb_status: "X".to_string(),
5783 element_index: Some(0),
5784 component_index: Some(0),
5785 codes: vec![
5786 AhbCodeRule {
5787 value: "Z45".into(),
5788 description: "NNA".into(),
5789 ahb_status: "X [1P1..4294967295]".into(),
5790 },
5791 AhbCodeRule {
5792 value: "Z84".into(),
5793 description: "Differenz-NNA".into(),
5794 ahb_status: "X [1P0..4294967295]".into(),
5795 },
5796 ],
5797 parent_group_ahb_status: Some("Muss".to_string()),
5798 segment_ahb_status: None,
5799 mig_number: Some("00171".to_string()),
5800 format: None,
5801 }],
5802 };
5803
5804 let report = validator.validate_with_navigator(
5805 &[seq_z01, seq_z45],
5806 &workflow,
5807 &external,
5808 ValidationLevel::Full,
5809 &nav,
5810 );
5811 let pkg_errors: Vec<_> = report
5812 .by_category(ValidationCategory::Ahb)
5813 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5814 .collect();
5815 assert!(
5816 pkg_errors.is_empty(),
5817 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
5818 pkg_errors
5819 );
5820 }
5821
5822 #[test]
5823 fn test_unt_count_correct() {
5824 let segments = vec![
5826 make_segment("UNH", vec![vec!["001"]]),
5827 make_segment("BGM", vec![vec!["E01"]]),
5828 make_segment("DTM", vec![vec!["137", "20250401"]]),
5829 make_segment("UNT", vec![vec!["4", "001"]]),
5830 ];
5831 assert!(
5832 validate_unt_segment_count(&segments).is_none(),
5833 "Correct count should produce no issue"
5834 );
5835 }
5836
5837 #[test]
5838 fn test_unt_count_mismatch() {
5839 let segments = vec![
5841 make_segment("UNH", vec![vec!["001"]]),
5842 make_segment("BGM", vec![vec!["E01"]]),
5843 make_segment("UNT", vec![vec!["5", "001"]]),
5844 ];
5845 let issue =
5846 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
5847 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5848 assert_eq!(issue.severity, Severity::Error);
5849 assert!(narrate(&issue).contains("declared 5"));
5850 assert!(narrate(&issue).contains("actual 3"));
5851 }
5852
5853 #[test]
5854 fn test_unt_count_excludes_envelope() {
5855 let segments = vec![
5857 make_segment("UNA", vec![]),
5858 make_segment("UNB", vec![vec!["UNOC", "3"]]),
5859 make_segment("UNH", vec![vec!["001"]]),
5860 make_segment("BGM", vec![vec!["E01"]]),
5861 make_segment("UNT", vec![vec!["3", "001"]]),
5862 make_segment("UNZ", vec![vec!["1"]]),
5863 ];
5864 assert!(
5865 validate_unt_segment_count(&segments).is_none(),
5866 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
5867 );
5868 }
5869
5870 #[test]
5871 fn test_unt_count_no_unt_returns_none() {
5872 let segments = vec![
5873 make_segment("UNH", vec![vec!["001"]]),
5874 make_segment("BGM", vec![vec!["E01"]]),
5875 ];
5876 assert!(
5877 validate_unt_segment_count(&segments).is_none(),
5878 "No UNT segment should return None (not our problem)"
5879 );
5880 }
5881
5882 #[test]
5883 fn test_unt_count_rejects_multi_message_input() {
5884 let segments = vec![
5886 make_segment("UNH", vec![vec!["001"]]),
5887 make_segment("BGM", vec![vec!["E01"]]),
5888 make_segment("UNT", vec![vec!["3", "001"]]),
5889 make_segment("UNH", vec![vec!["002"]]),
5890 make_segment("BGM", vec![vec!["E02"]]),
5891 make_segment("UNT", vec![vec!["3", "002"]]),
5892 ];
5893 let issue = validate_unt_segment_count(&segments)
5894 .expect("Multi-message input should produce an error");
5895 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5896 assert!(
5897 narrate(&issue).contains("2 UNH"),
5898 "Should mention UNH count: {}",
5899 narrate(&issue)
5900 );
5901 }
5902
5903 #[test]
5904 fn test_code_validation_accepts_multi_code_variant_qualifier() {
5905 let evaluator = MockEvaluator::new(vec![]);
5912 let validator = EdifactValidator::new(evaluator);
5913 let external = NoOpExternalProvider;
5914
5915 let rff_z39 = OwnedSegment {
5916 id: "RFF".to_string(),
5917 elements: vec![
5918 vec!["RFF".to_string()],
5919 vec!["Z39".to_string(), "REF1".to_string()],
5920 ],
5921 segment_number: 1,
5922 };
5923
5924 let workflow = AhbWorkflow {
5925 pruefidentifikator: "55035".to_string(),
5926 description: "Test".to_string(),
5927 communication_direction: None,
5928 fields: vec![
5929 AhbFieldRule {
5931 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5932 name: "Referenznummer Qualifier".to_string(),
5933 ahb_status: "Muss".to_string(),
5934 codes: vec![
5935 AhbCodeRule {
5936 value: "Z31".to_string(),
5937 description: "".to_string(),
5938 ahb_status: "X".to_string(),
5939 },
5940 AhbCodeRule {
5941 value: "Z39".to_string(),
5942 description: "".to_string(),
5943 ahb_status: "X".to_string(),
5944 },
5945 ],
5946 parent_group_ahb_status: None,
5947 segment_ahb_status: None,
5948 element_index: Some(1),
5949 component_index: Some(0),
5950 mig_number: Some("00075".to_string()),
5951 format: None,
5952 },
5953 AhbFieldRule {
5955 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5956 name: "Referenznummer Qualifier".to_string(),
5957 ahb_status: "Muss".to_string(),
5958 codes: vec![AhbCodeRule {
5959 value: "Z33".to_string(),
5960 description: "".to_string(),
5961 ahb_status: "X".to_string(),
5962 }],
5963 parent_group_ahb_status: None,
5964 segment_ahb_status: None,
5965 element_index: Some(1),
5966 component_index: Some(0),
5967 mig_number: Some("00078".to_string()),
5968 format: None,
5969 },
5970 ],
5971 ub_definitions: BTreeMap::new(),
5972 package_conditions: Default::default(),
5973 };
5974
5975 let report = validator.validate(
5976 &[rff_z39],
5977 &workflow,
5978 &external,
5979 ValidationLevel::Conditions,
5980 );
5981
5982 let code_errors: Vec<_> = report
5983 .by_category(ValidationCategory::Code)
5984 .filter(|i| {
5985 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5986 })
5987 .collect();
5988 assert!(
5989 code_errors.is_empty(),
5990 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
5991 code_errors
5992 );
5993 }
5994
5995 #[test]
5996 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
5997 let evaluator = MockEvaluator::new(vec![]);
6003 let validator = EdifactValidator::new(evaluator);
6004 let external = NoOpExternalProvider;
6005
6006 let pia_5_z12 = OwnedSegment {
6010 id: "PIA".to_string(),
6011 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
6012 segment_number: 1,
6013 };
6014 let pia_5_srw = OwnedSegment {
6015 id: "PIA".to_string(),
6016 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
6017 segment_number: 2,
6018 };
6019
6020 let make_rules = |mig: &str, composite_code: &str| {
6021 vec![
6022 AhbFieldRule {
6023 segment_path: "SG4/SG8/PIA/4347".to_string(),
6024 name: "Produkt-ID-Funktion".to_string(),
6025 ahb_status: "Muss".to_string(),
6026 codes: vec![AhbCodeRule {
6027 value: "5".to_string(),
6028 description: "".to_string(),
6029 ahb_status: "X".to_string(),
6030 }],
6031 parent_group_ahb_status: None,
6032 segment_ahb_status: None,
6033 element_index: Some(0),
6034 component_index: Some(0),
6035 mig_number: Some(mig.to_string()),
6036 format: None,
6037 },
6038 AhbFieldRule {
6039 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
6040 name: "Artikel/Dienstleistung-Identifikator".to_string(),
6041 ahb_status: "Muss".to_string(),
6042 codes: vec![AhbCodeRule {
6043 value: composite_code.to_string(),
6044 description: "".to_string(),
6045 ahb_status: "X".to_string(),
6046 }],
6047 parent_group_ahb_status: None,
6048 segment_ahb_status: None,
6049 element_index: Some(1),
6050 component_index: Some(0),
6051 mig_number: Some(mig.to_string()),
6052 format: None,
6053 },
6054 ]
6055 };
6056
6057 let mut fields = make_rules("00108", "Z12");
6058 fields.extend(make_rules("00197", "SRW"));
6059
6060 let workflow = AhbWorkflow {
6061 pruefidentifikator: "55035".to_string(),
6062 description: "Test".to_string(),
6063 communication_direction: None,
6064 fields,
6065 ub_definitions: BTreeMap::new(),
6066 package_conditions: Default::default(),
6067 };
6068
6069 let report = validator.validate(
6070 &[pia_5_z12, pia_5_srw],
6071 &workflow,
6072 &external,
6073 ValidationLevel::Conditions,
6074 );
6075
6076 let code_errors: Vec<_> = report
6077 .by_category(ValidationCategory::Code)
6078 .filter(|i| {
6079 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6080 })
6081 .collect();
6082 assert!(
6083 code_errors.is_empty(),
6084 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
6085 code_errors
6086 );
6087 }
6088}