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: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::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: &HashMap<Option<String>, MigCodeBucket>,
1240 tag_qualifiers: Option<&HashMap<(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: HashMap<(usize, usize), HashSet<&'a str>>,
2596}
2597
2598fn partition_codes_by_mig(
2603 workflow: &AhbWorkflow,
2604) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
2605 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
2606 for field in &workflow.fields {
2607 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2608 continue;
2609 }
2610 if codes_are_alternatives(field) {
2613 continue;
2614 }
2615 let tag = extract_segment_id(&field.segment_path);
2616 let group_key = extract_group_path_key(&field.segment_path);
2617 let mig = field.mig_number.clone();
2618 let el = field.element_index.unwrap_or(0);
2619 let c = field.component_index.unwrap_or(0);
2620
2621 let bucket = out
2622 .entry((group_key, tag))
2623 .or_default()
2624 .entry(mig)
2625 .or_default();
2626
2627 let required: Vec<&str> = field
2634 .codes
2635 .iter()
2636 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2637 .map(|code| code.value.as_str())
2638 .collect();
2639
2640 if !required.is_empty() {
2647 if bucket.qualifier_position.is_none() {
2648 bucket.qualifier_position = Some((el, c));
2649 }
2650 if bucket.qualifier_position == Some((el, c)) {
2651 bucket.qualifier_values.extend(required.iter().copied());
2652 }
2653 }
2654
2655 for v in required {
2656 bucket.codes.entry((el, c)).or_default().insert(v);
2657 }
2658 }
2659 out
2660}
2661
2662fn global_qualifiers_by_tag(
2668 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
2669) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
2670 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
2671 for ((_group, tag), migs) in by_loc {
2672 let tag_entry = out.entry(tag.clone()).or_default();
2673 for bucket in migs.values() {
2674 if let Some(pos) = bucket.qualifier_position {
2675 tag_entry
2676 .entry(pos)
2677 .or_default()
2678 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2679 }
2680 }
2681 }
2682 out
2683}
2684
2685fn matches_qualifier(seg: &OwnedSegment, bucket: &MigCodeBucket<'_>) -> bool {
2687 bucket.qualifier_position.is_some_and(|(el, c)| {
2688 seg.elements
2689 .get(el)
2690 .and_then(|e| e.get(c))
2691 .is_some_and(|v| bucket.qualifier_values.contains(v.as_str()))
2692 })
2693}
2694
2695fn data_slot_entries(
2701 workflow: &AhbWorkflow,
2702) -> HashMap<(String, String), Vec<(String, Option<String>)>> {
2703 let mut out: HashMap<(String, String), Vec<(String, Option<String>)>> = HashMap::new();
2704 let mut tags_by_mig: HashMap<String, BTreeMap<&str, Vec<String>>> = HashMap::new();
2709 for f in &workflow.fields {
2710 if let Some(n) = f.mig_number.as_deref() {
2711 let tags = tags_by_mig
2712 .entry(extract_group_path_key(&f.segment_path))
2713 .or_default()
2714 .entry(n)
2715 .or_default();
2716 let tag = extract_segment_id(&f.segment_path);
2717 if tags.last() != Some(&tag) {
2718 tags.push(tag);
2719 }
2720 }
2721 }
2722 let mut judged: HashSet<(&str, &str)> = HashSet::new();
2723 for field in &workflow.fields {
2724 let leading =
2725 field.element_index.unwrap_or(0) == 0 && field.component_index.unwrap_or(0) == 0;
2726 let Some(mig) = field.mig_number.as_deref() else {
2727 continue;
2728 };
2729 if !field.codes.is_empty() || !leading || !is_qualifier_field(&field.segment_path) {
2730 continue;
2731 }
2732 if !judged.insert((field.segment_path.as_str(), mig)) {
2733 continue;
2734 }
2735 let tag = extract_segment_id(&field.segment_path);
2736 let group_key = extract_group_path_key(&field.segment_path);
2737 if group_key.is_empty() {
2738 continue;
2739 }
2740 let entry = tags_by_mig.get(&group_key).and_then(|migs| {
2743 migs.range(..mig).rev().find_map(|(n, tags)| {
2744 tags.iter()
2745 .rev()
2746 .find(|t| **t != tag)
2747 .map(|t| (*n, t.clone()))
2748 })
2749 });
2750 if let Some((n, entry_tag)) = entry {
2751 let slot = (entry_tag, Some(n.to_string()));
2752 let entries = out.entry((group_key, tag)).or_default();
2753 if !entries.contains(&slot) {
2754 entries.push(slot);
2755 }
2756 }
2757 }
2758 out
2759}
2760
2761fn match_segment_to_mig<'a, 'b>(
2772 seg: &OwnedSegment,
2773 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
2774) -> Option<&'a MigCodeBucket<'b>> {
2775 let actual_at = |el: usize, c: usize| -> &str {
2776 seg.elements
2777 .get(el)
2778 .and_then(|e| e.get(c))
2779 .map(|s| s.as_str())
2780 .unwrap_or("")
2781 };
2782
2783 let mut best: Option<&MigCodeBucket> = None;
2784 let mut best_matches = 0usize;
2785
2786 for bucket in migs.values() {
2787 let Some((el, c)) = bucket.qualifier_position else {
2788 continue;
2789 };
2790 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2791 continue;
2792 }
2793 let extra_matches = bucket
2794 .codes
2795 .iter()
2796 .filter(|(pos, _)| **pos != (el, c))
2797 .filter(|((e, k), allowed)| {
2798 let v = actual_at(*e, *k);
2799 !v.is_empty() && allowed.contains(v)
2800 })
2801 .count();
2802 if best.is_none() || extra_matches > best_matches {
2803 best = Some(bucket);
2804 best_matches = extra_matches;
2805 }
2806 }
2807 best
2808}
2809
2810fn extract_group_path_key(path: &str) -> String {
2815 let sg_parts: Vec<&str> = path
2816 .split('/')
2817 .take_while(|p| p.starts_with("SG"))
2818 .collect();
2819 sg_parts.join("/")
2820}
2821
2822fn extract_segment_id(path: &str) -> String {
2824 for part in path.split('/') {
2825 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2827 continue;
2828 }
2829 if part.len() >= 3
2831 && part
2832 .chars()
2833 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2834 {
2835 return part.to_string();
2836 }
2837 }
2838 path.split('/').next_back().unwrap_or(path).to_string()
2840}
2841
2842pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2854 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2856 if unh_count > 1 {
2857 return Some(ValidationIssue::new(
2858 Severity::Error,
2859 IssueKind::UntCountNotVerifiable { unh_count },
2860 ));
2861 }
2862
2863 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2865 let declared: usize = unt.get_element(0).parse().ok()?;
2866
2867 let actual = segments
2869 .iter()
2870 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2871 .count();
2872
2873 if declared != actual {
2874 Some(
2875 ValidationIssue::new(
2876 Severity::Error,
2877 IssueKind::UntSegmentCountMismatch { declared, actual },
2878 )
2879 .with_field_path("UNT/0074")
2880 .with_expected(actual.to_string())
2881 .with_actual(declared.to_string()),
2882 )
2883 } else {
2884 None
2885 }
2886}
2887
2888#[cfg(test)]
2889mod tests {
2890
2891 #[test]
2892 fn a_numeric_length_counts_digits_only() {
2893 assert_eq!(value_length("n..15", "-12.50"), 4);
2894 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
2895 assert_eq!(value_length("n5", "55001"), 5);
2896 assert_eq!(value_length("an..3", "Z.1"), 3);
2897 assert_eq!(value_length("an..35", "Müller"), 6);
2898 }
2899 use super::super::codes::ErrorCodes;
2900 use super::super::issue::ValidationCategory;
2901 use super::*;
2902 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2903 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2904 use std::collections::HashMap;
2905
2906 fn narrate(issue: &ValidationIssue) -> String {
2909 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
2910 }
2911
2912 struct MockEvaluator {
2914 results: HashMap<u32, CR>,
2915 }
2916
2917 impl MockEvaluator {
2918 fn new(results: Vec<(u32, CR)>) -> Self {
2919 Self {
2920 results: results.into_iter().collect(),
2921 }
2922 }
2923
2924 fn all_true(ids: &[u32]) -> Self {
2925 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
2926 }
2927 }
2928
2929 impl ConditionEvaluator for MockEvaluator {
2930 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
2931 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
2932 }
2933 fn is_external(&self, _condition: u32) -> bool {
2934 false
2935 }
2936 fn message_type(&self) -> &str {
2937 "UTILMD"
2938 }
2939 fn format_version(&self) -> &str {
2940 "FV2510"
2941 }
2942 }
2943
2944 #[test]
2947 fn a_present_code_is_judged_by_its_own_status() {
2948 let code = |value: &str, status: &str| AhbCodeRule {
2950 value: value.to_string(),
2951 description: String::new(),
2952 ahb_status: status.to_string(),
2953 };
2954 let field = AhbFieldRule {
2955 segment_path: "SG4/STS/C556/9013".to_string(),
2956 ahb_status: "X [192]".to_string(),
2957 codes: vec![code("E01", "X [192]"), code("E03", "X")],
2958 ..Default::default()
2959 };
2960 assert_eq!(governing_status(&field, Some("E03")), "X");
2961 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
2962 assert_eq!(governing_status(&field, None), "X [192]");
2963 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
2964
2965 let own = AhbFieldRule {
2967 ahb_status: "X [931]".to_string(),
2968 codes: vec![code("303", "X")],
2969 ..Default::default()
2970 };
2971 assert_eq!(governing_status(&own, Some("303")), "X [931]");
2972 assert!(!codes_are_alternatives(&own));
2973
2974 let alternatives = AhbFieldRule {
2977 ahb_status: "X [153] ∧ [538]".to_string(),
2978 codes: vec![
2979 code("Z74", "X [103] ∧ [537]"),
2980 code("Z75", "X [103] ∧ [537]"),
2981 ],
2982 ..Default::default()
2983 };
2984 assert!(codes_are_alternatives(&alternatives));
2985 assert_eq!(
2986 governing_status(&alternatives, Some("Z74")),
2987 "X [103] ∧ [537]"
2988 );
2989 assert_eq!(
2990 governing_status(&alternatives, Some("A01")),
2991 "X [153] ∧ [538]"
2992 );
2993 assert_eq!(governing_status(&alternatives, None), "X [153] ∧ [538]");
2994
2995 let restricting = AhbFieldRule {
2997 ahb_status: "X [2] ∨ [3]".to_string(),
2998 codes: vec![code("UNH", "X"), code("UNT", "X")],
2999 ..Default::default()
3000 };
3001 assert!(!codes_are_alternatives(&restricting));
3002 assert_eq!(governing_status(&restricting, Some("UNH")), "X [2] ∨ [3]");
3003 }
3004
3005 #[test]
3006 fn test_extract_segment_id_simple() {
3007 assert_eq!(extract_segment_id("NAD"), "NAD");
3008 }
3009
3010 #[test]
3011 fn test_extract_segment_id_with_sg_prefix() {
3012 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
3013 }
3014
3015 #[test]
3016 fn test_extract_segment_id_nested_sg() {
3017 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
3018 }
3019
3020 #[test]
3023 fn test_validate_missing_mandatory_field() {
3024 let evaluator = MockEvaluator::all_true(&[182, 152]);
3025 let validator = EdifactValidator::new(evaluator);
3026 let external = NoOpExternalProvider;
3027
3028 let workflow = AhbWorkflow {
3029 pruefidentifikator: "11001".to_string(),
3030 description: "Test".to_string(),
3031 communication_direction: None,
3032 fields: vec![AhbFieldRule {
3033 segment_path: "SG2/NAD/C082/3039".to_string(),
3034 name: "MP-ID des MSB".to_string(),
3035 ahb_status: "Muss [182] ∧ [152]".to_string(),
3036 codes: vec![],
3037 parent_group_ahb_status: None,
3038 segment_ahb_status: None,
3039 ..Default::default()
3040 }],
3041 ub_definitions: BTreeMap::new(),
3042 package_conditions: Default::default(),
3043 };
3044
3045 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3047
3048 assert!(!report.is_valid());
3050 let errors: Vec<_> = report.errors().collect();
3051 assert_eq!(errors.len(), 1);
3052 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3053 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
3054 }
3055
3056 #[test]
3057 fn test_validate_condition_false_no_error() {
3058 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
3060 let validator = EdifactValidator::new(evaluator);
3061 let external = NoOpExternalProvider;
3062
3063 let workflow = AhbWorkflow {
3064 pruefidentifikator: "11001".to_string(),
3065 description: "Test".to_string(),
3066 communication_direction: None,
3067 fields: vec![AhbFieldRule {
3068 segment_path: "NAD".to_string(),
3069 name: "Partnerrolle".to_string(),
3070 ahb_status: "Muss [182] ∧ [152]".to_string(),
3071 codes: vec![],
3072 parent_group_ahb_status: None,
3073 segment_ahb_status: None,
3074 ..Default::default()
3075 }],
3076 ub_definitions: BTreeMap::new(),
3077 package_conditions: Default::default(),
3078 };
3079
3080 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3081
3082 assert!(report.is_valid());
3084 }
3085
3086 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
3088 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3089 let workflow = AhbWorkflow {
3090 pruefidentifikator: "11001".to_string(),
3091 description: "Test".to_string(),
3092 communication_direction: None,
3093 fields: vec![AhbFieldRule {
3094 segment_path: "SG2/NAD/C082/3039".to_string(),
3095 name: "MP-ID".to_string(),
3096 ahb_status: status.to_string(),
3097 ..Default::default()
3098 }],
3099 ub_definitions: BTreeMap::new(),
3100 package_conditions: Default::default(),
3101 };
3102 validator.validate(
3103 &[],
3104 &workflow,
3105 &NoOpExternalProvider,
3106 ValidationLevel::Conditions,
3107 )
3108 }
3109
3110 #[test]
3111 fn a_later_soll_line_alone_does_not_require_the_field() {
3112 let report = validate_absent_field(
3114 "Muss [2119] \r\nSoll [130]",
3115 vec![(2119, CR::False), (130, CR::True)],
3116 );
3117 assert!(
3118 report.is_valid(),
3119 "{:?}",
3120 report.errors().collect::<Vec<_>>()
3121 );
3122 }
3123
3124 #[test]
3125 fn a_later_muss_line_requires_the_field() {
3126 let report = validate_absent_field(
3128 "Soll [47]\r\nMuss [46]",
3129 vec![(47, CR::False), (46, CR::True)],
3130 );
3131 let errors: Vec<_> = report.errors().collect();
3132 assert_eq!(errors.len(), 1);
3133 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3134 }
3135
3136 #[test]
3137 fn a_weaker_unknown_line_does_not_stop_a_muss() {
3138 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
3140 let errors: Vec<_> = report.errors().collect();
3141 assert_eq!(errors.len(), 1);
3142 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3143 }
3144
3145 #[test]
3146 fn a_stricter_unknown_line_leaves_the_status_undecided() {
3147 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
3148 assert!(report.is_valid());
3149 let infos: Vec<_> = report.infos().collect();
3150 assert_eq!(infos.len(), 1, "{infos:?}");
3151 match &infos[0].kind {
3152 IssueKind::ConditionUnknown { unresolved, .. } => {
3153 assert_eq!(unresolved.missing, vec![2119]);
3154 }
3155 other => panic!("expected ConditionUnknown, got {other:?}"),
3156 }
3157 }
3158
3159 #[test]
3160 fn test_validate_condition_unknown_adds_info() {
3161 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
3163 let validator = EdifactValidator::new(evaluator);
3165 let external = NoOpExternalProvider;
3166
3167 let workflow = AhbWorkflow {
3168 pruefidentifikator: "11001".to_string(),
3169 description: "Test".to_string(),
3170 communication_direction: None,
3171 fields: vec![AhbFieldRule {
3172 segment_path: "NAD".to_string(),
3173 name: "Partnerrolle".to_string(),
3174 ahb_status: "Muss [182] ∧ [152]".to_string(),
3175 codes: vec![],
3176 parent_group_ahb_status: None,
3177 segment_ahb_status: None,
3178 ..Default::default()
3179 }],
3180 ub_definitions: BTreeMap::new(),
3181 package_conditions: Default::default(),
3182 };
3183
3184 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3185
3186 assert!(report.is_valid());
3188 let infos: Vec<_> = report.infos().collect();
3189 assert_eq!(infos.len(), 1);
3190 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
3191 }
3192
3193 #[test]
3194 fn test_validate_structure_level_skips_conditions() {
3195 let evaluator = MockEvaluator::all_true(&[182, 152]);
3196 let validator = EdifactValidator::new(evaluator);
3197 let external = NoOpExternalProvider;
3198
3199 let workflow = AhbWorkflow {
3200 pruefidentifikator: "11001".to_string(),
3201 description: "Test".to_string(),
3202 communication_direction: None,
3203 fields: vec![AhbFieldRule {
3204 segment_path: "NAD".to_string(),
3205 name: "Partnerrolle".to_string(),
3206 ahb_status: "Muss [182] ∧ [152]".to_string(),
3207 codes: vec![],
3208 parent_group_ahb_status: None,
3209 segment_ahb_status: None,
3210 ..Default::default()
3211 }],
3212 ub_definitions: BTreeMap::new(),
3213 package_conditions: Default::default(),
3214 };
3215
3216 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
3218
3219 assert!(report.is_valid());
3221 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
3222 }
3223
3224 #[test]
3225 fn test_validate_empty_workflow_no_condition_errors() {
3226 let evaluator = MockEvaluator::all_true(&[]);
3227 let validator = EdifactValidator::new(evaluator);
3228 let external = NoOpExternalProvider;
3229
3230 let empty_workflow = AhbWorkflow {
3231 pruefidentifikator: String::new(),
3232 description: String::new(),
3233 communication_direction: None,
3234 fields: vec![],
3235 ub_definitions: BTreeMap::new(),
3236 package_conditions: Default::default(),
3237 };
3238
3239 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
3240
3241 assert!(report.is_valid());
3242 }
3243
3244 #[test]
3245 fn test_validate_bare_muss_always_required() {
3246 let evaluator = MockEvaluator::new(vec![]);
3247 let validator = EdifactValidator::new(evaluator);
3248 let external = NoOpExternalProvider;
3249
3250 let workflow = AhbWorkflow {
3251 pruefidentifikator: "55001".to_string(),
3252 description: "Test".to_string(),
3253 communication_direction: Some("NB an LF".to_string()),
3254 fields: vec![AhbFieldRule {
3255 segment_path: "SG2/NAD/3035".to_string(),
3256 name: "Partnerrolle".to_string(),
3257 ahb_status: "Muss".to_string(), codes: vec![],
3259 parent_group_ahb_status: None,
3260 segment_ahb_status: None,
3261 ..Default::default()
3262 }],
3263 ub_definitions: BTreeMap::new(),
3264 package_conditions: Default::default(),
3265 };
3266
3267 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3268
3269 assert!(!report.is_valid());
3271 assert_eq!(report.error_count(), 1);
3272 }
3273
3274 #[test]
3275 fn test_validate_x_status_is_mandatory() {
3276 let evaluator = MockEvaluator::new(vec![]);
3277 let validator = EdifactValidator::new(evaluator);
3278 let external = NoOpExternalProvider;
3279
3280 let workflow = AhbWorkflow {
3281 pruefidentifikator: "55001".to_string(),
3282 description: "Test".to_string(),
3283 communication_direction: None,
3284 fields: vec![AhbFieldRule {
3285 segment_path: "DTM".to_string(),
3286 name: "Datum".to_string(),
3287 ahb_status: "X".to_string(),
3288 codes: vec![],
3289 parent_group_ahb_status: None,
3290 segment_ahb_status: None,
3291 ..Default::default()
3292 }],
3293 ub_definitions: BTreeMap::new(),
3294 package_conditions: Default::default(),
3295 };
3296
3297 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3298
3299 assert!(!report.is_valid());
3300 let errors: Vec<_> = report.errors().collect();
3301 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3302 }
3303
3304 #[test]
3305 fn test_validate_soll_not_mandatory() {
3306 let evaluator = MockEvaluator::new(vec![]);
3307 let validator = EdifactValidator::new(evaluator);
3308 let external = NoOpExternalProvider;
3309
3310 let workflow = AhbWorkflow {
3311 pruefidentifikator: "55001".to_string(),
3312 description: "Test".to_string(),
3313 communication_direction: None,
3314 fields: vec![AhbFieldRule {
3315 segment_path: "DTM".to_string(),
3316 name: "Datum".to_string(),
3317 ahb_status: "Soll".to_string(),
3318 codes: vec![],
3319 parent_group_ahb_status: None,
3320 segment_ahb_status: None,
3321 ..Default::default()
3322 }],
3323 ub_definitions: BTreeMap::new(),
3324 package_conditions: Default::default(),
3325 };
3326
3327 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3328
3329 assert!(report.is_valid());
3331 }
3332
3333 #[test]
3334 fn test_report_includes_metadata() {
3335 let evaluator = MockEvaluator::new(vec![]);
3336 let validator = EdifactValidator::new(evaluator);
3337 let external = NoOpExternalProvider;
3338
3339 let workflow = AhbWorkflow {
3340 pruefidentifikator: "55001".to_string(),
3341 description: String::new(),
3342 communication_direction: None,
3343 fields: vec![],
3344 ub_definitions: BTreeMap::new(),
3345 package_conditions: Default::default(),
3346 };
3347
3348 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
3349
3350 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
3351 assert_eq!(report.level, ValidationLevel::Full);
3352 assert_eq!(report.message_type, "UTILMD");
3353 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
3354 }
3355
3356 #[test]
3357 fn test_validate_with_navigator_returns_report() {
3358 let evaluator = MockEvaluator::all_true(&[]);
3359 let validator = EdifactValidator::new(evaluator);
3360 let external = NoOpExternalProvider;
3361 let nav = crate::eval::NoOpGroupNavigator;
3362
3363 let workflow = AhbWorkflow {
3364 pruefidentifikator: "55001".to_string(),
3365 description: "Test".to_string(),
3366 communication_direction: None,
3367 fields: vec![],
3368 ub_definitions: BTreeMap::new(),
3369 package_conditions: Default::default(),
3370 };
3371
3372 let report = validator.validate_with_navigator(
3373 &[],
3374 &workflow,
3375 &external,
3376 ValidationLevel::Full,
3377 &nav,
3378 );
3379 assert!(report.is_valid());
3380 }
3381
3382 #[test]
3383 fn test_code_validation_composite_paths_valid_codes() {
3384 let evaluator = MockEvaluator::new(vec![]);
3388 let validator = EdifactValidator::new(evaluator);
3389 let external = NoOpExternalProvider;
3390
3391 let unh_segment = OwnedSegment {
3392 id: "UNH".to_string(),
3393 elements: vec![
3394 vec!["ALEXANDE951842".to_string()],
3395 vec![
3396 "UTILMD".to_string(),
3397 "D".to_string(),
3398 "11A".to_string(),
3399 "UN".to_string(),
3400 "S2.1".to_string(),
3401 ],
3402 ],
3403 segment_number: 1,
3404 };
3405
3406 let workflow = AhbWorkflow {
3407 pruefidentifikator: "55001".to_string(),
3408 description: "Test".to_string(),
3409 communication_direction: None,
3410 fields: vec![
3411 AhbFieldRule {
3412 segment_path: "UNH/S009/0065".to_string(),
3413 name: "Nachrichtentyp".to_string(),
3414 ahb_status: "X".to_string(),
3415 codes: vec![AhbCodeRule {
3416 value: "UTILMD".to_string(),
3417 description: "Stammdaten".to_string(),
3418 ahb_status: "X".to_string(),
3419 }],
3420 parent_group_ahb_status: None,
3421 segment_ahb_status: None,
3422 element_index: Some(1),
3423 component_index: Some(0),
3424 ..Default::default()
3425 },
3426 AhbFieldRule {
3427 segment_path: "UNH/S009/0052".to_string(),
3428 name: "Version".to_string(),
3429 ahb_status: "X".to_string(),
3430 codes: vec![AhbCodeRule {
3431 value: "D".to_string(),
3432 description: "Draft".to_string(),
3433 ahb_status: "X".to_string(),
3434 }],
3435 parent_group_ahb_status: None,
3436 segment_ahb_status: None,
3437 element_index: Some(1),
3438 component_index: Some(1),
3439 ..Default::default()
3440 },
3441 ],
3442 ub_definitions: BTreeMap::new(),
3443 package_conditions: Default::default(),
3444 };
3445
3446 let report = validator.validate(
3447 &[unh_segment],
3448 &workflow,
3449 &external,
3450 ValidationLevel::Conditions,
3451 );
3452
3453 let code_errors: Vec<_> = report
3454 .by_category(ValidationCategory::Code)
3455 .filter(|i| i.severity == Severity::Error)
3456 .collect();
3457 assert!(
3458 code_errors.is_empty(),
3459 "Expected no code errors when composite values match allowed codes, got: {:?}",
3460 code_errors
3461 );
3462 }
3463
3464 #[test]
3465 fn test_code_validation_partitions_by_mig_number() {
3466 let evaluator = MockEvaluator::new(vec![]);
3470 let validator = EdifactValidator::new(evaluator);
3471 let external = NoOpExternalProvider;
3472
3473 let sts_7 = OwnedSegment {
3474 id: "STS".to_string(),
3475 elements: vec![
3476 vec!["7".to_string()],
3477 vec![String::new()],
3478 vec!["GH02".to_string()],
3479 vec!["ZW4".to_string()],
3480 ],
3481 segment_number: 1,
3482 };
3483 let sts_e01 = OwnedSegment {
3484 id: "STS".to_string(),
3485 elements: vec![
3486 vec!["E01".to_string()],
3487 vec![String::new()],
3488 vec!["A99".to_string(), "E_0614".to_string()],
3489 ],
3490 segment_number: 2,
3491 };
3492
3493 let workflow = AhbWorkflow {
3494 pruefidentifikator: "55018".to_string(),
3495 description: "Test".to_string(),
3496 communication_direction: None,
3497 fields: vec![
3498 AhbFieldRule {
3500 segment_path: "SG4/STS/C601/9015".to_string(),
3501 name: "Statuskategorie".to_string(),
3502 ahb_status: "X".to_string(),
3503 codes: vec![AhbCodeRule {
3504 value: "7".to_string(),
3505 description: "Transaktionsgrund".to_string(),
3506 ahb_status: "X".to_string(),
3507 }],
3508 parent_group_ahb_status: None,
3509 segment_ahb_status: None,
3510 element_index: Some(0),
3511 component_index: Some(0),
3512 mig_number: Some("00035".to_string()),
3513 format: None,
3514 },
3515 AhbFieldRule {
3516 segment_path: "SG4/STS/C556/9013".to_string(),
3517 name: "Statusanlaß".to_string(),
3518 ahb_status: "X".to_string(),
3519 codes: vec![AhbCodeRule {
3520 value: "E03".to_string(),
3521 description: "Transaktionsgrund".to_string(),
3522 ahb_status: "X".to_string(),
3523 }],
3524 parent_group_ahb_status: None,
3525 segment_ahb_status: None,
3526 element_index: Some(2),
3527 component_index: Some(0),
3528 mig_number: Some("00035".to_string()),
3529 format: None,
3530 },
3531 AhbFieldRule {
3533 segment_path: "SG4/STS/C601/9015".to_string(),
3534 name: "Statuskategorie".to_string(),
3535 ahb_status: "X".to_string(),
3536 codes: vec![AhbCodeRule {
3537 value: "E01".to_string(),
3538 description: "Antwort".to_string(),
3539 ahb_status: "X".to_string(),
3540 }],
3541 parent_group_ahb_status: None,
3542 segment_ahb_status: None,
3543 element_index: Some(0),
3544 component_index: Some(0),
3545 mig_number: Some("00036".to_string()),
3546 format: None,
3547 },
3548 ],
3549 ub_definitions: BTreeMap::new(),
3550 package_conditions: Default::default(),
3551 };
3552
3553 let report = validator.validate(
3554 &[sts_7, sts_e01],
3555 &workflow,
3556 &external,
3557 ValidationLevel::Conditions,
3558 );
3559
3560 let code_errors: Vec<_> = report
3561 .by_category(ValidationCategory::Code)
3562 .filter(|i| {
3563 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3564 })
3565 .collect();
3566 assert_eq!(
3567 code_errors.len(),
3568 1,
3569 "Expected one COD002 (for GH02 only), got: {:?}",
3570 code_errors
3571 );
3572 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3573 }
3574
3575 #[test]
3576 fn test_code_validation_composite_paths_detects_invalid_code() {
3577 let evaluator = MockEvaluator::new(vec![]);
3580 let validator = EdifactValidator::new(evaluator);
3581 let external = NoOpExternalProvider;
3582
3583 let sts_segment = OwnedSegment {
3584 id: "STS".to_string(),
3585 elements: vec![
3586 vec!["7".to_string()],
3587 vec![String::new()],
3588 vec!["GH02".to_string()],
3589 vec!["ZW4".to_string()],
3590 ],
3591 segment_number: 1,
3592 };
3593
3594 let workflow = AhbWorkflow {
3595 pruefidentifikator: "55018".to_string(),
3596 description: "Test".to_string(),
3597 communication_direction: None,
3598 fields: vec![AhbFieldRule {
3599 segment_path: "SG4/STS/C556/9013".to_string(),
3600 name: "Statusanlaß".to_string(),
3601 ahb_status: "X".to_string(),
3602 codes: vec![AhbCodeRule {
3603 value: "E03".to_string(),
3604 description: "Transaktionsgrund".to_string(),
3605 ahb_status: "X".to_string(),
3606 }],
3607 parent_group_ahb_status: None,
3608 segment_ahb_status: None,
3609 element_index: Some(2),
3610 component_index: Some(0),
3611 ..Default::default()
3612 }],
3613 ub_definitions: BTreeMap::new(),
3614 package_conditions: Default::default(),
3615 };
3616
3617 let report = validator.validate(
3618 &[sts_segment],
3619 &workflow,
3620 &external,
3621 ValidationLevel::Conditions,
3622 );
3623
3624 let code_errors: Vec<_> = report
3625 .by_category(ValidationCategory::Code)
3626 .filter(|i| {
3627 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3628 })
3629 .collect();
3630 assert_eq!(
3631 code_errors.len(),
3632 1,
3633 "Expected COD002 for GH02, got: {:?}",
3634 code_errors
3635 );
3636 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3637 }
3638
3639 #[test]
3640 fn test_cross_field_code_validation_valid_qualifiers() {
3641 let evaluator = MockEvaluator::new(vec![]);
3644 let validator = EdifactValidator::new(evaluator);
3645 let external = NoOpExternalProvider;
3646
3647 let nad_ms = OwnedSegment {
3648 id: "NAD".to_string(),
3649 elements: vec![vec!["MS".to_string()]],
3650 segment_number: 4,
3651 };
3652 let nad_mr = OwnedSegment {
3653 id: "NAD".to_string(),
3654 elements: vec![vec!["MR".to_string()]],
3655 segment_number: 5,
3656 };
3657
3658 let workflow = AhbWorkflow {
3659 pruefidentifikator: "55001".to_string(),
3660 description: "Test".to_string(),
3661 communication_direction: None,
3662 fields: vec![
3663 AhbFieldRule {
3664 segment_path: "SG2/NAD/3035".to_string(),
3665 name: "Absender".to_string(),
3666 ahb_status: "X".to_string(),
3667 codes: vec![AhbCodeRule {
3668 value: "MS".to_string(),
3669 description: "Absender".to_string(),
3670 ahb_status: "X".to_string(),
3671 }],
3672 parent_group_ahb_status: None,
3673 segment_ahb_status: None,
3674 ..Default::default()
3675 },
3676 AhbFieldRule {
3677 segment_path: "SG2/NAD/3035".to_string(),
3678 name: "Empfaenger".to_string(),
3679 ahb_status: "X".to_string(),
3680 codes: vec![AhbCodeRule {
3681 value: "MR".to_string(),
3682 description: "Empfaenger".to_string(),
3683 ahb_status: "X".to_string(),
3684 }],
3685 parent_group_ahb_status: None,
3686 segment_ahb_status: None,
3687 ..Default::default()
3688 },
3689 ],
3690 ub_definitions: BTreeMap::new(),
3691 package_conditions: Default::default(),
3692 };
3693
3694 let report = validator.validate(
3695 &[nad_ms, nad_mr],
3696 &workflow,
3697 &external,
3698 ValidationLevel::Conditions,
3699 );
3700
3701 let code_errors: Vec<_> = report
3702 .by_category(ValidationCategory::Code)
3703 .filter(|i| i.severity == Severity::Error)
3704 .collect();
3705 assert!(
3706 code_errors.is_empty(),
3707 "Expected no code errors for valid qualifiers, got: {:?}",
3708 code_errors
3709 );
3710 }
3711
3712 #[test]
3713 fn test_cross_field_code_validation_catches_invalid_qualifier() {
3714 let evaluator = MockEvaluator::new(vec![]);
3716 let validator = EdifactValidator::new(evaluator);
3717 let external = NoOpExternalProvider;
3718
3719 let nad_ms = OwnedSegment {
3720 id: "NAD".to_string(),
3721 elements: vec![vec!["MS".to_string()]],
3722 segment_number: 4,
3723 };
3724 let nad_mt = OwnedSegment {
3725 id: "NAD".to_string(),
3726 elements: vec![vec!["MT".to_string()]], segment_number: 5,
3728 };
3729
3730 let workflow = AhbWorkflow {
3731 pruefidentifikator: "55001".to_string(),
3732 description: "Test".to_string(),
3733 communication_direction: None,
3734 fields: vec![
3735 AhbFieldRule {
3736 segment_path: "SG2/NAD/3035".to_string(),
3737 name: "Absender".to_string(),
3738 ahb_status: "X".to_string(),
3739 codes: vec![AhbCodeRule {
3740 value: "MS".to_string(),
3741 description: "Absender".to_string(),
3742 ahb_status: "X".to_string(),
3743 }],
3744 parent_group_ahb_status: None,
3745 segment_ahb_status: None,
3746 ..Default::default()
3747 },
3748 AhbFieldRule {
3749 segment_path: "SG2/NAD/3035".to_string(),
3750 name: "Empfaenger".to_string(),
3751 ahb_status: "X".to_string(),
3752 codes: vec![AhbCodeRule {
3753 value: "MR".to_string(),
3754 description: "Empfaenger".to_string(),
3755 ahb_status: "X".to_string(),
3756 }],
3757 parent_group_ahb_status: None,
3758 segment_ahb_status: None,
3759 ..Default::default()
3760 },
3761 ],
3762 ub_definitions: BTreeMap::new(),
3763 package_conditions: Default::default(),
3764 };
3765
3766 let report = validator.validate(
3767 &[nad_ms, nad_mt],
3768 &workflow,
3769 &external,
3770 ValidationLevel::Conditions,
3771 );
3772
3773 let code_errors: Vec<_> = report
3774 .by_category(ValidationCategory::Code)
3775 .filter(|i| i.severity == Severity::Error)
3776 .collect();
3777 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
3778 assert!(narrate(code_errors[0]).contains("MT"));
3779 assert!(narrate(code_errors[0]).contains("MR"));
3780 assert!(narrate(code_errors[0]).contains("MS"));
3781 }
3782
3783 #[test]
3784 fn test_cross_field_code_validation_unions_across_groups() {
3785 let evaluator = MockEvaluator::new(vec![]);
3789 let validator = EdifactValidator::new(evaluator);
3790 let external = NoOpExternalProvider;
3791
3792 let segments = vec![
3793 OwnedSegment {
3794 id: "NAD".to_string(),
3795 elements: vec![vec!["MS".to_string()]],
3796 segment_number: 3,
3797 },
3798 OwnedSegment {
3799 id: "NAD".to_string(),
3800 elements: vec![vec!["MR".to_string()]],
3801 segment_number: 4,
3802 },
3803 OwnedSegment {
3804 id: "NAD".to_string(),
3805 elements: vec![vec!["Z04".to_string()]],
3806 segment_number: 20,
3807 },
3808 OwnedSegment {
3809 id: "NAD".to_string(),
3810 elements: vec![vec!["Z09".to_string()]],
3811 segment_number: 21,
3812 },
3813 OwnedSegment {
3814 id: "NAD".to_string(),
3815 elements: vec![vec!["MT".to_string()]], segment_number: 22,
3817 },
3818 ];
3819
3820 let workflow = AhbWorkflow {
3821 pruefidentifikator: "55001".to_string(),
3822 description: "Test".to_string(),
3823 communication_direction: None,
3824 fields: vec![
3825 AhbFieldRule {
3826 segment_path: "SG2/NAD/3035".to_string(),
3827 name: "Absender".to_string(),
3828 ahb_status: "X".to_string(),
3829 codes: vec![AhbCodeRule {
3830 value: "MS".to_string(),
3831 description: "Absender".to_string(),
3832 ahb_status: "X".to_string(),
3833 }],
3834 parent_group_ahb_status: None,
3835 segment_ahb_status: None,
3836 ..Default::default()
3837 },
3838 AhbFieldRule {
3839 segment_path: "SG2/NAD/3035".to_string(),
3840 name: "Empfaenger".to_string(),
3841 ahb_status: "X".to_string(),
3842 codes: vec![AhbCodeRule {
3843 value: "MR".to_string(),
3844 description: "Empfaenger".to_string(),
3845 ahb_status: "X".to_string(),
3846 }],
3847 parent_group_ahb_status: None,
3848 segment_ahb_status: None,
3849 ..Default::default()
3850 },
3851 AhbFieldRule {
3852 segment_path: "SG4/SG12/NAD/3035".to_string(),
3853 name: "Anschlussnutzer".to_string(),
3854 ahb_status: "X".to_string(),
3855 codes: vec![AhbCodeRule {
3856 value: "Z04".to_string(),
3857 description: "Anschlussnutzer".to_string(),
3858 ahb_status: "X".to_string(),
3859 }],
3860 parent_group_ahb_status: None,
3861 segment_ahb_status: None,
3862 ..Default::default()
3863 },
3864 AhbFieldRule {
3865 segment_path: "SG4/SG12/NAD/3035".to_string(),
3866 name: "Korrespondenzanschrift".to_string(),
3867 ahb_status: "X".to_string(),
3868 codes: vec![AhbCodeRule {
3869 value: "Z09".to_string(),
3870 description: "Korrespondenzanschrift".to_string(),
3871 ahb_status: "X".to_string(),
3872 }],
3873 parent_group_ahb_status: None,
3874 segment_ahb_status: None,
3875 ..Default::default()
3876 },
3877 ],
3878 ub_definitions: BTreeMap::new(),
3879 package_conditions: Default::default(),
3880 };
3881
3882 let report =
3883 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3884
3885 let code_errors: Vec<_> = report
3886 .by_category(ValidationCategory::Code)
3887 .filter(|i| i.severity == Severity::Error)
3888 .collect();
3889 assert_eq!(
3890 code_errors.len(),
3891 1,
3892 "Expected exactly one COD002 error for MT, got: {:?}",
3893 code_errors
3894 );
3895 assert!(narrate(code_errors[0]).contains("MT"));
3896 }
3897
3898 #[test]
3899 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3900 let evaluator = MockEvaluator::new(vec![]);
3909 let validator = EdifactValidator::new(evaluator);
3910 let external = NoOpExternalProvider;
3911
3912 let qty_67 = OwnedSegment {
3913 id: "QTY".to_string(),
3914 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
3915 segment_number: 10,
3916 };
3917
3918 let workflow = AhbWorkflow {
3919 pruefidentifikator: "13025".to_string(),
3920 description: "Test".to_string(),
3921 communication_direction: None,
3922 fields: vec![AhbFieldRule {
3923 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
3924 name: "Menge, Qualifier".to_string(),
3925 ahb_status: "X".to_string(),
3926 codes: vec![
3927 AhbCodeRule {
3928 value: "220".to_string(),
3929 description: "Wahrer Wert".to_string(),
3930 ahb_status: "X".to_string(),
3931 },
3932 AhbCodeRule {
3933 value: "67".to_string(),
3934 description: "Ersatzwert".to_string(),
3935 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
3936 },
3937 AhbCodeRule {
3938 value: "Z18".to_string(),
3939 description: "Vorläufiger Wert".to_string(),
3940 ahb_status: "X [35]".to_string(),
3941 },
3942 ],
3943 parent_group_ahb_status: None,
3944 segment_ahb_status: None,
3945 element_index: Some(0),
3946 component_index: Some(0),
3947 ..Default::default()
3948 }],
3949 ub_definitions: BTreeMap::new(),
3950 package_conditions: Default::default(),
3951 };
3952
3953 let report =
3954 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
3955
3956 let code_errors: Vec<_> = report
3957 .by_category(ValidationCategory::Code)
3958 .filter(|i| i.severity == Severity::Error)
3959 .collect();
3960 assert!(
3961 code_errors.is_empty(),
3962 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
3963 code_errors
3964 );
3965 }
3966
3967 #[test]
3968 fn test_is_qualifier_field_simple_paths() {
3969 assert!(is_qualifier_field("NAD/3035"));
3970 assert!(is_qualifier_field("SG2/NAD/3035"));
3971 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
3972 assert!(is_qualifier_field("LOC/3227"));
3973 }
3974
3975 #[test]
3976 fn test_is_qualifier_field_composite_paths() {
3977 assert!(is_qualifier_field("UNH/S009/0065"));
3981 assert!(is_qualifier_field("NAD/C082/3039"));
3982 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
3983 assert!(is_qualifier_field("SG4/STS/C556/9013"));
3984 }
3985
3986 #[test]
3987 fn test_is_qualifier_field_bare_segment() {
3988 assert!(!is_qualifier_field("NAD"));
3989 assert!(!is_qualifier_field("SG2/NAD"));
3990 }
3991
3992 #[test]
3993 fn test_is_qualifier_field_rejects_deep_paths() {
3994 assert!(!is_qualifier_field("SEG/A/B/C/D"));
3996 }
3997
3998 #[test]
3999 fn test_missing_qualifier_instance_is_detected() {
4000 let evaluator = MockEvaluator::new(vec![]);
4003 let validator = EdifactValidator::new(evaluator);
4004 let external = NoOpExternalProvider;
4005
4006 let nad_ms = OwnedSegment {
4007 id: "NAD".to_string(),
4008 elements: vec![vec!["MS".to_string()]],
4009 segment_number: 3,
4010 };
4011
4012 let workflow = AhbWorkflow {
4013 pruefidentifikator: "55001".to_string(),
4014 description: "Test".to_string(),
4015 communication_direction: None,
4016 fields: vec![
4017 AhbFieldRule {
4018 segment_path: "SG2/NAD/3035".to_string(),
4019 name: "Absender".to_string(),
4020 ahb_status: "X".to_string(),
4021 codes: vec![AhbCodeRule {
4022 value: "MS".to_string(),
4023 description: "Absender".to_string(),
4024 ahb_status: "X".to_string(),
4025 }],
4026 parent_group_ahb_status: None,
4027 segment_ahb_status: None,
4028 ..Default::default()
4029 },
4030 AhbFieldRule {
4031 segment_path: "SG2/NAD/3035".to_string(),
4032 name: "Empfaenger".to_string(),
4033 ahb_status: "Muss".to_string(),
4034 codes: vec![AhbCodeRule {
4035 value: "MR".to_string(),
4036 description: "Empfaenger".to_string(),
4037 ahb_status: "X".to_string(),
4038 }],
4039 parent_group_ahb_status: None,
4040 segment_ahb_status: None,
4041 ..Default::default()
4042 },
4043 ],
4044 ub_definitions: BTreeMap::new(),
4045 package_conditions: Default::default(),
4046 };
4047
4048 let report =
4049 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
4050
4051 let ahb_errors: Vec<_> = report
4052 .by_category(ValidationCategory::Ahb)
4053 .filter(|i| i.severity == Severity::Error)
4054 .collect();
4055 assert_eq!(
4056 ahb_errors.len(),
4057 1,
4058 "Expected AHB001 for missing NAD+MR, got: {:?}",
4059 ahb_errors
4060 );
4061 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4062 }
4063
4064 #[test]
4065 fn test_present_qualifier_instance_no_error() {
4066 let evaluator = MockEvaluator::new(vec![]);
4068 let validator = EdifactValidator::new(evaluator);
4069 let external = NoOpExternalProvider;
4070
4071 let segments = vec![
4072 OwnedSegment {
4073 id: "NAD".to_string(),
4074 elements: vec![vec!["MS".to_string()]],
4075 segment_number: 3,
4076 },
4077 OwnedSegment {
4078 id: "NAD".to_string(),
4079 elements: vec![vec!["MR".to_string()]],
4080 segment_number: 4,
4081 },
4082 ];
4083
4084 let workflow = AhbWorkflow {
4085 pruefidentifikator: "55001".to_string(),
4086 description: "Test".to_string(),
4087 communication_direction: None,
4088 fields: vec![
4089 AhbFieldRule {
4090 segment_path: "SG2/NAD/3035".to_string(),
4091 name: "Absender".to_string(),
4092 ahb_status: "Muss".to_string(),
4093 codes: vec![AhbCodeRule {
4094 value: "MS".to_string(),
4095 description: "Absender".to_string(),
4096 ahb_status: "X".to_string(),
4097 }],
4098 parent_group_ahb_status: None,
4099 segment_ahb_status: None,
4100 ..Default::default()
4101 },
4102 AhbFieldRule {
4103 segment_path: "SG2/NAD/3035".to_string(),
4104 name: "Empfaenger".to_string(),
4105 ahb_status: "Muss".to_string(),
4106 codes: vec![AhbCodeRule {
4107 value: "MR".to_string(),
4108 description: "Empfaenger".to_string(),
4109 ahb_status: "X".to_string(),
4110 }],
4111 parent_group_ahb_status: None,
4112 segment_ahb_status: None,
4113 ..Default::default()
4114 },
4115 ],
4116 ub_definitions: BTreeMap::new(),
4117 package_conditions: Default::default(),
4118 };
4119
4120 let report =
4121 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
4122
4123 let ahb_errors: Vec<_> = report
4124 .by_category(ValidationCategory::Ahb)
4125 .filter(|i| i.severity == Severity::Error)
4126 .collect();
4127 assert!(
4128 ahb_errors.is_empty(),
4129 "Expected no AHB001 errors, got: {:?}",
4130 ahb_errors
4131 );
4132 }
4133
4134 #[test]
4135 fn test_extract_group_path_key() {
4136 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
4137 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
4138 assert_eq!(extract_group_path_key("NAD/3035"), "");
4139 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
4140 }
4141
4142 fn validate_field_of_absent_group(
4144 group_status: &str,
4145 conditions: Vec<(u32, CR)>,
4146 ) -> ValidationReport {
4147 use mig_types::navigator::GroupNavigator;
4148
4149 struct NoSg9;
4150 impl GroupNavigator for NoSg9 {
4151 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4152 vec![]
4153 }
4154 fn find_segments_with_qualifier_in_group(
4155 &self,
4156 _: &str,
4157 _: usize,
4158 _: &str,
4159 _: &[&str],
4160 _: usize,
4161 ) -> Vec<OwnedSegment> {
4162 vec![]
4163 }
4164 fn group_instance_count(&self, _: &[&str]) -> usize {
4165 0
4166 }
4167 }
4168
4169 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
4170 let workflow = AhbWorkflow {
4171 pruefidentifikator: "44001".to_string(),
4172 description: "Test".to_string(),
4173 communication_direction: None,
4174 fields: vec![AhbFieldRule {
4175 segment_path: "SG9/QTY/C186/6060".to_string(),
4176 name: "Menge".to_string(),
4177 ahb_status: "X".to_string(),
4178 parent_group_ahb_status: Some(group_status.to_string()),
4179 ..Default::default()
4180 }],
4181 ub_definitions: BTreeMap::new(),
4182 package_conditions: Default::default(),
4183 };
4184 validator.validate_with_navigator(
4185 &[],
4186 &workflow,
4187 &NoOpExternalProvider,
4188 ValidationLevel::Conditions,
4189 &NoSg9,
4190 )
4191 }
4192
4193 #[test]
4194 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
4195 let report = validate_field_of_absent_group(
4197 "Soll [47]\r\nMuss [46]",
4198 vec![(47, CR::True), (46, CR::False)],
4199 );
4200 assert!(
4201 report.is_valid(),
4202 "{:?}",
4203 report.errors().collect::<Vec<_>>()
4204 );
4205 }
4206
4207 #[test]
4208 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
4209 let report = validate_field_of_absent_group(
4210 "Soll [47]\r\nMuss [46]",
4211 vec![(47, CR::False), (46, CR::True)],
4212 );
4213 let errors: Vec<_> = report.errors().collect();
4214 assert_eq!(errors.len(), 1, "{errors:?}");
4215 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
4216 }
4217
4218 #[test]
4219 fn test_absent_optional_group_no_missing_field_error() {
4220 use mig_types::navigator::GroupNavigator;
4223
4224 struct NavWithoutSG3;
4225 impl GroupNavigator for NavWithoutSG3 {
4226 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4227 vec![]
4228 }
4229 fn find_segments_with_qualifier_in_group(
4230 &self,
4231 _: &str,
4232 _: usize,
4233 _: &str,
4234 _: &[&str],
4235 _: usize,
4236 ) -> Vec<OwnedSegment> {
4237 vec![]
4238 }
4239 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4240 match group_path {
4241 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
4244 }
4245 }
4246 }
4247
4248 let evaluator = MockEvaluator::new(vec![]);
4249 let validator = EdifactValidator::new(evaluator);
4250 let external = NoOpExternalProvider;
4251 let nav = NavWithoutSG3;
4252
4253 let segments = vec![
4255 OwnedSegment {
4256 id: "NAD".into(),
4257 elements: vec![vec!["MS".into()]],
4258 segment_number: 3,
4259 },
4260 OwnedSegment {
4261 id: "NAD".into(),
4262 elements: vec![vec!["MR".into()]],
4263 segment_number: 4,
4264 },
4265 ];
4266
4267 let workflow = AhbWorkflow {
4268 pruefidentifikator: "55001".to_string(),
4269 description: "Test".to_string(),
4270 communication_direction: None,
4271 fields: vec![
4272 AhbFieldRule {
4273 segment_path: "SG2/SG3/CTA/3139".to_string(),
4274 name: "Funktion des Ansprechpartners, Code".to_string(),
4275 ahb_status: "Muss".to_string(),
4276 codes: vec![],
4277 parent_group_ahb_status: None,
4278 segment_ahb_status: None,
4279 ..Default::default()
4280 },
4281 AhbFieldRule {
4282 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
4283 name: "Name vom Ansprechpartner".to_string(),
4284 ahb_status: "X".to_string(),
4285 codes: vec![],
4286 parent_group_ahb_status: None,
4287 segment_ahb_status: None,
4288 ..Default::default()
4289 },
4290 ],
4291 ub_definitions: BTreeMap::new(),
4292 package_conditions: Default::default(),
4293 };
4294
4295 let report = validator.validate_with_navigator(
4296 &segments,
4297 &workflow,
4298 &external,
4299 ValidationLevel::Conditions,
4300 &nav,
4301 );
4302
4303 let ahb_errors: Vec<_> = report
4304 .by_category(ValidationCategory::Ahb)
4305 .filter(|i| i.severity == Severity::Error)
4306 .collect();
4307 assert!(
4308 ahb_errors.is_empty(),
4309 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
4310 ahb_errors
4311 );
4312 }
4313
4314 #[test]
4315 fn test_present_group_still_checks_mandatory_fields() {
4316 use mig_types::navigator::GroupNavigator;
4318
4319 struct NavWithSG3;
4320 impl GroupNavigator for NavWithSG3 {
4321 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4322 vec![]
4323 }
4324 fn find_segments_with_qualifier_in_group(
4325 &self,
4326 _: &str,
4327 _: usize,
4328 _: &str,
4329 _: &[&str],
4330 _: usize,
4331 ) -> Vec<OwnedSegment> {
4332 vec![]
4333 }
4334 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4335 match group_path {
4336 ["SG2"] => 1,
4337 ["SG2", "SG3"] => 1, _ => 0,
4339 }
4340 }
4341 }
4342
4343 let evaluator = MockEvaluator::new(vec![]);
4344 let validator = EdifactValidator::new(evaluator);
4345 let external = NoOpExternalProvider;
4346 let nav = NavWithSG3;
4347
4348 let segments = vec![OwnedSegment {
4350 id: "NAD".into(),
4351 elements: vec![vec!["MS".into()]],
4352 segment_number: 3,
4353 }];
4354
4355 let workflow = AhbWorkflow {
4356 pruefidentifikator: "55001".to_string(),
4357 description: "Test".to_string(),
4358 communication_direction: None,
4359 fields: vec![AhbFieldRule {
4360 segment_path: "SG2/SG3/CTA/3139".to_string(),
4361 name: "Funktion des Ansprechpartners, Code".to_string(),
4362 ahb_status: "Muss".to_string(),
4363 codes: vec![],
4364 parent_group_ahb_status: None,
4365 segment_ahb_status: None,
4366 ..Default::default()
4367 }],
4368 ub_definitions: BTreeMap::new(),
4369 package_conditions: Default::default(),
4370 };
4371
4372 let report = validator.validate_with_navigator(
4373 &segments,
4374 &workflow,
4375 &external,
4376 ValidationLevel::Conditions,
4377 &nav,
4378 );
4379
4380 let ahb_errors: Vec<_> = report
4381 .by_category(ValidationCategory::Ahb)
4382 .filter(|i| i.severity == Severity::Error)
4383 .collect();
4384 assert_eq!(
4385 ahb_errors.len(),
4386 1,
4387 "Expected AHB001 error when SG3 is present but CTA missing"
4388 );
4389 assert!(narrate(ahb_errors[0]).contains("CTA"));
4390 }
4391
4392 #[test]
4393 fn test_missing_qualifier_with_navigator_is_detected() {
4394 use mig_types::navigator::GroupNavigator;
4397
4398 struct NavWithSG2;
4399 impl GroupNavigator for NavWithSG2 {
4400 fn find_segments_in_group(
4401 &self,
4402 segment_id: &str,
4403 group_path: &[&str],
4404 instance_index: usize,
4405 ) -> Vec<OwnedSegment> {
4406 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
4407 vec![OwnedSegment {
4408 id: "NAD".into(),
4409 elements: vec![vec!["MS".into()]],
4410 segment_number: 3,
4411 }]
4412 } else {
4413 vec![]
4414 }
4415 }
4416 fn find_segments_with_qualifier_in_group(
4417 &self,
4418 _: &str,
4419 _: usize,
4420 _: &str,
4421 _: &[&str],
4422 _: usize,
4423 ) -> Vec<OwnedSegment> {
4424 vec![]
4425 }
4426 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4427 match group_path {
4428 ["SG2"] => 1,
4429 _ => 0,
4430 }
4431 }
4432 }
4433
4434 let evaluator = MockEvaluator::new(vec![]);
4435 let validator = EdifactValidator::new(evaluator);
4436 let external = NoOpExternalProvider;
4437 let nav = NavWithSG2;
4438
4439 let segments = vec![OwnedSegment {
4440 id: "NAD".into(),
4441 elements: vec![vec!["MS".into()]],
4442 segment_number: 3,
4443 }];
4444
4445 let workflow = AhbWorkflow {
4446 pruefidentifikator: "55001".to_string(),
4447 description: "Test".to_string(),
4448 communication_direction: None,
4449 fields: vec![
4450 AhbFieldRule {
4451 segment_path: "SG2/NAD/3035".to_string(),
4452 name: "Absender".to_string(),
4453 ahb_status: "X".to_string(),
4454 codes: vec![AhbCodeRule {
4455 value: "MS".to_string(),
4456 description: "Absender".to_string(),
4457 ahb_status: "X".to_string(),
4458 }],
4459 parent_group_ahb_status: None,
4460 segment_ahb_status: None,
4461 ..Default::default()
4462 },
4463 AhbFieldRule {
4464 segment_path: "SG2/NAD/3035".to_string(),
4465 name: "Empfaenger".to_string(),
4466 ahb_status: "Muss".to_string(),
4467 codes: vec![AhbCodeRule {
4468 value: "MR".to_string(),
4469 description: "Empfaenger".to_string(),
4470 ahb_status: "X".to_string(),
4471 }],
4472 parent_group_ahb_status: None,
4473 segment_ahb_status: None,
4474 ..Default::default()
4475 },
4476 ],
4477 ub_definitions: BTreeMap::new(),
4478 package_conditions: Default::default(),
4479 };
4480
4481 let report = validator.validate_with_navigator(
4482 &segments,
4483 &workflow,
4484 &external,
4485 ValidationLevel::Conditions,
4486 &nav,
4487 );
4488
4489 let ahb_errors: Vec<_> = report
4490 .by_category(ValidationCategory::Ahb)
4491 .filter(|i| i.severity == Severity::Error)
4492 .collect();
4493 assert_eq!(
4494 ahb_errors.len(),
4495 1,
4496 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
4497 ahb_errors
4498 );
4499 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4500 }
4501
4502 #[test]
4503 fn test_optional_group_variant_absent_no_error() {
4504 use mig_types::navigator::GroupNavigator;
4509
4510 struct TestNav;
4511 impl GroupNavigator for TestNav {
4512 fn find_segments_in_group(
4513 &self,
4514 segment_id: &str,
4515 group_path: &[&str],
4516 instance_index: usize,
4517 ) -> Vec<OwnedSegment> {
4518 match (segment_id, group_path, instance_index) {
4519 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
4520 id: "LOC".into(),
4521 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4522 segment_number: 10,
4523 }],
4524 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
4525 id: "NAD".into(),
4526 elements: vec![vec!["MS".into()]],
4527 segment_number: 3,
4528 }],
4529 _ => vec![],
4530 }
4531 }
4532 fn find_segments_with_qualifier_in_group(
4533 &self,
4534 _: &str,
4535 _: usize,
4536 _: &str,
4537 _: &[&str],
4538 _: usize,
4539 ) -> Vec<OwnedSegment> {
4540 vec![]
4541 }
4542 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4543 match group_path {
4544 ["SG2"] => 1,
4545 ["SG4"] => 1,
4546 ["SG4", "SG5"] => 1, _ => 0,
4548 }
4549 }
4550 }
4551
4552 let evaluator = MockEvaluator::new(vec![]);
4553 let validator = EdifactValidator::new(evaluator);
4554 let external = NoOpExternalProvider;
4555 let nav = TestNav;
4556
4557 let segments = vec![
4558 OwnedSegment {
4559 id: "NAD".into(),
4560 elements: vec![vec!["MS".into()]],
4561 segment_number: 3,
4562 },
4563 OwnedSegment {
4564 id: "LOC".into(),
4565 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4566 segment_number: 10,
4567 },
4568 ];
4569
4570 let workflow = AhbWorkflow {
4571 pruefidentifikator: "55001".to_string(),
4572 description: "Test".to_string(),
4573 communication_direction: None,
4574 fields: vec![
4575 AhbFieldRule {
4577 segment_path: "SG2/NAD/3035".to_string(),
4578 name: "Absender".to_string(),
4579 ahb_status: "X".to_string(),
4580 codes: vec![AhbCodeRule {
4581 value: "MS".to_string(),
4582 description: "Absender".to_string(),
4583 ahb_status: "X".to_string(),
4584 }],
4585 parent_group_ahb_status: Some("Muss".to_string()),
4586 segment_ahb_status: None,
4587 ..Default::default()
4588 },
4589 AhbFieldRule {
4590 segment_path: "SG2/NAD/3035".to_string(),
4591 name: "Empfaenger".to_string(),
4592 ahb_status: "Muss".to_string(),
4593 codes: vec![AhbCodeRule {
4594 value: "MR".to_string(),
4595 description: "Empfaenger".to_string(),
4596 ahb_status: "X".to_string(),
4597 }],
4598 parent_group_ahb_status: Some("Muss".to_string()),
4599 segment_ahb_status: None,
4600 ..Default::default()
4601 },
4602 AhbFieldRule {
4604 segment_path: "SG4/SG5/LOC/3227".to_string(),
4605 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4606 ahb_status: "X".to_string(),
4607 codes: vec![AhbCodeRule {
4608 value: "Z16".to_string(),
4609 description: "Marktlokation".to_string(),
4610 ahb_status: "X".to_string(),
4611 }],
4612 parent_group_ahb_status: Some("Kann".to_string()),
4613 segment_ahb_status: None,
4614 ..Default::default()
4615 },
4616 AhbFieldRule {
4617 segment_path: "SG4/SG5/LOC/3227".to_string(),
4618 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4619 ahb_status: "Muss".to_string(),
4620 codes: vec![AhbCodeRule {
4621 value: "Z17".to_string(),
4622 description: "Messlokation".to_string(),
4623 ahb_status: "X".to_string(),
4624 }],
4625 parent_group_ahb_status: Some("Kann".to_string()),
4626 segment_ahb_status: None,
4627 ..Default::default()
4628 },
4629 ],
4630 ub_definitions: BTreeMap::new(),
4631 package_conditions: Default::default(),
4632 };
4633
4634 let report = validator.validate_with_navigator(
4635 &segments,
4636 &workflow,
4637 &external,
4638 ValidationLevel::Conditions,
4639 &nav,
4640 );
4641
4642 let ahb_errors: Vec<_> = report
4643 .by_category(ValidationCategory::Ahb)
4644 .filter(|i| i.severity == Severity::Error)
4645 .collect();
4646
4647 assert_eq!(
4650 ahb_errors.len(),
4651 1,
4652 "Expected only AHB001 for missing NAD+MR, got: {:?}",
4653 ahb_errors
4654 );
4655 assert!(
4656 narrate(ahb_errors[0]).contains("Empfaenger"),
4657 "Error should be for missing NAD+MR (Empfaenger)"
4658 );
4659 }
4660
4661 #[test]
4665 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4666 use mig_types::navigator::GroupNavigator;
4667
4668 struct TestNav;
4669 impl GroupNavigator for TestNav {
4670 fn find_segments_in_group(
4671 &self,
4672 segment_id: &str,
4673 group_path: &[&str],
4674 instance_index: usize,
4675 ) -> Vec<OwnedSegment> {
4676 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4677 vec![OwnedSegment {
4678 id: "SEQ".into(),
4679 elements: vec![vec!["Z03".into()]],
4680 segment_number: 10,
4681 }]
4682 } else {
4683 vec![]
4684 }
4685 }
4686 fn find_segments_with_qualifier_in_group(
4687 &self,
4688 _: &str,
4689 _: usize,
4690 _: &str,
4691 _: &[&str],
4692 _: usize,
4693 ) -> Vec<OwnedSegment> {
4694 vec![]
4695 }
4696 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4697 match group_path {
4698 ["SG4"] => 1,
4699 ["SG4", "SG8"] => 1,
4700 _ => 0,
4701 }
4702 }
4703 }
4704
4705 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
4706 let validator = EdifactValidator::new(evaluator);
4707 let segments = vec![OwnedSegment {
4708 id: "SEQ".into(),
4709 elements: vec![vec!["Z03".into()]],
4710 segment_number: 10,
4711 }];
4712 let seq = |code: &str, group: &str| AhbFieldRule {
4713 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4714 name: format!("Handlung, Code ({code})"),
4715 ahb_status: "X".to_string(),
4716 codes: vec![AhbCodeRule {
4717 value: code.to_string(),
4718 description: String::new(),
4719 ahb_status: "X".to_string(),
4720 }],
4721 parent_group_ahb_status: Some(group.to_string()),
4722 segment_ahb_status: None,
4723 ..Default::default()
4724 };
4725 let workflow = AhbWorkflow {
4726 pruefidentifikator: "44143".to_string(),
4727 description: "Test".to_string(),
4728 communication_direction: None,
4729 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
4730 ub_definitions: BTreeMap::new(),
4731 package_conditions: Default::default(),
4732 };
4733
4734 let report = validator.validate_with_navigator(
4735 &segments,
4736 &workflow,
4737 &NoOpExternalProvider,
4738 ValidationLevel::Conditions,
4739 &TestNav,
4740 );
4741 let missing: Vec<_> = report
4742 .issues
4743 .iter()
4744 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
4745 .collect();
4746 assert!(missing.is_empty(), "{missing:#?}");
4747 }
4748
4749 #[test]
4750 fn test_conditional_group_variant_absent_no_error() {
4751 use mig_types::navigator::GroupNavigator;
4756
4757 struct TestNav;
4758 impl GroupNavigator for TestNav {
4759 fn find_segments_in_group(
4760 &self,
4761 segment_id: &str,
4762 group_path: &[&str],
4763 instance_index: usize,
4764 ) -> Vec<OwnedSegment> {
4765 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
4766 vec![OwnedSegment {
4767 id: "LOC".into(),
4768 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4769 segment_number: 10,
4770 }]
4771 } else {
4772 vec![]
4773 }
4774 }
4775 fn find_segments_with_qualifier_in_group(
4776 &self,
4777 _: &str,
4778 _: usize,
4779 _: &str,
4780 _: &[&str],
4781 _: usize,
4782 ) -> Vec<OwnedSegment> {
4783 vec![]
4784 }
4785 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4786 match group_path {
4787 ["SG4"] => 1,
4788 ["SG4", "SG5"] => 1, _ => 0,
4790 }
4791 }
4792 }
4793
4794 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
4797 let validator = EdifactValidator::new(evaluator);
4798 let external = NoOpExternalProvider;
4799 let nav = TestNav;
4800
4801 let segments = vec![OwnedSegment {
4802 id: "LOC".into(),
4803 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4804 segment_number: 10,
4805 }];
4806
4807 let workflow = AhbWorkflow {
4808 pruefidentifikator: "55001".to_string(),
4809 description: "Test".to_string(),
4810 communication_direction: None,
4811 fields: vec![
4812 AhbFieldRule {
4814 segment_path: "SG4/SG5/LOC/3227".to_string(),
4815 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4816 ahb_status: "X".to_string(),
4817 codes: vec![AhbCodeRule {
4818 value: "Z16".to_string(),
4819 description: "Marktlokation".to_string(),
4820 ahb_status: "X".to_string(),
4821 }],
4822 parent_group_ahb_status: Some("Muss [2061]".to_string()),
4823 segment_ahb_status: None,
4824 ..Default::default()
4825 },
4826 AhbFieldRule {
4828 segment_path: "SG4/SG5/LOC/3227".to_string(),
4829 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4830 ahb_status: "X".to_string(),
4831 codes: vec![AhbCodeRule {
4832 value: "Z17".to_string(),
4833 description: "Messlokation".to_string(),
4834 ahb_status: "X".to_string(),
4835 }],
4836 parent_group_ahb_status: Some("Soll [165]".to_string()),
4837 segment_ahb_status: None,
4838 ..Default::default()
4839 },
4840 ],
4841 ub_definitions: BTreeMap::new(),
4842 package_conditions: Default::default(),
4843 };
4844
4845 let report = validator.validate_with_navigator(
4846 &segments,
4847 &workflow,
4848 &external,
4849 ValidationLevel::Conditions,
4850 &nav,
4851 );
4852
4853 let ahb_errors: Vec<_> = report
4854 .by_category(ValidationCategory::Ahb)
4855 .filter(|i| i.severity == Severity::Error)
4856 .collect();
4857
4858 assert!(
4860 ahb_errors.is_empty(),
4861 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4862 ahb_errors
4863 );
4864 }
4865
4866 #[test]
4867 fn test_conditional_group_variant_unknown_no_error() {
4868 let evaluator = MockEvaluator::new(vec![]);
4874 let validator = EdifactValidator::new(evaluator);
4875 let external = NoOpExternalProvider;
4876
4877 let workflow = AhbWorkflow {
4878 pruefidentifikator: "55001".to_string(),
4879 description: "Test".to_string(),
4880 communication_direction: None,
4881 fields: vec![AhbFieldRule {
4882 segment_path: "SG4/SG5/LOC/3227".to_string(),
4883 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4884 ahb_status: "X".to_string(),
4885 codes: vec![AhbCodeRule {
4886 value: "Z17".to_string(),
4887 description: "Messlokation".to_string(),
4888 ahb_status: "X".to_string(),
4889 }],
4890 parent_group_ahb_status: Some("Soll [165]".to_string()),
4891 segment_ahb_status: None,
4892 ..Default::default()
4893 }],
4894 ub_definitions: BTreeMap::new(),
4895 package_conditions: Default::default(),
4896 };
4897
4898 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4899
4900 let ahb_errors: Vec<_> = report
4901 .by_category(ValidationCategory::Ahb)
4902 .filter(|i| i.severity == Severity::Error)
4903 .collect();
4904
4905 assert!(
4907 ahb_errors.is_empty(),
4908 "Expected no errors when parent group condition is Unknown, got: {:?}",
4909 ahb_errors
4910 );
4911 }
4912
4913 #[test]
4914 fn test_segment_absent_within_present_group_no_error() {
4915 use mig_types::navigator::GroupNavigator;
4919
4920 struct TestNav;
4921 impl GroupNavigator for TestNav {
4922 fn find_segments_in_group(
4923 &self,
4924 segment_id: &str,
4925 group_path: &[&str],
4926 instance_index: usize,
4927 ) -> Vec<OwnedSegment> {
4928 if segment_id == "QTY"
4930 && group_path == ["SG5", "SG6", "SG9", "SG10"]
4931 && instance_index == 0
4932 {
4933 vec![OwnedSegment {
4934 id: "QTY".into(),
4935 elements: vec![vec!["220".into(), "0".into()]],
4936 segment_number: 14,
4937 }]
4938 } else {
4939 vec![]
4940 }
4941 }
4942 fn find_segments_with_qualifier_in_group(
4943 &self,
4944 _: &str,
4945 _: usize,
4946 _: &str,
4947 _: &[&str],
4948 _: usize,
4949 ) -> Vec<OwnedSegment> {
4950 vec![]
4951 }
4952 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4953 match group_path {
4954 ["SG5"] => 1,
4955 ["SG5", "SG6"] => 1,
4956 ["SG5", "SG6", "SG9"] => 1,
4957 ["SG5", "SG6", "SG9", "SG10"] => 1,
4958 _ => 0,
4959 }
4960 }
4961 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
4962 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
4964 }
4965 }
4966
4967 let evaluator = MockEvaluator::all_true(&[]);
4968 let validator = EdifactValidator::new(evaluator);
4969 let external = NoOpExternalProvider;
4970 let nav = TestNav;
4971
4972 let segments = vec![OwnedSegment {
4973 id: "QTY".into(),
4974 elements: vec![vec!["220".into(), "0".into()]],
4975 segment_number: 14,
4976 }];
4977
4978 let workflow = AhbWorkflow {
4979 pruefidentifikator: "13017".to_string(),
4980 description: "Test".to_string(),
4981 communication_direction: None,
4982 fields: vec![
4983 AhbFieldRule {
4985 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
4986 name: "Statuskategorie, Code".to_string(),
4987 ahb_status: "X".to_string(),
4988 codes: vec![],
4989 parent_group_ahb_status: Some("Muss".to_string()),
4990 segment_ahb_status: None,
4991 ..Default::default()
4992 },
4993 AhbFieldRule {
4995 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
4996 name: "Statusanlaß, Code".to_string(),
4997 ahb_status: "X [5]".to_string(),
4998 codes: vec![],
4999 parent_group_ahb_status: Some("Muss".to_string()),
5000 segment_ahb_status: None,
5001 ..Default::default()
5002 },
5003 ],
5004 ub_definitions: BTreeMap::new(),
5005 package_conditions: Default::default(),
5006 };
5007
5008 let report = validator.validate_with_navigator(
5009 &segments,
5010 &workflow,
5011 &external,
5012 ValidationLevel::Conditions,
5013 &nav,
5014 );
5015
5016 let ahb_errors: Vec<_> = report
5017 .by_category(ValidationCategory::Ahb)
5018 .filter(|i| i.severity == Severity::Error)
5019 .collect();
5020
5021 assert!(
5022 ahb_errors.is_empty(),
5023 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
5024 ahb_errors
5025 );
5026 }
5027
5028 #[test]
5029 fn test_group_scoped_code_validation_with_navigator() {
5030 use mig_types::navigator::GroupNavigator;
5034
5035 struct TestNav;
5036 impl GroupNavigator for TestNav {
5037 fn find_segments_in_group(
5038 &self,
5039 segment_id: &str,
5040 group_path: &[&str],
5041 _instance_index: usize,
5042 ) -> Vec<OwnedSegment> {
5043 if segment_id != "NAD" {
5044 return vec![];
5045 }
5046 match group_path {
5047 ["SG2"] => vec![
5048 OwnedSegment {
5049 id: "NAD".into(),
5050 elements: vec![vec!["MS".into()]],
5051 segment_number: 3,
5052 },
5053 OwnedSegment {
5054 id: "NAD".into(),
5055 elements: vec![vec!["MT".into()]], segment_number: 4,
5057 },
5058 ],
5059 ["SG4", "SG12"] => vec![
5060 OwnedSegment {
5061 id: "NAD".into(),
5062 elements: vec![vec!["Z04".into()]],
5063 segment_number: 20,
5064 },
5065 OwnedSegment {
5066 id: "NAD".into(),
5067 elements: vec![vec!["Z09".into()]],
5068 segment_number: 21,
5069 },
5070 ],
5071 _ => vec![],
5072 }
5073 }
5074 fn find_segments_with_qualifier_in_group(
5075 &self,
5076 _: &str,
5077 _: usize,
5078 _: &str,
5079 _: &[&str],
5080 _: usize,
5081 ) -> Vec<OwnedSegment> {
5082 vec![]
5083 }
5084 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5085 match group_path {
5086 ["SG2"] | ["SG4", "SG12"] => 1,
5087 _ => 0,
5088 }
5089 }
5090 }
5091
5092 let evaluator = MockEvaluator::new(vec![]);
5093 let validator = EdifactValidator::new(evaluator);
5094 let external = NoOpExternalProvider;
5095 let nav = TestNav;
5096
5097 let workflow = AhbWorkflow {
5098 pruefidentifikator: "55001".to_string(),
5099 description: "Test".to_string(),
5100 communication_direction: None,
5101 fields: vec![
5102 AhbFieldRule {
5103 segment_path: "SG2/NAD/3035".to_string(),
5104 name: "Absender".to_string(),
5105 ahb_status: "X".to_string(),
5106 codes: vec![AhbCodeRule {
5107 value: "MS".to_string(),
5108 description: "Absender".to_string(),
5109 ahb_status: "X".to_string(),
5110 }],
5111 parent_group_ahb_status: None,
5112 segment_ahb_status: None,
5113 ..Default::default()
5114 },
5115 AhbFieldRule {
5116 segment_path: "SG2/NAD/3035".to_string(),
5117 name: "Empfaenger".to_string(),
5118 ahb_status: "X".to_string(),
5119 codes: vec![AhbCodeRule {
5120 value: "MR".to_string(),
5121 description: "Empfaenger".to_string(),
5122 ahb_status: "X".to_string(),
5123 }],
5124 parent_group_ahb_status: None,
5125 segment_ahb_status: None,
5126 ..Default::default()
5127 },
5128 AhbFieldRule {
5129 segment_path: "SG4/SG12/NAD/3035".to_string(),
5130 name: "Anschlussnutzer".to_string(),
5131 ahb_status: "X".to_string(),
5132 codes: vec![AhbCodeRule {
5133 value: "Z04".to_string(),
5134 description: "Anschlussnutzer".to_string(),
5135 ahb_status: "X".to_string(),
5136 }],
5137 parent_group_ahb_status: None,
5138 segment_ahb_status: None,
5139 ..Default::default()
5140 },
5141 AhbFieldRule {
5142 segment_path: "SG4/SG12/NAD/3035".to_string(),
5143 name: "Korrespondenzanschrift".to_string(),
5144 ahb_status: "X".to_string(),
5145 codes: vec![AhbCodeRule {
5146 value: "Z09".to_string(),
5147 description: "Korrespondenzanschrift".to_string(),
5148 ahb_status: "X".to_string(),
5149 }],
5150 parent_group_ahb_status: None,
5151 segment_ahb_status: None,
5152 ..Default::default()
5153 },
5154 ],
5155 ub_definitions: BTreeMap::new(),
5156 package_conditions: Default::default(),
5157 };
5158
5159 let all_segments = vec![
5161 OwnedSegment {
5162 id: "NAD".into(),
5163 elements: vec![vec!["MS".into()]],
5164 segment_number: 3,
5165 },
5166 OwnedSegment {
5167 id: "NAD".into(),
5168 elements: vec![vec!["MT".into()]],
5169 segment_number: 4,
5170 },
5171 OwnedSegment {
5172 id: "NAD".into(),
5173 elements: vec![vec!["Z04".into()]],
5174 segment_number: 20,
5175 },
5176 OwnedSegment {
5177 id: "NAD".into(),
5178 elements: vec![vec!["Z09".into()]],
5179 segment_number: 21,
5180 },
5181 ];
5182
5183 let report = validator.validate_with_navigator(
5184 &all_segments,
5185 &workflow,
5186 &external,
5187 ValidationLevel::Conditions,
5188 &nav,
5189 );
5190
5191 let code_errors: Vec<_> = report
5192 .by_category(ValidationCategory::Code)
5193 .filter(|i| i.severity == Severity::Error)
5194 .collect();
5195
5196 assert_eq!(
5199 code_errors.len(),
5200 1,
5201 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
5202 code_errors
5203 );
5204 assert!(narrate(code_errors[0]).contains("MT"));
5205 assert!(narrate(code_errors[0]).contains("MR"));
5207 assert!(narrate(code_errors[0]).contains("MS"));
5208 assert!(
5209 !narrate(code_errors[0]).contains("Z04"),
5210 "SG4/SG12 codes should not leak into SG2 error"
5211 );
5212 assert!(
5214 code_errors[0]
5215 .field_path
5216 .as_deref()
5217 .unwrap_or("")
5218 .contains("SG2"),
5219 "Error field_path should reference SG2, got: {:?}",
5220 code_errors[0].field_path
5221 );
5222 }
5223
5224 #[test]
5227 fn test_package_cardinality_within_bounds() {
5228 let evaluator = MockEvaluator::all_true(&[]);
5230 let validator = EdifactValidator::new(evaluator);
5231 let external = NoOpExternalProvider;
5232
5233 let segments = vec![OwnedSegment {
5234 id: "STS".into(),
5235 elements: vec![
5236 vec!["Z33".into()], vec![], vec!["E01".into()], ],
5240 segment_number: 5,
5241 }];
5242
5243 let workflow = AhbWorkflow {
5244 pruefidentifikator: "13017".to_string(),
5245 description: "Test".to_string(),
5246 communication_direction: None,
5247 ub_definitions: BTreeMap::new(),
5248 package_conditions: Default::default(),
5249 fields: vec![AhbFieldRule {
5250 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5251 name: "Statusanlaß, Code".to_string(),
5252 ahb_status: "X".to_string(),
5253 element_index: Some(2),
5254 component_index: Some(0),
5255 codes: vec![
5256 AhbCodeRule {
5257 value: "E01".into(),
5258 description: "Code 1".into(),
5259 ahb_status: "X [4P0..1]".into(),
5260 },
5261 AhbCodeRule {
5262 value: "E02".into(),
5263 description: "Code 2".into(),
5264 ahb_status: "X [4P0..1]".into(),
5265 },
5266 ],
5267 parent_group_ahb_status: Some("Muss".to_string()),
5268 segment_ahb_status: None,
5269 mig_number: None,
5270 format: None,
5271 }],
5272 };
5273
5274 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5275 let pkg_errors: Vec<_> = report
5276 .by_category(ValidationCategory::Ahb)
5277 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5278 .collect();
5279 assert!(
5280 pkg_errors.is_empty(),
5281 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
5282 pkg_errors
5283 );
5284 }
5285
5286 #[test]
5287 fn test_package_cardinality_zero_present_min_zero() {
5288 let evaluator = MockEvaluator::all_true(&[]);
5290 let validator = EdifactValidator::new(evaluator);
5291 let external = NoOpExternalProvider;
5292
5293 let segments = vec![OwnedSegment {
5294 id: "STS".into(),
5295 elements: vec![
5296 vec!["Z33".into()],
5297 vec![],
5298 vec!["X99".into()], ],
5300 segment_number: 5,
5301 }];
5302
5303 let workflow = AhbWorkflow {
5304 pruefidentifikator: "13017".to_string(),
5305 description: "Test".to_string(),
5306 communication_direction: None,
5307 ub_definitions: BTreeMap::new(),
5308 package_conditions: Default::default(),
5309 fields: vec![AhbFieldRule {
5310 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5311 name: "Statusanlaß, Code".to_string(),
5312 ahb_status: "X".to_string(),
5313 element_index: Some(2),
5314 component_index: Some(0),
5315 codes: vec![
5316 AhbCodeRule {
5317 value: "E01".into(),
5318 description: "Code 1".into(),
5319 ahb_status: "X [4P0..1]".into(),
5320 },
5321 AhbCodeRule {
5322 value: "E02".into(),
5323 description: "Code 2".into(),
5324 ahb_status: "X [4P0..1]".into(),
5325 },
5326 ],
5327 parent_group_ahb_status: Some("Muss".to_string()),
5328 segment_ahb_status: None,
5329 mig_number: None,
5330 format: None,
5331 }],
5332 };
5333
5334 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5335 let pkg_errors: Vec<_> = report
5336 .by_category(ValidationCategory::Ahb)
5337 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5338 .collect();
5339 assert!(
5340 pkg_errors.is_empty(),
5341 "0 codes, min=0 — no error expected, got: {:?}",
5342 pkg_errors
5343 );
5344 }
5345
5346 #[test]
5347 fn test_package_cardinality_too_many() {
5348 let evaluator = MockEvaluator::all_true(&[]);
5350 let validator = EdifactValidator::new(evaluator);
5351 let external = NoOpExternalProvider;
5352
5353 let segments = vec![
5355 OwnedSegment {
5356 id: "STS".into(),
5357 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
5358 segment_number: 5,
5359 },
5360 OwnedSegment {
5361 id: "STS".into(),
5362 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
5363 segment_number: 6,
5364 },
5365 ];
5366
5367 let workflow = AhbWorkflow {
5368 pruefidentifikator: "13017".to_string(),
5369 description: "Test".to_string(),
5370 communication_direction: None,
5371 ub_definitions: BTreeMap::new(),
5372 package_conditions: Default::default(),
5373 fields: vec![AhbFieldRule {
5374 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5375 name: "Statusanlaß, Code".to_string(),
5376 ahb_status: "X".to_string(),
5377 element_index: Some(2),
5378 component_index: Some(0),
5379 codes: vec![
5380 AhbCodeRule {
5381 value: "E01".into(),
5382 description: "Code 1".into(),
5383 ahb_status: "X [4P0..1]".into(),
5384 },
5385 AhbCodeRule {
5386 value: "E02".into(),
5387 description: "Code 2".into(),
5388 ahb_status: "X [4P0..1]".into(),
5389 },
5390 ],
5391 parent_group_ahb_status: Some("Muss".to_string()),
5392 segment_ahb_status: None,
5393 mig_number: None,
5394 format: None,
5395 }],
5396 };
5397
5398 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5399 let pkg_errors: Vec<_> = report
5400 .by_category(ValidationCategory::Ahb)
5401 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5402 .collect();
5403 assert_eq!(
5404 pkg_errors.len(),
5405 1,
5406 "2 codes present, max=1 — expected 1 error, got: {:?}",
5407 pkg_errors
5408 );
5409 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
5410 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
5411 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
5412 }
5413
5414 #[test]
5415 fn test_package_cardinality_too_few() {
5416 let evaluator = MockEvaluator::all_true(&[]);
5418 let validator = EdifactValidator::new(evaluator);
5419 let external = NoOpExternalProvider;
5420
5421 let segments = vec![OwnedSegment {
5422 id: "STS".into(),
5423 elements: vec![
5424 vec!["Z33".into()],
5425 vec![],
5426 vec!["X99".into()], ],
5428 segment_number: 5,
5429 }];
5430
5431 let workflow = AhbWorkflow {
5432 pruefidentifikator: "13017".to_string(),
5433 description: "Test".to_string(),
5434 communication_direction: None,
5435 ub_definitions: BTreeMap::new(),
5436 package_conditions: Default::default(),
5437 fields: vec![AhbFieldRule {
5438 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5439 name: "Statusanlaß, Code".to_string(),
5440 ahb_status: "X".to_string(),
5441 element_index: Some(2),
5442 component_index: Some(0),
5443 codes: vec![
5444 AhbCodeRule {
5445 value: "E01".into(),
5446 description: "Code 1".into(),
5447 ahb_status: "X [5P1..3]".into(),
5448 },
5449 AhbCodeRule {
5450 value: "E02".into(),
5451 description: "Code 2".into(),
5452 ahb_status: "X [5P1..3]".into(),
5453 },
5454 AhbCodeRule {
5455 value: "E03".into(),
5456 description: "Code 3".into(),
5457 ahb_status: "X [5P1..3]".into(),
5458 },
5459 ],
5460 parent_group_ahb_status: Some("Muss".to_string()),
5461 segment_ahb_status: None,
5462 mig_number: None,
5463 format: None,
5464 }],
5465 };
5466
5467 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5468 let pkg_errors: Vec<_> = report
5469 .by_category(ValidationCategory::Ahb)
5470 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5471 .collect();
5472 assert_eq!(
5473 pkg_errors.len(),
5474 1,
5475 "0 codes present, min=1 — expected 1 error, got: {:?}",
5476 pkg_errors
5477 );
5478 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
5479 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
5480 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
5481 }
5482
5483 #[test]
5484 fn test_package_cardinality_no_packages_in_workflow() {
5485 let evaluator = MockEvaluator::all_true(&[]);
5487 let validator = EdifactValidator::new(evaluator);
5488 let external = NoOpExternalProvider;
5489
5490 let segments = vec![OwnedSegment {
5491 id: "STS".into(),
5492 elements: vec![vec!["E01".into()]],
5493 segment_number: 5,
5494 }];
5495
5496 let workflow = AhbWorkflow {
5497 pruefidentifikator: "13017".to_string(),
5498 description: "Test".to_string(),
5499 communication_direction: None,
5500 ub_definitions: BTreeMap::new(),
5501 package_conditions: Default::default(),
5502 fields: vec![AhbFieldRule {
5503 segment_path: "STS/9015".to_string(),
5504 name: "Status Code".to_string(),
5505 ahb_status: "X".to_string(),
5506 codes: vec![AhbCodeRule {
5507 value: "E01".into(),
5508 description: "Code 1".into(),
5509 ahb_status: "X".into(),
5510 }],
5511 parent_group_ahb_status: Some("Muss".to_string()),
5512 segment_ahb_status: None,
5513 ..Default::default()
5514 }],
5515 };
5516
5517 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5518 let pkg_errors: Vec<_> = report
5519 .by_category(ValidationCategory::Ahb)
5520 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5521 .collect();
5522 assert!(
5523 pkg_errors.is_empty(),
5524 "No packages in workflow — no errors expected"
5525 );
5526 }
5527
5528 #[test]
5529 fn test_package_cardinality_with_condition_and_package() {
5530 let evaluator = MockEvaluator::all_true(&[901]);
5532 let validator = EdifactValidator::new(evaluator);
5533 let external = NoOpExternalProvider;
5534
5535 let segments = vec![OwnedSegment {
5536 id: "STS".into(),
5537 elements: vec![vec![], vec![], vec!["E01".into()]],
5538 segment_number: 5,
5539 }];
5540
5541 let workflow = AhbWorkflow {
5542 pruefidentifikator: "13017".to_string(),
5543 description: "Test".to_string(),
5544 communication_direction: None,
5545 ub_definitions: BTreeMap::new(),
5546 package_conditions: Default::default(),
5547 fields: vec![AhbFieldRule {
5548 segment_path: "SG10/STS/C556/9013".to_string(),
5549 name: "Code".to_string(),
5550 ahb_status: "X".to_string(),
5551 element_index: Some(2),
5552 component_index: Some(0),
5553 codes: vec![
5554 AhbCodeRule {
5555 value: "E01".into(),
5556 description: "Code 1".into(),
5557 ahb_status: "X [901] [4P0..1]".into(),
5558 },
5559 AhbCodeRule {
5560 value: "E02".into(),
5561 description: "Code 2".into(),
5562 ahb_status: "X [901] [4P0..1]".into(),
5563 },
5564 ],
5565 parent_group_ahb_status: Some("Muss".to_string()),
5566 segment_ahb_status: None,
5567 mig_number: None,
5568 format: None,
5569 }],
5570 };
5571
5572 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5573 let pkg_errors: Vec<_> = report
5574 .by_category(ValidationCategory::Ahb)
5575 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5576 .collect();
5577 assert!(
5578 pkg_errors.is_empty(),
5579 "1 code within [4P0..1] bounds — no error, got: {:?}",
5580 pkg_errors
5581 );
5582 }
5583
5584 #[test]
5585 fn test_package_binds_only_where_its_condition_holds() {
5586 let report_for = |package_condition: CR| {
5590 let validator =
5591 EdifactValidator::new(MockEvaluator::new(vec![(902, package_condition)]));
5592 let segments = vec![OwnedSegment {
5593 id: "STS".into(),
5594 elements: vec![vec![], vec![], vec!["E09".into()]],
5595 segment_number: 5,
5596 }];
5597 let workflow = AhbWorkflow {
5598 pruefidentifikator: "13017".to_string(),
5599 description: "Test".to_string(),
5600 communication_direction: None,
5601 ub_definitions: BTreeMap::new(),
5602 package_conditions: BTreeMap::from([(5, ConditionExpr::Ref(902))]),
5603 fields: vec![AhbFieldRule {
5604 segment_path: "SG10/STS/C556/9013".to_string(),
5605 name: "Code".to_string(),
5606 ahb_status: "X".to_string(),
5607 element_index: Some(2),
5608 component_index: Some(0),
5609 codes: vec![
5610 AhbCodeRule {
5611 value: "E01".into(),
5612 description: "Code 1".into(),
5613 ahb_status: "X [5P1..1]".into(),
5614 },
5615 AhbCodeRule {
5616 value: "E09".into(),
5617 description: "Code 9".into(),
5618 ahb_status: "X".into(),
5619 },
5620 ],
5621 parent_group_ahb_status: Some("Muss".to_string()),
5622 segment_ahb_status: None,
5623 mig_number: None,
5624 format: None,
5625 }],
5626 };
5627 validator
5628 .validate(
5629 &segments,
5630 &workflow,
5631 &NoOpExternalProvider,
5632 ValidationLevel::Full,
5633 )
5634 .by_category(ValidationCategory::Ahb)
5635 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5636 .count()
5637 };
5638 assert_eq!(report_for(CR::False), 0, "[902] false: [5P] does not bind");
5639 assert_eq!(report_for(CR::True), 1, "[902] true: [5P1..1] binds");
5640 }
5641
5642 #[test]
5643 fn test_package_cardinality_scoped_per_group_instance() {
5644 use mig_types::navigator::GroupNavigator;
5652
5653 struct TwoSg10s {
5654 sts_a: OwnedSegment,
5655 sts_b: OwnedSegment,
5656 }
5657 impl GroupNavigator for TwoSg10s {
5658 fn find_segments_in_group(
5659 &self,
5660 segment_id: &str,
5661 group_path: &[&str],
5662 instance_index: usize,
5663 ) -> Vec<OwnedSegment> {
5664 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
5665 match instance_index {
5666 0 => vec![self.sts_a.clone()],
5667 1 => vec![self.sts_b.clone()],
5668 _ => vec![],
5669 }
5670 } else {
5671 vec![]
5672 }
5673 }
5674 fn find_segments_with_qualifier_in_group(
5675 &self,
5676 _: &str,
5677 _: usize,
5678 _: &str,
5679 _: &[&str],
5680 _: usize,
5681 ) -> Vec<OwnedSegment> {
5682 vec![]
5683 }
5684 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5685 match group_path {
5686 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
5687 ["SG5", "SG6", "SG9", "SG10"] => 2,
5688 _ => 0,
5689 }
5690 }
5691 }
5692
5693 let sts_a = OwnedSegment {
5694 id: "STS".into(),
5695 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5696 segment_number: 10,
5697 };
5698 let sts_b = OwnedSegment {
5699 id: "STS".into(),
5700 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5701 segment_number: 15,
5702 };
5703 let nav = TwoSg10s {
5704 sts_a: sts_a.clone(),
5705 sts_b: sts_b.clone(),
5706 };
5707
5708 let evaluator = MockEvaluator::all_true(&[]);
5709 let validator = EdifactValidator::new(evaluator);
5710 let external = NoOpExternalProvider;
5711
5712 let workflow = AhbWorkflow {
5713 pruefidentifikator: "13025".to_string(),
5714 description: "Test".to_string(),
5715 communication_direction: None,
5716 ub_definitions: BTreeMap::new(),
5717 package_conditions: Default::default(),
5718 fields: vec![AhbFieldRule {
5719 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5720 name: "Statusanlaß, Code".to_string(),
5721 ahb_status: "X".to_string(),
5722 element_index: Some(2),
5723 component_index: Some(0),
5724 codes: vec![
5725 AhbCodeRule {
5726 value: "E01".into(),
5727 description: "Code 1".into(),
5728 ahb_status: "X [4P0..1]".into(),
5729 },
5730 AhbCodeRule {
5731 value: "E02".into(),
5732 description: "Code 2".into(),
5733 ahb_status: "X [4P0..1]".into(),
5734 },
5735 ],
5736 parent_group_ahb_status: Some("Muss".to_string()),
5737 segment_ahb_status: None,
5738 mig_number: None,
5739 format: None,
5740 }],
5741 };
5742
5743 let report = validator.validate_with_navigator(
5744 &[sts_a, sts_b],
5745 &workflow,
5746 &external,
5747 ValidationLevel::Full,
5748 &nav,
5749 );
5750 let pkg_errors: Vec<_> = report
5751 .by_category(ValidationCategory::Ahb)
5752 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5753 .collect();
5754 assert!(
5755 pkg_errors.is_empty(),
5756 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
5757 pkg_errors
5758 );
5759 }
5760
5761 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
5762 OwnedSegment {
5763 id: id.to_string(),
5764 elements: elements
5765 .into_iter()
5766 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
5767 .collect(),
5768 segment_number: 0,
5769 }
5770 }
5771
5772 #[test]
5783 fn test_package_cardinality_scoped_to_rule_mig_variant() {
5784 use mig_types::navigator::GroupNavigator;
5785
5786 struct TwoSg8Variants {
5787 seq_z01: OwnedSegment,
5788 seq_z45: OwnedSegment,
5789 }
5790 impl GroupNavigator for TwoSg8Variants {
5791 fn find_segments_in_group(
5792 &self,
5793 segment_id: &str,
5794 group_path: &[&str],
5795 instance_index: usize,
5796 ) -> Vec<OwnedSegment> {
5797 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
5798 match instance_index {
5799 0 => vec![self.seq_z01.clone()],
5800 1 => vec![self.seq_z45.clone()],
5801 _ => vec![],
5802 }
5803 } else {
5804 vec![]
5805 }
5806 }
5807 fn find_segments_with_qualifier_in_group(
5808 &self,
5809 _: &str,
5810 _: usize,
5811 _: &str,
5812 _: &[&str],
5813 _: usize,
5814 ) -> Vec<OwnedSegment> {
5815 vec![]
5816 }
5817 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5818 match group_path {
5819 ["SG4"] => 1,
5820 ["SG4", "SG8"] => 2,
5821 _ => 0,
5822 }
5823 }
5824 fn instance_has_mig_number(
5825 &self,
5826 group_path: &[&str],
5827 instance_index: usize,
5828 mig_number: &str,
5829 ) -> bool {
5830 if group_path != ["SG4", "SG8"] {
5831 return true;
5832 }
5833 match (instance_index, mig_number) {
5834 (0, "00115") => true,
5835 (0, _) => false,
5836 (1, "00171") => true,
5837 (1, _) => false,
5838 _ => false,
5839 }
5840 }
5841 }
5842
5843 let seq_z01 = OwnedSegment {
5844 id: "SEQ".into(),
5845 elements: vec![vec!["Z01".into()], vec!["1".into()]],
5846 segment_number: 10,
5847 };
5848 let seq_z45 = OwnedSegment {
5849 id: "SEQ".into(),
5850 elements: vec![vec!["Z45".into()], vec!["1".into()]],
5851 segment_number: 20,
5852 };
5853 let nav = TwoSg8Variants {
5854 seq_z01: seq_z01.clone(),
5855 seq_z45: seq_z45.clone(),
5856 };
5857
5858 let evaluator = MockEvaluator::all_true(&[]);
5859 let validator = EdifactValidator::new(evaluator);
5860 let external = NoOpExternalProvider;
5861
5862 let workflow = AhbWorkflow {
5863 pruefidentifikator: "55218".to_string(),
5864 description: "Test".to_string(),
5865 communication_direction: None,
5866 ub_definitions: BTreeMap::new(),
5867 package_conditions: Default::default(),
5868 fields: vec![AhbFieldRule {
5869 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5870 name: "Handlung, Code".to_string(),
5871 ahb_status: "X".to_string(),
5872 element_index: Some(0),
5873 component_index: Some(0),
5874 codes: vec![
5875 AhbCodeRule {
5876 value: "Z45".into(),
5877 description: "NNA".into(),
5878 ahb_status: "X [1P1..4294967295]".into(),
5879 },
5880 AhbCodeRule {
5881 value: "Z84".into(),
5882 description: "Differenz-NNA".into(),
5883 ahb_status: "X [1P0..4294967295]".into(),
5884 },
5885 ],
5886 parent_group_ahb_status: Some("Muss".to_string()),
5887 segment_ahb_status: None,
5888 mig_number: Some("00171".to_string()),
5889 format: None,
5890 }],
5891 };
5892
5893 let report = validator.validate_with_navigator(
5894 &[seq_z01, seq_z45],
5895 &workflow,
5896 &external,
5897 ValidationLevel::Full,
5898 &nav,
5899 );
5900 let pkg_errors: Vec<_> = report
5901 .by_category(ValidationCategory::Ahb)
5902 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5903 .collect();
5904 assert!(
5905 pkg_errors.is_empty(),
5906 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
5907 pkg_errors
5908 );
5909 }
5910
5911 #[test]
5912 fn test_unt_count_correct() {
5913 let segments = vec![
5915 make_segment("UNH", vec![vec!["001"]]),
5916 make_segment("BGM", vec![vec!["E01"]]),
5917 make_segment("DTM", vec![vec!["137", "20250401"]]),
5918 make_segment("UNT", vec![vec!["4", "001"]]),
5919 ];
5920 assert!(
5921 validate_unt_segment_count(&segments).is_none(),
5922 "Correct count should produce no issue"
5923 );
5924 }
5925
5926 #[test]
5927 fn test_unt_count_mismatch() {
5928 let segments = vec![
5930 make_segment("UNH", vec![vec!["001"]]),
5931 make_segment("BGM", vec![vec!["E01"]]),
5932 make_segment("UNT", vec![vec!["5", "001"]]),
5933 ];
5934 let issue =
5935 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
5936 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5937 assert_eq!(issue.severity, Severity::Error);
5938 assert!(narrate(&issue).contains("declared 5"));
5939 assert!(narrate(&issue).contains("actual 3"));
5940 }
5941
5942 #[test]
5943 fn test_unt_count_excludes_envelope() {
5944 let segments = vec![
5946 make_segment("UNA", vec![]),
5947 make_segment("UNB", vec![vec!["UNOC", "3"]]),
5948 make_segment("UNH", vec![vec!["001"]]),
5949 make_segment("BGM", vec![vec!["E01"]]),
5950 make_segment("UNT", vec![vec!["3", "001"]]),
5951 make_segment("UNZ", vec![vec!["1"]]),
5952 ];
5953 assert!(
5954 validate_unt_segment_count(&segments).is_none(),
5955 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
5956 );
5957 }
5958
5959 #[test]
5960 fn test_unt_count_no_unt_returns_none() {
5961 let segments = vec![
5962 make_segment("UNH", vec![vec!["001"]]),
5963 make_segment("BGM", vec![vec!["E01"]]),
5964 ];
5965 assert!(
5966 validate_unt_segment_count(&segments).is_none(),
5967 "No UNT segment should return None (not our problem)"
5968 );
5969 }
5970
5971 #[test]
5972 fn test_unt_count_rejects_multi_message_input() {
5973 let segments = vec![
5975 make_segment("UNH", vec![vec!["001"]]),
5976 make_segment("BGM", vec![vec!["E01"]]),
5977 make_segment("UNT", vec![vec!["3", "001"]]),
5978 make_segment("UNH", vec![vec!["002"]]),
5979 make_segment("BGM", vec![vec!["E02"]]),
5980 make_segment("UNT", vec![vec!["3", "002"]]),
5981 ];
5982 let issue = validate_unt_segment_count(&segments)
5983 .expect("Multi-message input should produce an error");
5984 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
5985 assert!(
5986 narrate(&issue).contains("2 UNH"),
5987 "Should mention UNH count: {}",
5988 narrate(&issue)
5989 );
5990 }
5991
5992 #[test]
5993 fn test_code_validation_accepts_multi_code_variant_qualifier() {
5994 let evaluator = MockEvaluator::new(vec![]);
6001 let validator = EdifactValidator::new(evaluator);
6002 let external = NoOpExternalProvider;
6003
6004 let rff_z39 = OwnedSegment {
6005 id: "RFF".to_string(),
6006 elements: vec![
6007 vec!["RFF".to_string()],
6008 vec!["Z39".to_string(), "REF1".to_string()],
6009 ],
6010 segment_number: 1,
6011 };
6012
6013 let workflow = AhbWorkflow {
6014 pruefidentifikator: "55035".to_string(),
6015 description: "Test".to_string(),
6016 communication_direction: None,
6017 fields: vec![
6018 AhbFieldRule {
6020 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6021 name: "Referenznummer Qualifier".to_string(),
6022 ahb_status: "Muss".to_string(),
6023 codes: vec![
6024 AhbCodeRule {
6025 value: "Z31".to_string(),
6026 description: "".to_string(),
6027 ahb_status: "X".to_string(),
6028 },
6029 AhbCodeRule {
6030 value: "Z39".to_string(),
6031 description: "".to_string(),
6032 ahb_status: "X".to_string(),
6033 },
6034 ],
6035 parent_group_ahb_status: None,
6036 segment_ahb_status: None,
6037 element_index: Some(1),
6038 component_index: Some(0),
6039 mig_number: Some("00075".to_string()),
6040 format: None,
6041 },
6042 AhbFieldRule {
6044 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6045 name: "Referenznummer Qualifier".to_string(),
6046 ahb_status: "Muss".to_string(),
6047 codes: vec![AhbCodeRule {
6048 value: "Z33".to_string(),
6049 description: "".to_string(),
6050 ahb_status: "X".to_string(),
6051 }],
6052 parent_group_ahb_status: None,
6053 segment_ahb_status: None,
6054 element_index: Some(1),
6055 component_index: Some(0),
6056 mig_number: Some("00078".to_string()),
6057 format: None,
6058 },
6059 ],
6060 ub_definitions: BTreeMap::new(),
6061 package_conditions: Default::default(),
6062 };
6063
6064 let report = validator.validate(
6065 &[rff_z39],
6066 &workflow,
6067 &external,
6068 ValidationLevel::Conditions,
6069 );
6070
6071 let code_errors: Vec<_> = report
6072 .by_category(ValidationCategory::Code)
6073 .filter(|i| {
6074 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6075 })
6076 .collect();
6077 assert!(
6078 code_errors.is_empty(),
6079 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
6080 code_errors
6081 );
6082 }
6083
6084 #[test]
6085 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
6086 let evaluator = MockEvaluator::new(vec![]);
6092 let validator = EdifactValidator::new(evaluator);
6093 let external = NoOpExternalProvider;
6094
6095 let pia_5_z12 = OwnedSegment {
6099 id: "PIA".to_string(),
6100 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
6101 segment_number: 1,
6102 };
6103 let pia_5_srw = OwnedSegment {
6104 id: "PIA".to_string(),
6105 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
6106 segment_number: 2,
6107 };
6108
6109 let make_rules = |mig: &str, composite_code: &str| {
6110 vec![
6111 AhbFieldRule {
6112 segment_path: "SG4/SG8/PIA/4347".to_string(),
6113 name: "Produkt-ID-Funktion".to_string(),
6114 ahb_status: "Muss".to_string(),
6115 codes: vec![AhbCodeRule {
6116 value: "5".to_string(),
6117 description: "".to_string(),
6118 ahb_status: "X".to_string(),
6119 }],
6120 parent_group_ahb_status: None,
6121 segment_ahb_status: None,
6122 element_index: Some(0),
6123 component_index: Some(0),
6124 mig_number: Some(mig.to_string()),
6125 format: None,
6126 },
6127 AhbFieldRule {
6128 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
6129 name: "Artikel/Dienstleistung-Identifikator".to_string(),
6130 ahb_status: "Muss".to_string(),
6131 codes: vec![AhbCodeRule {
6132 value: composite_code.to_string(),
6133 description: "".to_string(),
6134 ahb_status: "X".to_string(),
6135 }],
6136 parent_group_ahb_status: None,
6137 segment_ahb_status: None,
6138 element_index: Some(1),
6139 component_index: Some(0),
6140 mig_number: Some(mig.to_string()),
6141 format: None,
6142 },
6143 ]
6144 };
6145
6146 let mut fields = make_rules("00108", "Z12");
6147 fields.extend(make_rules("00197", "SRW"));
6148
6149 let workflow = AhbWorkflow {
6150 pruefidentifikator: "55035".to_string(),
6151 description: "Test".to_string(),
6152 communication_direction: None,
6153 fields,
6154 ub_definitions: BTreeMap::new(),
6155 package_conditions: Default::default(),
6156 };
6157
6158 let report = validator.validate(
6159 &[pia_5_z12, pia_5_srw],
6160 &workflow,
6161 &external,
6162 ValidationLevel::Conditions,
6163 );
6164
6165 let code_errors: Vec<_> = report
6166 .by_category(ValidationCategory::Code)
6167 .filter(|i| {
6168 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6169 })
6170 .collect();
6171 assert!(
6172 code_errors.is_empty(),
6173 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
6174 code_errors
6175 );
6176 }
6177}