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 package_conditions: validated_tree.package_conditions.clone(),
349 };
350 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
351 self.validate_answer_lists(&synthetic_workflow, &ctx, &mut report);
352 self.validate_package_cardinality(&synthetic_workflow, &ctx, &mut report);
353 }
354
355 report
356 }
357
358 fn validate_conditions(
360 &self,
361 workflow: &AhbWorkflow,
362 ctx: &EvaluationContext,
363 report: &mut ValidationReport,
364 ) {
365 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
366
367 for field in &workflow.fields {
368 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
370 continue;
371 }
372
373 let StatusResolution {
376 result: condition_result,
377 kind,
378 unknown_ids,
379 } = expr_eval.resolve_status(&field.ahb_status, ctx, &workflow.ub_definitions);
380 let mandatory = kind == Some(StatusKind::Muss);
381
382 match condition_result {
383 ConditionResult::True => {
384 if mandatory
386 && !is_field_present(ctx, field)
387 && !is_group_variant_absent(
388 ctx,
389 field,
390 is_parent_group_required(
391 field,
392 &expr_eval,
393 ctx,
394 &workflow.ub_definitions,
395 ),
396 )
397 {
398 let mut issue = ValidationIssue::new(
399 Severity::Error,
400 IssueKind::MissingRequiredField {
401 field_name: field.name.clone(),
402 },
403 )
404 .with_field_path(&field.segment_path)
405 .with_rule(&field.ahb_status);
406 if let Some(first_code) = field.codes.first() {
410 issue.expected_value = Some(first_code.value.clone());
411 }
412 report.add_issue(issue);
413 }
414 }
415 ConditionResult::False => {
416 if mandatory && is_field_present(ctx, field) {
422 report.add_issue(
423 ValidationIssue::new(
424 Severity::Error,
425 IssueKind::FieldConditionNotSatisfied {
426 field_name: field.name.clone(),
427 },
428 )
429 .with_field_path(&field.segment_path)
430 .with_rule(&field.ahb_status),
431 );
432 }
433 }
434 ConditionResult::Unknown => {
435 let mut external_ids = Vec::new();
440 let mut undetermined_ids = Vec::new();
441 let mut missing_ids = Vec::new();
442 for id in unknown_ids {
443 if self.evaluator.is_external(id) {
444 external_ids.push(id);
445 } else if self.evaluator.is_known(id) {
446 undetermined_ids.push(id);
447 } else {
448 missing_ids.push(id);
449 }
450 }
451
452 report.add_issue(
453 ValidationIssue::new(
454 Severity::Info,
455 IssueKind::ConditionUnknown {
456 field_name: field.name.clone(),
457 unresolved: UnresolvedConditions {
458 external: external_ids,
459 undetermined: undetermined_ids,
460 missing: missing_ids,
461 },
462 },
463 )
464 .with_field_path(&field.segment_path)
465 .with_rule(&field.ahb_status),
466 );
467 }
468 }
469 }
470
471 self.validate_codes_cross_field(workflow, ctx, report);
476 self.validate_answer_lists(workflow, ctx, report);
477
478 self.validate_package_cardinality(workflow, ctx, report);
481 }
482
483 fn validate_package_cardinality(
490 &self,
491 workflow: &AhbWorkflow,
492 ctx: &EvaluationContext,
493 report: &mut ValidationReport,
494 ) {
495 struct PackageGroup {
498 min: u32,
499 max: u32,
500 max_sum: u32,
503 bounds: BTreeSet<(u32, u32)>,
504 code_values: Vec<String>,
505 element_index: usize,
506 component_index: usize,
507 }
508
509 let mut groups: HashMap<(String, Option<String>, u32, u32, u32), PackageGroup> =
518 HashMap::new();
519 let mut alternatives: HashMap<(String, Option<String>, u32), BTreeSet<u32>> =
523 HashMap::new();
524
525 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
526
527 for field in &workflow.fields {
528 let el_idx = field.element_index.unwrap_or(0);
529 let comp_idx = field.component_index.unwrap_or(0);
530
531 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
533 continue;
534 }
535
536 for code in &field.codes {
537 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
539 let mut packages = Vec::new();
541 collect_packages(&expr, &mut packages);
542
543 if packages.len() > 1 {
544 let ids: BTreeSet<u32> = packages.iter().map(|(id, _, _)| *id).collect();
545 for id in &ids {
546 alternatives
547 .entry((field.segment_path.clone(), field.mig_number.clone(), *id))
548 .or_default()
549 .extend(ids.iter().copied().filter(|other| other != id));
550 }
551 }
552
553 for (pkg_id, pkg_min, pkg_max) in packages {
554 let key = (
555 field.segment_path.clone(),
556 field.mig_number.clone(),
557 pkg_id,
558 0,
559 0,
560 );
561 let group = groups.entry(key).or_insert_with(|| PackageGroup {
562 min: pkg_min,
563 max: pkg_max,
564 max_sum: 0,
565 bounds: BTreeSet::new(),
566 code_values: Vec::new(),
567 element_index: el_idx,
568 component_index: comp_idx,
569 });
570 group.bounds.insert((pkg_min, pkg_max));
577 group.min = group.min.min(pkg_min);
578 if !group.code_values.contains(&code.value) {
579 group.max_sum = group.max_sum.saturating_add(pkg_max);
580 }
581 group.max = group.max.max(pkg_max);
582 group.code_values.push(code.value.clone());
583 }
584 }
585 }
586 }
587
588 let mut field_packages: HashMap<(String, Option<String>), BTreeSet<u32>> = HashMap::new();
599 for (seg_path, mig_number, pkg_id, _, _) in groups.keys() {
600 field_packages
601 .entry((seg_path.clone(), mig_number.clone()))
602 .or_default()
603 .insert(*pkg_id);
604 }
605 let with_alternatives: Vec<(String, Option<String>)> = alternatives
606 .keys()
607 .map(|(p, m, _)| (p.clone(), m.clone()))
608 .collect();
609 for field in with_alternatives {
610 let Some(ids) = field_packages.get(&field) else {
611 continue;
612 };
613 for id in ids {
614 alternatives
615 .entry((field.0.clone(), field.1.clone(), *id))
616 .or_default()
617 .extend(ids.iter().copied().filter(|other| other != id));
618 }
619 }
620
621 type Counted = (Vec<usize>, Vec<bool>, usize, usize, Vec<String>);
623 let mut counted: HashMap<(String, Option<String>, u32, u32, u32), Counted> = HashMap::new();
624 for ((seg_path, mig_number, pkg_id, _, _), group) in &groups {
625 let segment_id = extract_segment_id(seg_path);
626
627 let mut unique_codes: Vec<&str> =
631 group.code_values.iter().map(|s| s.as_str()).collect();
632 unique_codes.sort_unstable();
633 unique_codes.dedup();
634 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
635
636 let group_path_str = extract_group_path_key(seg_path);
637 let group_path: Vec<&str> = if group_path_str.is_empty() {
638 Vec::new()
639 } else {
640 group_path_str.split('/').collect()
641 };
642
643 let min = group.min as usize;
644 let max = if group.bounds.len() > 1 {
645 group.max_sum as usize
646 } else {
647 group.max as usize
648 };
649
650 type Instance<'p> = (usize, Option<Vec<(&'p str, usize)>>);
653 let per_instance_counts: Option<Vec<Instance>> =
654 match (ctx.navigator, group_path.is_empty()) {
655 (Some(nav), false)
663 if group_path.len() > 1
664 && !full_instance_paths(nav, &group_path).is_empty() =>
665 {
666 let paths = full_instance_paths(nav, &group_path);
667 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
675 .as_deref()
676 {
677 Some(m) => (0..group_path.len())
678 .filter_map(|level| {
679 let prefix = group_path[..=level].join("/");
680 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
681 })
682 .collect(),
683 None => Vec::new(),
684 };
685 if paths.is_empty() {
686 None
687 } else {
688 Some(
689 paths
690 .into_iter()
691 .filter(|path| match mig_number.as_deref() {
692 Some(m) => nav.instance_at_has_mig_number(path, m),
693 None => true,
694 })
695 .filter(|path| {
696 variant_entries.iter().all(|(level, entry)| {
697 nav.find_segments_in_instance(
698 &entry.tag,
699 &path[..=*level],
700 )
701 .first()
702 .and_then(|seg| {
703 seg.elements
704 .get(entry.element)
705 .and_then(|e| e.get(entry.component))
706 .cloned()
707 })
708 .map_or(true, |v| entry.codes.contains(v.as_str()))
709 })
710 })
711 .map(|path| {
712 let count = nav
713 .find_segments_in_instance(&segment_id, &path)
714 .iter()
715 .filter_map(|seg| {
716 seg.elements
717 .get(group.element_index)
718 .and_then(|e| e.get(group.component_index))
719 .filter(|v| !v.is_empty())
720 .cloned()
721 })
722 .filter(|v| code_set.contains(v.as_str()))
723 .count();
724 (count, Some(path))
725 })
726 .collect(),
727 )
728 }
729 }
730 (Some(nav), false) => {
731 let instance_count = nav.group_instance_count(&group_path);
732 if instance_count == 0 {
733 None
734 } else {
735 Some(
743 (0..instance_count)
744 .filter(|i| match mig_number.as_deref() {
745 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
746 None => true,
747 })
748 .map(|i| {
749 let count = nav
750 .find_segments_in_group(&segment_id, &group_path, i)
751 .iter()
752 .filter_map(|seg| {
753 seg.elements
754 .get(group.element_index)
755 .and_then(|e| e.get(group.component_index))
756 .filter(|v| !v.is_empty())
757 .cloned()
758 })
759 .filter(|v| code_set.contains(v.as_str()))
760 .count();
761 let path = (group_path.len() == 1)
763 .then(|| vec![(group_path[0], i)]);
764 (count, path)
765 })
766 .collect(),
767 )
768 }
769 }
770 _ => None,
771 };
772
773 let instances = per_instance_counts.unwrap_or_else(|| {
774 let segments = ctx.find_segments(&segment_id);
775 let count = segments
776 .iter()
777 .filter_map(|seg| {
778 seg.elements
779 .get(group.element_index)
780 .and_then(|e| e.get(group.component_index))
781 .filter(|v| !v.is_empty())
782 .map(|s| s.as_str())
783 })
784 .filter(|v| code_set.contains(v))
785 .count();
786 vec![(count, None)]
787 });
788 let applies: Vec<bool> = instances
791 .iter()
792 .map(|(_, path)| match workflow.package_conditions.get(pkg_id) {
793 None => true,
794 Some(condition) => {
795 let result = match path {
796 Some(path) => expr_eval.evaluate(
797 condition,
798 &ctx.with_scope(crate::eval::GroupScope { path }),
799 ),
800 None => expr_eval.evaluate(condition, ctx),
801 };
802 result == ConditionResult::True
803 }
804 })
805 .collect();
806 let counts: Vec<usize> = instances.iter().map(|(c, _)| *c).collect();
807
808 counted.insert(
809 (
810 seg_path.clone(),
811 mig_number.clone(),
812 *pkg_id,
813 group.min,
814 group.max,
815 ),
816 (
817 counts,
818 applies,
819 min,
820 max,
821 unique_codes.iter().map(|s| s.to_string()).collect(),
822 ),
823 );
824 }
825
826 let mut keys: Vec<&(String, Option<String>, u32, u32, u32)> = counted.keys().collect();
831 keys.sort();
832 for key in keys {
833 let (counts, applies, min, max, codes) = &counted[key];
834 let (seg_path, mig_number, pkg_id, _, _) = key;
835 let holds = |c: &[usize], a: &[bool], lo: usize, hi: usize, i: usize| {
836 c.len() == counts.len() && a[i] && (lo..=hi).contains(&c[i]) && c[i] > 0
837 };
838 let alternative_holds = |i: usize| {
839 alternatives
840 .get(&(seg_path.clone(), mig_number.clone(), *pkg_id))
841 .is_some_and(|alts| {
842 counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
843 p == seg_path
844 && m == mig_number
845 && alts.contains(id)
846 && holds(c, a, *lo, *hi, i)
847 })
848 })
849 };
850 let applicable_alternative_holds = |i: usize| {
855 *min > 0
856 && counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
857 p == seg_path
858 && m == mig_number
859 && id != pkg_id
860 && *lo > 0
861 && holds(c, a, *lo, *hi, i)
862 })
863 };
864 let mut reported_counts: HashSet<usize> = HashSet::new();
865 for (i, &present_count) in counts.iter().enumerate() {
866 if applies[i]
867 && (present_count < *min || present_count > *max)
868 && !alternative_holds(i)
869 && !(present_count < *min && applicable_alternative_holds(i))
870 && reported_counts.insert(present_count)
871 {
872 report.add_issue(
873 ValidationIssue::new(
874 Severity::Error,
875 IssueKind::PackageCardinality {
876 package_id: pkg_id.to_string(),
877 present: present_count,
878 min: *min,
879 max: *max,
880 codes: codes.clone(),
881 },
882 )
883 .with_field_path(seg_path)
884 .with_expected(format!("{min}..{max}"))
885 .with_actual(present_count.to_string()),
886 );
887 }
888 }
889 }
890 }
891
892 fn validate_codes_cross_field(
904 &self,
905 workflow: &AhbWorkflow,
906 ctx: &EvaluationContext,
907 report: &mut ValidationReport,
908 ) {
909 if ctx.navigator.is_some() {
910 self.validate_codes_group_scoped(workflow, ctx, report);
911 } else {
912 self.validate_codes_tag_scoped(workflow, ctx, report);
913 }
914 }
915
916 fn validate_codes_group_scoped(
919 &self,
920 workflow: &AhbWorkflow,
921 ctx: &EvaluationContext,
922 report: &mut ValidationReport,
923 ) {
924 let by_loc = partition_codes_by_mig(workflow);
925 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
926 let nav = ctx.navigator.unwrap();
927
928 for ((group_key, tag), migs) in &by_loc {
929 let field_path = if group_key.is_empty() {
930 format!("{tag}/qualifier")
931 } else {
932 format!("{group_key}/{tag}/qualifier")
933 };
934
935 let group_path: Vec<&str> = if group_key.is_empty() {
936 Vec::new()
937 } else {
938 group_key.split('/').collect()
939 };
940
941 let tag_qualifiers = known_qualifiers.get(tag);
942
943 if group_path.is_empty() {
944 Self::validate_segments_per_mig(
945 &ctx.find_segments(tag),
946 migs,
947 tag_qualifiers,
948 tag,
949 &field_path,
950 report,
951 );
952 } else {
953 let instance_count = nav.group_instance_count(&group_path);
954 for i in 0..instance_count {
955 let owned = nav.find_segments_in_group(tag, &group_path, i);
956 let refs: Vec<&OwnedSegment> = owned.iter().collect();
957 Self::validate_segments_per_mig(
958 &refs,
959 migs,
960 tag_qualifiers,
961 tag,
962 &field_path,
963 report,
964 );
965 }
966 }
967 }
968 }
969
970 fn validate_codes_tag_scoped(
974 &self,
975 workflow: &AhbWorkflow,
976 ctx: &EvaluationContext,
977 report: &mut ValidationReport,
978 ) {
979 let by_loc = partition_codes_by_mig(workflow);
980 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
981 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
983 for ((_group_key, tag), migs) in by_loc {
984 let merged = by_tag.entry(tag).or_default();
985 for (mig_key, bucket) in migs {
986 let entry = merged.entry(mig_key).or_default();
987 if entry.qualifier_position.is_none() {
988 entry.qualifier_position = bucket.qualifier_position;
989 }
990 if entry.qualifier_position == bucket.qualifier_position {
991 entry.qualifier_values.extend(&bucket.qualifier_values);
992 }
993 for (pos, codes) in bucket.codes {
994 entry.codes.entry(pos).or_default().extend(codes);
995 }
996 }
997 }
998
999 for (tag, migs) in &by_tag {
1000 let field_path = format!("{tag}/qualifier");
1001 Self::validate_segments_per_mig(
1002 &ctx.find_segments(tag),
1003 migs,
1004 known_qualifiers.get(tag),
1005 tag,
1006 &field_path,
1007 report,
1008 );
1009 }
1010 }
1011
1012 fn validate_segments_per_mig(
1021 segments: &[&OwnedSegment],
1022 migs: &HashMap<Option<String>, MigCodeBucket>,
1023 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
1024 tag: &str,
1025 field_path: &str,
1026 report: &mut ValidationReport,
1027 ) {
1028 for seg in segments {
1029 match match_segment_to_mig(seg, migs) {
1030 Some(bucket) => {
1031 for ((el, c), allowed) in &bucket.codes {
1032 if allowed.is_empty() {
1033 continue;
1034 }
1035 Self::check_segments_against_codes(
1036 vec![*seg],
1037 allowed,
1038 tag,
1039 *el,
1040 *c,
1041 field_path,
1042 report,
1043 );
1044 }
1045 }
1046 None => {
1047 let Some(qualifiers) = tag_qualifiers else {
1048 continue;
1049 };
1050 for ((el, c), allowed) in qualifiers {
1055 let Some(actual) = seg
1056 .elements
1057 .get(*el)
1058 .and_then(|e| e.get(*c))
1059 .filter(|v| !v.is_empty())
1060 .map(|s| s.as_str())
1061 else {
1062 continue;
1063 };
1064 if allowed.iter().any(|v| v == actual) {
1065 break;
1067 }
1068 let bucket_values: HashSet<&str> = migs
1071 .values()
1072 .filter(|b| b.qualifier_position == Some((*el, *c)))
1073 .flat_map(|b| b.qualifier_values.iter().copied())
1074 .collect();
1075 if !bucket_values.is_empty() {
1076 Self::check_segments_against_codes(
1077 vec![*seg],
1078 &bucket_values,
1079 tag,
1080 *el,
1081 *c,
1082 field_path,
1083 report,
1084 );
1085 break;
1086 }
1087 }
1088 }
1089 }
1090 }
1091 }
1092
1093 fn validate_answer_lists(
1102 &self,
1103 workflow: &AhbWorkflow,
1104 ctx: &EvaluationContext,
1105 report: &mut ValidationReport,
1106 ) {
1107 let lists = AnswerCodeLists::embedded();
1108 let fv = self.evaluator.format_version();
1109 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1110 for list_rule in &workflow.fields {
1111 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1112 continue;
1113 };
1114 if list_rule.codes.is_empty()
1115 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1116 {
1117 continue;
1118 }
1119 let Some(answer_rule) = workflow
1120 .fields
1121 .iter()
1122 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1123 else {
1124 continue;
1125 };
1126 let at = |rule: &AhbFieldRule| {
1127 (
1128 rule.element_index.unwrap_or(0),
1129 rule.component_index.unwrap_or(0),
1130 )
1131 };
1132 let (list_el, list_c) = at(list_rule);
1133 let (answer_el, answer_c) = at(answer_rule);
1134 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1135 seg.elements
1136 .get(el)
1137 .and_then(|e| e.get(c))
1138 .filter(|v| !v.is_empty())
1139 .cloned()
1140 };
1141 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1142 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1143 continue;
1144 }
1145 let (Some(list), Some(answer)) =
1146 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1147 else {
1148 continue;
1149 };
1150 if !list_rule.codes.iter().any(|c| c.value == list) {
1151 continue;
1152 }
1153 if answer_rule.codes.iter().any(|c| c.value == answer) {
1157 continue;
1158 }
1159 let Some(codes) = lists.codes(fv, &list) else {
1160 continue;
1161 };
1162 if codes.contains(&answer) {
1163 continue;
1164 }
1165 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1166 allowed.sort_unstable();
1167 report.add_issue(
1168 ValidationIssue::new(
1169 Severity::Warning,
1170 IssueKind::AnswerNotInList {
1171 answer: answer.clone(),
1172 list: list.clone(),
1173 allowed: allowed.clone(),
1174 },
1175 )
1176 .with_field_path(&answer_rule.segment_path)
1177 .with_actual(&answer)
1178 .with_expected(allowed.join(", ")),
1179 );
1180 }
1181 }
1182 }
1183
1184 fn check_segments_against_codes(
1186 segments: Vec<&OwnedSegment>,
1187 allowed_codes: &HashSet<&str>,
1188 _tag: &str,
1189 el_idx: usize,
1190 comp_idx: usize,
1191 field_path: &str,
1192 report: &mut ValidationReport,
1193 ) {
1194 for segment in segments {
1195 if let Some(code_value) = segment
1196 .elements
1197 .get(el_idx)
1198 .and_then(|e| e.get(comp_idx))
1199 .filter(|v| !v.is_empty())
1200 {
1201 if !allowed_codes.contains(code_value.as_str()) {
1202 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1203 sorted_codes.sort_unstable();
1204 let kind = IssueKind::CodeNotAllowedForPid {
1205 value: code_value.to_string(),
1206 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1207 };
1208 report.add_issue(
1209 ValidationIssue::new(Severity::Error, kind)
1210 .with_field_path(field_path)
1211 .with_actual(code_value)
1212 .with_expected(sorted_codes.join(", ")),
1213 );
1214 }
1215 }
1216 }
1217 }
1218}
1219
1220fn full_instance_paths<'p>(
1223 nav: &dyn GroupNavigator,
1224 group_path: &[&'p str],
1225) -> Vec<Vec<(&'p str, usize)>> {
1226 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1227 for g in group_path {
1228 paths = paths
1229 .into_iter()
1230 .flat_map(|parent| {
1231 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1232 let mut path = parent.clone();
1233 path.push((*g, i));
1234 path
1235 })
1236 })
1237 .collect();
1238 }
1239 paths
1240}
1241
1242struct VariantEntry<'a> {
1245 tag: String,
1246 element: usize,
1247 component: usize,
1248 codes: HashSet<&'a str>,
1249}
1250
1251fn merged_variant_entry<'a>(
1257 workflow: &'a AhbWorkflow,
1258 group_path: &str,
1259 mig: &str,
1260) -> Option<VariantEntry<'a>> {
1261 let mig: u32 = mig.parse().ok()?;
1262 let prefix = format!("{group_path}/");
1263 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1265 .fields
1266 .iter()
1267 .filter_map(|r| {
1268 let rest = r.segment_path.strip_prefix(&prefix)?;
1269 let tag = rest.split('/').next()?;
1270 if tag.starts_with("SG") {
1271 return None;
1272 }
1273 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1274 })
1275 .collect();
1276 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1278 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1279 .iter()
1280 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1281 .collect();
1282 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1283 if numbers.len() < 2 {
1284 return None;
1285 }
1286 let own = entries
1287 .iter()
1288 .filter(|(_, n, _)| *n <= mig)
1289 .map(|(_, n, _)| *n)
1290 .max()?;
1291 let (rule, _, tag) = entries
1293 .iter()
1294 .filter(|(_, n, _)| *n == own)
1295 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1296 Some(VariantEntry {
1297 tag: tag.to_string(),
1298 element: rule.element_index.unwrap_or(0),
1299 component: rule.component_index.unwrap_or(0),
1300 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1301 })
1302}
1303
1304fn value_length(format: &str, value: &str) -> usize {
1308 let format = format.trim();
1309 if format.starts_with('n') {
1310 value
1311 .chars()
1312 .filter(|c| !matches!(c, '.' | ',' | '-'))
1313 .count()
1314 } else {
1315 value.chars().count()
1316 }
1317}
1318
1319fn max_length(format: &str) -> Option<usize> {
1322 let digits = format
1323 .trim()
1324 .strip_prefix("an")
1325 .or_else(|| format.trim().strip_prefix('a'))
1326 .or_else(|| format.trim().strip_prefix('n'))?;
1327 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1328}
1329
1330fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1341 let derived_from_codes = field
1342 .codes
1343 .first()
1344 .is_some_and(|c| c.ahb_status == field.ahb_status);
1345 if !derived_from_codes && !codes_are_alternatives(field) {
1346 return &field.ahb_status;
1347 }
1348 value
1349 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1350 .map_or(&field.ahb_status, |c| &c.ahb_status)
1351}
1352
1353fn codes_are_alternatives(field: &AhbFieldRule) -> bool {
1364 !field.codes.is_empty()
1365 && field.ahb_status.contains('[')
1366 && field
1367 .codes
1368 .iter()
1369 .all(|c| c.ahb_status.contains('[') && c.ahb_status != field.ahb_status)
1370}
1371
1372fn should_skip_for_parent_group<E: ConditionEvaluator>(
1378 field: &AhbFieldRule,
1379 expr_eval: &ConditionExprEvaluator<E>,
1380 ctx: &EvaluationContext,
1381 ub_definitions: &BTreeMap<String, ConditionExpr>,
1382) -> bool {
1383 if let Some(ref group_status) = field.parent_group_ahb_status {
1384 if group_status.contains('[') {
1385 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
1386 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
1387 }
1388 }
1389 false
1390}
1391
1392fn is_parent_group_required<E: ConditionEvaluator>(
1399 field: &AhbFieldRule,
1400 expr_eval: &ConditionExprEvaluator<E>,
1401 ctx: &EvaluationContext,
1402 ub_definitions: &BTreeMap<String, ConditionExpr>,
1403) -> bool {
1404 field
1405 .parent_group_ahb_status
1406 .as_deref()
1407 .is_some_and(|status| {
1408 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1409 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1410 })
1411}
1412
1413fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1422 let segment_id = extract_segment_id(&field.segment_path);
1423
1424 if !field.codes.is_empty() {
1430 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1431 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1432 let matching = ctx.find_segments(&segment_id);
1433 return matching.iter().any(|seg| {
1434 seg.elements
1435 .get(el_idx)
1436 .and_then(|e| e.get(comp_idx))
1437 .is_some_and(|v| required_codes.contains(&v.as_str()))
1438 });
1439 }
1440 if is_qualifier_field(&field.segment_path) {
1443 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1444 let el_idx = field.element_index.unwrap_or(0);
1445 let comp_idx = field.component_index.unwrap_or(0);
1446 let matching = ctx.find_segments(&segment_id);
1447 return matching.iter().any(|seg| {
1448 seg.elements
1449 .get(el_idx)
1450 .and_then(|e| e.get(comp_idx))
1451 .is_some_and(|v| required_codes.contains(&v.as_str()))
1452 });
1453 }
1454 }
1455
1456 ctx.has_segment(&segment_id)
1457}
1458
1459fn is_group_variant_absent(
1474 ctx: &EvaluationContext,
1475 field: &AhbFieldRule,
1476 is_group_mandatory: bool,
1477) -> bool {
1478 let group_path: Vec<&str> = field
1479 .segment_path
1480 .split('/')
1481 .take_while(|p| p.starts_with("SG"))
1482 .collect();
1483
1484 if group_path.is_empty() {
1485 return false;
1486 }
1487
1488 let nav = match ctx.navigator {
1489 Some(nav) => nav,
1490 None => return false,
1491 };
1492
1493 let instance_count = nav.group_instance_count(&group_path);
1494
1495 if instance_count == 0 {
1500 if !is_group_mandatory {
1501 return true;
1502 }
1503 return false;
1505 }
1506
1507 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1512 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1514 let segment_id = extract_segment_id(&field.segment_path);
1515 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1516
1517 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1518 nav.find_segments_in_group(&segment_id, &group_path, i)
1519 .iter()
1520 .any(|seg| {
1521 seg.elements
1522 .first()
1523 .and_then(|e| e.first())
1524 .is_some_and(|v| required_codes.contains(&v.as_str()))
1525 })
1526 });
1527
1528 if !any_instance_has_qualifier {
1529 return true; }
1531 }
1532 }
1533
1534 let segment_id = extract_segment_id(&field.segment_path);
1543 let segment_absent_from_all = (0..instance_count).all(|i| {
1544 nav.find_segments_in_group(&segment_id, &group_path, i)
1545 .is_empty()
1546 });
1547 if segment_absent_from_all {
1548 let group_has_other_segments =
1549 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1550 if group_has_other_segments {
1551 return true;
1552 }
1553 }
1554
1555 false
1556}
1557
1558fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1560 out.extend(group.fields.iter());
1561 for child in &group.children {
1562 collect_nodes_depth_first(child, out);
1563 }
1564}
1565
1566#[allow(clippy::too_many_arguments)]
1570fn evaluate_node<E: ConditionEvaluator>(
1571 node: &AhbNode,
1572 ctx: &EvaluationContext,
1573 expr_eval: &ConditionExprEvaluator<E>,
1574 evaluator: &E,
1575 ub_definitions: &BTreeMap<String, ConditionExpr>,
1576 tag_migs: &HashMap<String, HashSet<&str>>,
1577 instance_index: Option<usize>,
1578 slot_holds: bool,
1579 report: &mut ValidationReport,
1580) {
1581 let field = node.rule;
1582
1583 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1585
1586 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1591 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1592 if length > max {
1593 let mut issue = ValidationIssue::new(
1594 Severity::Error,
1595 IssueKind::ValueTooLong {
1596 field_name: field.name.clone(),
1597 length,
1598 max,
1599 },
1600 )
1601 .with_field_path(&field.segment_path)
1602 .with_actual(value)
1603 .with_expected(field.format.clone().unwrap_or_default());
1604 if let Some(idx) = instance_index {
1605 issue = issue.with_instance_index(idx);
1606 }
1607 if let Some(num) = node.matched_segment_number {
1608 issue = issue.with_position(crate::SegmentPosition {
1609 segment_number: num,
1610 byte_offset: 0,
1611 message_number: 1,
1612 });
1613 }
1614 report.add_issue(attach_field_position(issue, field));
1615 }
1616 }
1617
1618 if let (Some(value), Some(format)) = (node.value, field.format.as_deref()) {
1621 if slot_holds
1622 && format.trim().starts_with('n')
1623 && field.codes.is_empty()
1624 && !value.is_empty()
1625 && !crate::eval::format_validators::is_edifact_number(value)
1626 {
1627 let mut issue = ValidationIssue::new(
1628 Severity::Error,
1629 IssueKind::ValueNotNumeric {
1630 field_name: field.name.clone(),
1631 },
1632 )
1633 .with_field_path(&field.segment_path)
1634 .with_actual(value)
1635 .with_expected(format);
1636 if let Some(idx) = instance_index {
1637 issue = issue.with_instance_index(idx);
1638 }
1639 if let Some(num) = node.matched_segment_number {
1640 issue = issue.with_position(crate::SegmentPosition {
1641 segment_number: num,
1642 byte_offset: 0,
1643 message_number: 1,
1644 });
1645 }
1646 report.add_issue(attach_field_position(issue, field));
1647 }
1648 }
1649
1650 if should_skip_for_parent_group(field, expr_eval, ctx, ub_definitions) {
1652 return;
1653 }
1654
1655 let status = governing_status(field, node.value);
1658 let StatusResolution {
1659 result: condition_result,
1660 kind,
1661 unknown_ids,
1662 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1663 let mandatory = kind == Some(StatusKind::Muss);
1664
1665 match condition_result {
1666 ConditionResult::True => {
1667 if mandatory && node.value.is_none() {
1676 let tag = extract_segment_id(&field.segment_path);
1677 let single_variant_present = node.segment_elements.is_none()
1678 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1679 && is_field_present(ctx, field);
1680 let segment_optional_and_absent = node.segment_elements.is_none()
1685 && field
1686 .segment_ahb_status
1687 .as_deref()
1688 .is_some_and(is_optional_segment_status);
1689 let segment_not_required_and_absent = node.segment_elements.is_none()
1695 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1696 expr_eval
1697 .resolve_status(st, &node_ctx, ub_definitions)
1698 .result
1699 != ConditionResult::True
1700 });
1701 if !single_variant_present
1702 && !segment_optional_and_absent
1703 && !segment_not_required_and_absent
1704 {
1705 let mut issue = ValidationIssue::new(
1706 Severity::Error,
1707 IssueKind::MissingRequiredField {
1708 field_name: field.name.clone(),
1709 },
1710 )
1711 .with_field_path(&field.segment_path)
1712 .with_rule(&field.ahb_status);
1713 if let Some(first_code) = field.codes.first() {
1714 issue.expected_value = Some(first_code.value.clone());
1715 }
1716 if let Some(idx) = instance_index {
1717 issue = issue.with_instance_index(idx);
1718 }
1719 if let Some(num) = node.matched_segment_number {
1720 issue = issue.with_position(crate::SegmentPosition {
1721 segment_number: num,
1722 byte_offset: 0,
1723 message_number: 1,
1724 });
1725 }
1726 issue = attach_field_position(issue, field);
1727 report.add_issue(issue);
1728 }
1729 }
1730 }
1731 ConditionResult::False => {
1732 if mandatory && node.value.is_some() {
1735 let mut issue = ValidationIssue::new(
1736 Severity::Error,
1737 IssueKind::FieldConditionNotSatisfied {
1738 field_name: field.name.clone(),
1739 },
1740 )
1741 .with_field_path(&field.segment_path)
1742 .with_rule(&field.ahb_status);
1743 if let Some(idx) = instance_index {
1744 issue = issue.with_instance_index(idx);
1745 }
1746 if let Some(num) = node.matched_segment_number {
1747 issue = issue.with_position(crate::SegmentPosition {
1748 segment_number: num,
1749 byte_offset: 0,
1750 message_number: 1,
1751 });
1752 }
1753 issue = attach_field_position(issue, field);
1754 report.add_issue(issue);
1755 }
1756 }
1757 ConditionResult::Unknown => {
1758 let mut external_ids = Vec::new();
1760 let mut undetermined_ids = Vec::new();
1761 let mut missing_ids = Vec::new();
1762 for id in unknown_ids {
1763 if evaluator.is_external(id) {
1764 external_ids.push(id);
1765 } else if evaluator.is_known(id) {
1766 undetermined_ids.push(id);
1767 } else {
1768 missing_ids.push(id);
1769 }
1770 }
1771
1772 let mut issue = ValidationIssue::new(
1773 Severity::Info,
1774 IssueKind::ConditionUnknown {
1775 field_name: field.name.clone(),
1776 unresolved: UnresolvedConditions {
1777 external: external_ids,
1778 undetermined: undetermined_ids,
1779 missing: missing_ids,
1780 },
1781 },
1782 )
1783 .with_field_path(&field.segment_path)
1784 .with_rule(&field.ahb_status);
1785 if let Some(idx) = instance_index {
1786 issue = issue.with_instance_index(idx);
1787 }
1788 if let Some(num) = node.matched_segment_number {
1789 issue = issue.with_position(crate::SegmentPosition {
1790 segment_number: num,
1791 byte_offset: 0,
1792 message_number: 1,
1793 });
1794 }
1795 issue = attach_field_position(issue, field);
1796 report.add_issue(issue);
1797 }
1798 }
1799}
1800
1801fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
1806 match field.element_index {
1807 Some(el) => {
1808 let element_pos = (el as u32) + 2;
1809 let component_pos = field.component_index.map(|c| (c as u32) + 1);
1810 issue.with_field_position(element_pos, component_pos)
1811 }
1812 None => issue,
1813 }
1814}
1815
1816fn report_repeated_in_transaction(
1823 transaction: &AhbGroupNode,
1824 transaction_index: usize,
1825 once: u32,
1826 ctx: &EvaluationContext,
1827 report: &mut ValidationReport,
1828) {
1829 let mut counts: BTreeMap<(String, String), (usize, String)> = BTreeMap::new();
1831 fn visit<'a>(
1832 node: &'a AhbGroupNode<'a>,
1833 path: &mut Vec<(&'a str, usize)>,
1834 once: u32,
1835 ctx: &EvaluationContext,
1836 counts: &mut BTreeMap<(String, String), (usize, String)>,
1837 ) {
1838 let ids: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1839 let mut seen: HashSet<Option<&str>> = HashSet::new();
1840 for n in &node.fields {
1841 let rule = n.rule;
1842 let Some(status) = rule.segment_ahb_status.as_deref() else {
1843 continue;
1844 };
1845 if Some(status) == node.ahb_status
1846 || rule.element_index.unwrap_or(0) != 0
1847 || rule.component_index.unwrap_or(0) != 0
1848 || rule.codes.is_empty()
1849 || !crate::eval::binds_once(status, once)
1850 || !seen.insert(rule.mig_number.as_deref())
1851 {
1852 continue;
1853 }
1854 let tag = extract_segment_id(&rule.segment_path);
1855 let sent = ctx.navigator().map_or(0, |nav| {
1856 nav.find_segments_in_instance(&tag, path)
1857 .iter()
1858 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
1859 .count()
1860 });
1861 let entry = counts
1862 .entry((
1863 format!("{}/{tag}", ids.join("/")),
1864 rule.mig_number.clone().unwrap_or_default(),
1865 ))
1866 .or_insert((0, status.to_string()));
1867 entry.0 += sent;
1868 }
1869 for child in &node.children {
1870 if let Some(status) = child
1871 .ahb_status
1872 .filter(|s| crate::eval::binds_once(s, once))
1873 {
1874 let variant = child
1875 .fields
1876 .first()
1877 .and_then(|n| n.rule.mig_number.clone())
1878 .unwrap_or_default();
1879 let entry = counts
1880 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
1881 .or_insert((0, status.to_string()));
1882 entry.0 += 1;
1883 }
1884 path.push((child.group_id, child.instance_index));
1885 visit(child, path, once, ctx, counts);
1886 path.pop();
1887 }
1888 }
1889 let mut path = vec![(transaction.group_id, transaction_index)];
1890 visit(transaction, &mut path, once, ctx, &mut counts);
1891 for ((path, _), (count, status)) in counts {
1892 if count > 1 {
1893 report.add_issue(
1894 ValidationIssue::new(
1895 Severity::Error,
1896 IssueKind::RepeatedInTransaction {
1897 path: path.clone(),
1898 count,
1899 },
1900 )
1901 .with_field_path(path)
1902 .with_rule(status)
1903 .with_instance_index(transaction_index),
1904 );
1905 }
1906 }
1907}
1908
1909fn refuse_segments<E: ConditionEvaluator>(
1915 group: &AhbGroupNode,
1916 path: &[(&str, usize)],
1917 scoped_ctx: &EvaluationContext,
1918 evaluator: &E,
1919 ub_definitions: &BTreeMap<String, ConditionExpr>,
1920 instance_index: usize,
1921 report: &mut ValidationReport,
1922) {
1923 let mut seen: Vec<u32> = Vec::new();
1924 for node in &group.fields {
1925 let rule = node.rule;
1926 let (Some(status), Some(value), Some(number)) = (
1927 rule.segment_ahb_status.as_deref(),
1928 node.value,
1929 node.matched_segment_number,
1930 ) else {
1931 continue;
1932 };
1933 let qualifier =
1936 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
1937 if !qualifier
1938 || seen.contains(&number)
1939 || Some(status) == group.ahb_status
1940 || !rule.codes.iter().any(|c| c.value == value)
1941 {
1942 continue;
1943 }
1944 seen.push(number);
1945 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1946 continue;
1947 }
1948 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1949 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
1950 report.add_issue(
1951 ValidationIssue::new(
1952 Severity::Warning,
1953 IssueKind::SegmentNotAllowed {
1954 segment: segment.join("/"),
1955 },
1956 )
1957 .with_field_path(segment.join("/"))
1958 .with_rule(status)
1959 .with_instance_index(instance_index)
1960 .with_position(crate::SegmentPosition {
1961 segment_number: number,
1962 byte_offset: 0,
1963 message_number: 1,
1964 }),
1965 );
1966 }
1967}
1968
1969#[allow(clippy::too_many_arguments)]
1978fn walk_group_instance<E: ConditionEvaluator>(
1979 group: &AhbGroupNode,
1980 path: &[(&str, usize)],
1981 scoped_ctx: &EvaluationContext,
1982 expr_eval: &ConditionExprEvaluator<E>,
1983 evaluator: &E,
1984 ub_definitions: &BTreeMap<String, ConditionExpr>,
1985 tag_migs: &HashMap<String, HashSet<&str>>,
1986 instance_index: usize,
1987 report_segments: bool,
1988 report: &mut ValidationReport,
1989) {
1990 let mut group_refused = false;
1993 if let Some(status) = group.ahb_status {
1994 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
1995 group_refused = true;
1996 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
1997 let mut issue = ValidationIssue::new(
1998 Severity::Warning,
1999 IssueKind::GroupNotAllowed {
2000 group: group_path.join("/"),
2001 },
2002 )
2003 .with_field_path(group_path.join("/"))
2004 .with_rule(status)
2005 .with_instance_index(instance_index);
2006 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
2007 issue = issue.with_position(crate::SegmentPosition {
2008 segment_number: num,
2009 byte_offset: 0,
2010 message_number: 1,
2011 });
2012 }
2013 report.add_issue(issue);
2014 }
2015 }
2016 if report_segments && !group_refused {
2017 refuse_segments(
2018 group,
2019 path,
2020 scoped_ctx,
2021 evaluator,
2022 ub_definitions,
2023 instance_index,
2024 report,
2025 );
2026 }
2027 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
2031 .fields
2032 .iter()
2033 .filter(|n| {
2034 n.rule.element_index.unwrap_or(0) == 0
2035 && n.rule.component_index.unwrap_or(0) == 0
2036 && !n.rule.codes.is_empty()
2037 })
2038 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
2039 .collect();
2040 let holds = |node: &AhbNode| {
2041 let (Some(codes), Some(elements)) = (
2042 slot_codes.get(&node.rule.mig_number.as_deref()),
2043 node.segment_elements,
2044 ) else {
2045 return true;
2046 };
2047 let qualifier = elements
2048 .first()
2049 .and_then(|e| e.first())
2050 .map(String::as_str)
2051 .unwrap_or_default();
2052 codes.iter().any(|c| c.value == qualifier)
2053 };
2054 for node in &group.fields {
2055 evaluate_node(
2056 node,
2057 scoped_ctx,
2058 expr_eval,
2059 evaluator,
2060 ub_definitions,
2061 tag_migs,
2062 Some(instance_index),
2063 holds(node),
2064 report,
2065 );
2066 }
2067 for child in &group.children {
2068 let mut child_path = path.to_vec();
2069 child_path.push((child.group_id, child.instance_index));
2070 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2071 walk_group_instance(
2072 child,
2073 &child_path,
2074 &child_ctx,
2075 expr_eval,
2076 evaluator,
2077 ub_definitions,
2078 tag_migs,
2079 instance_index,
2080 report_segments,
2081 report,
2082 );
2083 }
2084}
2085
2086fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2088 match expr {
2089 ConditionExpr::Package { id, min, max } => {
2090 out.push((*id, *min, *max));
2091 }
2092 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2093 for e in exprs {
2094 collect_packages(e, out);
2095 }
2096 }
2097 ConditionExpr::Xor(left, right) => {
2098 collect_packages(left, out);
2099 collect_packages(right, out);
2100 }
2101 ConditionExpr::Not(inner) => {
2102 collect_packages(inner, out);
2103 }
2104 ConditionExpr::Ref(_) => {}
2105 }
2106}
2107
2108fn is_optional_segment_status(status: &str) -> bool {
2118 let trimmed = status.trim();
2119 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
2120}
2121
2122fn is_qualifier_field(path: &str) -> bool {
2132 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
2133 matches!(parts.len(), 2 | 3)
2135}
2136
2137#[derive(Default)]
2145struct MigCodeBucket<'a> {
2146 qualifier_position: Option<(usize, usize)>,
2149 qualifier_values: HashSet<&'a str>,
2152 codes: HashMap<(usize, usize), HashSet<&'a str>>,
2155}
2156
2157fn partition_codes_by_mig(
2162 workflow: &AhbWorkflow,
2163) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
2164 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
2165 for field in &workflow.fields {
2166 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2167 continue;
2168 }
2169 if codes_are_alternatives(field) {
2172 continue;
2173 }
2174 let tag = extract_segment_id(&field.segment_path);
2175 let group_key = extract_group_path_key(&field.segment_path);
2176 let mig = field.mig_number.clone();
2177 let el = field.element_index.unwrap_or(0);
2178 let c = field.component_index.unwrap_or(0);
2179
2180 let bucket = out
2181 .entry((group_key, tag))
2182 .or_default()
2183 .entry(mig)
2184 .or_default();
2185
2186 let required: Vec<&str> = field
2193 .codes
2194 .iter()
2195 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2196 .map(|code| code.value.as_str())
2197 .collect();
2198
2199 if !required.is_empty() {
2206 if bucket.qualifier_position.is_none() {
2207 bucket.qualifier_position = Some((el, c));
2208 }
2209 if bucket.qualifier_position == Some((el, c)) {
2210 bucket.qualifier_values.extend(required.iter().copied());
2211 }
2212 }
2213
2214 for v in required {
2215 bucket.codes.entry((el, c)).or_default().insert(v);
2216 }
2217 }
2218 out
2219}
2220
2221fn global_qualifiers_by_tag(
2227 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
2228) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
2229 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
2230 for ((_group, tag), migs) in by_loc {
2231 let tag_entry = out.entry(tag.clone()).or_default();
2232 for bucket in migs.values() {
2233 if let Some(pos) = bucket.qualifier_position {
2234 tag_entry
2235 .entry(pos)
2236 .or_default()
2237 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2238 }
2239 }
2240 }
2241 out
2242}
2243
2244fn match_segment_to_mig<'a, 'b>(
2255 seg: &OwnedSegment,
2256 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
2257) -> Option<&'a MigCodeBucket<'b>> {
2258 let actual_at = |el: usize, c: usize| -> &str {
2259 seg.elements
2260 .get(el)
2261 .and_then(|e| e.get(c))
2262 .map(|s| s.as_str())
2263 .unwrap_or("")
2264 };
2265
2266 let mut best: Option<&MigCodeBucket> = None;
2267 let mut best_matches = 0usize;
2268
2269 for bucket in migs.values() {
2270 let Some((el, c)) = bucket.qualifier_position else {
2271 continue;
2272 };
2273 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2274 continue;
2275 }
2276 let extra_matches = bucket
2277 .codes
2278 .iter()
2279 .filter(|(pos, _)| **pos != (el, c))
2280 .filter(|((e, k), allowed)| {
2281 let v = actual_at(*e, *k);
2282 !v.is_empty() && allowed.contains(v)
2283 })
2284 .count();
2285 if best.is_none() || extra_matches > best_matches {
2286 best = Some(bucket);
2287 best_matches = extra_matches;
2288 }
2289 }
2290 best
2291}
2292
2293fn extract_group_path_key(path: &str) -> String {
2298 let sg_parts: Vec<&str> = path
2299 .split('/')
2300 .take_while(|p| p.starts_with("SG"))
2301 .collect();
2302 sg_parts.join("/")
2303}
2304
2305fn extract_segment_id(path: &str) -> String {
2307 for part in path.split('/') {
2308 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2310 continue;
2311 }
2312 if part.len() >= 3
2314 && part
2315 .chars()
2316 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2317 {
2318 return part.to_string();
2319 }
2320 }
2321 path.split('/').next_back().unwrap_or(path).to_string()
2323}
2324
2325pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2337 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2339 if unh_count > 1 {
2340 return Some(ValidationIssue::new(
2341 Severity::Error,
2342 IssueKind::UntCountNotVerifiable { unh_count },
2343 ));
2344 }
2345
2346 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2348 let declared: usize = unt.get_element(0).parse().ok()?;
2349
2350 let actual = segments
2352 .iter()
2353 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2354 .count();
2355
2356 if declared != actual {
2357 Some(
2358 ValidationIssue::new(
2359 Severity::Error,
2360 IssueKind::UntSegmentCountMismatch { declared, actual },
2361 )
2362 .with_field_path("UNT/0074")
2363 .with_expected(actual.to_string())
2364 .with_actual(declared.to_string()),
2365 )
2366 } else {
2367 None
2368 }
2369}
2370
2371#[cfg(test)]
2372mod tests {
2373
2374 #[test]
2375 fn a_numeric_length_counts_digits_only() {
2376 assert_eq!(value_length("n..15", "-12.50"), 4);
2377 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
2378 assert_eq!(value_length("n5", "55001"), 5);
2379 assert_eq!(value_length("an..3", "Z.1"), 3);
2380 assert_eq!(value_length("an..35", "Müller"), 6);
2381 }
2382 use super::super::codes::ErrorCodes;
2383 use super::super::issue::ValidationCategory;
2384 use super::*;
2385 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2386 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2387 use std::collections::HashMap;
2388
2389 fn narrate(issue: &ValidationIssue) -> String {
2392 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2393 }
2394
2395 struct MockEvaluator {
2397 results: HashMap<u32, CR>,
2398 }
2399
2400 impl MockEvaluator {
2401 fn new(results: Vec<(u32, CR)>) -> Self {
2402 Self {
2403 results: results.into_iter().collect(),
2404 }
2405 }
2406
2407 fn all_true(ids: &[u32]) -> Self {
2408 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2409 }
2410 }
2411
2412 impl ConditionEvaluator for MockEvaluator {
2413 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2414 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2415 }
2416 fn is_external(&self, _condition: u32) -> bool {
2417 false
2418 }
2419 fn message_type(&self) -> &str {
2420 "UTILMD"
2421 }
2422 fn format_version(&self) -> &str {
2423 "FV2510"
2424 }
2425 }
2426
2427 #[test]
2430 fn a_present_code_is_judged_by_its_own_status() {
2431 let code = |value: &str, status: &str| AhbCodeRule {
2433 value: value.to_string(),
2434 description: String::new(),
2435 ahb_status: status.to_string(),
2436 };
2437 let field = AhbFieldRule {
2438 segment_path: "SG4/STS/C556/9013".to_string(),
2439 ahb_status: "X [192]".to_string(),
2440 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2441 ..Default::default()
2442 };
2443 assert_eq!(governing_status(&field, Some("E03")), "X");
2444 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2445 assert_eq!(governing_status(&field, None), "X [192]");
2446 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2447
2448 let own = AhbFieldRule {
2450 ahb_status: "X [931]".to_string(),
2451 codes: vec![code("303", "X")],
2452 ..Default::default()
2453 };
2454 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2455 assert!(!codes_are_alternatives(&own));
2456
2457 let alternatives = AhbFieldRule {
2460 ahb_status: "X [153] ∧ [538]".to_string(),
2461 codes: vec![
2462 code("Z74", "X [103] ∧ [537]"),
2463 code("Z75", "X [103] ∧ [537]"),
2464 ],
2465 ..Default::default()
2466 };
2467 assert!(codes_are_alternatives(&alternatives));
2468 assert_eq!(
2469 governing_status(&alternatives, Some("Z74")),
2470 "X [103] ∧ [537]"
2471 );
2472 assert_eq!(
2473 governing_status(&alternatives, Some("A01")),
2474 "X [153] ∧ [538]"
2475 );
2476 assert_eq!(governing_status(&alternatives, None), "X [153] ∧ [538]");
2477
2478 let restricting = AhbFieldRule {
2480 ahb_status: "X [2] ∨ [3]".to_string(),
2481 codes: vec![code("UNH", "X"), code("UNT", "X")],
2482 ..Default::default()
2483 };
2484 assert!(!codes_are_alternatives(&restricting));
2485 assert_eq!(governing_status(&restricting, Some("UNH")), "X [2] ∨ [3]");
2486 }
2487
2488 #[test]
2489 fn test_extract_segment_id_simple() {
2490 assert_eq!(extract_segment_id("NAD"), "NAD");
2491 }
2492
2493 #[test]
2494 fn test_extract_segment_id_with_sg_prefix() {
2495 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
2496 }
2497
2498 #[test]
2499 fn test_extract_segment_id_nested_sg() {
2500 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
2501 }
2502
2503 #[test]
2506 fn test_validate_missing_mandatory_field() {
2507 let evaluator = MockEvaluator::all_true(&[182, 152]);
2508 let validator = EdifactValidator::new(evaluator);
2509 let external = NoOpExternalProvider;
2510
2511 let workflow = AhbWorkflow {
2512 pruefidentifikator: "11001".to_string(),
2513 description: "Test".to_string(),
2514 communication_direction: None,
2515 fields: vec![AhbFieldRule {
2516 segment_path: "SG2/NAD/C082/3039".to_string(),
2517 name: "MP-ID des MSB".to_string(),
2518 ahb_status: "Muss [182] ∧ [152]".to_string(),
2519 codes: vec![],
2520 parent_group_ahb_status: None,
2521 segment_ahb_status: None,
2522 ..Default::default()
2523 }],
2524 ub_definitions: BTreeMap::new(),
2525 package_conditions: Default::default(),
2526 };
2527
2528 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2530
2531 assert!(!report.is_valid());
2533 let errors: Vec<_> = report.errors().collect();
2534 assert_eq!(errors.len(), 1);
2535 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2536 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
2537 }
2538
2539 #[test]
2540 fn test_validate_condition_false_no_error() {
2541 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
2543 let validator = EdifactValidator::new(evaluator);
2544 let external = NoOpExternalProvider;
2545
2546 let workflow = AhbWorkflow {
2547 pruefidentifikator: "11001".to_string(),
2548 description: "Test".to_string(),
2549 communication_direction: None,
2550 fields: vec![AhbFieldRule {
2551 segment_path: "NAD".to_string(),
2552 name: "Partnerrolle".to_string(),
2553 ahb_status: "Muss [182] ∧ [152]".to_string(),
2554 codes: vec![],
2555 parent_group_ahb_status: None,
2556 segment_ahb_status: None,
2557 ..Default::default()
2558 }],
2559 ub_definitions: BTreeMap::new(),
2560 package_conditions: Default::default(),
2561 };
2562
2563 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2564
2565 assert!(report.is_valid());
2567 }
2568
2569 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
2571 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
2572 let workflow = AhbWorkflow {
2573 pruefidentifikator: "11001".to_string(),
2574 description: "Test".to_string(),
2575 communication_direction: None,
2576 fields: vec![AhbFieldRule {
2577 segment_path: "SG2/NAD/C082/3039".to_string(),
2578 name: "MP-ID".to_string(),
2579 ahb_status: status.to_string(),
2580 ..Default::default()
2581 }],
2582 ub_definitions: BTreeMap::new(),
2583 package_conditions: Default::default(),
2584 };
2585 validator.validate(
2586 &[],
2587 &workflow,
2588 &NoOpExternalProvider,
2589 ValidationLevel::Conditions,
2590 )
2591 }
2592
2593 #[test]
2594 fn a_later_soll_line_alone_does_not_require_the_field() {
2595 let report = validate_absent_field(
2597 "Muss [2119] \r\nSoll [130]",
2598 vec![(2119, CR::False), (130, CR::True)],
2599 );
2600 assert!(
2601 report.is_valid(),
2602 "{:?}",
2603 report.errors().collect::<Vec<_>>()
2604 );
2605 }
2606
2607 #[test]
2608 fn a_later_muss_line_requires_the_field() {
2609 let report = validate_absent_field(
2611 "Soll [47]\r\nMuss [46]",
2612 vec![(47, CR::False), (46, CR::True)],
2613 );
2614 let errors: Vec<_> = report.errors().collect();
2615 assert_eq!(errors.len(), 1);
2616 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2617 }
2618
2619 #[test]
2620 fn a_weaker_unknown_line_does_not_stop_a_muss() {
2621 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
2623 let errors: Vec<_> = report.errors().collect();
2624 assert_eq!(errors.len(), 1);
2625 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2626 }
2627
2628 #[test]
2629 fn a_stricter_unknown_line_leaves_the_status_undecided() {
2630 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
2631 assert!(report.is_valid());
2632 let infos: Vec<_> = report.infos().collect();
2633 assert_eq!(infos.len(), 1, "{infos:?}");
2634 match &infos[0].kind {
2635 IssueKind::ConditionUnknown { unresolved, .. } => {
2636 assert_eq!(unresolved.missing, vec![2119]);
2637 }
2638 other => panic!("expected ConditionUnknown, got {other:?}"),
2639 }
2640 }
2641
2642 #[test]
2643 fn test_validate_condition_unknown_adds_info() {
2644 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
2646 let validator = EdifactValidator::new(evaluator);
2648 let external = NoOpExternalProvider;
2649
2650 let workflow = AhbWorkflow {
2651 pruefidentifikator: "11001".to_string(),
2652 description: "Test".to_string(),
2653 communication_direction: None,
2654 fields: vec![AhbFieldRule {
2655 segment_path: "NAD".to_string(),
2656 name: "Partnerrolle".to_string(),
2657 ahb_status: "Muss [182] ∧ [152]".to_string(),
2658 codes: vec![],
2659 parent_group_ahb_status: None,
2660 segment_ahb_status: None,
2661 ..Default::default()
2662 }],
2663 ub_definitions: BTreeMap::new(),
2664 package_conditions: Default::default(),
2665 };
2666
2667 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2668
2669 assert!(report.is_valid());
2671 let infos: Vec<_> = report.infos().collect();
2672 assert_eq!(infos.len(), 1);
2673 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
2674 }
2675
2676 #[test]
2677 fn test_validate_structure_level_skips_conditions() {
2678 let evaluator = MockEvaluator::all_true(&[182, 152]);
2679 let validator = EdifactValidator::new(evaluator);
2680 let external = NoOpExternalProvider;
2681
2682 let workflow = AhbWorkflow {
2683 pruefidentifikator: "11001".to_string(),
2684 description: "Test".to_string(),
2685 communication_direction: None,
2686 fields: vec![AhbFieldRule {
2687 segment_path: "NAD".to_string(),
2688 name: "Partnerrolle".to_string(),
2689 ahb_status: "Muss [182] ∧ [152]".to_string(),
2690 codes: vec![],
2691 parent_group_ahb_status: None,
2692 segment_ahb_status: None,
2693 ..Default::default()
2694 }],
2695 ub_definitions: BTreeMap::new(),
2696 package_conditions: Default::default(),
2697 };
2698
2699 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
2701
2702 assert!(report.is_valid());
2704 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
2705 }
2706
2707 #[test]
2708 fn test_validate_empty_workflow_no_condition_errors() {
2709 let evaluator = MockEvaluator::all_true(&[]);
2710 let validator = EdifactValidator::new(evaluator);
2711 let external = NoOpExternalProvider;
2712
2713 let empty_workflow = AhbWorkflow {
2714 pruefidentifikator: String::new(),
2715 description: String::new(),
2716 communication_direction: None,
2717 fields: vec![],
2718 ub_definitions: BTreeMap::new(),
2719 package_conditions: Default::default(),
2720 };
2721
2722 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
2723
2724 assert!(report.is_valid());
2725 }
2726
2727 #[test]
2728 fn test_validate_bare_muss_always_required() {
2729 let evaluator = MockEvaluator::new(vec![]);
2730 let validator = EdifactValidator::new(evaluator);
2731 let external = NoOpExternalProvider;
2732
2733 let workflow = AhbWorkflow {
2734 pruefidentifikator: "55001".to_string(),
2735 description: "Test".to_string(),
2736 communication_direction: Some("NB an LF".to_string()),
2737 fields: vec![AhbFieldRule {
2738 segment_path: "SG2/NAD/3035".to_string(),
2739 name: "Partnerrolle".to_string(),
2740 ahb_status: "Muss".to_string(), codes: vec![],
2742 parent_group_ahb_status: None,
2743 segment_ahb_status: None,
2744 ..Default::default()
2745 }],
2746 ub_definitions: BTreeMap::new(),
2747 package_conditions: Default::default(),
2748 };
2749
2750 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2751
2752 assert!(!report.is_valid());
2754 assert_eq!(report.error_count(), 1);
2755 }
2756
2757 #[test]
2758 fn test_validate_x_status_is_mandatory() {
2759 let evaluator = MockEvaluator::new(vec![]);
2760 let validator = EdifactValidator::new(evaluator);
2761 let external = NoOpExternalProvider;
2762
2763 let workflow = AhbWorkflow {
2764 pruefidentifikator: "55001".to_string(),
2765 description: "Test".to_string(),
2766 communication_direction: None,
2767 fields: vec![AhbFieldRule {
2768 segment_path: "DTM".to_string(),
2769 name: "Datum".to_string(),
2770 ahb_status: "X".to_string(),
2771 codes: vec![],
2772 parent_group_ahb_status: None,
2773 segment_ahb_status: None,
2774 ..Default::default()
2775 }],
2776 ub_definitions: BTreeMap::new(),
2777 package_conditions: Default::default(),
2778 };
2779
2780 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2781
2782 assert!(!report.is_valid());
2783 let errors: Vec<_> = report.errors().collect();
2784 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
2785 }
2786
2787 #[test]
2788 fn test_validate_soll_not_mandatory() {
2789 let evaluator = MockEvaluator::new(vec![]);
2790 let validator = EdifactValidator::new(evaluator);
2791 let external = NoOpExternalProvider;
2792
2793 let workflow = AhbWorkflow {
2794 pruefidentifikator: "55001".to_string(),
2795 description: "Test".to_string(),
2796 communication_direction: None,
2797 fields: vec![AhbFieldRule {
2798 segment_path: "DTM".to_string(),
2799 name: "Datum".to_string(),
2800 ahb_status: "Soll".to_string(),
2801 codes: vec![],
2802 parent_group_ahb_status: None,
2803 segment_ahb_status: None,
2804 ..Default::default()
2805 }],
2806 ub_definitions: BTreeMap::new(),
2807 package_conditions: Default::default(),
2808 };
2809
2810 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2811
2812 assert!(report.is_valid());
2814 }
2815
2816 #[test]
2817 fn test_report_includes_metadata() {
2818 let evaluator = MockEvaluator::new(vec![]);
2819 let validator = EdifactValidator::new(evaluator);
2820 let external = NoOpExternalProvider;
2821
2822 let workflow = AhbWorkflow {
2823 pruefidentifikator: "55001".to_string(),
2824 description: String::new(),
2825 communication_direction: None,
2826 fields: vec![],
2827 ub_definitions: BTreeMap::new(),
2828 package_conditions: Default::default(),
2829 };
2830
2831 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
2832
2833 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
2834 assert_eq!(report.level, ValidationLevel::Full);
2835 assert_eq!(report.message_type, "UTILMD");
2836 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
2837 }
2838
2839 #[test]
2840 fn test_validate_with_navigator_returns_report() {
2841 let evaluator = MockEvaluator::all_true(&[]);
2842 let validator = EdifactValidator::new(evaluator);
2843 let external = NoOpExternalProvider;
2844 let nav = crate::eval::NoOpGroupNavigator;
2845
2846 let workflow = AhbWorkflow {
2847 pruefidentifikator: "55001".to_string(),
2848 description: "Test".to_string(),
2849 communication_direction: None,
2850 fields: vec![],
2851 ub_definitions: BTreeMap::new(),
2852 package_conditions: Default::default(),
2853 };
2854
2855 let report = validator.validate_with_navigator(
2856 &[],
2857 &workflow,
2858 &external,
2859 ValidationLevel::Full,
2860 &nav,
2861 );
2862 assert!(report.is_valid());
2863 }
2864
2865 #[test]
2866 fn test_code_validation_composite_paths_valid_codes() {
2867 let evaluator = MockEvaluator::new(vec![]);
2871 let validator = EdifactValidator::new(evaluator);
2872 let external = NoOpExternalProvider;
2873
2874 let unh_segment = OwnedSegment {
2875 id: "UNH".to_string(),
2876 elements: vec![
2877 vec!["ALEXANDE951842".to_string()],
2878 vec![
2879 "UTILMD".to_string(),
2880 "D".to_string(),
2881 "11A".to_string(),
2882 "UN".to_string(),
2883 "S2.1".to_string(),
2884 ],
2885 ],
2886 segment_number: 1,
2887 };
2888
2889 let workflow = AhbWorkflow {
2890 pruefidentifikator: "55001".to_string(),
2891 description: "Test".to_string(),
2892 communication_direction: None,
2893 fields: vec![
2894 AhbFieldRule {
2895 segment_path: "UNH/S009/0065".to_string(),
2896 name: "Nachrichtentyp".to_string(),
2897 ahb_status: "X".to_string(),
2898 codes: vec![AhbCodeRule {
2899 value: "UTILMD".to_string(),
2900 description: "Stammdaten".to_string(),
2901 ahb_status: "X".to_string(),
2902 }],
2903 parent_group_ahb_status: None,
2904 segment_ahb_status: None,
2905 element_index: Some(1),
2906 component_index: Some(0),
2907 ..Default::default()
2908 },
2909 AhbFieldRule {
2910 segment_path: "UNH/S009/0052".to_string(),
2911 name: "Version".to_string(),
2912 ahb_status: "X".to_string(),
2913 codes: vec![AhbCodeRule {
2914 value: "D".to_string(),
2915 description: "Draft".to_string(),
2916 ahb_status: "X".to_string(),
2917 }],
2918 parent_group_ahb_status: None,
2919 segment_ahb_status: None,
2920 element_index: Some(1),
2921 component_index: Some(1),
2922 ..Default::default()
2923 },
2924 ],
2925 ub_definitions: BTreeMap::new(),
2926 package_conditions: Default::default(),
2927 };
2928
2929 let report = validator.validate(
2930 &[unh_segment],
2931 &workflow,
2932 &external,
2933 ValidationLevel::Conditions,
2934 );
2935
2936 let code_errors: Vec<_> = report
2937 .by_category(ValidationCategory::Code)
2938 .filter(|i| i.severity == Severity::Error)
2939 .collect();
2940 assert!(
2941 code_errors.is_empty(),
2942 "Expected no code errors when composite values match allowed codes, got: {:?}",
2943 code_errors
2944 );
2945 }
2946
2947 #[test]
2948 fn test_code_validation_partitions_by_mig_number() {
2949 let evaluator = MockEvaluator::new(vec![]);
2953 let validator = EdifactValidator::new(evaluator);
2954 let external = NoOpExternalProvider;
2955
2956 let sts_7 = OwnedSegment {
2957 id: "STS".to_string(),
2958 elements: vec![
2959 vec!["7".to_string()],
2960 vec![String::new()],
2961 vec!["GH02".to_string()],
2962 vec!["ZW4".to_string()],
2963 ],
2964 segment_number: 1,
2965 };
2966 let sts_e01 = OwnedSegment {
2967 id: "STS".to_string(),
2968 elements: vec![
2969 vec!["E01".to_string()],
2970 vec![String::new()],
2971 vec!["A99".to_string(), "E_0614".to_string()],
2972 ],
2973 segment_number: 2,
2974 };
2975
2976 let workflow = AhbWorkflow {
2977 pruefidentifikator: "55018".to_string(),
2978 description: "Test".to_string(),
2979 communication_direction: None,
2980 fields: vec![
2981 AhbFieldRule {
2983 segment_path: "SG4/STS/C601/9015".to_string(),
2984 name: "Statuskategorie".to_string(),
2985 ahb_status: "X".to_string(),
2986 codes: vec![AhbCodeRule {
2987 value: "7".to_string(),
2988 description: "Transaktionsgrund".to_string(),
2989 ahb_status: "X".to_string(),
2990 }],
2991 parent_group_ahb_status: None,
2992 segment_ahb_status: None,
2993 element_index: Some(0),
2994 component_index: Some(0),
2995 mig_number: Some("00035".to_string()),
2996 format: None,
2997 },
2998 AhbFieldRule {
2999 segment_path: "SG4/STS/C556/9013".to_string(),
3000 name: "Statusanlaß".to_string(),
3001 ahb_status: "X".to_string(),
3002 codes: vec![AhbCodeRule {
3003 value: "E03".to_string(),
3004 description: "Transaktionsgrund".to_string(),
3005 ahb_status: "X".to_string(),
3006 }],
3007 parent_group_ahb_status: None,
3008 segment_ahb_status: None,
3009 element_index: Some(2),
3010 component_index: Some(0),
3011 mig_number: Some("00035".to_string()),
3012 format: None,
3013 },
3014 AhbFieldRule {
3016 segment_path: "SG4/STS/C601/9015".to_string(),
3017 name: "Statuskategorie".to_string(),
3018 ahb_status: "X".to_string(),
3019 codes: vec![AhbCodeRule {
3020 value: "E01".to_string(),
3021 description: "Antwort".to_string(),
3022 ahb_status: "X".to_string(),
3023 }],
3024 parent_group_ahb_status: None,
3025 segment_ahb_status: None,
3026 element_index: Some(0),
3027 component_index: Some(0),
3028 mig_number: Some("00036".to_string()),
3029 format: None,
3030 },
3031 ],
3032 ub_definitions: BTreeMap::new(),
3033 package_conditions: Default::default(),
3034 };
3035
3036 let report = validator.validate(
3037 &[sts_7, sts_e01],
3038 &workflow,
3039 &external,
3040 ValidationLevel::Conditions,
3041 );
3042
3043 let code_errors: Vec<_> = report
3044 .by_category(ValidationCategory::Code)
3045 .filter(|i| {
3046 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3047 })
3048 .collect();
3049 assert_eq!(
3050 code_errors.len(),
3051 1,
3052 "Expected one COD002 (for GH02 only), got: {:?}",
3053 code_errors
3054 );
3055 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3056 }
3057
3058 #[test]
3059 fn test_code_validation_composite_paths_detects_invalid_code() {
3060 let evaluator = MockEvaluator::new(vec![]);
3063 let validator = EdifactValidator::new(evaluator);
3064 let external = NoOpExternalProvider;
3065
3066 let sts_segment = OwnedSegment {
3067 id: "STS".to_string(),
3068 elements: vec![
3069 vec!["7".to_string()],
3070 vec![String::new()],
3071 vec!["GH02".to_string()],
3072 vec!["ZW4".to_string()],
3073 ],
3074 segment_number: 1,
3075 };
3076
3077 let workflow = AhbWorkflow {
3078 pruefidentifikator: "55018".to_string(),
3079 description: "Test".to_string(),
3080 communication_direction: None,
3081 fields: vec![AhbFieldRule {
3082 segment_path: "SG4/STS/C556/9013".to_string(),
3083 name: "Statusanlaß".to_string(),
3084 ahb_status: "X".to_string(),
3085 codes: vec![AhbCodeRule {
3086 value: "E03".to_string(),
3087 description: "Transaktionsgrund".to_string(),
3088 ahb_status: "X".to_string(),
3089 }],
3090 parent_group_ahb_status: None,
3091 segment_ahb_status: None,
3092 element_index: Some(2),
3093 component_index: Some(0),
3094 ..Default::default()
3095 }],
3096 ub_definitions: BTreeMap::new(),
3097 package_conditions: Default::default(),
3098 };
3099
3100 let report = validator.validate(
3101 &[sts_segment],
3102 &workflow,
3103 &external,
3104 ValidationLevel::Conditions,
3105 );
3106
3107 let code_errors: Vec<_> = report
3108 .by_category(ValidationCategory::Code)
3109 .filter(|i| {
3110 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3111 })
3112 .collect();
3113 assert_eq!(
3114 code_errors.len(),
3115 1,
3116 "Expected COD002 for GH02, got: {:?}",
3117 code_errors
3118 );
3119 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3120 }
3121
3122 #[test]
3123 fn test_cross_field_code_validation_valid_qualifiers() {
3124 let evaluator = MockEvaluator::new(vec![]);
3127 let validator = EdifactValidator::new(evaluator);
3128 let external = NoOpExternalProvider;
3129
3130 let nad_ms = OwnedSegment {
3131 id: "NAD".to_string(),
3132 elements: vec![vec!["MS".to_string()]],
3133 segment_number: 4,
3134 };
3135 let nad_mr = OwnedSegment {
3136 id: "NAD".to_string(),
3137 elements: vec![vec!["MR".to_string()]],
3138 segment_number: 5,
3139 };
3140
3141 let workflow = AhbWorkflow {
3142 pruefidentifikator: "55001".to_string(),
3143 description: "Test".to_string(),
3144 communication_direction: None,
3145 fields: vec![
3146 AhbFieldRule {
3147 segment_path: "SG2/NAD/3035".to_string(),
3148 name: "Absender".to_string(),
3149 ahb_status: "X".to_string(),
3150 codes: vec![AhbCodeRule {
3151 value: "MS".to_string(),
3152 description: "Absender".to_string(),
3153 ahb_status: "X".to_string(),
3154 }],
3155 parent_group_ahb_status: None,
3156 segment_ahb_status: None,
3157 ..Default::default()
3158 },
3159 AhbFieldRule {
3160 segment_path: "SG2/NAD/3035".to_string(),
3161 name: "Empfaenger".to_string(),
3162 ahb_status: "X".to_string(),
3163 codes: vec![AhbCodeRule {
3164 value: "MR".to_string(),
3165 description: "Empfaenger".to_string(),
3166 ahb_status: "X".to_string(),
3167 }],
3168 parent_group_ahb_status: None,
3169 segment_ahb_status: None,
3170 ..Default::default()
3171 },
3172 ],
3173 ub_definitions: BTreeMap::new(),
3174 package_conditions: Default::default(),
3175 };
3176
3177 let report = validator.validate(
3178 &[nad_ms, nad_mr],
3179 &workflow,
3180 &external,
3181 ValidationLevel::Conditions,
3182 );
3183
3184 let code_errors: Vec<_> = report
3185 .by_category(ValidationCategory::Code)
3186 .filter(|i| i.severity == Severity::Error)
3187 .collect();
3188 assert!(
3189 code_errors.is_empty(),
3190 "Expected no code errors for valid qualifiers, got: {:?}",
3191 code_errors
3192 );
3193 }
3194
3195 #[test]
3196 fn test_cross_field_code_validation_catches_invalid_qualifier() {
3197 let evaluator = MockEvaluator::new(vec![]);
3199 let validator = EdifactValidator::new(evaluator);
3200 let external = NoOpExternalProvider;
3201
3202 let nad_ms = OwnedSegment {
3203 id: "NAD".to_string(),
3204 elements: vec![vec!["MS".to_string()]],
3205 segment_number: 4,
3206 };
3207 let nad_mt = OwnedSegment {
3208 id: "NAD".to_string(),
3209 elements: vec![vec!["MT".to_string()]], segment_number: 5,
3211 };
3212
3213 let workflow = AhbWorkflow {
3214 pruefidentifikator: "55001".to_string(),
3215 description: "Test".to_string(),
3216 communication_direction: None,
3217 fields: vec![
3218 AhbFieldRule {
3219 segment_path: "SG2/NAD/3035".to_string(),
3220 name: "Absender".to_string(),
3221 ahb_status: "X".to_string(),
3222 codes: vec![AhbCodeRule {
3223 value: "MS".to_string(),
3224 description: "Absender".to_string(),
3225 ahb_status: "X".to_string(),
3226 }],
3227 parent_group_ahb_status: None,
3228 segment_ahb_status: None,
3229 ..Default::default()
3230 },
3231 AhbFieldRule {
3232 segment_path: "SG2/NAD/3035".to_string(),
3233 name: "Empfaenger".to_string(),
3234 ahb_status: "X".to_string(),
3235 codes: vec![AhbCodeRule {
3236 value: "MR".to_string(),
3237 description: "Empfaenger".to_string(),
3238 ahb_status: "X".to_string(),
3239 }],
3240 parent_group_ahb_status: None,
3241 segment_ahb_status: None,
3242 ..Default::default()
3243 },
3244 ],
3245 ub_definitions: BTreeMap::new(),
3246 package_conditions: Default::default(),
3247 };
3248
3249 let report = validator.validate(
3250 &[nad_ms, nad_mt],
3251 &workflow,
3252 &external,
3253 ValidationLevel::Conditions,
3254 );
3255
3256 let code_errors: Vec<_> = report
3257 .by_category(ValidationCategory::Code)
3258 .filter(|i| i.severity == Severity::Error)
3259 .collect();
3260 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
3261 assert!(narrate(code_errors[0]).contains("MT"));
3262 assert!(narrate(code_errors[0]).contains("MR"));
3263 assert!(narrate(code_errors[0]).contains("MS"));
3264 }
3265
3266 #[test]
3267 fn test_cross_field_code_validation_unions_across_groups() {
3268 let evaluator = MockEvaluator::new(vec![]);
3272 let validator = EdifactValidator::new(evaluator);
3273 let external = NoOpExternalProvider;
3274
3275 let segments = vec![
3276 OwnedSegment {
3277 id: "NAD".to_string(),
3278 elements: vec![vec!["MS".to_string()]],
3279 segment_number: 3,
3280 },
3281 OwnedSegment {
3282 id: "NAD".to_string(),
3283 elements: vec![vec!["MR".to_string()]],
3284 segment_number: 4,
3285 },
3286 OwnedSegment {
3287 id: "NAD".to_string(),
3288 elements: vec![vec!["Z04".to_string()]],
3289 segment_number: 20,
3290 },
3291 OwnedSegment {
3292 id: "NAD".to_string(),
3293 elements: vec![vec!["Z09".to_string()]],
3294 segment_number: 21,
3295 },
3296 OwnedSegment {
3297 id: "NAD".to_string(),
3298 elements: vec![vec!["MT".to_string()]], segment_number: 22,
3300 },
3301 ];
3302
3303 let workflow = AhbWorkflow {
3304 pruefidentifikator: "55001".to_string(),
3305 description: "Test".to_string(),
3306 communication_direction: None,
3307 fields: vec![
3308 AhbFieldRule {
3309 segment_path: "SG2/NAD/3035".to_string(),
3310 name: "Absender".to_string(),
3311 ahb_status: "X".to_string(),
3312 codes: vec![AhbCodeRule {
3313 value: "MS".to_string(),
3314 description: "Absender".to_string(),
3315 ahb_status: "X".to_string(),
3316 }],
3317 parent_group_ahb_status: None,
3318 segment_ahb_status: None,
3319 ..Default::default()
3320 },
3321 AhbFieldRule {
3322 segment_path: "SG2/NAD/3035".to_string(),
3323 name: "Empfaenger".to_string(),
3324 ahb_status: "X".to_string(),
3325 codes: vec![AhbCodeRule {
3326 value: "MR".to_string(),
3327 description: "Empfaenger".to_string(),
3328 ahb_status: "X".to_string(),
3329 }],
3330 parent_group_ahb_status: None,
3331 segment_ahb_status: None,
3332 ..Default::default()
3333 },
3334 AhbFieldRule {
3335 segment_path: "SG4/SG12/NAD/3035".to_string(),
3336 name: "Anschlussnutzer".to_string(),
3337 ahb_status: "X".to_string(),
3338 codes: vec![AhbCodeRule {
3339 value: "Z04".to_string(),
3340 description: "Anschlussnutzer".to_string(),
3341 ahb_status: "X".to_string(),
3342 }],
3343 parent_group_ahb_status: None,
3344 segment_ahb_status: None,
3345 ..Default::default()
3346 },
3347 AhbFieldRule {
3348 segment_path: "SG4/SG12/NAD/3035".to_string(),
3349 name: "Korrespondenzanschrift".to_string(),
3350 ahb_status: "X".to_string(),
3351 codes: vec![AhbCodeRule {
3352 value: "Z09".to_string(),
3353 description: "Korrespondenzanschrift".to_string(),
3354 ahb_status: "X".to_string(),
3355 }],
3356 parent_group_ahb_status: None,
3357 segment_ahb_status: None,
3358 ..Default::default()
3359 },
3360 ],
3361 ub_definitions: BTreeMap::new(),
3362 package_conditions: Default::default(),
3363 };
3364
3365 let report =
3366 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3367
3368 let code_errors: Vec<_> = report
3369 .by_category(ValidationCategory::Code)
3370 .filter(|i| i.severity == Severity::Error)
3371 .collect();
3372 assert_eq!(
3373 code_errors.len(),
3374 1,
3375 "Expected exactly one COD002 error for MT, got: {:?}",
3376 code_errors
3377 );
3378 assert!(narrate(code_errors[0]).contains("MT"));
3379 }
3380
3381 #[test]
3382 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3383 let evaluator = MockEvaluator::new(vec![]);
3392 let validator = EdifactValidator::new(evaluator);
3393 let external = NoOpExternalProvider;
3394
3395 let qty_67 = OwnedSegment {
3396 id: "QTY".to_string(),
3397 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
3398 segment_number: 10,
3399 };
3400
3401 let workflow = AhbWorkflow {
3402 pruefidentifikator: "13025".to_string(),
3403 description: "Test".to_string(),
3404 communication_direction: None,
3405 fields: vec![AhbFieldRule {
3406 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
3407 name: "Menge, Qualifier".to_string(),
3408 ahb_status: "X".to_string(),
3409 codes: vec![
3410 AhbCodeRule {
3411 value: "220".to_string(),
3412 description: "Wahrer Wert".to_string(),
3413 ahb_status: "X".to_string(),
3414 },
3415 AhbCodeRule {
3416 value: "67".to_string(),
3417 description: "Ersatzwert".to_string(),
3418 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
3419 },
3420 AhbCodeRule {
3421 value: "Z18".to_string(),
3422 description: "Vorläufiger Wert".to_string(),
3423 ahb_status: "X [35]".to_string(),
3424 },
3425 ],
3426 parent_group_ahb_status: None,
3427 segment_ahb_status: None,
3428 element_index: Some(0),
3429 component_index: Some(0),
3430 ..Default::default()
3431 }],
3432 ub_definitions: BTreeMap::new(),
3433 package_conditions: Default::default(),
3434 };
3435
3436 let report =
3437 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3438
3439 let code_errors: Vec<_> = report
3440 .by_category(ValidationCategory::Code)
3441 .filter(|i| i.severity == Severity::Error)
3442 .collect();
3443 assert!(
3444 code_errors.is_empty(),
3445 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3446 code_errors
3447 );
3448 }
3449
3450 #[test]
3451 fn test_is_qualifier_field_simple_paths() {
3452 assert!(is_qualifier_field("NAD/3035"));
3453 assert!(is_qualifier_field("SG2/NAD/3035"));
3454 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3455 assert!(is_qualifier_field("LOC/3227"));
3456 }
3457
3458 #[test]
3459 fn test_is_qualifier_field_composite_paths() {
3460 assert!(is_qualifier_field("UNH/S009/0065"));
3464 assert!(is_qualifier_field("NAD/C082/3039"));
3465 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3466 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3467 }
3468
3469 #[test]
3470 fn test_is_qualifier_field_bare_segment() {
3471 assert!(!is_qualifier_field("NAD"));
3472 assert!(!is_qualifier_field("SG2/NAD"));
3473 }
3474
3475 #[test]
3476 fn test_is_qualifier_field_rejects_deep_paths() {
3477 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3479 }
3480
3481 #[test]
3482 fn test_missing_qualifier_instance_is_detected() {
3483 let evaluator = MockEvaluator::new(vec![]);
3486 let validator = EdifactValidator::new(evaluator);
3487 let external = NoOpExternalProvider;
3488
3489 let nad_ms = OwnedSegment {
3490 id: "NAD".to_string(),
3491 elements: vec![vec!["MS".to_string()]],
3492 segment_number: 3,
3493 };
3494
3495 let workflow = AhbWorkflow {
3496 pruefidentifikator: "55001".to_string(),
3497 description: "Test".to_string(),
3498 communication_direction: None,
3499 fields: vec![
3500 AhbFieldRule {
3501 segment_path: "SG2/NAD/3035".to_string(),
3502 name: "Absender".to_string(),
3503 ahb_status: "X".to_string(),
3504 codes: vec![AhbCodeRule {
3505 value: "MS".to_string(),
3506 description: "Absender".to_string(),
3507 ahb_status: "X".to_string(),
3508 }],
3509 parent_group_ahb_status: None,
3510 segment_ahb_status: None,
3511 ..Default::default()
3512 },
3513 AhbFieldRule {
3514 segment_path: "SG2/NAD/3035".to_string(),
3515 name: "Empfaenger".to_string(),
3516 ahb_status: "Muss".to_string(),
3517 codes: vec![AhbCodeRule {
3518 value: "MR".to_string(),
3519 description: "Empfaenger".to_string(),
3520 ahb_status: "X".to_string(),
3521 }],
3522 parent_group_ahb_status: None,
3523 segment_ahb_status: None,
3524 ..Default::default()
3525 },
3526 ],
3527 ub_definitions: BTreeMap::new(),
3528 package_conditions: Default::default(),
3529 };
3530
3531 let report =
3532 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
3533
3534 let ahb_errors: Vec<_> = report
3535 .by_category(ValidationCategory::Ahb)
3536 .filter(|i| i.severity == Severity::Error)
3537 .collect();
3538 assert_eq!(
3539 ahb_errors.len(),
3540 1,
3541 "Expected AHB001 for missing NAD+MR, got: {:?}",
3542 ahb_errors
3543 );
3544 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3545 }
3546
3547 #[test]
3548 fn test_present_qualifier_instance_no_error() {
3549 let evaluator = MockEvaluator::new(vec![]);
3551 let validator = EdifactValidator::new(evaluator);
3552 let external = NoOpExternalProvider;
3553
3554 let segments = vec![
3555 OwnedSegment {
3556 id: "NAD".to_string(),
3557 elements: vec![vec!["MS".to_string()]],
3558 segment_number: 3,
3559 },
3560 OwnedSegment {
3561 id: "NAD".to_string(),
3562 elements: vec![vec!["MR".to_string()]],
3563 segment_number: 4,
3564 },
3565 ];
3566
3567 let workflow = AhbWorkflow {
3568 pruefidentifikator: "55001".to_string(),
3569 description: "Test".to_string(),
3570 communication_direction: None,
3571 fields: vec![
3572 AhbFieldRule {
3573 segment_path: "SG2/NAD/3035".to_string(),
3574 name: "Absender".to_string(),
3575 ahb_status: "Muss".to_string(),
3576 codes: vec![AhbCodeRule {
3577 value: "MS".to_string(),
3578 description: "Absender".to_string(),
3579 ahb_status: "X".to_string(),
3580 }],
3581 parent_group_ahb_status: None,
3582 segment_ahb_status: None,
3583 ..Default::default()
3584 },
3585 AhbFieldRule {
3586 segment_path: "SG2/NAD/3035".to_string(),
3587 name: "Empfaenger".to_string(),
3588 ahb_status: "Muss".to_string(),
3589 codes: vec![AhbCodeRule {
3590 value: "MR".to_string(),
3591 description: "Empfaenger".to_string(),
3592 ahb_status: "X".to_string(),
3593 }],
3594 parent_group_ahb_status: None,
3595 segment_ahb_status: None,
3596 ..Default::default()
3597 },
3598 ],
3599 ub_definitions: BTreeMap::new(),
3600 package_conditions: Default::default(),
3601 };
3602
3603 let report =
3604 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3605
3606 let ahb_errors: Vec<_> = report
3607 .by_category(ValidationCategory::Ahb)
3608 .filter(|i| i.severity == Severity::Error)
3609 .collect();
3610 assert!(
3611 ahb_errors.is_empty(),
3612 "Expected no AHB001 errors, got: {:?}",
3613 ahb_errors
3614 );
3615 }
3616
3617 #[test]
3618 fn test_extract_group_path_key() {
3619 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
3620 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
3621 assert_eq!(extract_group_path_key("NAD/3035"), "");
3622 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
3623 }
3624
3625 fn validate_field_of_absent_group(
3627 group_status: &str,
3628 conditions: Vec<(u32, CR)>,
3629 ) -> ValidationReport {
3630 use mig_types::navigator::GroupNavigator;
3631
3632 struct NoSg9;
3633 impl GroupNavigator for NoSg9 {
3634 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3635 vec![]
3636 }
3637 fn find_segments_with_qualifier_in_group(
3638 &self,
3639 _: &str,
3640 _: usize,
3641 _: &str,
3642 _: &[&str],
3643 _: usize,
3644 ) -> Vec<OwnedSegment> {
3645 vec![]
3646 }
3647 fn group_instance_count(&self, _: &[&str]) -> usize {
3648 0
3649 }
3650 }
3651
3652 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3653 let workflow = AhbWorkflow {
3654 pruefidentifikator: "44001".to_string(),
3655 description: "Test".to_string(),
3656 communication_direction: None,
3657 fields: vec![AhbFieldRule {
3658 segment_path: "SG9/QTY/C186/6060".to_string(),
3659 name: "Menge".to_string(),
3660 ahb_status: "X".to_string(),
3661 parent_group_ahb_status: Some(group_status.to_string()),
3662 ..Default::default()
3663 }],
3664 ub_definitions: BTreeMap::new(),
3665 package_conditions: Default::default(),
3666 };
3667 validator.validate_with_navigator(
3668 &[],
3669 &workflow,
3670 &NoOpExternalProvider,
3671 ValidationLevel::Conditions,
3672 &NoSg9,
3673 )
3674 }
3675
3676 #[test]
3677 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
3678 let report = validate_field_of_absent_group(
3680 "Soll [47]\r\nMuss [46]",
3681 vec![(47, CR::True), (46, CR::False)],
3682 );
3683 assert!(
3684 report.is_valid(),
3685 "{:?}",
3686 report.errors().collect::<Vec<_>>()
3687 );
3688 }
3689
3690 #[test]
3691 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
3692 let report = validate_field_of_absent_group(
3693 "Soll [47]\r\nMuss [46]",
3694 vec![(47, CR::False), (46, CR::True)],
3695 );
3696 let errors: Vec<_> = report.errors().collect();
3697 assert_eq!(errors.len(), 1, "{errors:?}");
3698 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3699 }
3700
3701 #[test]
3702 fn test_absent_optional_group_no_missing_field_error() {
3703 use mig_types::navigator::GroupNavigator;
3706
3707 struct NavWithoutSG3;
3708 impl GroupNavigator for NavWithoutSG3 {
3709 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3710 vec![]
3711 }
3712 fn find_segments_with_qualifier_in_group(
3713 &self,
3714 _: &str,
3715 _: usize,
3716 _: &str,
3717 _: &[&str],
3718 _: usize,
3719 ) -> Vec<OwnedSegment> {
3720 vec![]
3721 }
3722 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3723 match group_path {
3724 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
3727 }
3728 }
3729 }
3730
3731 let evaluator = MockEvaluator::new(vec![]);
3732 let validator = EdifactValidator::new(evaluator);
3733 let external = NoOpExternalProvider;
3734 let nav = NavWithoutSG3;
3735
3736 let segments = vec![
3738 OwnedSegment {
3739 id: "NAD".into(),
3740 elements: vec![vec!["MS".into()]],
3741 segment_number: 3,
3742 },
3743 OwnedSegment {
3744 id: "NAD".into(),
3745 elements: vec![vec!["MR".into()]],
3746 segment_number: 4,
3747 },
3748 ];
3749
3750 let workflow = AhbWorkflow {
3751 pruefidentifikator: "55001".to_string(),
3752 description: "Test".to_string(),
3753 communication_direction: None,
3754 fields: vec![
3755 AhbFieldRule {
3756 segment_path: "SG2/SG3/CTA/3139".to_string(),
3757 name: "Funktion des Ansprechpartners, Code".to_string(),
3758 ahb_status: "Muss".to_string(),
3759 codes: vec![],
3760 parent_group_ahb_status: None,
3761 segment_ahb_status: None,
3762 ..Default::default()
3763 },
3764 AhbFieldRule {
3765 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
3766 name: "Name vom Ansprechpartner".to_string(),
3767 ahb_status: "X".to_string(),
3768 codes: vec![],
3769 parent_group_ahb_status: None,
3770 segment_ahb_status: None,
3771 ..Default::default()
3772 },
3773 ],
3774 ub_definitions: BTreeMap::new(),
3775 package_conditions: Default::default(),
3776 };
3777
3778 let report = validator.validate_with_navigator(
3779 &segments,
3780 &workflow,
3781 &external,
3782 ValidationLevel::Conditions,
3783 &nav,
3784 );
3785
3786 let ahb_errors: Vec<_> = report
3787 .by_category(ValidationCategory::Ahb)
3788 .filter(|i| i.severity == Severity::Error)
3789 .collect();
3790 assert!(
3791 ahb_errors.is_empty(),
3792 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
3793 ahb_errors
3794 );
3795 }
3796
3797 #[test]
3798 fn test_present_group_still_checks_mandatory_fields() {
3799 use mig_types::navigator::GroupNavigator;
3801
3802 struct NavWithSG3;
3803 impl GroupNavigator for NavWithSG3 {
3804 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
3805 vec![]
3806 }
3807 fn find_segments_with_qualifier_in_group(
3808 &self,
3809 _: &str,
3810 _: usize,
3811 _: &str,
3812 _: &[&str],
3813 _: usize,
3814 ) -> Vec<OwnedSegment> {
3815 vec![]
3816 }
3817 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3818 match group_path {
3819 ["SG2"] => 1,
3820 ["SG2", "SG3"] => 1, _ => 0,
3822 }
3823 }
3824 }
3825
3826 let evaluator = MockEvaluator::new(vec![]);
3827 let validator = EdifactValidator::new(evaluator);
3828 let external = NoOpExternalProvider;
3829 let nav = NavWithSG3;
3830
3831 let segments = vec![OwnedSegment {
3833 id: "NAD".into(),
3834 elements: vec![vec!["MS".into()]],
3835 segment_number: 3,
3836 }];
3837
3838 let workflow = AhbWorkflow {
3839 pruefidentifikator: "55001".to_string(),
3840 description: "Test".to_string(),
3841 communication_direction: None,
3842 fields: vec![AhbFieldRule {
3843 segment_path: "SG2/SG3/CTA/3139".to_string(),
3844 name: "Funktion des Ansprechpartners, Code".to_string(),
3845 ahb_status: "Muss".to_string(),
3846 codes: vec![],
3847 parent_group_ahb_status: None,
3848 segment_ahb_status: None,
3849 ..Default::default()
3850 }],
3851 ub_definitions: BTreeMap::new(),
3852 package_conditions: Default::default(),
3853 };
3854
3855 let report = validator.validate_with_navigator(
3856 &segments,
3857 &workflow,
3858 &external,
3859 ValidationLevel::Conditions,
3860 &nav,
3861 );
3862
3863 let ahb_errors: Vec<_> = report
3864 .by_category(ValidationCategory::Ahb)
3865 .filter(|i| i.severity == Severity::Error)
3866 .collect();
3867 assert_eq!(
3868 ahb_errors.len(),
3869 1,
3870 "Expected AHB001 error when SG3 is present but CTA missing"
3871 );
3872 assert!(narrate(ahb_errors[0]).contains("CTA"));
3873 }
3874
3875 #[test]
3876 fn test_missing_qualifier_with_navigator_is_detected() {
3877 use mig_types::navigator::GroupNavigator;
3880
3881 struct NavWithSG2;
3882 impl GroupNavigator for NavWithSG2 {
3883 fn find_segments_in_group(
3884 &self,
3885 segment_id: &str,
3886 group_path: &[&str],
3887 instance_index: usize,
3888 ) -> Vec<OwnedSegment> {
3889 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
3890 vec![OwnedSegment {
3891 id: "NAD".into(),
3892 elements: vec![vec!["MS".into()]],
3893 segment_number: 3,
3894 }]
3895 } else {
3896 vec![]
3897 }
3898 }
3899 fn find_segments_with_qualifier_in_group(
3900 &self,
3901 _: &str,
3902 _: usize,
3903 _: &str,
3904 _: &[&str],
3905 _: usize,
3906 ) -> Vec<OwnedSegment> {
3907 vec![]
3908 }
3909 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3910 match group_path {
3911 ["SG2"] => 1,
3912 _ => 0,
3913 }
3914 }
3915 }
3916
3917 let evaluator = MockEvaluator::new(vec![]);
3918 let validator = EdifactValidator::new(evaluator);
3919 let external = NoOpExternalProvider;
3920 let nav = NavWithSG2;
3921
3922 let segments = vec![OwnedSegment {
3923 id: "NAD".into(),
3924 elements: vec![vec!["MS".into()]],
3925 segment_number: 3,
3926 }];
3927
3928 let workflow = AhbWorkflow {
3929 pruefidentifikator: "55001".to_string(),
3930 description: "Test".to_string(),
3931 communication_direction: None,
3932 fields: vec![
3933 AhbFieldRule {
3934 segment_path: "SG2/NAD/3035".to_string(),
3935 name: "Absender".to_string(),
3936 ahb_status: "X".to_string(),
3937 codes: vec![AhbCodeRule {
3938 value: "MS".to_string(),
3939 description: "Absender".to_string(),
3940 ahb_status: "X".to_string(),
3941 }],
3942 parent_group_ahb_status: None,
3943 segment_ahb_status: None,
3944 ..Default::default()
3945 },
3946 AhbFieldRule {
3947 segment_path: "SG2/NAD/3035".to_string(),
3948 name: "Empfaenger".to_string(),
3949 ahb_status: "Muss".to_string(),
3950 codes: vec![AhbCodeRule {
3951 value: "MR".to_string(),
3952 description: "Empfaenger".to_string(),
3953 ahb_status: "X".to_string(),
3954 }],
3955 parent_group_ahb_status: None,
3956 segment_ahb_status: None,
3957 ..Default::default()
3958 },
3959 ],
3960 ub_definitions: BTreeMap::new(),
3961 package_conditions: Default::default(),
3962 };
3963
3964 let report = validator.validate_with_navigator(
3965 &segments,
3966 &workflow,
3967 &external,
3968 ValidationLevel::Conditions,
3969 &nav,
3970 );
3971
3972 let ahb_errors: Vec<_> = report
3973 .by_category(ValidationCategory::Ahb)
3974 .filter(|i| i.severity == Severity::Error)
3975 .collect();
3976 assert_eq!(
3977 ahb_errors.len(),
3978 1,
3979 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
3980 ahb_errors
3981 );
3982 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
3983 }
3984
3985 #[test]
3986 fn test_optional_group_variant_absent_no_error() {
3987 use mig_types::navigator::GroupNavigator;
3992
3993 struct TestNav;
3994 impl GroupNavigator for TestNav {
3995 fn find_segments_in_group(
3996 &self,
3997 segment_id: &str,
3998 group_path: &[&str],
3999 instance_index: usize,
4000 ) -> Vec<OwnedSegment> {
4001 match (segment_id, group_path, instance_index) {
4002 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
4003 id: "LOC".into(),
4004 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4005 segment_number: 10,
4006 }],
4007 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
4008 id: "NAD".into(),
4009 elements: vec![vec!["MS".into()]],
4010 segment_number: 3,
4011 }],
4012 _ => vec![],
4013 }
4014 }
4015 fn find_segments_with_qualifier_in_group(
4016 &self,
4017 _: &str,
4018 _: usize,
4019 _: &str,
4020 _: &[&str],
4021 _: usize,
4022 ) -> Vec<OwnedSegment> {
4023 vec![]
4024 }
4025 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4026 match group_path {
4027 ["SG2"] => 1,
4028 ["SG4"] => 1,
4029 ["SG4", "SG5"] => 1, _ => 0,
4031 }
4032 }
4033 }
4034
4035 let evaluator = MockEvaluator::new(vec![]);
4036 let validator = EdifactValidator::new(evaluator);
4037 let external = NoOpExternalProvider;
4038 let nav = TestNav;
4039
4040 let segments = vec![
4041 OwnedSegment {
4042 id: "NAD".into(),
4043 elements: vec![vec!["MS".into()]],
4044 segment_number: 3,
4045 },
4046 OwnedSegment {
4047 id: "LOC".into(),
4048 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4049 segment_number: 10,
4050 },
4051 ];
4052
4053 let workflow = AhbWorkflow {
4054 pruefidentifikator: "55001".to_string(),
4055 description: "Test".to_string(),
4056 communication_direction: None,
4057 fields: vec![
4058 AhbFieldRule {
4060 segment_path: "SG2/NAD/3035".to_string(),
4061 name: "Absender".to_string(),
4062 ahb_status: "X".to_string(),
4063 codes: vec![AhbCodeRule {
4064 value: "MS".to_string(),
4065 description: "Absender".to_string(),
4066 ahb_status: "X".to_string(),
4067 }],
4068 parent_group_ahb_status: Some("Muss".to_string()),
4069 segment_ahb_status: None,
4070 ..Default::default()
4071 },
4072 AhbFieldRule {
4073 segment_path: "SG2/NAD/3035".to_string(),
4074 name: "Empfaenger".to_string(),
4075 ahb_status: "Muss".to_string(),
4076 codes: vec![AhbCodeRule {
4077 value: "MR".to_string(),
4078 description: "Empfaenger".to_string(),
4079 ahb_status: "X".to_string(),
4080 }],
4081 parent_group_ahb_status: Some("Muss".to_string()),
4082 segment_ahb_status: None,
4083 ..Default::default()
4084 },
4085 AhbFieldRule {
4087 segment_path: "SG4/SG5/LOC/3227".to_string(),
4088 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4089 ahb_status: "X".to_string(),
4090 codes: vec![AhbCodeRule {
4091 value: "Z16".to_string(),
4092 description: "Marktlokation".to_string(),
4093 ahb_status: "X".to_string(),
4094 }],
4095 parent_group_ahb_status: Some("Kann".to_string()),
4096 segment_ahb_status: None,
4097 ..Default::default()
4098 },
4099 AhbFieldRule {
4100 segment_path: "SG4/SG5/LOC/3227".to_string(),
4101 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4102 ahb_status: "Muss".to_string(),
4103 codes: vec![AhbCodeRule {
4104 value: "Z17".to_string(),
4105 description: "Messlokation".to_string(),
4106 ahb_status: "X".to_string(),
4107 }],
4108 parent_group_ahb_status: Some("Kann".to_string()),
4109 segment_ahb_status: None,
4110 ..Default::default()
4111 },
4112 ],
4113 ub_definitions: BTreeMap::new(),
4114 package_conditions: Default::default(),
4115 };
4116
4117 let report = validator.validate_with_navigator(
4118 &segments,
4119 &workflow,
4120 &external,
4121 ValidationLevel::Conditions,
4122 &nav,
4123 );
4124
4125 let ahb_errors: Vec<_> = report
4126 .by_category(ValidationCategory::Ahb)
4127 .filter(|i| i.severity == Severity::Error)
4128 .collect();
4129
4130 assert_eq!(
4133 ahb_errors.len(),
4134 1,
4135 "Expected only AHB001 for missing NAD+MR, got: {:?}",
4136 ahb_errors
4137 );
4138 assert!(
4139 narrate(ahb_errors[0]).contains("Empfaenger"),
4140 "Error should be for missing NAD+MR (Empfaenger)"
4141 );
4142 }
4143
4144 #[test]
4148 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4149 use mig_types::navigator::GroupNavigator;
4150
4151 struct TestNav;
4152 impl GroupNavigator for TestNav {
4153 fn find_segments_in_group(
4154 &self,
4155 segment_id: &str,
4156 group_path: &[&str],
4157 instance_index: usize,
4158 ) -> Vec<OwnedSegment> {
4159 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4160 vec![OwnedSegment {
4161 id: "SEQ".into(),
4162 elements: vec![vec!["Z03".into()]],
4163 segment_number: 10,
4164 }]
4165 } else {
4166 vec![]
4167 }
4168 }
4169 fn find_segments_with_qualifier_in_group(
4170 &self,
4171 _: &str,
4172 _: usize,
4173 _: &str,
4174 _: &[&str],
4175 _: usize,
4176 ) -> Vec<OwnedSegment> {
4177 vec![]
4178 }
4179 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4180 match group_path {
4181 ["SG4"] => 1,
4182 ["SG4", "SG8"] => 1,
4183 _ => 0,
4184 }
4185 }
4186 }
4187
4188 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
4189 let validator = EdifactValidator::new(evaluator);
4190 let segments = vec![OwnedSegment {
4191 id: "SEQ".into(),
4192 elements: vec![vec!["Z03".into()]],
4193 segment_number: 10,
4194 }];
4195 let seq = |code: &str, group: &str| AhbFieldRule {
4196 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4197 name: format!("Handlung, Code ({code})"),
4198 ahb_status: "X".to_string(),
4199 codes: vec![AhbCodeRule {
4200 value: code.to_string(),
4201 description: String::new(),
4202 ahb_status: "X".to_string(),
4203 }],
4204 parent_group_ahb_status: Some(group.to_string()),
4205 segment_ahb_status: None,
4206 ..Default::default()
4207 };
4208 let workflow = AhbWorkflow {
4209 pruefidentifikator: "44143".to_string(),
4210 description: "Test".to_string(),
4211 communication_direction: None,
4212 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
4213 ub_definitions: BTreeMap::new(),
4214 package_conditions: Default::default(),
4215 };
4216
4217 let report = validator.validate_with_navigator(
4218 &segments,
4219 &workflow,
4220 &NoOpExternalProvider,
4221 ValidationLevel::Conditions,
4222 &TestNav,
4223 );
4224 let missing: Vec<_> = report
4225 .issues
4226 .iter()
4227 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
4228 .collect();
4229 assert!(missing.is_empty(), "{missing:#?}");
4230 }
4231
4232 #[test]
4233 fn test_conditional_group_variant_absent_no_error() {
4234 use mig_types::navigator::GroupNavigator;
4239
4240 struct TestNav;
4241 impl GroupNavigator for TestNav {
4242 fn find_segments_in_group(
4243 &self,
4244 segment_id: &str,
4245 group_path: &[&str],
4246 instance_index: usize,
4247 ) -> Vec<OwnedSegment> {
4248 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
4249 vec![OwnedSegment {
4250 id: "LOC".into(),
4251 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4252 segment_number: 10,
4253 }]
4254 } else {
4255 vec![]
4256 }
4257 }
4258 fn find_segments_with_qualifier_in_group(
4259 &self,
4260 _: &str,
4261 _: usize,
4262 _: &str,
4263 _: &[&str],
4264 _: usize,
4265 ) -> Vec<OwnedSegment> {
4266 vec![]
4267 }
4268 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4269 match group_path {
4270 ["SG4"] => 1,
4271 ["SG4", "SG5"] => 1, _ => 0,
4273 }
4274 }
4275 }
4276
4277 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
4280 let validator = EdifactValidator::new(evaluator);
4281 let external = NoOpExternalProvider;
4282 let nav = TestNav;
4283
4284 let segments = vec![OwnedSegment {
4285 id: "LOC".into(),
4286 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4287 segment_number: 10,
4288 }];
4289
4290 let workflow = AhbWorkflow {
4291 pruefidentifikator: "55001".to_string(),
4292 description: "Test".to_string(),
4293 communication_direction: None,
4294 fields: vec![
4295 AhbFieldRule {
4297 segment_path: "SG4/SG5/LOC/3227".to_string(),
4298 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4299 ahb_status: "X".to_string(),
4300 codes: vec![AhbCodeRule {
4301 value: "Z16".to_string(),
4302 description: "Marktlokation".to_string(),
4303 ahb_status: "X".to_string(),
4304 }],
4305 parent_group_ahb_status: Some("Muss [2061]".to_string()),
4306 segment_ahb_status: None,
4307 ..Default::default()
4308 },
4309 AhbFieldRule {
4311 segment_path: "SG4/SG5/LOC/3227".to_string(),
4312 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4313 ahb_status: "X".to_string(),
4314 codes: vec![AhbCodeRule {
4315 value: "Z17".to_string(),
4316 description: "Messlokation".to_string(),
4317 ahb_status: "X".to_string(),
4318 }],
4319 parent_group_ahb_status: Some("Soll [165]".to_string()),
4320 segment_ahb_status: None,
4321 ..Default::default()
4322 },
4323 ],
4324 ub_definitions: BTreeMap::new(),
4325 package_conditions: Default::default(),
4326 };
4327
4328 let report = validator.validate_with_navigator(
4329 &segments,
4330 &workflow,
4331 &external,
4332 ValidationLevel::Conditions,
4333 &nav,
4334 );
4335
4336 let ahb_errors: Vec<_> = report
4337 .by_category(ValidationCategory::Ahb)
4338 .filter(|i| i.severity == Severity::Error)
4339 .collect();
4340
4341 assert!(
4343 ahb_errors.is_empty(),
4344 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4345 ahb_errors
4346 );
4347 }
4348
4349 #[test]
4350 fn test_conditional_group_variant_unknown_no_error() {
4351 let evaluator = MockEvaluator::new(vec![]);
4357 let validator = EdifactValidator::new(evaluator);
4358 let external = NoOpExternalProvider;
4359
4360 let workflow = AhbWorkflow {
4361 pruefidentifikator: "55001".to_string(),
4362 description: "Test".to_string(),
4363 communication_direction: None,
4364 fields: vec![AhbFieldRule {
4365 segment_path: "SG4/SG5/LOC/3227".to_string(),
4366 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4367 ahb_status: "X".to_string(),
4368 codes: vec![AhbCodeRule {
4369 value: "Z17".to_string(),
4370 description: "Messlokation".to_string(),
4371 ahb_status: "X".to_string(),
4372 }],
4373 parent_group_ahb_status: Some("Soll [165]".to_string()),
4374 segment_ahb_status: None,
4375 ..Default::default()
4376 }],
4377 ub_definitions: BTreeMap::new(),
4378 package_conditions: Default::default(),
4379 };
4380
4381 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4382
4383 let ahb_errors: Vec<_> = report
4384 .by_category(ValidationCategory::Ahb)
4385 .filter(|i| i.severity == Severity::Error)
4386 .collect();
4387
4388 assert!(
4390 ahb_errors.is_empty(),
4391 "Expected no errors when parent group condition is Unknown, got: {:?}",
4392 ahb_errors
4393 );
4394 }
4395
4396 #[test]
4397 fn test_segment_absent_within_present_group_no_error() {
4398 use mig_types::navigator::GroupNavigator;
4402
4403 struct TestNav;
4404 impl GroupNavigator for TestNav {
4405 fn find_segments_in_group(
4406 &self,
4407 segment_id: &str,
4408 group_path: &[&str],
4409 instance_index: usize,
4410 ) -> Vec<OwnedSegment> {
4411 if segment_id == "QTY"
4413 && group_path == ["SG5", "SG6", "SG9", "SG10"]
4414 && instance_index == 0
4415 {
4416 vec![OwnedSegment {
4417 id: "QTY".into(),
4418 elements: vec![vec!["220".into(), "0".into()]],
4419 segment_number: 14,
4420 }]
4421 } else {
4422 vec![]
4423 }
4424 }
4425 fn find_segments_with_qualifier_in_group(
4426 &self,
4427 _: &str,
4428 _: usize,
4429 _: &str,
4430 _: &[&str],
4431 _: usize,
4432 ) -> Vec<OwnedSegment> {
4433 vec![]
4434 }
4435 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4436 match group_path {
4437 ["SG5"] => 1,
4438 ["SG5", "SG6"] => 1,
4439 ["SG5", "SG6", "SG9"] => 1,
4440 ["SG5", "SG6", "SG9", "SG10"] => 1,
4441 _ => 0,
4442 }
4443 }
4444 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4445 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4447 }
4448 }
4449
4450 let evaluator = MockEvaluator::all_true(&[]);
4451 let validator = EdifactValidator::new(evaluator);
4452 let external = NoOpExternalProvider;
4453 let nav = TestNav;
4454
4455 let segments = vec![OwnedSegment {
4456 id: "QTY".into(),
4457 elements: vec![vec!["220".into(), "0".into()]],
4458 segment_number: 14,
4459 }];
4460
4461 let workflow = AhbWorkflow {
4462 pruefidentifikator: "13017".to_string(),
4463 description: "Test".to_string(),
4464 communication_direction: None,
4465 fields: vec![
4466 AhbFieldRule {
4468 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4469 name: "Statuskategorie, Code".to_string(),
4470 ahb_status: "X".to_string(),
4471 codes: vec![],
4472 parent_group_ahb_status: Some("Muss".to_string()),
4473 segment_ahb_status: None,
4474 ..Default::default()
4475 },
4476 AhbFieldRule {
4478 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4479 name: "Statusanlaß, Code".to_string(),
4480 ahb_status: "X [5]".to_string(),
4481 codes: vec![],
4482 parent_group_ahb_status: Some("Muss".to_string()),
4483 segment_ahb_status: None,
4484 ..Default::default()
4485 },
4486 ],
4487 ub_definitions: BTreeMap::new(),
4488 package_conditions: Default::default(),
4489 };
4490
4491 let report = validator.validate_with_navigator(
4492 &segments,
4493 &workflow,
4494 &external,
4495 ValidationLevel::Conditions,
4496 &nav,
4497 );
4498
4499 let ahb_errors: Vec<_> = report
4500 .by_category(ValidationCategory::Ahb)
4501 .filter(|i| i.severity == Severity::Error)
4502 .collect();
4503
4504 assert!(
4505 ahb_errors.is_empty(),
4506 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
4507 ahb_errors
4508 );
4509 }
4510
4511 #[test]
4512 fn test_group_scoped_code_validation_with_navigator() {
4513 use mig_types::navigator::GroupNavigator;
4517
4518 struct TestNav;
4519 impl GroupNavigator for TestNav {
4520 fn find_segments_in_group(
4521 &self,
4522 segment_id: &str,
4523 group_path: &[&str],
4524 _instance_index: usize,
4525 ) -> Vec<OwnedSegment> {
4526 if segment_id != "NAD" {
4527 return vec![];
4528 }
4529 match group_path {
4530 ["SG2"] => vec![
4531 OwnedSegment {
4532 id: "NAD".into(),
4533 elements: vec![vec!["MS".into()]],
4534 segment_number: 3,
4535 },
4536 OwnedSegment {
4537 id: "NAD".into(),
4538 elements: vec![vec!["MT".into()]], segment_number: 4,
4540 },
4541 ],
4542 ["SG4", "SG12"] => vec![
4543 OwnedSegment {
4544 id: "NAD".into(),
4545 elements: vec![vec!["Z04".into()]],
4546 segment_number: 20,
4547 },
4548 OwnedSegment {
4549 id: "NAD".into(),
4550 elements: vec![vec!["Z09".into()]],
4551 segment_number: 21,
4552 },
4553 ],
4554 _ => vec![],
4555 }
4556 }
4557 fn find_segments_with_qualifier_in_group(
4558 &self,
4559 _: &str,
4560 _: usize,
4561 _: &str,
4562 _: &[&str],
4563 _: usize,
4564 ) -> Vec<OwnedSegment> {
4565 vec![]
4566 }
4567 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4568 match group_path {
4569 ["SG2"] | ["SG4", "SG12"] => 1,
4570 _ => 0,
4571 }
4572 }
4573 }
4574
4575 let evaluator = MockEvaluator::new(vec![]);
4576 let validator = EdifactValidator::new(evaluator);
4577 let external = NoOpExternalProvider;
4578 let nav = TestNav;
4579
4580 let workflow = AhbWorkflow {
4581 pruefidentifikator: "55001".to_string(),
4582 description: "Test".to_string(),
4583 communication_direction: None,
4584 fields: vec![
4585 AhbFieldRule {
4586 segment_path: "SG2/NAD/3035".to_string(),
4587 name: "Absender".to_string(),
4588 ahb_status: "X".to_string(),
4589 codes: vec![AhbCodeRule {
4590 value: "MS".to_string(),
4591 description: "Absender".to_string(),
4592 ahb_status: "X".to_string(),
4593 }],
4594 parent_group_ahb_status: None,
4595 segment_ahb_status: None,
4596 ..Default::default()
4597 },
4598 AhbFieldRule {
4599 segment_path: "SG2/NAD/3035".to_string(),
4600 name: "Empfaenger".to_string(),
4601 ahb_status: "X".to_string(),
4602 codes: vec![AhbCodeRule {
4603 value: "MR".to_string(),
4604 description: "Empfaenger".to_string(),
4605 ahb_status: "X".to_string(),
4606 }],
4607 parent_group_ahb_status: None,
4608 segment_ahb_status: None,
4609 ..Default::default()
4610 },
4611 AhbFieldRule {
4612 segment_path: "SG4/SG12/NAD/3035".to_string(),
4613 name: "Anschlussnutzer".to_string(),
4614 ahb_status: "X".to_string(),
4615 codes: vec![AhbCodeRule {
4616 value: "Z04".to_string(),
4617 description: "Anschlussnutzer".to_string(),
4618 ahb_status: "X".to_string(),
4619 }],
4620 parent_group_ahb_status: None,
4621 segment_ahb_status: None,
4622 ..Default::default()
4623 },
4624 AhbFieldRule {
4625 segment_path: "SG4/SG12/NAD/3035".to_string(),
4626 name: "Korrespondenzanschrift".to_string(),
4627 ahb_status: "X".to_string(),
4628 codes: vec![AhbCodeRule {
4629 value: "Z09".to_string(),
4630 description: "Korrespondenzanschrift".to_string(),
4631 ahb_status: "X".to_string(),
4632 }],
4633 parent_group_ahb_status: None,
4634 segment_ahb_status: None,
4635 ..Default::default()
4636 },
4637 ],
4638 ub_definitions: BTreeMap::new(),
4639 package_conditions: Default::default(),
4640 };
4641
4642 let all_segments = vec![
4644 OwnedSegment {
4645 id: "NAD".into(),
4646 elements: vec![vec!["MS".into()]],
4647 segment_number: 3,
4648 },
4649 OwnedSegment {
4650 id: "NAD".into(),
4651 elements: vec![vec!["MT".into()]],
4652 segment_number: 4,
4653 },
4654 OwnedSegment {
4655 id: "NAD".into(),
4656 elements: vec![vec!["Z04".into()]],
4657 segment_number: 20,
4658 },
4659 OwnedSegment {
4660 id: "NAD".into(),
4661 elements: vec![vec!["Z09".into()]],
4662 segment_number: 21,
4663 },
4664 ];
4665
4666 let report = validator.validate_with_navigator(
4667 &all_segments,
4668 &workflow,
4669 &external,
4670 ValidationLevel::Conditions,
4671 &nav,
4672 );
4673
4674 let code_errors: Vec<_> = report
4675 .by_category(ValidationCategory::Code)
4676 .filter(|i| i.severity == Severity::Error)
4677 .collect();
4678
4679 assert_eq!(
4682 code_errors.len(),
4683 1,
4684 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
4685 code_errors
4686 );
4687 assert!(narrate(code_errors[0]).contains("MT"));
4688 assert!(narrate(code_errors[0]).contains("MR"));
4690 assert!(narrate(code_errors[0]).contains("MS"));
4691 assert!(
4692 !narrate(code_errors[0]).contains("Z04"),
4693 "SG4/SG12 codes should not leak into SG2 error"
4694 );
4695 assert!(
4697 code_errors[0]
4698 .field_path
4699 .as_deref()
4700 .unwrap_or("")
4701 .contains("SG2"),
4702 "Error field_path should reference SG2, got: {:?}",
4703 code_errors[0].field_path
4704 );
4705 }
4706
4707 #[test]
4710 fn test_package_cardinality_within_bounds() {
4711 let evaluator = MockEvaluator::all_true(&[]);
4713 let validator = EdifactValidator::new(evaluator);
4714 let external = NoOpExternalProvider;
4715
4716 let segments = vec![OwnedSegment {
4717 id: "STS".into(),
4718 elements: vec![
4719 vec!["Z33".into()], vec![], vec!["E01".into()], ],
4723 segment_number: 5,
4724 }];
4725
4726 let workflow = AhbWorkflow {
4727 pruefidentifikator: "13017".to_string(),
4728 description: "Test".to_string(),
4729 communication_direction: None,
4730 ub_definitions: BTreeMap::new(),
4731 package_conditions: Default::default(),
4732 fields: vec![AhbFieldRule {
4733 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4734 name: "Statusanlaß, Code".to_string(),
4735 ahb_status: "X".to_string(),
4736 element_index: Some(2),
4737 component_index: Some(0),
4738 codes: vec![
4739 AhbCodeRule {
4740 value: "E01".into(),
4741 description: "Code 1".into(),
4742 ahb_status: "X [4P0..1]".into(),
4743 },
4744 AhbCodeRule {
4745 value: "E02".into(),
4746 description: "Code 2".into(),
4747 ahb_status: "X [4P0..1]".into(),
4748 },
4749 ],
4750 parent_group_ahb_status: Some("Muss".to_string()),
4751 segment_ahb_status: None,
4752 mig_number: None,
4753 format: None,
4754 }],
4755 };
4756
4757 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4758 let pkg_errors: Vec<_> = report
4759 .by_category(ValidationCategory::Ahb)
4760 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4761 .collect();
4762 assert!(
4763 pkg_errors.is_empty(),
4764 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
4765 pkg_errors
4766 );
4767 }
4768
4769 #[test]
4770 fn test_package_cardinality_zero_present_min_zero() {
4771 let evaluator = MockEvaluator::all_true(&[]);
4773 let validator = EdifactValidator::new(evaluator);
4774 let external = NoOpExternalProvider;
4775
4776 let segments = vec![OwnedSegment {
4777 id: "STS".into(),
4778 elements: vec![
4779 vec!["Z33".into()],
4780 vec![],
4781 vec!["X99".into()], ],
4783 segment_number: 5,
4784 }];
4785
4786 let workflow = AhbWorkflow {
4787 pruefidentifikator: "13017".to_string(),
4788 description: "Test".to_string(),
4789 communication_direction: None,
4790 ub_definitions: BTreeMap::new(),
4791 package_conditions: Default::default(),
4792 fields: vec![AhbFieldRule {
4793 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4794 name: "Statusanlaß, Code".to_string(),
4795 ahb_status: "X".to_string(),
4796 element_index: Some(2),
4797 component_index: Some(0),
4798 codes: vec![
4799 AhbCodeRule {
4800 value: "E01".into(),
4801 description: "Code 1".into(),
4802 ahb_status: "X [4P0..1]".into(),
4803 },
4804 AhbCodeRule {
4805 value: "E02".into(),
4806 description: "Code 2".into(),
4807 ahb_status: "X [4P0..1]".into(),
4808 },
4809 ],
4810 parent_group_ahb_status: Some("Muss".to_string()),
4811 segment_ahb_status: None,
4812 mig_number: None,
4813 format: None,
4814 }],
4815 };
4816
4817 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4818 let pkg_errors: Vec<_> = report
4819 .by_category(ValidationCategory::Ahb)
4820 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4821 .collect();
4822 assert!(
4823 pkg_errors.is_empty(),
4824 "0 codes, min=0 — no error expected, got: {:?}",
4825 pkg_errors
4826 );
4827 }
4828
4829 #[test]
4830 fn test_package_cardinality_too_many() {
4831 let evaluator = MockEvaluator::all_true(&[]);
4833 let validator = EdifactValidator::new(evaluator);
4834 let external = NoOpExternalProvider;
4835
4836 let segments = vec![
4838 OwnedSegment {
4839 id: "STS".into(),
4840 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
4841 segment_number: 5,
4842 },
4843 OwnedSegment {
4844 id: "STS".into(),
4845 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
4846 segment_number: 6,
4847 },
4848 ];
4849
4850 let workflow = AhbWorkflow {
4851 pruefidentifikator: "13017".to_string(),
4852 description: "Test".to_string(),
4853 communication_direction: None,
4854 ub_definitions: BTreeMap::new(),
4855 package_conditions: Default::default(),
4856 fields: vec![AhbFieldRule {
4857 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4858 name: "Statusanlaß, Code".to_string(),
4859 ahb_status: "X".to_string(),
4860 element_index: Some(2),
4861 component_index: Some(0),
4862 codes: vec![
4863 AhbCodeRule {
4864 value: "E01".into(),
4865 description: "Code 1".into(),
4866 ahb_status: "X [4P0..1]".into(),
4867 },
4868 AhbCodeRule {
4869 value: "E02".into(),
4870 description: "Code 2".into(),
4871 ahb_status: "X [4P0..1]".into(),
4872 },
4873 ],
4874 parent_group_ahb_status: Some("Muss".to_string()),
4875 segment_ahb_status: None,
4876 mig_number: None,
4877 format: None,
4878 }],
4879 };
4880
4881 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4882 let pkg_errors: Vec<_> = report
4883 .by_category(ValidationCategory::Ahb)
4884 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4885 .collect();
4886 assert_eq!(
4887 pkg_errors.len(),
4888 1,
4889 "2 codes present, max=1 — expected 1 error, got: {:?}",
4890 pkg_errors
4891 );
4892 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
4893 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
4894 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
4895 }
4896
4897 #[test]
4898 fn test_package_cardinality_too_few() {
4899 let evaluator = MockEvaluator::all_true(&[]);
4901 let validator = EdifactValidator::new(evaluator);
4902 let external = NoOpExternalProvider;
4903
4904 let segments = vec![OwnedSegment {
4905 id: "STS".into(),
4906 elements: vec![
4907 vec!["Z33".into()],
4908 vec![],
4909 vec!["X99".into()], ],
4911 segment_number: 5,
4912 }];
4913
4914 let workflow = AhbWorkflow {
4915 pruefidentifikator: "13017".to_string(),
4916 description: "Test".to_string(),
4917 communication_direction: None,
4918 ub_definitions: BTreeMap::new(),
4919 package_conditions: Default::default(),
4920 fields: vec![AhbFieldRule {
4921 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4922 name: "Statusanlaß, Code".to_string(),
4923 ahb_status: "X".to_string(),
4924 element_index: Some(2),
4925 component_index: Some(0),
4926 codes: vec![
4927 AhbCodeRule {
4928 value: "E01".into(),
4929 description: "Code 1".into(),
4930 ahb_status: "X [5P1..3]".into(),
4931 },
4932 AhbCodeRule {
4933 value: "E02".into(),
4934 description: "Code 2".into(),
4935 ahb_status: "X [5P1..3]".into(),
4936 },
4937 AhbCodeRule {
4938 value: "E03".into(),
4939 description: "Code 3".into(),
4940 ahb_status: "X [5P1..3]".into(),
4941 },
4942 ],
4943 parent_group_ahb_status: Some("Muss".to_string()),
4944 segment_ahb_status: None,
4945 mig_number: None,
4946 format: None,
4947 }],
4948 };
4949
4950 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
4951 let pkg_errors: Vec<_> = report
4952 .by_category(ValidationCategory::Ahb)
4953 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
4954 .collect();
4955 assert_eq!(
4956 pkg_errors.len(),
4957 1,
4958 "0 codes present, min=1 — expected 1 error, got: {:?}",
4959 pkg_errors
4960 );
4961 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
4962 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
4963 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
4964 }
4965
4966 #[test]
4967 fn test_package_cardinality_no_packages_in_workflow() {
4968 let evaluator = MockEvaluator::all_true(&[]);
4970 let validator = EdifactValidator::new(evaluator);
4971 let external = NoOpExternalProvider;
4972
4973 let segments = vec![OwnedSegment {
4974 id: "STS".into(),
4975 elements: vec![vec!["E01".into()]],
4976 segment_number: 5,
4977 }];
4978
4979 let workflow = AhbWorkflow {
4980 pruefidentifikator: "13017".to_string(),
4981 description: "Test".to_string(),
4982 communication_direction: None,
4983 ub_definitions: BTreeMap::new(),
4984 package_conditions: Default::default(),
4985 fields: vec![AhbFieldRule {
4986 segment_path: "STS/9015".to_string(),
4987 name: "Status Code".to_string(),
4988 ahb_status: "X".to_string(),
4989 codes: vec![AhbCodeRule {
4990 value: "E01".into(),
4991 description: "Code 1".into(),
4992 ahb_status: "X".into(),
4993 }],
4994 parent_group_ahb_status: Some("Muss".to_string()),
4995 segment_ahb_status: None,
4996 ..Default::default()
4997 }],
4998 };
4999
5000 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5001 let pkg_errors: Vec<_> = report
5002 .by_category(ValidationCategory::Ahb)
5003 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5004 .collect();
5005 assert!(
5006 pkg_errors.is_empty(),
5007 "No packages in workflow — no errors expected"
5008 );
5009 }
5010
5011 #[test]
5012 fn test_package_cardinality_with_condition_and_package() {
5013 let evaluator = MockEvaluator::all_true(&[901]);
5015 let validator = EdifactValidator::new(evaluator);
5016 let external = NoOpExternalProvider;
5017
5018 let segments = vec![OwnedSegment {
5019 id: "STS".into(),
5020 elements: vec![vec![], vec![], vec!["E01".into()]],
5021 segment_number: 5,
5022 }];
5023
5024 let workflow = AhbWorkflow {
5025 pruefidentifikator: "13017".to_string(),
5026 description: "Test".to_string(),
5027 communication_direction: None,
5028 ub_definitions: BTreeMap::new(),
5029 package_conditions: Default::default(),
5030 fields: vec![AhbFieldRule {
5031 segment_path: "SG10/STS/C556/9013".to_string(),
5032 name: "Code".to_string(),
5033 ahb_status: "X".to_string(),
5034 element_index: Some(2),
5035 component_index: Some(0),
5036 codes: vec![
5037 AhbCodeRule {
5038 value: "E01".into(),
5039 description: "Code 1".into(),
5040 ahb_status: "X [901] [4P0..1]".into(),
5041 },
5042 AhbCodeRule {
5043 value: "E02".into(),
5044 description: "Code 2".into(),
5045 ahb_status: "X [901] [4P0..1]".into(),
5046 },
5047 ],
5048 parent_group_ahb_status: Some("Muss".to_string()),
5049 segment_ahb_status: None,
5050 mig_number: None,
5051 format: None,
5052 }],
5053 };
5054
5055 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5056 let pkg_errors: Vec<_> = report
5057 .by_category(ValidationCategory::Ahb)
5058 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5059 .collect();
5060 assert!(
5061 pkg_errors.is_empty(),
5062 "1 code within [4P0..1] bounds — no error, got: {:?}",
5063 pkg_errors
5064 );
5065 }
5066
5067 #[test]
5068 fn test_package_binds_only_where_its_condition_holds() {
5069 let report_for = |package_condition: CR| {
5073 let validator =
5074 EdifactValidator::new(MockEvaluator::new(vec![(902, package_condition)]));
5075 let segments = vec![OwnedSegment {
5076 id: "STS".into(),
5077 elements: vec![vec![], vec![], vec!["E09".into()]],
5078 segment_number: 5,
5079 }];
5080 let workflow = AhbWorkflow {
5081 pruefidentifikator: "13017".to_string(),
5082 description: "Test".to_string(),
5083 communication_direction: None,
5084 ub_definitions: BTreeMap::new(),
5085 package_conditions: BTreeMap::from([(5, ConditionExpr::Ref(902))]),
5086 fields: vec![AhbFieldRule {
5087 segment_path: "SG10/STS/C556/9013".to_string(),
5088 name: "Code".to_string(),
5089 ahb_status: "X".to_string(),
5090 element_index: Some(2),
5091 component_index: Some(0),
5092 codes: vec![
5093 AhbCodeRule {
5094 value: "E01".into(),
5095 description: "Code 1".into(),
5096 ahb_status: "X [5P1..1]".into(),
5097 },
5098 AhbCodeRule {
5099 value: "E09".into(),
5100 description: "Code 9".into(),
5101 ahb_status: "X".into(),
5102 },
5103 ],
5104 parent_group_ahb_status: Some("Muss".to_string()),
5105 segment_ahb_status: None,
5106 mig_number: None,
5107 format: None,
5108 }],
5109 };
5110 validator
5111 .validate(
5112 &segments,
5113 &workflow,
5114 &NoOpExternalProvider,
5115 ValidationLevel::Full,
5116 )
5117 .by_category(ValidationCategory::Ahb)
5118 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5119 .count()
5120 };
5121 assert_eq!(report_for(CR::False), 0, "[902] false: [5P] does not bind");
5122 assert_eq!(report_for(CR::True), 1, "[902] true: [5P1..1] binds");
5123 }
5124
5125 #[test]
5126 fn test_package_cardinality_scoped_per_group_instance() {
5127 use mig_types::navigator::GroupNavigator;
5135
5136 struct TwoSg10s {
5137 sts_a: OwnedSegment,
5138 sts_b: OwnedSegment,
5139 }
5140 impl GroupNavigator for TwoSg10s {
5141 fn find_segments_in_group(
5142 &self,
5143 segment_id: &str,
5144 group_path: &[&str],
5145 instance_index: usize,
5146 ) -> Vec<OwnedSegment> {
5147 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
5148 match instance_index {
5149 0 => vec![self.sts_a.clone()],
5150 1 => vec![self.sts_b.clone()],
5151 _ => vec![],
5152 }
5153 } else {
5154 vec![]
5155 }
5156 }
5157 fn find_segments_with_qualifier_in_group(
5158 &self,
5159 _: &str,
5160 _: usize,
5161 _: &str,
5162 _: &[&str],
5163 _: usize,
5164 ) -> Vec<OwnedSegment> {
5165 vec![]
5166 }
5167 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5168 match group_path {
5169 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
5170 ["SG5", "SG6", "SG9", "SG10"] => 2,
5171 _ => 0,
5172 }
5173 }
5174 }
5175
5176 let sts_a = OwnedSegment {
5177 id: "STS".into(),
5178 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5179 segment_number: 10,
5180 };
5181 let sts_b = OwnedSegment {
5182 id: "STS".into(),
5183 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5184 segment_number: 15,
5185 };
5186 let nav = TwoSg10s {
5187 sts_a: sts_a.clone(),
5188 sts_b: sts_b.clone(),
5189 };
5190
5191 let evaluator = MockEvaluator::all_true(&[]);
5192 let validator = EdifactValidator::new(evaluator);
5193 let external = NoOpExternalProvider;
5194
5195 let workflow = AhbWorkflow {
5196 pruefidentifikator: "13025".to_string(),
5197 description: "Test".to_string(),
5198 communication_direction: None,
5199 ub_definitions: BTreeMap::new(),
5200 package_conditions: Default::default(),
5201 fields: vec![AhbFieldRule {
5202 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5203 name: "Statusanlaß, Code".to_string(),
5204 ahb_status: "X".to_string(),
5205 element_index: Some(2),
5206 component_index: Some(0),
5207 codes: vec![
5208 AhbCodeRule {
5209 value: "E01".into(),
5210 description: "Code 1".into(),
5211 ahb_status: "X [4P0..1]".into(),
5212 },
5213 AhbCodeRule {
5214 value: "E02".into(),
5215 description: "Code 2".into(),
5216 ahb_status: "X [4P0..1]".into(),
5217 },
5218 ],
5219 parent_group_ahb_status: Some("Muss".to_string()),
5220 segment_ahb_status: None,
5221 mig_number: None,
5222 format: None,
5223 }],
5224 };
5225
5226 let report = validator.validate_with_navigator(
5227 &[sts_a, sts_b],
5228 &workflow,
5229 &external,
5230 ValidationLevel::Full,
5231 &nav,
5232 );
5233 let pkg_errors: Vec<_> = report
5234 .by_category(ValidationCategory::Ahb)
5235 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5236 .collect();
5237 assert!(
5238 pkg_errors.is_empty(),
5239 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
5240 pkg_errors
5241 );
5242 }
5243
5244 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
5245 OwnedSegment {
5246 id: id.to_string(),
5247 elements: elements
5248 .into_iter()
5249 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
5250 .collect(),
5251 segment_number: 0,
5252 }
5253 }
5254
5255 #[test]
5266 fn test_package_cardinality_scoped_to_rule_mig_variant() {
5267 use mig_types::navigator::GroupNavigator;
5268
5269 struct TwoSg8Variants {
5270 seq_z01: OwnedSegment,
5271 seq_z45: OwnedSegment,
5272 }
5273 impl GroupNavigator for TwoSg8Variants {
5274 fn find_segments_in_group(
5275 &self,
5276 segment_id: &str,
5277 group_path: &[&str],
5278 instance_index: usize,
5279 ) -> Vec<OwnedSegment> {
5280 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
5281 match instance_index {
5282 0 => vec![self.seq_z01.clone()],
5283 1 => vec![self.seq_z45.clone()],
5284 _ => vec![],
5285 }
5286 } else {
5287 vec![]
5288 }
5289 }
5290 fn find_segments_with_qualifier_in_group(
5291 &self,
5292 _: &str,
5293 _: usize,
5294 _: &str,
5295 _: &[&str],
5296 _: usize,
5297 ) -> Vec<OwnedSegment> {
5298 vec![]
5299 }
5300 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5301 match group_path {
5302 ["SG4"] => 1,
5303 ["SG4", "SG8"] => 2,
5304 _ => 0,
5305 }
5306 }
5307 fn instance_has_mig_number(
5308 &self,
5309 group_path: &[&str],
5310 instance_index: usize,
5311 mig_number: &str,
5312 ) -> bool {
5313 if group_path != ["SG4", "SG8"] {
5314 return true;
5315 }
5316 match (instance_index, mig_number) {
5317 (0, "00115") => true,
5318 (0, _) => false,
5319 (1, "00171") => true,
5320 (1, _) => false,
5321 _ => false,
5322 }
5323 }
5324 }
5325
5326 let seq_z01 = OwnedSegment {
5327 id: "SEQ".into(),
5328 elements: vec![vec!["Z01".into()], vec!["1".into()]],
5329 segment_number: 10,
5330 };
5331 let seq_z45 = OwnedSegment {
5332 id: "SEQ".into(),
5333 elements: vec![vec!["Z45".into()], vec!["1".into()]],
5334 segment_number: 20,
5335 };
5336 let nav = TwoSg8Variants {
5337 seq_z01: seq_z01.clone(),
5338 seq_z45: seq_z45.clone(),
5339 };
5340
5341 let evaluator = MockEvaluator::all_true(&[]);
5342 let validator = EdifactValidator::new(evaluator);
5343 let external = NoOpExternalProvider;
5344
5345 let workflow = AhbWorkflow {
5346 pruefidentifikator: "55218".to_string(),
5347 description: "Test".to_string(),
5348 communication_direction: None,
5349 ub_definitions: BTreeMap::new(),
5350 package_conditions: Default::default(),
5351 fields: vec![AhbFieldRule {
5352 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5353 name: "Handlung, Code".to_string(),
5354 ahb_status: "X".to_string(),
5355 element_index: Some(0),
5356 component_index: Some(0),
5357 codes: vec![
5358 AhbCodeRule {
5359 value: "Z45".into(),
5360 description: "NNA".into(),
5361 ahb_status: "X [1P1..4294967295]".into(),
5362 },
5363 AhbCodeRule {
5364 value: "Z84".into(),
5365 description: "Differenz-NNA".into(),
5366 ahb_status: "X [1P0..4294967295]".into(),
5367 },
5368 ],
5369 parent_group_ahb_status: Some("Muss".to_string()),
5370 segment_ahb_status: None,
5371 mig_number: Some("00171".to_string()),
5372 format: None,
5373 }],
5374 };
5375
5376 let report = validator.validate_with_navigator(
5377 &[seq_z01, seq_z45],
5378 &workflow,
5379 &external,
5380 ValidationLevel::Full,
5381 &nav,
5382 );
5383 let pkg_errors: Vec<_> = report
5384 .by_category(ValidationCategory::Ahb)
5385 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5386 .collect();
5387 assert!(
5388 pkg_errors.is_empty(),
5389 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
5390 pkg_errors
5391 );
5392 }
5393
5394 #[test]
5395 fn test_unt_count_correct() {
5396 let segments = vec![
5398 make_segment("UNH", vec![vec!["001"]]),
5399 make_segment("BGM", vec![vec!["E01"]]),
5400 make_segment("DTM", vec![vec!["137", "20250401"]]),
5401 make_segment("UNT", vec![vec!["4", "001"]]),
5402 ];
5403 assert!(
5404 validate_unt_segment_count(&segments).is_none(),
5405 "Correct count should produce no issue"
5406 );
5407 }
5408
5409 #[test]
5410 fn test_unt_count_mismatch() {
5411 let segments = vec![
5413 make_segment("UNH", vec![vec!["001"]]),
5414 make_segment("BGM", vec![vec!["E01"]]),
5415 make_segment("UNT", vec![vec!["5", "001"]]),
5416 ];
5417 let issue =
5418 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
5419 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5420 assert_eq!(issue.severity, Severity::Error);
5421 assert!(narrate(&issue).contains("declared 5"));
5422 assert!(narrate(&issue).contains("actual 3"));
5423 }
5424
5425 #[test]
5426 fn test_unt_count_excludes_envelope() {
5427 let segments = vec![
5429 make_segment("UNA", vec![]),
5430 make_segment("UNB", vec![vec!["UNOC", "3"]]),
5431 make_segment("UNH", vec![vec!["001"]]),
5432 make_segment("BGM", vec![vec!["E01"]]),
5433 make_segment("UNT", vec![vec!["3", "001"]]),
5434 make_segment("UNZ", vec![vec!["1"]]),
5435 ];
5436 assert!(
5437 validate_unt_segment_count(&segments).is_none(),
5438 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
5439 );
5440 }
5441
5442 #[test]
5443 fn test_unt_count_no_unt_returns_none() {
5444 let segments = vec![
5445 make_segment("UNH", vec![vec!["001"]]),
5446 make_segment("BGM", vec![vec!["E01"]]),
5447 ];
5448 assert!(
5449 validate_unt_segment_count(&segments).is_none(),
5450 "No UNT segment should return None (not our problem)"
5451 );
5452 }
5453
5454 #[test]
5455 fn test_unt_count_rejects_multi_message_input() {
5456 let segments = vec![
5458 make_segment("UNH", vec![vec!["001"]]),
5459 make_segment("BGM", vec![vec!["E01"]]),
5460 make_segment("UNT", vec![vec!["3", "001"]]),
5461 make_segment("UNH", vec![vec!["002"]]),
5462 make_segment("BGM", vec![vec!["E02"]]),
5463 make_segment("UNT", vec![vec!["3", "002"]]),
5464 ];
5465 let issue = validate_unt_segment_count(&segments)
5466 .expect("Multi-message input should produce an error");
5467 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5468 assert!(
5469 narrate(&issue).contains("2 UNH"),
5470 "Should mention UNH count: {}",
5471 narrate(&issue)
5472 );
5473 }
5474
5475 #[test]
5476 fn test_code_validation_accepts_multi_code_variant_qualifier() {
5477 let evaluator = MockEvaluator::new(vec![]);
5484 let validator = EdifactValidator::new(evaluator);
5485 let external = NoOpExternalProvider;
5486
5487 let rff_z39 = OwnedSegment {
5488 id: "RFF".to_string(),
5489 elements: vec![
5490 vec!["RFF".to_string()],
5491 vec!["Z39".to_string(), "REF1".to_string()],
5492 ],
5493 segment_number: 1,
5494 };
5495
5496 let workflow = AhbWorkflow {
5497 pruefidentifikator: "55035".to_string(),
5498 description: "Test".to_string(),
5499 communication_direction: None,
5500 fields: vec![
5501 AhbFieldRule {
5503 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5504 name: "Referenznummer Qualifier".to_string(),
5505 ahb_status: "Muss".to_string(),
5506 codes: vec![
5507 AhbCodeRule {
5508 value: "Z31".to_string(),
5509 description: "".to_string(),
5510 ahb_status: "X".to_string(),
5511 },
5512 AhbCodeRule {
5513 value: "Z39".to_string(),
5514 description: "".to_string(),
5515 ahb_status: "X".to_string(),
5516 },
5517 ],
5518 parent_group_ahb_status: None,
5519 segment_ahb_status: None,
5520 element_index: Some(1),
5521 component_index: Some(0),
5522 mig_number: Some("00075".to_string()),
5523 format: None,
5524 },
5525 AhbFieldRule {
5527 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
5528 name: "Referenznummer Qualifier".to_string(),
5529 ahb_status: "Muss".to_string(),
5530 codes: vec![AhbCodeRule {
5531 value: "Z33".to_string(),
5532 description: "".to_string(),
5533 ahb_status: "X".to_string(),
5534 }],
5535 parent_group_ahb_status: None,
5536 segment_ahb_status: None,
5537 element_index: Some(1),
5538 component_index: Some(0),
5539 mig_number: Some("00078".to_string()),
5540 format: None,
5541 },
5542 ],
5543 ub_definitions: BTreeMap::new(),
5544 package_conditions: Default::default(),
5545 };
5546
5547 let report = validator.validate(
5548 &[rff_z39],
5549 &workflow,
5550 &external,
5551 ValidationLevel::Conditions,
5552 );
5553
5554 let code_errors: Vec<_> = report
5555 .by_category(ValidationCategory::Code)
5556 .filter(|i| {
5557 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5558 })
5559 .collect();
5560 assert!(
5561 code_errors.is_empty(),
5562 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
5563 code_errors
5564 );
5565 }
5566
5567 #[test]
5568 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
5569 let evaluator = MockEvaluator::new(vec![]);
5575 let validator = EdifactValidator::new(evaluator);
5576 let external = NoOpExternalProvider;
5577
5578 let pia_5_z12 = OwnedSegment {
5582 id: "PIA".to_string(),
5583 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
5584 segment_number: 1,
5585 };
5586 let pia_5_srw = OwnedSegment {
5587 id: "PIA".to_string(),
5588 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
5589 segment_number: 2,
5590 };
5591
5592 let make_rules = |mig: &str, composite_code: &str| {
5593 vec![
5594 AhbFieldRule {
5595 segment_path: "SG4/SG8/PIA/4347".to_string(),
5596 name: "Produkt-ID-Funktion".to_string(),
5597 ahb_status: "Muss".to_string(),
5598 codes: vec![AhbCodeRule {
5599 value: "5".to_string(),
5600 description: "".to_string(),
5601 ahb_status: "X".to_string(),
5602 }],
5603 parent_group_ahb_status: None,
5604 segment_ahb_status: None,
5605 element_index: Some(0),
5606 component_index: Some(0),
5607 mig_number: Some(mig.to_string()),
5608 format: None,
5609 },
5610 AhbFieldRule {
5611 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
5612 name: "Artikel/Dienstleistung-Identifikator".to_string(),
5613 ahb_status: "Muss".to_string(),
5614 codes: vec![AhbCodeRule {
5615 value: composite_code.to_string(),
5616 description: "".to_string(),
5617 ahb_status: "X".to_string(),
5618 }],
5619 parent_group_ahb_status: None,
5620 segment_ahb_status: None,
5621 element_index: Some(1),
5622 component_index: Some(0),
5623 mig_number: Some(mig.to_string()),
5624 format: None,
5625 },
5626 ]
5627 };
5628
5629 let mut fields = make_rules("00108", "Z12");
5630 fields.extend(make_rules("00197", "SRW"));
5631
5632 let workflow = AhbWorkflow {
5633 pruefidentifikator: "55035".to_string(),
5634 description: "Test".to_string(),
5635 communication_direction: None,
5636 fields,
5637 ub_definitions: BTreeMap::new(),
5638 package_conditions: Default::default(),
5639 };
5640
5641 let report = validator.validate(
5642 &[pia_5_z12, pia_5_srw],
5643 &workflow,
5644 &external,
5645 ValidationLevel::Conditions,
5646 );
5647
5648 let code_errors: Vec<_> = report
5649 .by_category(ValidationCategory::Code)
5650 .filter(|i| {
5651 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
5652 })
5653 .collect();
5654 assert!(
5655 code_errors.is_empty(),
5656 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
5657 code_errors
5658 );
5659 }
5660}