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 nav = ctx.navigator.unwrap();
1039
1040 for ((group_key, tag), migs) in &by_loc {
1041 let field_path = if group_key.is_empty() {
1042 format!("{tag}/qualifier")
1043 } else {
1044 format!("{group_key}/{tag}/qualifier")
1045 };
1046
1047 let group_path: Vec<&str> = if group_key.is_empty() {
1048 Vec::new()
1049 } else {
1050 group_key.split('/').collect()
1051 };
1052
1053 let tag_qualifiers = known_qualifiers.get(tag);
1054
1055 if group_path.is_empty() {
1056 Self::validate_segments_per_mig(
1057 &ctx.find_segments(tag),
1058 migs,
1059 tag_qualifiers,
1060 tag,
1061 &field_path,
1062 report,
1063 );
1064 } else {
1065 let instance_count = nav.group_instance_count(&group_path);
1066 for i in 0..instance_count {
1067 let owned = nav.find_segments_in_group(tag, &group_path, i);
1068 let refs: Vec<&OwnedSegment> = owned.iter().collect();
1069 Self::validate_segments_per_mig(
1070 &refs,
1071 migs,
1072 tag_qualifiers,
1073 tag,
1074 &field_path,
1075 report,
1076 );
1077 }
1078 }
1079 }
1080 }
1081
1082 fn validate_codes_tag_scoped(
1086 &self,
1087 workflow: &AhbWorkflow,
1088 ctx: &EvaluationContext,
1089 report: &mut ValidationReport,
1090 ) {
1091 let by_loc = partition_codes_by_mig(workflow);
1092 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
1093 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
1095 for ((_group_key, tag), migs) in by_loc {
1096 let merged = by_tag.entry(tag).or_default();
1097 for (mig_key, bucket) in migs {
1098 let entry = merged.entry(mig_key).or_default();
1099 if entry.qualifier_position.is_none() {
1100 entry.qualifier_position = bucket.qualifier_position;
1101 }
1102 if entry.qualifier_position == bucket.qualifier_position {
1103 entry.qualifier_values.extend(&bucket.qualifier_values);
1104 }
1105 for (pos, codes) in bucket.codes {
1106 entry.codes.entry(pos).or_default().extend(codes);
1107 }
1108 }
1109 }
1110
1111 for (tag, migs) in &by_tag {
1112 let field_path = format!("{tag}/qualifier");
1113 Self::validate_segments_per_mig(
1114 &ctx.find_segments(tag),
1115 migs,
1116 known_qualifiers.get(tag),
1117 tag,
1118 &field_path,
1119 report,
1120 );
1121 }
1122 }
1123
1124 fn validate_segments_per_mig(
1133 segments: &[&OwnedSegment],
1134 migs: &HashMap<Option<String>, MigCodeBucket>,
1135 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
1136 tag: &str,
1137 field_path: &str,
1138 report: &mut ValidationReport,
1139 ) {
1140 for seg in segments {
1141 match match_segment_to_mig(seg, migs) {
1142 Some(bucket) => {
1143 for ((el, c), allowed) in &bucket.codes {
1144 if allowed.is_empty() {
1145 continue;
1146 }
1147 Self::check_segments_against_codes(
1148 vec![*seg],
1149 allowed,
1150 tag,
1151 *el,
1152 *c,
1153 field_path,
1154 report,
1155 );
1156 }
1157 }
1158 None => {
1159 let Some(qualifiers) = tag_qualifiers else {
1160 continue;
1161 };
1162 for ((el, c), allowed) in qualifiers {
1167 let Some(actual) = seg
1168 .elements
1169 .get(*el)
1170 .and_then(|e| e.get(*c))
1171 .filter(|v| !v.is_empty())
1172 .map(|s| s.as_str())
1173 else {
1174 continue;
1175 };
1176 if allowed.iter().any(|v| v == actual) {
1177 break;
1179 }
1180 let bucket_values: HashSet<&str> = migs
1183 .values()
1184 .filter(|b| b.qualifier_position == Some((*el, *c)))
1185 .flat_map(|b| b.qualifier_values.iter().copied())
1186 .collect();
1187 if !bucket_values.is_empty() {
1188 Self::check_segments_against_codes(
1189 vec![*seg],
1190 &bucket_values,
1191 tag,
1192 *el,
1193 *c,
1194 field_path,
1195 report,
1196 );
1197 break;
1198 }
1199 }
1200 }
1201 }
1202 }
1203 }
1204
1205 fn validate_answer_lists(
1214 &self,
1215 workflow: &AhbWorkflow,
1216 ctx: &EvaluationContext,
1217 report: &mut ValidationReport,
1218 ) {
1219 let lists = AnswerCodeLists::embedded();
1220 let fv = self.evaluator.format_version();
1221 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1222 for list_rule in &workflow.fields {
1223 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1224 continue;
1225 };
1226 if list_rule.codes.is_empty()
1227 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1228 {
1229 continue;
1230 }
1231 let Some(answer_rule) = workflow
1232 .fields
1233 .iter()
1234 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1235 else {
1236 continue;
1237 };
1238 let at = |rule: &AhbFieldRule| {
1239 (
1240 rule.element_index.unwrap_or(0),
1241 rule.component_index.unwrap_or(0),
1242 )
1243 };
1244 let (list_el, list_c) = at(list_rule);
1245 let (answer_el, answer_c) = at(answer_rule);
1246 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1247 seg.elements
1248 .get(el)
1249 .and_then(|e| e.get(c))
1250 .filter(|v| !v.is_empty())
1251 .cloned()
1252 };
1253 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1254 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1255 continue;
1256 }
1257 let (Some(list), Some(answer)) =
1258 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1259 else {
1260 continue;
1261 };
1262 if !list_rule.codes.iter().any(|c| c.value == list) {
1263 continue;
1264 }
1265 if answer_rule.codes.iter().any(|c| c.value == answer) {
1269 continue;
1270 }
1271 let Some(codes) = lists.codes(fv, &list) else {
1272 continue;
1273 };
1274 if codes.contains(&answer) {
1275 continue;
1276 }
1277 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1278 allowed.sort_unstable();
1279 report.add_issue(
1280 ValidationIssue::new(
1281 Severity::Warning,
1282 IssueKind::AnswerNotInList {
1283 answer: answer.clone(),
1284 list: list.clone(),
1285 allowed: allowed.clone(),
1286 },
1287 )
1288 .with_field_path(&answer_rule.segment_path)
1289 .with_actual(&answer)
1290 .with_expected(allowed.join(", ")),
1291 );
1292 }
1293 }
1294 }
1295
1296 fn check_segments_against_codes(
1298 segments: Vec<&OwnedSegment>,
1299 allowed_codes: &HashSet<&str>,
1300 _tag: &str,
1301 el_idx: usize,
1302 comp_idx: usize,
1303 field_path: &str,
1304 report: &mut ValidationReport,
1305 ) {
1306 for segment in segments {
1307 if let Some(code_value) = segment
1308 .elements
1309 .get(el_idx)
1310 .and_then(|e| e.get(comp_idx))
1311 .filter(|v| !v.is_empty())
1312 {
1313 if !allowed_codes.contains(code_value.as_str()) {
1314 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1315 sorted_codes.sort_unstable();
1316 let kind = IssueKind::CodeNotAllowedForPid {
1317 value: code_value.to_string(),
1318 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1319 };
1320 report.add_issue(
1321 ValidationIssue::new(Severity::Error, kind)
1322 .with_field_path(field_path)
1323 .with_actual(code_value)
1324 .with_expected(sorted_codes.join(", ")),
1325 );
1326 }
1327 }
1328 }
1329 }
1330}
1331
1332fn full_instance_paths<'p>(
1335 nav: &dyn GroupNavigator,
1336 group_path: &[&'p str],
1337) -> Vec<Vec<(&'p str, usize)>> {
1338 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1339 for g in group_path {
1340 paths = paths
1341 .into_iter()
1342 .flat_map(|parent| {
1343 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1344 let mut path = parent.clone();
1345 path.push((*g, i));
1346 path
1347 })
1348 })
1349 .collect();
1350 }
1351 paths
1352}
1353
1354struct VariantEntry<'a> {
1357 tag: String,
1358 element: usize,
1359 component: usize,
1360 codes: HashSet<&'a str>,
1361}
1362
1363fn merged_variant_entry<'a>(
1369 workflow: &'a AhbWorkflow,
1370 group_path: &str,
1371 mig: &str,
1372) -> Option<VariantEntry<'a>> {
1373 let mig: u32 = mig.parse().ok()?;
1374 let prefix = format!("{group_path}/");
1375 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1377 .fields
1378 .iter()
1379 .filter_map(|r| {
1380 let rest = r.segment_path.strip_prefix(&prefix)?;
1381 let tag = rest.split('/').next()?;
1382 if tag.starts_with("SG") {
1383 return None;
1384 }
1385 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1386 })
1387 .collect();
1388 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1390 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1391 .iter()
1392 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1393 .collect();
1394 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1395 if numbers.len() < 2 {
1396 return None;
1397 }
1398 let own = entries
1399 .iter()
1400 .filter(|(_, n, _)| *n <= mig)
1401 .map(|(_, n, _)| *n)
1402 .max()?;
1403 let (rule, _, tag) = entries
1405 .iter()
1406 .filter(|(_, n, _)| *n == own)
1407 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1408 Some(VariantEntry {
1409 tag: tag.to_string(),
1410 element: rule.element_index.unwrap_or(0),
1411 component: rule.component_index.unwrap_or(0),
1412 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1413 })
1414}
1415
1416fn value_length(format: &str, value: &str) -> usize {
1420 let format = format.trim();
1421 if format.starts_with('n') {
1422 value
1423 .chars()
1424 .filter(|c| !matches!(c, '.' | ',' | '-'))
1425 .count()
1426 } else {
1427 value.chars().count()
1428 }
1429}
1430
1431fn max_length(format: &str) -> Option<usize> {
1434 let digits = format
1435 .trim()
1436 .strip_prefix("an")
1437 .or_else(|| format.trim().strip_prefix('a'))
1438 .or_else(|| format.trim().strip_prefix('n'))?;
1439 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1440}
1441
1442fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1453 let derived_from_codes = field
1454 .codes
1455 .first()
1456 .is_some_and(|c| c.ahb_status == field.ahb_status);
1457 if !derived_from_codes && !codes_are_alternatives(field) {
1458 return &field.ahb_status;
1459 }
1460 value
1461 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1462 .map_or(&field.ahb_status, |c| &c.ahb_status)
1463}
1464
1465fn codes_are_alternatives(field: &AhbFieldRule) -> bool {
1476 !field.codes.is_empty()
1477 && field.ahb_status.contains('[')
1478 && field
1479 .codes
1480 .iter()
1481 .all(|c| c.ahb_status.contains('[') && c.ahb_status != field.ahb_status)
1482}
1483
1484fn should_skip_for_parent_group<E: ConditionEvaluator>(
1494 field: &AhbFieldRule,
1495 expr_eval: &ConditionExprEvaluator<E>,
1496 ctx: &EvaluationContext,
1497 ub_definitions: &BTreeMap<String, ConditionExpr>,
1498) -> bool {
1499 let Some(group_status) = field.parent_group_ahb_status.as_deref() else {
1500 return false;
1501 };
1502 if !group_status.contains('[') {
1503 return false;
1504 }
1505 match ConditionParser::parse_status_branches(group_status, ub_definitions) {
1506 Ok(branches) => !branches.iter().any(|branch| {
1507 branch.condition.as_ref().map_or(true, |c| {
1508 expr_eval.evaluate(c, ctx) == ConditionResult::True
1509 })
1510 }),
1511 Err(_) => true,
1512 }
1513}
1514
1515fn parent_group_refused<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().all(|branch| {
1532 branch
1533 .condition
1534 .as_ref()
1535 .is_some_and(|c| expr_eval.evaluate(c, ctx) == ConditionResult::False)
1536 }),
1537 Err(_) => true,
1538 }
1539}
1540
1541fn is_parent_group_required<E: ConditionEvaluator>(
1548 field: &AhbFieldRule,
1549 expr_eval: &ConditionExprEvaluator<E>,
1550 ctx: &EvaluationContext,
1551 ub_definitions: &BTreeMap<String, ConditionExpr>,
1552) -> bool {
1553 field
1554 .parent_group_ahb_status
1555 .as_deref()
1556 .is_some_and(|status| {
1557 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1558 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1559 })
1560}
1561
1562fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1571 let segment_id = extract_segment_id(&field.segment_path);
1572
1573 if !field.codes.is_empty() {
1579 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1580 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1581 let matching = ctx.find_segments(&segment_id);
1582 return matching.iter().any(|seg| {
1583 seg.elements
1584 .get(el_idx)
1585 .and_then(|e| e.get(comp_idx))
1586 .is_some_and(|v| required_codes.contains(&v.as_str()))
1587 });
1588 }
1589 if is_qualifier_field(&field.segment_path) {
1592 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1593 let el_idx = field.element_index.unwrap_or(0);
1594 let comp_idx = field.component_index.unwrap_or(0);
1595 let matching = ctx.find_segments(&segment_id);
1596 return matching.iter().any(|seg| {
1597 seg.elements
1598 .get(el_idx)
1599 .and_then(|e| e.get(comp_idx))
1600 .is_some_and(|v| required_codes.contains(&v.as_str()))
1601 });
1602 }
1603 }
1604
1605 ctx.has_segment(&segment_id)
1606}
1607
1608fn is_group_variant_absent(
1623 ctx: &EvaluationContext,
1624 field: &AhbFieldRule,
1625 is_group_mandatory: bool,
1626) -> bool {
1627 let group_path: Vec<&str> = field
1628 .segment_path
1629 .split('/')
1630 .take_while(|p| p.starts_with("SG"))
1631 .collect();
1632
1633 if group_path.is_empty() {
1634 return false;
1635 }
1636
1637 let nav = match ctx.navigator {
1638 Some(nav) => nav,
1639 None => return false,
1640 };
1641
1642 let instance_count = nav.group_instance_count(&group_path);
1643
1644 if instance_count == 0 {
1649 if !is_group_mandatory {
1650 return true;
1651 }
1652 return false;
1654 }
1655
1656 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1661 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1663 let segment_id = extract_segment_id(&field.segment_path);
1664 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1665
1666 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1667 nav.find_segments_in_group(&segment_id, &group_path, i)
1668 .iter()
1669 .any(|seg| {
1670 seg.elements
1671 .first()
1672 .and_then(|e| e.first())
1673 .is_some_and(|v| required_codes.contains(&v.as_str()))
1674 })
1675 });
1676
1677 if !any_instance_has_qualifier {
1678 return true; }
1680 }
1681 }
1682
1683 let segment_id = extract_segment_id(&field.segment_path);
1692 let segment_absent_from_all = (0..instance_count).all(|i| {
1693 nav.find_segments_in_group(&segment_id, &group_path, i)
1694 .is_empty()
1695 });
1696 if segment_absent_from_all {
1697 let group_has_other_segments =
1698 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1699 if group_has_other_segments {
1700 return true;
1701 }
1702 }
1703
1704 false
1705}
1706
1707fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1709 out.extend(group.fields.iter());
1710 for child in &group.children {
1711 collect_nodes_depth_first(child, out);
1712 }
1713}
1714
1715#[allow(clippy::too_many_arguments)]
1719fn evaluate_node<E: ConditionEvaluator>(
1720 node: &AhbNode,
1721 ctx: &EvaluationContext,
1722 expr_eval: &ConditionExprEvaluator<E>,
1723 evaluator: &E,
1724 ub_definitions: &BTreeMap<String, ConditionExpr>,
1725 tag_migs: &HashMap<String, HashSet<&str>>,
1726 instance_index: Option<usize>,
1727 slot_holds: bool,
1728 report: &mut ValidationReport,
1729) {
1730 let field = node.rule;
1731
1732 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1734
1735 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1740 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1741 if length > max {
1742 let mut issue = ValidationIssue::new(
1743 Severity::Error,
1744 IssueKind::ValueTooLong {
1745 field_name: field.name.clone(),
1746 length,
1747 max,
1748 },
1749 )
1750 .with_field_path(&field.segment_path)
1751 .with_actual(value)
1752 .with_expected(field.format.clone().unwrap_or_default());
1753 if let Some(idx) = instance_index {
1754 issue = issue.with_instance_index(idx);
1755 }
1756 if let Some(num) = node.matched_segment_number {
1757 issue = issue.with_position(crate::SegmentPosition {
1758 segment_number: num,
1759 byte_offset: 0,
1760 message_number: 1,
1761 });
1762 }
1763 report.add_issue(attach_field_position(issue, field));
1764 }
1765 }
1766
1767 if let (Some(value), Some(format)) = (node.value, field.format.as_deref()) {
1770 if slot_holds
1771 && format.trim().starts_with('n')
1772 && field.codes.is_empty()
1773 && !value.is_empty()
1774 && !crate::eval::format_validators::is_edifact_number(value)
1775 {
1776 let mut issue = ValidationIssue::new(
1777 Severity::Error,
1778 IssueKind::ValueNotNumeric {
1779 field_name: field.name.clone(),
1780 },
1781 )
1782 .with_field_path(&field.segment_path)
1783 .with_actual(value)
1784 .with_expected(format);
1785 if let Some(idx) = instance_index {
1786 issue = issue.with_instance_index(idx);
1787 }
1788 if let Some(num) = node.matched_segment_number {
1789 issue = issue.with_position(crate::SegmentPosition {
1790 segment_number: num,
1791 byte_offset: 0,
1792 message_number: 1,
1793 });
1794 }
1795 report.add_issue(attach_field_position(issue, field));
1796 }
1797 }
1798
1799 if parent_group_refused(field, expr_eval, ctx, ub_definitions) {
1803 return;
1804 }
1805
1806 let status = governing_status(field, node.value);
1809 let StatusResolution {
1810 result: condition_result,
1811 kind,
1812 unknown_ids,
1813 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1814 let mandatory = kind == Some(StatusKind::Muss);
1815
1816 match condition_result {
1817 ConditionResult::True => {
1818 if mandatory && node.value.is_none() {
1827 let tag = extract_segment_id(&field.segment_path);
1828 let single_variant_present = node.segment_elements.is_none()
1829 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1830 && is_field_present(ctx, field);
1831 let segment_optional_and_absent = node.segment_elements.is_none()
1836 && field
1837 .segment_ahb_status
1838 .as_deref()
1839 .is_some_and(is_optional_segment_status);
1840 let segment_not_required_and_absent = node.segment_elements.is_none()
1847 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1848 let status = expr_eval.resolve_status(st, &node_ctx, ub_definitions);
1849 status.result != ConditionResult::True
1850 || status.kind != Some(StatusKind::Muss)
1851 });
1852 if !single_variant_present
1853 && !segment_optional_and_absent
1854 && !segment_not_required_and_absent
1855 {
1856 let mut issue = ValidationIssue::new(
1857 Severity::Error,
1858 IssueKind::MissingRequiredField {
1859 field_name: field.name.clone(),
1860 },
1861 )
1862 .with_field_path(&field.segment_path)
1863 .with_rule(&field.ahb_status);
1864 if let Some(first_code) = field.codes.first() {
1865 issue.expected_value = Some(first_code.value.clone());
1866 }
1867 if let Some(idx) = instance_index {
1868 issue = issue.with_instance_index(idx);
1869 }
1870 if let Some(num) = node.matched_segment_number {
1871 issue = issue.with_position(crate::SegmentPosition {
1872 segment_number: num,
1873 byte_offset: 0,
1874 message_number: 1,
1875 });
1876 }
1877 issue = attach_field_position(issue, field);
1878 report.add_issue(issue);
1879 }
1880 }
1881 }
1882 ConditionResult::False => {
1883 if mandatory && node.value.is_some() {
1886 let mut issue = ValidationIssue::new(
1887 Severity::Error,
1888 IssueKind::FieldConditionNotSatisfied {
1889 field_name: field.name.clone(),
1890 },
1891 )
1892 .with_field_path(&field.segment_path)
1893 .with_rule(&field.ahb_status);
1894 if let Some(idx) = instance_index {
1895 issue = issue.with_instance_index(idx);
1896 }
1897 if let Some(num) = node.matched_segment_number {
1898 issue = issue.with_position(crate::SegmentPosition {
1899 segment_number: num,
1900 byte_offset: 0,
1901 message_number: 1,
1902 });
1903 }
1904 issue = attach_field_position(issue, field);
1905 report.add_issue(issue);
1906 }
1907 }
1908 ConditionResult::Unknown => {
1909 let mut external_ids = Vec::new();
1911 let mut undetermined_ids = Vec::new();
1912 let mut missing_ids = Vec::new();
1913 for id in unknown_ids {
1914 if evaluator.is_external(id) {
1915 external_ids.push(id);
1916 } else if evaluator.is_known(id) {
1917 undetermined_ids.push(id);
1918 } else {
1919 missing_ids.push(id);
1920 }
1921 }
1922
1923 let mut issue = ValidationIssue::new(
1924 Severity::Info,
1925 IssueKind::ConditionUnknown {
1926 field_name: field.name.clone(),
1927 unresolved: UnresolvedConditions {
1928 external: external_ids,
1929 undetermined: undetermined_ids,
1930 missing: missing_ids,
1931 },
1932 },
1933 )
1934 .with_field_path(&field.segment_path)
1935 .with_rule(&field.ahb_status);
1936 if let Some(idx) = instance_index {
1937 issue = issue.with_instance_index(idx);
1938 }
1939 if let Some(num) = node.matched_segment_number {
1940 issue = issue.with_position(crate::SegmentPosition {
1941 segment_number: num,
1942 byte_offset: 0,
1943 message_number: 1,
1944 });
1945 }
1946 issue = attach_field_position(issue, field);
1947 report.add_issue(issue);
1948 }
1949 }
1950}
1951
1952fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1957 match field.element_index {
1958 Some(el) => {
1959 let element_pos = (el as u32) + 2;
1960 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1961 issue.with_field_position(element_pos, component_pos)
1962 }
1963 None => issue,
1964 }
1965}
1966
1967fn report_repeated_in_transaction(
1974 transaction: &AhbGroupNode,
1975 transaction_index: usize,
1976 once: u32,
1977 ctx: &EvaluationContext,
1978 report: &mut ValidationReport,
1979) {
1980 let mut counts: BTreeMap<(String, String), (usize, String)> = BTreeMap::new();
1982 fn visit<'a>(
1983 node: &'a AhbGroupNode<'a>,
1984 path: &mut Vec<(&'a str, usize)>,
1985 once: u32,
1986 ctx: &EvaluationContext,
1987 counts: &mut BTreeMap<(String, String), (usize, String)>,
1988 ) {
1989 let ids: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1990 let mut seen: HashSet<Option<&str>> = HashSet::new();
1991 for n in &node.fields {
1992 let rule = n.rule;
1993 let Some(status) = rule.segment_ahb_status.as_deref() else {
1994 continue;
1995 };
1996 if Some(status) == node.ahb_status
1997 || rule.element_index.unwrap_or(0) != 0
1998 || rule.component_index.unwrap_or(0) != 0
1999 || rule.codes.is_empty()
2000 || !crate::eval::binds_once(status, once)
2001 || !seen.insert(rule.mig_number.as_deref())
2002 {
2003 continue;
2004 }
2005 let tag = extract_segment_id(&rule.segment_path);
2006 let sent = ctx.navigator().map_or(0, |nav| {
2007 nav.find_segments_in_instance(&tag, path)
2008 .iter()
2009 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2010 .count()
2011 });
2012 let entry = counts
2013 .entry((
2014 format!("{}/{tag}", ids.join("/")),
2015 rule.mig_number.clone().unwrap_or_default(),
2016 ))
2017 .or_insert((0, status.to_string()));
2018 entry.0 += sent;
2019 }
2020 for child in &node.children {
2021 if let Some(status) = child
2022 .ahb_status
2023 .filter(|s| crate::eval::binds_once(s, once))
2024 {
2025 let variant = child
2026 .fields
2027 .first()
2028 .and_then(|n| n.rule.mig_number.clone())
2029 .unwrap_or_default();
2030 let entry = counts
2031 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
2032 .or_insert((0, status.to_string()));
2033 entry.0 += 1;
2034 }
2035 path.push((child.group_id, child.instance_index));
2036 visit(child, path, once, ctx, counts);
2037 path.pop();
2038 }
2039 }
2040 let mut path = vec![(transaction.group_id, transaction_index)];
2041 visit(transaction, &mut path, once, ctx, &mut counts);
2042 for ((path, _), (count, status)) in counts {
2043 if count > 1 {
2044 report.add_issue(
2045 ValidationIssue::new(
2046 Severity::Error,
2047 IssueKind::RepeatedInTransaction {
2048 path: path.clone(),
2049 count,
2050 },
2051 )
2052 .with_field_path(path)
2053 .with_rule(status)
2054 .with_instance_index(transaction_index),
2055 );
2056 }
2057 }
2058}
2059
2060#[allow(clippy::too_many_arguments)]
2067fn refuse_segments<E: ConditionEvaluator>(
2068 fields: &[AhbNode],
2069 group_status: Option<&str>,
2070 path: &[(&str, usize)],
2071 scoped_ctx: &EvaluationContext,
2072 evaluator: &E,
2073 ub_definitions: &BTreeMap<String, ConditionExpr>,
2074 instance_index: usize,
2075 report: &mut ValidationReport,
2076) {
2077 let mut seen: Vec<u32> = Vec::new();
2078 for node in fields {
2079 let rule = node.rule;
2080 let (Some(status), Some(value), Some(number)) = (
2081 rule.segment_ahb_status.as_deref(),
2082 node.value,
2083 node.matched_segment_number,
2084 ) else {
2085 continue;
2086 };
2087 let qualifier =
2090 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
2091 if !qualifier
2092 || seen.contains(&number)
2093 || Some(status) == group_status
2094 || !rule.codes.iter().any(|c| c.value == value)
2095 {
2096 continue;
2097 }
2098 seen.push(number);
2099 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2100 continue;
2101 }
2102 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2103 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
2104 report.add_issue(
2105 ValidationIssue::new(
2106 Severity::Warning,
2107 IssueKind::SegmentNotAllowed {
2108 segment: segment.join("/"),
2109 },
2110 )
2111 .with_field_path(segment.join("/"))
2112 .with_rule(status)
2113 .with_instance_index(instance_index)
2114 .with_position(crate::SegmentPosition {
2115 segment_number: number,
2116 byte_offset: 0,
2117 message_number: 1,
2118 }),
2119 );
2120 }
2121}
2122
2123#[allow(clippy::too_many_arguments)]
2132fn walk_group_instance<E: ConditionEvaluator>(
2133 group: &AhbGroupNode,
2134 path: &[(&str, usize)],
2135 scoped_ctx: &EvaluationContext,
2136 expr_eval: &ConditionExprEvaluator<E>,
2137 evaluator: &E,
2138 ub_definitions: &BTreeMap<String, ConditionExpr>,
2139 tag_migs: &HashMap<String, HashSet<&str>>,
2140 instance_index: usize,
2141 report_segments: bool,
2142 report: &mut ValidationReport,
2143) {
2144 let mut group_refused = false;
2147 if let Some(status) = group.ahb_status {
2148 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2149 group_refused = true;
2150 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2151 let mut issue = ValidationIssue::new(
2152 Severity::Warning,
2153 IssueKind::GroupNotAllowed {
2154 group: group_path.join("/"),
2155 },
2156 )
2157 .with_field_path(group_path.join("/"))
2158 .with_rule(status)
2159 .with_instance_index(instance_index);
2160 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
2161 issue = issue.with_position(crate::SegmentPosition {
2162 segment_number: num,
2163 byte_offset: 0,
2164 message_number: 1,
2165 });
2166 }
2167 report.add_issue(issue);
2168 }
2169 }
2170 if report_segments && !group_refused {
2171 refuse_segments(
2172 &group.fields,
2173 group.ahb_status,
2174 path,
2175 scoped_ctx,
2176 evaluator,
2177 ub_definitions,
2178 instance_index,
2179 report,
2180 );
2181 }
2182 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
2186 .fields
2187 .iter()
2188 .filter(|n| {
2189 n.rule.element_index.unwrap_or(0) == 0
2190 && n.rule.component_index.unwrap_or(0) == 0
2191 && !n.rule.codes.is_empty()
2192 })
2193 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
2194 .collect();
2195 let holds = |node: &AhbNode| {
2196 let (Some(codes), Some(elements)) = (
2197 slot_codes.get(&node.rule.mig_number.as_deref()),
2198 node.segment_elements,
2199 ) else {
2200 return true;
2201 };
2202 let qualifier = elements
2203 .first()
2204 .and_then(|e| e.first())
2205 .map(String::as_str)
2206 .unwrap_or_default();
2207 codes.iter().any(|c| c.value == qualifier)
2208 };
2209 for node in &group.fields {
2210 evaluate_node(
2211 node,
2212 scoped_ctx,
2213 expr_eval,
2214 evaluator,
2215 ub_definitions,
2216 tag_migs,
2217 Some(instance_index),
2218 holds(node),
2219 report,
2220 );
2221 }
2222 for child in &group.children {
2223 let mut child_path = path.to_vec();
2224 child_path.push((child.group_id, child.instance_index));
2225 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2226 walk_group_instance(
2227 child,
2228 &child_path,
2229 &child_ctx,
2230 expr_eval,
2231 evaluator,
2232 ub_definitions,
2233 tag_migs,
2234 instance_index,
2235 report_segments,
2236 report,
2237 );
2238 }
2239}
2240
2241#[allow(clippy::too_many_arguments)]
2251fn report_absent_children<E: ConditionEvaluator>(
2252 group: &AhbGroupNode,
2253 path: &[(&str, usize)],
2254 scoped_ctx: &EvaluationContext,
2255 absent_eval: &ConditionExprEvaluator<crate::eval::AbsentTarget<'_, E>>,
2256 evaluator: &E,
2257 ub_definitions: &BTreeMap<String, ConditionExpr>,
2258 instance_index: usize,
2259 report: &mut ValidationReport,
2260) {
2261 if group.ahb_status.is_some_and(|status| {
2262 crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions)
2263 }) {
2264 return;
2265 }
2266 for rule in &group.absent_children {
2267 if should_skip_for_parent_group(rule, absent_eval, scoped_ctx, ub_definitions)
2268 || !is_parent_group_required(rule, absent_eval, scoped_ctx, ub_definitions)
2269 {
2270 continue;
2271 }
2272 let required = |status: &str| {
2273 let resolved = absent_eval.resolve_status(status, scoped_ctx, ub_definitions);
2274 resolved.result == ConditionResult::True && resolved.kind == Some(StatusKind::Muss)
2275 };
2276 if !required(&rule.ahb_status)
2277 || rule
2278 .segment_ahb_status
2279 .as_deref()
2280 .is_some_and(|status| !required(status))
2281 {
2282 continue;
2283 }
2284 let mut issue = ValidationIssue::new(
2285 Severity::Error,
2286 IssueKind::MissingRequiredField {
2287 field_name: rule.name.clone(),
2288 },
2289 )
2290 .with_field_path(&rule.segment_path)
2291 .with_rule(&rule.ahb_status)
2292 .with_instance_index(instance_index);
2293 if let Some(first_code) = rule.codes.first() {
2294 issue.expected_value = Some(first_code.value.clone());
2295 }
2296 report.add_issue(attach_field_position(issue, rule));
2297 }
2298 for child in &group.children {
2299 let mut child_path = path.to_vec();
2300 child_path.push((child.group_id, child.instance_index));
2301 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2302 report_absent_children(
2303 child,
2304 &child_path,
2305 &child_ctx,
2306 absent_eval,
2307 evaluator,
2308 ub_definitions,
2309 instance_index,
2310 report,
2311 );
2312 }
2313}
2314
2315fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2316 match expr {
2317 ConditionExpr::Package { id, min, max } => {
2318 out.push((*id, *min, *max));
2319 }
2320 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2321 for e in exprs {
2322 collect_packages(e, out);
2323 }
2324 }
2325 ConditionExpr::Xor(left, right) => {
2326 collect_packages(left, out);
2327 collect_packages(right, out);
2328 }
2329 ConditionExpr::Not(inner) => {
2330 collect_packages(inner, out);
2331 }
2332 ConditionExpr::Ref(_) => {}
2333 }
2334}
2335
2336fn is_optional_segment_status(status: &str) -> bool {
2346 let trimmed = status.trim();
2347 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
2348}
2349
2350fn is_qualifier_field(path: &str) -> bool {
2360 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
2361 matches!(parts.len(), 2 | 3)
2363}
2364
2365#[derive(Default)]
2373struct MigCodeBucket<'a> {
2374 qualifier_position: Option<(usize, usize)>,
2377 qualifier_values: HashSet<&'a str>,
2380 codes: HashMap<(usize, usize), HashSet<&'a str>>,
2383}
2384
2385fn partition_codes_by_mig(
2390 workflow: &AhbWorkflow,
2391) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
2392 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
2393 for field in &workflow.fields {
2394 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2395 continue;
2396 }
2397 if codes_are_alternatives(field) {
2400 continue;
2401 }
2402 let tag = extract_segment_id(&field.segment_path);
2403 let group_key = extract_group_path_key(&field.segment_path);
2404 let mig = field.mig_number.clone();
2405 let el = field.element_index.unwrap_or(0);
2406 let c = field.component_index.unwrap_or(0);
2407
2408 let bucket = out
2409 .entry((group_key, tag))
2410 .or_default()
2411 .entry(mig)
2412 .or_default();
2413
2414 let required: Vec<&str> = field
2421 .codes
2422 .iter()
2423 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2424 .map(|code| code.value.as_str())
2425 .collect();
2426
2427 if !required.is_empty() {
2434 if bucket.qualifier_position.is_none() {
2435 bucket.qualifier_position = Some((el, c));
2436 }
2437 if bucket.qualifier_position == Some((el, c)) {
2438 bucket.qualifier_values.extend(required.iter().copied());
2439 }
2440 }
2441
2442 for v in required {
2443 bucket.codes.entry((el, c)).or_default().insert(v);
2444 }
2445 }
2446 out
2447}
2448
2449fn global_qualifiers_by_tag(
2455 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
2456) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
2457 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
2458 for ((_group, tag), migs) in by_loc {
2459 let tag_entry = out.entry(tag.clone()).or_default();
2460 for bucket in migs.values() {
2461 if let Some(pos) = bucket.qualifier_position {
2462 tag_entry
2463 .entry(pos)
2464 .or_default()
2465 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2466 }
2467 }
2468 }
2469 out
2470}
2471
2472fn match_segment_to_mig<'a, 'b>(
2483 seg: &OwnedSegment,
2484 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
2485) -> Option<&'a MigCodeBucket<'b>> {
2486 let actual_at = |el: usize, c: usize| -> &str {
2487 seg.elements
2488 .get(el)
2489 .and_then(|e| e.get(c))
2490 .map(|s| s.as_str())
2491 .unwrap_or("")
2492 };
2493
2494 let mut best: Option<&MigCodeBucket> = None;
2495 let mut best_matches = 0usize;
2496
2497 for bucket in migs.values() {
2498 let Some((el, c)) = bucket.qualifier_position else {
2499 continue;
2500 };
2501 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2502 continue;
2503 }
2504 let extra_matches = bucket
2505 .codes
2506 .iter()
2507 .filter(|(pos, _)| **pos != (el, c))
2508 .filter(|((e, k), allowed)| {
2509 let v = actual_at(*e, *k);
2510 !v.is_empty() && allowed.contains(v)
2511 })
2512 .count();
2513 if best.is_none() || extra_matches > best_matches {
2514 best = Some(bucket);
2515 best_matches = extra_matches;
2516 }
2517 }
2518 best
2519}
2520
2521fn extract_group_path_key(path: &str) -> String {
2526 let sg_parts: Vec<&str> = path
2527 .split('/')
2528 .take_while(|p| p.starts_with("SG"))
2529 .collect();
2530 sg_parts.join("/")
2531}
2532
2533fn extract_segment_id(path: &str) -> String {
2535 for part in path.split('/') {
2536 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2538 continue;
2539 }
2540 if part.len() >= 3
2542 && part
2543 .chars()
2544 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2545 {
2546 return part.to_string();
2547 }
2548 }
2549 path.split('/').next_back().unwrap_or(path).to_string()
2551}
2552
2553pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2565 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2567 if unh_count > 1 {
2568 return Some(ValidationIssue::new(
2569 Severity::Error,
2570 IssueKind::UntCountNotVerifiable { unh_count },
2571 ));
2572 }
2573
2574 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2576 let declared: usize = unt.get_element(0).parse().ok()?;
2577
2578 let actual = segments
2580 .iter()
2581 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2582 .count();
2583
2584 if declared != actual {
2585 Some(
2586 ValidationIssue::new(
2587 Severity::Error,
2588 IssueKind::UntSegmentCountMismatch { declared, actual },
2589 )
2590 .with_field_path("UNT/0074")
2591 .with_expected(actual.to_string())
2592 .with_actual(declared.to_string()),
2593 )
2594 } else {
2595 None
2596 }
2597}
2598
2599#[cfg(test)]
2600mod tests {
2601
2602 #[test]
2603 fn a_numeric_length_counts_digits_only() {
2604 assert_eq!(value_length("n..15", "-12.50"), 4);
2605 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
2606 assert_eq!(value_length("n5", "55001"), 5);
2607 assert_eq!(value_length("an..3", "Z.1"), 3);
2608 assert_eq!(value_length("an..35", "Müller"), 6);
2609 }
2610 use super::super::codes::ErrorCodes;
2611 use super::super::issue::ValidationCategory;
2612 use super::*;
2613 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2614 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2615 use std::collections::HashMap;
2616
2617 fn narrate(issue: &ValidationIssue) -> String {
2620 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2621 }
2622
2623 struct MockEvaluator {
2625 results: HashMap<u32, CR>,
2626 }
2627
2628 impl MockEvaluator {
2629 fn new(results: Vec<(u32, CR)>) -> Self {
2630 Self {
2631 results: results.into_iter().collect(),
2632 }
2633 }
2634
2635 fn all_true(ids: &[u32]) -> Self {
2636 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2637 }
2638 }
2639
2640 impl ConditionEvaluator for MockEvaluator {
2641 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2642 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2643 }
2644 fn is_external(&self, _condition: u32) -> bool {
2645 false
2646 }
2647 fn message_type(&self) -> &str {
2648 "UTILMD"
2649 }
2650 fn format_version(&self) -> &str {
2651 "FV2510"
2652 }
2653 }
2654
2655 #[test]
2658 fn a_present_code_is_judged_by_its_own_status() {
2659 let code = |value: &str, status: &str| AhbCodeRule {
2661 value: value.to_string(),
2662 description: String::new(),
2663 ahb_status: status.to_string(),
2664 };
2665 let field = AhbFieldRule {
2666 segment_path: "SG4/STS/C556/9013".to_string(),
2667 ahb_status: "X [192]".to_string(),
2668 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2669 ..Default::default()
2670 };
2671 assert_eq!(governing_status(&field, Some("E03")), "X");
2672 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2673 assert_eq!(governing_status(&field, None), "X [192]");
2674 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2675
2676 let own = AhbFieldRule {
2678 ahb_status: "X [931]".to_string(),
2679 codes: vec![code("303", "X")],
2680 ..Default::default()
2681 };
2682 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2683 assert!(!codes_are_alternatives(&own));
2684
2685 let alternatives = AhbFieldRule {
2688 ahb_status: "X [153] ∧ [538]".to_string(),
2689 codes: vec![
2690 code("Z74", "X [103] ∧ [537]"),
2691 code("Z75", "X [103] ∧ [537]"),
2692 ],
2693 ..Default::default()
2694 };
2695 assert!(codes_are_alternatives(&alternatives));
2696 assert_eq!(
2697 governing_status(&alternatives, Some("Z74")),
2698 "X [103] ∧ [537]"
2699 );
2700 assert_eq!(
2701 governing_status(&alternatives, Some("A01")),
2702 "X [153] ∧ [538]"
2703 );
2704 assert_eq!(governing_status(&alternatives, None), "X [153] ∧ [538]");
2705
2706 let restricting = AhbFieldRule {
2708 ahb_status: "X [2] ∨ [3]".to_string(),
2709 codes: vec![code("UNH", "X"), code("UNT", "X")],
2710 ..Default::default()
2711 };
2712 assert!(!codes_are_alternatives(&restricting));
2713 assert_eq!(governing_status(&restricting, Some("UNH")), "X [2] ∨ [3]");
2714 }
2715
2716 #[test]
2717 fn test_extract_segment_id_simple() {
2718 assert_eq!(extract_segment_id("NAD"), "NAD");
2719 }
2720
2721 #[test]
2722 fn test_extract_segment_id_with_sg_prefix() {
2723 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
2724 }
2725
2726 #[test]
2727 fn test_extract_segment_id_nested_sg() {
2728 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
2729 }
2730
2731 #[test]
2734 fn test_validate_missing_mandatory_field() {
2735 let evaluator = MockEvaluator::all_true(&[182, 152]);
2736 let validator = EdifactValidator::new(evaluator);
2737 let external = NoOpExternalProvider;
2738
2739 let workflow = AhbWorkflow {
2740 pruefidentifikator: "11001".to_string(),
2741 description: "Test".to_string(),
2742 communication_direction: None,
2743 fields: vec![AhbFieldRule {
2744 segment_path: "SG2/NAD/C082/3039".to_string(),
2745 name: "MP-ID des MSB".to_string(),
2746 ahb_status: "Muss [182] ∧ [152]".to_string(),
2747 codes: vec![],
2748 parent_group_ahb_status: None,
2749 segment_ahb_status: None,
2750 ..Default::default()
2751 }],
2752 ub_definitions: BTreeMap::new(),
2753 package_conditions: Default::default(),
2754 };
2755
2756 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2758
2759 assert!(!report.is_valid());
2761 let errors: Vec<_> = report.errors().collect();
2762 assert_eq!(errors.len(), 1);
2763 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2764 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
2765 }
2766
2767 #[test]
2768 fn test_validate_condition_false_no_error() {
2769 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
2771 let validator = EdifactValidator::new(evaluator);
2772 let external = NoOpExternalProvider;
2773
2774 let workflow = AhbWorkflow {
2775 pruefidentifikator: "11001".to_string(),
2776 description: "Test".to_string(),
2777 communication_direction: None,
2778 fields: vec![AhbFieldRule {
2779 segment_path: "NAD".to_string(),
2780 name: "Partnerrolle".to_string(),
2781 ahb_status: "Muss [182] ∧ [152]".to_string(),
2782 codes: vec![],
2783 parent_group_ahb_status: None,
2784 segment_ahb_status: None,
2785 ..Default::default()
2786 }],
2787 ub_definitions: BTreeMap::new(),
2788 package_conditions: Default::default(),
2789 };
2790
2791 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2792
2793 assert!(report.is_valid());
2795 }
2796
2797 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
2799 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
2800 let workflow = AhbWorkflow {
2801 pruefidentifikator: "11001".to_string(),
2802 description: "Test".to_string(),
2803 communication_direction: None,
2804 fields: vec![AhbFieldRule {
2805 segment_path: "SG2/NAD/C082/3039".to_string(),
2806 name: "MP-ID".to_string(),
2807 ahb_status: status.to_string(),
2808 ..Default::default()
2809 }],
2810 ub_definitions: BTreeMap::new(),
2811 package_conditions: Default::default(),
2812 };
2813 validator.validate(
2814 &[],
2815 &workflow,
2816 &NoOpExternalProvider,
2817 ValidationLevel::Conditions,
2818 )
2819 }
2820
2821 #[test]
2822 fn a_later_soll_line_alone_does_not_require_the_field() {
2823 let report = validate_absent_field(
2825 "Muss [2119] \r\nSoll [130]",
2826 vec![(2119, CR::False), (130, CR::True)],
2827 );
2828 assert!(
2829 report.is_valid(),
2830 "{:?}",
2831 report.errors().collect::<Vec<_>>()
2832 );
2833 }
2834
2835 #[test]
2836 fn a_later_muss_line_requires_the_field() {
2837 let report = validate_absent_field(
2839 "Soll [47]\r\nMuss [46]",
2840 vec![(47, CR::False), (46, CR::True)],
2841 );
2842 let errors: Vec<_> = report.errors().collect();
2843 assert_eq!(errors.len(), 1);
2844 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2845 }
2846
2847 #[test]
2848 fn a_weaker_unknown_line_does_not_stop_a_muss() {
2849 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
2851 let errors: Vec<_> = report.errors().collect();
2852 assert_eq!(errors.len(), 1);
2853 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2854 }
2855
2856 #[test]
2857 fn a_stricter_unknown_line_leaves_the_status_undecided() {
2858 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
2859 assert!(report.is_valid());
2860 let infos: Vec<_> = report.infos().collect();
2861 assert_eq!(infos.len(), 1, "{infos:?}");
2862 match &infos[0].kind {
2863 IssueKind::ConditionUnknown { unresolved, .. } => {
2864 assert_eq!(unresolved.missing, vec![2119]);
2865 }
2866 other => panic!("expected ConditionUnknown, got {other:?}"),
2867 }
2868 }
2869
2870 #[test]
2871 fn test_validate_condition_unknown_adds_info() {
2872 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
2874 let validator = EdifactValidator::new(evaluator);
2876 let external = NoOpExternalProvider;
2877
2878 let workflow = AhbWorkflow {
2879 pruefidentifikator: "11001".to_string(),
2880 description: "Test".to_string(),
2881 communication_direction: None,
2882 fields: vec![AhbFieldRule {
2883 segment_path: "NAD".to_string(),
2884 name: "Partnerrolle".to_string(),
2885 ahb_status: "Muss [182] ∧ [152]".to_string(),
2886 codes: vec![],
2887 parent_group_ahb_status: None,
2888 segment_ahb_status: None,
2889 ..Default::default()
2890 }],
2891 ub_definitions: BTreeMap::new(),
2892 package_conditions: Default::default(),
2893 };
2894
2895 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2896
2897 assert!(report.is_valid());
2899 let infos: Vec<_> = report.infos().collect();
2900 assert_eq!(infos.len(), 1);
2901 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
2902 }
2903
2904 #[test]
2905 fn test_validate_structure_level_skips_conditions() {
2906 let evaluator = MockEvaluator::all_true(&[182, 152]);
2907 let validator = EdifactValidator::new(evaluator);
2908 let external = NoOpExternalProvider;
2909
2910 let workflow = AhbWorkflow {
2911 pruefidentifikator: "11001".to_string(),
2912 description: "Test".to_string(),
2913 communication_direction: None,
2914 fields: vec![AhbFieldRule {
2915 segment_path: "NAD".to_string(),
2916 name: "Partnerrolle".to_string(),
2917 ahb_status: "Muss [182] ∧ [152]".to_string(),
2918 codes: vec![],
2919 parent_group_ahb_status: None,
2920 segment_ahb_status: None,
2921 ..Default::default()
2922 }],
2923 ub_definitions: BTreeMap::new(),
2924 package_conditions: Default::default(),
2925 };
2926
2927 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
2929
2930 assert!(report.is_valid());
2932 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
2933 }
2934
2935 #[test]
2936 fn test_validate_empty_workflow_no_condition_errors() {
2937 let evaluator = MockEvaluator::all_true(&[]);
2938 let validator = EdifactValidator::new(evaluator);
2939 let external = NoOpExternalProvider;
2940
2941 let empty_workflow = AhbWorkflow {
2942 pruefidentifikator: String::new(),
2943 description: String::new(),
2944 communication_direction: None,
2945 fields: vec![],
2946 ub_definitions: BTreeMap::new(),
2947 package_conditions: Default::default(),
2948 };
2949
2950 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
2951
2952 assert!(report.is_valid());
2953 }
2954
2955 #[test]
2956 fn test_validate_bare_muss_always_required() {
2957 let evaluator = MockEvaluator::new(vec![]);
2958 let validator = EdifactValidator::new(evaluator);
2959 let external = NoOpExternalProvider;
2960
2961 let workflow = AhbWorkflow {
2962 pruefidentifikator: "55001".to_string(),
2963 description: "Test".to_string(),
2964 communication_direction: Some("NB an LF".to_string()),
2965 fields: vec![AhbFieldRule {
2966 segment_path: "SG2/NAD/3035".to_string(),
2967 name: "Partnerrolle".to_string(),
2968 ahb_status: "Muss".to_string(), codes: vec![],
2970 parent_group_ahb_status: None,
2971 segment_ahb_status: None,
2972 ..Default::default()
2973 }],
2974 ub_definitions: BTreeMap::new(),
2975 package_conditions: Default::default(),
2976 };
2977
2978 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2979
2980 assert!(!report.is_valid());
2982 assert_eq!(report.error_count(), 1);
2983 }
2984
2985 #[test]
2986 fn test_validate_x_status_is_mandatory() {
2987 let evaluator = MockEvaluator::new(vec![]);
2988 let validator = EdifactValidator::new(evaluator);
2989 let external = NoOpExternalProvider;
2990
2991 let workflow = AhbWorkflow {
2992 pruefidentifikator: "55001".to_string(),
2993 description: "Test".to_string(),
2994 communication_direction: None,
2995 fields: vec![AhbFieldRule {
2996 segment_path: "DTM".to_string(),
2997 name: "Datum".to_string(),
2998 ahb_status: "X".to_string(),
2999 codes: vec![],
3000 parent_group_ahb_status: None,
3001 segment_ahb_status: None,
3002 ..Default::default()
3003 }],
3004 ub_definitions: BTreeMap::new(),
3005 package_conditions: Default::default(),
3006 };
3007
3008 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3009
3010 assert!(!report.is_valid());
3011 let errors: Vec<_> = report.errors().collect();
3012 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3013 }
3014
3015 #[test]
3016 fn test_validate_soll_not_mandatory() {
3017 let evaluator = MockEvaluator::new(vec![]);
3018 let validator = EdifactValidator::new(evaluator);
3019 let external = NoOpExternalProvider;
3020
3021 let workflow = AhbWorkflow {
3022 pruefidentifikator: "55001".to_string(),
3023 description: "Test".to_string(),
3024 communication_direction: None,
3025 fields: vec![AhbFieldRule {
3026 segment_path: "DTM".to_string(),
3027 name: "Datum".to_string(),
3028 ahb_status: "Soll".to_string(),
3029 codes: vec![],
3030 parent_group_ahb_status: None,
3031 segment_ahb_status: None,
3032 ..Default::default()
3033 }],
3034 ub_definitions: BTreeMap::new(),
3035 package_conditions: Default::default(),
3036 };
3037
3038 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3039
3040 assert!(report.is_valid());
3042 }
3043
3044 #[test]
3045 fn test_report_includes_metadata() {
3046 let evaluator = MockEvaluator::new(vec![]);
3047 let validator = EdifactValidator::new(evaluator);
3048 let external = NoOpExternalProvider;
3049
3050 let workflow = AhbWorkflow {
3051 pruefidentifikator: "55001".to_string(),
3052 description: String::new(),
3053 communication_direction: None,
3054 fields: vec![],
3055 ub_definitions: BTreeMap::new(),
3056 package_conditions: Default::default(),
3057 };
3058
3059 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
3060
3061 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
3062 assert_eq!(report.level, ValidationLevel::Full);
3063 assert_eq!(report.message_type, "UTILMD");
3064 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
3065 }
3066
3067 #[test]
3068 fn test_validate_with_navigator_returns_report() {
3069 let evaluator = MockEvaluator::all_true(&[]);
3070 let validator = EdifactValidator::new(evaluator);
3071 let external = NoOpExternalProvider;
3072 let nav = crate::eval::NoOpGroupNavigator;
3073
3074 let workflow = AhbWorkflow {
3075 pruefidentifikator: "55001".to_string(),
3076 description: "Test".to_string(),
3077 communication_direction: None,
3078 fields: vec![],
3079 ub_definitions: BTreeMap::new(),
3080 package_conditions: Default::default(),
3081 };
3082
3083 let report = validator.validate_with_navigator(
3084 &[],
3085 &workflow,
3086 &external,
3087 ValidationLevel::Full,
3088 &nav,
3089 );
3090 assert!(report.is_valid());
3091 }
3092
3093 #[test]
3094 fn test_code_validation_composite_paths_valid_codes() {
3095 let evaluator = MockEvaluator::new(vec![]);
3099 let validator = EdifactValidator::new(evaluator);
3100 let external = NoOpExternalProvider;
3101
3102 let unh_segment = OwnedSegment {
3103 id: "UNH".to_string(),
3104 elements: vec![
3105 vec!["ALEXANDE951842".to_string()],
3106 vec![
3107 "UTILMD".to_string(),
3108 "D".to_string(),
3109 "11A".to_string(),
3110 "UN".to_string(),
3111 "S2.1".to_string(),
3112 ],
3113 ],
3114 segment_number: 1,
3115 };
3116
3117 let workflow = AhbWorkflow {
3118 pruefidentifikator: "55001".to_string(),
3119 description: "Test".to_string(),
3120 communication_direction: None,
3121 fields: vec![
3122 AhbFieldRule {
3123 segment_path: "UNH/S009/0065".to_string(),
3124 name: "Nachrichtentyp".to_string(),
3125 ahb_status: "X".to_string(),
3126 codes: vec![AhbCodeRule {
3127 value: "UTILMD".to_string(),
3128 description: "Stammdaten".to_string(),
3129 ahb_status: "X".to_string(),
3130 }],
3131 parent_group_ahb_status: None,
3132 segment_ahb_status: None,
3133 element_index: Some(1),
3134 component_index: Some(0),
3135 ..Default::default()
3136 },
3137 AhbFieldRule {
3138 segment_path: "UNH/S009/0052".to_string(),
3139 name: "Version".to_string(),
3140 ahb_status: "X".to_string(),
3141 codes: vec![AhbCodeRule {
3142 value: "D".to_string(),
3143 description: "Draft".to_string(),
3144 ahb_status: "X".to_string(),
3145 }],
3146 parent_group_ahb_status: None,
3147 segment_ahb_status: None,
3148 element_index: Some(1),
3149 component_index: Some(1),
3150 ..Default::default()
3151 },
3152 ],
3153 ub_definitions: BTreeMap::new(),
3154 package_conditions: Default::default(),
3155 };
3156
3157 let report = validator.validate(
3158 &[unh_segment],
3159 &workflow,
3160 &external,
3161 ValidationLevel::Conditions,
3162 );
3163
3164 let code_errors: Vec<_> = report
3165 .by_category(ValidationCategory::Code)
3166 .filter(|i| i.severity == Severity::Error)
3167 .collect();
3168 assert!(
3169 code_errors.is_empty(),
3170 "Expected no code errors when composite values match allowed codes, got: {:?}",
3171 code_errors
3172 );
3173 }
3174
3175 #[test]
3176 fn test_code_validation_partitions_by_mig_number() {
3177 let evaluator = MockEvaluator::new(vec![]);
3181 let validator = EdifactValidator::new(evaluator);
3182 let external = NoOpExternalProvider;
3183
3184 let sts_7 = OwnedSegment {
3185 id: "STS".to_string(),
3186 elements: vec![
3187 vec!["7".to_string()],
3188 vec![String::new()],
3189 vec!["GH02".to_string()],
3190 vec!["ZW4".to_string()],
3191 ],
3192 segment_number: 1,
3193 };
3194 let sts_e01 = OwnedSegment {
3195 id: "STS".to_string(),
3196 elements: vec![
3197 vec!["E01".to_string()],
3198 vec![String::new()],
3199 vec!["A99".to_string(), "E_0614".to_string()],
3200 ],
3201 segment_number: 2,
3202 };
3203
3204 let workflow = AhbWorkflow {
3205 pruefidentifikator: "55018".to_string(),
3206 description: "Test".to_string(),
3207 communication_direction: None,
3208 fields: vec![
3209 AhbFieldRule {
3211 segment_path: "SG4/STS/C601/9015".to_string(),
3212 name: "Statuskategorie".to_string(),
3213 ahb_status: "X".to_string(),
3214 codes: vec![AhbCodeRule {
3215 value: "7".to_string(),
3216 description: "Transaktionsgrund".to_string(),
3217 ahb_status: "X".to_string(),
3218 }],
3219 parent_group_ahb_status: None,
3220 segment_ahb_status: None,
3221 element_index: Some(0),
3222 component_index: Some(0),
3223 mig_number: Some("00035".to_string()),
3224 format: None,
3225 },
3226 AhbFieldRule {
3227 segment_path: "SG4/STS/C556/9013".to_string(),
3228 name: "Statusanlaß".to_string(),
3229 ahb_status: "X".to_string(),
3230 codes: vec![AhbCodeRule {
3231 value: "E03".to_string(),
3232 description: "Transaktionsgrund".to_string(),
3233 ahb_status: "X".to_string(),
3234 }],
3235 parent_group_ahb_status: None,
3236 segment_ahb_status: None,
3237 element_index: Some(2),
3238 component_index: Some(0),
3239 mig_number: Some("00035".to_string()),
3240 format: None,
3241 },
3242 AhbFieldRule {
3244 segment_path: "SG4/STS/C601/9015".to_string(),
3245 name: "Statuskategorie".to_string(),
3246 ahb_status: "X".to_string(),
3247 codes: vec![AhbCodeRule {
3248 value: "E01".to_string(),
3249 description: "Antwort".to_string(),
3250 ahb_status: "X".to_string(),
3251 }],
3252 parent_group_ahb_status: None,
3253 segment_ahb_status: None,
3254 element_index: Some(0),
3255 component_index: Some(0),
3256 mig_number: Some("00036".to_string()),
3257 format: None,
3258 },
3259 ],
3260 ub_definitions: BTreeMap::new(),
3261 package_conditions: Default::default(),
3262 };
3263
3264 let report = validator.validate(
3265 &[sts_7, sts_e01],
3266 &workflow,
3267 &external,
3268 ValidationLevel::Conditions,
3269 );
3270
3271 let code_errors: Vec<_> = report
3272 .by_category(ValidationCategory::Code)
3273 .filter(|i| {
3274 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3275 })
3276 .collect();
3277 assert_eq!(
3278 code_errors.len(),
3279 1,
3280 "Expected one COD002 (for GH02 only), got: {:?}",
3281 code_errors
3282 );
3283 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3284 }
3285
3286 #[test]
3287 fn test_code_validation_composite_paths_detects_invalid_code() {
3288 let evaluator = MockEvaluator::new(vec![]);
3291 let validator = EdifactValidator::new(evaluator);
3292 let external = NoOpExternalProvider;
3293
3294 let sts_segment = OwnedSegment {
3295 id: "STS".to_string(),
3296 elements: vec![
3297 vec!["7".to_string()],
3298 vec![String::new()],
3299 vec!["GH02".to_string()],
3300 vec!["ZW4".to_string()],
3301 ],
3302 segment_number: 1,
3303 };
3304
3305 let workflow = AhbWorkflow {
3306 pruefidentifikator: "55018".to_string(),
3307 description: "Test".to_string(),
3308 communication_direction: None,
3309 fields: vec![AhbFieldRule {
3310 segment_path: "SG4/STS/C556/9013".to_string(),
3311 name: "Statusanlaß".to_string(),
3312 ahb_status: "X".to_string(),
3313 codes: vec![AhbCodeRule {
3314 value: "E03".to_string(),
3315 description: "Transaktionsgrund".to_string(),
3316 ahb_status: "X".to_string(),
3317 }],
3318 parent_group_ahb_status: None,
3319 segment_ahb_status: None,
3320 element_index: Some(2),
3321 component_index: Some(0),
3322 ..Default::default()
3323 }],
3324 ub_definitions: BTreeMap::new(),
3325 package_conditions: Default::default(),
3326 };
3327
3328 let report = validator.validate(
3329 &[sts_segment],
3330 &workflow,
3331 &external,
3332 ValidationLevel::Conditions,
3333 );
3334
3335 let code_errors: Vec<_> = report
3336 .by_category(ValidationCategory::Code)
3337 .filter(|i| {
3338 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3339 })
3340 .collect();
3341 assert_eq!(
3342 code_errors.len(),
3343 1,
3344 "Expected COD002 for GH02, got: {:?}",
3345 code_errors
3346 );
3347 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3348 }
3349
3350 #[test]
3351 fn test_cross_field_code_validation_valid_qualifiers() {
3352 let evaluator = MockEvaluator::new(vec![]);
3355 let validator = EdifactValidator::new(evaluator);
3356 let external = NoOpExternalProvider;
3357
3358 let nad_ms = OwnedSegment {
3359 id: "NAD".to_string(),
3360 elements: vec![vec!["MS".to_string()]],
3361 segment_number: 4,
3362 };
3363 let nad_mr = OwnedSegment {
3364 id: "NAD".to_string(),
3365 elements: vec![vec!["MR".to_string()]],
3366 segment_number: 5,
3367 };
3368
3369 let workflow = AhbWorkflow {
3370 pruefidentifikator: "55001".to_string(),
3371 description: "Test".to_string(),
3372 communication_direction: None,
3373 fields: vec![
3374 AhbFieldRule {
3375 segment_path: "SG2/NAD/3035".to_string(),
3376 name: "Absender".to_string(),
3377 ahb_status: "X".to_string(),
3378 codes: vec![AhbCodeRule {
3379 value: "MS".to_string(),
3380 description: "Absender".to_string(),
3381 ahb_status: "X".to_string(),
3382 }],
3383 parent_group_ahb_status: None,
3384 segment_ahb_status: None,
3385 ..Default::default()
3386 },
3387 AhbFieldRule {
3388 segment_path: "SG2/NAD/3035".to_string(),
3389 name: "Empfaenger".to_string(),
3390 ahb_status: "X".to_string(),
3391 codes: vec![AhbCodeRule {
3392 value: "MR".to_string(),
3393 description: "Empfaenger".to_string(),
3394 ahb_status: "X".to_string(),
3395 }],
3396 parent_group_ahb_status: None,
3397 segment_ahb_status: None,
3398 ..Default::default()
3399 },
3400 ],
3401 ub_definitions: BTreeMap::new(),
3402 package_conditions: Default::default(),
3403 };
3404
3405 let report = validator.validate(
3406 &[nad_ms, nad_mr],
3407 &workflow,
3408 &external,
3409 ValidationLevel::Conditions,
3410 );
3411
3412 let code_errors: Vec<_> = report
3413 .by_category(ValidationCategory::Code)
3414 .filter(|i| i.severity == Severity::Error)
3415 .collect();
3416 assert!(
3417 code_errors.is_empty(),
3418 "Expected no code errors for valid qualifiers, got: {:?}",
3419 code_errors
3420 );
3421 }
3422
3423 #[test]
3424 fn test_cross_field_code_validation_catches_invalid_qualifier() {
3425 let evaluator = MockEvaluator::new(vec![]);
3427 let validator = EdifactValidator::new(evaluator);
3428 let external = NoOpExternalProvider;
3429
3430 let nad_ms = OwnedSegment {
3431 id: "NAD".to_string(),
3432 elements: vec![vec!["MS".to_string()]],
3433 segment_number: 4,
3434 };
3435 let nad_mt = OwnedSegment {
3436 id: "NAD".to_string(),
3437 elements: vec![vec!["MT".to_string()]], segment_number: 5,
3439 };
3440
3441 let workflow = AhbWorkflow {
3442 pruefidentifikator: "55001".to_string(),
3443 description: "Test".to_string(),
3444 communication_direction: None,
3445 fields: vec![
3446 AhbFieldRule {
3447 segment_path: "SG2/NAD/3035".to_string(),
3448 name: "Absender".to_string(),
3449 ahb_status: "X".to_string(),
3450 codes: vec![AhbCodeRule {
3451 value: "MS".to_string(),
3452 description: "Absender".to_string(),
3453 ahb_status: "X".to_string(),
3454 }],
3455 parent_group_ahb_status: None,
3456 segment_ahb_status: None,
3457 ..Default::default()
3458 },
3459 AhbFieldRule {
3460 segment_path: "SG2/NAD/3035".to_string(),
3461 name: "Empfaenger".to_string(),
3462 ahb_status: "X".to_string(),
3463 codes: vec![AhbCodeRule {
3464 value: "MR".to_string(),
3465 description: "Empfaenger".to_string(),
3466 ahb_status: "X".to_string(),
3467 }],
3468 parent_group_ahb_status: None,
3469 segment_ahb_status: None,
3470 ..Default::default()
3471 },
3472 ],
3473 ub_definitions: BTreeMap::new(),
3474 package_conditions: Default::default(),
3475 };
3476
3477 let report = validator.validate(
3478 &[nad_ms, nad_mt],
3479 &workflow,
3480 &external,
3481 ValidationLevel::Conditions,
3482 );
3483
3484 let code_errors: Vec<_> = report
3485 .by_category(ValidationCategory::Code)
3486 .filter(|i| i.severity == Severity::Error)
3487 .collect();
3488 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
3489 assert!(narrate(code_errors[0]).contains("MT"));
3490 assert!(narrate(code_errors[0]).contains("MR"));
3491 assert!(narrate(code_errors[0]).contains("MS"));
3492 }
3493
3494 #[test]
3495 fn test_cross_field_code_validation_unions_across_groups() {
3496 let evaluator = MockEvaluator::new(vec![]);
3500 let validator = EdifactValidator::new(evaluator);
3501 let external = NoOpExternalProvider;
3502
3503 let segments = vec![
3504 OwnedSegment {
3505 id: "NAD".to_string(),
3506 elements: vec![vec!["MS".to_string()]],
3507 segment_number: 3,
3508 },
3509 OwnedSegment {
3510 id: "NAD".to_string(),
3511 elements: vec![vec!["MR".to_string()]],
3512 segment_number: 4,
3513 },
3514 OwnedSegment {
3515 id: "NAD".to_string(),
3516 elements: vec![vec!["Z04".to_string()]],
3517 segment_number: 20,
3518 },
3519 OwnedSegment {
3520 id: "NAD".to_string(),
3521 elements: vec![vec!["Z09".to_string()]],
3522 segment_number: 21,
3523 },
3524 OwnedSegment {
3525 id: "NAD".to_string(),
3526 elements: vec![vec!["MT".to_string()]], segment_number: 22,
3528 },
3529 ];
3530
3531 let workflow = AhbWorkflow {
3532 pruefidentifikator: "55001".to_string(),
3533 description: "Test".to_string(),
3534 communication_direction: None,
3535 fields: vec![
3536 AhbFieldRule {
3537 segment_path: "SG2/NAD/3035".to_string(),
3538 name: "Absender".to_string(),
3539 ahb_status: "X".to_string(),
3540 codes: vec![AhbCodeRule {
3541 value: "MS".to_string(),
3542 description: "Absender".to_string(),
3543 ahb_status: "X".to_string(),
3544 }],
3545 parent_group_ahb_status: None,
3546 segment_ahb_status: None,
3547 ..Default::default()
3548 },
3549 AhbFieldRule {
3550 segment_path: "SG2/NAD/3035".to_string(),
3551 name: "Empfaenger".to_string(),
3552 ahb_status: "X".to_string(),
3553 codes: vec![AhbCodeRule {
3554 value: "MR".to_string(),
3555 description: "Empfaenger".to_string(),
3556 ahb_status: "X".to_string(),
3557 }],
3558 parent_group_ahb_status: None,
3559 segment_ahb_status: None,
3560 ..Default::default()
3561 },
3562 AhbFieldRule {
3563 segment_path: "SG4/SG12/NAD/3035".to_string(),
3564 name: "Anschlussnutzer".to_string(),
3565 ahb_status: "X".to_string(),
3566 codes: vec![AhbCodeRule {
3567 value: "Z04".to_string(),
3568 description: "Anschlussnutzer".to_string(),
3569 ahb_status: "X".to_string(),
3570 }],
3571 parent_group_ahb_status: None,
3572 segment_ahb_status: None,
3573 ..Default::default()
3574 },
3575 AhbFieldRule {
3576 segment_path: "SG4/SG12/NAD/3035".to_string(),
3577 name: "Korrespondenzanschrift".to_string(),
3578 ahb_status: "X".to_string(),
3579 codes: vec![AhbCodeRule {
3580 value: "Z09".to_string(),
3581 description: "Korrespondenzanschrift".to_string(),
3582 ahb_status: "X".to_string(),
3583 }],
3584 parent_group_ahb_status: None,
3585 segment_ahb_status: None,
3586 ..Default::default()
3587 },
3588 ],
3589 ub_definitions: BTreeMap::new(),
3590 package_conditions: Default::default(),
3591 };
3592
3593 let report =
3594 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3595
3596 let code_errors: Vec<_> = report
3597 .by_category(ValidationCategory::Code)
3598 .filter(|i| i.severity == Severity::Error)
3599 .collect();
3600 assert_eq!(
3601 code_errors.len(),
3602 1,
3603 "Expected exactly one COD002 error for MT, got: {:?}",
3604 code_errors
3605 );
3606 assert!(narrate(code_errors[0]).contains("MT"));
3607 }
3608
3609 #[test]
3610 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3611 let evaluator = MockEvaluator::new(vec![]);
3620 let validator = EdifactValidator::new(evaluator);
3621 let external = NoOpExternalProvider;
3622
3623 let qty_67 = OwnedSegment {
3624 id: "QTY".to_string(),
3625 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
3626 segment_number: 10,
3627 };
3628
3629 let workflow = AhbWorkflow {
3630 pruefidentifikator: "13025".to_string(),
3631 description: "Test".to_string(),
3632 communication_direction: None,
3633 fields: vec![AhbFieldRule {
3634 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
3635 name: "Menge, Qualifier".to_string(),
3636 ahb_status: "X".to_string(),
3637 codes: vec![
3638 AhbCodeRule {
3639 value: "220".to_string(),
3640 description: "Wahrer Wert".to_string(),
3641 ahb_status: "X".to_string(),
3642 },
3643 AhbCodeRule {
3644 value: "67".to_string(),
3645 description: "Ersatzwert".to_string(),
3646 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
3647 },
3648 AhbCodeRule {
3649 value: "Z18".to_string(),
3650 description: "Vorläufiger Wert".to_string(),
3651 ahb_status: "X [35]".to_string(),
3652 },
3653 ],
3654 parent_group_ahb_status: None,
3655 segment_ahb_status: None,
3656 element_index: Some(0),
3657 component_index: Some(0),
3658 ..Default::default()
3659 }],
3660 ub_definitions: BTreeMap::new(),
3661 package_conditions: Default::default(),
3662 };
3663
3664 let report =
3665 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3666
3667 let code_errors: Vec<_> = report
3668 .by_category(ValidationCategory::Code)
3669 .filter(|i| i.severity == Severity::Error)
3670 .collect();
3671 assert!(
3672 code_errors.is_empty(),
3673 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3674 code_errors
3675 );
3676 }
3677
3678 #[test]
3679 fn test_is_qualifier_field_simple_paths() {
3680 assert!(is_qualifier_field("NAD/3035"));
3681 assert!(is_qualifier_field("SG2/NAD/3035"));
3682 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3683 assert!(is_qualifier_field("LOC/3227"));
3684 }
3685
3686 #[test]
3687 fn test_is_qualifier_field_composite_paths() {
3688 assert!(is_qualifier_field("UNH/S009/0065"));
3692 assert!(is_qualifier_field("NAD/C082/3039"));
3693 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3694 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3695 }
3696
3697 #[test]
3698 fn test_is_qualifier_field_bare_segment() {
3699 assert!(!is_qualifier_field("NAD"));
3700 assert!(!is_qualifier_field("SG2/NAD"));
3701 }
3702
3703 #[test]
3704 fn test_is_qualifier_field_rejects_deep_paths() {
3705 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3707 }
3708
3709 #[test]
3710 fn test_missing_qualifier_instance_is_detected() {
3711 let evaluator = MockEvaluator::new(vec![]);
3714 let validator = EdifactValidator::new(evaluator);
3715 let external = NoOpExternalProvider;
3716
3717 let nad_ms = OwnedSegment {
3718 id: "NAD".to_string(),
3719 elements: vec![vec!["MS".to_string()]],
3720 segment_number: 3,
3721 };
3722
3723 let workflow = AhbWorkflow {
3724 pruefidentifikator: "55001".to_string(),
3725 description: "Test".to_string(),
3726 communication_direction: None,
3727 fields: vec![
3728 AhbFieldRule {
3729 segment_path: "SG2/NAD/3035".to_string(),
3730 name: "Absender".to_string(),
3731 ahb_status: "X".to_string(),
3732 codes: vec![AhbCodeRule {
3733 value: "MS".to_string(),
3734 description: "Absender".to_string(),
3735 ahb_status: "X".to_string(),
3736 }],
3737 parent_group_ahb_status: None,
3738 segment_ahb_status: None,
3739 ..Default::default()
3740 },
3741 AhbFieldRule {
3742 segment_path: "SG2/NAD/3035".to_string(),
3743 name: "Empfaenger".to_string(),
3744 ahb_status: "Muss".to_string(),
3745 codes: vec![AhbCodeRule {
3746 value: "MR".to_string(),
3747 description: "Empfaenger".to_string(),
3748 ahb_status: "X".to_string(),
3749 }],
3750 parent_group_ahb_status: None,
3751 segment_ahb_status: None,
3752 ..Default::default()
3753 },
3754 ],
3755 ub_definitions: BTreeMap::new(),
3756 package_conditions: Default::default(),
3757 };
3758
3759 let report =
3760 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
3761
3762 let ahb_errors: Vec<_> = report
3763 .by_category(ValidationCategory::Ahb)
3764 .filter(|i| i.severity == Severity::Error)
3765 .collect();
3766 assert_eq!(
3767 ahb_errors.len(),
3768 1,
3769 "Expected AHB001 for missing NAD+MR, got: {:?}",
3770 ahb_errors
3771 );
3772 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3773 }
3774
3775 #[test]
3776 fn test_present_qualifier_instance_no_error() {
3777 let evaluator = MockEvaluator::new(vec![]);
3779 let validator = EdifactValidator::new(evaluator);
3780 let external = NoOpExternalProvider;
3781
3782 let segments = vec![
3783 OwnedSegment {
3784 id: "NAD".to_string(),
3785 elements: vec![vec!["MS".to_string()]],
3786 segment_number: 3,
3787 },
3788 OwnedSegment {
3789 id: "NAD".to_string(),
3790 elements: vec![vec!["MR".to_string()]],
3791 segment_number: 4,
3792 },
3793 ];
3794
3795 let workflow = AhbWorkflow {
3796 pruefidentifikator: "55001".to_string(),
3797 description: "Test".to_string(),
3798 communication_direction: None,
3799 fields: vec![
3800 AhbFieldRule {
3801 segment_path: "SG2/NAD/3035".to_string(),
3802 name: "Absender".to_string(),
3803 ahb_status: "Muss".to_string(),
3804 codes: vec![AhbCodeRule {
3805 value: "MS".to_string(),
3806 description: "Absender".to_string(),
3807 ahb_status: "X".to_string(),
3808 }],
3809 parent_group_ahb_status: None,
3810 segment_ahb_status: None,
3811 ..Default::default()
3812 },
3813 AhbFieldRule {
3814 segment_path: "SG2/NAD/3035".to_string(),
3815 name: "Empfaenger".to_string(),
3816 ahb_status: "Muss".to_string(),
3817 codes: vec![AhbCodeRule {
3818 value: "MR".to_string(),
3819 description: "Empfaenger".to_string(),
3820 ahb_status: "X".to_string(),
3821 }],
3822 parent_group_ahb_status: None,
3823 segment_ahb_status: None,
3824 ..Default::default()
3825 },
3826 ],
3827 ub_definitions: BTreeMap::new(),
3828 package_conditions: Default::default(),
3829 };
3830
3831 let report =
3832 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3833
3834 let ahb_errors: Vec<_> = report
3835 .by_category(ValidationCategory::Ahb)
3836 .filter(|i| i.severity == Severity::Error)
3837 .collect();
3838 assert!(
3839 ahb_errors.is_empty(),
3840 "Expected no AHB001 errors, got: {:?}",
3841 ahb_errors
3842 );
3843 }
3844
3845 #[test]
3846 fn test_extract_group_path_key() {
3847 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
3848 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
3849 assert_eq!(extract_group_path_key("NAD/3035"), "");
3850 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
3851 }
3852
3853 fn validate_field_of_absent_group(
3855 group_status: &str,
3856 conditions: Vec<(u32, CR)>,
3857 ) -> ValidationReport {
3858 use mig_types::navigator::GroupNavigator;
3859
3860 struct NoSg9;
3861 impl GroupNavigator for NoSg9 {
3862 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3863 vec![]
3864 }
3865 fn find_segments_with_qualifier_in_group(
3866 &self,
3867 _: &str,
3868 _: usize,
3869 _: &str,
3870 _: &[&str],
3871 _: usize,
3872 ) -> Vec<OwnedSegment> {
3873 vec![]
3874 }
3875 fn group_instance_count(&self, _: &[&str]) -> usize {
3876 0
3877 }
3878 }
3879
3880 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3881 let workflow = AhbWorkflow {
3882 pruefidentifikator: "44001".to_string(),
3883 description: "Test".to_string(),
3884 communication_direction: None,
3885 fields: vec![AhbFieldRule {
3886 segment_path: "SG9/QTY/C186/6060".to_string(),
3887 name: "Menge".to_string(),
3888 ahb_status: "X".to_string(),
3889 parent_group_ahb_status: Some(group_status.to_string()),
3890 ..Default::default()
3891 }],
3892 ub_definitions: BTreeMap::new(),
3893 package_conditions: Default::default(),
3894 };
3895 validator.validate_with_navigator(
3896 &[],
3897 &workflow,
3898 &NoOpExternalProvider,
3899 ValidationLevel::Conditions,
3900 &NoSg9,
3901 )
3902 }
3903
3904 #[test]
3905 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
3906 let report = validate_field_of_absent_group(
3908 "Soll [47]\r\nMuss [46]",
3909 vec![(47, CR::True), (46, CR::False)],
3910 );
3911 assert!(
3912 report.is_valid(),
3913 "{:?}",
3914 report.errors().collect::<Vec<_>>()
3915 );
3916 }
3917
3918 #[test]
3919 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
3920 let report = validate_field_of_absent_group(
3921 "Soll [47]\r\nMuss [46]",
3922 vec![(47, CR::False), (46, CR::True)],
3923 );
3924 let errors: Vec<_> = report.errors().collect();
3925 assert_eq!(errors.len(), 1, "{errors:?}");
3926 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3927 }
3928
3929 #[test]
3930 fn test_absent_optional_group_no_missing_field_error() {
3931 use mig_types::navigator::GroupNavigator;
3934
3935 struct NavWithoutSG3;
3936 impl GroupNavigator for NavWithoutSG3 {
3937 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3938 vec![]
3939 }
3940 fn find_segments_with_qualifier_in_group(
3941 &self,
3942 _: &str,
3943 _: usize,
3944 _: &str,
3945 _: &[&str],
3946 _: usize,
3947 ) -> Vec<OwnedSegment> {
3948 vec![]
3949 }
3950 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3951 match group_path {
3952 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
3955 }
3956 }
3957 }
3958
3959 let evaluator = MockEvaluator::new(vec![]);
3960 let validator = EdifactValidator::new(evaluator);
3961 let external = NoOpExternalProvider;
3962 let nav = NavWithoutSG3;
3963
3964 let segments = vec![
3966 OwnedSegment {
3967 id: "NAD".into(),
3968 elements: vec![vec!["MS".into()]],
3969 segment_number: 3,
3970 },
3971 OwnedSegment {
3972 id: "NAD".into(),
3973 elements: vec![vec!["MR".into()]],
3974 segment_number: 4,
3975 },
3976 ];
3977
3978 let workflow = AhbWorkflow {
3979 pruefidentifikator: "55001".to_string(),
3980 description: "Test".to_string(),
3981 communication_direction: None,
3982 fields: vec![
3983 AhbFieldRule {
3984 segment_path: "SG2/SG3/CTA/3139".to_string(),
3985 name: "Funktion des Ansprechpartners, Code".to_string(),
3986 ahb_status: "Muss".to_string(),
3987 codes: vec![],
3988 parent_group_ahb_status: None,
3989 segment_ahb_status: None,
3990 ..Default::default()
3991 },
3992 AhbFieldRule {
3993 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
3994 name: "Name vom Ansprechpartner".to_string(),
3995 ahb_status: "X".to_string(),
3996 codes: vec![],
3997 parent_group_ahb_status: None,
3998 segment_ahb_status: None,
3999 ..Default::default()
4000 },
4001 ],
4002 ub_definitions: BTreeMap::new(),
4003 package_conditions: Default::default(),
4004 };
4005
4006 let report = validator.validate_with_navigator(
4007 &segments,
4008 &workflow,
4009 &external,
4010 ValidationLevel::Conditions,
4011 &nav,
4012 );
4013
4014 let ahb_errors: Vec<_> = report
4015 .by_category(ValidationCategory::Ahb)
4016 .filter(|i| i.severity == Severity::Error)
4017 .collect();
4018 assert!(
4019 ahb_errors.is_empty(),
4020 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
4021 ahb_errors
4022 );
4023 }
4024
4025 #[test]
4026 fn test_present_group_still_checks_mandatory_fields() {
4027 use mig_types::navigator::GroupNavigator;
4029
4030 struct NavWithSG3;
4031 impl GroupNavigator for NavWithSG3 {
4032 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4033 vec![]
4034 }
4035 fn find_segments_with_qualifier_in_group(
4036 &self,
4037 _: &str,
4038 _: usize,
4039 _: &str,
4040 _: &[&str],
4041 _: usize,
4042 ) -> Vec<OwnedSegment> {
4043 vec![]
4044 }
4045 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4046 match group_path {
4047 ["SG2"] => 1,
4048 ["SG2", "SG3"] => 1, _ => 0,
4050 }
4051 }
4052 }
4053
4054 let evaluator = MockEvaluator::new(vec![]);
4055 let validator = EdifactValidator::new(evaluator);
4056 let external = NoOpExternalProvider;
4057 let nav = NavWithSG3;
4058
4059 let segments = vec![OwnedSegment {
4061 id: "NAD".into(),
4062 elements: vec![vec!["MS".into()]],
4063 segment_number: 3,
4064 }];
4065
4066 let workflow = AhbWorkflow {
4067 pruefidentifikator: "55001".to_string(),
4068 description: "Test".to_string(),
4069 communication_direction: None,
4070 fields: vec![AhbFieldRule {
4071 segment_path: "SG2/SG3/CTA/3139".to_string(),
4072 name: "Funktion des Ansprechpartners, Code".to_string(),
4073 ahb_status: "Muss".to_string(),
4074 codes: vec![],
4075 parent_group_ahb_status: None,
4076 segment_ahb_status: None,
4077 ..Default::default()
4078 }],
4079 ub_definitions: BTreeMap::new(),
4080 package_conditions: Default::default(),
4081 };
4082
4083 let report = validator.validate_with_navigator(
4084 &segments,
4085 &workflow,
4086 &external,
4087 ValidationLevel::Conditions,
4088 &nav,
4089 );
4090
4091 let ahb_errors: Vec<_> = report
4092 .by_category(ValidationCategory::Ahb)
4093 .filter(|i| i.severity == Severity::Error)
4094 .collect();
4095 assert_eq!(
4096 ahb_errors.len(),
4097 1,
4098 "Expected AHB001 error when SG3 is present but CTA missing"
4099 );
4100 assert!(narrate(ahb_errors[0]).contains("CTA"));
4101 }
4102
4103 #[test]
4104 fn test_missing_qualifier_with_navigator_is_detected() {
4105 use mig_types::navigator::GroupNavigator;
4108
4109 struct NavWithSG2;
4110 impl GroupNavigator for NavWithSG2 {
4111 fn find_segments_in_group(
4112 &self,
4113 segment_id: &str,
4114 group_path: &[&str],
4115 instance_index: usize,
4116 ) -> Vec<OwnedSegment> {
4117 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
4118 vec![OwnedSegment {
4119 id: "NAD".into(),
4120 elements: vec![vec!["MS".into()]],
4121 segment_number: 3,
4122 }]
4123 } else {
4124 vec![]
4125 }
4126 }
4127 fn find_segments_with_qualifier_in_group(
4128 &self,
4129 _: &str,
4130 _: usize,
4131 _: &str,
4132 _: &[&str],
4133 _: usize,
4134 ) -> Vec<OwnedSegment> {
4135 vec![]
4136 }
4137 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4138 match group_path {
4139 ["SG2"] => 1,
4140 _ => 0,
4141 }
4142 }
4143 }
4144
4145 let evaluator = MockEvaluator::new(vec![]);
4146 let validator = EdifactValidator::new(evaluator);
4147 let external = NoOpExternalProvider;
4148 let nav = NavWithSG2;
4149
4150 let segments = vec![OwnedSegment {
4151 id: "NAD".into(),
4152 elements: vec![vec!["MS".into()]],
4153 segment_number: 3,
4154 }];
4155
4156 let workflow = AhbWorkflow {
4157 pruefidentifikator: "55001".to_string(),
4158 description: "Test".to_string(),
4159 communication_direction: None,
4160 fields: vec![
4161 AhbFieldRule {
4162 segment_path: "SG2/NAD/3035".to_string(),
4163 name: "Absender".to_string(),
4164 ahb_status: "X".to_string(),
4165 codes: vec![AhbCodeRule {
4166 value: "MS".to_string(),
4167 description: "Absender".to_string(),
4168 ahb_status: "X".to_string(),
4169 }],
4170 parent_group_ahb_status: None,
4171 segment_ahb_status: None,
4172 ..Default::default()
4173 },
4174 AhbFieldRule {
4175 segment_path: "SG2/NAD/3035".to_string(),
4176 name: "Empfaenger".to_string(),
4177 ahb_status: "Muss".to_string(),
4178 codes: vec![AhbCodeRule {
4179 value: "MR".to_string(),
4180 description: "Empfaenger".to_string(),
4181 ahb_status: "X".to_string(),
4182 }],
4183 parent_group_ahb_status: None,
4184 segment_ahb_status: None,
4185 ..Default::default()
4186 },
4187 ],
4188 ub_definitions: BTreeMap::new(),
4189 package_conditions: Default::default(),
4190 };
4191
4192 let report = validator.validate_with_navigator(
4193 &segments,
4194 &workflow,
4195 &external,
4196 ValidationLevel::Conditions,
4197 &nav,
4198 );
4199
4200 let ahb_errors: Vec<_> = report
4201 .by_category(ValidationCategory::Ahb)
4202 .filter(|i| i.severity == Severity::Error)
4203 .collect();
4204 assert_eq!(
4205 ahb_errors.len(),
4206 1,
4207 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
4208 ahb_errors
4209 );
4210 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4211 }
4212
4213 #[test]
4214 fn test_optional_group_variant_absent_no_error() {
4215 use mig_types::navigator::GroupNavigator;
4220
4221 struct TestNav;
4222 impl GroupNavigator for TestNav {
4223 fn find_segments_in_group(
4224 &self,
4225 segment_id: &str,
4226 group_path: &[&str],
4227 instance_index: usize,
4228 ) -> Vec<OwnedSegment> {
4229 match (segment_id, group_path, instance_index) {
4230 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
4231 id: "LOC".into(),
4232 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4233 segment_number: 10,
4234 }],
4235 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
4236 id: "NAD".into(),
4237 elements: vec![vec!["MS".into()]],
4238 segment_number: 3,
4239 }],
4240 _ => vec![],
4241 }
4242 }
4243 fn find_segments_with_qualifier_in_group(
4244 &self,
4245 _: &str,
4246 _: usize,
4247 _: &str,
4248 _: &[&str],
4249 _: usize,
4250 ) -> Vec<OwnedSegment> {
4251 vec![]
4252 }
4253 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4254 match group_path {
4255 ["SG2"] => 1,
4256 ["SG4"] => 1,
4257 ["SG4", "SG5"] => 1, _ => 0,
4259 }
4260 }
4261 }
4262
4263 let evaluator = MockEvaluator::new(vec![]);
4264 let validator = EdifactValidator::new(evaluator);
4265 let external = NoOpExternalProvider;
4266 let nav = TestNav;
4267
4268 let segments = vec![
4269 OwnedSegment {
4270 id: "NAD".into(),
4271 elements: vec![vec!["MS".into()]],
4272 segment_number: 3,
4273 },
4274 OwnedSegment {
4275 id: "LOC".into(),
4276 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4277 segment_number: 10,
4278 },
4279 ];
4280
4281 let workflow = AhbWorkflow {
4282 pruefidentifikator: "55001".to_string(),
4283 description: "Test".to_string(),
4284 communication_direction: None,
4285 fields: vec![
4286 AhbFieldRule {
4288 segment_path: "SG2/NAD/3035".to_string(),
4289 name: "Absender".to_string(),
4290 ahb_status: "X".to_string(),
4291 codes: vec![AhbCodeRule {
4292 value: "MS".to_string(),
4293 description: "Absender".to_string(),
4294 ahb_status: "X".to_string(),
4295 }],
4296 parent_group_ahb_status: Some("Muss".to_string()),
4297 segment_ahb_status: None,
4298 ..Default::default()
4299 },
4300 AhbFieldRule {
4301 segment_path: "SG2/NAD/3035".to_string(),
4302 name: "Empfaenger".to_string(),
4303 ahb_status: "Muss".to_string(),
4304 codes: vec![AhbCodeRule {
4305 value: "MR".to_string(),
4306 description: "Empfaenger".to_string(),
4307 ahb_status: "X".to_string(),
4308 }],
4309 parent_group_ahb_status: Some("Muss".to_string()),
4310 segment_ahb_status: None,
4311 ..Default::default()
4312 },
4313 AhbFieldRule {
4315 segment_path: "SG4/SG5/LOC/3227".to_string(),
4316 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4317 ahb_status: "X".to_string(),
4318 codes: vec![AhbCodeRule {
4319 value: "Z16".to_string(),
4320 description: "Marktlokation".to_string(),
4321 ahb_status: "X".to_string(),
4322 }],
4323 parent_group_ahb_status: Some("Kann".to_string()),
4324 segment_ahb_status: None,
4325 ..Default::default()
4326 },
4327 AhbFieldRule {
4328 segment_path: "SG4/SG5/LOC/3227".to_string(),
4329 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4330 ahb_status: "Muss".to_string(),
4331 codes: vec![AhbCodeRule {
4332 value: "Z17".to_string(),
4333 description: "Messlokation".to_string(),
4334 ahb_status: "X".to_string(),
4335 }],
4336 parent_group_ahb_status: Some("Kann".to_string()),
4337 segment_ahb_status: None,
4338 ..Default::default()
4339 },
4340 ],
4341 ub_definitions: BTreeMap::new(),
4342 package_conditions: Default::default(),
4343 };
4344
4345 let report = validator.validate_with_navigator(
4346 &segments,
4347 &workflow,
4348 &external,
4349 ValidationLevel::Conditions,
4350 &nav,
4351 );
4352
4353 let ahb_errors: Vec<_> = report
4354 .by_category(ValidationCategory::Ahb)
4355 .filter(|i| i.severity == Severity::Error)
4356 .collect();
4357
4358 assert_eq!(
4361 ahb_errors.len(),
4362 1,
4363 "Expected only AHB001 for missing NAD+MR, got: {:?}",
4364 ahb_errors
4365 );
4366 assert!(
4367 narrate(ahb_errors[0]).contains("Empfaenger"),
4368 "Error should be for missing NAD+MR (Empfaenger)"
4369 );
4370 }
4371
4372 #[test]
4376 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4377 use mig_types::navigator::GroupNavigator;
4378
4379 struct TestNav;
4380 impl GroupNavigator for TestNav {
4381 fn find_segments_in_group(
4382 &self,
4383 segment_id: &str,
4384 group_path: &[&str],
4385 instance_index: usize,
4386 ) -> Vec<OwnedSegment> {
4387 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4388 vec![OwnedSegment {
4389 id: "SEQ".into(),
4390 elements: vec![vec!["Z03".into()]],
4391 segment_number: 10,
4392 }]
4393 } else {
4394 vec![]
4395 }
4396 }
4397 fn find_segments_with_qualifier_in_group(
4398 &self,
4399 _: &str,
4400 _: usize,
4401 _: &str,
4402 _: &[&str],
4403 _: usize,
4404 ) -> Vec<OwnedSegment> {
4405 vec![]
4406 }
4407 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4408 match group_path {
4409 ["SG4"] => 1,
4410 ["SG4", "SG8"] => 1,
4411 _ => 0,
4412 }
4413 }
4414 }
4415
4416 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
4417 let validator = EdifactValidator::new(evaluator);
4418 let segments = vec![OwnedSegment {
4419 id: "SEQ".into(),
4420 elements: vec![vec!["Z03".into()]],
4421 segment_number: 10,
4422 }];
4423 let seq = |code: &str, group: &str| AhbFieldRule {
4424 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4425 name: format!("Handlung, Code ({code})"),
4426 ahb_status: "X".to_string(),
4427 codes: vec![AhbCodeRule {
4428 value: code.to_string(),
4429 description: String::new(),
4430 ahb_status: "X".to_string(),
4431 }],
4432 parent_group_ahb_status: Some(group.to_string()),
4433 segment_ahb_status: None,
4434 ..Default::default()
4435 };
4436 let workflow = AhbWorkflow {
4437 pruefidentifikator: "44143".to_string(),
4438 description: "Test".to_string(),
4439 communication_direction: None,
4440 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
4441 ub_definitions: BTreeMap::new(),
4442 package_conditions: Default::default(),
4443 };
4444
4445 let report = validator.validate_with_navigator(
4446 &segments,
4447 &workflow,
4448 &NoOpExternalProvider,
4449 ValidationLevel::Conditions,
4450 &TestNav,
4451 );
4452 let missing: Vec<_> = report
4453 .issues
4454 .iter()
4455 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
4456 .collect();
4457 assert!(missing.is_empty(), "{missing:#?}");
4458 }
4459
4460 #[test]
4461 fn test_conditional_group_variant_absent_no_error() {
4462 use mig_types::navigator::GroupNavigator;
4467
4468 struct TestNav;
4469 impl GroupNavigator for TestNav {
4470 fn find_segments_in_group(
4471 &self,
4472 segment_id: &str,
4473 group_path: &[&str],
4474 instance_index: usize,
4475 ) -> Vec<OwnedSegment> {
4476 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
4477 vec![OwnedSegment {
4478 id: "LOC".into(),
4479 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4480 segment_number: 10,
4481 }]
4482 } else {
4483 vec![]
4484 }
4485 }
4486 fn find_segments_with_qualifier_in_group(
4487 &self,
4488 _: &str,
4489 _: usize,
4490 _: &str,
4491 _: &[&str],
4492 _: usize,
4493 ) -> Vec<OwnedSegment> {
4494 vec![]
4495 }
4496 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4497 match group_path {
4498 ["SG4"] => 1,
4499 ["SG4", "SG5"] => 1, _ => 0,
4501 }
4502 }
4503 }
4504
4505 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
4508 let validator = EdifactValidator::new(evaluator);
4509 let external = NoOpExternalProvider;
4510 let nav = TestNav;
4511
4512 let segments = vec![OwnedSegment {
4513 id: "LOC".into(),
4514 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4515 segment_number: 10,
4516 }];
4517
4518 let workflow = AhbWorkflow {
4519 pruefidentifikator: "55001".to_string(),
4520 description: "Test".to_string(),
4521 communication_direction: None,
4522 fields: vec![
4523 AhbFieldRule {
4525 segment_path: "SG4/SG5/LOC/3227".to_string(),
4526 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4527 ahb_status: "X".to_string(),
4528 codes: vec![AhbCodeRule {
4529 value: "Z16".to_string(),
4530 description: "Marktlokation".to_string(),
4531 ahb_status: "X".to_string(),
4532 }],
4533 parent_group_ahb_status: Some("Muss [2061]".to_string()),
4534 segment_ahb_status: None,
4535 ..Default::default()
4536 },
4537 AhbFieldRule {
4539 segment_path: "SG4/SG5/LOC/3227".to_string(),
4540 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4541 ahb_status: "X".to_string(),
4542 codes: vec![AhbCodeRule {
4543 value: "Z17".to_string(),
4544 description: "Messlokation".to_string(),
4545 ahb_status: "X".to_string(),
4546 }],
4547 parent_group_ahb_status: Some("Soll [165]".to_string()),
4548 segment_ahb_status: None,
4549 ..Default::default()
4550 },
4551 ],
4552 ub_definitions: BTreeMap::new(),
4553 package_conditions: Default::default(),
4554 };
4555
4556 let report = validator.validate_with_navigator(
4557 &segments,
4558 &workflow,
4559 &external,
4560 ValidationLevel::Conditions,
4561 &nav,
4562 );
4563
4564 let ahb_errors: Vec<_> = report
4565 .by_category(ValidationCategory::Ahb)
4566 .filter(|i| i.severity == Severity::Error)
4567 .collect();
4568
4569 assert!(
4571 ahb_errors.is_empty(),
4572 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4573 ahb_errors
4574 );
4575 }
4576
4577 #[test]
4578 fn test_conditional_group_variant_unknown_no_error() {
4579 let evaluator = MockEvaluator::new(vec![]);
4585 let validator = EdifactValidator::new(evaluator);
4586 let external = NoOpExternalProvider;
4587
4588 let workflow = AhbWorkflow {
4589 pruefidentifikator: "55001".to_string(),
4590 description: "Test".to_string(),
4591 communication_direction: None,
4592 fields: vec![AhbFieldRule {
4593 segment_path: "SG4/SG5/LOC/3227".to_string(),
4594 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4595 ahb_status: "X".to_string(),
4596 codes: vec![AhbCodeRule {
4597 value: "Z17".to_string(),
4598 description: "Messlokation".to_string(),
4599 ahb_status: "X".to_string(),
4600 }],
4601 parent_group_ahb_status: Some("Soll [165]".to_string()),
4602 segment_ahb_status: None,
4603 ..Default::default()
4604 }],
4605 ub_definitions: BTreeMap::new(),
4606 package_conditions: Default::default(),
4607 };
4608
4609 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4610
4611 let ahb_errors: Vec<_> = report
4612 .by_category(ValidationCategory::Ahb)
4613 .filter(|i| i.severity == Severity::Error)
4614 .collect();
4615
4616 assert!(
4618 ahb_errors.is_empty(),
4619 "Expected no errors when parent group condition is Unknown, got: {:?}",
4620 ahb_errors
4621 );
4622 }
4623
4624 #[test]
4625 fn test_segment_absent_within_present_group_no_error() {
4626 use mig_types::navigator::GroupNavigator;
4630
4631 struct TestNav;
4632 impl GroupNavigator for TestNav {
4633 fn find_segments_in_group(
4634 &self,
4635 segment_id: &str,
4636 group_path: &[&str],
4637 instance_index: usize,
4638 ) -> Vec<OwnedSegment> {
4639 if segment_id == "QTY"
4641 && group_path == ["SG5", "SG6", "SG9", "SG10"]
4642 && instance_index == 0
4643 {
4644 vec![OwnedSegment {
4645 id: "QTY".into(),
4646 elements: vec![vec!["220".into(), "0".into()]],
4647 segment_number: 14,
4648 }]
4649 } else {
4650 vec![]
4651 }
4652 }
4653 fn find_segments_with_qualifier_in_group(
4654 &self,
4655 _: &str,
4656 _: usize,
4657 _: &str,
4658 _: &[&str],
4659 _: usize,
4660 ) -> Vec<OwnedSegment> {
4661 vec![]
4662 }
4663 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4664 match group_path {
4665 ["SG5"] => 1,
4666 ["SG5", "SG6"] => 1,
4667 ["SG5", "SG6", "SG9"] => 1,
4668 ["SG5", "SG6", "SG9", "SG10"] => 1,
4669 _ => 0,
4670 }
4671 }
4672 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4673 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4675 }
4676 }
4677
4678 let evaluator = MockEvaluator::all_true(&[]);
4679 let validator = EdifactValidator::new(evaluator);
4680 let external = NoOpExternalProvider;
4681 let nav = TestNav;
4682
4683 let segments = vec![OwnedSegment {
4684 id: "QTY".into(),
4685 elements: vec![vec!["220".into(), "0".into()]],
4686 segment_number: 14,
4687 }];
4688
4689 let workflow = AhbWorkflow {
4690 pruefidentifikator: "13017".to_string(),
4691 description: "Test".to_string(),
4692 communication_direction: None,
4693 fields: vec![
4694 AhbFieldRule {
4696 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4697 name: "Statuskategorie, Code".to_string(),
4698 ahb_status: "X".to_string(),
4699 codes: vec![],
4700 parent_group_ahb_status: Some("Muss".to_string()),
4701 segment_ahb_status: None,
4702 ..Default::default()
4703 },
4704 AhbFieldRule {
4706 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4707 name: "Statusanlaß, Code".to_string(),
4708 ahb_status: "X [5]".to_string(),
4709 codes: vec![],
4710 parent_group_ahb_status: Some("Muss".to_string()),
4711 segment_ahb_status: None,
4712 ..Default::default()
4713 },
4714 ],
4715 ub_definitions: BTreeMap::new(),
4716 package_conditions: Default::default(),
4717 };
4718
4719 let report = validator.validate_with_navigator(
4720 &segments,
4721 &workflow,
4722 &external,
4723 ValidationLevel::Conditions,
4724 &nav,
4725 );
4726
4727 let ahb_errors: Vec<_> = report
4728 .by_category(ValidationCategory::Ahb)
4729 .filter(|i| i.severity == Severity::Error)
4730 .collect();
4731
4732 assert!(
4733 ahb_errors.is_empty(),
4734 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
4735 ahb_errors
4736 );
4737 }
4738
4739 #[test]
4740 fn test_group_scoped_code_validation_with_navigator() {
4741 use mig_types::navigator::GroupNavigator;
4745
4746 struct TestNav;
4747 impl GroupNavigator for TestNav {
4748 fn find_segments_in_group(
4749 &self,
4750 segment_id: &str,
4751 group_path: &[&str],
4752 _instance_index: usize,
4753 ) -> Vec<OwnedSegment> {
4754 if segment_id != "NAD" {
4755 return vec![];
4756 }
4757 match group_path {
4758 ["SG2"] => vec![
4759 OwnedSegment {
4760 id: "NAD".into(),
4761 elements: vec![vec!["MS".into()]],
4762 segment_number: 3,
4763 },
4764 OwnedSegment {
4765 id: "NAD".into(),
4766 elements: vec![vec!["MT".into()]], segment_number: 4,
4768 },
4769 ],
4770 ["SG4", "SG12"] => vec![
4771 OwnedSegment {
4772 id: "NAD".into(),
4773 elements: vec![vec!["Z04".into()]],
4774 segment_number: 20,
4775 },
4776 OwnedSegment {
4777 id: "NAD".into(),
4778 elements: vec![vec!["Z09".into()]],
4779 segment_number: 21,
4780 },
4781 ],
4782 _ => vec![],
4783 }
4784 }
4785 fn find_segments_with_qualifier_in_group(
4786 &self,
4787 _: &str,
4788 _: usize,
4789 _: &str,
4790 _: &[&str],
4791 _: usize,
4792 ) -> Vec<OwnedSegment> {
4793 vec![]
4794 }
4795 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4796 match group_path {
4797 ["SG2"] | ["SG4", "SG12"] => 1,
4798 _ => 0,
4799 }
4800 }
4801 }
4802
4803 let evaluator = MockEvaluator::new(vec![]);
4804 let validator = EdifactValidator::new(evaluator);
4805 let external = NoOpExternalProvider;
4806 let nav = TestNav;
4807
4808 let workflow = AhbWorkflow {
4809 pruefidentifikator: "55001".to_string(),
4810 description: "Test".to_string(),
4811 communication_direction: None,
4812 fields: vec![
4813 AhbFieldRule {
4814 segment_path: "SG2/NAD/3035".to_string(),
4815 name: "Absender".to_string(),
4816 ahb_status: "X".to_string(),
4817 codes: vec![AhbCodeRule {
4818 value: "MS".to_string(),
4819 description: "Absender".to_string(),
4820 ahb_status: "X".to_string(),
4821 }],
4822 parent_group_ahb_status: None,
4823 segment_ahb_status: None,
4824 ..Default::default()
4825 },
4826 AhbFieldRule {
4827 segment_path: "SG2/NAD/3035".to_string(),
4828 name: "Empfaenger".to_string(),
4829 ahb_status: "X".to_string(),
4830 codes: vec![AhbCodeRule {
4831 value: "MR".to_string(),
4832 description: "Empfaenger".to_string(),
4833 ahb_status: "X".to_string(),
4834 }],
4835 parent_group_ahb_status: None,
4836 segment_ahb_status: None,
4837 ..Default::default()
4838 },
4839 AhbFieldRule {
4840 segment_path: "SG4/SG12/NAD/3035".to_string(),
4841 name: "Anschlussnutzer".to_string(),
4842 ahb_status: "X".to_string(),
4843 codes: vec![AhbCodeRule {
4844 value: "Z04".to_string(),
4845 description: "Anschlussnutzer".to_string(),
4846 ahb_status: "X".to_string(),
4847 }],
4848 parent_group_ahb_status: None,
4849 segment_ahb_status: None,
4850 ..Default::default()
4851 },
4852 AhbFieldRule {
4853 segment_path: "SG4/SG12/NAD/3035".to_string(),
4854 name: "Korrespondenzanschrift".to_string(),
4855 ahb_status: "X".to_string(),
4856 codes: vec![AhbCodeRule {
4857 value: "Z09".to_string(),
4858 description: "Korrespondenzanschrift".to_string(),
4859 ahb_status: "X".to_string(),
4860 }],
4861 parent_group_ahb_status: None,
4862 segment_ahb_status: None,
4863 ..Default::default()
4864 },
4865 ],
4866 ub_definitions: BTreeMap::new(),
4867 package_conditions: Default::default(),
4868 };
4869
4870 let all_segments = vec![
4872 OwnedSegment {
4873 id: "NAD".into(),
4874 elements: vec![vec!["MS".into()]],
4875 segment_number: 3,
4876 },
4877 OwnedSegment {
4878 id: "NAD".into(),
4879 elements: vec![vec!["MT".into()]],
4880 segment_number: 4,
4881 },
4882 OwnedSegment {
4883 id: "NAD".into(),
4884 elements: vec![vec!["Z04".into()]],
4885 segment_number: 20,
4886 },
4887 OwnedSegment {
4888 id: "NAD".into(),
4889 elements: vec![vec!["Z09".into()]],
4890 segment_number: 21,
4891 },
4892 ];
4893
4894 let report = validator.validate_with_navigator(
4895 &all_segments,
4896 &workflow,
4897 &external,
4898 ValidationLevel::Conditions,
4899 &nav,
4900 );
4901
4902 let code_errors: Vec<_> = report
4903 .by_category(ValidationCategory::Code)
4904 .filter(|i| i.severity == Severity::Error)
4905 .collect();
4906
4907 assert_eq!(
4910 code_errors.len(),
4911 1,
4912 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
4913 code_errors
4914 );
4915 assert!(narrate(code_errors[0]).contains("MT"));
4916 assert!(narrate(code_errors[0]).contains("MR"));
4918 assert!(narrate(code_errors[0]).contains("MS"));
4919 assert!(
4920 !narrate(code_errors[0]).contains("Z04"),
4921 "SG4/SG12 codes should not leak into SG2 error"
4922 );
4923 assert!(
4925 code_errors[0]
4926 .field_path
4927 .as_deref()
4928 .unwrap_or("")
4929 .contains("SG2"),
4930 "Error field_path should reference SG2, got: {:?}",
4931 code_errors[0].field_path
4932 );
4933 }
4934
4935 #[test]
4938 fn test_package_cardinality_within_bounds() {
4939 let evaluator = MockEvaluator::all_true(&[]);
4941 let validator = EdifactValidator::new(evaluator);
4942 let external = NoOpExternalProvider;
4943
4944 let segments = vec![OwnedSegment {
4945 id: "STS".into(),
4946 elements: vec![
4947 vec!["Z33".into()], vec![], vec!["E01".into()], ],
4951 segment_number: 5,
4952 }];
4953
4954 let workflow = AhbWorkflow {
4955 pruefidentifikator: "13017".to_string(),
4956 description: "Test".to_string(),
4957 communication_direction: None,
4958 ub_definitions: BTreeMap::new(),
4959 package_conditions: Default::default(),
4960 fields: vec![AhbFieldRule {
4961 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4962 name: "Statusanlaß, Code".to_string(),
4963 ahb_status: "X".to_string(),
4964 element_index: Some(2),
4965 component_index: Some(0),
4966 codes: vec![
4967 AhbCodeRule {
4968 value: "E01".into(),
4969 description: "Code 1".into(),
4970 ahb_status: "X [4P0..1]".into(),
4971 },
4972 AhbCodeRule {
4973 value: "E02".into(),
4974 description: "Code 2".into(),
4975 ahb_status: "X [4P0..1]".into(),
4976 },
4977 ],
4978 parent_group_ahb_status: Some("Muss".to_string()),
4979 segment_ahb_status: None,
4980 mig_number: None,
4981 format: None,
4982 }],
4983 };
4984
4985 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4986 let pkg_errors: Vec<_> = report
4987 .by_category(ValidationCategory::Ahb)
4988 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4989 .collect();
4990 assert!(
4991 pkg_errors.is_empty(),
4992 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
4993 pkg_errors
4994 );
4995 }
4996
4997 #[test]
4998 fn test_package_cardinality_zero_present_min_zero() {
4999 let evaluator = MockEvaluator::all_true(&[]);
5001 let validator = EdifactValidator::new(evaluator);
5002 let external = NoOpExternalProvider;
5003
5004 let segments = vec![OwnedSegment {
5005 id: "STS".into(),
5006 elements: vec![
5007 vec!["Z33".into()],
5008 vec![],
5009 vec!["X99".into()], ],
5011 segment_number: 5,
5012 }];
5013
5014 let workflow = AhbWorkflow {
5015 pruefidentifikator: "13017".to_string(),
5016 description: "Test".to_string(),
5017 communication_direction: None,
5018 ub_definitions: BTreeMap::new(),
5019 package_conditions: Default::default(),
5020 fields: vec![AhbFieldRule {
5021 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5022 name: "Statusanlaß, Code".to_string(),
5023 ahb_status: "X".to_string(),
5024 element_index: Some(2),
5025 component_index: Some(0),
5026 codes: vec![
5027 AhbCodeRule {
5028 value: "E01".into(),
5029 description: "Code 1".into(),
5030 ahb_status: "X [4P0..1]".into(),
5031 },
5032 AhbCodeRule {
5033 value: "E02".into(),
5034 description: "Code 2".into(),
5035 ahb_status: "X [4P0..1]".into(),
5036 },
5037 ],
5038 parent_group_ahb_status: Some("Muss".to_string()),
5039 segment_ahb_status: None,
5040 mig_number: None,
5041 format: None,
5042 }],
5043 };
5044
5045 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5046 let pkg_errors: Vec<_> = report
5047 .by_category(ValidationCategory::Ahb)
5048 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5049 .collect();
5050 assert!(
5051 pkg_errors.is_empty(),
5052 "0 codes, min=0 — no error expected, got: {:?}",
5053 pkg_errors
5054 );
5055 }
5056
5057 #[test]
5058 fn test_package_cardinality_too_many() {
5059 let evaluator = MockEvaluator::all_true(&[]);
5061 let validator = EdifactValidator::new(evaluator);
5062 let external = NoOpExternalProvider;
5063
5064 let segments = vec![
5066 OwnedSegment {
5067 id: "STS".into(),
5068 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
5069 segment_number: 5,
5070 },
5071 OwnedSegment {
5072 id: "STS".into(),
5073 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
5074 segment_number: 6,
5075 },
5076 ];
5077
5078 let workflow = AhbWorkflow {
5079 pruefidentifikator: "13017".to_string(),
5080 description: "Test".to_string(),
5081 communication_direction: None,
5082 ub_definitions: BTreeMap::new(),
5083 package_conditions: Default::default(),
5084 fields: vec![AhbFieldRule {
5085 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5086 name: "Statusanlaß, Code".to_string(),
5087 ahb_status: "X".to_string(),
5088 element_index: Some(2),
5089 component_index: Some(0),
5090 codes: vec![
5091 AhbCodeRule {
5092 value: "E01".into(),
5093 description: "Code 1".into(),
5094 ahb_status: "X [4P0..1]".into(),
5095 },
5096 AhbCodeRule {
5097 value: "E02".into(),
5098 description: "Code 2".into(),
5099 ahb_status: "X [4P0..1]".into(),
5100 },
5101 ],
5102 parent_group_ahb_status: Some("Muss".to_string()),
5103 segment_ahb_status: None,
5104 mig_number: None,
5105 format: None,
5106 }],
5107 };
5108
5109 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5110 let pkg_errors: Vec<_> = report
5111 .by_category(ValidationCategory::Ahb)
5112 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5113 .collect();
5114 assert_eq!(
5115 pkg_errors.len(),
5116 1,
5117 "2 codes present, max=1 — expected 1 error, got: {:?}",
5118 pkg_errors
5119 );
5120 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
5121 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
5122 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
5123 }
5124
5125 #[test]
5126 fn test_package_cardinality_too_few() {
5127 let evaluator = MockEvaluator::all_true(&[]);
5129 let validator = EdifactValidator::new(evaluator);
5130 let external = NoOpExternalProvider;
5131
5132 let segments = vec![OwnedSegment {
5133 id: "STS".into(),
5134 elements: vec![
5135 vec!["Z33".into()],
5136 vec![],
5137 vec!["X99".into()], ],
5139 segment_number: 5,
5140 }];
5141
5142 let workflow = AhbWorkflow {
5143 pruefidentifikator: "13017".to_string(),
5144 description: "Test".to_string(),
5145 communication_direction: None,
5146 ub_definitions: BTreeMap::new(),
5147 package_conditions: Default::default(),
5148 fields: vec![AhbFieldRule {
5149 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5150 name: "Statusanlaß, Code".to_string(),
5151 ahb_status: "X".to_string(),
5152 element_index: Some(2),
5153 component_index: Some(0),
5154 codes: vec![
5155 AhbCodeRule {
5156 value: "E01".into(),
5157 description: "Code 1".into(),
5158 ahb_status: "X [5P1..3]".into(),
5159 },
5160 AhbCodeRule {
5161 value: "E02".into(),
5162 description: "Code 2".into(),
5163 ahb_status: "X [5P1..3]".into(),
5164 },
5165 AhbCodeRule {
5166 value: "E03".into(),
5167 description: "Code 3".into(),
5168 ahb_status: "X [5P1..3]".into(),
5169 },
5170 ],
5171 parent_group_ahb_status: Some("Muss".to_string()),
5172 segment_ahb_status: None,
5173 mig_number: None,
5174 format: None,
5175 }],
5176 };
5177
5178 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5179 let pkg_errors: Vec<_> = report
5180 .by_category(ValidationCategory::Ahb)
5181 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5182 .collect();
5183 assert_eq!(
5184 pkg_errors.len(),
5185 1,
5186 "0 codes present, min=1 — expected 1 error, got: {:?}",
5187 pkg_errors
5188 );
5189 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
5190 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
5191 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
5192 }
5193
5194 #[test]
5195 fn test_package_cardinality_no_packages_in_workflow() {
5196 let evaluator = MockEvaluator::all_true(&[]);
5198 let validator = EdifactValidator::new(evaluator);
5199 let external = NoOpExternalProvider;
5200
5201 let segments = vec![OwnedSegment {
5202 id: "STS".into(),
5203 elements: vec![vec!["E01".into()]],
5204 segment_number: 5,
5205 }];
5206
5207 let workflow = AhbWorkflow {
5208 pruefidentifikator: "13017".to_string(),
5209 description: "Test".to_string(),
5210 communication_direction: None,
5211 ub_definitions: BTreeMap::new(),
5212 package_conditions: Default::default(),
5213 fields: vec![AhbFieldRule {
5214 segment_path: "STS/9015".to_string(),
5215 name: "Status Code".to_string(),
5216 ahb_status: "X".to_string(),
5217 codes: vec![AhbCodeRule {
5218 value: "E01".into(),
5219 description: "Code 1".into(),
5220 ahb_status: "X".into(),
5221 }],
5222 parent_group_ahb_status: Some("Muss".to_string()),
5223 segment_ahb_status: None,
5224 ..Default::default()
5225 }],
5226 };
5227
5228 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5229 let pkg_errors: Vec<_> = report
5230 .by_category(ValidationCategory::Ahb)
5231 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5232 .collect();
5233 assert!(
5234 pkg_errors.is_empty(),
5235 "No packages in workflow — no errors expected"
5236 );
5237 }
5238
5239 #[test]
5240 fn test_package_cardinality_with_condition_and_package() {
5241 let evaluator = MockEvaluator::all_true(&[901]);
5243 let validator = EdifactValidator::new(evaluator);
5244 let external = NoOpExternalProvider;
5245
5246 let segments = vec![OwnedSegment {
5247 id: "STS".into(),
5248 elements: vec![vec![], vec![], vec!["E01".into()]],
5249 segment_number: 5,
5250 }];
5251
5252 let workflow = AhbWorkflow {
5253 pruefidentifikator: "13017".to_string(),
5254 description: "Test".to_string(),
5255 communication_direction: None,
5256 ub_definitions: BTreeMap::new(),
5257 package_conditions: Default::default(),
5258 fields: vec![AhbFieldRule {
5259 segment_path: "SG10/STS/C556/9013".to_string(),
5260 name: "Code".to_string(),
5261 ahb_status: "X".to_string(),
5262 element_index: Some(2),
5263 component_index: Some(0),
5264 codes: vec![
5265 AhbCodeRule {
5266 value: "E01".into(),
5267 description: "Code 1".into(),
5268 ahb_status: "X [901] [4P0..1]".into(),
5269 },
5270 AhbCodeRule {
5271 value: "E02".into(),
5272 description: "Code 2".into(),
5273 ahb_status: "X [901] [4P0..1]".into(),
5274 },
5275 ],
5276 parent_group_ahb_status: Some("Muss".to_string()),
5277 segment_ahb_status: None,
5278 mig_number: None,
5279 format: None,
5280 }],
5281 };
5282
5283 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5284 let pkg_errors: Vec<_> = report
5285 .by_category(ValidationCategory::Ahb)
5286 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5287 .collect();
5288 assert!(
5289 pkg_errors.is_empty(),
5290 "1 code within [4P0..1] bounds — no error, got: {:?}",
5291 pkg_errors
5292 );
5293 }
5294
5295 #[test]
5296 fn test_package_binds_only_where_its_condition_holds() {
5297 let report_for = |package_condition: CR| {
5301 let validator =
5302 EdifactValidator::new(MockEvaluator::new(vec![(902, package_condition)]));
5303 let segments = vec![OwnedSegment {
5304 id: "STS".into(),
5305 elements: vec![vec![], vec![], vec!["E09".into()]],
5306 segment_number: 5,
5307 }];
5308 let workflow = AhbWorkflow {
5309 pruefidentifikator: "13017".to_string(),
5310 description: "Test".to_string(),
5311 communication_direction: None,
5312 ub_definitions: BTreeMap::new(),
5313 package_conditions: BTreeMap::from([(5, ConditionExpr::Ref(902))]),
5314 fields: vec![AhbFieldRule {
5315 segment_path: "SG10/STS/C556/9013".to_string(),
5316 name: "Code".to_string(),
5317 ahb_status: "X".to_string(),
5318 element_index: Some(2),
5319 component_index: Some(0),
5320 codes: vec![
5321 AhbCodeRule {
5322 value: "E01".into(),
5323 description: "Code 1".into(),
5324 ahb_status: "X [5P1..1]".into(),
5325 },
5326 AhbCodeRule {
5327 value: "E09".into(),
5328 description: "Code 9".into(),
5329 ahb_status: "X".into(),
5330 },
5331 ],
5332 parent_group_ahb_status: Some("Muss".to_string()),
5333 segment_ahb_status: None,
5334 mig_number: None,
5335 format: None,
5336 }],
5337 };
5338 validator
5339 .validate(
5340 &segments,
5341 &workflow,
5342 &NoOpExternalProvider,
5343 ValidationLevel::Full,
5344 )
5345 .by_category(ValidationCategory::Ahb)
5346 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5347 .count()
5348 };
5349 assert_eq!(report_for(CR::False), 0, "[902] false: [5P] does not bind");
5350 assert_eq!(report_for(CR::True), 1, "[902] true: [5P1..1] binds");
5351 }
5352
5353 #[test]
5354 fn test_package_cardinality_scoped_per_group_instance() {
5355 use mig_types::navigator::GroupNavigator;
5363
5364 struct TwoSg10s {
5365 sts_a: OwnedSegment,
5366 sts_b: OwnedSegment,
5367 }
5368 impl GroupNavigator for TwoSg10s {
5369 fn find_segments_in_group(
5370 &self,
5371 segment_id: &str,
5372 group_path: &[&str],
5373 instance_index: usize,
5374 ) -> Vec<OwnedSegment> {
5375 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
5376 match instance_index {
5377 0 => vec![self.sts_a.clone()],
5378 1 => vec![self.sts_b.clone()],
5379 _ => vec![],
5380 }
5381 } else {
5382 vec![]
5383 }
5384 }
5385 fn find_segments_with_qualifier_in_group(
5386 &self,
5387 _: &str,
5388 _: usize,
5389 _: &str,
5390 _: &[&str],
5391 _: usize,
5392 ) -> Vec<OwnedSegment> {
5393 vec![]
5394 }
5395 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5396 match group_path {
5397 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
5398 ["SG5", "SG6", "SG9", "SG10"] => 2,
5399 _ => 0,
5400 }
5401 }
5402 }
5403
5404 let sts_a = OwnedSegment {
5405 id: "STS".into(),
5406 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5407 segment_number: 10,
5408 };
5409 let sts_b = OwnedSegment {
5410 id: "STS".into(),
5411 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5412 segment_number: 15,
5413 };
5414 let nav = TwoSg10s {
5415 sts_a: sts_a.clone(),
5416 sts_b: sts_b.clone(),
5417 };
5418
5419 let evaluator = MockEvaluator::all_true(&[]);
5420 let validator = EdifactValidator::new(evaluator);
5421 let external = NoOpExternalProvider;
5422
5423 let workflow = AhbWorkflow {
5424 pruefidentifikator: "13025".to_string(),
5425 description: "Test".to_string(),
5426 communication_direction: None,
5427 ub_definitions: BTreeMap::new(),
5428 package_conditions: Default::default(),
5429 fields: vec![AhbFieldRule {
5430 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5431 name: "Statusanlaß, Code".to_string(),
5432 ahb_status: "X".to_string(),
5433 element_index: Some(2),
5434 component_index: Some(0),
5435 codes: vec![
5436 AhbCodeRule {
5437 value: "E01".into(),
5438 description: "Code 1".into(),
5439 ahb_status: "X [4P0..1]".into(),
5440 },
5441 AhbCodeRule {
5442 value: "E02".into(),
5443 description: "Code 2".into(),
5444 ahb_status: "X [4P0..1]".into(),
5445 },
5446 ],
5447 parent_group_ahb_status: Some("Muss".to_string()),
5448 segment_ahb_status: None,
5449 mig_number: None,
5450 format: None,
5451 }],
5452 };
5453
5454 let report = validator.validate_with_navigator(
5455 &[sts_a, sts_b],
5456 &workflow,
5457 &external,
5458 ValidationLevel::Full,
5459 &nav,
5460 );
5461 let pkg_errors: Vec<_> = report
5462 .by_category(ValidationCategory::Ahb)
5463 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5464 .collect();
5465 assert!(
5466 pkg_errors.is_empty(),
5467 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
5468 pkg_errors
5469 );
5470 }
5471
5472 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
5473 OwnedSegment {
5474 id: id.to_string(),
5475 elements: elements
5476 .into_iter()
5477 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
5478 .collect(),
5479 segment_number: 0,
5480 }
5481 }
5482
5483 #[test]
5494 fn test_package_cardinality_scoped_to_rule_mig_variant() {
5495 use mig_types::navigator::GroupNavigator;
5496
5497 struct TwoSg8Variants {
5498 seq_z01: OwnedSegment,
5499 seq_z45: OwnedSegment,
5500 }
5501 impl GroupNavigator for TwoSg8Variants {
5502 fn find_segments_in_group(
5503 &self,
5504 segment_id: &str,
5505 group_path: &[&str],
5506 instance_index: usize,
5507 ) -> Vec<OwnedSegment> {
5508 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
5509 match instance_index {
5510 0 => vec![self.seq_z01.clone()],
5511 1 => vec![self.seq_z45.clone()],
5512 _ => vec![],
5513 }
5514 } else {
5515 vec![]
5516 }
5517 }
5518 fn find_segments_with_qualifier_in_group(
5519 &self,
5520 _: &str,
5521 _: usize,
5522 _: &str,
5523 _: &[&str],
5524 _: usize,
5525 ) -> Vec<OwnedSegment> {
5526 vec![]
5527 }
5528 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5529 match group_path {
5530 ["SG4"] => 1,
5531 ["SG4", "SG8"] => 2,
5532 _ => 0,
5533 }
5534 }
5535 fn instance_has_mig_number(
5536 &self,
5537 group_path: &[&str],
5538 instance_index: usize,
5539 mig_number: &str,
5540 ) -> bool {
5541 if group_path != ["SG4", "SG8"] {
5542 return true;
5543 }
5544 match (instance_index, mig_number) {
5545 (0, "00115") => true,
5546 (0, _) => false,
5547 (1, "00171") => true,
5548 (1, _) => false,
5549 _ => false,
5550 }
5551 }
5552 }
5553
5554 let seq_z01 = OwnedSegment {
5555 id: "SEQ".into(),
5556 elements: vec![vec!["Z01".into()], vec!["1".into()]],
5557 segment_number: 10,
5558 };
5559 let seq_z45 = OwnedSegment {
5560 id: "SEQ".into(),
5561 elements: vec![vec!["Z45".into()], vec!["1".into()]],
5562 segment_number: 20,
5563 };
5564 let nav = TwoSg8Variants {
5565 seq_z01: seq_z01.clone(),
5566 seq_z45: seq_z45.clone(),
5567 };
5568
5569 let evaluator = MockEvaluator::all_true(&[]);
5570 let validator = EdifactValidator::new(evaluator);
5571 let external = NoOpExternalProvider;
5572
5573 let workflow = AhbWorkflow {
5574 pruefidentifikator: "55218".to_string(),
5575 description: "Test".to_string(),
5576 communication_direction: None,
5577 ub_definitions: BTreeMap::new(),
5578 package_conditions: Default::default(),
5579 fields: vec![AhbFieldRule {
5580 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5581 name: "Handlung, Code".to_string(),
5582 ahb_status: "X".to_string(),
5583 element_index: Some(0),
5584 component_index: Some(0),
5585 codes: vec![
5586 AhbCodeRule {
5587 value: "Z45".into(),
5588 description: "NNA".into(),
5589 ahb_status: "X [1P1..4294967295]".into(),
5590 },
5591 AhbCodeRule {
5592 value: "Z84".into(),
5593 description: "Differenz-NNA".into(),
5594 ahb_status: "X [1P0..4294967295]".into(),
5595 },
5596 ],
5597 parent_group_ahb_status: Some("Muss".to_string()),
5598 segment_ahb_status: None,
5599 mig_number: Some("00171".to_string()),
5600 format: None,
5601 }],
5602 };
5603
5604 let report = validator.validate_with_navigator(
5605 &[seq_z01, seq_z45],
5606 &workflow,
5607 &external,
5608 ValidationLevel::Full,
5609 &nav,
5610 );
5611 let pkg_errors: Vec<_> = report
5612 .by_category(ValidationCategory::Ahb)
5613 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5614 .collect();
5615 assert!(
5616 pkg_errors.is_empty(),
5617 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
5618 pkg_errors
5619 );
5620 }
5621
5622 #[test]
5623 fn test_unt_count_correct() {
5624 let segments = vec![
5626 make_segment("UNH", vec![vec!["001"]]),
5627 make_segment("BGM", vec![vec!["E01"]]),
5628 make_segment("DTM", vec![vec!["137", "20250401"]]),
5629 make_segment("UNT", vec![vec!["4", "001"]]),
5630 ];
5631 assert!(
5632 validate_unt_segment_count(&segments).is_none(),
5633 "Correct count should produce no issue"
5634 );
5635 }
5636
5637 #[test]
5638 fn test_unt_count_mismatch() {
5639 let segments = vec![
5641 make_segment("UNH", vec![vec!["001"]]),
5642 make_segment("BGM", vec![vec!["E01"]]),
5643 make_segment("UNT", vec![vec!["5", "001"]]),
5644 ];
5645 let issue =
5646 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
5647 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5648 assert_eq!(issue.severity, Severity::Error);
5649 assert!(narrate(&issue).contains("declared 5"));
5650 assert!(narrate(&issue).contains("actual 3"));
5651 }
5652
5653 #[test]
5654 fn test_unt_count_excludes_envelope() {
5655 let segments = vec![
5657 make_segment("UNA", vec![]),
5658 make_segment("UNB", vec![vec!["UNOC", "3"]]),
5659 make_segment("UNH", vec![vec!["001"]]),
5660 make_segment("BGM", vec![vec!["E01"]]),
5661 make_segment("UNT", vec![vec!["3", "001"]]),
5662 make_segment("UNZ", vec![vec!["1"]]),
5663 ];
5664 assert!(
5665 validate_unt_segment_count(&segments).is_none(),
5666 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
5667 );
5668 }
5669
5670 #[test]
5671 fn test_unt_count_no_unt_returns_none() {
5672 let segments = vec![
5673 make_segment("UNH", vec![vec!["001"]]),
5674 make_segment("BGM", vec![vec!["E01"]]),
5675 ];
5676 assert!(
5677 validate_unt_segment_count(&segments).is_none(),
5678 "No UNT segment should return None (not our problem)"
5679 );
5680 }
5681
5682 #[test]
5683 fn test_unt_count_rejects_multi_message_input() {
5684 let segments = vec![
5686 make_segment("UNH", vec![vec!["001"]]),
5687 make_segment("BGM", vec![vec!["E01"]]),
5688 make_segment("UNT", vec![vec!["3", "001"]]),
5689 make_segment("UNH", vec![vec!["002"]]),
5690 make_segment("BGM", vec![vec!["E02"]]),
5691 make_segment("UNT", vec![vec!["3", "002"]]),
5692 ];
5693 let issue = validate_unt_segment_count(&segments)
5694 .expect("Multi-message input should produce an error");
5695 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5696 assert!(
5697 narrate(&issue).contains("2 UNH"),
5698 "Should mention UNH count: {}",
5699 narrate(&issue)
5700 );
5701 }
5702
5703 #[test]
5704 fn test_code_validation_accepts_multi_code_variant_qualifier() {
5705 let evaluator = MockEvaluator::new(vec![]);
5712 let validator = EdifactValidator::new(evaluator);
5713 let external = NoOpExternalProvider;
5714
5715 let rff_z39 = OwnedSegment {
5716 id: "RFF".to_string(),
5717 elements: vec![
5718 vec!["RFF".to_string()],
5719 vec!["Z39".to_string(), "REF1".to_string()],
5720 ],
5721 segment_number: 1,
5722 };
5723
5724 let workflow = AhbWorkflow {
5725 pruefidentifikator: "55035".to_string(),
5726 description: "Test".to_string(),
5727 communication_direction: None,
5728 fields: vec![
5729 AhbFieldRule {
5731 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5732 name: "Referenznummer Qualifier".to_string(),
5733 ahb_status: "Muss".to_string(),
5734 codes: vec![
5735 AhbCodeRule {
5736 value: "Z31".to_string(),
5737 description: "".to_string(),
5738 ahb_status: "X".to_string(),
5739 },
5740 AhbCodeRule {
5741 value: "Z39".to_string(),
5742 description: "".to_string(),
5743 ahb_status: "X".to_string(),
5744 },
5745 ],
5746 parent_group_ahb_status: None,
5747 segment_ahb_status: None,
5748 element_index: Some(1),
5749 component_index: Some(0),
5750 mig_number: Some("00075".to_string()),
5751 format: None,
5752 },
5753 AhbFieldRule {
5755 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5756 name: "Referenznummer Qualifier".to_string(),
5757 ahb_status: "Muss".to_string(),
5758 codes: vec![AhbCodeRule {
5759 value: "Z33".to_string(),
5760 description: "".to_string(),
5761 ahb_status: "X".to_string(),
5762 }],
5763 parent_group_ahb_status: None,
5764 segment_ahb_status: None,
5765 element_index: Some(1),
5766 component_index: Some(0),
5767 mig_number: Some("00078".to_string()),
5768 format: None,
5769 },
5770 ],
5771 ub_definitions: BTreeMap::new(),
5772 package_conditions: Default::default(),
5773 };
5774
5775 let report = validator.validate(
5776 &[rff_z39],
5777 &workflow,
5778 &external,
5779 ValidationLevel::Conditions,
5780 );
5781
5782 let code_errors: Vec<_> = report
5783 .by_category(ValidationCategory::Code)
5784 .filter(|i| {
5785 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5786 })
5787 .collect();
5788 assert!(
5789 code_errors.is_empty(),
5790 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
5791 code_errors
5792 );
5793 }
5794
5795 #[test]
5796 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
5797 let evaluator = MockEvaluator::new(vec![]);
5803 let validator = EdifactValidator::new(evaluator);
5804 let external = NoOpExternalProvider;
5805
5806 let pia_5_z12 = OwnedSegment {
5810 id: "PIA".to_string(),
5811 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
5812 segment_number: 1,
5813 };
5814 let pia_5_srw = OwnedSegment {
5815 id: "PIA".to_string(),
5816 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
5817 segment_number: 2,
5818 };
5819
5820 let make_rules = |mig: &str, composite_code: &str| {
5821 vec![
5822 AhbFieldRule {
5823 segment_path: "SG4/SG8/PIA/4347".to_string(),
5824 name: "Produkt-ID-Funktion".to_string(),
5825 ahb_status: "Muss".to_string(),
5826 codes: vec![AhbCodeRule {
5827 value: "5".to_string(),
5828 description: "".to_string(),
5829 ahb_status: "X".to_string(),
5830 }],
5831 parent_group_ahb_status: None,
5832 segment_ahb_status: None,
5833 element_index: Some(0),
5834 component_index: Some(0),
5835 mig_number: Some(mig.to_string()),
5836 format: None,
5837 },
5838 AhbFieldRule {
5839 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
5840 name: "Artikel/Dienstleistung-Identifikator".to_string(),
5841 ahb_status: "Muss".to_string(),
5842 codes: vec![AhbCodeRule {
5843 value: composite_code.to_string(),
5844 description: "".to_string(),
5845 ahb_status: "X".to_string(),
5846 }],
5847 parent_group_ahb_status: None,
5848 segment_ahb_status: None,
5849 element_index: Some(1),
5850 component_index: Some(0),
5851 mig_number: Some(mig.to_string()),
5852 format: None,
5853 },
5854 ]
5855 };
5856
5857 let mut fields = make_rules("00108", "Z12");
5858 fields.extend(make_rules("00197", "SRW"));
5859
5860 let workflow = AhbWorkflow {
5861 pruefidentifikator: "55035".to_string(),
5862 description: "Test".to_string(),
5863 communication_direction: None,
5864 fields,
5865 ub_definitions: BTreeMap::new(),
5866 package_conditions: Default::default(),
5867 };
5868
5869 let report = validator.validate(
5870 &[pia_5_z12, pia_5_srw],
5871 &workflow,
5872 &external,
5873 ValidationLevel::Conditions,
5874 );
5875
5876 let code_errors: Vec<_> = report
5877 .by_category(ValidationCategory::Code)
5878 .filter(|i| {
5879 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5880 })
5881 .collect();
5882 assert!(
5883 code_errors.is_empty(),
5884 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
5885 code_errors
5886 );
5887 }
5888}