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