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