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_types::navigator::GroupNavigator;
13use mig_types::segment::OwnedSegment;
14
15use super::tree::{AhbGroupNode, AhbNode, ValidatedTree};
16
17use super::issue::{Severity, ValidationIssue};
18use super::level::ValidationLevel;
19use super::report::ValidationReport;
20use crate::{IssueKind, UnresolvedConditions};
21
22pub use ahb_types::{AhbCodeRule, AhbFieldRule, AhbWorkflow};
28
29pub struct EdifactValidator<E: ConditionEvaluator> {
62 evaluator: E,
63 report_segments: bool,
64}
65
66impl<E: ConditionEvaluator> EdifactValidator<E> {
67 pub fn new(evaluator: E) -> Self {
69 Self {
70 evaluator,
71 report_segments: false,
72 }
73 }
74
75 pub fn reporting_segments(mut self) -> Self {
81 self.report_segments = true;
82 self
83 }
84
85 pub fn validate(
98 &self,
99 segments: &[OwnedSegment],
100 workflow: &AhbWorkflow,
101 external: &dyn ExternalConditionProvider,
102 level: ValidationLevel,
103 ) -> ValidationReport {
104 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
105 .with_format_version(self.evaluator.format_version())
106 .with_pruefidentifikator(&workflow.pruefidentifikator);
107
108 let ctx = EvaluationContext::new(&workflow.pruefidentifikator, external, segments);
109
110 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
111 self.validate_conditions(workflow, &ctx, &mut report);
112 }
113
114 report
115 }
116
117 pub fn validate_with_navigator(
123 &self,
124 segments: &[OwnedSegment],
125 workflow: &AhbWorkflow,
126 external: &dyn ExternalConditionProvider,
127 level: ValidationLevel,
128 navigator: &dyn GroupNavigator,
129 ) -> ValidationReport {
130 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
131 .with_format_version(self.evaluator.format_version())
132 .with_pruefidentifikator(&workflow.pruefidentifikator);
133
134 let ctx = EvaluationContext::with_navigator(
135 &workflow.pruefidentifikator,
136 external,
137 segments,
138 navigator,
139 );
140
141 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
142 self.validate_conditions(workflow, &ctx, &mut report);
143 }
144
145 report
146 }
147
148 pub fn validate_tree(
155 &self,
156 validated_tree: &ValidatedTree,
157 segments: &[OwnedSegment],
158 external: &dyn ExternalConditionProvider,
159 level: ValidationLevel,
160 navigator: Option<&dyn GroupNavigator>,
161 ) -> ValidationReport {
162 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
163 .with_format_version(self.evaluator.format_version())
164 .with_pruefidentifikator(validated_tree.pruefidentifikator);
165
166 if !matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
167 return report;
168 }
169
170 let ctx = match navigator {
171 Some(nav) => EvaluationContext::with_navigator(
172 validated_tree.pruefidentifikator,
173 external,
174 segments,
175 nav,
176 ),
177 None => EvaluationContext::new(validated_tree.pruefidentifikator, external, segments),
178 };
179
180 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
181
182 let mut all_nodes: Vec<&AhbNode> = Vec::new();
185 all_nodes.extend(validated_tree.root_fields.iter());
186 for group in &validated_tree.groups {
187 collect_nodes_depth_first(group, &mut all_nodes);
188 }
189
190 let mut tag_migs: HashMap<String, HashSet<&str>> = HashMap::new();
197 for node in &all_nodes {
198 if let Some(ref m) = node.rule.mig_number {
199 tag_migs
200 .entry(extract_segment_id(&node.rule.segment_path))
201 .or_default()
202 .insert(m.as_str());
203 }
204 }
205 for rule in &validated_tree.unmatched_rules {
206 if let Some(ref m) = rule.mig_number {
207 tag_migs
208 .entry(extract_segment_id(&rule.segment_path))
209 .or_default()
210 .insert(m.as_str());
211 }
212 }
213
214 for node in &validated_tree.root_fields {
216 evaluate_node(
217 node,
218 &ctx,
219 &expr_eval,
220 &self.evaluator,
221 validated_tree.ub_definitions,
222 &tag_migs,
223 None,
224 true,
225 &mut report,
226 );
227 }
228
229 let mut instance_counter: HashMap<&str, usize> = HashMap::new();
234 for group in &validated_tree.groups {
235 let instance_index = *instance_counter
236 .entry(group.group_id)
237 .and_modify(|c| *c += 1)
238 .or_insert(0);
239
240 let path_storage = [(group.group_id, instance_index)];
241 let scoped_ctx = ctx.with_scope(GroupScope {
244 path: &path_storage,
245 });
246
247 walk_group_instance(
248 group,
249 &path_storage,
250 &scoped_ctx,
251 &expr_eval,
252 &self.evaluator,
253 validated_tree.ub_definitions,
254 &tag_migs,
255 instance_index,
256 self.report_segments,
257 &mut report,
258 );
259 }
260
261 if let Some((once, transaction)) =
263 crate::eval::once_per_transaction(self.evaluator.message_type())
264 {
265 let mut index: HashMap<&str, usize> = HashMap::new();
266 for group in &validated_tree.groups {
267 let i = *index
268 .entry(group.group_id)
269 .and_modify(|c| *c += 1)
270 .or_insert(0);
271 if group.group_id == transaction {
272 report_repeated_in_transaction(group, i, once, &ctx, &mut report);
273 }
274 }
275 }
276
277 let absent = crate::eval::AbsentTarget(&self.evaluator);
287 let absent_eval = ConditionExprEvaluator::new(&absent);
288 for field in &validated_tree.unmatched_rules {
289 if should_skip_for_parent_group(
290 field,
291 &absent_eval,
292 &ctx,
293 validated_tree.ub_definitions,
294 ) {
295 continue;
296 }
297
298 let status =
299 absent_eval.resolve_status(&field.ahb_status, &ctx, validated_tree.ub_definitions);
300
301 if status.result == ConditionResult::True
302 && status.kind == Some(StatusKind::Muss)
303 && !is_field_present(&ctx, field)
304 && !is_group_variant_absent(
305 &ctx,
306 field,
307 is_parent_group_required(
308 field,
309 &absent_eval,
310 &ctx,
311 validated_tree.ub_definitions,
312 ),
313 )
314 {
315 let mut issue = ValidationIssue::new(
316 Severity::Error,
317 IssueKind::MissingRequiredField {
318 field_name: field.name.clone(),
319 },
320 )
321 .with_field_path(&field.segment_path)
322 .with_rule(&field.ahb_status);
323 if let Some(first_code) = field.codes.first() {
324 issue.expected_value = Some(first_code.value.clone());
325 }
326 issue = attach_field_position(issue, field);
327 report.add_issue(issue);
328 }
329 }
330
331 if matches!(level, ValidationLevel::Full) {
335 let mut all_fields: Vec<AhbFieldRule> = Vec::new();
336 for node in &all_nodes {
337 all_fields.push(node.rule.clone());
338 }
339 for rule in &validated_tree.unmatched_rules {
340 all_fields.push((*rule).clone());
341 }
342 let synthetic_workflow = AhbWorkflow {
343 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
344 description: String::new(),
345 communication_direction: None,
346 fields: all_fields,
347 ub_definitions: validated_tree.ub_definitions.clone(),
348 };
349 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
350 self.validate_answer_lists(&synthetic_workflow, &ctx, &mut report);
351 self.validate_package_cardinality(&synthetic_workflow, &ctx, &mut report);
352 }
353
354 report
355 }
356
357 fn validate_conditions(
359 &self,
360 workflow: &AhbWorkflow,
361 ctx: &EvaluationContext,
362 report: &mut ValidationReport,
363 ) {
364 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
365
366 for field in &workflow.fields {
367 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
369 continue;
370 }
371
372 let StatusResolution {
375 result: condition_result,
376 kind,
377 unknown_ids,
378 } = expr_eval.resolve_status(&field.ahb_status, ctx, &workflow.ub_definitions);
379 let mandatory = kind == Some(StatusKind::Muss);
380
381 match condition_result {
382 ConditionResult::True => {
383 if mandatory
385 && !is_field_present(ctx, field)
386 && !is_group_variant_absent(
387 ctx,
388 field,
389 is_parent_group_required(
390 field,
391 &expr_eval,
392 ctx,
393 &workflow.ub_definitions,
394 ),
395 )
396 {
397 let mut issue = ValidationIssue::new(
398 Severity::Error,
399 IssueKind::MissingRequiredField {
400 field_name: field.name.clone(),
401 },
402 )
403 .with_field_path(&field.segment_path)
404 .with_rule(&field.ahb_status);
405 if let Some(first_code) = field.codes.first() {
409 issue.expected_value = Some(first_code.value.clone());
410 }
411 report.add_issue(issue);
412 }
413 }
414 ConditionResult::False => {
415 if mandatory && is_field_present(ctx, field) {
421 report.add_issue(
422 ValidationIssue::new(
423 Severity::Error,
424 IssueKind::FieldConditionNotSatisfied {
425 field_name: field.name.clone(),
426 },
427 )
428 .with_field_path(&field.segment_path)
429 .with_rule(&field.ahb_status),
430 );
431 }
432 }
433 ConditionResult::Unknown => {
434 let mut external_ids = Vec::new();
439 let mut undetermined_ids = Vec::new();
440 let mut missing_ids = Vec::new();
441 for id in unknown_ids {
442 if self.evaluator.is_external(id) {
443 external_ids.push(id);
444 } else if self.evaluator.is_known(id) {
445 undetermined_ids.push(id);
446 } else {
447 missing_ids.push(id);
448 }
449 }
450
451 report.add_issue(
452 ValidationIssue::new(
453 Severity::Info,
454 IssueKind::ConditionUnknown {
455 field_name: field.name.clone(),
456 unresolved: UnresolvedConditions {
457 external: external_ids,
458 undetermined: undetermined_ids,
459 missing: missing_ids,
460 },
461 },
462 )
463 .with_field_path(&field.segment_path)
464 .with_rule(&field.ahb_status),
465 );
466 }
467 }
468 }
469
470 self.validate_codes_cross_field(workflow, ctx, report);
475 self.validate_answer_lists(workflow, ctx, report);
476
477 self.validate_package_cardinality(workflow, ctx, report);
480 }
481
482 fn validate_package_cardinality(
489 &self,
490 workflow: &AhbWorkflow,
491 ctx: &EvaluationContext,
492 report: &mut ValidationReport,
493 ) {
494 struct PackageGroup {
497 min: u32,
498 max: u32,
499 max_sum: u32,
502 bounds: BTreeSet<(u32, u32)>,
503 code_values: Vec<String>,
504 element_index: usize,
505 component_index: usize,
506 }
507
508 let mut groups: HashMap<(String, Option<String>, u32, u32, u32), PackageGroup> =
517 HashMap::new();
518 let mut alternatives: HashMap<(String, Option<String>, u32), BTreeSet<u32>> =
522 HashMap::new();
523
524 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
525
526 for field in &workflow.fields {
527 let el_idx = field.element_index.unwrap_or(0);
528 let comp_idx = field.component_index.unwrap_or(0);
529
530 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
532 continue;
533 }
534
535 for code in &field.codes {
536 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
538 let mut packages = Vec::new();
540 collect_packages(&expr, &mut packages);
541
542 if packages.len() > 1 {
543 let ids: BTreeSet<u32> = packages.iter().map(|(id, _, _)| *id).collect();
544 for id in &ids {
545 alternatives
546 .entry((field.segment_path.clone(), field.mig_number.clone(), *id))
547 .or_default()
548 .extend(ids.iter().copied().filter(|other| other != id));
549 }
550 }
551
552 for (pkg_id, pkg_min, pkg_max) in packages {
553 let key = (
554 field.segment_path.clone(),
555 field.mig_number.clone(),
556 pkg_id,
557 0,
558 0,
559 );
560 let group = groups.entry(key).or_insert_with(|| PackageGroup {
561 min: pkg_min,
562 max: pkg_max,
563 max_sum: 0,
564 bounds: BTreeSet::new(),
565 code_values: Vec::new(),
566 element_index: el_idx,
567 component_index: comp_idx,
568 });
569 group.bounds.insert((pkg_min, pkg_max));
576 group.min = group.min.min(pkg_min);
577 if !group.code_values.contains(&code.value) {
578 group.max_sum = group.max_sum.saturating_add(pkg_max);
579 }
580 group.max = group.max.max(pkg_max);
581 group.code_values.push(code.value.clone());
582 }
583 }
584 }
585 }
586
587 let mut field_packages: HashMap<(String, Option<String>), BTreeSet<u32>> = HashMap::new();
598 for (seg_path, mig_number, pkg_id, _, _) in groups.keys() {
599 field_packages
600 .entry((seg_path.clone(), mig_number.clone()))
601 .or_default()
602 .insert(*pkg_id);
603 }
604 let with_alternatives: Vec<(String, Option<String>)> = alternatives
605 .keys()
606 .map(|(p, m, _)| (p.clone(), m.clone()))
607 .collect();
608 for field in with_alternatives {
609 let Some(ids) = field_packages.get(&field) else {
610 continue;
611 };
612 for id in ids {
613 alternatives
614 .entry((field.0.clone(), field.1.clone(), *id))
615 .or_default()
616 .extend(ids.iter().copied().filter(|other| other != id));
617 }
618 }
619
620 type Counted = (Vec<usize>, usize, usize, Vec<String>);
622 let mut counted: HashMap<(String, Option<String>, u32, u32, u32), Counted> = HashMap::new();
623 for ((seg_path, mig_number, pkg_id, _, _), group) in &groups {
624 let segment_id = extract_segment_id(seg_path);
625
626 let mut unique_codes: Vec<&str> =
630 group.code_values.iter().map(|s| s.as_str()).collect();
631 unique_codes.sort_unstable();
632 unique_codes.dedup();
633 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
634
635 let group_path_str = extract_group_path_key(seg_path);
636 let group_path: Vec<&str> = if group_path_str.is_empty() {
637 Vec::new()
638 } else {
639 group_path_str.split('/').collect()
640 };
641
642 let min = group.min as usize;
643 let max = if group.bounds.len() > 1 {
644 group.max_sum as usize
645 } else {
646 group.max as usize
647 };
648
649 let per_instance_counts: Option<Vec<usize>> =
650 match (ctx.navigator, group_path.is_empty()) {
651 (Some(nav), false)
659 if group_path.len() > 1
660 && !full_instance_paths(nav, &group_path).is_empty() =>
661 {
662 let paths = full_instance_paths(nav, &group_path);
663 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
671 .as_deref()
672 {
673 Some(m) => (0..group_path.len())
674 .filter_map(|level| {
675 let prefix = group_path[..=level].join("/");
676 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
677 })
678 .collect(),
679 None => Vec::new(),
680 };
681 if paths.is_empty() {
682 None
683 } else {
684 Some(
685 paths
686 .iter()
687 .filter(|path| match mig_number.as_deref() {
688 Some(m) => nav.instance_at_has_mig_number(path, m),
689 None => true,
690 })
691 .filter(|path| {
692 variant_entries.iter().all(|(level, entry)| {
693 nav.find_segments_in_instance(
694 &entry.tag,
695 &path[..=*level],
696 )
697 .first()
698 .and_then(|seg| {
699 seg.elements
700 .get(entry.element)
701 .and_then(|e| e.get(entry.component))
702 .cloned()
703 })
704 .map_or(true, |v| entry.codes.contains(v.as_str()))
705 })
706 })
707 .map(|path| {
708 nav.find_segments_in_instance(&segment_id, path)
709 .iter()
710 .filter_map(|seg| {
711 seg.elements
712 .get(group.element_index)
713 .and_then(|e| e.get(group.component_index))
714 .filter(|v| !v.is_empty())
715 .cloned()
716 })
717 .filter(|v| code_set.contains(v.as_str()))
718 .count()
719 })
720 .collect(),
721 )
722 }
723 }
724 (Some(nav), false) => {
725 let instance_count = nav.group_instance_count(&group_path);
726 if instance_count == 0 {
727 None
728 } else {
729 Some(
737 (0..instance_count)
738 .filter(|i| match mig_number.as_deref() {
739 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
740 None => true,
741 })
742 .map(|i| {
743 nav.find_segments_in_group(&segment_id, &group_path, i)
744 .iter()
745 .filter_map(|seg| {
746 seg.elements
747 .get(group.element_index)
748 .and_then(|e| e.get(group.component_index))
749 .filter(|v| !v.is_empty())
750 .cloned()
751 })
752 .filter(|v| code_set.contains(v.as_str()))
753 .count()
754 })
755 .collect(),
756 )
757 }
758 }
759 _ => None,
760 };
761
762 let counts: Vec<usize> = per_instance_counts.unwrap_or_else(|| {
763 let segments = ctx.find_segments(&segment_id);
764 let count = segments
765 .iter()
766 .filter_map(|seg| {
767 seg.elements
768 .get(group.element_index)
769 .and_then(|e| e.get(group.component_index))
770 .filter(|v| !v.is_empty())
771 .map(|s| s.as_str())
772 })
773 .filter(|v| code_set.contains(v))
774 .count();
775 vec![count]
776 });
777
778 counted.insert(
779 (
780 seg_path.clone(),
781 mig_number.clone(),
782 *pkg_id,
783 group.min,
784 group.max,
785 ),
786 (
787 counts,
788 min,
789 max,
790 unique_codes.iter().map(|s| s.to_string()).collect(),
791 ),
792 );
793 }
794
795 let mut keys: Vec<&(String, Option<String>, u32, u32, u32)> = counted.keys().collect();
800 keys.sort();
801 for key in keys {
802 let (counts, min, max, codes) = &counted[key];
803 let (seg_path, mig_number, pkg_id, _, _) = key;
804 let alternative_holds = |i: usize| {
805 alternatives
806 .get(&(seg_path.clone(), mig_number.clone(), *pkg_id))
807 .is_some_and(|alts| {
808 counted.iter().any(|((p, m, id, _, _), (c, lo, hi, _))| {
809 p == seg_path
810 && m == mig_number
811 && alts.contains(id)
812 && c.len() == counts.len()
813 && (*lo..=*hi).contains(&c[i])
814 && c[i] > 0
815 })
816 })
817 };
818 let mut reported_counts: HashSet<usize> = HashSet::new();
819 for (i, &present_count) in counts.iter().enumerate() {
820 if (present_count < *min || present_count > *max)
821 && !alternative_holds(i)
822 && reported_counts.insert(present_count)
823 {
824 report.add_issue(
825 ValidationIssue::new(
826 Severity::Error,
827 IssueKind::PackageCardinality {
828 package_id: pkg_id.to_string(),
829 present: present_count,
830 min: *min,
831 max: *max,
832 codes: codes.clone(),
833 },
834 )
835 .with_field_path(seg_path)
836 .with_expected(format!("{min}..{max}"))
837 .with_actual(present_count.to_string()),
838 );
839 }
840 }
841 }
842 }
843
844 fn validate_codes_cross_field(
856 &self,
857 workflow: &AhbWorkflow,
858 ctx: &EvaluationContext,
859 report: &mut ValidationReport,
860 ) {
861 if ctx.navigator.is_some() {
862 self.validate_codes_group_scoped(workflow, ctx, report);
863 } else {
864 self.validate_codes_tag_scoped(workflow, ctx, report);
865 }
866 }
867
868 fn validate_codes_group_scoped(
871 &self,
872 workflow: &AhbWorkflow,
873 ctx: &EvaluationContext,
874 report: &mut ValidationReport,
875 ) {
876 let by_loc = partition_codes_by_mig(workflow);
877 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
878 let nav = ctx.navigator.unwrap();
879
880 for ((group_key, tag), migs) in &by_loc {
881 let field_path = if group_key.is_empty() {
882 format!("{tag}/qualifier")
883 } else {
884 format!("{group_key}/{tag}/qualifier")
885 };
886
887 let group_path: Vec<&str> = if group_key.is_empty() {
888 Vec::new()
889 } else {
890 group_key.split('/').collect()
891 };
892
893 let tag_qualifiers = known_qualifiers.get(tag);
894
895 if group_path.is_empty() {
896 Self::validate_segments_per_mig(
897 &ctx.find_segments(tag),
898 migs,
899 tag_qualifiers,
900 tag,
901 &field_path,
902 report,
903 );
904 } else {
905 let instance_count = nav.group_instance_count(&group_path);
906 for i in 0..instance_count {
907 let owned = nav.find_segments_in_group(tag, &group_path, i);
908 let refs: Vec<&OwnedSegment> = owned.iter().collect();
909 Self::validate_segments_per_mig(
910 &refs,
911 migs,
912 tag_qualifiers,
913 tag,
914 &field_path,
915 report,
916 );
917 }
918 }
919 }
920 }
921
922 fn validate_codes_tag_scoped(
926 &self,
927 workflow: &AhbWorkflow,
928 ctx: &EvaluationContext,
929 report: &mut ValidationReport,
930 ) {
931 let by_loc = partition_codes_by_mig(workflow);
932 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
933 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
935 for ((_group_key, tag), migs) in by_loc {
936 let merged = by_tag.entry(tag).or_default();
937 for (mig_key, bucket) in migs {
938 let entry = merged.entry(mig_key).or_default();
939 if entry.qualifier_position.is_none() {
940 entry.qualifier_position = bucket.qualifier_position;
941 }
942 if entry.qualifier_position == bucket.qualifier_position {
943 entry.qualifier_values.extend(&bucket.qualifier_values);
944 }
945 for (pos, codes) in bucket.codes {
946 entry.codes.entry(pos).or_default().extend(codes);
947 }
948 }
949 }
950
951 for (tag, migs) in &by_tag {
952 let field_path = format!("{tag}/qualifier");
953 Self::validate_segments_per_mig(
954 &ctx.find_segments(tag),
955 migs,
956 known_qualifiers.get(tag),
957 tag,
958 &field_path,
959 report,
960 );
961 }
962 }
963
964 fn validate_segments_per_mig(
973 segments: &[&OwnedSegment],
974 migs: &HashMap<Option<String>, MigCodeBucket>,
975 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
976 tag: &str,
977 field_path: &str,
978 report: &mut ValidationReport,
979 ) {
980 for seg in segments {
981 match match_segment_to_mig(seg, migs) {
982 Some(bucket) => {
983 for ((el, c), allowed) in &bucket.codes {
984 if allowed.is_empty() {
985 continue;
986 }
987 Self::check_segments_against_codes(
988 vec![*seg],
989 allowed,
990 tag,
991 *el,
992 *c,
993 field_path,
994 report,
995 );
996 }
997 }
998 None => {
999 let Some(qualifiers) = tag_qualifiers else {
1000 continue;
1001 };
1002 for ((el, c), allowed) in qualifiers {
1007 let Some(actual) = seg
1008 .elements
1009 .get(*el)
1010 .and_then(|e| e.get(*c))
1011 .filter(|v| !v.is_empty())
1012 .map(|s| s.as_str())
1013 else {
1014 continue;
1015 };
1016 if allowed.iter().any(|v| v == actual) {
1017 break;
1019 }
1020 let bucket_values: HashSet<&str> = migs
1023 .values()
1024 .filter(|b| b.qualifier_position == Some((*el, *c)))
1025 .flat_map(|b| b.qualifier_values.iter().copied())
1026 .collect();
1027 if !bucket_values.is_empty() {
1028 Self::check_segments_against_codes(
1029 vec![*seg],
1030 &bucket_values,
1031 tag,
1032 *el,
1033 *c,
1034 field_path,
1035 report,
1036 );
1037 break;
1038 }
1039 }
1040 }
1041 }
1042 }
1043 }
1044
1045 fn validate_answer_lists(
1054 &self,
1055 workflow: &AhbWorkflow,
1056 ctx: &EvaluationContext,
1057 report: &mut ValidationReport,
1058 ) {
1059 let lists = AnswerCodeLists::embedded();
1060 let fv = self.evaluator.format_version();
1061 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1062 for list_rule in &workflow.fields {
1063 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1064 continue;
1065 };
1066 if list_rule.codes.is_empty()
1067 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1068 {
1069 continue;
1070 }
1071 let Some(answer_rule) = workflow
1072 .fields
1073 .iter()
1074 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1075 else {
1076 continue;
1077 };
1078 let at = |rule: &AhbFieldRule| {
1079 (
1080 rule.element_index.unwrap_or(0),
1081 rule.component_index.unwrap_or(0),
1082 )
1083 };
1084 let (list_el, list_c) = at(list_rule);
1085 let (answer_el, answer_c) = at(answer_rule);
1086 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1087 seg.elements
1088 .get(el)
1089 .and_then(|e| e.get(c))
1090 .filter(|v| !v.is_empty())
1091 .cloned()
1092 };
1093 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1094 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1095 continue;
1096 }
1097 let (Some(list), Some(answer)) =
1098 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1099 else {
1100 continue;
1101 };
1102 if !list_rule.codes.iter().any(|c| c.value == list) {
1103 continue;
1104 }
1105 if answer_rule.codes.iter().any(|c| c.value == answer) {
1109 continue;
1110 }
1111 let Some(codes) = lists.codes(fv, &list) else {
1112 continue;
1113 };
1114 if codes.contains(&answer) {
1115 continue;
1116 }
1117 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1118 allowed.sort_unstable();
1119 report.add_issue(
1120 ValidationIssue::new(
1121 Severity::Warning,
1122 IssueKind::AnswerNotInList {
1123 answer: answer.clone(),
1124 list: list.clone(),
1125 allowed: allowed.clone(),
1126 },
1127 )
1128 .with_field_path(&answer_rule.segment_path)
1129 .with_actual(&answer)
1130 .with_expected(allowed.join(", ")),
1131 );
1132 }
1133 }
1134 }
1135
1136 fn check_segments_against_codes(
1138 segments: Vec<&OwnedSegment>,
1139 allowed_codes: &HashSet<&str>,
1140 _tag: &str,
1141 el_idx: usize,
1142 comp_idx: usize,
1143 field_path: &str,
1144 report: &mut ValidationReport,
1145 ) {
1146 for segment in segments {
1147 if let Some(code_value) = segment
1148 .elements
1149 .get(el_idx)
1150 .and_then(|e| e.get(comp_idx))
1151 .filter(|v| !v.is_empty())
1152 {
1153 if !allowed_codes.contains(code_value.as_str()) {
1154 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1155 sorted_codes.sort_unstable();
1156 let kind = IssueKind::CodeNotAllowedForPid {
1157 value: code_value.to_string(),
1158 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1159 };
1160 report.add_issue(
1161 ValidationIssue::new(Severity::Error, kind)
1162 .with_field_path(field_path)
1163 .with_actual(code_value)
1164 .with_expected(sorted_codes.join(", ")),
1165 );
1166 }
1167 }
1168 }
1169 }
1170}
1171
1172fn full_instance_paths<'p>(
1175 nav: &dyn GroupNavigator,
1176 group_path: &[&'p str],
1177) -> Vec<Vec<(&'p str, usize)>> {
1178 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1179 for g in group_path {
1180 paths = paths
1181 .into_iter()
1182 .flat_map(|parent| {
1183 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1184 let mut path = parent.clone();
1185 path.push((*g, i));
1186 path
1187 })
1188 })
1189 .collect();
1190 }
1191 paths
1192}
1193
1194struct VariantEntry<'a> {
1197 tag: String,
1198 element: usize,
1199 component: usize,
1200 codes: HashSet<&'a str>,
1201}
1202
1203fn merged_variant_entry<'a>(
1209 workflow: &'a AhbWorkflow,
1210 group_path: &str,
1211 mig: &str,
1212) -> Option<VariantEntry<'a>> {
1213 let mig: u32 = mig.parse().ok()?;
1214 let prefix = format!("{group_path}/");
1215 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1217 .fields
1218 .iter()
1219 .filter_map(|r| {
1220 let rest = r.segment_path.strip_prefix(&prefix)?;
1221 let tag = rest.split('/').next()?;
1222 if tag.starts_with("SG") {
1223 return None;
1224 }
1225 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1226 })
1227 .collect();
1228 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1230 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1231 .iter()
1232 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1233 .collect();
1234 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1235 if numbers.len() < 2 {
1236 return None;
1237 }
1238 let own = entries
1239 .iter()
1240 .filter(|(_, n, _)| *n <= mig)
1241 .map(|(_, n, _)| *n)
1242 .max()?;
1243 let (rule, _, tag) = entries
1245 .iter()
1246 .filter(|(_, n, _)| *n == own)
1247 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1248 Some(VariantEntry {
1249 tag: tag.to_string(),
1250 element: rule.element_index.unwrap_or(0),
1251 component: rule.component_index.unwrap_or(0),
1252 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1253 })
1254}
1255
1256fn value_length(format: &str, value: &str) -> usize {
1260 let format = format.trim();
1261 if format.starts_with('n') {
1262 value
1263 .chars()
1264 .filter(|c| !matches!(c, '.' | ',' | '-'))
1265 .count()
1266 } else {
1267 value.chars().count()
1268 }
1269}
1270
1271fn max_length(format: &str) -> Option<usize> {
1274 let digits = format
1275 .trim()
1276 .strip_prefix("an")
1277 .or_else(|| format.trim().strip_prefix('a'))
1278 .or_else(|| format.trim().strip_prefix('n'))?;
1279 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1280}
1281
1282fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1292 let derived_from_codes = field
1293 .codes
1294 .first()
1295 .is_some_and(|c| c.ahb_status == field.ahb_status);
1296 if !derived_from_codes {
1297 return &field.ahb_status;
1298 }
1299 value
1300 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1301 .map_or(&field.ahb_status, |c| &c.ahb_status)
1302}
1303
1304fn should_skip_for_parent_group<E: ConditionEvaluator>(
1310 field: &AhbFieldRule,
1311 expr_eval: &ConditionExprEvaluator<E>,
1312 ctx: &EvaluationContext,
1313 ub_definitions: &BTreeMap<String, ConditionExpr>,
1314) -> bool {
1315 if let Some(ref group_status) = field.parent_group_ahb_status {
1316 if group_status.contains('[') {
1317 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
1318 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
1319 }
1320 }
1321 false
1322}
1323
1324fn is_parent_group_required<E: ConditionEvaluator>(
1331 field: &AhbFieldRule,
1332 expr_eval: &ConditionExprEvaluator<E>,
1333 ctx: &EvaluationContext,
1334 ub_definitions: &BTreeMap<String, ConditionExpr>,
1335) -> bool {
1336 field
1337 .parent_group_ahb_status
1338 .as_deref()
1339 .is_some_and(|status| {
1340 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1341 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1342 })
1343}
1344
1345fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1354 let segment_id = extract_segment_id(&field.segment_path);
1355
1356 if !field.codes.is_empty() {
1362 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1363 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1364 let matching = ctx.find_segments(&segment_id);
1365 return matching.iter().any(|seg| {
1366 seg.elements
1367 .get(el_idx)
1368 .and_then(|e| e.get(comp_idx))
1369 .is_some_and(|v| required_codes.contains(&v.as_str()))
1370 });
1371 }
1372 if is_qualifier_field(&field.segment_path) {
1375 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1376 let el_idx = field.element_index.unwrap_or(0);
1377 let comp_idx = field.component_index.unwrap_or(0);
1378 let matching = ctx.find_segments(&segment_id);
1379 return matching.iter().any(|seg| {
1380 seg.elements
1381 .get(el_idx)
1382 .and_then(|e| e.get(comp_idx))
1383 .is_some_and(|v| required_codes.contains(&v.as_str()))
1384 });
1385 }
1386 }
1387
1388 ctx.has_segment(&segment_id)
1389}
1390
1391fn is_group_variant_absent(
1406 ctx: &EvaluationContext,
1407 field: &AhbFieldRule,
1408 is_group_mandatory: bool,
1409) -> bool {
1410 let group_path: Vec<&str> = field
1411 .segment_path
1412 .split('/')
1413 .take_while(|p| p.starts_with("SG"))
1414 .collect();
1415
1416 if group_path.is_empty() {
1417 return false;
1418 }
1419
1420 let nav = match ctx.navigator {
1421 Some(nav) => nav,
1422 None => return false,
1423 };
1424
1425 let instance_count = nav.group_instance_count(&group_path);
1426
1427 if instance_count == 0 {
1432 if !is_group_mandatory {
1433 return true;
1434 }
1435 return false;
1437 }
1438
1439 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1444 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1446 let segment_id = extract_segment_id(&field.segment_path);
1447 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1448
1449 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1450 nav.find_segments_in_group(&segment_id, &group_path, i)
1451 .iter()
1452 .any(|seg| {
1453 seg.elements
1454 .first()
1455 .and_then(|e| e.first())
1456 .is_some_and(|v| required_codes.contains(&v.as_str()))
1457 })
1458 });
1459
1460 if !any_instance_has_qualifier {
1461 return true; }
1463 }
1464 }
1465
1466 let segment_id = extract_segment_id(&field.segment_path);
1475 let segment_absent_from_all = (0..instance_count).all(|i| {
1476 nav.find_segments_in_group(&segment_id, &group_path, i)
1477 .is_empty()
1478 });
1479 if segment_absent_from_all {
1480 let group_has_other_segments =
1481 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1482 if group_has_other_segments {
1483 return true;
1484 }
1485 }
1486
1487 false
1488}
1489
1490fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1492 out.extend(group.fields.iter());
1493 for child in &group.children {
1494 collect_nodes_depth_first(child, out);
1495 }
1496}
1497
1498#[allow(clippy::too_many_arguments)]
1502fn evaluate_node<E: ConditionEvaluator>(
1503 node: &AhbNode,
1504 ctx: &EvaluationContext,
1505 expr_eval: &ConditionExprEvaluator<E>,
1506 evaluator: &E,
1507 ub_definitions: &BTreeMap<String, ConditionExpr>,
1508 tag_migs: &HashMap<String, HashSet<&str>>,
1509 instance_index: Option<usize>,
1510 slot_holds: bool,
1511 report: &mut ValidationReport,
1512) {
1513 let field = node.rule;
1514
1515 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1517
1518 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1523 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1524 if length > max {
1525 let mut issue = ValidationIssue::new(
1526 Severity::Error,
1527 IssueKind::ValueTooLong {
1528 field_name: field.name.clone(),
1529 length,
1530 max,
1531 },
1532 )
1533 .with_field_path(&field.segment_path)
1534 .with_actual(value)
1535 .with_expected(field.format.clone().unwrap_or_default());
1536 if let Some(idx) = instance_index {
1537 issue = issue.with_instance_index(idx);
1538 }
1539 if let Some(num) = node.matched_segment_number {
1540 issue = issue.with_position(crate::SegmentPosition {
1541 segment_number: num,
1542 byte_offset: 0,
1543 message_number: 1,
1544 });
1545 }
1546 report.add_issue(attach_field_position(issue, field));
1547 }
1548 }
1549
1550 if let (Some(value), Some(format)) = (node.value, field.format.as_deref()) {
1553 if slot_holds
1554 && format.trim().starts_with('n')
1555 && field.codes.is_empty()
1556 && !value.is_empty()
1557 && !crate::eval::format_validators::is_edifact_number(value)
1558 {
1559 let mut issue = ValidationIssue::new(
1560 Severity::Error,
1561 IssueKind::ValueNotNumeric {
1562 field_name: field.name.clone(),
1563 },
1564 )
1565 .with_field_path(&field.segment_path)
1566 .with_actual(value)
1567 .with_expected(format);
1568 if let Some(idx) = instance_index {
1569 issue = issue.with_instance_index(idx);
1570 }
1571 if let Some(num) = node.matched_segment_number {
1572 issue = issue.with_position(crate::SegmentPosition {
1573 segment_number: num,
1574 byte_offset: 0,
1575 message_number: 1,
1576 });
1577 }
1578 report.add_issue(attach_field_position(issue, field));
1579 }
1580 }
1581
1582 if should_skip_for_parent_group(field, expr_eval, ctx, ub_definitions) {
1584 return;
1585 }
1586
1587 let status = governing_status(field, node.value);
1590 let StatusResolution {
1591 result: condition_result,
1592 kind,
1593 unknown_ids,
1594 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1595 let mandatory = kind == Some(StatusKind::Muss);
1596
1597 match condition_result {
1598 ConditionResult::True => {
1599 if mandatory && node.value.is_none() {
1608 let tag = extract_segment_id(&field.segment_path);
1609 let single_variant_present = node.segment_elements.is_none()
1610 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1611 && is_field_present(ctx, field);
1612 let segment_optional_and_absent = node.segment_elements.is_none()
1617 && field
1618 .segment_ahb_status
1619 .as_deref()
1620 .is_some_and(is_optional_segment_status);
1621 let segment_not_required_and_absent = node.segment_elements.is_none()
1627 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1628 expr_eval
1629 .resolve_status(st, &node_ctx, ub_definitions)
1630 .result
1631 != ConditionResult::True
1632 });
1633 if !single_variant_present
1634 && !segment_optional_and_absent
1635 && !segment_not_required_and_absent
1636 {
1637 let mut issue = ValidationIssue::new(
1638 Severity::Error,
1639 IssueKind::MissingRequiredField {
1640 field_name: field.name.clone(),
1641 },
1642 )
1643 .with_field_path(&field.segment_path)
1644 .with_rule(&field.ahb_status);
1645 if let Some(first_code) = field.codes.first() {
1646 issue.expected_value = Some(first_code.value.clone());
1647 }
1648 if let Some(idx) = instance_index {
1649 issue = issue.with_instance_index(idx);
1650 }
1651 if let Some(num) = node.matched_segment_number {
1652 issue = issue.with_position(crate::SegmentPosition {
1653 segment_number: num,
1654 byte_offset: 0,
1655 message_number: 1,
1656 });
1657 }
1658 issue = attach_field_position(issue, field);
1659 report.add_issue(issue);
1660 }
1661 }
1662 }
1663 ConditionResult::False => {
1664 if mandatory && node.value.is_some() {
1667 let mut issue = ValidationIssue::new(
1668 Severity::Error,
1669 IssueKind::FieldConditionNotSatisfied {
1670 field_name: field.name.clone(),
1671 },
1672 )
1673 .with_field_path(&field.segment_path)
1674 .with_rule(&field.ahb_status);
1675 if let Some(idx) = instance_index {
1676 issue = issue.with_instance_index(idx);
1677 }
1678 if let Some(num) = node.matched_segment_number {
1679 issue = issue.with_position(crate::SegmentPosition {
1680 segment_number: num,
1681 byte_offset: 0,
1682 message_number: 1,
1683 });
1684 }
1685 issue = attach_field_position(issue, field);
1686 report.add_issue(issue);
1687 }
1688 }
1689 ConditionResult::Unknown => {
1690 let mut external_ids = Vec::new();
1692 let mut undetermined_ids = Vec::new();
1693 let mut missing_ids = Vec::new();
1694 for id in unknown_ids {
1695 if evaluator.is_external(id) {
1696 external_ids.push(id);
1697 } else if evaluator.is_known(id) {
1698 undetermined_ids.push(id);
1699 } else {
1700 missing_ids.push(id);
1701 }
1702 }
1703
1704 let mut issue = ValidationIssue::new(
1705 Severity::Info,
1706 IssueKind::ConditionUnknown {
1707 field_name: field.name.clone(),
1708 unresolved: UnresolvedConditions {
1709 external: external_ids,
1710 undetermined: undetermined_ids,
1711 missing: missing_ids,
1712 },
1713 },
1714 )
1715 .with_field_path(&field.segment_path)
1716 .with_rule(&field.ahb_status);
1717 if let Some(idx) = instance_index {
1718 issue = issue.with_instance_index(idx);
1719 }
1720 if let Some(num) = node.matched_segment_number {
1721 issue = issue.with_position(crate::SegmentPosition {
1722 segment_number: num,
1723 byte_offset: 0,
1724 message_number: 1,
1725 });
1726 }
1727 issue = attach_field_position(issue, field);
1728 report.add_issue(issue);
1729 }
1730 }
1731}
1732
1733fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1738 match field.element_index {
1739 Some(el) => {
1740 let element_pos = (el as u32) + 2;
1741 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1742 issue.with_field_position(element_pos, component_pos)
1743 }
1744 None => issue,
1745 }
1746}
1747
1748fn report_repeated_in_transaction(
1755 transaction: &AhbGroupNode,
1756 transaction_index: usize,
1757 once: u32,
1758 ctx: &EvaluationContext,
1759 report: &mut ValidationReport,
1760) {
1761 let mut counts: BTreeMap<(String, String), (usize, String)> = BTreeMap::new();
1763 fn visit<'a>(
1764 node: &'a AhbGroupNode<'a>,
1765 path: &mut Vec<(&'a str, usize)>,
1766 once: u32,
1767 ctx: &EvaluationContext,
1768 counts: &mut BTreeMap<(String, String), (usize, String)>,
1769 ) {
1770 let ids: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1771 let mut seen: HashSet<Option<&str>> = HashSet::new();
1772 for n in &node.fields {
1773 let rule = n.rule;
1774 let Some(status) = rule.segment_ahb_status.as_deref() else {
1775 continue;
1776 };
1777 if Some(status) == node.ahb_status
1778 || rule.element_index.unwrap_or(0) != 0
1779 || rule.component_index.unwrap_or(0) != 0
1780 || rule.codes.is_empty()
1781 || !crate::eval::binds_once(status, once)
1782 || !seen.insert(rule.mig_number.as_deref())
1783 {
1784 continue;
1785 }
1786 let tag = extract_segment_id(&rule.segment_path);
1787 let sent = ctx.navigator().map_or(0, |nav| {
1788 nav.find_segments_in_instance(&tag, path)
1789 .iter()
1790 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
1791 .count()
1792 });
1793 let entry = counts
1794 .entry((
1795 format!("{}/{tag}", ids.join("/")),
1796 rule.mig_number.clone().unwrap_or_default(),
1797 ))
1798 .or_insert((0, status.to_string()));
1799 entry.0 += sent;
1800 }
1801 for child in &node.children {
1802 if let Some(status) = child
1803 .ahb_status
1804 .filter(|s| crate::eval::binds_once(s, once))
1805 {
1806 let variant = child
1807 .fields
1808 .first()
1809 .and_then(|n| n.rule.mig_number.clone())
1810 .unwrap_or_default();
1811 let entry = counts
1812 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
1813 .or_insert((0, status.to_string()));
1814 entry.0 += 1;
1815 }
1816 path.push((child.group_id, child.instance_index));
1817 visit(child, path, once, ctx, counts);
1818 path.pop();
1819 }
1820 }
1821 let mut path = vec![(transaction.group_id, transaction_index)];
1822 visit(transaction, &mut path, once, ctx, &mut counts);
1823 for ((path, _), (count, status)) in counts {
1824 if count > 1 {
1825 report.add_issue(
1826 ValidationIssue::new(
1827 Severity::Error,
1828 IssueKind::RepeatedInTransaction {
1829 path: path.clone(),
1830 count,
1831 },
1832 )
1833 .with_field_path(path)
1834 .with_rule(status)
1835 .with_instance_index(transaction_index),
1836 );
1837 }
1838 }
1839}
1840
1841fn refuse_segments<E: ConditionEvaluator>(
1847 group: &AhbGroupNode,
1848 path: &[(&str, usize)],
1849 scoped_ctx: &EvaluationContext,
1850 evaluator: &E,
1851 ub_definitions: &BTreeMap<String, ConditionExpr>,
1852 instance_index: usize,
1853 report: &mut ValidationReport,
1854) {
1855 let mut seen: Vec<u32> = Vec::new();
1856 for node in &group.fields {
1857 let rule = node.rule;
1858 let (Some(status), Some(value), Some(number)) = (
1859 rule.segment_ahb_status.as_deref(),
1860 node.value,
1861 node.matched_segment_number,
1862 ) else {
1863 continue;
1864 };
1865 let qualifier =
1868 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
1869 if !qualifier
1870 || seen.contains(&number)
1871 || Some(status) == group.ahb_status
1872 || !rule.codes.iter().any(|c| c.value == value)
1873 {
1874 continue;
1875 }
1876 seen.push(number);
1877 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1878 continue;
1879 }
1880 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1881 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
1882 report.add_issue(
1883 ValidationIssue::new(
1884 Severity::Warning,
1885 IssueKind::SegmentNotAllowed {
1886 segment: segment.join("/"),
1887 },
1888 )
1889 .with_field_path(segment.join("/"))
1890 .with_rule(status)
1891 .with_instance_index(instance_index)
1892 .with_position(crate::SegmentPosition {
1893 segment_number: number,
1894 byte_offset: 0,
1895 message_number: 1,
1896 }),
1897 );
1898 }
1899}
1900
1901#[allow(clippy::too_many_arguments)]
1910fn walk_group_instance<E: ConditionEvaluator>(
1911 group: &AhbGroupNode,
1912 path: &[(&str, usize)],
1913 scoped_ctx: &EvaluationContext,
1914 expr_eval: &ConditionExprEvaluator<E>,
1915 evaluator: &E,
1916 ub_definitions: &BTreeMap<String, ConditionExpr>,
1917 tag_migs: &HashMap<String, HashSet<&str>>,
1918 instance_index: usize,
1919 report_segments: bool,
1920 report: &mut ValidationReport,
1921) {
1922 let mut group_refused = false;
1925 if let Some(status) = group.ahb_status {
1926 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1927 group_refused = true;
1928 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1929 let mut issue = ValidationIssue::new(
1930 Severity::Warning,
1931 IssueKind::GroupNotAllowed {
1932 group: group_path.join("/"),
1933 },
1934 )
1935 .with_field_path(group_path.join("/"))
1936 .with_rule(status)
1937 .with_instance_index(instance_index);
1938 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
1939 issue = issue.with_position(crate::SegmentPosition {
1940 segment_number: num,
1941 byte_offset: 0,
1942 message_number: 1,
1943 });
1944 }
1945 report.add_issue(issue);
1946 }
1947 }
1948 if report_segments && !group_refused {
1949 refuse_segments(
1950 group,
1951 path,
1952 scoped_ctx,
1953 evaluator,
1954 ub_definitions,
1955 instance_index,
1956 report,
1957 );
1958 }
1959 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
1963 .fields
1964 .iter()
1965 .filter(|n| {
1966 n.rule.element_index.unwrap_or(0) == 0
1967 && n.rule.component_index.unwrap_or(0) == 0
1968 && !n.rule.codes.is_empty()
1969 })
1970 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
1971 .collect();
1972 let holds = |node: &AhbNode| {
1973 let (Some(codes), Some(elements)) = (
1974 slot_codes.get(&node.rule.mig_number.as_deref()),
1975 node.segment_elements,
1976 ) else {
1977 return true;
1978 };
1979 let qualifier = elements
1980 .first()
1981 .and_then(|e| e.first())
1982 .map(String::as_str)
1983 .unwrap_or_default();
1984 codes.iter().any(|c| c.value == qualifier)
1985 };
1986 for node in &group.fields {
1987 evaluate_node(
1988 node,
1989 scoped_ctx,
1990 expr_eval,
1991 evaluator,
1992 ub_definitions,
1993 tag_migs,
1994 Some(instance_index),
1995 holds(node),
1996 report,
1997 );
1998 }
1999 for child in &group.children {
2000 let mut child_path = path.to_vec();
2001 child_path.push((child.group_id, child.instance_index));
2002 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2003 walk_group_instance(
2004 child,
2005 &child_path,
2006 &child_ctx,
2007 expr_eval,
2008 evaluator,
2009 ub_definitions,
2010 tag_migs,
2011 instance_index,
2012 report_segments,
2013 report,
2014 );
2015 }
2016}
2017
2018fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2020 match expr {
2021 ConditionExpr::Package { id, min, max } => {
2022 out.push((*id, *min, *max));
2023 }
2024 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2025 for e in exprs {
2026 collect_packages(e, out);
2027 }
2028 }
2029 ConditionExpr::Xor(left, right) => {
2030 collect_packages(left, out);
2031 collect_packages(right, out);
2032 }
2033 ConditionExpr::Not(inner) => {
2034 collect_packages(inner, out);
2035 }
2036 ConditionExpr::Ref(_) => {}
2037 }
2038}
2039
2040fn is_optional_segment_status(status: &str) -> bool {
2050 let trimmed = status.trim();
2051 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
2052}
2053
2054fn is_qualifier_field(path: &str) -> bool {
2064 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
2065 matches!(parts.len(), 2 | 3)
2067}
2068
2069#[derive(Default)]
2077struct MigCodeBucket<'a> {
2078 qualifier_position: Option<(usize, usize)>,
2081 qualifier_values: HashSet<&'a str>,
2084 codes: HashMap<(usize, usize), HashSet<&'a str>>,
2087}
2088
2089fn partition_codes_by_mig(
2094 workflow: &AhbWorkflow,
2095) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
2096 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
2097 for field in &workflow.fields {
2098 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2099 continue;
2100 }
2101 let tag = extract_segment_id(&field.segment_path);
2102 let group_key = extract_group_path_key(&field.segment_path);
2103 let mig = field.mig_number.clone();
2104 let el = field.element_index.unwrap_or(0);
2105 let c = field.component_index.unwrap_or(0);
2106
2107 let bucket = out
2108 .entry((group_key, tag))
2109 .or_default()
2110 .entry(mig)
2111 .or_default();
2112
2113 let required: Vec<&str> = field
2120 .codes
2121 .iter()
2122 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2123 .map(|code| code.value.as_str())
2124 .collect();
2125
2126 if !required.is_empty() {
2133 if bucket.qualifier_position.is_none() {
2134 bucket.qualifier_position = Some((el, c));
2135 }
2136 if bucket.qualifier_position == Some((el, c)) {
2137 bucket.qualifier_values.extend(required.iter().copied());
2138 }
2139 }
2140
2141 for v in required {
2142 bucket.codes.entry((el, c)).or_default().insert(v);
2143 }
2144 }
2145 out
2146}
2147
2148fn global_qualifiers_by_tag(
2154 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
2155) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
2156 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
2157 for ((_group, tag), migs) in by_loc {
2158 let tag_entry = out.entry(tag.clone()).or_default();
2159 for bucket in migs.values() {
2160 if let Some(pos) = bucket.qualifier_position {
2161 tag_entry
2162 .entry(pos)
2163 .or_default()
2164 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2165 }
2166 }
2167 }
2168 out
2169}
2170
2171fn match_segment_to_mig<'a, 'b>(
2182 seg: &OwnedSegment,
2183 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
2184) -> Option<&'a MigCodeBucket<'b>> {
2185 let actual_at = |el: usize, c: usize| -> &str {
2186 seg.elements
2187 .get(el)
2188 .and_then(|e| e.get(c))
2189 .map(|s| s.as_str())
2190 .unwrap_or("")
2191 };
2192
2193 let mut best: Option<&MigCodeBucket> = None;
2194 let mut best_matches = 0usize;
2195
2196 for bucket in migs.values() {
2197 let Some((el, c)) = bucket.qualifier_position else {
2198 continue;
2199 };
2200 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2201 continue;
2202 }
2203 let extra_matches = bucket
2204 .codes
2205 .iter()
2206 .filter(|(pos, _)| **pos != (el, c))
2207 .filter(|((e, k), allowed)| {
2208 let v = actual_at(*e, *k);
2209 !v.is_empty() && allowed.contains(v)
2210 })
2211 .count();
2212 if best.is_none() || extra_matches > best_matches {
2213 best = Some(bucket);
2214 best_matches = extra_matches;
2215 }
2216 }
2217 best
2218}
2219
2220fn extract_group_path_key(path: &str) -> String {
2225 let sg_parts: Vec<&str> = path
2226 .split('/')
2227 .take_while(|p| p.starts_with("SG"))
2228 .collect();
2229 sg_parts.join("/")
2230}
2231
2232fn extract_segment_id(path: &str) -> String {
2234 for part in path.split('/') {
2235 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2237 continue;
2238 }
2239 if part.len() >= 3
2241 && part
2242 .chars()
2243 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2244 {
2245 return part.to_string();
2246 }
2247 }
2248 path.split('/').next_back().unwrap_or(path).to_string()
2250}
2251
2252pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2264 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2266 if unh_count > 1 {
2267 return Some(ValidationIssue::new(
2268 Severity::Error,
2269 IssueKind::UntCountNotVerifiable { unh_count },
2270 ));
2271 }
2272
2273 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2275 let declared: usize = unt.get_element(0).parse().ok()?;
2276
2277 let actual = segments
2279 .iter()
2280 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2281 .count();
2282
2283 if declared != actual {
2284 Some(
2285 ValidationIssue::new(
2286 Severity::Error,
2287 IssueKind::UntSegmentCountMismatch { declared, actual },
2288 )
2289 .with_field_path("UNT/0074")
2290 .with_expected(actual.to_string())
2291 .with_actual(declared.to_string()),
2292 )
2293 } else {
2294 None
2295 }
2296}
2297
2298#[cfg(test)]
2299mod tests {
2300
2301 #[test]
2302 fn a_numeric_length_counts_digits_only() {
2303 assert_eq!(value_length("n..15", "-12.50"), 4);
2304 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
2305 assert_eq!(value_length("n5", "55001"), 5);
2306 assert_eq!(value_length("an..3", "Z.1"), 3);
2307 assert_eq!(value_length("an..35", "Müller"), 6);
2308 }
2309 use super::super::codes::ErrorCodes;
2310 use super::super::issue::ValidationCategory;
2311 use super::*;
2312 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2313 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2314 use std::collections::HashMap;
2315
2316 fn narrate(issue: &ValidationIssue) -> String {
2319 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2320 }
2321
2322 struct MockEvaluator {
2324 results: HashMap<u32, CR>,
2325 }
2326
2327 impl MockEvaluator {
2328 fn new(results: Vec<(u32, CR)>) -> Self {
2329 Self {
2330 results: results.into_iter().collect(),
2331 }
2332 }
2333
2334 fn all_true(ids: &[u32]) -> Self {
2335 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2336 }
2337 }
2338
2339 impl ConditionEvaluator for MockEvaluator {
2340 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2341 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2342 }
2343 fn is_external(&self, _condition: u32) -> bool {
2344 false
2345 }
2346 fn message_type(&self) -> &str {
2347 "UTILMD"
2348 }
2349 fn format_version(&self) -> &str {
2350 "FV2510"
2351 }
2352 }
2353
2354 #[test]
2357 fn a_present_code_is_judged_by_its_own_status() {
2358 let code = |value: &str, status: &str| AhbCodeRule {
2360 value: value.to_string(),
2361 description: String::new(),
2362 ahb_status: status.to_string(),
2363 };
2364 let field = AhbFieldRule {
2365 segment_path: "SG4/STS/C556/9013".to_string(),
2366 ahb_status: "X [192]".to_string(),
2367 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2368 ..Default::default()
2369 };
2370 assert_eq!(governing_status(&field, Some("E03")), "X");
2371 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2372 assert_eq!(governing_status(&field, None), "X [192]");
2373 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2374
2375 let own = AhbFieldRule {
2377 ahb_status: "X [931]".to_string(),
2378 codes: vec![code("303", "X")],
2379 ..Default::default()
2380 };
2381 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2382 }
2383
2384 #[test]
2385 fn test_extract_segment_id_simple() {
2386 assert_eq!(extract_segment_id("NAD"), "NAD");
2387 }
2388
2389 #[test]
2390 fn test_extract_segment_id_with_sg_prefix() {
2391 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
2392 }
2393
2394 #[test]
2395 fn test_extract_segment_id_nested_sg() {
2396 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
2397 }
2398
2399 #[test]
2402 fn test_validate_missing_mandatory_field() {
2403 let evaluator = MockEvaluator::all_true(&[182, 152]);
2404 let validator = EdifactValidator::new(evaluator);
2405 let external = NoOpExternalProvider;
2406
2407 let workflow = AhbWorkflow {
2408 pruefidentifikator: "11001".to_string(),
2409 description: "Test".to_string(),
2410 communication_direction: None,
2411 fields: vec![AhbFieldRule {
2412 segment_path: "SG2/NAD/C082/3039".to_string(),
2413 name: "MP-ID des MSB".to_string(),
2414 ahb_status: "Muss [182] ∧ [152]".to_string(),
2415 codes: vec![],
2416 parent_group_ahb_status: None,
2417 segment_ahb_status: None,
2418 ..Default::default()
2419 }],
2420 ub_definitions: BTreeMap::new(),
2421 };
2422
2423 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2425
2426 assert!(!report.is_valid());
2428 let errors: Vec<_> = report.errors().collect();
2429 assert_eq!(errors.len(), 1);
2430 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2431 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
2432 }
2433
2434 #[test]
2435 fn test_validate_condition_false_no_error() {
2436 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
2438 let validator = EdifactValidator::new(evaluator);
2439 let external = NoOpExternalProvider;
2440
2441 let workflow = AhbWorkflow {
2442 pruefidentifikator: "11001".to_string(),
2443 description: "Test".to_string(),
2444 communication_direction: None,
2445 fields: vec![AhbFieldRule {
2446 segment_path: "NAD".to_string(),
2447 name: "Partnerrolle".to_string(),
2448 ahb_status: "Muss [182] ∧ [152]".to_string(),
2449 codes: vec![],
2450 parent_group_ahb_status: None,
2451 segment_ahb_status: None,
2452 ..Default::default()
2453 }],
2454 ub_definitions: BTreeMap::new(),
2455 };
2456
2457 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2458
2459 assert!(report.is_valid());
2461 }
2462
2463 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
2465 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
2466 let workflow = AhbWorkflow {
2467 pruefidentifikator: "11001".to_string(),
2468 description: "Test".to_string(),
2469 communication_direction: None,
2470 fields: vec![AhbFieldRule {
2471 segment_path: "SG2/NAD/C082/3039".to_string(),
2472 name: "MP-ID".to_string(),
2473 ahb_status: status.to_string(),
2474 ..Default::default()
2475 }],
2476 ub_definitions: BTreeMap::new(),
2477 };
2478 validator.validate(
2479 &[],
2480 &workflow,
2481 &NoOpExternalProvider,
2482 ValidationLevel::Conditions,
2483 )
2484 }
2485
2486 #[test]
2487 fn a_later_soll_line_alone_does_not_require_the_field() {
2488 let report = validate_absent_field(
2490 "Muss [2119] \r\nSoll [130]",
2491 vec![(2119, CR::False), (130, CR::True)],
2492 );
2493 assert!(
2494 report.is_valid(),
2495 "{:?}",
2496 report.errors().collect::<Vec<_>>()
2497 );
2498 }
2499
2500 #[test]
2501 fn a_later_muss_line_requires_the_field() {
2502 let report = validate_absent_field(
2504 "Soll [47]\r\nMuss [46]",
2505 vec![(47, CR::False), (46, CR::True)],
2506 );
2507 let errors: Vec<_> = report.errors().collect();
2508 assert_eq!(errors.len(), 1);
2509 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2510 }
2511
2512 #[test]
2513 fn a_weaker_unknown_line_does_not_stop_a_muss() {
2514 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
2516 let errors: Vec<_> = report.errors().collect();
2517 assert_eq!(errors.len(), 1);
2518 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2519 }
2520
2521 #[test]
2522 fn a_stricter_unknown_line_leaves_the_status_undecided() {
2523 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
2524 assert!(report.is_valid());
2525 let infos: Vec<_> = report.infos().collect();
2526 assert_eq!(infos.len(), 1, "{infos:?}");
2527 match &infos[0].kind {
2528 IssueKind::ConditionUnknown { unresolved, .. } => {
2529 assert_eq!(unresolved.missing, vec![2119]);
2530 }
2531 other => panic!("expected ConditionUnknown, got {other:?}"),
2532 }
2533 }
2534
2535 #[test]
2536 fn test_validate_condition_unknown_adds_info() {
2537 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
2539 let validator = EdifactValidator::new(evaluator);
2541 let external = NoOpExternalProvider;
2542
2543 let workflow = AhbWorkflow {
2544 pruefidentifikator: "11001".to_string(),
2545 description: "Test".to_string(),
2546 communication_direction: None,
2547 fields: vec![AhbFieldRule {
2548 segment_path: "NAD".to_string(),
2549 name: "Partnerrolle".to_string(),
2550 ahb_status: "Muss [182] ∧ [152]".to_string(),
2551 codes: vec![],
2552 parent_group_ahb_status: None,
2553 segment_ahb_status: None,
2554 ..Default::default()
2555 }],
2556 ub_definitions: BTreeMap::new(),
2557 };
2558
2559 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2560
2561 assert!(report.is_valid());
2563 let infos: Vec<_> = report.infos().collect();
2564 assert_eq!(infos.len(), 1);
2565 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
2566 }
2567
2568 #[test]
2569 fn test_validate_structure_level_skips_conditions() {
2570 let evaluator = MockEvaluator::all_true(&[182, 152]);
2571 let validator = EdifactValidator::new(evaluator);
2572 let external = NoOpExternalProvider;
2573
2574 let workflow = AhbWorkflow {
2575 pruefidentifikator: "11001".to_string(),
2576 description: "Test".to_string(),
2577 communication_direction: None,
2578 fields: vec![AhbFieldRule {
2579 segment_path: "NAD".to_string(),
2580 name: "Partnerrolle".to_string(),
2581 ahb_status: "Muss [182] ∧ [152]".to_string(),
2582 codes: vec![],
2583 parent_group_ahb_status: None,
2584 segment_ahb_status: None,
2585 ..Default::default()
2586 }],
2587 ub_definitions: BTreeMap::new(),
2588 };
2589
2590 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
2592
2593 assert!(report.is_valid());
2595 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
2596 }
2597
2598 #[test]
2599 fn test_validate_empty_workflow_no_condition_errors() {
2600 let evaluator = MockEvaluator::all_true(&[]);
2601 let validator = EdifactValidator::new(evaluator);
2602 let external = NoOpExternalProvider;
2603
2604 let empty_workflow = AhbWorkflow {
2605 pruefidentifikator: String::new(),
2606 description: String::new(),
2607 communication_direction: None,
2608 fields: vec![],
2609 ub_definitions: BTreeMap::new(),
2610 };
2611
2612 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
2613
2614 assert!(report.is_valid());
2615 }
2616
2617 #[test]
2618 fn test_validate_bare_muss_always_required() {
2619 let evaluator = MockEvaluator::new(vec![]);
2620 let validator = EdifactValidator::new(evaluator);
2621 let external = NoOpExternalProvider;
2622
2623 let workflow = AhbWorkflow {
2624 pruefidentifikator: "55001".to_string(),
2625 description: "Test".to_string(),
2626 communication_direction: Some("NB an LF".to_string()),
2627 fields: vec![AhbFieldRule {
2628 segment_path: "SG2/NAD/3035".to_string(),
2629 name: "Partnerrolle".to_string(),
2630 ahb_status: "Muss".to_string(), codes: vec![],
2632 parent_group_ahb_status: None,
2633 segment_ahb_status: None,
2634 ..Default::default()
2635 }],
2636 ub_definitions: BTreeMap::new(),
2637 };
2638
2639 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2640
2641 assert!(!report.is_valid());
2643 assert_eq!(report.error_count(), 1);
2644 }
2645
2646 #[test]
2647 fn test_validate_x_status_is_mandatory() {
2648 let evaluator = MockEvaluator::new(vec![]);
2649 let validator = EdifactValidator::new(evaluator);
2650 let external = NoOpExternalProvider;
2651
2652 let workflow = AhbWorkflow {
2653 pruefidentifikator: "55001".to_string(),
2654 description: "Test".to_string(),
2655 communication_direction: None,
2656 fields: vec![AhbFieldRule {
2657 segment_path: "DTM".to_string(),
2658 name: "Datum".to_string(),
2659 ahb_status: "X".to_string(),
2660 codes: vec![],
2661 parent_group_ahb_status: None,
2662 segment_ahb_status: None,
2663 ..Default::default()
2664 }],
2665 ub_definitions: BTreeMap::new(),
2666 };
2667
2668 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2669
2670 assert!(!report.is_valid());
2671 let errors: Vec<_> = report.errors().collect();
2672 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2673 }
2674
2675 #[test]
2676 fn test_validate_soll_not_mandatory() {
2677 let evaluator = MockEvaluator::new(vec![]);
2678 let validator = EdifactValidator::new(evaluator);
2679 let external = NoOpExternalProvider;
2680
2681 let workflow = AhbWorkflow {
2682 pruefidentifikator: "55001".to_string(),
2683 description: "Test".to_string(),
2684 communication_direction: None,
2685 fields: vec![AhbFieldRule {
2686 segment_path: "DTM".to_string(),
2687 name: "Datum".to_string(),
2688 ahb_status: "Soll".to_string(),
2689 codes: vec![],
2690 parent_group_ahb_status: None,
2691 segment_ahb_status: None,
2692 ..Default::default()
2693 }],
2694 ub_definitions: BTreeMap::new(),
2695 };
2696
2697 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2698
2699 assert!(report.is_valid());
2701 }
2702
2703 #[test]
2704 fn test_report_includes_metadata() {
2705 let evaluator = MockEvaluator::new(vec![]);
2706 let validator = EdifactValidator::new(evaluator);
2707 let external = NoOpExternalProvider;
2708
2709 let workflow = AhbWorkflow {
2710 pruefidentifikator: "55001".to_string(),
2711 description: String::new(),
2712 communication_direction: None,
2713 fields: vec![],
2714 ub_definitions: BTreeMap::new(),
2715 };
2716
2717 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
2718
2719 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
2720 assert_eq!(report.level, ValidationLevel::Full);
2721 assert_eq!(report.message_type, "UTILMD");
2722 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
2723 }
2724
2725 #[test]
2726 fn test_validate_with_navigator_returns_report() {
2727 let evaluator = MockEvaluator::all_true(&[]);
2728 let validator = EdifactValidator::new(evaluator);
2729 let external = NoOpExternalProvider;
2730 let nav = crate::eval::NoOpGroupNavigator;
2731
2732 let workflow = AhbWorkflow {
2733 pruefidentifikator: "55001".to_string(),
2734 description: "Test".to_string(),
2735 communication_direction: None,
2736 fields: vec![],
2737 ub_definitions: BTreeMap::new(),
2738 };
2739
2740 let report = validator.validate_with_navigator(
2741 &[],
2742 &workflow,
2743 &external,
2744 ValidationLevel::Full,
2745 &nav,
2746 );
2747 assert!(report.is_valid());
2748 }
2749
2750 #[test]
2751 fn test_code_validation_composite_paths_valid_codes() {
2752 let evaluator = MockEvaluator::new(vec![]);
2756 let validator = EdifactValidator::new(evaluator);
2757 let external = NoOpExternalProvider;
2758
2759 let unh_segment = OwnedSegment {
2760 id: "UNH".to_string(),
2761 elements: vec![
2762 vec!["ALEXANDE951842".to_string()],
2763 vec![
2764 "UTILMD".to_string(),
2765 "D".to_string(),
2766 "11A".to_string(),
2767 "UN".to_string(),
2768 "S2.1".to_string(),
2769 ],
2770 ],
2771 segment_number: 1,
2772 };
2773
2774 let workflow = AhbWorkflow {
2775 pruefidentifikator: "55001".to_string(),
2776 description: "Test".to_string(),
2777 communication_direction: None,
2778 fields: vec![
2779 AhbFieldRule {
2780 segment_path: "UNH/S009/0065".to_string(),
2781 name: "Nachrichtentyp".to_string(),
2782 ahb_status: "X".to_string(),
2783 codes: vec![AhbCodeRule {
2784 value: "UTILMD".to_string(),
2785 description: "Stammdaten".to_string(),
2786 ahb_status: "X".to_string(),
2787 }],
2788 parent_group_ahb_status: None,
2789 segment_ahb_status: None,
2790 element_index: Some(1),
2791 component_index: Some(0),
2792 ..Default::default()
2793 },
2794 AhbFieldRule {
2795 segment_path: "UNH/S009/0052".to_string(),
2796 name: "Version".to_string(),
2797 ahb_status: "X".to_string(),
2798 codes: vec![AhbCodeRule {
2799 value: "D".to_string(),
2800 description: "Draft".to_string(),
2801 ahb_status: "X".to_string(),
2802 }],
2803 parent_group_ahb_status: None,
2804 segment_ahb_status: None,
2805 element_index: Some(1),
2806 component_index: Some(1),
2807 ..Default::default()
2808 },
2809 ],
2810 ub_definitions: BTreeMap::new(),
2811 };
2812
2813 let report = validator.validate(
2814 &[unh_segment],
2815 &workflow,
2816 &external,
2817 ValidationLevel::Conditions,
2818 );
2819
2820 let code_errors: Vec<_> = report
2821 .by_category(ValidationCategory::Code)
2822 .filter(|i| i.severity == Severity::Error)
2823 .collect();
2824 assert!(
2825 code_errors.is_empty(),
2826 "Expected no code errors when composite values match allowed codes, got: {:?}",
2827 code_errors
2828 );
2829 }
2830
2831 #[test]
2832 fn test_code_validation_partitions_by_mig_number() {
2833 let evaluator = MockEvaluator::new(vec![]);
2837 let validator = EdifactValidator::new(evaluator);
2838 let external = NoOpExternalProvider;
2839
2840 let sts_7 = OwnedSegment {
2841 id: "STS".to_string(),
2842 elements: vec![
2843 vec!["7".to_string()],
2844 vec![String::new()],
2845 vec!["GH02".to_string()],
2846 vec!["ZW4".to_string()],
2847 ],
2848 segment_number: 1,
2849 };
2850 let sts_e01 = OwnedSegment {
2851 id: "STS".to_string(),
2852 elements: vec![
2853 vec!["E01".to_string()],
2854 vec![String::new()],
2855 vec!["A99".to_string(), "E_0614".to_string()],
2856 ],
2857 segment_number: 2,
2858 };
2859
2860 let workflow = AhbWorkflow {
2861 pruefidentifikator: "55018".to_string(),
2862 description: "Test".to_string(),
2863 communication_direction: None,
2864 fields: vec![
2865 AhbFieldRule {
2867 segment_path: "SG4/STS/C601/9015".to_string(),
2868 name: "Statuskategorie".to_string(),
2869 ahb_status: "X".to_string(),
2870 codes: vec![AhbCodeRule {
2871 value: "7".to_string(),
2872 description: "Transaktionsgrund".to_string(),
2873 ahb_status: "X".to_string(),
2874 }],
2875 parent_group_ahb_status: None,
2876 segment_ahb_status: None,
2877 element_index: Some(0),
2878 component_index: Some(0),
2879 mig_number: Some("00035".to_string()),
2880 format: None,
2881 },
2882 AhbFieldRule {
2883 segment_path: "SG4/STS/C556/9013".to_string(),
2884 name: "Statusanlaß".to_string(),
2885 ahb_status: "X".to_string(),
2886 codes: vec![AhbCodeRule {
2887 value: "E03".to_string(),
2888 description: "Transaktionsgrund".to_string(),
2889 ahb_status: "X".to_string(),
2890 }],
2891 parent_group_ahb_status: None,
2892 segment_ahb_status: None,
2893 element_index: Some(2),
2894 component_index: Some(0),
2895 mig_number: Some("00035".to_string()),
2896 format: None,
2897 },
2898 AhbFieldRule {
2900 segment_path: "SG4/STS/C601/9015".to_string(),
2901 name: "Statuskategorie".to_string(),
2902 ahb_status: "X".to_string(),
2903 codes: vec![AhbCodeRule {
2904 value: "E01".to_string(),
2905 description: "Antwort".to_string(),
2906 ahb_status: "X".to_string(),
2907 }],
2908 parent_group_ahb_status: None,
2909 segment_ahb_status: None,
2910 element_index: Some(0),
2911 component_index: Some(0),
2912 mig_number: Some("00036".to_string()),
2913 format: None,
2914 },
2915 ],
2916 ub_definitions: BTreeMap::new(),
2917 };
2918
2919 let report = validator.validate(
2920 &[sts_7, sts_e01],
2921 &workflow,
2922 &external,
2923 ValidationLevel::Conditions,
2924 );
2925
2926 let code_errors: Vec<_> = report
2927 .by_category(ValidationCategory::Code)
2928 .filter(|i| {
2929 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2930 })
2931 .collect();
2932 assert_eq!(
2933 code_errors.len(),
2934 1,
2935 "Expected one COD002 (for GH02 only), got: {:?}",
2936 code_errors
2937 );
2938 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2939 }
2940
2941 #[test]
2942 fn test_code_validation_composite_paths_detects_invalid_code() {
2943 let evaluator = MockEvaluator::new(vec![]);
2946 let validator = EdifactValidator::new(evaluator);
2947 let external = NoOpExternalProvider;
2948
2949 let sts_segment = OwnedSegment {
2950 id: "STS".to_string(),
2951 elements: vec![
2952 vec!["7".to_string()],
2953 vec![String::new()],
2954 vec!["GH02".to_string()],
2955 vec!["ZW4".to_string()],
2956 ],
2957 segment_number: 1,
2958 };
2959
2960 let workflow = AhbWorkflow {
2961 pruefidentifikator: "55018".to_string(),
2962 description: "Test".to_string(),
2963 communication_direction: None,
2964 fields: vec![AhbFieldRule {
2965 segment_path: "SG4/STS/C556/9013".to_string(),
2966 name: "Statusanlaß".to_string(),
2967 ahb_status: "X".to_string(),
2968 codes: vec![AhbCodeRule {
2969 value: "E03".to_string(),
2970 description: "Transaktionsgrund".to_string(),
2971 ahb_status: "X".to_string(),
2972 }],
2973 parent_group_ahb_status: None,
2974 segment_ahb_status: None,
2975 element_index: Some(2),
2976 component_index: Some(0),
2977 ..Default::default()
2978 }],
2979 ub_definitions: BTreeMap::new(),
2980 };
2981
2982 let report = validator.validate(
2983 &[sts_segment],
2984 &workflow,
2985 &external,
2986 ValidationLevel::Conditions,
2987 );
2988
2989 let code_errors: Vec<_> = report
2990 .by_category(ValidationCategory::Code)
2991 .filter(|i| {
2992 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2993 })
2994 .collect();
2995 assert_eq!(
2996 code_errors.len(),
2997 1,
2998 "Expected COD002 for GH02, got: {:?}",
2999 code_errors
3000 );
3001 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3002 }
3003
3004 #[test]
3005 fn test_cross_field_code_validation_valid_qualifiers() {
3006 let evaluator = MockEvaluator::new(vec![]);
3009 let validator = EdifactValidator::new(evaluator);
3010 let external = NoOpExternalProvider;
3011
3012 let nad_ms = OwnedSegment {
3013 id: "NAD".to_string(),
3014 elements: vec![vec!["MS".to_string()]],
3015 segment_number: 4,
3016 };
3017 let nad_mr = OwnedSegment {
3018 id: "NAD".to_string(),
3019 elements: vec![vec!["MR".to_string()]],
3020 segment_number: 5,
3021 };
3022
3023 let workflow = AhbWorkflow {
3024 pruefidentifikator: "55001".to_string(),
3025 description: "Test".to_string(),
3026 communication_direction: None,
3027 fields: vec![
3028 AhbFieldRule {
3029 segment_path: "SG2/NAD/3035".to_string(),
3030 name: "Absender".to_string(),
3031 ahb_status: "X".to_string(),
3032 codes: vec![AhbCodeRule {
3033 value: "MS".to_string(),
3034 description: "Absender".to_string(),
3035 ahb_status: "X".to_string(),
3036 }],
3037 parent_group_ahb_status: None,
3038 segment_ahb_status: None,
3039 ..Default::default()
3040 },
3041 AhbFieldRule {
3042 segment_path: "SG2/NAD/3035".to_string(),
3043 name: "Empfaenger".to_string(),
3044 ahb_status: "X".to_string(),
3045 codes: vec![AhbCodeRule {
3046 value: "MR".to_string(),
3047 description: "Empfaenger".to_string(),
3048 ahb_status: "X".to_string(),
3049 }],
3050 parent_group_ahb_status: None,
3051 segment_ahb_status: None,
3052 ..Default::default()
3053 },
3054 ],
3055 ub_definitions: BTreeMap::new(),
3056 };
3057
3058 let report = validator.validate(
3059 &[nad_ms, nad_mr],
3060 &workflow,
3061 &external,
3062 ValidationLevel::Conditions,
3063 );
3064
3065 let code_errors: Vec<_> = report
3066 .by_category(ValidationCategory::Code)
3067 .filter(|i| i.severity == Severity::Error)
3068 .collect();
3069 assert!(
3070 code_errors.is_empty(),
3071 "Expected no code errors for valid qualifiers, got: {:?}",
3072 code_errors
3073 );
3074 }
3075
3076 #[test]
3077 fn test_cross_field_code_validation_catches_invalid_qualifier() {
3078 let evaluator = MockEvaluator::new(vec![]);
3080 let validator = EdifactValidator::new(evaluator);
3081 let external = NoOpExternalProvider;
3082
3083 let nad_ms = OwnedSegment {
3084 id: "NAD".to_string(),
3085 elements: vec![vec!["MS".to_string()]],
3086 segment_number: 4,
3087 };
3088 let nad_mt = OwnedSegment {
3089 id: "NAD".to_string(),
3090 elements: vec![vec!["MT".to_string()]], segment_number: 5,
3092 };
3093
3094 let workflow = AhbWorkflow {
3095 pruefidentifikator: "55001".to_string(),
3096 description: "Test".to_string(),
3097 communication_direction: None,
3098 fields: vec![
3099 AhbFieldRule {
3100 segment_path: "SG2/NAD/3035".to_string(),
3101 name: "Absender".to_string(),
3102 ahb_status: "X".to_string(),
3103 codes: vec![AhbCodeRule {
3104 value: "MS".to_string(),
3105 description: "Absender".to_string(),
3106 ahb_status: "X".to_string(),
3107 }],
3108 parent_group_ahb_status: None,
3109 segment_ahb_status: None,
3110 ..Default::default()
3111 },
3112 AhbFieldRule {
3113 segment_path: "SG2/NAD/3035".to_string(),
3114 name: "Empfaenger".to_string(),
3115 ahb_status: "X".to_string(),
3116 codes: vec![AhbCodeRule {
3117 value: "MR".to_string(),
3118 description: "Empfaenger".to_string(),
3119 ahb_status: "X".to_string(),
3120 }],
3121 parent_group_ahb_status: None,
3122 segment_ahb_status: None,
3123 ..Default::default()
3124 },
3125 ],
3126 ub_definitions: BTreeMap::new(),
3127 };
3128
3129 let report = validator.validate(
3130 &[nad_ms, nad_mt],
3131 &workflow,
3132 &external,
3133 ValidationLevel::Conditions,
3134 );
3135
3136 let code_errors: Vec<_> = report
3137 .by_category(ValidationCategory::Code)
3138 .filter(|i| i.severity == Severity::Error)
3139 .collect();
3140 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
3141 assert!(narrate(code_errors[0]).contains("MT"));
3142 assert!(narrate(code_errors[0]).contains("MR"));
3143 assert!(narrate(code_errors[0]).contains("MS"));
3144 }
3145
3146 #[test]
3147 fn test_cross_field_code_validation_unions_across_groups() {
3148 let evaluator = MockEvaluator::new(vec![]);
3152 let validator = EdifactValidator::new(evaluator);
3153 let external = NoOpExternalProvider;
3154
3155 let segments = vec![
3156 OwnedSegment {
3157 id: "NAD".to_string(),
3158 elements: vec![vec!["MS".to_string()]],
3159 segment_number: 3,
3160 },
3161 OwnedSegment {
3162 id: "NAD".to_string(),
3163 elements: vec![vec!["MR".to_string()]],
3164 segment_number: 4,
3165 },
3166 OwnedSegment {
3167 id: "NAD".to_string(),
3168 elements: vec![vec!["Z04".to_string()]],
3169 segment_number: 20,
3170 },
3171 OwnedSegment {
3172 id: "NAD".to_string(),
3173 elements: vec![vec!["Z09".to_string()]],
3174 segment_number: 21,
3175 },
3176 OwnedSegment {
3177 id: "NAD".to_string(),
3178 elements: vec![vec!["MT".to_string()]], segment_number: 22,
3180 },
3181 ];
3182
3183 let workflow = AhbWorkflow {
3184 pruefidentifikator: "55001".to_string(),
3185 description: "Test".to_string(),
3186 communication_direction: None,
3187 fields: vec![
3188 AhbFieldRule {
3189 segment_path: "SG2/NAD/3035".to_string(),
3190 name: "Absender".to_string(),
3191 ahb_status: "X".to_string(),
3192 codes: vec![AhbCodeRule {
3193 value: "MS".to_string(),
3194 description: "Absender".to_string(),
3195 ahb_status: "X".to_string(),
3196 }],
3197 parent_group_ahb_status: None,
3198 segment_ahb_status: None,
3199 ..Default::default()
3200 },
3201 AhbFieldRule {
3202 segment_path: "SG2/NAD/3035".to_string(),
3203 name: "Empfaenger".to_string(),
3204 ahb_status: "X".to_string(),
3205 codes: vec![AhbCodeRule {
3206 value: "MR".to_string(),
3207 description: "Empfaenger".to_string(),
3208 ahb_status: "X".to_string(),
3209 }],
3210 parent_group_ahb_status: None,
3211 segment_ahb_status: None,
3212 ..Default::default()
3213 },
3214 AhbFieldRule {
3215 segment_path: "SG4/SG12/NAD/3035".to_string(),
3216 name: "Anschlussnutzer".to_string(),
3217 ahb_status: "X".to_string(),
3218 codes: vec![AhbCodeRule {
3219 value: "Z04".to_string(),
3220 description: "Anschlussnutzer".to_string(),
3221 ahb_status: "X".to_string(),
3222 }],
3223 parent_group_ahb_status: None,
3224 segment_ahb_status: None,
3225 ..Default::default()
3226 },
3227 AhbFieldRule {
3228 segment_path: "SG4/SG12/NAD/3035".to_string(),
3229 name: "Korrespondenzanschrift".to_string(),
3230 ahb_status: "X".to_string(),
3231 codes: vec![AhbCodeRule {
3232 value: "Z09".to_string(),
3233 description: "Korrespondenzanschrift".to_string(),
3234 ahb_status: "X".to_string(),
3235 }],
3236 parent_group_ahb_status: None,
3237 segment_ahb_status: None,
3238 ..Default::default()
3239 },
3240 ],
3241 ub_definitions: BTreeMap::new(),
3242 };
3243
3244 let report =
3245 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3246
3247 let code_errors: Vec<_> = report
3248 .by_category(ValidationCategory::Code)
3249 .filter(|i| i.severity == Severity::Error)
3250 .collect();
3251 assert_eq!(
3252 code_errors.len(),
3253 1,
3254 "Expected exactly one COD002 error for MT, got: {:?}",
3255 code_errors
3256 );
3257 assert!(narrate(code_errors[0]).contains("MT"));
3258 }
3259
3260 #[test]
3261 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3262 let evaluator = MockEvaluator::new(vec![]);
3271 let validator = EdifactValidator::new(evaluator);
3272 let external = NoOpExternalProvider;
3273
3274 let qty_67 = OwnedSegment {
3275 id: "QTY".to_string(),
3276 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
3277 segment_number: 10,
3278 };
3279
3280 let workflow = AhbWorkflow {
3281 pruefidentifikator: "13025".to_string(),
3282 description: "Test".to_string(),
3283 communication_direction: None,
3284 fields: vec![AhbFieldRule {
3285 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
3286 name: "Menge, Qualifier".to_string(),
3287 ahb_status: "X".to_string(),
3288 codes: vec![
3289 AhbCodeRule {
3290 value: "220".to_string(),
3291 description: "Wahrer Wert".to_string(),
3292 ahb_status: "X".to_string(),
3293 },
3294 AhbCodeRule {
3295 value: "67".to_string(),
3296 description: "Ersatzwert".to_string(),
3297 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
3298 },
3299 AhbCodeRule {
3300 value: "Z18".to_string(),
3301 description: "Vorläufiger Wert".to_string(),
3302 ahb_status: "X [35]".to_string(),
3303 },
3304 ],
3305 parent_group_ahb_status: None,
3306 segment_ahb_status: None,
3307 element_index: Some(0),
3308 component_index: Some(0),
3309 ..Default::default()
3310 }],
3311 ub_definitions: BTreeMap::new(),
3312 };
3313
3314 let report =
3315 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3316
3317 let code_errors: Vec<_> = report
3318 .by_category(ValidationCategory::Code)
3319 .filter(|i| i.severity == Severity::Error)
3320 .collect();
3321 assert!(
3322 code_errors.is_empty(),
3323 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3324 code_errors
3325 );
3326 }
3327
3328 #[test]
3329 fn test_is_qualifier_field_simple_paths() {
3330 assert!(is_qualifier_field("NAD/3035"));
3331 assert!(is_qualifier_field("SG2/NAD/3035"));
3332 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3333 assert!(is_qualifier_field("LOC/3227"));
3334 }
3335
3336 #[test]
3337 fn test_is_qualifier_field_composite_paths() {
3338 assert!(is_qualifier_field("UNH/S009/0065"));
3342 assert!(is_qualifier_field("NAD/C082/3039"));
3343 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3344 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3345 }
3346
3347 #[test]
3348 fn test_is_qualifier_field_bare_segment() {
3349 assert!(!is_qualifier_field("NAD"));
3350 assert!(!is_qualifier_field("SG2/NAD"));
3351 }
3352
3353 #[test]
3354 fn test_is_qualifier_field_rejects_deep_paths() {
3355 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3357 }
3358
3359 #[test]
3360 fn test_missing_qualifier_instance_is_detected() {
3361 let evaluator = MockEvaluator::new(vec![]);
3364 let validator = EdifactValidator::new(evaluator);
3365 let external = NoOpExternalProvider;
3366
3367 let nad_ms = OwnedSegment {
3368 id: "NAD".to_string(),
3369 elements: vec![vec!["MS".to_string()]],
3370 segment_number: 3,
3371 };
3372
3373 let workflow = AhbWorkflow {
3374 pruefidentifikator: "55001".to_string(),
3375 description: "Test".to_string(),
3376 communication_direction: None,
3377 fields: vec![
3378 AhbFieldRule {
3379 segment_path: "SG2/NAD/3035".to_string(),
3380 name: "Absender".to_string(),
3381 ahb_status: "X".to_string(),
3382 codes: vec![AhbCodeRule {
3383 value: "MS".to_string(),
3384 description: "Absender".to_string(),
3385 ahb_status: "X".to_string(),
3386 }],
3387 parent_group_ahb_status: None,
3388 segment_ahb_status: None,
3389 ..Default::default()
3390 },
3391 AhbFieldRule {
3392 segment_path: "SG2/NAD/3035".to_string(),
3393 name: "Empfaenger".to_string(),
3394 ahb_status: "Muss".to_string(),
3395 codes: vec![AhbCodeRule {
3396 value: "MR".to_string(),
3397 description: "Empfaenger".to_string(),
3398 ahb_status: "X".to_string(),
3399 }],
3400 parent_group_ahb_status: None,
3401 segment_ahb_status: None,
3402 ..Default::default()
3403 },
3404 ],
3405 ub_definitions: BTreeMap::new(),
3406 };
3407
3408 let report =
3409 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
3410
3411 let ahb_errors: Vec<_> = report
3412 .by_category(ValidationCategory::Ahb)
3413 .filter(|i| i.severity == Severity::Error)
3414 .collect();
3415 assert_eq!(
3416 ahb_errors.len(),
3417 1,
3418 "Expected AHB001 for missing NAD+MR, got: {:?}",
3419 ahb_errors
3420 );
3421 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3422 }
3423
3424 #[test]
3425 fn test_present_qualifier_instance_no_error() {
3426 let evaluator = MockEvaluator::new(vec![]);
3428 let validator = EdifactValidator::new(evaluator);
3429 let external = NoOpExternalProvider;
3430
3431 let segments = vec![
3432 OwnedSegment {
3433 id: "NAD".to_string(),
3434 elements: vec![vec!["MS".to_string()]],
3435 segment_number: 3,
3436 },
3437 OwnedSegment {
3438 id: "NAD".to_string(),
3439 elements: vec![vec!["MR".to_string()]],
3440 segment_number: 4,
3441 },
3442 ];
3443
3444 let workflow = AhbWorkflow {
3445 pruefidentifikator: "55001".to_string(),
3446 description: "Test".to_string(),
3447 communication_direction: None,
3448 fields: vec![
3449 AhbFieldRule {
3450 segment_path: "SG2/NAD/3035".to_string(),
3451 name: "Absender".to_string(),
3452 ahb_status: "Muss".to_string(),
3453 codes: vec![AhbCodeRule {
3454 value: "MS".to_string(),
3455 description: "Absender".to_string(),
3456 ahb_status: "X".to_string(),
3457 }],
3458 parent_group_ahb_status: None,
3459 segment_ahb_status: None,
3460 ..Default::default()
3461 },
3462 AhbFieldRule {
3463 segment_path: "SG2/NAD/3035".to_string(),
3464 name: "Empfaenger".to_string(),
3465 ahb_status: "Muss".to_string(),
3466 codes: vec![AhbCodeRule {
3467 value: "MR".to_string(),
3468 description: "Empfaenger".to_string(),
3469 ahb_status: "X".to_string(),
3470 }],
3471 parent_group_ahb_status: None,
3472 segment_ahb_status: None,
3473 ..Default::default()
3474 },
3475 ],
3476 ub_definitions: BTreeMap::new(),
3477 };
3478
3479 let report =
3480 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3481
3482 let ahb_errors: Vec<_> = report
3483 .by_category(ValidationCategory::Ahb)
3484 .filter(|i| i.severity == Severity::Error)
3485 .collect();
3486 assert!(
3487 ahb_errors.is_empty(),
3488 "Expected no AHB001 errors, got: {:?}",
3489 ahb_errors
3490 );
3491 }
3492
3493 #[test]
3494 fn test_extract_group_path_key() {
3495 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
3496 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
3497 assert_eq!(extract_group_path_key("NAD/3035"), "");
3498 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
3499 }
3500
3501 fn validate_field_of_absent_group(
3503 group_status: &str,
3504 conditions: Vec<(u32, CR)>,
3505 ) -> ValidationReport {
3506 use mig_types::navigator::GroupNavigator;
3507
3508 struct NoSg9;
3509 impl GroupNavigator for NoSg9 {
3510 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3511 vec![]
3512 }
3513 fn find_segments_with_qualifier_in_group(
3514 &self,
3515 _: &str,
3516 _: usize,
3517 _: &str,
3518 _: &[&str],
3519 _: usize,
3520 ) -> Vec<OwnedSegment> {
3521 vec![]
3522 }
3523 fn group_instance_count(&self, _: &[&str]) -> usize {
3524 0
3525 }
3526 }
3527
3528 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3529 let workflow = AhbWorkflow {
3530 pruefidentifikator: "44001".to_string(),
3531 description: "Test".to_string(),
3532 communication_direction: None,
3533 fields: vec![AhbFieldRule {
3534 segment_path: "SG9/QTY/C186/6060".to_string(),
3535 name: "Menge".to_string(),
3536 ahb_status: "X".to_string(),
3537 parent_group_ahb_status: Some(group_status.to_string()),
3538 ..Default::default()
3539 }],
3540 ub_definitions: BTreeMap::new(),
3541 };
3542 validator.validate_with_navigator(
3543 &[],
3544 &workflow,
3545 &NoOpExternalProvider,
3546 ValidationLevel::Conditions,
3547 &NoSg9,
3548 )
3549 }
3550
3551 #[test]
3552 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
3553 let report = validate_field_of_absent_group(
3555 "Soll [47]\r\nMuss [46]",
3556 vec![(47, CR::True), (46, CR::False)],
3557 );
3558 assert!(
3559 report.is_valid(),
3560 "{:?}",
3561 report.errors().collect::<Vec<_>>()
3562 );
3563 }
3564
3565 #[test]
3566 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
3567 let report = validate_field_of_absent_group(
3568 "Soll [47]\r\nMuss [46]",
3569 vec![(47, CR::False), (46, CR::True)],
3570 );
3571 let errors: Vec<_> = report.errors().collect();
3572 assert_eq!(errors.len(), 1, "{errors:?}");
3573 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3574 }
3575
3576 #[test]
3577 fn test_absent_optional_group_no_missing_field_error() {
3578 use mig_types::navigator::GroupNavigator;
3581
3582 struct NavWithoutSG3;
3583 impl GroupNavigator for NavWithoutSG3 {
3584 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3585 vec![]
3586 }
3587 fn find_segments_with_qualifier_in_group(
3588 &self,
3589 _: &str,
3590 _: usize,
3591 _: &str,
3592 _: &[&str],
3593 _: usize,
3594 ) -> Vec<OwnedSegment> {
3595 vec![]
3596 }
3597 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3598 match group_path {
3599 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
3602 }
3603 }
3604 }
3605
3606 let evaluator = MockEvaluator::new(vec![]);
3607 let validator = EdifactValidator::new(evaluator);
3608 let external = NoOpExternalProvider;
3609 let nav = NavWithoutSG3;
3610
3611 let segments = vec![
3613 OwnedSegment {
3614 id: "NAD".into(),
3615 elements: vec![vec!["MS".into()]],
3616 segment_number: 3,
3617 },
3618 OwnedSegment {
3619 id: "NAD".into(),
3620 elements: vec![vec!["MR".into()]],
3621 segment_number: 4,
3622 },
3623 ];
3624
3625 let workflow = AhbWorkflow {
3626 pruefidentifikator: "55001".to_string(),
3627 description: "Test".to_string(),
3628 communication_direction: None,
3629 fields: vec![
3630 AhbFieldRule {
3631 segment_path: "SG2/SG3/CTA/3139".to_string(),
3632 name: "Funktion des Ansprechpartners, Code".to_string(),
3633 ahb_status: "Muss".to_string(),
3634 codes: vec![],
3635 parent_group_ahb_status: None,
3636 segment_ahb_status: None,
3637 ..Default::default()
3638 },
3639 AhbFieldRule {
3640 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
3641 name: "Name vom Ansprechpartner".to_string(),
3642 ahb_status: "X".to_string(),
3643 codes: vec![],
3644 parent_group_ahb_status: None,
3645 segment_ahb_status: None,
3646 ..Default::default()
3647 },
3648 ],
3649 ub_definitions: BTreeMap::new(),
3650 };
3651
3652 let report = validator.validate_with_navigator(
3653 &segments,
3654 &workflow,
3655 &external,
3656 ValidationLevel::Conditions,
3657 &nav,
3658 );
3659
3660 let ahb_errors: Vec<_> = report
3661 .by_category(ValidationCategory::Ahb)
3662 .filter(|i| i.severity == Severity::Error)
3663 .collect();
3664 assert!(
3665 ahb_errors.is_empty(),
3666 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
3667 ahb_errors
3668 );
3669 }
3670
3671 #[test]
3672 fn test_present_group_still_checks_mandatory_fields() {
3673 use mig_types::navigator::GroupNavigator;
3675
3676 struct NavWithSG3;
3677 impl GroupNavigator for NavWithSG3 {
3678 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3679 vec![]
3680 }
3681 fn find_segments_with_qualifier_in_group(
3682 &self,
3683 _: &str,
3684 _: usize,
3685 _: &str,
3686 _: &[&str],
3687 _: usize,
3688 ) -> Vec<OwnedSegment> {
3689 vec![]
3690 }
3691 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3692 match group_path {
3693 ["SG2"] => 1,
3694 ["SG2", "SG3"] => 1, _ => 0,
3696 }
3697 }
3698 }
3699
3700 let evaluator = MockEvaluator::new(vec![]);
3701 let validator = EdifactValidator::new(evaluator);
3702 let external = NoOpExternalProvider;
3703 let nav = NavWithSG3;
3704
3705 let segments = vec![OwnedSegment {
3707 id: "NAD".into(),
3708 elements: vec![vec!["MS".into()]],
3709 segment_number: 3,
3710 }];
3711
3712 let workflow = AhbWorkflow {
3713 pruefidentifikator: "55001".to_string(),
3714 description: "Test".to_string(),
3715 communication_direction: None,
3716 fields: vec![AhbFieldRule {
3717 segment_path: "SG2/SG3/CTA/3139".to_string(),
3718 name: "Funktion des Ansprechpartners, Code".to_string(),
3719 ahb_status: "Muss".to_string(),
3720 codes: vec![],
3721 parent_group_ahb_status: None,
3722 segment_ahb_status: None,
3723 ..Default::default()
3724 }],
3725 ub_definitions: BTreeMap::new(),
3726 };
3727
3728 let report = validator.validate_with_navigator(
3729 &segments,
3730 &workflow,
3731 &external,
3732 ValidationLevel::Conditions,
3733 &nav,
3734 );
3735
3736 let ahb_errors: Vec<_> = report
3737 .by_category(ValidationCategory::Ahb)
3738 .filter(|i| i.severity == Severity::Error)
3739 .collect();
3740 assert_eq!(
3741 ahb_errors.len(),
3742 1,
3743 "Expected AHB001 error when SG3 is present but CTA missing"
3744 );
3745 assert!(narrate(ahb_errors[0]).contains("CTA"));
3746 }
3747
3748 #[test]
3749 fn test_missing_qualifier_with_navigator_is_detected() {
3750 use mig_types::navigator::GroupNavigator;
3753
3754 struct NavWithSG2;
3755 impl GroupNavigator for NavWithSG2 {
3756 fn find_segments_in_group(
3757 &self,
3758 segment_id: &str,
3759 group_path: &[&str],
3760 instance_index: usize,
3761 ) -> Vec<OwnedSegment> {
3762 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
3763 vec![OwnedSegment {
3764 id: "NAD".into(),
3765 elements: vec![vec!["MS".into()]],
3766 segment_number: 3,
3767 }]
3768 } else {
3769 vec![]
3770 }
3771 }
3772 fn find_segments_with_qualifier_in_group(
3773 &self,
3774 _: &str,
3775 _: usize,
3776 _: &str,
3777 _: &[&str],
3778 _: usize,
3779 ) -> Vec<OwnedSegment> {
3780 vec![]
3781 }
3782 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3783 match group_path {
3784 ["SG2"] => 1,
3785 _ => 0,
3786 }
3787 }
3788 }
3789
3790 let evaluator = MockEvaluator::new(vec![]);
3791 let validator = EdifactValidator::new(evaluator);
3792 let external = NoOpExternalProvider;
3793 let nav = NavWithSG2;
3794
3795 let segments = vec![OwnedSegment {
3796 id: "NAD".into(),
3797 elements: vec![vec!["MS".into()]],
3798 segment_number: 3,
3799 }];
3800
3801 let workflow = AhbWorkflow {
3802 pruefidentifikator: "55001".to_string(),
3803 description: "Test".to_string(),
3804 communication_direction: None,
3805 fields: vec![
3806 AhbFieldRule {
3807 segment_path: "SG2/NAD/3035".to_string(),
3808 name: "Absender".to_string(),
3809 ahb_status: "X".to_string(),
3810 codes: vec![AhbCodeRule {
3811 value: "MS".to_string(),
3812 description: "Absender".to_string(),
3813 ahb_status: "X".to_string(),
3814 }],
3815 parent_group_ahb_status: None,
3816 segment_ahb_status: None,
3817 ..Default::default()
3818 },
3819 AhbFieldRule {
3820 segment_path: "SG2/NAD/3035".to_string(),
3821 name: "Empfaenger".to_string(),
3822 ahb_status: "Muss".to_string(),
3823 codes: vec![AhbCodeRule {
3824 value: "MR".to_string(),
3825 description: "Empfaenger".to_string(),
3826 ahb_status: "X".to_string(),
3827 }],
3828 parent_group_ahb_status: None,
3829 segment_ahb_status: None,
3830 ..Default::default()
3831 },
3832 ],
3833 ub_definitions: BTreeMap::new(),
3834 };
3835
3836 let report = validator.validate_with_navigator(
3837 &segments,
3838 &workflow,
3839 &external,
3840 ValidationLevel::Conditions,
3841 &nav,
3842 );
3843
3844 let ahb_errors: Vec<_> = report
3845 .by_category(ValidationCategory::Ahb)
3846 .filter(|i| i.severity == Severity::Error)
3847 .collect();
3848 assert_eq!(
3849 ahb_errors.len(),
3850 1,
3851 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
3852 ahb_errors
3853 );
3854 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3855 }
3856
3857 #[test]
3858 fn test_optional_group_variant_absent_no_error() {
3859 use mig_types::navigator::GroupNavigator;
3864
3865 struct TestNav;
3866 impl GroupNavigator for TestNav {
3867 fn find_segments_in_group(
3868 &self,
3869 segment_id: &str,
3870 group_path: &[&str],
3871 instance_index: usize,
3872 ) -> Vec<OwnedSegment> {
3873 match (segment_id, group_path, instance_index) {
3874 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
3875 id: "LOC".into(),
3876 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3877 segment_number: 10,
3878 }],
3879 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
3880 id: "NAD".into(),
3881 elements: vec![vec!["MS".into()]],
3882 segment_number: 3,
3883 }],
3884 _ => vec![],
3885 }
3886 }
3887 fn find_segments_with_qualifier_in_group(
3888 &self,
3889 _: &str,
3890 _: usize,
3891 _: &str,
3892 _: &[&str],
3893 _: usize,
3894 ) -> Vec<OwnedSegment> {
3895 vec![]
3896 }
3897 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3898 match group_path {
3899 ["SG2"] => 1,
3900 ["SG4"] => 1,
3901 ["SG4", "SG5"] => 1, _ => 0,
3903 }
3904 }
3905 }
3906
3907 let evaluator = MockEvaluator::new(vec![]);
3908 let validator = EdifactValidator::new(evaluator);
3909 let external = NoOpExternalProvider;
3910 let nav = TestNav;
3911
3912 let segments = vec![
3913 OwnedSegment {
3914 id: "NAD".into(),
3915 elements: vec![vec!["MS".into()]],
3916 segment_number: 3,
3917 },
3918 OwnedSegment {
3919 id: "LOC".into(),
3920 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3921 segment_number: 10,
3922 },
3923 ];
3924
3925 let workflow = AhbWorkflow {
3926 pruefidentifikator: "55001".to_string(),
3927 description: "Test".to_string(),
3928 communication_direction: None,
3929 fields: vec![
3930 AhbFieldRule {
3932 segment_path: "SG2/NAD/3035".to_string(),
3933 name: "Absender".to_string(),
3934 ahb_status: "X".to_string(),
3935 codes: vec![AhbCodeRule {
3936 value: "MS".to_string(),
3937 description: "Absender".to_string(),
3938 ahb_status: "X".to_string(),
3939 }],
3940 parent_group_ahb_status: Some("Muss".to_string()),
3941 segment_ahb_status: None,
3942 ..Default::default()
3943 },
3944 AhbFieldRule {
3945 segment_path: "SG2/NAD/3035".to_string(),
3946 name: "Empfaenger".to_string(),
3947 ahb_status: "Muss".to_string(),
3948 codes: vec![AhbCodeRule {
3949 value: "MR".to_string(),
3950 description: "Empfaenger".to_string(),
3951 ahb_status: "X".to_string(),
3952 }],
3953 parent_group_ahb_status: Some("Muss".to_string()),
3954 segment_ahb_status: None,
3955 ..Default::default()
3956 },
3957 AhbFieldRule {
3959 segment_path: "SG4/SG5/LOC/3227".to_string(),
3960 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3961 ahb_status: "X".to_string(),
3962 codes: vec![AhbCodeRule {
3963 value: "Z16".to_string(),
3964 description: "Marktlokation".to_string(),
3965 ahb_status: "X".to_string(),
3966 }],
3967 parent_group_ahb_status: Some("Kann".to_string()),
3968 segment_ahb_status: None,
3969 ..Default::default()
3970 },
3971 AhbFieldRule {
3972 segment_path: "SG4/SG5/LOC/3227".to_string(),
3973 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3974 ahb_status: "Muss".to_string(),
3975 codes: vec![AhbCodeRule {
3976 value: "Z17".to_string(),
3977 description: "Messlokation".to_string(),
3978 ahb_status: "X".to_string(),
3979 }],
3980 parent_group_ahb_status: Some("Kann".to_string()),
3981 segment_ahb_status: None,
3982 ..Default::default()
3983 },
3984 ],
3985 ub_definitions: BTreeMap::new(),
3986 };
3987
3988 let report = validator.validate_with_navigator(
3989 &segments,
3990 &workflow,
3991 &external,
3992 ValidationLevel::Conditions,
3993 &nav,
3994 );
3995
3996 let ahb_errors: Vec<_> = report
3997 .by_category(ValidationCategory::Ahb)
3998 .filter(|i| i.severity == Severity::Error)
3999 .collect();
4000
4001 assert_eq!(
4004 ahb_errors.len(),
4005 1,
4006 "Expected only AHB001 for missing NAD+MR, got: {:?}",
4007 ahb_errors
4008 );
4009 assert!(
4010 narrate(ahb_errors[0]).contains("Empfaenger"),
4011 "Error should be for missing NAD+MR (Empfaenger)"
4012 );
4013 }
4014
4015 #[test]
4019 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4020 use mig_types::navigator::GroupNavigator;
4021
4022 struct TestNav;
4023 impl GroupNavigator for TestNav {
4024 fn find_segments_in_group(
4025 &self,
4026 segment_id: &str,
4027 group_path: &[&str],
4028 instance_index: usize,
4029 ) -> Vec<OwnedSegment> {
4030 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4031 vec![OwnedSegment {
4032 id: "SEQ".into(),
4033 elements: vec![vec!["Z03".into()]],
4034 segment_number: 10,
4035 }]
4036 } else {
4037 vec![]
4038 }
4039 }
4040 fn find_segments_with_qualifier_in_group(
4041 &self,
4042 _: &str,
4043 _: usize,
4044 _: &str,
4045 _: &[&str],
4046 _: usize,
4047 ) -> Vec<OwnedSegment> {
4048 vec![]
4049 }
4050 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4051 match group_path {
4052 ["SG4"] => 1,
4053 ["SG4", "SG8"] => 1,
4054 _ => 0,
4055 }
4056 }
4057 }
4058
4059 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
4060 let validator = EdifactValidator::new(evaluator);
4061 let segments = vec![OwnedSegment {
4062 id: "SEQ".into(),
4063 elements: vec![vec!["Z03".into()]],
4064 segment_number: 10,
4065 }];
4066 let seq = |code: &str, group: &str| AhbFieldRule {
4067 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4068 name: format!("Handlung, Code ({code})"),
4069 ahb_status: "X".to_string(),
4070 codes: vec![AhbCodeRule {
4071 value: code.to_string(),
4072 description: String::new(),
4073 ahb_status: "X".to_string(),
4074 }],
4075 parent_group_ahb_status: Some(group.to_string()),
4076 segment_ahb_status: None,
4077 ..Default::default()
4078 };
4079 let workflow = AhbWorkflow {
4080 pruefidentifikator: "44143".to_string(),
4081 description: "Test".to_string(),
4082 communication_direction: None,
4083 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
4084 ub_definitions: BTreeMap::new(),
4085 };
4086
4087 let report = validator.validate_with_navigator(
4088 &segments,
4089 &workflow,
4090 &NoOpExternalProvider,
4091 ValidationLevel::Conditions,
4092 &TestNav,
4093 );
4094 let missing: Vec<_> = report
4095 .issues
4096 .iter()
4097 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
4098 .collect();
4099 assert!(missing.is_empty(), "{missing:#?}");
4100 }
4101
4102 #[test]
4103 fn test_conditional_group_variant_absent_no_error() {
4104 use mig_types::navigator::GroupNavigator;
4109
4110 struct TestNav;
4111 impl GroupNavigator for TestNav {
4112 fn find_segments_in_group(
4113 &self,
4114 segment_id: &str,
4115 group_path: &[&str],
4116 instance_index: usize,
4117 ) -> Vec<OwnedSegment> {
4118 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
4119 vec![OwnedSegment {
4120 id: "LOC".into(),
4121 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4122 segment_number: 10,
4123 }]
4124 } else {
4125 vec![]
4126 }
4127 }
4128 fn find_segments_with_qualifier_in_group(
4129 &self,
4130 _: &str,
4131 _: usize,
4132 _: &str,
4133 _: &[&str],
4134 _: usize,
4135 ) -> Vec<OwnedSegment> {
4136 vec![]
4137 }
4138 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4139 match group_path {
4140 ["SG4"] => 1,
4141 ["SG4", "SG5"] => 1, _ => 0,
4143 }
4144 }
4145 }
4146
4147 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
4150 let validator = EdifactValidator::new(evaluator);
4151 let external = NoOpExternalProvider;
4152 let nav = TestNav;
4153
4154 let segments = vec![OwnedSegment {
4155 id: "LOC".into(),
4156 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4157 segment_number: 10,
4158 }];
4159
4160 let workflow = AhbWorkflow {
4161 pruefidentifikator: "55001".to_string(),
4162 description: "Test".to_string(),
4163 communication_direction: None,
4164 fields: vec![
4165 AhbFieldRule {
4167 segment_path: "SG4/SG5/LOC/3227".to_string(),
4168 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4169 ahb_status: "X".to_string(),
4170 codes: vec![AhbCodeRule {
4171 value: "Z16".to_string(),
4172 description: "Marktlokation".to_string(),
4173 ahb_status: "X".to_string(),
4174 }],
4175 parent_group_ahb_status: Some("Muss [2061]".to_string()),
4176 segment_ahb_status: None,
4177 ..Default::default()
4178 },
4179 AhbFieldRule {
4181 segment_path: "SG4/SG5/LOC/3227".to_string(),
4182 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4183 ahb_status: "X".to_string(),
4184 codes: vec![AhbCodeRule {
4185 value: "Z17".to_string(),
4186 description: "Messlokation".to_string(),
4187 ahb_status: "X".to_string(),
4188 }],
4189 parent_group_ahb_status: Some("Soll [165]".to_string()),
4190 segment_ahb_status: None,
4191 ..Default::default()
4192 },
4193 ],
4194 ub_definitions: BTreeMap::new(),
4195 };
4196
4197 let report = validator.validate_with_navigator(
4198 &segments,
4199 &workflow,
4200 &external,
4201 ValidationLevel::Conditions,
4202 &nav,
4203 );
4204
4205 let ahb_errors: Vec<_> = report
4206 .by_category(ValidationCategory::Ahb)
4207 .filter(|i| i.severity == Severity::Error)
4208 .collect();
4209
4210 assert!(
4212 ahb_errors.is_empty(),
4213 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4214 ahb_errors
4215 );
4216 }
4217
4218 #[test]
4219 fn test_conditional_group_variant_unknown_no_error() {
4220 let evaluator = MockEvaluator::new(vec![]);
4226 let validator = EdifactValidator::new(evaluator);
4227 let external = NoOpExternalProvider;
4228
4229 let workflow = AhbWorkflow {
4230 pruefidentifikator: "55001".to_string(),
4231 description: "Test".to_string(),
4232 communication_direction: None,
4233 fields: vec![AhbFieldRule {
4234 segment_path: "SG4/SG5/LOC/3227".to_string(),
4235 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4236 ahb_status: "X".to_string(),
4237 codes: vec![AhbCodeRule {
4238 value: "Z17".to_string(),
4239 description: "Messlokation".to_string(),
4240 ahb_status: "X".to_string(),
4241 }],
4242 parent_group_ahb_status: Some("Soll [165]".to_string()),
4243 segment_ahb_status: None,
4244 ..Default::default()
4245 }],
4246 ub_definitions: BTreeMap::new(),
4247 };
4248
4249 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4250
4251 let ahb_errors: Vec<_> = report
4252 .by_category(ValidationCategory::Ahb)
4253 .filter(|i| i.severity == Severity::Error)
4254 .collect();
4255
4256 assert!(
4258 ahb_errors.is_empty(),
4259 "Expected no errors when parent group condition is Unknown, got: {:?}",
4260 ahb_errors
4261 );
4262 }
4263
4264 #[test]
4265 fn test_segment_absent_within_present_group_no_error() {
4266 use mig_types::navigator::GroupNavigator;
4270
4271 struct TestNav;
4272 impl GroupNavigator for TestNav {
4273 fn find_segments_in_group(
4274 &self,
4275 segment_id: &str,
4276 group_path: &[&str],
4277 instance_index: usize,
4278 ) -> Vec<OwnedSegment> {
4279 if segment_id == "QTY"
4281 && group_path == ["SG5", "SG6", "SG9", "SG10"]
4282 && instance_index == 0
4283 {
4284 vec![OwnedSegment {
4285 id: "QTY".into(),
4286 elements: vec![vec!["220".into(), "0".into()]],
4287 segment_number: 14,
4288 }]
4289 } else {
4290 vec![]
4291 }
4292 }
4293 fn find_segments_with_qualifier_in_group(
4294 &self,
4295 _: &str,
4296 _: usize,
4297 _: &str,
4298 _: &[&str],
4299 _: usize,
4300 ) -> Vec<OwnedSegment> {
4301 vec![]
4302 }
4303 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4304 match group_path {
4305 ["SG5"] => 1,
4306 ["SG5", "SG6"] => 1,
4307 ["SG5", "SG6", "SG9"] => 1,
4308 ["SG5", "SG6", "SG9", "SG10"] => 1,
4309 _ => 0,
4310 }
4311 }
4312 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4313 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4315 }
4316 }
4317
4318 let evaluator = MockEvaluator::all_true(&[]);
4319 let validator = EdifactValidator::new(evaluator);
4320 let external = NoOpExternalProvider;
4321 let nav = TestNav;
4322
4323 let segments = vec![OwnedSegment {
4324 id: "QTY".into(),
4325 elements: vec![vec!["220".into(), "0".into()]],
4326 segment_number: 14,
4327 }];
4328
4329 let workflow = AhbWorkflow {
4330 pruefidentifikator: "13017".to_string(),
4331 description: "Test".to_string(),
4332 communication_direction: None,
4333 fields: vec![
4334 AhbFieldRule {
4336 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4337 name: "Statuskategorie, Code".to_string(),
4338 ahb_status: "X".to_string(),
4339 codes: vec![],
4340 parent_group_ahb_status: Some("Muss".to_string()),
4341 segment_ahb_status: None,
4342 ..Default::default()
4343 },
4344 AhbFieldRule {
4346 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4347 name: "Statusanlaß, Code".to_string(),
4348 ahb_status: "X [5]".to_string(),
4349 codes: vec![],
4350 parent_group_ahb_status: Some("Muss".to_string()),
4351 segment_ahb_status: None,
4352 ..Default::default()
4353 },
4354 ],
4355 ub_definitions: BTreeMap::new(),
4356 };
4357
4358 let report = validator.validate_with_navigator(
4359 &segments,
4360 &workflow,
4361 &external,
4362 ValidationLevel::Conditions,
4363 &nav,
4364 );
4365
4366 let ahb_errors: Vec<_> = report
4367 .by_category(ValidationCategory::Ahb)
4368 .filter(|i| i.severity == Severity::Error)
4369 .collect();
4370
4371 assert!(
4372 ahb_errors.is_empty(),
4373 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
4374 ahb_errors
4375 );
4376 }
4377
4378 #[test]
4379 fn test_group_scoped_code_validation_with_navigator() {
4380 use mig_types::navigator::GroupNavigator;
4384
4385 struct TestNav;
4386 impl GroupNavigator for TestNav {
4387 fn find_segments_in_group(
4388 &self,
4389 segment_id: &str,
4390 group_path: &[&str],
4391 _instance_index: usize,
4392 ) -> Vec<OwnedSegment> {
4393 if segment_id != "NAD" {
4394 return vec![];
4395 }
4396 match group_path {
4397 ["SG2"] => vec![
4398 OwnedSegment {
4399 id: "NAD".into(),
4400 elements: vec![vec!["MS".into()]],
4401 segment_number: 3,
4402 },
4403 OwnedSegment {
4404 id: "NAD".into(),
4405 elements: vec![vec!["MT".into()]], segment_number: 4,
4407 },
4408 ],
4409 ["SG4", "SG12"] => vec![
4410 OwnedSegment {
4411 id: "NAD".into(),
4412 elements: vec![vec!["Z04".into()]],
4413 segment_number: 20,
4414 },
4415 OwnedSegment {
4416 id: "NAD".into(),
4417 elements: vec![vec!["Z09".into()]],
4418 segment_number: 21,
4419 },
4420 ],
4421 _ => vec![],
4422 }
4423 }
4424 fn find_segments_with_qualifier_in_group(
4425 &self,
4426 _: &str,
4427 _: usize,
4428 _: &str,
4429 _: &[&str],
4430 _: usize,
4431 ) -> Vec<OwnedSegment> {
4432 vec![]
4433 }
4434 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4435 match group_path {
4436 ["SG2"] | ["SG4", "SG12"] => 1,
4437 _ => 0,
4438 }
4439 }
4440 }
4441
4442 let evaluator = MockEvaluator::new(vec![]);
4443 let validator = EdifactValidator::new(evaluator);
4444 let external = NoOpExternalProvider;
4445 let nav = TestNav;
4446
4447 let workflow = AhbWorkflow {
4448 pruefidentifikator: "55001".to_string(),
4449 description: "Test".to_string(),
4450 communication_direction: None,
4451 fields: vec![
4452 AhbFieldRule {
4453 segment_path: "SG2/NAD/3035".to_string(),
4454 name: "Absender".to_string(),
4455 ahb_status: "X".to_string(),
4456 codes: vec![AhbCodeRule {
4457 value: "MS".to_string(),
4458 description: "Absender".to_string(),
4459 ahb_status: "X".to_string(),
4460 }],
4461 parent_group_ahb_status: None,
4462 segment_ahb_status: None,
4463 ..Default::default()
4464 },
4465 AhbFieldRule {
4466 segment_path: "SG2/NAD/3035".to_string(),
4467 name: "Empfaenger".to_string(),
4468 ahb_status: "X".to_string(),
4469 codes: vec![AhbCodeRule {
4470 value: "MR".to_string(),
4471 description: "Empfaenger".to_string(),
4472 ahb_status: "X".to_string(),
4473 }],
4474 parent_group_ahb_status: None,
4475 segment_ahb_status: None,
4476 ..Default::default()
4477 },
4478 AhbFieldRule {
4479 segment_path: "SG4/SG12/NAD/3035".to_string(),
4480 name: "Anschlussnutzer".to_string(),
4481 ahb_status: "X".to_string(),
4482 codes: vec![AhbCodeRule {
4483 value: "Z04".to_string(),
4484 description: "Anschlussnutzer".to_string(),
4485 ahb_status: "X".to_string(),
4486 }],
4487 parent_group_ahb_status: None,
4488 segment_ahb_status: None,
4489 ..Default::default()
4490 },
4491 AhbFieldRule {
4492 segment_path: "SG4/SG12/NAD/3035".to_string(),
4493 name: "Korrespondenzanschrift".to_string(),
4494 ahb_status: "X".to_string(),
4495 codes: vec![AhbCodeRule {
4496 value: "Z09".to_string(),
4497 description: "Korrespondenzanschrift".to_string(),
4498 ahb_status: "X".to_string(),
4499 }],
4500 parent_group_ahb_status: None,
4501 segment_ahb_status: None,
4502 ..Default::default()
4503 },
4504 ],
4505 ub_definitions: BTreeMap::new(),
4506 };
4507
4508 let all_segments = vec![
4510 OwnedSegment {
4511 id: "NAD".into(),
4512 elements: vec![vec!["MS".into()]],
4513 segment_number: 3,
4514 },
4515 OwnedSegment {
4516 id: "NAD".into(),
4517 elements: vec![vec!["MT".into()]],
4518 segment_number: 4,
4519 },
4520 OwnedSegment {
4521 id: "NAD".into(),
4522 elements: vec![vec!["Z04".into()]],
4523 segment_number: 20,
4524 },
4525 OwnedSegment {
4526 id: "NAD".into(),
4527 elements: vec![vec!["Z09".into()]],
4528 segment_number: 21,
4529 },
4530 ];
4531
4532 let report = validator.validate_with_navigator(
4533 &all_segments,
4534 &workflow,
4535 &external,
4536 ValidationLevel::Conditions,
4537 &nav,
4538 );
4539
4540 let code_errors: Vec<_> = report
4541 .by_category(ValidationCategory::Code)
4542 .filter(|i| i.severity == Severity::Error)
4543 .collect();
4544
4545 assert_eq!(
4548 code_errors.len(),
4549 1,
4550 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
4551 code_errors
4552 );
4553 assert!(narrate(code_errors[0]).contains("MT"));
4554 assert!(narrate(code_errors[0]).contains("MR"));
4556 assert!(narrate(code_errors[0]).contains("MS"));
4557 assert!(
4558 !narrate(code_errors[0]).contains("Z04"),
4559 "SG4/SG12 codes should not leak into SG2 error"
4560 );
4561 assert!(
4563 code_errors[0]
4564 .field_path
4565 .as_deref()
4566 .unwrap_or("")
4567 .contains("SG2"),
4568 "Error field_path should reference SG2, got: {:?}",
4569 code_errors[0].field_path
4570 );
4571 }
4572
4573 #[test]
4576 fn test_package_cardinality_within_bounds() {
4577 let evaluator = MockEvaluator::all_true(&[]);
4579 let validator = EdifactValidator::new(evaluator);
4580 let external = NoOpExternalProvider;
4581
4582 let segments = vec![OwnedSegment {
4583 id: "STS".into(),
4584 elements: vec![
4585 vec!["Z33".into()], vec![], vec!["E01".into()], ],
4589 segment_number: 5,
4590 }];
4591
4592 let workflow = AhbWorkflow {
4593 pruefidentifikator: "13017".to_string(),
4594 description: "Test".to_string(),
4595 communication_direction: None,
4596 ub_definitions: BTreeMap::new(),
4597 fields: vec![AhbFieldRule {
4598 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4599 name: "Statusanlaß, Code".to_string(),
4600 ahb_status: "X".to_string(),
4601 element_index: Some(2),
4602 component_index: Some(0),
4603 codes: vec![
4604 AhbCodeRule {
4605 value: "E01".into(),
4606 description: "Code 1".into(),
4607 ahb_status: "X [4P0..1]".into(),
4608 },
4609 AhbCodeRule {
4610 value: "E02".into(),
4611 description: "Code 2".into(),
4612 ahb_status: "X [4P0..1]".into(),
4613 },
4614 ],
4615 parent_group_ahb_status: Some("Muss".to_string()),
4616 segment_ahb_status: None,
4617 mig_number: None,
4618 format: None,
4619 }],
4620 };
4621
4622 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4623 let pkg_errors: Vec<_> = report
4624 .by_category(ValidationCategory::Ahb)
4625 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4626 .collect();
4627 assert!(
4628 pkg_errors.is_empty(),
4629 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
4630 pkg_errors
4631 );
4632 }
4633
4634 #[test]
4635 fn test_package_cardinality_zero_present_min_zero() {
4636 let evaluator = MockEvaluator::all_true(&[]);
4638 let validator = EdifactValidator::new(evaluator);
4639 let external = NoOpExternalProvider;
4640
4641 let segments = vec![OwnedSegment {
4642 id: "STS".into(),
4643 elements: vec![
4644 vec!["Z33".into()],
4645 vec![],
4646 vec!["X99".into()], ],
4648 segment_number: 5,
4649 }];
4650
4651 let workflow = AhbWorkflow {
4652 pruefidentifikator: "13017".to_string(),
4653 description: "Test".to_string(),
4654 communication_direction: None,
4655 ub_definitions: BTreeMap::new(),
4656 fields: vec![AhbFieldRule {
4657 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4658 name: "Statusanlaß, Code".to_string(),
4659 ahb_status: "X".to_string(),
4660 element_index: Some(2),
4661 component_index: Some(0),
4662 codes: vec![
4663 AhbCodeRule {
4664 value: "E01".into(),
4665 description: "Code 1".into(),
4666 ahb_status: "X [4P0..1]".into(),
4667 },
4668 AhbCodeRule {
4669 value: "E02".into(),
4670 description: "Code 2".into(),
4671 ahb_status: "X [4P0..1]".into(),
4672 },
4673 ],
4674 parent_group_ahb_status: Some("Muss".to_string()),
4675 segment_ahb_status: None,
4676 mig_number: None,
4677 format: None,
4678 }],
4679 };
4680
4681 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4682 let pkg_errors: Vec<_> = report
4683 .by_category(ValidationCategory::Ahb)
4684 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4685 .collect();
4686 assert!(
4687 pkg_errors.is_empty(),
4688 "0 codes, min=0 — no error expected, got: {:?}",
4689 pkg_errors
4690 );
4691 }
4692
4693 #[test]
4694 fn test_package_cardinality_too_many() {
4695 let evaluator = MockEvaluator::all_true(&[]);
4697 let validator = EdifactValidator::new(evaluator);
4698 let external = NoOpExternalProvider;
4699
4700 let segments = vec![
4702 OwnedSegment {
4703 id: "STS".into(),
4704 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
4705 segment_number: 5,
4706 },
4707 OwnedSegment {
4708 id: "STS".into(),
4709 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
4710 segment_number: 6,
4711 },
4712 ];
4713
4714 let workflow = AhbWorkflow {
4715 pruefidentifikator: "13017".to_string(),
4716 description: "Test".to_string(),
4717 communication_direction: None,
4718 ub_definitions: BTreeMap::new(),
4719 fields: vec![AhbFieldRule {
4720 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4721 name: "Statusanlaß, Code".to_string(),
4722 ahb_status: "X".to_string(),
4723 element_index: Some(2),
4724 component_index: Some(0),
4725 codes: vec![
4726 AhbCodeRule {
4727 value: "E01".into(),
4728 description: "Code 1".into(),
4729 ahb_status: "X [4P0..1]".into(),
4730 },
4731 AhbCodeRule {
4732 value: "E02".into(),
4733 description: "Code 2".into(),
4734 ahb_status: "X [4P0..1]".into(),
4735 },
4736 ],
4737 parent_group_ahb_status: Some("Muss".to_string()),
4738 segment_ahb_status: None,
4739 mig_number: None,
4740 format: None,
4741 }],
4742 };
4743
4744 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4745 let pkg_errors: Vec<_> = report
4746 .by_category(ValidationCategory::Ahb)
4747 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4748 .collect();
4749 assert_eq!(
4750 pkg_errors.len(),
4751 1,
4752 "2 codes present, max=1 — expected 1 error, got: {:?}",
4753 pkg_errors
4754 );
4755 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
4756 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
4757 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
4758 }
4759
4760 #[test]
4761 fn test_package_cardinality_too_few() {
4762 let evaluator = MockEvaluator::all_true(&[]);
4764 let validator = EdifactValidator::new(evaluator);
4765 let external = NoOpExternalProvider;
4766
4767 let segments = vec![OwnedSegment {
4768 id: "STS".into(),
4769 elements: vec![
4770 vec!["Z33".into()],
4771 vec![],
4772 vec!["X99".into()], ],
4774 segment_number: 5,
4775 }];
4776
4777 let workflow = AhbWorkflow {
4778 pruefidentifikator: "13017".to_string(),
4779 description: "Test".to_string(),
4780 communication_direction: None,
4781 ub_definitions: BTreeMap::new(),
4782 fields: vec![AhbFieldRule {
4783 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4784 name: "Statusanlaß, Code".to_string(),
4785 ahb_status: "X".to_string(),
4786 element_index: Some(2),
4787 component_index: Some(0),
4788 codes: vec![
4789 AhbCodeRule {
4790 value: "E01".into(),
4791 description: "Code 1".into(),
4792 ahb_status: "X [5P1..3]".into(),
4793 },
4794 AhbCodeRule {
4795 value: "E02".into(),
4796 description: "Code 2".into(),
4797 ahb_status: "X [5P1..3]".into(),
4798 },
4799 AhbCodeRule {
4800 value: "E03".into(),
4801 description: "Code 3".into(),
4802 ahb_status: "X [5P1..3]".into(),
4803 },
4804 ],
4805 parent_group_ahb_status: Some("Muss".to_string()),
4806 segment_ahb_status: None,
4807 mig_number: None,
4808 format: None,
4809 }],
4810 };
4811
4812 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4813 let pkg_errors: Vec<_> = report
4814 .by_category(ValidationCategory::Ahb)
4815 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4816 .collect();
4817 assert_eq!(
4818 pkg_errors.len(),
4819 1,
4820 "0 codes present, min=1 — expected 1 error, got: {:?}",
4821 pkg_errors
4822 );
4823 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
4824 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
4825 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
4826 }
4827
4828 #[test]
4829 fn test_package_cardinality_no_packages_in_workflow() {
4830 let evaluator = MockEvaluator::all_true(&[]);
4832 let validator = EdifactValidator::new(evaluator);
4833 let external = NoOpExternalProvider;
4834
4835 let segments = vec![OwnedSegment {
4836 id: "STS".into(),
4837 elements: vec![vec!["E01".into()]],
4838 segment_number: 5,
4839 }];
4840
4841 let workflow = AhbWorkflow {
4842 pruefidentifikator: "13017".to_string(),
4843 description: "Test".to_string(),
4844 communication_direction: None,
4845 ub_definitions: BTreeMap::new(),
4846 fields: vec![AhbFieldRule {
4847 segment_path: "STS/9015".to_string(),
4848 name: "Status Code".to_string(),
4849 ahb_status: "X".to_string(),
4850 codes: vec![AhbCodeRule {
4851 value: "E01".into(),
4852 description: "Code 1".into(),
4853 ahb_status: "X".into(),
4854 }],
4855 parent_group_ahb_status: Some("Muss".to_string()),
4856 segment_ahb_status: None,
4857 ..Default::default()
4858 }],
4859 };
4860
4861 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4862 let pkg_errors: Vec<_> = report
4863 .by_category(ValidationCategory::Ahb)
4864 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4865 .collect();
4866 assert!(
4867 pkg_errors.is_empty(),
4868 "No packages in workflow — no errors expected"
4869 );
4870 }
4871
4872 #[test]
4873 fn test_package_cardinality_with_condition_and_package() {
4874 let evaluator = MockEvaluator::all_true(&[901]);
4876 let validator = EdifactValidator::new(evaluator);
4877 let external = NoOpExternalProvider;
4878
4879 let segments = vec![OwnedSegment {
4880 id: "STS".into(),
4881 elements: vec![vec![], vec![], vec!["E01".into()]],
4882 segment_number: 5,
4883 }];
4884
4885 let workflow = AhbWorkflow {
4886 pruefidentifikator: "13017".to_string(),
4887 description: "Test".to_string(),
4888 communication_direction: None,
4889 ub_definitions: BTreeMap::new(),
4890 fields: vec![AhbFieldRule {
4891 segment_path: "SG10/STS/C556/9013".to_string(),
4892 name: "Code".to_string(),
4893 ahb_status: "X".to_string(),
4894 element_index: Some(2),
4895 component_index: Some(0),
4896 codes: vec![
4897 AhbCodeRule {
4898 value: "E01".into(),
4899 description: "Code 1".into(),
4900 ahb_status: "X [901] [4P0..1]".into(),
4901 },
4902 AhbCodeRule {
4903 value: "E02".into(),
4904 description: "Code 2".into(),
4905 ahb_status: "X [901] [4P0..1]".into(),
4906 },
4907 ],
4908 parent_group_ahb_status: Some("Muss".to_string()),
4909 segment_ahb_status: None,
4910 mig_number: None,
4911 format: None,
4912 }],
4913 };
4914
4915 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4916 let pkg_errors: Vec<_> = report
4917 .by_category(ValidationCategory::Ahb)
4918 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4919 .collect();
4920 assert!(
4921 pkg_errors.is_empty(),
4922 "1 code within [4P0..1] bounds — no error, got: {:?}",
4923 pkg_errors
4924 );
4925 }
4926
4927 #[test]
4928 fn test_package_cardinality_scoped_per_group_instance() {
4929 use mig_types::navigator::GroupNavigator;
4937
4938 struct TwoSg10s {
4939 sts_a: OwnedSegment,
4940 sts_b: OwnedSegment,
4941 }
4942 impl GroupNavigator for TwoSg10s {
4943 fn find_segments_in_group(
4944 &self,
4945 segment_id: &str,
4946 group_path: &[&str],
4947 instance_index: usize,
4948 ) -> Vec<OwnedSegment> {
4949 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
4950 match instance_index {
4951 0 => vec![self.sts_a.clone()],
4952 1 => vec![self.sts_b.clone()],
4953 _ => vec![],
4954 }
4955 } else {
4956 vec![]
4957 }
4958 }
4959 fn find_segments_with_qualifier_in_group(
4960 &self,
4961 _: &str,
4962 _: usize,
4963 _: &str,
4964 _: &[&str],
4965 _: usize,
4966 ) -> Vec<OwnedSegment> {
4967 vec![]
4968 }
4969 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4970 match group_path {
4971 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
4972 ["SG5", "SG6", "SG9", "SG10"] => 2,
4973 _ => 0,
4974 }
4975 }
4976 }
4977
4978 let sts_a = OwnedSegment {
4979 id: "STS".into(),
4980 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4981 segment_number: 10,
4982 };
4983 let sts_b = OwnedSegment {
4984 id: "STS".into(),
4985 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
4986 segment_number: 15,
4987 };
4988 let nav = TwoSg10s {
4989 sts_a: sts_a.clone(),
4990 sts_b: sts_b.clone(),
4991 };
4992
4993 let evaluator = MockEvaluator::all_true(&[]);
4994 let validator = EdifactValidator::new(evaluator);
4995 let external = NoOpExternalProvider;
4996
4997 let workflow = AhbWorkflow {
4998 pruefidentifikator: "13025".to_string(),
4999 description: "Test".to_string(),
5000 communication_direction: None,
5001 ub_definitions: BTreeMap::new(),
5002 fields: vec![AhbFieldRule {
5003 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5004 name: "Statusanlaß, Code".to_string(),
5005 ahb_status: "X".to_string(),
5006 element_index: Some(2),
5007 component_index: Some(0),
5008 codes: vec![
5009 AhbCodeRule {
5010 value: "E01".into(),
5011 description: "Code 1".into(),
5012 ahb_status: "X [4P0..1]".into(),
5013 },
5014 AhbCodeRule {
5015 value: "E02".into(),
5016 description: "Code 2".into(),
5017 ahb_status: "X [4P0..1]".into(),
5018 },
5019 ],
5020 parent_group_ahb_status: Some("Muss".to_string()),
5021 segment_ahb_status: None,
5022 mig_number: None,
5023 format: None,
5024 }],
5025 };
5026
5027 let report = validator.validate_with_navigator(
5028 &[sts_a, sts_b],
5029 &workflow,
5030 &external,
5031 ValidationLevel::Full,
5032 &nav,
5033 );
5034 let pkg_errors: Vec<_> = report
5035 .by_category(ValidationCategory::Ahb)
5036 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5037 .collect();
5038 assert!(
5039 pkg_errors.is_empty(),
5040 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
5041 pkg_errors
5042 );
5043 }
5044
5045 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
5046 OwnedSegment {
5047 id: id.to_string(),
5048 elements: elements
5049 .into_iter()
5050 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
5051 .collect(),
5052 segment_number: 0,
5053 }
5054 }
5055
5056 #[test]
5067 fn test_package_cardinality_scoped_to_rule_mig_variant() {
5068 use mig_types::navigator::GroupNavigator;
5069
5070 struct TwoSg8Variants {
5071 seq_z01: OwnedSegment,
5072 seq_z45: OwnedSegment,
5073 }
5074 impl GroupNavigator for TwoSg8Variants {
5075 fn find_segments_in_group(
5076 &self,
5077 segment_id: &str,
5078 group_path: &[&str],
5079 instance_index: usize,
5080 ) -> Vec<OwnedSegment> {
5081 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
5082 match instance_index {
5083 0 => vec![self.seq_z01.clone()],
5084 1 => vec![self.seq_z45.clone()],
5085 _ => vec![],
5086 }
5087 } else {
5088 vec![]
5089 }
5090 }
5091 fn find_segments_with_qualifier_in_group(
5092 &self,
5093 _: &str,
5094 _: usize,
5095 _: &str,
5096 _: &[&str],
5097 _: usize,
5098 ) -> Vec<OwnedSegment> {
5099 vec![]
5100 }
5101 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5102 match group_path {
5103 ["SG4"] => 1,
5104 ["SG4", "SG8"] => 2,
5105 _ => 0,
5106 }
5107 }
5108 fn instance_has_mig_number(
5109 &self,
5110 group_path: &[&str],
5111 instance_index: usize,
5112 mig_number: &str,
5113 ) -> bool {
5114 if group_path != ["SG4", "SG8"] {
5115 return true;
5116 }
5117 match (instance_index, mig_number) {
5118 (0, "00115") => true,
5119 (0, _) => false,
5120 (1, "00171") => true,
5121 (1, _) => false,
5122 _ => false,
5123 }
5124 }
5125 }
5126
5127 let seq_z01 = OwnedSegment {
5128 id: "SEQ".into(),
5129 elements: vec![vec!["Z01".into()], vec!["1".into()]],
5130 segment_number: 10,
5131 };
5132 let seq_z45 = OwnedSegment {
5133 id: "SEQ".into(),
5134 elements: vec![vec!["Z45".into()], vec!["1".into()]],
5135 segment_number: 20,
5136 };
5137 let nav = TwoSg8Variants {
5138 seq_z01: seq_z01.clone(),
5139 seq_z45: seq_z45.clone(),
5140 };
5141
5142 let evaluator = MockEvaluator::all_true(&[]);
5143 let validator = EdifactValidator::new(evaluator);
5144 let external = NoOpExternalProvider;
5145
5146 let workflow = AhbWorkflow {
5147 pruefidentifikator: "55218".to_string(),
5148 description: "Test".to_string(),
5149 communication_direction: None,
5150 ub_definitions: BTreeMap::new(),
5151 fields: vec![AhbFieldRule {
5152 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5153 name: "Handlung, Code".to_string(),
5154 ahb_status: "X".to_string(),
5155 element_index: Some(0),
5156 component_index: Some(0),
5157 codes: vec![
5158 AhbCodeRule {
5159 value: "Z45".into(),
5160 description: "NNA".into(),
5161 ahb_status: "X [1P1..4294967295]".into(),
5162 },
5163 AhbCodeRule {
5164 value: "Z84".into(),
5165 description: "Differenz-NNA".into(),
5166 ahb_status: "X [1P0..4294967295]".into(),
5167 },
5168 ],
5169 parent_group_ahb_status: Some("Muss".to_string()),
5170 segment_ahb_status: None,
5171 mig_number: Some("00171".to_string()),
5172 format: None,
5173 }],
5174 };
5175
5176 let report = validator.validate_with_navigator(
5177 &[seq_z01, seq_z45],
5178 &workflow,
5179 &external,
5180 ValidationLevel::Full,
5181 &nav,
5182 );
5183 let pkg_errors: Vec<_> = report
5184 .by_category(ValidationCategory::Ahb)
5185 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5186 .collect();
5187 assert!(
5188 pkg_errors.is_empty(),
5189 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
5190 pkg_errors
5191 );
5192 }
5193
5194 #[test]
5195 fn test_unt_count_correct() {
5196 let segments = vec![
5198 make_segment("UNH", vec![vec!["001"]]),
5199 make_segment("BGM", vec![vec!["E01"]]),
5200 make_segment("DTM", vec![vec!["137", "20250401"]]),
5201 make_segment("UNT", vec![vec!["4", "001"]]),
5202 ];
5203 assert!(
5204 validate_unt_segment_count(&segments).is_none(),
5205 "Correct count should produce no issue"
5206 );
5207 }
5208
5209 #[test]
5210 fn test_unt_count_mismatch() {
5211 let segments = vec![
5213 make_segment("UNH", vec![vec!["001"]]),
5214 make_segment("BGM", vec![vec!["E01"]]),
5215 make_segment("UNT", vec![vec!["5", "001"]]),
5216 ];
5217 let issue =
5218 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
5219 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5220 assert_eq!(issue.severity, Severity::Error);
5221 assert!(narrate(&issue).contains("declared 5"));
5222 assert!(narrate(&issue).contains("actual 3"));
5223 }
5224
5225 #[test]
5226 fn test_unt_count_excludes_envelope() {
5227 let segments = vec![
5229 make_segment("UNA", vec![]),
5230 make_segment("UNB", vec![vec!["UNOC", "3"]]),
5231 make_segment("UNH", vec![vec!["001"]]),
5232 make_segment("BGM", vec![vec!["E01"]]),
5233 make_segment("UNT", vec![vec!["3", "001"]]),
5234 make_segment("UNZ", vec![vec!["1"]]),
5235 ];
5236 assert!(
5237 validate_unt_segment_count(&segments).is_none(),
5238 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
5239 );
5240 }
5241
5242 #[test]
5243 fn test_unt_count_no_unt_returns_none() {
5244 let segments = vec![
5245 make_segment("UNH", vec![vec!["001"]]),
5246 make_segment("BGM", vec![vec!["E01"]]),
5247 ];
5248 assert!(
5249 validate_unt_segment_count(&segments).is_none(),
5250 "No UNT segment should return None (not our problem)"
5251 );
5252 }
5253
5254 #[test]
5255 fn test_unt_count_rejects_multi_message_input() {
5256 let segments = vec![
5258 make_segment("UNH", vec![vec!["001"]]),
5259 make_segment("BGM", vec![vec!["E01"]]),
5260 make_segment("UNT", vec![vec!["3", "001"]]),
5261 make_segment("UNH", vec![vec!["002"]]),
5262 make_segment("BGM", vec![vec!["E02"]]),
5263 make_segment("UNT", vec![vec!["3", "002"]]),
5264 ];
5265 let issue = validate_unt_segment_count(&segments)
5266 .expect("Multi-message input should produce an error");
5267 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5268 assert!(
5269 narrate(&issue).contains("2 UNH"),
5270 "Should mention UNH count: {}",
5271 narrate(&issue)
5272 );
5273 }
5274
5275 #[test]
5276 fn test_code_validation_accepts_multi_code_variant_qualifier() {
5277 let evaluator = MockEvaluator::new(vec![]);
5284 let validator = EdifactValidator::new(evaluator);
5285 let external = NoOpExternalProvider;
5286
5287 let rff_z39 = OwnedSegment {
5288 id: "RFF".to_string(),
5289 elements: vec![
5290 vec!["RFF".to_string()],
5291 vec!["Z39".to_string(), "REF1".to_string()],
5292 ],
5293 segment_number: 1,
5294 };
5295
5296 let workflow = AhbWorkflow {
5297 pruefidentifikator: "55035".to_string(),
5298 description: "Test".to_string(),
5299 communication_direction: None,
5300 fields: vec![
5301 AhbFieldRule {
5303 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5304 name: "Referenznummer Qualifier".to_string(),
5305 ahb_status: "Muss".to_string(),
5306 codes: vec![
5307 AhbCodeRule {
5308 value: "Z31".to_string(),
5309 description: "".to_string(),
5310 ahb_status: "X".to_string(),
5311 },
5312 AhbCodeRule {
5313 value: "Z39".to_string(),
5314 description: "".to_string(),
5315 ahb_status: "X".to_string(),
5316 },
5317 ],
5318 parent_group_ahb_status: None,
5319 segment_ahb_status: None,
5320 element_index: Some(1),
5321 component_index: Some(0),
5322 mig_number: Some("00075".to_string()),
5323 format: None,
5324 },
5325 AhbFieldRule {
5327 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5328 name: "Referenznummer Qualifier".to_string(),
5329 ahb_status: "Muss".to_string(),
5330 codes: vec![AhbCodeRule {
5331 value: "Z33".to_string(),
5332 description: "".to_string(),
5333 ahb_status: "X".to_string(),
5334 }],
5335 parent_group_ahb_status: None,
5336 segment_ahb_status: None,
5337 element_index: Some(1),
5338 component_index: Some(0),
5339 mig_number: Some("00078".to_string()),
5340 format: None,
5341 },
5342 ],
5343 ub_definitions: BTreeMap::new(),
5344 };
5345
5346 let report = validator.validate(
5347 &[rff_z39],
5348 &workflow,
5349 &external,
5350 ValidationLevel::Conditions,
5351 );
5352
5353 let code_errors: Vec<_> = report
5354 .by_category(ValidationCategory::Code)
5355 .filter(|i| {
5356 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5357 })
5358 .collect();
5359 assert!(
5360 code_errors.is_empty(),
5361 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
5362 code_errors
5363 );
5364 }
5365
5366 #[test]
5367 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
5368 let evaluator = MockEvaluator::new(vec![]);
5374 let validator = EdifactValidator::new(evaluator);
5375 let external = NoOpExternalProvider;
5376
5377 let pia_5_z12 = OwnedSegment {
5381 id: "PIA".to_string(),
5382 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
5383 segment_number: 1,
5384 };
5385 let pia_5_srw = OwnedSegment {
5386 id: "PIA".to_string(),
5387 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
5388 segment_number: 2,
5389 };
5390
5391 let make_rules = |mig: &str, composite_code: &str| {
5392 vec![
5393 AhbFieldRule {
5394 segment_path: "SG4/SG8/PIA/4347".to_string(),
5395 name: "Produkt-ID-Funktion".to_string(),
5396 ahb_status: "Muss".to_string(),
5397 codes: vec![AhbCodeRule {
5398 value: "5".to_string(),
5399 description: "".to_string(),
5400 ahb_status: "X".to_string(),
5401 }],
5402 parent_group_ahb_status: None,
5403 segment_ahb_status: None,
5404 element_index: Some(0),
5405 component_index: Some(0),
5406 mig_number: Some(mig.to_string()),
5407 format: None,
5408 },
5409 AhbFieldRule {
5410 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
5411 name: "Artikel/Dienstleistung-Identifikator".to_string(),
5412 ahb_status: "Muss".to_string(),
5413 codes: vec![AhbCodeRule {
5414 value: composite_code.to_string(),
5415 description: "".to_string(),
5416 ahb_status: "X".to_string(),
5417 }],
5418 parent_group_ahb_status: None,
5419 segment_ahb_status: None,
5420 element_index: Some(1),
5421 component_index: Some(0),
5422 mig_number: Some(mig.to_string()),
5423 format: None,
5424 },
5425 ]
5426 };
5427
5428 let mut fields = make_rules("00108", "Z12");
5429 fields.extend(make_rules("00197", "SRW"));
5430
5431 let workflow = AhbWorkflow {
5432 pruefidentifikator: "55035".to_string(),
5433 description: "Test".to_string(),
5434 communication_direction: None,
5435 fields,
5436 ub_definitions: BTreeMap::new(),
5437 };
5438
5439 let report = validator.validate(
5440 &[pia_5_z12, pia_5_srw],
5441 &workflow,
5442 &external,
5443 ValidationLevel::Conditions,
5444 );
5445
5446 let code_errors: Vec<_> = report
5447 .by_category(ValidationCategory::Code)
5448 .filter(|i| {
5449 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5450 })
5451 .collect();
5452 assert!(
5453 code_errors.is_empty(),
5454 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
5455 code_errors
5456 );
5457 }
5458}