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.statuses_only {
431 report_repeated_in_message(
432 &validated_tree.groups,
433 self.evaluator.message_type(),
434 &mut report,
435 );
436 }
437
438 if self.evaluator.message_type() == "UTILMD_Strom" && !self.statuses_only {
441 let mut index: HashMap<&str, usize> = HashMap::new();
442 for group in &validated_tree.groups {
443 let i = *index
444 .entry(group.group_id)
445 .and_modify(|c| *c += 1)
446 .or_insert(0);
447 if group.group_id == "SG4" {
448 let first_new = report.issues.len();
449 report_off_count_per_id(
450 group,
451 i,
452 &ctx,
453 &self.evaluator,
454 validated_tree.ub_definitions,
455 &mut report,
456 );
457 for issue in &mut report.issues[first_new..] {
458 issue.transaction_index = Some(i);
459 }
460 }
461 }
462 }
463
464 for field in &validated_tree.unmatched_rules {
474 if field
475 .mig_number
476 .as_deref()
477 .is_some_and(|n| per_transaction.contains(n))
478 {
479 continue;
480 }
481 if should_skip_for_parent_group(
482 field,
483 &absent_eval,
484 &ctx,
485 validated_tree.ub_definitions,
486 ) {
487 continue;
488 }
489
490 let status =
491 absent_eval.resolve_status(&field.ahb_status, &ctx, validated_tree.ub_definitions);
492
493 if status.result == ConditionResult::True
494 && status.kind == Some(StatusKind::Muss)
495 && !is_field_present(&ctx, field)
496 && !is_group_variant_absent(
497 &ctx,
498 field,
499 is_parent_group_required(
500 field,
501 &absent_eval,
502 &ctx,
503 validated_tree.ub_definitions,
504 ),
505 )
506 {
507 let mut issue = ValidationIssue::new(
508 Severity::Error,
509 IssueKind::MissingRequiredField {
510 field_name: field.name.clone(),
511 },
512 )
513 .with_field_path(&field.segment_path)
514 .with_rule(&field.ahb_status);
515 if let Some(first_code) = field.codes.first() {
516 issue.expected_value = Some(first_code.value.clone());
517 }
518 issue = attach_field_position(issue, field);
519 report.add_issue(issue);
520 }
521 }
522
523 if matches!(level, ValidationLevel::Full) && !self.statuses_only {
527 let mut seen: HashSet<*const AhbFieldRule> = HashSet::new();
531 let all_fields: Vec<AhbFieldRule> = all_nodes
532 .iter()
533 .map(|node| node.rule)
534 .chain(validated_tree.unmatched_rules.iter().copied())
535 .filter(|rule| seen.insert(*rule as *const AhbFieldRule))
536 .cloned()
537 .collect();
538 let synthetic_workflow = AhbWorkflow {
539 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
540 description: String::new(),
541 communication_direction: None,
542 fields: all_fields,
543 ub_definitions: validated_tree.ub_definitions.clone(),
544 package_conditions: validated_tree.package_conditions.clone(),
545 };
546 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
547 self.validate_answer_lists(&synthetic_workflow, &ctx, &mut report);
548 self.validate_package_cardinality(
549 &synthetic_workflow,
550 &ctx,
551 views.as_ref(),
552 &mut report,
553 );
554 }
555
556 report
557 }
558
559 fn validate_conditions(
561 &self,
562 workflow: &AhbWorkflow,
563 ctx: &EvaluationContext,
564 report: &mut ValidationReport,
565 ) {
566 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
567
568 for field in &workflow.fields {
569 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
571 continue;
572 }
573
574 let StatusResolution {
577 result: condition_result,
578 kind,
579 unknown_ids,
580 } = expr_eval.resolve_status(&field.ahb_status, ctx, &workflow.ub_definitions);
581 let mandatory = kind == Some(StatusKind::Muss);
582
583 match condition_result {
584 ConditionResult::True => {
585 if mandatory
587 && !is_field_present(ctx, field)
588 && !is_group_variant_absent(
589 ctx,
590 field,
591 is_parent_group_required(
592 field,
593 &expr_eval,
594 ctx,
595 &workflow.ub_definitions,
596 ),
597 )
598 {
599 let mut issue = ValidationIssue::new(
600 Severity::Error,
601 IssueKind::MissingRequiredField {
602 field_name: field.name.clone(),
603 },
604 )
605 .with_field_path(&field.segment_path)
606 .with_rule(&field.ahb_status)
607 .with_mig_number(field.mig_number.as_deref());
608 if let Some(first_code) = field.codes.first() {
612 issue.expected_value = Some(first_code.value.clone());
613 }
614 report.add_issue(issue);
615 }
616 }
617 ConditionResult::False => {
618 if mandatory && is_field_present(ctx, field) {
624 let failed = expr_eval.false_ids_with_ub(
625 &field.ahb_status,
626 ctx,
627 &workflow.ub_definitions,
628 );
629 report.add_issue(
630 ValidationIssue::new(
631 Severity::Error,
632 IssueKind::FieldConditionNotSatisfied {
633 field_name: field.name.clone(),
634 failed,
635 },
636 )
637 .with_field_path(&field.segment_path)
638 .with_rule(&field.ahb_status)
639 .with_mig_number(field.mig_number.as_deref()),
640 );
641 }
642 }
643 ConditionResult::Unknown => {
644 let mut external_ids = Vec::new();
649 let mut undetermined_ids = Vec::new();
650 let mut missing_ids = Vec::new();
651 for id in unknown_ids {
652 if self.evaluator.is_external(id) {
653 external_ids.push(id);
654 } else if self.evaluator.is_known(id) {
655 undetermined_ids.push(id);
656 } else {
657 missing_ids.push(id);
658 }
659 }
660
661 report.add_issue(
662 ValidationIssue::new(
663 Severity::Info,
664 IssueKind::ConditionUnknown {
665 field_name: field.name.clone(),
666 unresolved: UnresolvedConditions {
667 external: external_ids,
668 undetermined: undetermined_ids,
669 missing: missing_ids,
670 },
671 },
672 )
673 .with_field_path(&field.segment_path)
674 .with_rule(&field.ahb_status),
675 );
676 }
677 }
678 }
679
680 self.validate_codes_cross_field(workflow, ctx, report);
685 self.validate_answer_lists(workflow, ctx, report);
686
687 self.validate_package_cardinality(workflow, ctx, None, report);
690 }
691
692 fn validate_package_cardinality(
703 &self,
704 workflow: &AhbWorkflow,
705 ctx: &EvaluationContext,
706 views: Option<&TransactionViews<'_>>,
707 report: &mut ValidationReport,
708 ) {
709 struct PackageGroup {
712 min: u32,
713 max: u32,
714 max_sum: u32,
717 bounds: BTreeSet<(u32, u32)>,
718 code_values: Vec<String>,
719 element_index: usize,
720 component_index: usize,
721 }
722
723 let mut groups: HashMap<(String, Option<String>, u32, u32, u32), PackageGroup> =
732 HashMap::new();
733 let mut alternatives: HashMap<(String, Option<String>, u32), BTreeSet<u32>> =
737 HashMap::new();
738
739 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
740
741 let mut parent_group_skipped: HashMap<&str, bool> = HashMap::new();
748
749 for field in &workflow.fields {
750 let el_idx = field.element_index.unwrap_or(0);
751 let comp_idx = field.component_index.unwrap_or(0);
752
753 let code_packages: Vec<(&str, Vec<(u32, u32, u32)>)> = field
755 .codes
756 .iter()
757 .filter_map(|code| {
758 let expr = ConditionParser::parse(&code.ahb_status).ok()??;
759 let mut packages = Vec::new();
760 collect_packages(&expr, &mut packages);
761 (!packages.is_empty()).then_some((code.value.as_str(), packages))
762 })
763 .collect();
764 if code_packages.is_empty() {
765 continue;
766 }
767
768 let skipped = match field.parent_group_ahb_status.as_deref() {
770 Some(status) => *parent_group_skipped.entry(status).or_insert_with(|| {
771 should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions)
772 }),
773 None => false,
774 };
775 if skipped {
776 continue;
777 }
778
779 for (code_value, packages) in code_packages {
780 if packages.len() > 1 {
781 let ids: BTreeSet<u32> = packages.iter().map(|(id, _, _)| *id).collect();
782 for id in &ids {
783 alternatives
784 .entry((field.segment_path.clone(), field.mig_number.clone(), *id))
785 .or_default()
786 .extend(ids.iter().copied().filter(|other| other != id));
787 }
788 }
789
790 for (pkg_id, pkg_min, pkg_max) in packages {
791 let key = (
792 field.segment_path.clone(),
793 field.mig_number.clone(),
794 pkg_id,
795 0,
796 0,
797 );
798 let group = groups.entry(key).or_insert_with(|| PackageGroup {
799 min: pkg_min,
800 max: pkg_max,
801 max_sum: 0,
802 bounds: BTreeSet::new(),
803 code_values: Vec::new(),
804 element_index: el_idx,
805 component_index: comp_idx,
806 });
807 group.bounds.insert((pkg_min, pkg_max));
814 group.min = group.min.min(pkg_min);
815 if !group.code_values.iter().any(|v| v == code_value) {
816 group.max_sum = group.max_sum.saturating_add(pkg_max);
817 group.code_values.push(code_value.to_string());
818 }
819 group.max = group.max.max(pkg_max);
820 }
821 }
822 }
823
824 let mut field_packages: HashMap<(String, Option<String>), BTreeSet<u32>> = HashMap::new();
835 for (seg_path, mig_number, pkg_id, _, _) in groups.keys() {
836 field_packages
837 .entry((seg_path.clone(), mig_number.clone()))
838 .or_default()
839 .insert(*pkg_id);
840 }
841 let with_alternatives: Vec<(String, Option<String>)> = alternatives
842 .keys()
843 .map(|(p, m, _)| (p.clone(), m.clone()))
844 .collect();
845 for field in with_alternatives {
846 let Some(ids) = field_packages.get(&field) else {
847 continue;
848 };
849 for id in ids {
850 alternatives
851 .entry((field.0.clone(), field.1.clone(), *id))
852 .or_default()
853 .extend(ids.iter().copied().filter(|other| other != id));
854 }
855 }
856
857 type Counted = (Vec<usize>, Vec<bool>, usize, usize, Vec<String>);
859 let mut counted: HashMap<(String, Option<String>, u32, u32, u32), Counted> = HashMap::new();
860 let mut transaction_views: HashMap<(String, usize), TransactionSegmentsView> =
863 HashMap::new();
864 for ((seg_path, mig_number, pkg_id, _, _), group) in &groups {
865 let segment_id = extract_segment_id(seg_path);
866
867 let mut unique_codes: Vec<&str> =
871 group.code_values.iter().map(|s| s.as_str()).collect();
872 unique_codes.sort_unstable();
873 unique_codes.dedup();
874 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
875
876 let group_path_str = extract_group_path_key(seg_path);
877 let group_path: Vec<&str> = if group_path_str.is_empty() {
878 Vec::new()
879 } else {
880 group_path_str.split('/').collect()
881 };
882
883 let min = group.min as usize;
884 let max = if group.bounds.len() > 1 {
885 group.max_sum as usize
886 } else {
887 group.max as usize
888 };
889
890 type Instance<'p> = (usize, Option<Vec<(&'p str, usize)>>);
893 let per_instance_counts: Option<Vec<Instance>> =
894 match (ctx.navigator, group_path.is_empty()) {
895 (Some(nav), false)
903 if group_path.len() > 1
904 && !full_instance_paths(nav, &group_path).is_empty() =>
905 {
906 let paths = full_instance_paths(nav, &group_path);
907 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
915 .as_deref()
916 {
917 Some(m) => (0..group_path.len())
918 .filter_map(|level| {
919 let prefix = group_path[..=level].join("/");
920 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
921 })
922 .collect(),
923 None => Vec::new(),
924 };
925 if paths.is_empty() {
926 None
927 } else {
928 Some(
929 paths
930 .into_iter()
931 .filter(|path| match mig_number.as_deref() {
932 Some(m) => nav.instance_at_has_mig_number(path, m),
933 None => true,
934 })
935 .filter(|path| {
936 variant_entries.iter().all(|(level, entry)| {
937 nav.find_segments_in_instance(
938 &entry.tag,
939 &path[..=*level],
940 )
941 .first()
942 .and_then(|seg| {
943 seg.elements
944 .get(entry.element)
945 .and_then(|e| e.get(entry.component))
946 .cloned()
947 })
948 .map_or(true, |v| entry.codes.contains(v.as_str()))
949 })
950 })
951 .map(|path| {
952 let count = nav
953 .find_segments_in_instance(&segment_id, &path)
954 .iter()
955 .filter_map(|seg| {
956 seg.elements
957 .get(group.element_index)
958 .and_then(|e| e.get(group.component_index))
959 .filter(|v| !v.is_empty())
960 .cloned()
961 })
962 .filter(|v| code_set.contains(v.as_str()))
963 .count();
964 (count, Some(path))
965 })
966 .collect(),
967 )
968 }
969 }
970 (Some(nav), false) => {
971 let instance_count = nav.group_instance_count(&group_path);
972 if instance_count == 0 {
973 None
974 } else {
975 Some(
983 (0..instance_count)
984 .filter(|i| match mig_number.as_deref() {
985 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
986 None => true,
987 })
988 .map(|i| {
989 let count = nav
990 .find_segments_in_group(&segment_id, &group_path, i)
991 .iter()
992 .filter_map(|seg| {
993 seg.elements
994 .get(group.element_index)
995 .and_then(|e| e.get(group.component_index))
996 .filter(|v| !v.is_empty())
997 .cloned()
998 })
999 .filter(|v| code_set.contains(v.as_str()))
1000 .count();
1001 let path = (group_path.len() == 1)
1003 .then(|| vec![(group_path[0], i)]);
1004 (count, path)
1005 })
1006 .collect(),
1007 )
1008 }
1009 }
1010 _ => None,
1011 };
1012
1013 let instances = per_instance_counts.unwrap_or_else(|| {
1014 let segments = ctx.find_segments(&segment_id);
1015 let count = segments
1016 .iter()
1017 .filter_map(|seg| {
1018 seg.elements
1019 .get(group.element_index)
1020 .and_then(|e| e.get(group.component_index))
1021 .filter(|v| !v.is_empty())
1022 .map(|s| s.as_str())
1023 })
1024 .filter(|v| code_set.contains(v))
1025 .count();
1026 vec![(count, None)]
1027 });
1028 let applies: Vec<bool> = instances
1034 .iter()
1035 .map(|(_, path)| match workflow.package_conditions.get(pkg_id) {
1036 None => true,
1037 Some(condition) => {
1038 let result = match path {
1039 Some(path) => match path.first().and_then(|&(group_id, index)| {
1040 let key = (group_id.to_string(), index);
1041 if !transaction_views.contains_key(&key) {
1042 let view = views?.view(group_id, index)?;
1043 transaction_views.insert(key.clone(), view);
1044 }
1045 transaction_views.get(&key)
1046 }) {
1047 Some(view) => {
1048 let navigator = AssembledTreeNavigator::new(&view.tree);
1049 let mut in_view = path.clone();
1050 in_view[0].1 = 0;
1051 expr_eval.evaluate(
1052 condition,
1053 &ctx.with_view(&view.segments, &navigator)
1054 .with_scope(crate::eval::GroupScope { path: &in_view }),
1055 )
1056 }
1057 None => expr_eval.evaluate(
1058 condition,
1059 &ctx.with_scope(crate::eval::GroupScope { path }),
1060 ),
1061 },
1062 None => expr_eval.evaluate(condition, ctx),
1063 };
1064 result == ConditionResult::True
1065 }
1066 })
1067 .collect();
1068 let counts: Vec<usize> = instances.iter().map(|(c, _)| *c).collect();
1069
1070 counted.insert(
1071 (
1072 seg_path.clone(),
1073 mig_number.clone(),
1074 *pkg_id,
1075 group.min,
1076 group.max,
1077 ),
1078 (
1079 counts,
1080 applies,
1081 min,
1082 max,
1083 unique_codes.iter().map(|s| s.to_string()).collect(),
1084 ),
1085 );
1086 }
1087
1088 let mut keys: Vec<&(String, Option<String>, u32, u32, u32)> = counted.keys().collect();
1093 keys.sort();
1094 for key in keys {
1095 let (counts, applies, min, max, codes) = &counted[key];
1096 let (seg_path, mig_number, pkg_id, _, _) = key;
1097 let holds = |c: &[usize], a: &[bool], lo: usize, hi: usize, i: usize| {
1098 c.len() == counts.len() && a[i] && (lo..=hi).contains(&c[i]) && c[i] > 0
1099 };
1100 let alternative_holds = |i: usize| {
1101 alternatives
1102 .get(&(seg_path.clone(), mig_number.clone(), *pkg_id))
1103 .is_some_and(|alts| {
1104 counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
1105 p == seg_path
1106 && m == mig_number
1107 && alts.contains(id)
1108 && holds(c, a, *lo, *hi, i)
1109 })
1110 })
1111 };
1112 let applicable_alternative_holds = |i: usize| {
1117 *min > 0
1118 && counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
1119 p == seg_path
1120 && m == mig_number
1121 && id != pkg_id
1122 && *lo > 0
1123 && holds(c, a, *lo, *hi, i)
1124 })
1125 };
1126 let mut reported_counts: HashSet<usize> = HashSet::new();
1127 for (i, &present_count) in counts.iter().enumerate() {
1128 if applies[i]
1129 && (present_count < *min || present_count > *max)
1130 && !alternative_holds(i)
1131 && !(present_count < *min && applicable_alternative_holds(i))
1132 && reported_counts.insert(present_count)
1133 {
1134 report.add_issue(
1135 ValidationIssue::new(
1136 Severity::Error,
1137 IssueKind::PackageCardinality {
1138 package_id: pkg_id.to_string(),
1139 present: present_count,
1140 min: *min,
1141 max: *max,
1142 codes: codes.clone(),
1143 },
1144 )
1145 .with_field_path(seg_path)
1146 .with_expected(format!("{min}..{max}"))
1147 .with_actual(present_count.to_string()),
1148 );
1149 }
1150 }
1151 }
1152 }
1153
1154 fn validate_codes_cross_field(
1166 &self,
1167 workflow: &AhbWorkflow,
1168 ctx: &EvaluationContext,
1169 report: &mut ValidationReport,
1170 ) {
1171 if ctx.navigator.is_some() {
1172 self.validate_codes_group_scoped(workflow, ctx, report);
1173 } else {
1174 self.validate_codes_tag_scoped(workflow, ctx, report);
1175 }
1176 }
1177
1178 fn validate_codes_group_scoped(
1181 &self,
1182 workflow: &AhbWorkflow,
1183 ctx: &EvaluationContext,
1184 report: &mut ValidationReport,
1185 ) {
1186 let by_loc = partition_codes_by_mig(workflow);
1187 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
1188 let data_slots = data_slot_entries(workflow);
1189 let nav = ctx.navigator.unwrap();
1190
1191 for ((group_key, tag), migs) in &by_loc {
1192 let field_path = if group_key.is_empty() {
1193 format!("{tag}/qualifier")
1194 } else {
1195 format!("{group_key}/{tag}/qualifier")
1196 };
1197
1198 let group_path: Vec<&str> = if group_key.is_empty() {
1199 Vec::new()
1200 } else {
1201 group_key.split('/').collect()
1202 };
1203
1204 let tag_qualifiers = known_qualifiers.get(tag);
1205
1206 if group_path.is_empty() {
1207 Self::validate_segments_per_mig(
1208 &ctx.find_segments(tag),
1209 migs,
1210 tag_qualifiers,
1211 false,
1212 tag,
1213 &field_path,
1214 report,
1215 );
1216 } else {
1217 let instance_count = nav.group_instance_count(&group_path);
1218 let slots = data_slots.get(&(group_key.clone(), tag.clone()));
1219 for i in 0..instance_count {
1220 let owned = nav.find_segments_in_group(tag, &group_path, i);
1221 let refs: Vec<&OwnedSegment> = owned.iter().collect();
1222 let holds_data_slot = slots.is_some_and(|entries| {
1225 entries.iter().any(|(entry_tag, entry_mig)| {
1226 let Some(bucket) = by_loc
1227 .get(&(group_key.clone(), entry_tag.clone()))
1228 .and_then(|m| m.get(entry_mig))
1229 else {
1230 return false;
1231 };
1232 nav.find_segments_in_group(entry_tag, &group_path, i)
1233 .iter()
1234 .any(|s| matches_qualifier(s, bucket))
1235 })
1236 });
1237 Self::validate_segments_per_mig(
1238 &refs,
1239 migs,
1240 tag_qualifiers,
1241 holds_data_slot,
1242 tag,
1243 &field_path,
1244 report,
1245 );
1246 }
1247 }
1248 }
1249 }
1250
1251 fn validate_codes_tag_scoped(
1255 &self,
1256 workflow: &AhbWorkflow,
1257 ctx: &EvaluationContext,
1258 report: &mut ValidationReport,
1259 ) {
1260 let by_loc = partition_codes_by_mig(workflow);
1261 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
1262 let mut by_tag: BTreeMap<String, BTreeMap<Option<String>, MigCodeBucket>> = BTreeMap::new();
1264 for ((_group_key, tag), migs) in by_loc {
1265 let merged = by_tag.entry(tag).or_default();
1266 for (mig_key, bucket) in migs {
1267 let entry = merged.entry(mig_key).or_default();
1268 if entry.qualifier_position.is_none() {
1269 entry.qualifier_position = bucket.qualifier_position;
1270 }
1271 if entry.qualifier_position == bucket.qualifier_position {
1272 entry.qualifier_values.extend(&bucket.qualifier_values);
1273 }
1274 for (pos, codes) in bucket.codes {
1275 entry.codes.entry(pos).or_default().extend(codes);
1276 }
1277 }
1278 }
1279
1280 for (tag, migs) in &by_tag {
1281 let field_path = format!("{tag}/qualifier");
1282 Self::validate_segments_per_mig(
1283 &ctx.find_segments(tag),
1284 migs,
1285 known_qualifiers.get(tag),
1286 false,
1287 tag,
1288 &field_path,
1289 report,
1290 );
1291 }
1292 }
1293
1294 fn validate_segments_per_mig(
1307 segments: &[&OwnedSegment],
1308 migs: &BTreeMap<Option<String>, MigCodeBucket>,
1309 tag_qualifiers: Option<&BTreeMap<(usize, usize), HashSet<String>>>,
1310 holds_data_slot: bool,
1311 tag: &str,
1312 field_path: &str,
1313 report: &mut ValidationReport,
1314 ) {
1315 for seg in segments {
1316 match match_segment_to_mig(seg, migs) {
1317 Some(bucket) => {
1318 for ((el, c), allowed) in &bucket.codes {
1319 if allowed.is_empty() {
1320 continue;
1321 }
1322 Self::check_segments_against_codes(
1323 vec![*seg],
1324 allowed,
1325 tag,
1326 *el,
1327 *c,
1328 field_path,
1329 report,
1330 );
1331 }
1332 }
1333 None => {
1334 let Some(qualifiers) = tag_qualifiers.filter(|_| !holds_data_slot) else {
1335 continue;
1336 };
1337 for ((el, c), allowed) in qualifiers {
1342 let Some(actual) = seg
1343 .elements
1344 .get(*el)
1345 .and_then(|e| e.get(*c))
1346 .filter(|v| !v.is_empty())
1347 .map(|s| s.as_str())
1348 else {
1349 continue;
1350 };
1351 if allowed.iter().any(|v| v == actual) {
1352 break;
1354 }
1355 let bucket_values: HashSet<&str> = migs
1358 .values()
1359 .filter(|b| b.qualifier_position == Some((*el, *c)))
1360 .flat_map(|b| b.qualifier_values.iter().copied())
1361 .collect();
1362 if !bucket_values.is_empty() {
1363 Self::check_segments_against_codes(
1364 vec![*seg],
1365 &bucket_values,
1366 tag,
1367 *el,
1368 *c,
1369 field_path,
1370 report,
1371 );
1372 break;
1373 }
1374 }
1375 }
1376 }
1377 }
1378 }
1379
1380 fn validate_answer_lists(
1389 &self,
1390 workflow: &AhbWorkflow,
1391 ctx: &EvaluationContext,
1392 report: &mut ValidationReport,
1393 ) {
1394 let lists = AnswerCodeLists::embedded();
1395 let fv = self.evaluator.format_version();
1396 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1397 for list_rule in &workflow.fields {
1398 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1399 continue;
1400 };
1401 if list_rule.codes.is_empty()
1402 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1403 {
1404 continue;
1405 }
1406 let Some(answer_rule) = workflow
1407 .fields
1408 .iter()
1409 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1410 else {
1411 continue;
1412 };
1413 let at = |rule: &AhbFieldRule| {
1414 (
1415 rule.element_index.unwrap_or(0),
1416 rule.component_index.unwrap_or(0),
1417 )
1418 };
1419 let (list_el, list_c) = at(list_rule);
1420 let (answer_el, answer_c) = at(answer_rule);
1421 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1422 seg.elements
1423 .get(el)
1424 .and_then(|e| e.get(c))
1425 .filter(|v| !v.is_empty())
1426 .cloned()
1427 };
1428 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1429 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1430 continue;
1431 }
1432 let (Some(list), Some(answer)) =
1433 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1434 else {
1435 continue;
1436 };
1437 if !list_rule.codes.iter().any(|c| c.value == list) {
1438 continue;
1439 }
1440 if answer_rule.codes.iter().any(|c| c.value == answer) {
1444 continue;
1445 }
1446 let Some(codes) = lists.codes(fv, &list) else {
1447 continue;
1448 };
1449 if codes.contains(&answer) {
1450 continue;
1451 }
1452 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1453 allowed.sort_unstable();
1454 report.add_issue(
1455 ValidationIssue::new(
1456 Severity::Warning,
1457 IssueKind::AnswerNotInList {
1458 answer: answer.clone(),
1459 list: list.clone(),
1460 allowed: allowed.clone(),
1461 },
1462 )
1463 .with_field_path(&answer_rule.segment_path)
1464 .with_actual(&answer)
1465 .with_expected(allowed.join(", ")),
1466 );
1467 }
1468 }
1469 }
1470
1471 fn check_segments_against_codes(
1473 segments: Vec<&OwnedSegment>,
1474 allowed_codes: &HashSet<&str>,
1475 _tag: &str,
1476 el_idx: usize,
1477 comp_idx: usize,
1478 field_path: &str,
1479 report: &mut ValidationReport,
1480 ) {
1481 for segment in segments {
1482 if let Some(code_value) = segment
1483 .elements
1484 .get(el_idx)
1485 .and_then(|e| e.get(comp_idx))
1486 .filter(|v| !v.is_empty())
1487 {
1488 if !allowed_codes.contains(code_value.as_str()) {
1489 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1490 sorted_codes.sort_unstable();
1491 let kind = IssueKind::CodeNotAllowedForPid {
1492 value: code_value.to_string(),
1493 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1494 };
1495 let mut issue = ValidationIssue::new(Severity::Error, kind)
1496 .with_field_path(field_path)
1497 .with_actual(code_value)
1498 .with_expected(sorted_codes.join(", "))
1499 .with_field_position(el_idx as u32 + 2, Some(comp_idx as u32 + 1));
1500 if segment.segment_number > 0 {
1501 issue =
1502 issue.with_position(crate::SegmentPosition::at(segment.segment_number));
1503 }
1504 report.add_issue(issue);
1505 }
1506 }
1507 }
1508 }
1509}
1510
1511fn full_instance_paths<'p>(
1514 nav: &dyn GroupNavigator,
1515 group_path: &[&'p str],
1516) -> Vec<Vec<(&'p str, usize)>> {
1517 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1518 for g in group_path {
1519 paths = paths
1520 .into_iter()
1521 .flat_map(|parent| {
1522 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1523 let mut path = parent.clone();
1524 path.push((*g, i));
1525 path
1526 })
1527 })
1528 .collect();
1529 }
1530 paths
1531}
1532
1533struct VariantEntry<'a> {
1536 tag: String,
1537 element: usize,
1538 component: usize,
1539 codes: HashSet<&'a str>,
1540}
1541
1542fn merged_variant_entry<'a>(
1548 workflow: &'a AhbWorkflow,
1549 group_path: &str,
1550 mig: &str,
1551) -> Option<VariantEntry<'a>> {
1552 let mig: u32 = mig.parse().ok()?;
1553 let prefix = format!("{group_path}/");
1554 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1556 .fields
1557 .iter()
1558 .filter_map(|r| {
1559 let rest = r.segment_path.strip_prefix(&prefix)?;
1560 let tag = rest.split('/').next()?;
1561 if tag.starts_with("SG") {
1562 return None;
1563 }
1564 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1565 })
1566 .collect();
1567 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1569 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1570 .iter()
1571 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1572 .collect();
1573 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1574 if numbers.len() < 2 {
1575 return None;
1576 }
1577 let own = entries
1578 .iter()
1579 .filter(|(_, n, _)| *n <= mig)
1580 .map(|(_, n, _)| *n)
1581 .max()?;
1582 let (rule, _, tag) = entries
1584 .iter()
1585 .filter(|(_, n, _)| *n == own)
1586 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1587 Some(VariantEntry {
1588 tag: tag.to_string(),
1589 element: rule.element_index.unwrap_or(0),
1590 component: rule.component_index.unwrap_or(0),
1591 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1592 })
1593}
1594
1595fn value_length(format: &str, value: &str) -> usize {
1599 let format = format.trim();
1600 if format.starts_with('n') {
1601 value
1602 .chars()
1603 .filter(|c| !matches!(c, '.' | ',' | '-'))
1604 .count()
1605 } else {
1606 value.chars().count()
1607 }
1608}
1609
1610fn max_length(format: &str) -> Option<usize> {
1613 let digits = format
1614 .trim()
1615 .strip_prefix("an")
1616 .or_else(|| format.trim().strip_prefix('a'))
1617 .or_else(|| format.trim().strip_prefix('n'))?;
1618 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1619}
1620
1621fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1632 let derived_from_codes = field
1633 .codes
1634 .first()
1635 .is_some_and(|c| c.ahb_status == field.ahb_status);
1636 if !derived_from_codes && !codes_are_alternatives(field) {
1637 return &field.ahb_status;
1638 }
1639 value
1640 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1641 .map_or(&field.ahb_status, |c| &c.ahb_status)
1642}
1643
1644fn codes_are_alternatives(field: &AhbFieldRule) -> bool {
1655 !field.codes.is_empty()
1656 && field.ahb_status.contains('[')
1657 && field
1658 .codes
1659 .iter()
1660 .all(|c| c.ahb_status.contains('[') && c.ahb_status != field.ahb_status)
1661}
1662
1663fn should_skip_for_parent_group<E: ConditionEvaluator>(
1673 field: &AhbFieldRule,
1674 expr_eval: &ConditionExprEvaluator<E>,
1675 ctx: &EvaluationContext,
1676 ub_definitions: &BTreeMap<String, ConditionExpr>,
1677) -> bool {
1678 let Some(group_status) = field.parent_group_ahb_status.as_deref() else {
1679 return false;
1680 };
1681 if !group_status.contains('[') {
1682 return false;
1683 }
1684 match ConditionParser::parse_status_branches(group_status, ub_definitions) {
1685 Ok(branches) => !branches.iter().any(|branch| {
1686 branch.condition.as_ref().map_or(true, |c| {
1687 expr_eval.evaluate(c, ctx) == ConditionResult::True
1688 })
1689 }),
1690 Err(_) => true,
1691 }
1692}
1693
1694fn parent_group_refused<E: ConditionEvaluator>(
1698 field: &AhbFieldRule,
1699 expr_eval: &ConditionExprEvaluator<E>,
1700 ctx: &EvaluationContext,
1701 ub_definitions: &BTreeMap<String, ConditionExpr>,
1702) -> bool {
1703 let Some(group_status) = field.parent_group_ahb_status.as_deref() else {
1704 return false;
1705 };
1706 if !group_status.contains('[') {
1707 return false;
1708 }
1709 match ConditionParser::parse_status_branches(group_status, ub_definitions) {
1710 Ok(branches) => branches.iter().all(|branch| {
1711 branch
1712 .condition
1713 .as_ref()
1714 .is_some_and(|c| expr_eval.evaluate(c, ctx) == ConditionResult::False)
1715 }),
1716 Err(_) => true,
1717 }
1718}
1719
1720fn is_parent_group_required<E: ConditionEvaluator>(
1732 field: &AhbFieldRule,
1733 expr_eval: &ConditionExprEvaluator<E>,
1734 ctx: &EvaluationContext,
1735 ub_definitions: &BTreeMap<String, ConditionExpr>,
1736) -> bool {
1737 let required = |status: &str| {
1738 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1739 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1740 };
1741 field
1742 .parent_group_ahb_status
1743 .as_deref()
1744 .is_some_and(required)
1745 && field.group_ahb_status.as_deref().map_or(true, required)
1746}
1747
1748fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1757 let segment_id = extract_segment_id(&field.segment_path);
1758
1759 if !field.codes.is_empty() {
1765 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1766 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1767 let matching = ctx.find_segments(&segment_id);
1768 return matching.iter().any(|seg| {
1769 seg.elements
1770 .get(el_idx)
1771 .and_then(|e| e.get(comp_idx))
1772 .is_some_and(|v| required_codes.contains(&v.as_str()))
1773 });
1774 }
1775 if is_qualifier_field(&field.segment_path) {
1778 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1779 let el_idx = field.element_index.unwrap_or(0);
1780 let comp_idx = field.component_index.unwrap_or(0);
1781 let matching = ctx.find_segments(&segment_id);
1782 return matching.iter().any(|seg| {
1783 seg.elements
1784 .get(el_idx)
1785 .and_then(|e| e.get(comp_idx))
1786 .is_some_and(|v| required_codes.contains(&v.as_str()))
1787 });
1788 }
1789 }
1790
1791 ctx.has_segment(&segment_id)
1792}
1793
1794fn is_group_variant_absent(
1809 ctx: &EvaluationContext,
1810 field: &AhbFieldRule,
1811 is_group_mandatory: bool,
1812) -> bool {
1813 let group_path: Vec<&str> = field
1814 .segment_path
1815 .split('/')
1816 .take_while(|p| p.starts_with("SG"))
1817 .collect();
1818
1819 if group_path.is_empty() {
1820 return false;
1821 }
1822
1823 let nav = match ctx.navigator {
1824 Some(nav) => nav,
1825 None => return false,
1826 };
1827
1828 let instance_count = nav.group_instance_count(&group_path);
1829
1830 if instance_count == 0 {
1835 if !is_group_mandatory {
1836 return true;
1837 }
1838 return false;
1840 }
1841
1842 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1847 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1849 let segment_id = extract_segment_id(&field.segment_path);
1850 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1851
1852 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1853 nav.find_segments_in_group(&segment_id, &group_path, i)
1854 .iter()
1855 .any(|seg| {
1856 seg.elements
1857 .first()
1858 .and_then(|e| e.first())
1859 .is_some_and(|v| required_codes.contains(&v.as_str()))
1860 })
1861 });
1862
1863 if !any_instance_has_qualifier {
1864 return true; }
1866 }
1867 }
1868
1869 let segment_id = extract_segment_id(&field.segment_path);
1878 let segment_absent_from_all = (0..instance_count).all(|i| {
1879 nav.find_segments_in_group(&segment_id, &group_path, i)
1880 .is_empty()
1881 });
1882 if segment_absent_from_all {
1883 let group_has_other_segments =
1884 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1885 if group_has_other_segments {
1886 return true;
1887 }
1888 }
1889
1890 false
1891}
1892
1893fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1895 out.extend(group.fields.iter());
1896 for child in &group.children {
1897 collect_nodes_depth_first(child, out);
1898 }
1899}
1900
1901#[allow(clippy::too_many_arguments)]
1905fn evaluate_node<E: ConditionEvaluator>(
1906 node: &AhbNode,
1907 ctx: &EvaluationContext,
1908 expr_eval: &ConditionExprEvaluator<E>,
1909 evaluator: &E,
1910 ub_definitions: &BTreeMap<String, ConditionExpr>,
1911 tag_migs: &HashMap<String, HashSet<&str>>,
1912 instance_index: Option<usize>,
1913 slot_holds: bool,
1914 statuses_only: bool,
1915 report: &mut ValidationReport,
1916) {
1917 let field = node.rule;
1918 if statuses_only && node.value.is_some() {
1920 return;
1921 }
1922
1923 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1925
1926 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1931 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1932 if length > max {
1933 let mut issue = ValidationIssue::new(
1934 Severity::Error,
1935 IssueKind::ValueTooLong {
1936 field_name: field.name.clone(),
1937 length,
1938 max,
1939 },
1940 )
1941 .with_field_path(&field.segment_path)
1942 .with_actual(value)
1943 .with_expected(field.format.clone().unwrap_or_default());
1944 if let Some(idx) = instance_index {
1945 issue = issue.with_instance_index(idx);
1946 }
1947 if let Some(num) = node.matched_segment_number {
1948 issue = issue.with_position(crate::SegmentPosition::at(num));
1949 }
1950 report.add_issue(attach_field_position(issue, field));
1951 }
1952 }
1953
1954 if let (Some(value), Some(format)) = (node.value, field.format.as_deref()) {
1957 if slot_holds
1958 && format.trim().starts_with('n')
1959 && field.codes.is_empty()
1960 && !value.is_empty()
1961 && !crate::eval::format_validators::is_edifact_number(value)
1962 {
1963 let mut issue = ValidationIssue::new(
1964 Severity::Error,
1965 IssueKind::ValueNotNumeric {
1966 field_name: field.name.clone(),
1967 },
1968 )
1969 .with_field_path(&field.segment_path)
1970 .with_actual(value)
1971 .with_expected(format);
1972 if let Some(idx) = instance_index {
1973 issue = issue.with_instance_index(idx);
1974 }
1975 if let Some(num) = node.matched_segment_number {
1976 issue = issue.with_position(crate::SegmentPosition::at(num));
1977 }
1978 report.add_issue(attach_field_position(issue, field));
1979 }
1980 }
1981
1982 if let (Some(value), Some(elements)) = (node.value, node.segment_elements) {
1986 let format = elements
1987 .first()
1988 .and_then(|composite| composite.get(2))
1989 .map(String::as_str)
1990 .unwrap_or_default();
1991 if slot_holds
1992 && field.segment_path.ends_with("DTM/C507/2380")
1993 && !value.is_empty()
1994 && crate::eval::format_validators::fits_date_format(value, format) == Some(false)
1995 {
1996 let mut issue = ValidationIssue::new(
1997 Severity::Error,
1998 IssueKind::DateNotInFormat {
1999 field_name: field.name.clone(),
2000 format: format.to_string(),
2001 },
2002 )
2003 .with_field_path(&field.segment_path)
2004 .with_actual(value)
2005 .with_expected(format);
2006 if let Some(idx) = instance_index {
2007 issue = issue.with_instance_index(idx);
2008 }
2009 if let Some(num) = node.matched_segment_number {
2010 issue = issue.with_position(crate::SegmentPosition::at(num));
2011 }
2012 report.add_issue(attach_field_position(issue, field));
2013 }
2014 }
2015
2016 if parent_group_refused(field, expr_eval, ctx, ub_definitions) {
2020 return;
2021 }
2022
2023 let status = governing_status(field, node.value);
2026 let StatusResolution {
2027 result: condition_result,
2028 kind,
2029 unknown_ids,
2030 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
2031 let mandatory = kind == Some(StatusKind::Muss);
2032
2033 match condition_result {
2034 ConditionResult::True => {
2035 if mandatory && node.value.is_none() {
2044 let tag = extract_segment_id(&field.segment_path);
2045 let single_variant_present = node.segment_elements.is_none()
2046 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
2047 && is_field_present(ctx, field);
2048 let segment_optional_and_absent = node.segment_elements.is_none()
2053 && field
2054 .segment_ahb_status
2055 .as_deref()
2056 .is_some_and(is_optional_segment_status);
2057 let segment_not_required_and_absent = node.segment_elements.is_none()
2064 && field.segment_ahb_status.as_deref().is_some_and(|st| {
2065 let status = expr_eval.resolve_status(st, &node_ctx, ub_definitions);
2066 status.result != ConditionResult::True
2067 || status.kind != Some(StatusKind::Muss)
2068 });
2069 if !single_variant_present
2070 && !segment_optional_and_absent
2071 && !segment_not_required_and_absent
2072 {
2073 let mut issue = ValidationIssue::new(
2074 Severity::Error,
2075 IssueKind::MissingRequiredField {
2076 field_name: field.name.clone(),
2077 },
2078 )
2079 .with_field_path(&field.segment_path)
2080 .with_rule(&field.ahb_status);
2081 if let Some(first_code) = field.codes.first() {
2082 issue.expected_value = Some(first_code.value.clone());
2083 }
2084 if let Some(idx) = instance_index {
2085 issue = issue.with_instance_index(idx);
2086 }
2087 if let Some(num) = node.matched_segment_number {
2088 issue = issue.with_position(crate::SegmentPosition::at(num));
2089 }
2090 issue = attach_field_position(issue, field);
2091 report.add_issue(issue);
2092 }
2093 }
2094 }
2095 ConditionResult::False => {
2096 if mandatory && node.value.is_some() {
2099 let failed = expr_eval.false_ids_with_ub(status, &node_ctx, ub_definitions);
2100 let mut issue = ValidationIssue::new(
2101 Severity::Error,
2102 IssueKind::FieldConditionNotSatisfied {
2103 field_name: field.name.clone(),
2104 failed,
2105 },
2106 )
2107 .with_field_path(&field.segment_path)
2108 .with_rule(&field.ahb_status);
2109 if let Some(idx) = instance_index {
2110 issue = issue.with_instance_index(idx);
2111 }
2112 if let Some(num) = node.matched_segment_number {
2113 issue = issue.with_position(crate::SegmentPosition::at(num));
2114 }
2115 issue = attach_field_position(issue, field);
2116 report.add_issue(issue);
2117 }
2118 }
2119 ConditionResult::Unknown if statuses_only => {}
2120 ConditionResult::Unknown => {
2121 let mut external_ids = Vec::new();
2123 let mut undetermined_ids = Vec::new();
2124 let mut missing_ids = Vec::new();
2125 for id in unknown_ids {
2126 if evaluator.is_external(id) {
2127 external_ids.push(id);
2128 } else if evaluator.is_known(id) {
2129 undetermined_ids.push(id);
2130 } else {
2131 missing_ids.push(id);
2132 }
2133 }
2134
2135 let mut issue = ValidationIssue::new(
2136 Severity::Info,
2137 IssueKind::ConditionUnknown {
2138 field_name: field.name.clone(),
2139 unresolved: UnresolvedConditions {
2140 external: external_ids,
2141 undetermined: undetermined_ids,
2142 missing: missing_ids,
2143 },
2144 },
2145 )
2146 .with_field_path(&field.segment_path)
2147 .with_rule(&field.ahb_status);
2148 if let Some(idx) = instance_index {
2149 issue = issue.with_instance_index(idx);
2150 }
2151 if let Some(num) = node.matched_segment_number {
2152 issue = issue.with_position(crate::SegmentPosition::at(num));
2153 }
2154 issue = attach_field_position(issue, field);
2155 report.add_issue(issue);
2156 }
2157 }
2158}
2159
2160fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
2165 let issue = issue.with_mig_number(field.mig_number.as_deref());
2166 match field.element_index {
2167 Some(el) => {
2168 let element_pos = (el as u32) + 2;
2169 let component_pos = field.component_index.map(|c| (c as u32) + 1);
2170 issue.with_field_position(element_pos, component_pos)
2171 }
2172 None => issue,
2173 }
2174}
2175
2176fn report_repeated_in_transaction(
2183 transaction: &AhbGroupNode,
2184 transaction_index: usize,
2185 once: u32,
2186 ctx: &EvaluationContext,
2187 report: &mut ValidationReport,
2188) {
2189 let mut counts: BTreeMap<(String, String), (usize, String)> = BTreeMap::new();
2191 fn visit<'a>(
2192 node: &'a AhbGroupNode<'a>,
2193 path: &mut Vec<(&'a str, usize)>,
2194 once: u32,
2195 ctx: &EvaluationContext,
2196 counts: &mut BTreeMap<(String, String), (usize, String)>,
2197 ) {
2198 let ids: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2199 let mut seen: HashSet<Option<&str>> = HashSet::new();
2200 for n in &node.fields {
2201 let rule = n.rule;
2202 let Some(status) = rule.segment_ahb_status.as_deref() else {
2203 continue;
2204 };
2205 if Some(status) == node.ahb_status
2206 || rule.element_index.unwrap_or(0) != 0
2207 || rule.component_index.unwrap_or(0) != 0
2208 || rule.codes.is_empty()
2209 || !crate::eval::binds_once(status, once)
2210 || !seen.insert(rule.mig_number.as_deref())
2211 {
2212 continue;
2213 }
2214 let tag = extract_segment_id(&rule.segment_path);
2215 let sent = ctx.navigator().map_or(0, |nav| {
2216 nav.find_segments_in_instance(&tag, path)
2217 .iter()
2218 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2219 .count()
2220 });
2221 let entry = counts
2222 .entry((
2223 format!("{}/{tag}", ids.join("/")),
2224 rule.mig_number.clone().unwrap_or_default(),
2225 ))
2226 .or_insert((0, status.to_string()));
2227 entry.0 += sent;
2228 }
2229 for child in &node.children {
2230 if let Some(status) = child
2234 .own_status
2235 .or(child.ahb_status)
2236 .filter(|s| crate::eval::binds_once(s, once))
2237 {
2238 let variant = child
2239 .fields
2240 .first()
2241 .and_then(|n| n.rule.mig_number.clone())
2242 .unwrap_or_default();
2243 let entry = counts
2244 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
2245 .or_insert((0, status.to_string()));
2246 entry.0 += 1;
2247 }
2248 path.push((child.group_id, child.instance_index));
2249 visit(child, path, once, ctx, counts);
2250 path.pop();
2251 }
2252 }
2253 let mut path = vec![(transaction.group_id, transaction_index)];
2254 visit(transaction, &mut path, once, ctx, &mut counts);
2255 for ((path, _), (count, status)) in counts {
2256 if count > 1 {
2257 report.add_issue(
2258 ValidationIssue::new(
2259 Severity::Error,
2260 IssueKind::RepeatedInTransaction {
2261 path: path.clone(),
2262 count,
2263 },
2264 )
2265 .with_field_path(path)
2266 .with_rule(status)
2267 .with_instance_index(transaction_index),
2268 );
2269 }
2270 }
2271}
2272
2273#[allow(clippy::too_many_arguments)]
2280fn refuse_segments<E: ConditionEvaluator>(
2281 fields: &[AhbNode],
2282 group_status: Option<&str>,
2283 path: &[(&str, usize)],
2284 scoped_ctx: &EvaluationContext,
2285 evaluator: &E,
2286 ub_definitions: &BTreeMap<String, ConditionExpr>,
2287 instance_index: usize,
2288 report: &mut ValidationReport,
2289) {
2290 let mut seen: Vec<u32> = Vec::new();
2291 for node in fields {
2292 let rule = node.rule;
2293 let (Some(status), Some(value), Some(number)) = (
2294 rule.segment_ahb_status.as_deref(),
2295 node.value,
2296 node.matched_segment_number,
2297 ) else {
2298 continue;
2299 };
2300 let qualifier =
2303 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
2304 if !qualifier
2305 || seen.contains(&number)
2306 || Some(status) == group_status
2307 || !rule.codes.iter().any(|c| c.value == value)
2308 {
2309 continue;
2310 }
2311 seen.push(number);
2312 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2313 continue;
2314 }
2315 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2316 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
2317 let failed = ConditionExprEvaluator::new(evaluator).false_ids_with_ub(
2318 status,
2319 scoped_ctx,
2320 ub_definitions,
2321 );
2322 report.add_issue(
2323 ValidationIssue::new(
2324 Severity::Warning,
2325 IssueKind::SegmentNotAllowed {
2326 segment: segment.join("/"),
2327 failed,
2328 },
2329 )
2330 .with_field_path(segment.join("/"))
2331 .with_rule(status)
2332 .with_instance_index(instance_index)
2333 .with_position(crate::SegmentPosition::at(number)),
2334 );
2335 }
2336}
2337
2338fn report_segment_counts<E: ConditionEvaluator>(
2345 group: &AhbGroupNode,
2346 path: &[(&str, usize)],
2347 scoped_ctx: &EvaluationContext,
2348 evaluator: &E,
2349 instance_index: usize,
2350 report: &mut ValidationReport,
2351) {
2352 if crate::eval::repetition_limits(evaluator.message_type()).is_empty() {
2353 return;
2354 }
2355 let mut seen: HashSet<Option<&str>> = HashSet::new();
2356 for node in &group.fields {
2357 let rule = node.rule;
2358 let Some(status) = rule.segment_ahb_status.as_deref() else {
2359 continue;
2360 };
2361 if rule.element_index.unwrap_or(0) != 0
2362 || rule.component_index.unwrap_or(0) != 0
2363 || rule.codes.is_empty()
2364 || !seen.insert(rule.mig_number.as_deref())
2365 {
2366 continue;
2367 }
2368 let tag = extract_segment_id(&rule.segment_path);
2369 let sent: Vec<_> = scoped_ctx
2370 .find_segments_at(path, &tag)
2371 .into_iter()
2372 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2373 .collect();
2374 if sent.is_empty()
2375 || !crate::eval::breaks_repetition_limit(status, evaluator, scoped_ctx, sent.len())
2376 {
2377 continue;
2378 }
2379 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2380 segment.push(&tag);
2381 let mut issue = ValidationIssue::new(
2382 Severity::Error,
2383 IssueKind::RepetitionCount {
2384 path: segment.join("/"),
2385 count: sent.len(),
2386 },
2387 )
2388 .with_field_path(segment.join("/"))
2389 .with_rule(status)
2390 .with_instance_index(instance_index);
2391 if let Some(num) = node.matched_segment_number {
2392 issue = issue.with_position(crate::SegmentPosition::at(num));
2393 }
2394 report.add_issue(issue);
2395 }
2396}
2397
2398fn report_off_count_per_id<E: ConditionEvaluator>(
2407 transaction: &AhbGroupNode,
2408 tx: usize,
2409 ctx: &EvaluationContext,
2410 evaluator: &E,
2411 ub_definitions: &BTreeMap<String, ConditionExpr>,
2412 report: &mut ValidationReport,
2413) {
2414 let mut judged: Vec<(&str, Vec<&str>)> = Vec::new();
2415 for child in &transaction.children {
2416 let Some(status) = child.ahb_status else {
2417 continue;
2418 };
2419 let per_package = status.contains("[2002]");
2420 if child.group_id != "SG8"
2421 || !(per_package || status.contains("[2004]") || status.contains("[2005]"))
2422 || crate::eval::refuses_presence(status, evaluator, ctx, ub_definitions)
2423 {
2424 continue;
2425 }
2426 let Some(codes) = child.fields.iter().find_map(|node| {
2427 let rule = node.rule;
2428 (extract_segment_id(&rule.segment_path) == "SEQ"
2429 && rule.element_index.unwrap_or(0) == 0
2430 && rule.component_index.unwrap_or(0) == 0
2431 && !rule.codes.is_empty())
2432 .then(|| {
2433 rule.codes
2434 .iter()
2435 .map(|c| c.value.as_str())
2436 .collect::<Vec<_>>()
2437 })
2438 }) else {
2439 continue;
2440 };
2441 if judged.iter().any(|(s, c)| *s == status && *c == codes) {
2442 continue;
2443 }
2444 let position = child.fields.iter().find_map(|n| n.matched_segment_number);
2445 let (off, label) = if per_package {
2446 let off =
2447 crate::eval::sg4_instances::sg8_off_count_per_produktpaket(ctx, "SG4", tx, &codes);
2448 (off, "Produktpaket-ID")
2449 } else {
2450 let off =
2451 crate::eval::periods::sg8_off_count_per_zeitraum(ctx, "SG4", tx, &codes, status);
2452 (off, "Zeitraum-ID")
2453 };
2454 for (id, count) in off {
2455 let mut issue = ValidationIssue::new(
2456 Severity::Error,
2457 IssueKind::RepetitionCount {
2458 path: "SG4/SG8".to_string(),
2459 count,
2460 },
2461 )
2462 .with_field_path("SG4/SG8")
2463 .with_rule(status)
2464 .with_instance_index(tx);
2465 issue.expected_value = Some(format!("{label} {id}"));
2466 if let Some(num) = position {
2467 issue = issue.with_position(crate::SegmentPosition::at(num));
2468 }
2469 report.add_issue(issue);
2470 }
2471 judged.push((status, codes));
2472 }
2473}
2474
2475fn report_repeated_in_message(
2482 groups: &[AhbGroupNode],
2483 message_type: &str,
2484 report: &mut ValidationReport,
2485) {
2486 for &(condition, group_id, kind) in crate::eval::message_repetition_limits(message_type) {
2487 let tag = format!("[{condition}]");
2488 let mut index: HashMap<&str, usize> = HashMap::new();
2489 let counted: Vec<(usize, &AhbGroupNode)> = groups
2490 .iter()
2491 .filter_map(|g| {
2492 let i = *index.entry(g.group_id).and_modify(|c| *c += 1).or_insert(0);
2493 (g.group_id == group_id).then_some((i, g))
2494 })
2495 .filter(|(_, g)| g.ahb_status.is_some_and(|s| s.contains(&tag)))
2496 .filter(|(_, g)| {
2497 kind.map_or(true, |code| {
2498 g.fields.iter().any(|n| {
2499 extract_segment_id(&n.rule.segment_path) == "LIN"
2500 && n.rule.element_index == Some(1)
2501 && n.value == Some(code)
2502 })
2503 })
2504 })
2505 .collect();
2506 if counted.len() < 2 {
2507 continue;
2508 }
2509 let (instance_index, extra) = counted[1];
2510 let status = extra.ahb_status.unwrap_or_default();
2511 let mut issue = ValidationIssue::new(
2512 Severity::Error,
2513 IssueKind::RepetitionCount {
2514 path: group_id.to_string(),
2515 count: counted.len(),
2516 },
2517 )
2518 .with_field_path(group_id)
2519 .with_rule(status)
2520 .with_instance_index(instance_index)
2521 .with_mig_number(extra.fields.first().and_then(|n| n.rule.mig_number.as_deref()));
2523 if let Some(num) = extra.fields.iter().find_map(|n| n.matched_segment_number) {
2524 issue = issue.with_position(crate::SegmentPosition::at(num));
2525 }
2526 report.add_issue(issue);
2527 }
2528}
2529
2530#[allow(clippy::too_many_arguments)]
2536fn report_repeated_in_group<E: ConditionEvaluator>(
2537 group: &AhbGroupNode,
2538 path: &[(&str, usize)],
2539 scoped_ctx: &EvaluationContext,
2540 evaluator: &E,
2541 ub_definitions: &BTreeMap<String, ConditionExpr>,
2542 instance_index: usize,
2543 report: &mut ValidationReport,
2544) {
2545 if crate::eval::counted_repetitions(evaluator.message_type()).is_empty() {
2546 return;
2547 }
2548 let mut reported: HashSet<&str> = HashSet::new();
2549 for child in &group.children {
2550 let Some(status) = child.ahb_status else {
2551 continue;
2552 };
2553 let count = group
2554 .children
2555 .iter()
2556 .filter(|c| c.group_id == child.group_id)
2557 .count();
2558 if count < 2 || reported.contains(child.group_id) {
2561 continue;
2562 }
2563 let mut child_path = path.to_vec();
2564 child_path.push((child.group_id, child.instance_index));
2565 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2566 if !crate::eval::refuses_count(status, evaluator, &child_ctx, ub_definitions) {
2567 continue;
2568 }
2569 reported.insert(child.group_id);
2570 let group_path: Vec<&str> = child_path.iter().map(|(g, _)| *g).collect();
2571 let mut issue = ValidationIssue::new(
2572 Severity::Error,
2573 IssueKind::RepetitionCount {
2574 path: group_path.join("/"),
2575 count,
2576 },
2577 )
2578 .with_field_path(group_path.join("/"))
2579 .with_rule(status)
2580 .with_instance_index(instance_index);
2581 if let Some(num) = child.fields.iter().find_map(|n| n.matched_segment_number) {
2582 issue = issue.with_position(crate::SegmentPosition::at(num));
2583 }
2584 report.add_issue(issue);
2585 }
2586}
2587
2588#[allow(clippy::too_many_arguments)]
2597fn walk_group_instance<E: ConditionEvaluator>(
2598 group: &AhbGroupNode,
2599 path: &[(&str, usize)],
2600 scoped_ctx: &EvaluationContext,
2601 expr_eval: &ConditionExprEvaluator<E>,
2602 evaluator: &E,
2603 ub_definitions: &BTreeMap<String, ConditionExpr>,
2604 tag_migs: &HashMap<String, HashSet<&str>>,
2605 instance_index: usize,
2606 report_segments: bool,
2607 statuses_only: bool,
2608 report: &mut ValidationReport,
2609) {
2610 let mut group_refused = false;
2613 if let Some(status) = group.ahb_status {
2614 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2615 group_refused = true;
2616 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2617 let failed = ConditionExprEvaluator::new(evaluator).false_ids_with_ub(
2618 status,
2619 scoped_ctx,
2620 ub_definitions,
2621 );
2622 let mut issue = ValidationIssue::new(
2623 Severity::Warning,
2624 IssueKind::GroupNotAllowed {
2625 group: group_path.join("/"),
2626 failed,
2627 },
2628 )
2629 .with_field_path(group_path.join("/"))
2630 .with_rule(status)
2631 .with_instance_index(instance_index);
2632 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
2633 issue = issue.with_position(crate::SegmentPosition::at(num));
2634 }
2635 report.add_issue(issue);
2636 }
2637 }
2638 if report_segments && !group_refused {
2639 refuse_segments(
2640 &group.fields,
2641 group.ahb_status,
2642 path,
2643 scoped_ctx,
2644 evaluator,
2645 ub_definitions,
2646 instance_index,
2647 report,
2648 );
2649 }
2650 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
2654 .fields
2655 .iter()
2656 .filter(|n| {
2657 n.rule.element_index.unwrap_or(0) == 0
2658 && n.rule.component_index.unwrap_or(0) == 0
2659 && !n.rule.codes.is_empty()
2660 })
2661 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
2662 .collect();
2663 let holds = |node: &AhbNode| {
2664 let (Some(codes), Some(elements)) = (
2665 slot_codes.get(&node.rule.mig_number.as_deref()),
2666 node.segment_elements,
2667 ) else {
2668 return true;
2669 };
2670 let qualifier = elements
2671 .first()
2672 .and_then(|e| e.first())
2673 .map(String::as_str)
2674 .unwrap_or_default();
2675 codes.iter().any(|c| c.value == qualifier)
2676 };
2677 for node in &group.fields {
2678 evaluate_node(
2679 node,
2680 scoped_ctx,
2681 expr_eval,
2682 evaluator,
2683 ub_definitions,
2684 tag_migs,
2685 Some(instance_index),
2686 holds(node),
2687 statuses_only,
2688 report,
2689 );
2690 }
2691 if !statuses_only {
2692 report_repeated_in_group(
2693 group,
2694 path,
2695 scoped_ctx,
2696 evaluator,
2697 ub_definitions,
2698 instance_index,
2699 report,
2700 );
2701 report_segment_counts(group, path, scoped_ctx, evaluator, instance_index, report);
2702 }
2703 for child in &group.children {
2704 let mut child_path = path.to_vec();
2705 child_path.push((child.group_id, child.instance_index));
2706 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2707 walk_group_instance(
2708 child,
2709 &child_path,
2710 &child_ctx,
2711 expr_eval,
2712 evaluator,
2713 ub_definitions,
2714 tag_migs,
2715 instance_index,
2716 report_segments,
2717 statuses_only,
2718 report,
2719 );
2720 }
2721}
2722
2723#[allow(clippy::too_many_arguments)]
2733fn report_absent_children<E: ConditionEvaluator>(
2734 group: &AhbGroupNode,
2735 path: &[(&str, usize)],
2736 scoped_ctx: &EvaluationContext,
2737 absent_eval: &ConditionExprEvaluator<crate::eval::AbsentTarget<'_, E>>,
2738 evaluator: &E,
2739 ub_definitions: &BTreeMap<String, ConditionExpr>,
2740 instance_index: usize,
2741 report: &mut ValidationReport,
2742) {
2743 if group.ahb_status.is_some_and(|status| {
2744 crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions)
2745 }) {
2746 return;
2747 }
2748 for rule in &group.absent_children {
2749 let rule: &AhbFieldRule = rule;
2755 let own;
2756 let rule = match &rule.group_ahb_status {
2757 Some(status) => {
2758 own = AhbFieldRule {
2759 parent_group_ahb_status: Some(status.clone()),
2760 ..rule.clone()
2761 };
2762 &own
2763 }
2764 None => rule,
2765 };
2766 let child_id = rule
2772 .segment_path
2773 .split('/')
2774 .nth(path.len())
2775 .filter(|id| id.starts_with("SG"));
2776 let mut child_path = path.to_vec();
2777 if let Some(id) = child_id {
2778 let sent = group.children.iter().filter(|c| c.group_id == id).count();
2779 child_path.push((id, sent));
2780 }
2781 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2782 let scoped_ctx = &child_ctx;
2783 if should_skip_for_parent_group(rule, absent_eval, scoped_ctx, ub_definitions)
2784 || !is_parent_group_required(rule, absent_eval, scoped_ctx, ub_definitions)
2785 {
2786 continue;
2787 }
2788 let required = |status: &str| {
2789 let resolved = absent_eval.resolve_status(status, scoped_ctx, ub_definitions);
2790 resolved.result == ConditionResult::True && resolved.kind == Some(StatusKind::Muss)
2791 };
2792 let package_choice = |status: &str| {
2796 ConditionParser::parse_status_branches(status, ub_definitions).is_ok_and(|branches| {
2797 matches!(branches.as_slice(), [branch]
2798 if branch.kind == Some(StatusKind::Muss)
2799 && branch
2800 .condition
2801 .as_ref()
2802 .is_some_and(crate::eval::packages_allow_none))
2803 })
2804 };
2805 let segment_required = match rule.segment_ahb_status.as_deref() {
2806 Some(status) => required(status),
2807 None => true,
2808 };
2809 if !segment_required
2810 || !(required(&rule.ahb_status)
2811 || (rule.segment_ahb_status.is_some() && package_choice(&rule.ahb_status)))
2812 {
2813 continue;
2814 }
2815 let mut issue = ValidationIssue::new(
2816 Severity::Error,
2817 IssueKind::MissingRequiredField {
2818 field_name: rule.name.clone(),
2819 },
2820 )
2821 .with_field_path(&rule.segment_path)
2822 .with_rule(&rule.ahb_status)
2823 .with_instance_index(instance_index);
2824 if let Some(first_code) = rule.codes.first() {
2825 issue.expected_value = Some(first_code.value.clone());
2826 }
2827 report.add_issue(attach_field_position(issue, rule));
2828 }
2829 for child in &group.children {
2830 let mut child_path = path.to_vec();
2831 child_path.push((child.group_id, child.instance_index));
2832 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2833 report_absent_children(
2834 child,
2835 &child_path,
2836 &child_ctx,
2837 absent_eval,
2838 evaluator,
2839 ub_definitions,
2840 instance_index,
2841 report,
2842 );
2843 }
2844}
2845
2846fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2847 match expr {
2848 ConditionExpr::Package { id, min, max } => {
2849 out.push((*id, *min, *max));
2850 }
2851 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2852 for e in exprs {
2853 collect_packages(e, out);
2854 }
2855 }
2856 ConditionExpr::Xor(left, right) => {
2857 collect_packages(left, out);
2858 collect_packages(right, out);
2859 }
2860 ConditionExpr::Not(inner) => {
2861 collect_packages(inner, out);
2862 }
2863 ConditionExpr::Ref(_) => {}
2864 }
2865}
2866
2867fn is_optional_segment_status(status: &str) -> bool {
2877 let trimmed = status.trim();
2878 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
2879}
2880
2881fn is_qualifier_field(path: &str) -> bool {
2891 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
2892 matches!(parts.len(), 2 | 3)
2894}
2895
2896#[derive(Default)]
2904struct MigCodeBucket<'a> {
2905 qualifier_position: Option<(usize, usize)>,
2908 qualifier_values: HashSet<&'a str>,
2911 codes: BTreeMap<(usize, usize), HashSet<&'a str>>,
2914}
2915
2916fn partition_codes_by_mig(
2921 workflow: &AhbWorkflow,
2922) -> BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket<'_>>> {
2923 let mut out: BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket>> =
2924 BTreeMap::new();
2925 for field in &workflow.fields {
2926 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2927 continue;
2928 }
2929 if codes_are_alternatives(field) {
2932 continue;
2933 }
2934 let tag = extract_segment_id(&field.segment_path);
2935 let group_key = extract_group_path_key(&field.segment_path);
2936 let mig = field.mig_number.clone();
2937 let el = field.element_index.unwrap_or(0);
2938 let c = field.component_index.unwrap_or(0);
2939
2940 let bucket = out
2941 .entry((group_key, tag))
2942 .or_default()
2943 .entry(mig)
2944 .or_default();
2945
2946 let required: Vec<&str> = field
2953 .codes
2954 .iter()
2955 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2956 .map(|code| code.value.as_str())
2957 .collect();
2958
2959 if !required.is_empty() {
2966 if bucket.qualifier_position.is_none() {
2967 bucket.qualifier_position = Some((el, c));
2968 }
2969 if bucket.qualifier_position == Some((el, c)) {
2970 bucket.qualifier_values.extend(required.iter().copied());
2971 }
2972 }
2973
2974 for v in required {
2975 bucket.codes.entry((el, c)).or_default().insert(v);
2976 }
2977 }
2978 out
2979}
2980
2981fn global_qualifiers_by_tag(
2987 by_loc: &BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket<'_>>>,
2988) -> BTreeMap<String, BTreeMap<(usize, usize), HashSet<String>>> {
2989 let mut out: BTreeMap<String, BTreeMap<(usize, usize), HashSet<String>>> = BTreeMap::new();
2990 for ((_group, tag), migs) in by_loc {
2991 let tag_entry = out.entry(tag.clone()).or_default();
2992 for bucket in migs.values() {
2993 if let Some(pos) = bucket.qualifier_position {
2994 tag_entry
2995 .entry(pos)
2996 .or_default()
2997 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2998 }
2999 }
3000 }
3001 out
3002}
3003
3004fn matches_qualifier(seg: &OwnedSegment, bucket: &MigCodeBucket<'_>) -> bool {
3006 bucket.qualifier_position.is_some_and(|(el, c)| {
3007 seg.elements
3008 .get(el)
3009 .and_then(|e| e.get(c))
3010 .is_some_and(|v| bucket.qualifier_values.contains(v.as_str()))
3011 })
3012}
3013
3014fn data_slot_entries(
3020 workflow: &AhbWorkflow,
3021) -> HashMap<(String, String), Vec<(String, Option<String>)>> {
3022 let mut out: HashMap<(String, String), Vec<(String, Option<String>)>> = HashMap::new();
3023 let mut tags_by_mig: HashMap<String, BTreeMap<&str, Vec<String>>> = HashMap::new();
3028 for f in &workflow.fields {
3029 if let Some(n) = f.mig_number.as_deref() {
3030 let tags = tags_by_mig
3031 .entry(extract_group_path_key(&f.segment_path))
3032 .or_default()
3033 .entry(n)
3034 .or_default();
3035 let tag = extract_segment_id(&f.segment_path);
3036 if tags.last() != Some(&tag) {
3037 tags.push(tag);
3038 }
3039 }
3040 }
3041 let mut judged: HashSet<(&str, &str)> = HashSet::new();
3042 for field in &workflow.fields {
3043 let leading =
3044 field.element_index.unwrap_or(0) == 0 && field.component_index.unwrap_or(0) == 0;
3045 let Some(mig) = field.mig_number.as_deref() else {
3046 continue;
3047 };
3048 if !field.codes.is_empty() || !leading || !is_qualifier_field(&field.segment_path) {
3049 continue;
3050 }
3051 if !judged.insert((field.segment_path.as_str(), mig)) {
3052 continue;
3053 }
3054 let tag = extract_segment_id(&field.segment_path);
3055 let group_key = extract_group_path_key(&field.segment_path);
3056 if group_key.is_empty() {
3057 continue;
3058 }
3059 let entry = tags_by_mig.get(&group_key).and_then(|migs| {
3062 migs.range(..mig).rev().find_map(|(n, tags)| {
3063 tags.iter()
3064 .rev()
3065 .find(|t| **t != tag)
3066 .map(|t| (*n, t.clone()))
3067 })
3068 });
3069 if let Some((n, entry_tag)) = entry {
3070 let slot = (entry_tag, Some(n.to_string()));
3071 let entries = out.entry((group_key, tag)).or_default();
3072 if !entries.contains(&slot) {
3073 entries.push(slot);
3074 }
3075 }
3076 }
3077 out
3078}
3079
3080fn match_segment_to_mig<'a, 'b>(
3091 seg: &OwnedSegment,
3092 migs: &'a BTreeMap<Option<String>, MigCodeBucket<'b>>,
3093) -> Option<&'a MigCodeBucket<'b>> {
3094 let actual_at = |el: usize, c: usize| -> &str {
3095 seg.elements
3096 .get(el)
3097 .and_then(|e| e.get(c))
3098 .map(|s| s.as_str())
3099 .unwrap_or("")
3100 };
3101
3102 let mut best: Option<&MigCodeBucket> = None;
3103 let mut best_matches = 0usize;
3104
3105 for bucket in migs.values() {
3106 let Some((el, c)) = bucket.qualifier_position else {
3107 continue;
3108 };
3109 if !bucket.qualifier_values.contains(actual_at(el, c)) {
3110 continue;
3111 }
3112 let extra_matches = bucket
3113 .codes
3114 .iter()
3115 .filter(|(pos, _)| **pos != (el, c))
3116 .filter(|((e, k), allowed)| {
3117 let v = actual_at(*e, *k);
3118 !v.is_empty() && allowed.contains(v)
3119 })
3120 .count();
3121 if best.is_none() || extra_matches > best_matches {
3122 best = Some(bucket);
3123 best_matches = extra_matches;
3124 }
3125 }
3126 best
3127}
3128
3129fn extract_group_path_key(path: &str) -> String {
3134 let sg_parts: Vec<&str> = path
3135 .split('/')
3136 .take_while(|p| p.starts_with("SG"))
3137 .collect();
3138 sg_parts.join("/")
3139}
3140
3141fn extract_segment_id(path: &str) -> String {
3143 for part in path.split('/') {
3144 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
3146 continue;
3147 }
3148 if part.len() >= 3
3150 && part
3151 .chars()
3152 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
3153 {
3154 return part.to_string();
3155 }
3156 }
3157 path.split('/').next_back().unwrap_or(path).to_string()
3159}
3160
3161pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
3173 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
3175 if unh_count > 1 {
3176 return Some(ValidationIssue::new(
3177 Severity::Error,
3178 IssueKind::UntCountNotVerifiable { unh_count },
3179 ));
3180 }
3181
3182 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
3184 let declared: usize = unt.get_element(0).parse().ok()?;
3185
3186 let actual = segments
3188 .iter()
3189 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
3190 .count();
3191
3192 if declared != actual {
3193 Some(
3194 ValidationIssue::new(
3195 Severity::Error,
3196 IssueKind::UntSegmentCountMismatch { declared, actual },
3197 )
3198 .with_field_path("UNT/0074")
3199 .with_expected(actual.to_string())
3200 .with_actual(declared.to_string()),
3201 )
3202 } else {
3203 None
3204 }
3205}
3206
3207#[cfg(test)]
3208mod tests {
3209
3210 #[test]
3211 fn a_numeric_length_counts_digits_only() {
3212 assert_eq!(value_length("n..15", "-12.50"), 4);
3213 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
3214 assert_eq!(value_length("n5", "55001"), 5);
3215 assert_eq!(value_length("an..3", "Z.1"), 3);
3216 assert_eq!(value_length("an..35", "Müller"), 6);
3217 }
3218 use super::super::codes::ErrorCodes;
3219 use super::super::issue::ValidationCategory;
3220 use super::*;
3221 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
3222 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
3223 use std::collections::HashMap;
3224
3225 fn narrate(issue: &ValidationIssue) -> String {
3228 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
3229 }
3230
3231 struct MockEvaluator {
3233 results: HashMap<u32, CR>,
3234 }
3235
3236 impl MockEvaluator {
3237 fn new(results: Vec<(u32, CR)>) -> Self {
3238 Self {
3239 results: results.into_iter().collect(),
3240 }
3241 }
3242
3243 fn all_true(ids: &[u32]) -> Self {
3244 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
3245 }
3246 }
3247
3248 impl ConditionEvaluator for MockEvaluator {
3249 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
3250 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
3251 }
3252 fn is_external(&self, _condition: u32) -> bool {
3253 false
3254 }
3255 fn message_type(&self) -> &str {
3256 "UTILMD"
3257 }
3258 fn format_version(&self) -> &str {
3259 "FV2510"
3260 }
3261 }
3262
3263 #[test]
3266 fn a_present_code_is_judged_by_its_own_status() {
3267 let code = |value: &str, status: &str| AhbCodeRule {
3269 value: value.to_string(),
3270 description: String::new(),
3271 ahb_status: status.to_string(),
3272 };
3273 let field = AhbFieldRule {
3274 segment_path: "SG4/STS/C556/9013".to_string(),
3275 ahb_status: "X [192]".to_string(),
3276 codes: vec![code("E01", "X [192]"), code("E03", "X")],
3277 ..Default::default()
3278 };
3279 assert_eq!(governing_status(&field, Some("E03")), "X");
3280 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
3281 assert_eq!(governing_status(&field, None), "X [192]");
3282 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
3283
3284 let own = AhbFieldRule {
3286 ahb_status: "X [931]".to_string(),
3287 codes: vec![code("303", "X")],
3288 ..Default::default()
3289 };
3290 assert_eq!(governing_status(&own, Some("303")), "X [931]");
3291 assert!(!codes_are_alternatives(&own));
3292
3293 let alternatives = AhbFieldRule {
3296 ahb_status: "X [153] ∧ [538]".to_string(),
3297 codes: vec![
3298 code("Z74", "X [103] ∧ [537]"),
3299 code("Z75", "X [103] ∧ [537]"),
3300 ],
3301 ..Default::default()
3302 };
3303 assert!(codes_are_alternatives(&alternatives));
3304 assert_eq!(
3305 governing_status(&alternatives, Some("Z74")),
3306 "X [103] ∧ [537]"
3307 );
3308 assert_eq!(
3309 governing_status(&alternatives, Some("A01")),
3310 "X [153] ∧ [538]"
3311 );
3312 assert_eq!(governing_status(&alternatives, None), "X [153] ∧ [538]");
3313
3314 let restricting = AhbFieldRule {
3316 ahb_status: "X [2] ∨ [3]".to_string(),
3317 codes: vec![code("UNH", "X"), code("UNT", "X")],
3318 ..Default::default()
3319 };
3320 assert!(!codes_are_alternatives(&restricting));
3321 assert_eq!(governing_status(&restricting, Some("UNH")), "X [2] ∨ [3]");
3322 }
3323
3324 #[test]
3325 fn test_extract_segment_id_simple() {
3326 assert_eq!(extract_segment_id("NAD"), "NAD");
3327 }
3328
3329 #[test]
3330 fn test_extract_segment_id_with_sg_prefix() {
3331 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
3332 }
3333
3334 #[test]
3335 fn test_extract_segment_id_nested_sg() {
3336 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
3337 }
3338
3339 #[test]
3342 fn test_validate_missing_mandatory_field() {
3343 let evaluator = MockEvaluator::all_true(&[182, 152]);
3344 let validator = EdifactValidator::new(evaluator);
3345 let external = NoOpExternalProvider;
3346
3347 let workflow = AhbWorkflow {
3348 pruefidentifikator: "11001".to_string(),
3349 description: "Test".to_string(),
3350 communication_direction: None,
3351 fields: vec![AhbFieldRule {
3352 segment_path: "SG2/NAD/C082/3039".to_string(),
3353 name: "MP-ID des MSB".to_string(),
3354 ahb_status: "Muss [182] ∧ [152]".to_string(),
3355 codes: vec![],
3356 parent_group_ahb_status: None,
3357 segment_ahb_status: None,
3358 ..Default::default()
3359 }],
3360 ub_definitions: BTreeMap::new(),
3361 package_conditions: Default::default(),
3362 };
3363
3364 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3366
3367 assert!(!report.is_valid());
3369 let errors: Vec<_> = report.errors().collect();
3370 assert_eq!(errors.len(), 1);
3371 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3372 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
3373 }
3374
3375 #[test]
3376 fn test_validate_condition_false_no_error() {
3377 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
3379 let validator = EdifactValidator::new(evaluator);
3380 let external = NoOpExternalProvider;
3381
3382 let workflow = AhbWorkflow {
3383 pruefidentifikator: "11001".to_string(),
3384 description: "Test".to_string(),
3385 communication_direction: None,
3386 fields: vec![AhbFieldRule {
3387 segment_path: "NAD".to_string(),
3388 name: "Partnerrolle".to_string(),
3389 ahb_status: "Muss [182] ∧ [152]".to_string(),
3390 codes: vec![],
3391 parent_group_ahb_status: None,
3392 segment_ahb_status: None,
3393 ..Default::default()
3394 }],
3395 ub_definitions: BTreeMap::new(),
3396 package_conditions: Default::default(),
3397 };
3398
3399 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3400
3401 assert!(report.is_valid());
3403 }
3404
3405 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
3407 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3408 let workflow = AhbWorkflow {
3409 pruefidentifikator: "11001".to_string(),
3410 description: "Test".to_string(),
3411 communication_direction: None,
3412 fields: vec![AhbFieldRule {
3413 segment_path: "SG2/NAD/C082/3039".to_string(),
3414 name: "MP-ID".to_string(),
3415 ahb_status: status.to_string(),
3416 ..Default::default()
3417 }],
3418 ub_definitions: BTreeMap::new(),
3419 package_conditions: Default::default(),
3420 };
3421 validator.validate(
3422 &[],
3423 &workflow,
3424 &NoOpExternalProvider,
3425 ValidationLevel::Conditions,
3426 )
3427 }
3428
3429 #[test]
3430 fn a_later_soll_line_alone_does_not_require_the_field() {
3431 let report = validate_absent_field(
3433 "Muss [2119] \r\nSoll [130]",
3434 vec![(2119, CR::False), (130, CR::True)],
3435 );
3436 assert!(
3437 report.is_valid(),
3438 "{:?}",
3439 report.errors().collect::<Vec<_>>()
3440 );
3441 }
3442
3443 #[test]
3444 fn a_later_muss_line_requires_the_field() {
3445 let report = validate_absent_field(
3447 "Soll [47]\r\nMuss [46]",
3448 vec![(47, CR::False), (46, CR::True)],
3449 );
3450 let errors: Vec<_> = report.errors().collect();
3451 assert_eq!(errors.len(), 1);
3452 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3453 }
3454
3455 #[test]
3456 fn a_weaker_unknown_line_does_not_stop_a_muss() {
3457 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
3459 let errors: Vec<_> = report.errors().collect();
3460 assert_eq!(errors.len(), 1);
3461 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3462 }
3463
3464 #[test]
3465 fn a_stricter_unknown_line_leaves_the_status_undecided() {
3466 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
3467 assert!(report.is_valid());
3468 let infos: Vec<_> = report.infos().collect();
3469 assert_eq!(infos.len(), 1, "{infos:?}");
3470 match &infos[0].kind {
3471 IssueKind::ConditionUnknown { unresolved, .. } => {
3472 assert_eq!(unresolved.missing, vec![2119]);
3473 }
3474 other => panic!("expected ConditionUnknown, got {other:?}"),
3475 }
3476 }
3477
3478 #[test]
3479 fn test_validate_condition_unknown_adds_info() {
3480 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
3482 let validator = EdifactValidator::new(evaluator);
3484 let external = NoOpExternalProvider;
3485
3486 let workflow = AhbWorkflow {
3487 pruefidentifikator: "11001".to_string(),
3488 description: "Test".to_string(),
3489 communication_direction: None,
3490 fields: vec![AhbFieldRule {
3491 segment_path: "NAD".to_string(),
3492 name: "Partnerrolle".to_string(),
3493 ahb_status: "Muss [182] ∧ [152]".to_string(),
3494 codes: vec![],
3495 parent_group_ahb_status: None,
3496 segment_ahb_status: None,
3497 ..Default::default()
3498 }],
3499 ub_definitions: BTreeMap::new(),
3500 package_conditions: Default::default(),
3501 };
3502
3503 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3504
3505 assert!(report.is_valid());
3507 let infos: Vec<_> = report.infos().collect();
3508 assert_eq!(infos.len(), 1);
3509 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
3510 }
3511
3512 #[test]
3513 fn test_validate_structure_level_skips_conditions() {
3514 let evaluator = MockEvaluator::all_true(&[182, 152]);
3515 let validator = EdifactValidator::new(evaluator);
3516 let external = NoOpExternalProvider;
3517
3518 let workflow = AhbWorkflow {
3519 pruefidentifikator: "11001".to_string(),
3520 description: "Test".to_string(),
3521 communication_direction: None,
3522 fields: vec![AhbFieldRule {
3523 segment_path: "NAD".to_string(),
3524 name: "Partnerrolle".to_string(),
3525 ahb_status: "Muss [182] ∧ [152]".to_string(),
3526 codes: vec![],
3527 parent_group_ahb_status: None,
3528 segment_ahb_status: None,
3529 ..Default::default()
3530 }],
3531 ub_definitions: BTreeMap::new(),
3532 package_conditions: Default::default(),
3533 };
3534
3535 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
3537
3538 assert!(report.is_valid());
3540 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
3541 }
3542
3543 #[test]
3544 fn test_validate_empty_workflow_no_condition_errors() {
3545 let evaluator = MockEvaluator::all_true(&[]);
3546 let validator = EdifactValidator::new(evaluator);
3547 let external = NoOpExternalProvider;
3548
3549 let empty_workflow = AhbWorkflow {
3550 pruefidentifikator: String::new(),
3551 description: String::new(),
3552 communication_direction: None,
3553 fields: vec![],
3554 ub_definitions: BTreeMap::new(),
3555 package_conditions: Default::default(),
3556 };
3557
3558 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
3559
3560 assert!(report.is_valid());
3561 }
3562
3563 #[test]
3564 fn test_validate_bare_muss_always_required() {
3565 let evaluator = MockEvaluator::new(vec![]);
3566 let validator = EdifactValidator::new(evaluator);
3567 let external = NoOpExternalProvider;
3568
3569 let workflow = AhbWorkflow {
3570 pruefidentifikator: "55001".to_string(),
3571 description: "Test".to_string(),
3572 communication_direction: Some("NB an LF".to_string()),
3573 fields: vec![AhbFieldRule {
3574 segment_path: "SG2/NAD/3035".to_string(),
3575 name: "Partnerrolle".to_string(),
3576 ahb_status: "Muss".to_string(), codes: vec![],
3578 parent_group_ahb_status: None,
3579 segment_ahb_status: None,
3580 ..Default::default()
3581 }],
3582 ub_definitions: BTreeMap::new(),
3583 package_conditions: Default::default(),
3584 };
3585
3586 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3587
3588 assert!(!report.is_valid());
3590 assert_eq!(report.error_count(), 1);
3591 }
3592
3593 #[test]
3594 fn test_validate_x_status_is_mandatory() {
3595 let evaluator = MockEvaluator::new(vec![]);
3596 let validator = EdifactValidator::new(evaluator);
3597 let external = NoOpExternalProvider;
3598
3599 let workflow = AhbWorkflow {
3600 pruefidentifikator: "55001".to_string(),
3601 description: "Test".to_string(),
3602 communication_direction: None,
3603 fields: vec![AhbFieldRule {
3604 segment_path: "DTM".to_string(),
3605 name: "Datum".to_string(),
3606 ahb_status: "X".to_string(),
3607 codes: vec![],
3608 parent_group_ahb_status: None,
3609 segment_ahb_status: None,
3610 ..Default::default()
3611 }],
3612 ub_definitions: BTreeMap::new(),
3613 package_conditions: Default::default(),
3614 };
3615
3616 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3617
3618 assert!(!report.is_valid());
3619 let errors: Vec<_> = report.errors().collect();
3620 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3621 }
3622
3623 #[test]
3624 fn test_validate_soll_not_mandatory() {
3625 let evaluator = MockEvaluator::new(vec![]);
3626 let validator = EdifactValidator::new(evaluator);
3627 let external = NoOpExternalProvider;
3628
3629 let workflow = AhbWorkflow {
3630 pruefidentifikator: "55001".to_string(),
3631 description: "Test".to_string(),
3632 communication_direction: None,
3633 fields: vec![AhbFieldRule {
3634 segment_path: "DTM".to_string(),
3635 name: "Datum".to_string(),
3636 ahb_status: "Soll".to_string(),
3637 codes: vec![],
3638 parent_group_ahb_status: None,
3639 segment_ahb_status: None,
3640 ..Default::default()
3641 }],
3642 ub_definitions: BTreeMap::new(),
3643 package_conditions: Default::default(),
3644 };
3645
3646 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3647
3648 assert!(report.is_valid());
3650 }
3651
3652 #[test]
3653 fn test_report_includes_metadata() {
3654 let evaluator = MockEvaluator::new(vec![]);
3655 let validator = EdifactValidator::new(evaluator);
3656 let external = NoOpExternalProvider;
3657
3658 let workflow = AhbWorkflow {
3659 pruefidentifikator: "55001".to_string(),
3660 description: String::new(),
3661 communication_direction: None,
3662 fields: vec![],
3663 ub_definitions: BTreeMap::new(),
3664 package_conditions: Default::default(),
3665 };
3666
3667 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
3668
3669 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
3670 assert_eq!(report.level, ValidationLevel::Full);
3671 assert_eq!(report.message_type, "UTILMD");
3672 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
3673 }
3674
3675 #[test]
3676 fn test_validate_with_navigator_returns_report() {
3677 let evaluator = MockEvaluator::all_true(&[]);
3678 let validator = EdifactValidator::new(evaluator);
3679 let external = NoOpExternalProvider;
3680 let nav = crate::eval::NoOpGroupNavigator;
3681
3682 let workflow = AhbWorkflow {
3683 pruefidentifikator: "55001".to_string(),
3684 description: "Test".to_string(),
3685 communication_direction: None,
3686 fields: vec![],
3687 ub_definitions: BTreeMap::new(),
3688 package_conditions: Default::default(),
3689 };
3690
3691 let report = validator.validate_with_navigator(
3692 &[],
3693 &workflow,
3694 &external,
3695 ValidationLevel::Full,
3696 &nav,
3697 );
3698 assert!(report.is_valid());
3699 }
3700
3701 #[test]
3702 fn test_code_validation_composite_paths_valid_codes() {
3703 let evaluator = MockEvaluator::new(vec![]);
3707 let validator = EdifactValidator::new(evaluator);
3708 let external = NoOpExternalProvider;
3709
3710 let unh_segment = OwnedSegment {
3711 id: "UNH".to_string(),
3712 elements: vec![
3713 vec!["ALEXANDE951842".to_string()],
3714 vec![
3715 "UTILMD".to_string(),
3716 "D".to_string(),
3717 "11A".to_string(),
3718 "UN".to_string(),
3719 "S2.1".to_string(),
3720 ],
3721 ],
3722 segment_number: 1,
3723 };
3724
3725 let workflow = AhbWorkflow {
3726 pruefidentifikator: "55001".to_string(),
3727 description: "Test".to_string(),
3728 communication_direction: None,
3729 fields: vec![
3730 AhbFieldRule {
3731 segment_path: "UNH/S009/0065".to_string(),
3732 name: "Nachrichtentyp".to_string(),
3733 ahb_status: "X".to_string(),
3734 codes: vec![AhbCodeRule {
3735 value: "UTILMD".to_string(),
3736 description: "Stammdaten".to_string(),
3737 ahb_status: "X".to_string(),
3738 }],
3739 parent_group_ahb_status: None,
3740 segment_ahb_status: None,
3741 element_index: Some(1),
3742 component_index: Some(0),
3743 ..Default::default()
3744 },
3745 AhbFieldRule {
3746 segment_path: "UNH/S009/0052".to_string(),
3747 name: "Version".to_string(),
3748 ahb_status: "X".to_string(),
3749 codes: vec![AhbCodeRule {
3750 value: "D".to_string(),
3751 description: "Draft".to_string(),
3752 ahb_status: "X".to_string(),
3753 }],
3754 parent_group_ahb_status: None,
3755 segment_ahb_status: None,
3756 element_index: Some(1),
3757 component_index: Some(1),
3758 ..Default::default()
3759 },
3760 ],
3761 ub_definitions: BTreeMap::new(),
3762 package_conditions: Default::default(),
3763 };
3764
3765 let report = validator.validate(
3766 &[unh_segment],
3767 &workflow,
3768 &external,
3769 ValidationLevel::Conditions,
3770 );
3771
3772 let code_errors: Vec<_> = report
3773 .by_category(ValidationCategory::Code)
3774 .filter(|i| i.severity == Severity::Error)
3775 .collect();
3776 assert!(
3777 code_errors.is_empty(),
3778 "Expected no code errors when composite values match allowed codes, got: {:?}",
3779 code_errors
3780 );
3781 }
3782
3783 #[test]
3784 fn test_code_validation_partitions_by_mig_number() {
3785 let evaluator = MockEvaluator::new(vec![]);
3789 let validator = EdifactValidator::new(evaluator);
3790 let external = NoOpExternalProvider;
3791
3792 let sts_7 = OwnedSegment {
3793 id: "STS".to_string(),
3794 elements: vec![
3795 vec!["7".to_string()],
3796 vec![String::new()],
3797 vec!["GH02".to_string()],
3798 vec!["ZW4".to_string()],
3799 ],
3800 segment_number: 1,
3801 };
3802 let sts_e01 = OwnedSegment {
3803 id: "STS".to_string(),
3804 elements: vec![
3805 vec!["E01".to_string()],
3806 vec![String::new()],
3807 vec!["A99".to_string(), "E_0614".to_string()],
3808 ],
3809 segment_number: 2,
3810 };
3811
3812 let workflow = AhbWorkflow {
3813 pruefidentifikator: "55018".to_string(),
3814 description: "Test".to_string(),
3815 communication_direction: None,
3816 fields: vec![
3817 AhbFieldRule {
3819 group_ahb_status: None,
3820 segment_path: "SG4/STS/C601/9015".to_string(),
3821 name: "Statuskategorie".to_string(),
3822 ahb_status: "X".to_string(),
3823 codes: vec![AhbCodeRule {
3824 value: "7".to_string(),
3825 description: "Transaktionsgrund".to_string(),
3826 ahb_status: "X".to_string(),
3827 }],
3828 parent_group_ahb_status: None,
3829 segment_ahb_status: None,
3830 element_index: Some(0),
3831 component_index: Some(0),
3832 mig_number: Some("00035".to_string()),
3833 format: None,
3834 },
3835 AhbFieldRule {
3836 group_ahb_status: None,
3837 segment_path: "SG4/STS/C556/9013".to_string(),
3838 name: "Statusanlaß".to_string(),
3839 ahb_status: "X".to_string(),
3840 codes: vec![AhbCodeRule {
3841 value: "E03".to_string(),
3842 description: "Transaktionsgrund".to_string(),
3843 ahb_status: "X".to_string(),
3844 }],
3845 parent_group_ahb_status: None,
3846 segment_ahb_status: None,
3847 element_index: Some(2),
3848 component_index: Some(0),
3849 mig_number: Some("00035".to_string()),
3850 format: None,
3851 },
3852 AhbFieldRule {
3854 group_ahb_status: None,
3855 segment_path: "SG4/STS/C601/9015".to_string(),
3856 name: "Statuskategorie".to_string(),
3857 ahb_status: "X".to_string(),
3858 codes: vec![AhbCodeRule {
3859 value: "E01".to_string(),
3860 description: "Antwort".to_string(),
3861 ahb_status: "X".to_string(),
3862 }],
3863 parent_group_ahb_status: None,
3864 segment_ahb_status: None,
3865 element_index: Some(0),
3866 component_index: Some(0),
3867 mig_number: Some("00036".to_string()),
3868 format: None,
3869 },
3870 ],
3871 ub_definitions: BTreeMap::new(),
3872 package_conditions: Default::default(),
3873 };
3874
3875 let report = validator.validate(
3876 &[sts_7, sts_e01],
3877 &workflow,
3878 &external,
3879 ValidationLevel::Conditions,
3880 );
3881
3882 let code_errors: Vec<_> = report
3883 .by_category(ValidationCategory::Code)
3884 .filter(|i| {
3885 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3886 })
3887 .collect();
3888 assert_eq!(
3889 code_errors.len(),
3890 1,
3891 "Expected one COD002 (for GH02 only), got: {:?}",
3892 code_errors
3893 );
3894 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3895 }
3896
3897 #[test]
3898 fn test_code_validation_composite_paths_detects_invalid_code() {
3899 let evaluator = MockEvaluator::new(vec![]);
3902 let validator = EdifactValidator::new(evaluator);
3903 let external = NoOpExternalProvider;
3904
3905 let sts_segment = OwnedSegment {
3906 id: "STS".to_string(),
3907 elements: vec![
3908 vec!["7".to_string()],
3909 vec![String::new()],
3910 vec!["GH02".to_string()],
3911 vec!["ZW4".to_string()],
3912 ],
3913 segment_number: 1,
3914 };
3915
3916 let workflow = AhbWorkflow {
3917 pruefidentifikator: "55018".to_string(),
3918 description: "Test".to_string(),
3919 communication_direction: None,
3920 fields: vec![AhbFieldRule {
3921 segment_path: "SG4/STS/C556/9013".to_string(),
3922 name: "Statusanlaß".to_string(),
3923 ahb_status: "X".to_string(),
3924 codes: vec![AhbCodeRule {
3925 value: "E03".to_string(),
3926 description: "Transaktionsgrund".to_string(),
3927 ahb_status: "X".to_string(),
3928 }],
3929 parent_group_ahb_status: None,
3930 segment_ahb_status: None,
3931 element_index: Some(2),
3932 component_index: Some(0),
3933 ..Default::default()
3934 }],
3935 ub_definitions: BTreeMap::new(),
3936 package_conditions: Default::default(),
3937 };
3938
3939 let report = validator.validate(
3940 &[sts_segment],
3941 &workflow,
3942 &external,
3943 ValidationLevel::Conditions,
3944 );
3945
3946 let code_errors: Vec<_> = report
3947 .by_category(ValidationCategory::Code)
3948 .filter(|i| {
3949 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3950 })
3951 .collect();
3952 assert_eq!(
3953 code_errors.len(),
3954 1,
3955 "Expected COD002 for GH02, got: {:?}",
3956 code_errors
3957 );
3958 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3959 }
3960
3961 #[test]
3962 fn test_cross_field_code_validation_valid_qualifiers() {
3963 let evaluator = MockEvaluator::new(vec![]);
3966 let validator = EdifactValidator::new(evaluator);
3967 let external = NoOpExternalProvider;
3968
3969 let nad_ms = OwnedSegment {
3970 id: "NAD".to_string(),
3971 elements: vec![vec!["MS".to_string()]],
3972 segment_number: 4,
3973 };
3974 let nad_mr = OwnedSegment {
3975 id: "NAD".to_string(),
3976 elements: vec![vec!["MR".to_string()]],
3977 segment_number: 5,
3978 };
3979
3980 let workflow = AhbWorkflow {
3981 pruefidentifikator: "55001".to_string(),
3982 description: "Test".to_string(),
3983 communication_direction: None,
3984 fields: vec![
3985 AhbFieldRule {
3986 segment_path: "SG2/NAD/3035".to_string(),
3987 name: "Absender".to_string(),
3988 ahb_status: "X".to_string(),
3989 codes: vec![AhbCodeRule {
3990 value: "MS".to_string(),
3991 description: "Absender".to_string(),
3992 ahb_status: "X".to_string(),
3993 }],
3994 parent_group_ahb_status: None,
3995 segment_ahb_status: None,
3996 ..Default::default()
3997 },
3998 AhbFieldRule {
3999 segment_path: "SG2/NAD/3035".to_string(),
4000 name: "Empfaenger".to_string(),
4001 ahb_status: "X".to_string(),
4002 codes: vec![AhbCodeRule {
4003 value: "MR".to_string(),
4004 description: "Empfaenger".to_string(),
4005 ahb_status: "X".to_string(),
4006 }],
4007 parent_group_ahb_status: None,
4008 segment_ahb_status: None,
4009 ..Default::default()
4010 },
4011 ],
4012 ub_definitions: BTreeMap::new(),
4013 package_conditions: Default::default(),
4014 };
4015
4016 let report = validator.validate(
4017 &[nad_ms, nad_mr],
4018 &workflow,
4019 &external,
4020 ValidationLevel::Conditions,
4021 );
4022
4023 let code_errors: Vec<_> = report
4024 .by_category(ValidationCategory::Code)
4025 .filter(|i| i.severity == Severity::Error)
4026 .collect();
4027 assert!(
4028 code_errors.is_empty(),
4029 "Expected no code errors for valid qualifiers, got: {:?}",
4030 code_errors
4031 );
4032 }
4033
4034 #[test]
4035 fn test_cross_field_code_validation_catches_invalid_qualifier() {
4036 let evaluator = MockEvaluator::new(vec![]);
4038 let validator = EdifactValidator::new(evaluator);
4039 let external = NoOpExternalProvider;
4040
4041 let nad_ms = OwnedSegment {
4042 id: "NAD".to_string(),
4043 elements: vec![vec!["MS".to_string()]],
4044 segment_number: 4,
4045 };
4046 let nad_mt = OwnedSegment {
4047 id: "NAD".to_string(),
4048 elements: vec![vec!["MT".to_string()]], segment_number: 5,
4050 };
4051
4052 let workflow = AhbWorkflow {
4053 pruefidentifikator: "55001".to_string(),
4054 description: "Test".to_string(),
4055 communication_direction: None,
4056 fields: vec![
4057 AhbFieldRule {
4058 segment_path: "SG2/NAD/3035".to_string(),
4059 name: "Absender".to_string(),
4060 ahb_status: "X".to_string(),
4061 codes: vec![AhbCodeRule {
4062 value: "MS".to_string(),
4063 description: "Absender".to_string(),
4064 ahb_status: "X".to_string(),
4065 }],
4066 parent_group_ahb_status: None,
4067 segment_ahb_status: None,
4068 ..Default::default()
4069 },
4070 AhbFieldRule {
4071 segment_path: "SG2/NAD/3035".to_string(),
4072 name: "Empfaenger".to_string(),
4073 ahb_status: "X".to_string(),
4074 codes: vec![AhbCodeRule {
4075 value: "MR".to_string(),
4076 description: "Empfaenger".to_string(),
4077 ahb_status: "X".to_string(),
4078 }],
4079 parent_group_ahb_status: None,
4080 segment_ahb_status: None,
4081 ..Default::default()
4082 },
4083 ],
4084 ub_definitions: BTreeMap::new(),
4085 package_conditions: Default::default(),
4086 };
4087
4088 let report = validator.validate(
4089 &[nad_ms, nad_mt],
4090 &workflow,
4091 &external,
4092 ValidationLevel::Conditions,
4093 );
4094
4095 let code_errors: Vec<_> = report
4096 .by_category(ValidationCategory::Code)
4097 .filter(|i| i.severity == Severity::Error)
4098 .collect();
4099 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
4100 assert!(narrate(code_errors[0]).contains("MT"));
4101 assert!(narrate(code_errors[0]).contains("MR"));
4102 assert!(narrate(code_errors[0]).contains("MS"));
4103 }
4104
4105 #[test]
4106 fn test_cross_field_code_validation_unions_across_groups() {
4107 let evaluator = MockEvaluator::new(vec![]);
4111 let validator = EdifactValidator::new(evaluator);
4112 let external = NoOpExternalProvider;
4113
4114 let segments = vec![
4115 OwnedSegment {
4116 id: "NAD".to_string(),
4117 elements: vec![vec!["MS".to_string()]],
4118 segment_number: 3,
4119 },
4120 OwnedSegment {
4121 id: "NAD".to_string(),
4122 elements: vec![vec!["MR".to_string()]],
4123 segment_number: 4,
4124 },
4125 OwnedSegment {
4126 id: "NAD".to_string(),
4127 elements: vec![vec!["Z04".to_string()]],
4128 segment_number: 20,
4129 },
4130 OwnedSegment {
4131 id: "NAD".to_string(),
4132 elements: vec![vec!["Z09".to_string()]],
4133 segment_number: 21,
4134 },
4135 OwnedSegment {
4136 id: "NAD".to_string(),
4137 elements: vec![vec!["MT".to_string()]], segment_number: 22,
4139 },
4140 ];
4141
4142 let workflow = AhbWorkflow {
4143 pruefidentifikator: "55001".to_string(),
4144 description: "Test".to_string(),
4145 communication_direction: None,
4146 fields: vec![
4147 AhbFieldRule {
4148 segment_path: "SG2/NAD/3035".to_string(),
4149 name: "Absender".to_string(),
4150 ahb_status: "X".to_string(),
4151 codes: vec![AhbCodeRule {
4152 value: "MS".to_string(),
4153 description: "Absender".to_string(),
4154 ahb_status: "X".to_string(),
4155 }],
4156 parent_group_ahb_status: None,
4157 segment_ahb_status: None,
4158 ..Default::default()
4159 },
4160 AhbFieldRule {
4161 segment_path: "SG2/NAD/3035".to_string(),
4162 name: "Empfaenger".to_string(),
4163 ahb_status: "X".to_string(),
4164 codes: vec![AhbCodeRule {
4165 value: "MR".to_string(),
4166 description: "Empfaenger".to_string(),
4167 ahb_status: "X".to_string(),
4168 }],
4169 parent_group_ahb_status: None,
4170 segment_ahb_status: None,
4171 ..Default::default()
4172 },
4173 AhbFieldRule {
4174 segment_path: "SG4/SG12/NAD/3035".to_string(),
4175 name: "Anschlussnutzer".to_string(),
4176 ahb_status: "X".to_string(),
4177 codes: vec![AhbCodeRule {
4178 value: "Z04".to_string(),
4179 description: "Anschlussnutzer".to_string(),
4180 ahb_status: "X".to_string(),
4181 }],
4182 parent_group_ahb_status: None,
4183 segment_ahb_status: None,
4184 ..Default::default()
4185 },
4186 AhbFieldRule {
4187 segment_path: "SG4/SG12/NAD/3035".to_string(),
4188 name: "Korrespondenzanschrift".to_string(),
4189 ahb_status: "X".to_string(),
4190 codes: vec![AhbCodeRule {
4191 value: "Z09".to_string(),
4192 description: "Korrespondenzanschrift".to_string(),
4193 ahb_status: "X".to_string(),
4194 }],
4195 parent_group_ahb_status: None,
4196 segment_ahb_status: None,
4197 ..Default::default()
4198 },
4199 ],
4200 ub_definitions: BTreeMap::new(),
4201 package_conditions: Default::default(),
4202 };
4203
4204 let report =
4205 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
4206
4207 let code_errors: Vec<_> = report
4208 .by_category(ValidationCategory::Code)
4209 .filter(|i| i.severity == Severity::Error)
4210 .collect();
4211 assert_eq!(
4212 code_errors.len(),
4213 1,
4214 "Expected exactly one COD002 error for MT, got: {:?}",
4215 code_errors
4216 );
4217 assert!(narrate(code_errors[0]).contains("MT"));
4218 }
4219
4220 #[test]
4221 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
4222 let evaluator = MockEvaluator::new(vec![]);
4231 let validator = EdifactValidator::new(evaluator);
4232 let external = NoOpExternalProvider;
4233
4234 let qty_67 = OwnedSegment {
4235 id: "QTY".to_string(),
4236 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
4237 segment_number: 10,
4238 };
4239
4240 let workflow = AhbWorkflow {
4241 pruefidentifikator: "13025".to_string(),
4242 description: "Test".to_string(),
4243 communication_direction: None,
4244 fields: vec![AhbFieldRule {
4245 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
4246 name: "Menge, Qualifier".to_string(),
4247 ahb_status: "X".to_string(),
4248 codes: vec![
4249 AhbCodeRule {
4250 value: "220".to_string(),
4251 description: "Wahrer Wert".to_string(),
4252 ahb_status: "X".to_string(),
4253 },
4254 AhbCodeRule {
4255 value: "67".to_string(),
4256 description: "Ersatzwert".to_string(),
4257 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
4258 },
4259 AhbCodeRule {
4260 value: "Z18".to_string(),
4261 description: "Vorläufiger Wert".to_string(),
4262 ahb_status: "X [35]".to_string(),
4263 },
4264 ],
4265 parent_group_ahb_status: None,
4266 segment_ahb_status: None,
4267 element_index: Some(0),
4268 component_index: Some(0),
4269 ..Default::default()
4270 }],
4271 ub_definitions: BTreeMap::new(),
4272 package_conditions: Default::default(),
4273 };
4274
4275 let report =
4276 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
4277
4278 let code_errors: Vec<_> = report
4279 .by_category(ValidationCategory::Code)
4280 .filter(|i| i.severity == Severity::Error)
4281 .collect();
4282 assert!(
4283 code_errors.is_empty(),
4284 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
4285 code_errors
4286 );
4287 }
4288
4289 #[test]
4290 fn test_is_qualifier_field_simple_paths() {
4291 assert!(is_qualifier_field("NAD/3035"));
4292 assert!(is_qualifier_field("SG2/NAD/3035"));
4293 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
4294 assert!(is_qualifier_field("LOC/3227"));
4295 }
4296
4297 #[test]
4298 fn test_is_qualifier_field_composite_paths() {
4299 assert!(is_qualifier_field("UNH/S009/0065"));
4303 assert!(is_qualifier_field("NAD/C082/3039"));
4304 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
4305 assert!(is_qualifier_field("SG4/STS/C556/9013"));
4306 }
4307
4308 #[test]
4309 fn test_is_qualifier_field_bare_segment() {
4310 assert!(!is_qualifier_field("NAD"));
4311 assert!(!is_qualifier_field("SG2/NAD"));
4312 }
4313
4314 #[test]
4315 fn test_is_qualifier_field_rejects_deep_paths() {
4316 assert!(!is_qualifier_field("SEG/A/B/C/D"));
4318 }
4319
4320 #[test]
4321 fn test_missing_qualifier_instance_is_detected() {
4322 let evaluator = MockEvaluator::new(vec![]);
4325 let validator = EdifactValidator::new(evaluator);
4326 let external = NoOpExternalProvider;
4327
4328 let nad_ms = OwnedSegment {
4329 id: "NAD".to_string(),
4330 elements: vec![vec!["MS".to_string()]],
4331 segment_number: 3,
4332 };
4333
4334 let workflow = AhbWorkflow {
4335 pruefidentifikator: "55001".to_string(),
4336 description: "Test".to_string(),
4337 communication_direction: None,
4338 fields: vec![
4339 AhbFieldRule {
4340 segment_path: "SG2/NAD/3035".to_string(),
4341 name: "Absender".to_string(),
4342 ahb_status: "X".to_string(),
4343 codes: vec![AhbCodeRule {
4344 value: "MS".to_string(),
4345 description: "Absender".to_string(),
4346 ahb_status: "X".to_string(),
4347 }],
4348 parent_group_ahb_status: None,
4349 segment_ahb_status: None,
4350 ..Default::default()
4351 },
4352 AhbFieldRule {
4353 segment_path: "SG2/NAD/3035".to_string(),
4354 name: "Empfaenger".to_string(),
4355 ahb_status: "Muss".to_string(),
4356 codes: vec![AhbCodeRule {
4357 value: "MR".to_string(),
4358 description: "Empfaenger".to_string(),
4359 ahb_status: "X".to_string(),
4360 }],
4361 parent_group_ahb_status: None,
4362 segment_ahb_status: None,
4363 ..Default::default()
4364 },
4365 ],
4366 ub_definitions: BTreeMap::new(),
4367 package_conditions: Default::default(),
4368 };
4369
4370 let report =
4371 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
4372
4373 let ahb_errors: Vec<_> = report
4374 .by_category(ValidationCategory::Ahb)
4375 .filter(|i| i.severity == Severity::Error)
4376 .collect();
4377 assert_eq!(
4378 ahb_errors.len(),
4379 1,
4380 "Expected AHB001 for missing NAD+MR, got: {:?}",
4381 ahb_errors
4382 );
4383 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4384 }
4385
4386 #[test]
4387 fn test_present_qualifier_instance_no_error() {
4388 let evaluator = MockEvaluator::new(vec![]);
4390 let validator = EdifactValidator::new(evaluator);
4391 let external = NoOpExternalProvider;
4392
4393 let segments = vec![
4394 OwnedSegment {
4395 id: "NAD".to_string(),
4396 elements: vec![vec!["MS".to_string()]],
4397 segment_number: 3,
4398 },
4399 OwnedSegment {
4400 id: "NAD".to_string(),
4401 elements: vec![vec!["MR".to_string()]],
4402 segment_number: 4,
4403 },
4404 ];
4405
4406 let workflow = AhbWorkflow {
4407 pruefidentifikator: "55001".to_string(),
4408 description: "Test".to_string(),
4409 communication_direction: None,
4410 fields: vec![
4411 AhbFieldRule {
4412 segment_path: "SG2/NAD/3035".to_string(),
4413 name: "Absender".to_string(),
4414 ahb_status: "Muss".to_string(),
4415 codes: vec![AhbCodeRule {
4416 value: "MS".to_string(),
4417 description: "Absender".to_string(),
4418 ahb_status: "X".to_string(),
4419 }],
4420 parent_group_ahb_status: None,
4421 segment_ahb_status: None,
4422 ..Default::default()
4423 },
4424 AhbFieldRule {
4425 segment_path: "SG2/NAD/3035".to_string(),
4426 name: "Empfaenger".to_string(),
4427 ahb_status: "Muss".to_string(),
4428 codes: vec![AhbCodeRule {
4429 value: "MR".to_string(),
4430 description: "Empfaenger".to_string(),
4431 ahb_status: "X".to_string(),
4432 }],
4433 parent_group_ahb_status: None,
4434 segment_ahb_status: None,
4435 ..Default::default()
4436 },
4437 ],
4438 ub_definitions: BTreeMap::new(),
4439 package_conditions: Default::default(),
4440 };
4441
4442 let report =
4443 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
4444
4445 let ahb_errors: Vec<_> = report
4446 .by_category(ValidationCategory::Ahb)
4447 .filter(|i| i.severity == Severity::Error)
4448 .collect();
4449 assert!(
4450 ahb_errors.is_empty(),
4451 "Expected no AHB001 errors, got: {:?}",
4452 ahb_errors
4453 );
4454 }
4455
4456 #[test]
4457 fn test_extract_group_path_key() {
4458 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
4459 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
4460 assert_eq!(extract_group_path_key("NAD/3035"), "");
4461 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
4462 }
4463
4464 fn validate_field_of_absent_group(
4466 group_status: &str,
4467 conditions: Vec<(u32, CR)>,
4468 ) -> ValidationReport {
4469 use mig_types::navigator::GroupNavigator;
4470
4471 struct NoSg9;
4472 impl GroupNavigator for NoSg9 {
4473 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4474 vec![]
4475 }
4476 fn find_segments_with_qualifier_in_group(
4477 &self,
4478 _: &str,
4479 _: usize,
4480 _: &str,
4481 _: &[&str],
4482 _: usize,
4483 ) -> Vec<OwnedSegment> {
4484 vec![]
4485 }
4486 fn group_instance_count(&self, _: &[&str]) -> usize {
4487 0
4488 }
4489 }
4490
4491 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
4492 let workflow = AhbWorkflow {
4493 pruefidentifikator: "44001".to_string(),
4494 description: "Test".to_string(),
4495 communication_direction: None,
4496 fields: vec![AhbFieldRule {
4497 segment_path: "SG9/QTY/C186/6060".to_string(),
4498 name: "Menge".to_string(),
4499 ahb_status: "X".to_string(),
4500 parent_group_ahb_status: Some(group_status.to_string()),
4501 ..Default::default()
4502 }],
4503 ub_definitions: BTreeMap::new(),
4504 package_conditions: Default::default(),
4505 };
4506 validator.validate_with_navigator(
4507 &[],
4508 &workflow,
4509 &NoOpExternalProvider,
4510 ValidationLevel::Conditions,
4511 &NoSg9,
4512 )
4513 }
4514
4515 #[test]
4516 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
4517 let report = validate_field_of_absent_group(
4519 "Soll [47]\r\nMuss [46]",
4520 vec![(47, CR::True), (46, CR::False)],
4521 );
4522 assert!(
4523 report.is_valid(),
4524 "{:?}",
4525 report.errors().collect::<Vec<_>>()
4526 );
4527 }
4528
4529 #[test]
4530 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
4531 let report = validate_field_of_absent_group(
4532 "Soll [47]\r\nMuss [46]",
4533 vec![(47, CR::False), (46, CR::True)],
4534 );
4535 let errors: Vec<_> = report.errors().collect();
4536 assert_eq!(errors.len(), 1, "{errors:?}");
4537 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
4538 }
4539
4540 #[test]
4541 fn test_absent_optional_group_no_missing_field_error() {
4542 use mig_types::navigator::GroupNavigator;
4545
4546 struct NavWithoutSG3;
4547 impl GroupNavigator for NavWithoutSG3 {
4548 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4549 vec![]
4550 }
4551 fn find_segments_with_qualifier_in_group(
4552 &self,
4553 _: &str,
4554 _: usize,
4555 _: &str,
4556 _: &[&str],
4557 _: usize,
4558 ) -> Vec<OwnedSegment> {
4559 vec![]
4560 }
4561 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4562 match group_path {
4563 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
4566 }
4567 }
4568 }
4569
4570 let evaluator = MockEvaluator::new(vec![]);
4571 let validator = EdifactValidator::new(evaluator);
4572 let external = NoOpExternalProvider;
4573 let nav = NavWithoutSG3;
4574
4575 let segments = vec![
4577 OwnedSegment {
4578 id: "NAD".into(),
4579 elements: vec![vec!["MS".into()]],
4580 segment_number: 3,
4581 },
4582 OwnedSegment {
4583 id: "NAD".into(),
4584 elements: vec![vec!["MR".into()]],
4585 segment_number: 4,
4586 },
4587 ];
4588
4589 let workflow = AhbWorkflow {
4590 pruefidentifikator: "55001".to_string(),
4591 description: "Test".to_string(),
4592 communication_direction: None,
4593 fields: vec![
4594 AhbFieldRule {
4595 segment_path: "SG2/SG3/CTA/3139".to_string(),
4596 name: "Funktion des Ansprechpartners, Code".to_string(),
4597 ahb_status: "Muss".to_string(),
4598 codes: vec![],
4599 parent_group_ahb_status: None,
4600 segment_ahb_status: None,
4601 ..Default::default()
4602 },
4603 AhbFieldRule {
4604 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
4605 name: "Name vom Ansprechpartner".to_string(),
4606 ahb_status: "X".to_string(),
4607 codes: vec![],
4608 parent_group_ahb_status: None,
4609 segment_ahb_status: None,
4610 ..Default::default()
4611 },
4612 ],
4613 ub_definitions: BTreeMap::new(),
4614 package_conditions: Default::default(),
4615 };
4616
4617 let report = validator.validate_with_navigator(
4618 &segments,
4619 &workflow,
4620 &external,
4621 ValidationLevel::Conditions,
4622 &nav,
4623 );
4624
4625 let ahb_errors: Vec<_> = report
4626 .by_category(ValidationCategory::Ahb)
4627 .filter(|i| i.severity == Severity::Error)
4628 .collect();
4629 assert!(
4630 ahb_errors.is_empty(),
4631 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
4632 ahb_errors
4633 );
4634 }
4635
4636 #[test]
4637 fn test_present_group_still_checks_mandatory_fields() {
4638 use mig_types::navigator::GroupNavigator;
4640
4641 struct NavWithSG3;
4642 impl GroupNavigator for NavWithSG3 {
4643 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4644 vec![]
4645 }
4646 fn find_segments_with_qualifier_in_group(
4647 &self,
4648 _: &str,
4649 _: usize,
4650 _: &str,
4651 _: &[&str],
4652 _: usize,
4653 ) -> Vec<OwnedSegment> {
4654 vec![]
4655 }
4656 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4657 match group_path {
4658 ["SG2"] => 1,
4659 ["SG2", "SG3"] => 1, _ => 0,
4661 }
4662 }
4663 }
4664
4665 let evaluator = MockEvaluator::new(vec![]);
4666 let validator = EdifactValidator::new(evaluator);
4667 let external = NoOpExternalProvider;
4668 let nav = NavWithSG3;
4669
4670 let segments = vec![OwnedSegment {
4672 id: "NAD".into(),
4673 elements: vec![vec!["MS".into()]],
4674 segment_number: 3,
4675 }];
4676
4677 let workflow = AhbWorkflow {
4678 pruefidentifikator: "55001".to_string(),
4679 description: "Test".to_string(),
4680 communication_direction: None,
4681 fields: vec![AhbFieldRule {
4682 segment_path: "SG2/SG3/CTA/3139".to_string(),
4683 name: "Funktion des Ansprechpartners, Code".to_string(),
4684 ahb_status: "Muss".to_string(),
4685 codes: vec![],
4686 parent_group_ahb_status: None,
4687 segment_ahb_status: None,
4688 ..Default::default()
4689 }],
4690 ub_definitions: BTreeMap::new(),
4691 package_conditions: Default::default(),
4692 };
4693
4694 let report = validator.validate_with_navigator(
4695 &segments,
4696 &workflow,
4697 &external,
4698 ValidationLevel::Conditions,
4699 &nav,
4700 );
4701
4702 let ahb_errors: Vec<_> = report
4703 .by_category(ValidationCategory::Ahb)
4704 .filter(|i| i.severity == Severity::Error)
4705 .collect();
4706 assert_eq!(
4707 ahb_errors.len(),
4708 1,
4709 "Expected AHB001 error when SG3 is present but CTA missing"
4710 );
4711 assert!(narrate(ahb_errors[0]).contains("CTA"));
4712 }
4713
4714 #[test]
4715 fn test_missing_qualifier_with_navigator_is_detected() {
4716 use mig_types::navigator::GroupNavigator;
4719
4720 struct NavWithSG2;
4721 impl GroupNavigator for NavWithSG2 {
4722 fn find_segments_in_group(
4723 &self,
4724 segment_id: &str,
4725 group_path: &[&str],
4726 instance_index: usize,
4727 ) -> Vec<OwnedSegment> {
4728 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
4729 vec![OwnedSegment {
4730 id: "NAD".into(),
4731 elements: vec![vec!["MS".into()]],
4732 segment_number: 3,
4733 }]
4734 } else {
4735 vec![]
4736 }
4737 }
4738 fn find_segments_with_qualifier_in_group(
4739 &self,
4740 _: &str,
4741 _: usize,
4742 _: &str,
4743 _: &[&str],
4744 _: usize,
4745 ) -> Vec<OwnedSegment> {
4746 vec![]
4747 }
4748 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4749 match group_path {
4750 ["SG2"] => 1,
4751 _ => 0,
4752 }
4753 }
4754 }
4755
4756 let evaluator = MockEvaluator::new(vec![]);
4757 let validator = EdifactValidator::new(evaluator);
4758 let external = NoOpExternalProvider;
4759 let nav = NavWithSG2;
4760
4761 let segments = vec![OwnedSegment {
4762 id: "NAD".into(),
4763 elements: vec![vec!["MS".into()]],
4764 segment_number: 3,
4765 }];
4766
4767 let workflow = AhbWorkflow {
4768 pruefidentifikator: "55001".to_string(),
4769 description: "Test".to_string(),
4770 communication_direction: None,
4771 fields: vec![
4772 AhbFieldRule {
4773 segment_path: "SG2/NAD/3035".to_string(),
4774 name: "Absender".to_string(),
4775 ahb_status: "X".to_string(),
4776 codes: vec![AhbCodeRule {
4777 value: "MS".to_string(),
4778 description: "Absender".to_string(),
4779 ahb_status: "X".to_string(),
4780 }],
4781 parent_group_ahb_status: None,
4782 segment_ahb_status: None,
4783 ..Default::default()
4784 },
4785 AhbFieldRule {
4786 segment_path: "SG2/NAD/3035".to_string(),
4787 name: "Empfaenger".to_string(),
4788 ahb_status: "Muss".to_string(),
4789 codes: vec![AhbCodeRule {
4790 value: "MR".to_string(),
4791 description: "Empfaenger".to_string(),
4792 ahb_status: "X".to_string(),
4793 }],
4794 parent_group_ahb_status: None,
4795 segment_ahb_status: None,
4796 ..Default::default()
4797 },
4798 ],
4799 ub_definitions: BTreeMap::new(),
4800 package_conditions: Default::default(),
4801 };
4802
4803 let report = validator.validate_with_navigator(
4804 &segments,
4805 &workflow,
4806 &external,
4807 ValidationLevel::Conditions,
4808 &nav,
4809 );
4810
4811 let ahb_errors: Vec<_> = report
4812 .by_category(ValidationCategory::Ahb)
4813 .filter(|i| i.severity == Severity::Error)
4814 .collect();
4815 assert_eq!(
4816 ahb_errors.len(),
4817 1,
4818 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
4819 ahb_errors
4820 );
4821 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4822 }
4823
4824 #[test]
4825 fn test_optional_group_variant_absent_no_error() {
4826 use mig_types::navigator::GroupNavigator;
4831
4832 struct TestNav;
4833 impl GroupNavigator for TestNav {
4834 fn find_segments_in_group(
4835 &self,
4836 segment_id: &str,
4837 group_path: &[&str],
4838 instance_index: usize,
4839 ) -> Vec<OwnedSegment> {
4840 match (segment_id, group_path, instance_index) {
4841 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
4842 id: "LOC".into(),
4843 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4844 segment_number: 10,
4845 }],
4846 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
4847 id: "NAD".into(),
4848 elements: vec![vec!["MS".into()]],
4849 segment_number: 3,
4850 }],
4851 _ => vec![],
4852 }
4853 }
4854 fn find_segments_with_qualifier_in_group(
4855 &self,
4856 _: &str,
4857 _: usize,
4858 _: &str,
4859 _: &[&str],
4860 _: usize,
4861 ) -> Vec<OwnedSegment> {
4862 vec![]
4863 }
4864 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4865 match group_path {
4866 ["SG2"] => 1,
4867 ["SG4"] => 1,
4868 ["SG4", "SG5"] => 1, _ => 0,
4870 }
4871 }
4872 }
4873
4874 let evaluator = MockEvaluator::new(vec![]);
4875 let validator = EdifactValidator::new(evaluator);
4876 let external = NoOpExternalProvider;
4877 let nav = TestNav;
4878
4879 let segments = vec![
4880 OwnedSegment {
4881 id: "NAD".into(),
4882 elements: vec![vec!["MS".into()]],
4883 segment_number: 3,
4884 },
4885 OwnedSegment {
4886 id: "LOC".into(),
4887 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4888 segment_number: 10,
4889 },
4890 ];
4891
4892 let workflow = AhbWorkflow {
4893 pruefidentifikator: "55001".to_string(),
4894 description: "Test".to_string(),
4895 communication_direction: None,
4896 fields: vec![
4897 AhbFieldRule {
4899 segment_path: "SG2/NAD/3035".to_string(),
4900 name: "Absender".to_string(),
4901 ahb_status: "X".to_string(),
4902 codes: vec![AhbCodeRule {
4903 value: "MS".to_string(),
4904 description: "Absender".to_string(),
4905 ahb_status: "X".to_string(),
4906 }],
4907 parent_group_ahb_status: Some("Muss".to_string()),
4908 segment_ahb_status: None,
4909 ..Default::default()
4910 },
4911 AhbFieldRule {
4912 segment_path: "SG2/NAD/3035".to_string(),
4913 name: "Empfaenger".to_string(),
4914 ahb_status: "Muss".to_string(),
4915 codes: vec![AhbCodeRule {
4916 value: "MR".to_string(),
4917 description: "Empfaenger".to_string(),
4918 ahb_status: "X".to_string(),
4919 }],
4920 parent_group_ahb_status: Some("Muss".to_string()),
4921 segment_ahb_status: None,
4922 ..Default::default()
4923 },
4924 AhbFieldRule {
4926 segment_path: "SG4/SG5/LOC/3227".to_string(),
4927 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4928 ahb_status: "X".to_string(),
4929 codes: vec![AhbCodeRule {
4930 value: "Z16".to_string(),
4931 description: "Marktlokation".to_string(),
4932 ahb_status: "X".to_string(),
4933 }],
4934 parent_group_ahb_status: Some("Kann".to_string()),
4935 segment_ahb_status: None,
4936 ..Default::default()
4937 },
4938 AhbFieldRule {
4939 segment_path: "SG4/SG5/LOC/3227".to_string(),
4940 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4941 ahb_status: "Muss".to_string(),
4942 codes: vec![AhbCodeRule {
4943 value: "Z17".to_string(),
4944 description: "Messlokation".to_string(),
4945 ahb_status: "X".to_string(),
4946 }],
4947 parent_group_ahb_status: Some("Kann".to_string()),
4948 segment_ahb_status: None,
4949 ..Default::default()
4950 },
4951 ],
4952 ub_definitions: BTreeMap::new(),
4953 package_conditions: Default::default(),
4954 };
4955
4956 let report = validator.validate_with_navigator(
4957 &segments,
4958 &workflow,
4959 &external,
4960 ValidationLevel::Conditions,
4961 &nav,
4962 );
4963
4964 let ahb_errors: Vec<_> = report
4965 .by_category(ValidationCategory::Ahb)
4966 .filter(|i| i.severity == Severity::Error)
4967 .collect();
4968
4969 assert_eq!(
4972 ahb_errors.len(),
4973 1,
4974 "Expected only AHB001 for missing NAD+MR, got: {:?}",
4975 ahb_errors
4976 );
4977 assert!(
4978 narrate(ahb_errors[0]).contains("Empfaenger"),
4979 "Error should be for missing NAD+MR (Empfaenger)"
4980 );
4981 }
4982
4983 #[test]
4987 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4988 use mig_types::navigator::GroupNavigator;
4989
4990 struct TestNav;
4991 impl GroupNavigator for TestNav {
4992 fn find_segments_in_group(
4993 &self,
4994 segment_id: &str,
4995 group_path: &[&str],
4996 instance_index: usize,
4997 ) -> Vec<OwnedSegment> {
4998 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4999 vec![OwnedSegment {
5000 id: "SEQ".into(),
5001 elements: vec![vec!["Z03".into()]],
5002 segment_number: 10,
5003 }]
5004 } else {
5005 vec![]
5006 }
5007 }
5008 fn find_segments_with_qualifier_in_group(
5009 &self,
5010 _: &str,
5011 _: usize,
5012 _: &str,
5013 _: &[&str],
5014 _: usize,
5015 ) -> Vec<OwnedSegment> {
5016 vec![]
5017 }
5018 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5019 match group_path {
5020 ["SG4"] => 1,
5021 ["SG4", "SG8"] => 1,
5022 _ => 0,
5023 }
5024 }
5025 }
5026
5027 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
5028 let validator = EdifactValidator::new(evaluator);
5029 let segments = vec![OwnedSegment {
5030 id: "SEQ".into(),
5031 elements: vec![vec!["Z03".into()]],
5032 segment_number: 10,
5033 }];
5034 let seq = |code: &str, group: &str| AhbFieldRule {
5035 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5036 name: format!("Handlung, Code ({code})"),
5037 ahb_status: "X".to_string(),
5038 codes: vec![AhbCodeRule {
5039 value: code.to_string(),
5040 description: String::new(),
5041 ahb_status: "X".to_string(),
5042 }],
5043 parent_group_ahb_status: Some(group.to_string()),
5044 segment_ahb_status: None,
5045 ..Default::default()
5046 };
5047 let workflow = AhbWorkflow {
5048 pruefidentifikator: "44143".to_string(),
5049 description: "Test".to_string(),
5050 communication_direction: None,
5051 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
5052 ub_definitions: BTreeMap::new(),
5053 package_conditions: Default::default(),
5054 };
5055
5056 let report = validator.validate_with_navigator(
5057 &segments,
5058 &workflow,
5059 &NoOpExternalProvider,
5060 ValidationLevel::Conditions,
5061 &TestNav,
5062 );
5063 let missing: Vec<_> = report
5064 .issues
5065 .iter()
5066 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
5067 .collect();
5068 assert!(missing.is_empty(), "{missing:#?}");
5069 }
5070
5071 #[test]
5072 fn test_conditional_group_variant_absent_no_error() {
5073 use mig_types::navigator::GroupNavigator;
5078
5079 struct TestNav;
5080 impl GroupNavigator for TestNav {
5081 fn find_segments_in_group(
5082 &self,
5083 segment_id: &str,
5084 group_path: &[&str],
5085 instance_index: usize,
5086 ) -> Vec<OwnedSegment> {
5087 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
5088 vec![OwnedSegment {
5089 id: "LOC".into(),
5090 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
5091 segment_number: 10,
5092 }]
5093 } else {
5094 vec![]
5095 }
5096 }
5097 fn find_segments_with_qualifier_in_group(
5098 &self,
5099 _: &str,
5100 _: usize,
5101 _: &str,
5102 _: &[&str],
5103 _: usize,
5104 ) -> Vec<OwnedSegment> {
5105 vec![]
5106 }
5107 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5108 match group_path {
5109 ["SG4"] => 1,
5110 ["SG4", "SG5"] => 1, _ => 0,
5112 }
5113 }
5114 }
5115
5116 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
5119 let validator = EdifactValidator::new(evaluator);
5120 let external = NoOpExternalProvider;
5121 let nav = TestNav;
5122
5123 let segments = vec![OwnedSegment {
5124 id: "LOC".into(),
5125 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
5126 segment_number: 10,
5127 }];
5128
5129 let workflow = AhbWorkflow {
5130 pruefidentifikator: "55001".to_string(),
5131 description: "Test".to_string(),
5132 communication_direction: None,
5133 fields: vec![
5134 AhbFieldRule {
5136 segment_path: "SG4/SG5/LOC/3227".to_string(),
5137 name: "Ortsangabe, Qualifier (Z16)".to_string(),
5138 ahb_status: "X".to_string(),
5139 codes: vec![AhbCodeRule {
5140 value: "Z16".to_string(),
5141 description: "Marktlokation".to_string(),
5142 ahb_status: "X".to_string(),
5143 }],
5144 parent_group_ahb_status: Some("Muss [2061]".to_string()),
5145 segment_ahb_status: None,
5146 ..Default::default()
5147 },
5148 AhbFieldRule {
5150 segment_path: "SG4/SG5/LOC/3227".to_string(),
5151 name: "Ortsangabe, Qualifier (Z17)".to_string(),
5152 ahb_status: "X".to_string(),
5153 codes: vec![AhbCodeRule {
5154 value: "Z17".to_string(),
5155 description: "Messlokation".to_string(),
5156 ahb_status: "X".to_string(),
5157 }],
5158 parent_group_ahb_status: Some("Soll [165]".to_string()),
5159 segment_ahb_status: None,
5160 ..Default::default()
5161 },
5162 ],
5163 ub_definitions: BTreeMap::new(),
5164 package_conditions: Default::default(),
5165 };
5166
5167 let report = validator.validate_with_navigator(
5168 &segments,
5169 &workflow,
5170 &external,
5171 ValidationLevel::Conditions,
5172 &nav,
5173 );
5174
5175 let ahb_errors: Vec<_> = report
5176 .by_category(ValidationCategory::Ahb)
5177 .filter(|i| i.severity == Severity::Error)
5178 .collect();
5179
5180 assert!(
5182 ahb_errors.is_empty(),
5183 "Expected no errors when conditional group variant [165]=False, got: {:?}",
5184 ahb_errors
5185 );
5186 }
5187
5188 #[test]
5189 fn test_conditional_group_variant_unknown_no_error() {
5190 let evaluator = MockEvaluator::new(vec![]);
5196 let validator = EdifactValidator::new(evaluator);
5197 let external = NoOpExternalProvider;
5198
5199 let workflow = AhbWorkflow {
5200 pruefidentifikator: "55001".to_string(),
5201 description: "Test".to_string(),
5202 communication_direction: None,
5203 fields: vec![AhbFieldRule {
5204 segment_path: "SG4/SG5/LOC/3227".to_string(),
5205 name: "Ortsangabe, Qualifier (Z17)".to_string(),
5206 ahb_status: "X".to_string(),
5207 codes: vec![AhbCodeRule {
5208 value: "Z17".to_string(),
5209 description: "Messlokation".to_string(),
5210 ahb_status: "X".to_string(),
5211 }],
5212 parent_group_ahb_status: Some("Soll [165]".to_string()),
5213 segment_ahb_status: None,
5214 ..Default::default()
5215 }],
5216 ub_definitions: BTreeMap::new(),
5217 package_conditions: Default::default(),
5218 };
5219
5220 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
5221
5222 let ahb_errors: Vec<_> = report
5223 .by_category(ValidationCategory::Ahb)
5224 .filter(|i| i.severity == Severity::Error)
5225 .collect();
5226
5227 assert!(
5229 ahb_errors.is_empty(),
5230 "Expected no errors when parent group condition is Unknown, got: {:?}",
5231 ahb_errors
5232 );
5233 }
5234
5235 #[test]
5236 fn test_segment_absent_within_present_group_no_error() {
5237 use mig_types::navigator::GroupNavigator;
5241
5242 struct TestNav;
5243 impl GroupNavigator for TestNav {
5244 fn find_segments_in_group(
5245 &self,
5246 segment_id: &str,
5247 group_path: &[&str],
5248 instance_index: usize,
5249 ) -> Vec<OwnedSegment> {
5250 if segment_id == "QTY"
5252 && group_path == ["SG5", "SG6", "SG9", "SG10"]
5253 && instance_index == 0
5254 {
5255 vec![OwnedSegment {
5256 id: "QTY".into(),
5257 elements: vec![vec!["220".into(), "0".into()]],
5258 segment_number: 14,
5259 }]
5260 } else {
5261 vec![]
5262 }
5263 }
5264 fn find_segments_with_qualifier_in_group(
5265 &self,
5266 _: &str,
5267 _: usize,
5268 _: &str,
5269 _: &[&str],
5270 _: usize,
5271 ) -> Vec<OwnedSegment> {
5272 vec![]
5273 }
5274 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5275 match group_path {
5276 ["SG5"] => 1,
5277 ["SG5", "SG6"] => 1,
5278 ["SG5", "SG6", "SG9"] => 1,
5279 ["SG5", "SG6", "SG9", "SG10"] => 1,
5280 _ => 0,
5281 }
5282 }
5283 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
5284 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
5286 }
5287 }
5288
5289 let evaluator = MockEvaluator::all_true(&[]);
5290 let validator = EdifactValidator::new(evaluator);
5291 let external = NoOpExternalProvider;
5292 let nav = TestNav;
5293
5294 let segments = vec![OwnedSegment {
5295 id: "QTY".into(),
5296 elements: vec![vec!["220".into(), "0".into()]],
5297 segment_number: 14,
5298 }];
5299
5300 let workflow = AhbWorkflow {
5301 pruefidentifikator: "13017".to_string(),
5302 description: "Test".to_string(),
5303 communication_direction: None,
5304 fields: vec![
5305 AhbFieldRule {
5307 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
5308 name: "Statuskategorie, Code".to_string(),
5309 ahb_status: "X".to_string(),
5310 codes: vec![],
5311 parent_group_ahb_status: Some("Muss".to_string()),
5312 segment_ahb_status: None,
5313 ..Default::default()
5314 },
5315 AhbFieldRule {
5317 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5318 name: "Statusanlaß, Code".to_string(),
5319 ahb_status: "X [5]".to_string(),
5320 codes: vec![],
5321 parent_group_ahb_status: Some("Muss".to_string()),
5322 segment_ahb_status: None,
5323 ..Default::default()
5324 },
5325 ],
5326 ub_definitions: BTreeMap::new(),
5327 package_conditions: Default::default(),
5328 };
5329
5330 let report = validator.validate_with_navigator(
5331 &segments,
5332 &workflow,
5333 &external,
5334 ValidationLevel::Conditions,
5335 &nav,
5336 );
5337
5338 let ahb_errors: Vec<_> = report
5339 .by_category(ValidationCategory::Ahb)
5340 .filter(|i| i.severity == Severity::Error)
5341 .collect();
5342
5343 assert!(
5344 ahb_errors.is_empty(),
5345 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
5346 ahb_errors
5347 );
5348 }
5349
5350 #[test]
5351 fn test_group_scoped_code_validation_with_navigator() {
5352 use mig_types::navigator::GroupNavigator;
5356
5357 struct TestNav;
5358 impl GroupNavigator for TestNav {
5359 fn find_segments_in_group(
5360 &self,
5361 segment_id: &str,
5362 group_path: &[&str],
5363 _instance_index: usize,
5364 ) -> Vec<OwnedSegment> {
5365 if segment_id != "NAD" {
5366 return vec![];
5367 }
5368 match group_path {
5369 ["SG2"] => vec![
5370 OwnedSegment {
5371 id: "NAD".into(),
5372 elements: vec![vec!["MS".into()]],
5373 segment_number: 3,
5374 },
5375 OwnedSegment {
5376 id: "NAD".into(),
5377 elements: vec![vec!["MT".into()]], segment_number: 4,
5379 },
5380 ],
5381 ["SG4", "SG12"] => vec![
5382 OwnedSegment {
5383 id: "NAD".into(),
5384 elements: vec![vec!["Z04".into()]],
5385 segment_number: 20,
5386 },
5387 OwnedSegment {
5388 id: "NAD".into(),
5389 elements: vec![vec!["Z09".into()]],
5390 segment_number: 21,
5391 },
5392 ],
5393 _ => vec![],
5394 }
5395 }
5396 fn find_segments_with_qualifier_in_group(
5397 &self,
5398 _: &str,
5399 _: usize,
5400 _: &str,
5401 _: &[&str],
5402 _: usize,
5403 ) -> Vec<OwnedSegment> {
5404 vec![]
5405 }
5406 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5407 match group_path {
5408 ["SG2"] | ["SG4", "SG12"] => 1,
5409 _ => 0,
5410 }
5411 }
5412 }
5413
5414 let evaluator = MockEvaluator::new(vec![]);
5415 let validator = EdifactValidator::new(evaluator);
5416 let external = NoOpExternalProvider;
5417 let nav = TestNav;
5418
5419 let workflow = AhbWorkflow {
5420 pruefidentifikator: "55001".to_string(),
5421 description: "Test".to_string(),
5422 communication_direction: None,
5423 fields: vec![
5424 AhbFieldRule {
5425 segment_path: "SG2/NAD/3035".to_string(),
5426 name: "Absender".to_string(),
5427 ahb_status: "X".to_string(),
5428 codes: vec![AhbCodeRule {
5429 value: "MS".to_string(),
5430 description: "Absender".to_string(),
5431 ahb_status: "X".to_string(),
5432 }],
5433 parent_group_ahb_status: None,
5434 segment_ahb_status: None,
5435 ..Default::default()
5436 },
5437 AhbFieldRule {
5438 segment_path: "SG2/NAD/3035".to_string(),
5439 name: "Empfaenger".to_string(),
5440 ahb_status: "X".to_string(),
5441 codes: vec![AhbCodeRule {
5442 value: "MR".to_string(),
5443 description: "Empfaenger".to_string(),
5444 ahb_status: "X".to_string(),
5445 }],
5446 parent_group_ahb_status: None,
5447 segment_ahb_status: None,
5448 ..Default::default()
5449 },
5450 AhbFieldRule {
5451 segment_path: "SG4/SG12/NAD/3035".to_string(),
5452 name: "Anschlussnutzer".to_string(),
5453 ahb_status: "X".to_string(),
5454 codes: vec![AhbCodeRule {
5455 value: "Z04".to_string(),
5456 description: "Anschlussnutzer".to_string(),
5457 ahb_status: "X".to_string(),
5458 }],
5459 parent_group_ahb_status: None,
5460 segment_ahb_status: None,
5461 ..Default::default()
5462 },
5463 AhbFieldRule {
5464 segment_path: "SG4/SG12/NAD/3035".to_string(),
5465 name: "Korrespondenzanschrift".to_string(),
5466 ahb_status: "X".to_string(),
5467 codes: vec![AhbCodeRule {
5468 value: "Z09".to_string(),
5469 description: "Korrespondenzanschrift".to_string(),
5470 ahb_status: "X".to_string(),
5471 }],
5472 parent_group_ahb_status: None,
5473 segment_ahb_status: None,
5474 ..Default::default()
5475 },
5476 ],
5477 ub_definitions: BTreeMap::new(),
5478 package_conditions: Default::default(),
5479 };
5480
5481 let all_segments = vec![
5483 OwnedSegment {
5484 id: "NAD".into(),
5485 elements: vec![vec!["MS".into()]],
5486 segment_number: 3,
5487 },
5488 OwnedSegment {
5489 id: "NAD".into(),
5490 elements: vec![vec!["MT".into()]],
5491 segment_number: 4,
5492 },
5493 OwnedSegment {
5494 id: "NAD".into(),
5495 elements: vec![vec!["Z04".into()]],
5496 segment_number: 20,
5497 },
5498 OwnedSegment {
5499 id: "NAD".into(),
5500 elements: vec![vec!["Z09".into()]],
5501 segment_number: 21,
5502 },
5503 ];
5504
5505 let report = validator.validate_with_navigator(
5506 &all_segments,
5507 &workflow,
5508 &external,
5509 ValidationLevel::Conditions,
5510 &nav,
5511 );
5512
5513 let code_errors: Vec<_> = report
5514 .by_category(ValidationCategory::Code)
5515 .filter(|i| i.severity == Severity::Error)
5516 .collect();
5517
5518 assert_eq!(
5521 code_errors.len(),
5522 1,
5523 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
5524 code_errors
5525 );
5526 assert!(narrate(code_errors[0]).contains("MT"));
5527 assert!(narrate(code_errors[0]).contains("MR"));
5529 assert!(narrate(code_errors[0]).contains("MS"));
5530 assert!(
5531 !narrate(code_errors[0]).contains("Z04"),
5532 "SG4/SG12 codes should not leak into SG2 error"
5533 );
5534 assert!(
5536 code_errors[0]
5537 .field_path
5538 .as_deref()
5539 .unwrap_or("")
5540 .contains("SG2"),
5541 "Error field_path should reference SG2, got: {:?}",
5542 code_errors[0].field_path
5543 );
5544 }
5545
5546 #[test]
5549 fn test_package_cardinality_within_bounds() {
5550 let evaluator = MockEvaluator::all_true(&[]);
5552 let validator = EdifactValidator::new(evaluator);
5553 let external = NoOpExternalProvider;
5554
5555 let segments = vec![OwnedSegment {
5556 id: "STS".into(),
5557 elements: vec![
5558 vec!["Z33".into()], vec![], vec!["E01".into()], ],
5562 segment_number: 5,
5563 }];
5564
5565 let workflow = AhbWorkflow {
5566 pruefidentifikator: "13017".to_string(),
5567 description: "Test".to_string(),
5568 communication_direction: None,
5569 ub_definitions: BTreeMap::new(),
5570 package_conditions: Default::default(),
5571 fields: vec![AhbFieldRule {
5572 group_ahb_status: None,
5573 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5574 name: "Statusanlaß, Code".to_string(),
5575 ahb_status: "X".to_string(),
5576 element_index: Some(2),
5577 component_index: Some(0),
5578 codes: vec![
5579 AhbCodeRule {
5580 value: "E01".into(),
5581 description: "Code 1".into(),
5582 ahb_status: "X [4P0..1]".into(),
5583 },
5584 AhbCodeRule {
5585 value: "E02".into(),
5586 description: "Code 2".into(),
5587 ahb_status: "X [4P0..1]".into(),
5588 },
5589 ],
5590 parent_group_ahb_status: Some("Muss".to_string()),
5591 segment_ahb_status: None,
5592 mig_number: None,
5593 format: None,
5594 }],
5595 };
5596
5597 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5598 let pkg_errors: Vec<_> = report
5599 .by_category(ValidationCategory::Ahb)
5600 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5601 .collect();
5602 assert!(
5603 pkg_errors.is_empty(),
5604 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
5605 pkg_errors
5606 );
5607 }
5608
5609 #[test]
5610 fn test_package_cardinality_zero_present_min_zero() {
5611 let evaluator = MockEvaluator::all_true(&[]);
5613 let validator = EdifactValidator::new(evaluator);
5614 let external = NoOpExternalProvider;
5615
5616 let segments = vec![OwnedSegment {
5617 id: "STS".into(),
5618 elements: vec![
5619 vec!["Z33".into()],
5620 vec![],
5621 vec!["X99".into()], ],
5623 segment_number: 5,
5624 }];
5625
5626 let workflow = AhbWorkflow {
5627 pruefidentifikator: "13017".to_string(),
5628 description: "Test".to_string(),
5629 communication_direction: None,
5630 ub_definitions: BTreeMap::new(),
5631 package_conditions: Default::default(),
5632 fields: vec![AhbFieldRule {
5633 group_ahb_status: None,
5634 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5635 name: "Statusanlaß, Code".to_string(),
5636 ahb_status: "X".to_string(),
5637 element_index: Some(2),
5638 component_index: Some(0),
5639 codes: vec![
5640 AhbCodeRule {
5641 value: "E01".into(),
5642 description: "Code 1".into(),
5643 ahb_status: "X [4P0..1]".into(),
5644 },
5645 AhbCodeRule {
5646 value: "E02".into(),
5647 description: "Code 2".into(),
5648 ahb_status: "X [4P0..1]".into(),
5649 },
5650 ],
5651 parent_group_ahb_status: Some("Muss".to_string()),
5652 segment_ahb_status: None,
5653 mig_number: None,
5654 format: None,
5655 }],
5656 };
5657
5658 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5659 let pkg_errors: Vec<_> = report
5660 .by_category(ValidationCategory::Ahb)
5661 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5662 .collect();
5663 assert!(
5664 pkg_errors.is_empty(),
5665 "0 codes, min=0 — no error expected, got: {:?}",
5666 pkg_errors
5667 );
5668 }
5669
5670 #[test]
5671 fn test_package_cardinality_too_many() {
5672 let evaluator = MockEvaluator::all_true(&[]);
5674 let validator = EdifactValidator::new(evaluator);
5675 let external = NoOpExternalProvider;
5676
5677 let segments = vec![
5679 OwnedSegment {
5680 id: "STS".into(),
5681 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
5682 segment_number: 5,
5683 },
5684 OwnedSegment {
5685 id: "STS".into(),
5686 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
5687 segment_number: 6,
5688 },
5689 ];
5690
5691 let workflow = AhbWorkflow {
5692 pruefidentifikator: "13017".to_string(),
5693 description: "Test".to_string(),
5694 communication_direction: None,
5695 ub_definitions: BTreeMap::new(),
5696 package_conditions: Default::default(),
5697 fields: vec![AhbFieldRule {
5698 group_ahb_status: None,
5699 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5700 name: "Statusanlaß, Code".to_string(),
5701 ahb_status: "X".to_string(),
5702 element_index: Some(2),
5703 component_index: Some(0),
5704 codes: vec![
5705 AhbCodeRule {
5706 value: "E01".into(),
5707 description: "Code 1".into(),
5708 ahb_status: "X [4P0..1]".into(),
5709 },
5710 AhbCodeRule {
5711 value: "E02".into(),
5712 description: "Code 2".into(),
5713 ahb_status: "X [4P0..1]".into(),
5714 },
5715 ],
5716 parent_group_ahb_status: Some("Muss".to_string()),
5717 segment_ahb_status: None,
5718 mig_number: None,
5719 format: None,
5720 }],
5721 };
5722
5723 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5724 let pkg_errors: Vec<_> = report
5725 .by_category(ValidationCategory::Ahb)
5726 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5727 .collect();
5728 assert_eq!(
5729 pkg_errors.len(),
5730 1,
5731 "2 codes present, max=1 — expected 1 error, got: {:?}",
5732 pkg_errors
5733 );
5734 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
5735 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
5736 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
5737 }
5738
5739 #[test]
5740 fn test_package_cardinality_too_few() {
5741 let evaluator = MockEvaluator::all_true(&[]);
5743 let validator = EdifactValidator::new(evaluator);
5744 let external = NoOpExternalProvider;
5745
5746 let segments = vec![OwnedSegment {
5747 id: "STS".into(),
5748 elements: vec![
5749 vec!["Z33".into()],
5750 vec![],
5751 vec!["X99".into()], ],
5753 segment_number: 5,
5754 }];
5755
5756 let workflow = AhbWorkflow {
5757 pruefidentifikator: "13017".to_string(),
5758 description: "Test".to_string(),
5759 communication_direction: None,
5760 ub_definitions: BTreeMap::new(),
5761 package_conditions: Default::default(),
5762 fields: vec![AhbFieldRule {
5763 group_ahb_status: None,
5764 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5765 name: "Statusanlaß, Code".to_string(),
5766 ahb_status: "X".to_string(),
5767 element_index: Some(2),
5768 component_index: Some(0),
5769 codes: vec![
5770 AhbCodeRule {
5771 value: "E01".into(),
5772 description: "Code 1".into(),
5773 ahb_status: "X [5P1..3]".into(),
5774 },
5775 AhbCodeRule {
5776 value: "E02".into(),
5777 description: "Code 2".into(),
5778 ahb_status: "X [5P1..3]".into(),
5779 },
5780 AhbCodeRule {
5781 value: "E03".into(),
5782 description: "Code 3".into(),
5783 ahb_status: "X [5P1..3]".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
5793 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5794 let pkg_errors: Vec<_> = report
5795 .by_category(ValidationCategory::Ahb)
5796 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5797 .collect();
5798 assert_eq!(
5799 pkg_errors.len(),
5800 1,
5801 "0 codes present, min=1 — expected 1 error, got: {:?}",
5802 pkg_errors
5803 );
5804 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
5805 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
5806 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
5807 }
5808
5809 #[test]
5810 fn test_package_cardinality_no_packages_in_workflow() {
5811 let evaluator = MockEvaluator::all_true(&[]);
5813 let validator = EdifactValidator::new(evaluator);
5814 let external = NoOpExternalProvider;
5815
5816 let segments = vec![OwnedSegment {
5817 id: "STS".into(),
5818 elements: vec![vec!["E01".into()]],
5819 segment_number: 5,
5820 }];
5821
5822 let workflow = AhbWorkflow {
5823 pruefidentifikator: "13017".to_string(),
5824 description: "Test".to_string(),
5825 communication_direction: None,
5826 ub_definitions: BTreeMap::new(),
5827 package_conditions: Default::default(),
5828 fields: vec![AhbFieldRule {
5829 segment_path: "STS/9015".to_string(),
5830 name: "Status Code".to_string(),
5831 ahb_status: "X".to_string(),
5832 codes: vec![AhbCodeRule {
5833 value: "E01".into(),
5834 description: "Code 1".into(),
5835 ahb_status: "X".into(),
5836 }],
5837 parent_group_ahb_status: Some("Muss".to_string()),
5838 segment_ahb_status: None,
5839 ..Default::default()
5840 }],
5841 };
5842
5843 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5844 let pkg_errors: Vec<_> = report
5845 .by_category(ValidationCategory::Ahb)
5846 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5847 .collect();
5848 assert!(
5849 pkg_errors.is_empty(),
5850 "No packages in workflow — no errors expected"
5851 );
5852 }
5853
5854 #[test]
5855 fn test_package_cardinality_with_condition_and_package() {
5856 let evaluator = MockEvaluator::all_true(&[901]);
5858 let validator = EdifactValidator::new(evaluator);
5859 let external = NoOpExternalProvider;
5860
5861 let segments = vec![OwnedSegment {
5862 id: "STS".into(),
5863 elements: vec![vec![], vec![], vec!["E01".into()]],
5864 segment_number: 5,
5865 }];
5866
5867 let workflow = AhbWorkflow {
5868 pruefidentifikator: "13017".to_string(),
5869 description: "Test".to_string(),
5870 communication_direction: None,
5871 ub_definitions: BTreeMap::new(),
5872 package_conditions: Default::default(),
5873 fields: vec![AhbFieldRule {
5874 group_ahb_status: None,
5875 segment_path: "SG10/STS/C556/9013".to_string(),
5876 name: "Code".to_string(),
5877 ahb_status: "X".to_string(),
5878 element_index: Some(2),
5879 component_index: Some(0),
5880 codes: vec![
5881 AhbCodeRule {
5882 value: "E01".into(),
5883 description: "Code 1".into(),
5884 ahb_status: "X [901] [4P0..1]".into(),
5885 },
5886 AhbCodeRule {
5887 value: "E02".into(),
5888 description: "Code 2".into(),
5889 ahb_status: "X [901] [4P0..1]".into(),
5890 },
5891 ],
5892 parent_group_ahb_status: Some("Muss".to_string()),
5893 segment_ahb_status: None,
5894 mig_number: None,
5895 format: None,
5896 }],
5897 };
5898
5899 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5900 let pkg_errors: Vec<_> = report
5901 .by_category(ValidationCategory::Ahb)
5902 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5903 .collect();
5904 assert!(
5905 pkg_errors.is_empty(),
5906 "1 code within [4P0..1] bounds — no error, got: {:?}",
5907 pkg_errors
5908 );
5909 }
5910
5911 #[test]
5912 fn test_package_binds_only_where_its_condition_holds() {
5913 let report_for = |package_condition: CR| {
5917 let validator =
5918 EdifactValidator::new(MockEvaluator::new(vec![(902, package_condition)]));
5919 let segments = vec![OwnedSegment {
5920 id: "STS".into(),
5921 elements: vec![vec![], vec![], vec!["E09".into()]],
5922 segment_number: 5,
5923 }];
5924 let workflow = AhbWorkflow {
5925 pruefidentifikator: "13017".to_string(),
5926 description: "Test".to_string(),
5927 communication_direction: None,
5928 ub_definitions: BTreeMap::new(),
5929 package_conditions: BTreeMap::from([(5, ConditionExpr::Ref(902))]),
5930 fields: vec![AhbFieldRule {
5931 group_ahb_status: None,
5932 segment_path: "SG10/STS/C556/9013".to_string(),
5933 name: "Code".to_string(),
5934 ahb_status: "X".to_string(),
5935 element_index: Some(2),
5936 component_index: Some(0),
5937 codes: vec![
5938 AhbCodeRule {
5939 value: "E01".into(),
5940 description: "Code 1".into(),
5941 ahb_status: "X [5P1..1]".into(),
5942 },
5943 AhbCodeRule {
5944 value: "E09".into(),
5945 description: "Code 9".into(),
5946 ahb_status: "X".into(),
5947 },
5948 ],
5949 parent_group_ahb_status: Some("Muss".to_string()),
5950 segment_ahb_status: None,
5951 mig_number: None,
5952 format: None,
5953 }],
5954 };
5955 validator
5956 .validate(
5957 &segments,
5958 &workflow,
5959 &NoOpExternalProvider,
5960 ValidationLevel::Full,
5961 )
5962 .by_category(ValidationCategory::Ahb)
5963 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5964 .count()
5965 };
5966 assert_eq!(report_for(CR::False), 0, "[902] false: [5P] does not bind");
5967 assert_eq!(report_for(CR::True), 1, "[902] true: [5P1..1] binds");
5968 }
5969
5970 #[test]
5971 fn test_package_cardinality_scoped_per_group_instance() {
5972 use mig_types::navigator::GroupNavigator;
5980
5981 struct TwoSg10s {
5982 sts_a: OwnedSegment,
5983 sts_b: OwnedSegment,
5984 }
5985 impl GroupNavigator for TwoSg10s {
5986 fn find_segments_in_group(
5987 &self,
5988 segment_id: &str,
5989 group_path: &[&str],
5990 instance_index: usize,
5991 ) -> Vec<OwnedSegment> {
5992 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
5993 match instance_index {
5994 0 => vec![self.sts_a.clone()],
5995 1 => vec![self.sts_b.clone()],
5996 _ => vec![],
5997 }
5998 } else {
5999 vec![]
6000 }
6001 }
6002 fn find_segments_with_qualifier_in_group(
6003 &self,
6004 _: &str,
6005 _: usize,
6006 _: &str,
6007 _: &[&str],
6008 _: usize,
6009 ) -> Vec<OwnedSegment> {
6010 vec![]
6011 }
6012 fn group_instance_count(&self, group_path: &[&str]) -> usize {
6013 match group_path {
6014 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
6015 ["SG5", "SG6", "SG9", "SG10"] => 2,
6016 _ => 0,
6017 }
6018 }
6019 }
6020
6021 let sts_a = OwnedSegment {
6022 id: "STS".into(),
6023 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
6024 segment_number: 10,
6025 };
6026 let sts_b = OwnedSegment {
6027 id: "STS".into(),
6028 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
6029 segment_number: 15,
6030 };
6031 let nav = TwoSg10s {
6032 sts_a: sts_a.clone(),
6033 sts_b: sts_b.clone(),
6034 };
6035
6036 let evaluator = MockEvaluator::all_true(&[]);
6037 let validator = EdifactValidator::new(evaluator);
6038 let external = NoOpExternalProvider;
6039
6040 let workflow = AhbWorkflow {
6041 pruefidentifikator: "13025".to_string(),
6042 description: "Test".to_string(),
6043 communication_direction: None,
6044 ub_definitions: BTreeMap::new(),
6045 package_conditions: Default::default(),
6046 fields: vec![AhbFieldRule {
6047 group_ahb_status: None,
6048 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
6049 name: "Statusanlaß, Code".to_string(),
6050 ahb_status: "X".to_string(),
6051 element_index: Some(2),
6052 component_index: Some(0),
6053 codes: vec![
6054 AhbCodeRule {
6055 value: "E01".into(),
6056 description: "Code 1".into(),
6057 ahb_status: "X [4P0..1]".into(),
6058 },
6059 AhbCodeRule {
6060 value: "E02".into(),
6061 description: "Code 2".into(),
6062 ahb_status: "X [4P0..1]".into(),
6063 },
6064 ],
6065 parent_group_ahb_status: Some("Muss".to_string()),
6066 segment_ahb_status: None,
6067 mig_number: None,
6068 format: None,
6069 }],
6070 };
6071
6072 let report = validator.validate_with_navigator(
6073 &[sts_a, sts_b],
6074 &workflow,
6075 &external,
6076 ValidationLevel::Full,
6077 &nav,
6078 );
6079 let pkg_errors: Vec<_> = report
6080 .by_category(ValidationCategory::Ahb)
6081 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
6082 .collect();
6083 assert!(
6084 pkg_errors.is_empty(),
6085 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
6086 pkg_errors
6087 );
6088 }
6089
6090 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
6091 OwnedSegment {
6092 id: id.to_string(),
6093 elements: elements
6094 .into_iter()
6095 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
6096 .collect(),
6097 segment_number: 0,
6098 }
6099 }
6100
6101 #[test]
6112 fn test_package_cardinality_scoped_to_rule_mig_variant() {
6113 use mig_types::navigator::GroupNavigator;
6114
6115 struct TwoSg8Variants {
6116 seq_z01: OwnedSegment,
6117 seq_z45: OwnedSegment,
6118 }
6119 impl GroupNavigator for TwoSg8Variants {
6120 fn find_segments_in_group(
6121 &self,
6122 segment_id: &str,
6123 group_path: &[&str],
6124 instance_index: usize,
6125 ) -> Vec<OwnedSegment> {
6126 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
6127 match instance_index {
6128 0 => vec![self.seq_z01.clone()],
6129 1 => vec![self.seq_z45.clone()],
6130 _ => vec![],
6131 }
6132 } else {
6133 vec![]
6134 }
6135 }
6136 fn find_segments_with_qualifier_in_group(
6137 &self,
6138 _: &str,
6139 _: usize,
6140 _: &str,
6141 _: &[&str],
6142 _: usize,
6143 ) -> Vec<OwnedSegment> {
6144 vec![]
6145 }
6146 fn group_instance_count(&self, group_path: &[&str]) -> usize {
6147 match group_path {
6148 ["SG4"] => 1,
6149 ["SG4", "SG8"] => 2,
6150 _ => 0,
6151 }
6152 }
6153 fn instance_has_mig_number(
6154 &self,
6155 group_path: &[&str],
6156 instance_index: usize,
6157 mig_number: &str,
6158 ) -> bool {
6159 if group_path != ["SG4", "SG8"] {
6160 return true;
6161 }
6162 match (instance_index, mig_number) {
6163 (0, "00115") => true,
6164 (0, _) => false,
6165 (1, "00171") => true,
6166 (1, _) => false,
6167 _ => false,
6168 }
6169 }
6170 }
6171
6172 let seq_z01 = OwnedSegment {
6173 id: "SEQ".into(),
6174 elements: vec![vec!["Z01".into()], vec!["1".into()]],
6175 segment_number: 10,
6176 };
6177 let seq_z45 = OwnedSegment {
6178 id: "SEQ".into(),
6179 elements: vec![vec!["Z45".into()], vec!["1".into()]],
6180 segment_number: 20,
6181 };
6182 let nav = TwoSg8Variants {
6183 seq_z01: seq_z01.clone(),
6184 seq_z45: seq_z45.clone(),
6185 };
6186
6187 let evaluator = MockEvaluator::all_true(&[]);
6188 let validator = EdifactValidator::new(evaluator);
6189 let external = NoOpExternalProvider;
6190
6191 let workflow = AhbWorkflow {
6192 pruefidentifikator: "55218".to_string(),
6193 description: "Test".to_string(),
6194 communication_direction: None,
6195 ub_definitions: BTreeMap::new(),
6196 package_conditions: Default::default(),
6197 fields: vec![AhbFieldRule {
6198 group_ahb_status: None,
6199 segment_path: "SG4/SG8/SEQ/1229".to_string(),
6200 name: "Handlung, Code".to_string(),
6201 ahb_status: "X".to_string(),
6202 element_index: Some(0),
6203 component_index: Some(0),
6204 codes: vec![
6205 AhbCodeRule {
6206 value: "Z45".into(),
6207 description: "NNA".into(),
6208 ahb_status: "X [1P1..4294967295]".into(),
6209 },
6210 AhbCodeRule {
6211 value: "Z84".into(),
6212 description: "Differenz-NNA".into(),
6213 ahb_status: "X [1P0..4294967295]".into(),
6214 },
6215 ],
6216 parent_group_ahb_status: Some("Muss".to_string()),
6217 segment_ahb_status: None,
6218 mig_number: Some("00171".to_string()),
6219 format: None,
6220 }],
6221 };
6222
6223 let report = validator.validate_with_navigator(
6224 &[seq_z01, seq_z45],
6225 &workflow,
6226 &external,
6227 ValidationLevel::Full,
6228 &nav,
6229 );
6230 let pkg_errors: Vec<_> = report
6231 .by_category(ValidationCategory::Ahb)
6232 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
6233 .collect();
6234 assert!(
6235 pkg_errors.is_empty(),
6236 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
6237 pkg_errors
6238 );
6239 }
6240
6241 #[test]
6242 fn test_unt_count_correct() {
6243 let segments = vec![
6245 make_segment("UNH", vec![vec!["001"]]),
6246 make_segment("BGM", vec![vec!["E01"]]),
6247 make_segment("DTM", vec![vec!["137", "20250401"]]),
6248 make_segment("UNT", vec![vec!["4", "001"]]),
6249 ];
6250 assert!(
6251 validate_unt_segment_count(&segments).is_none(),
6252 "Correct count should produce no issue"
6253 );
6254 }
6255
6256 #[test]
6257 fn test_unt_count_mismatch() {
6258 let segments = vec![
6260 make_segment("UNH", vec![vec!["001"]]),
6261 make_segment("BGM", vec![vec!["E01"]]),
6262 make_segment("UNT", vec![vec!["5", "001"]]),
6263 ];
6264 let issue =
6265 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
6266 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
6267 assert_eq!(issue.severity, Severity::Error);
6268 assert!(narrate(&issue).contains("declared 5"));
6269 assert!(narrate(&issue).contains("actual 3"));
6270 }
6271
6272 #[test]
6273 fn test_unt_count_excludes_envelope() {
6274 let segments = vec![
6276 make_segment("UNA", vec![]),
6277 make_segment("UNB", vec![vec!["UNOC", "3"]]),
6278 make_segment("UNH", vec![vec!["001"]]),
6279 make_segment("BGM", vec![vec!["E01"]]),
6280 make_segment("UNT", vec![vec!["3", "001"]]),
6281 make_segment("UNZ", vec![vec!["1"]]),
6282 ];
6283 assert!(
6284 validate_unt_segment_count(&segments).is_none(),
6285 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
6286 );
6287 }
6288
6289 #[test]
6290 fn test_unt_count_no_unt_returns_none() {
6291 let segments = vec![
6292 make_segment("UNH", vec![vec!["001"]]),
6293 make_segment("BGM", vec![vec!["E01"]]),
6294 ];
6295 assert!(
6296 validate_unt_segment_count(&segments).is_none(),
6297 "No UNT segment should return None (not our problem)"
6298 );
6299 }
6300
6301 #[test]
6302 fn test_unt_count_rejects_multi_message_input() {
6303 let segments = vec![
6305 make_segment("UNH", vec![vec!["001"]]),
6306 make_segment("BGM", vec![vec!["E01"]]),
6307 make_segment("UNT", vec![vec!["3", "001"]]),
6308 make_segment("UNH", vec![vec!["002"]]),
6309 make_segment("BGM", vec![vec!["E02"]]),
6310 make_segment("UNT", vec![vec!["3", "002"]]),
6311 ];
6312 let issue = validate_unt_segment_count(&segments)
6313 .expect("Multi-message input should produce an error");
6314 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
6315 assert!(
6316 narrate(&issue).contains("2 UNH"),
6317 "Should mention UNH count: {}",
6318 narrate(&issue)
6319 );
6320 }
6321
6322 #[test]
6323 fn test_code_validation_accepts_multi_code_variant_qualifier() {
6324 let evaluator = MockEvaluator::new(vec![]);
6331 let validator = EdifactValidator::new(evaluator);
6332 let external = NoOpExternalProvider;
6333
6334 let rff_z39 = OwnedSegment {
6335 id: "RFF".to_string(),
6336 elements: vec![
6337 vec!["RFF".to_string()],
6338 vec!["Z39".to_string(), "REF1".to_string()],
6339 ],
6340 segment_number: 1,
6341 };
6342
6343 let workflow = AhbWorkflow {
6344 pruefidentifikator: "55035".to_string(),
6345 description: "Test".to_string(),
6346 communication_direction: None,
6347 fields: vec![
6348 AhbFieldRule {
6350 group_ahb_status: None,
6351 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6352 name: "Referenznummer Qualifier".to_string(),
6353 ahb_status: "Muss".to_string(),
6354 codes: vec![
6355 AhbCodeRule {
6356 value: "Z31".to_string(),
6357 description: "".to_string(),
6358 ahb_status: "X".to_string(),
6359 },
6360 AhbCodeRule {
6361 value: "Z39".to_string(),
6362 description: "".to_string(),
6363 ahb_status: "X".to_string(),
6364 },
6365 ],
6366 parent_group_ahb_status: None,
6367 segment_ahb_status: None,
6368 element_index: Some(1),
6369 component_index: Some(0),
6370 mig_number: Some("00075".to_string()),
6371 format: None,
6372 },
6373 AhbFieldRule {
6375 group_ahb_status: None,
6376 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6377 name: "Referenznummer Qualifier".to_string(),
6378 ahb_status: "Muss".to_string(),
6379 codes: vec![AhbCodeRule {
6380 value: "Z33".to_string(),
6381 description: "".to_string(),
6382 ahb_status: "X".to_string(),
6383 }],
6384 parent_group_ahb_status: None,
6385 segment_ahb_status: None,
6386 element_index: Some(1),
6387 component_index: Some(0),
6388 mig_number: Some("00078".to_string()),
6389 format: None,
6390 },
6391 ],
6392 ub_definitions: BTreeMap::new(),
6393 package_conditions: Default::default(),
6394 };
6395
6396 let report = validator.validate(
6397 &[rff_z39],
6398 &workflow,
6399 &external,
6400 ValidationLevel::Conditions,
6401 );
6402
6403 let code_errors: Vec<_> = report
6404 .by_category(ValidationCategory::Code)
6405 .filter(|i| {
6406 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6407 })
6408 .collect();
6409 assert!(
6410 code_errors.is_empty(),
6411 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
6412 code_errors
6413 );
6414 }
6415
6416 #[test]
6417 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
6418 let evaluator = MockEvaluator::new(vec![]);
6424 let validator = EdifactValidator::new(evaluator);
6425 let external = NoOpExternalProvider;
6426
6427 let pia_5_z12 = OwnedSegment {
6431 id: "PIA".to_string(),
6432 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
6433 segment_number: 1,
6434 };
6435 let pia_5_srw = OwnedSegment {
6436 id: "PIA".to_string(),
6437 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
6438 segment_number: 2,
6439 };
6440
6441 let make_rules = |mig: &str, composite_code: &str| {
6442 vec![
6443 AhbFieldRule {
6444 group_ahb_status: None,
6445 segment_path: "SG4/SG8/PIA/4347".to_string(),
6446 name: "Produkt-ID-Funktion".to_string(),
6447 ahb_status: "Muss".to_string(),
6448 codes: vec![AhbCodeRule {
6449 value: "5".to_string(),
6450 description: "".to_string(),
6451 ahb_status: "X".to_string(),
6452 }],
6453 parent_group_ahb_status: None,
6454 segment_ahb_status: None,
6455 element_index: Some(0),
6456 component_index: Some(0),
6457 mig_number: Some(mig.to_string()),
6458 format: None,
6459 },
6460 AhbFieldRule {
6461 group_ahb_status: None,
6462 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
6463 name: "Artikel/Dienstleistung-Identifikator".to_string(),
6464 ahb_status: "Muss".to_string(),
6465 codes: vec![AhbCodeRule {
6466 value: composite_code.to_string(),
6467 description: "".to_string(),
6468 ahb_status: "X".to_string(),
6469 }],
6470 parent_group_ahb_status: None,
6471 segment_ahb_status: None,
6472 element_index: Some(1),
6473 component_index: Some(0),
6474 mig_number: Some(mig.to_string()),
6475 format: None,
6476 },
6477 ]
6478 };
6479
6480 let mut fields = make_rules("00108", "Z12");
6481 fields.extend(make_rules("00197", "SRW"));
6482
6483 let workflow = AhbWorkflow {
6484 pruefidentifikator: "55035".to_string(),
6485 description: "Test".to_string(),
6486 communication_direction: None,
6487 fields,
6488 ub_definitions: BTreeMap::new(),
6489 package_conditions: Default::default(),
6490 };
6491
6492 let report = validator.validate(
6493 &[pia_5_z12, pia_5_srw],
6494 &workflow,
6495 &external,
6496 ValidationLevel::Conditions,
6497 );
6498
6499 let code_errors: Vec<_> = report
6500 .by_category(ValidationCategory::Code)
6501 .filter(|i| {
6502 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6503 })
6504 .collect();
6505 assert!(
6506 code_errors.is_empty(),
6507 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
6508 code_errors
6509 );
6510 }
6511}