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