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.statuses_only {
287 report_exclusive_alternatives(
288 &validated_tree.root_fields,
289 None,
290 &validated_tree.groups,
291 &[],
292 &ctx,
293 &self.evaluator,
294 validated_tree.ub_definitions,
295 0,
296 &mut report,
297 );
298 }
299
300 if self.report_segments {
301 refuse_segments(
302 &validated_tree.root_fields,
303 None,
304 &[],
305 &ctx,
306 &self.evaluator,
307 validated_tree.ub_definitions,
308 0,
309 &mut report,
310 );
311 }
312
313 let message_type = self.evaluator.message_type();
318 let absent = crate::eval::AbsentTarget(&self.evaluator);
319 let absent_eval = ConditionExprEvaluator::new(&absent);
320 let mut per_transaction: HashSet<&str> = HashSet::new();
324 let mut instance_counter: HashMap<&str, usize> = HashMap::new();
325 let views = tree.map(|t| {
328 t.transaction_views(segments, |g| {
329 crate::eval::is_transaction_group(message_type, g)
330 })
331 });
332 for group in &validated_tree.groups {
333 let instance_index = *instance_counter
334 .entry(group.group_id)
335 .and_modify(|c| *c += 1)
336 .or_insert(0);
337
338 let view = views
341 .as_ref()
342 .and_then(|v| v.view(group.group_id, instance_index));
343 if let Some(view) = view {
344 let first_new = report.issues.len();
345 let view_navigator = AssembledTreeNavigator::new(&view.tree);
346 let view_ctx = ctx.with_view(&view.segments, &view_navigator);
347 let path_storage = [(group.group_id, 0)];
348 let scoped_ctx = view_ctx.with_scope(GroupScope {
349 path: &path_storage,
350 });
351 walk_group_instance(
352 group,
353 &path_storage,
354 &scoped_ctx,
355 &expr_eval,
356 &self.evaluator,
357 validated_tree.ub_definitions,
358 &tag_migs,
359 instance_index,
360 self.report_segments,
361 self.statuses_only,
362 &mut report,
363 );
364 report_absent_children(
365 group,
366 &path_storage,
367 &scoped_ctx,
368 &absent_eval,
369 &self.evaluator,
370 validated_tree.ub_definitions,
371 instance_index,
372 &mut report,
373 );
374 for issue in &mut report.issues[first_new..] {
375 issue.transaction_index = Some(instance_index);
376 }
377 per_transaction.extend(
378 group
379 .variant_rules
380 .iter()
381 .filter_map(|r| r.mig_number.as_deref()),
382 );
383 continue;
384 }
385
386 let path_storage = [(group.group_id, instance_index)];
387 let scoped_ctx = ctx.with_scope(GroupScope {
390 path: &path_storage,
391 });
392
393 walk_group_instance(
394 group,
395 &path_storage,
396 &scoped_ctx,
397 &expr_eval,
398 &self.evaluator,
399 validated_tree.ub_definitions,
400 &tag_migs,
401 instance_index,
402 self.report_segments,
403 self.statuses_only,
404 &mut report,
405 );
406 report_absent_children(
410 group,
411 &path_storage,
412 &scoped_ctx,
413 &absent_eval,
414 &self.evaluator,
415 validated_tree.ub_definitions,
416 instance_index,
417 &mut report,
418 );
419 }
420
421 if let Some((once, transaction)) =
423 crate::eval::once_per_transaction(self.evaluator.message_type())
424 .filter(|_| !self.statuses_only)
425 {
426 let mut index: HashMap<&str, usize> = HashMap::new();
427 for group in &validated_tree.groups {
428 let i = *index
429 .entry(group.group_id)
430 .and_modify(|c| *c += 1)
431 .or_insert(0);
432 if group.group_id == transaction {
433 let first_new = report.issues.len();
434 report_repeated_in_transaction(group, i, once, &ctx, &mut report);
435 for issue in &mut report.issues[first_new..] {
436 issue.transaction_index = Some(i);
437 }
438 }
439 }
440 }
441
442 if !self.statuses_only {
445 report_repeated_in_message(
446 &validated_tree.groups,
447 self.evaluator.message_type(),
448 &mut report,
449 );
450 }
451
452 if self.evaluator.message_type() == "UTILMD_Strom" && !self.statuses_only {
455 let mut index: HashMap<&str, usize> = HashMap::new();
456 for group in &validated_tree.groups {
457 let i = *index
458 .entry(group.group_id)
459 .and_modify(|c| *c += 1)
460 .or_insert(0);
461 if group.group_id == "SG4" {
462 let first_new = report.issues.len();
463 report_off_count_per_id(
464 group,
465 i,
466 &ctx,
467 &self.evaluator,
468 validated_tree.ub_definitions,
469 &mut report,
470 );
471 for issue in &mut report.issues[first_new..] {
472 issue.transaction_index = Some(i);
473 }
474 }
475 }
476 }
477
478 for field in &validated_tree.unmatched_rules {
488 if field
489 .mig_number
490 .as_deref()
491 .is_some_and(|n| per_transaction.contains(n))
492 {
493 continue;
494 }
495 if should_skip_for_parent_group(
496 field,
497 &absent_eval,
498 &ctx,
499 validated_tree.ub_definitions,
500 ) {
501 continue;
502 }
503
504 let status =
505 absent_eval.resolve_status(&field.ahb_status, &ctx, validated_tree.ub_definitions);
506
507 if status.result == ConditionResult::True
508 && status.kind == Some(StatusKind::Muss)
509 && !is_field_present(&ctx, field)
510 && !is_group_variant_absent(
511 &ctx,
512 field,
513 is_parent_group_required(
514 field,
515 &absent_eval,
516 &ctx,
517 validated_tree.ub_definitions,
518 ),
519 )
520 {
521 let mut issue = ValidationIssue::new(
522 Severity::Error,
523 IssueKind::MissingRequiredField {
524 field_name: field.name.clone(),
525 },
526 )
527 .with_field_path(&field.segment_path)
528 .with_rule(&field.ahb_status);
529 if let Some(first_code) = field.codes.first() {
530 issue.expected_value = Some(first_code.value.clone());
531 }
532 issue = attach_field_position(issue, field);
533 report.add_issue(issue);
534 }
535 }
536
537 if matches!(level, ValidationLevel::Full) && !self.statuses_only {
541 let mut seen: HashSet<*const AhbFieldRule> = HashSet::new();
545 let all_fields: Vec<AhbFieldRule> = all_nodes
546 .iter()
547 .map(|node| node.rule)
548 .chain(validated_tree.unmatched_rules.iter().copied())
549 .filter(|rule| seen.insert(*rule as *const AhbFieldRule))
550 .cloned()
551 .collect();
552 let synthetic_workflow = AhbWorkflow {
553 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
554 description: String::new(),
555 communication_direction: None,
556 fields: all_fields,
557 ub_definitions: validated_tree.ub_definitions.clone(),
558 package_conditions: validated_tree.package_conditions.clone(),
559 };
560 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
561 self.validate_answer_lists(&synthetic_workflow, &ctx, &mut report);
562 self.validate_package_cardinality(
563 &synthetic_workflow,
564 &ctx,
565 views.as_ref(),
566 &mut report,
567 );
568 }
569
570 report
571 }
572
573 fn validate_conditions(
575 &self,
576 workflow: &AhbWorkflow,
577 ctx: &EvaluationContext,
578 report: &mut ValidationReport,
579 ) {
580 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
581
582 for field in &workflow.fields {
583 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
585 continue;
586 }
587
588 let StatusResolution {
591 result: condition_result,
592 kind,
593 unknown_ids,
594 } = expr_eval.resolve_status(&field.ahb_status, ctx, &workflow.ub_definitions);
595 let mandatory = kind == Some(StatusKind::Muss);
596
597 match condition_result {
598 ConditionResult::True => {
599 if mandatory
601 && !is_field_present(ctx, field)
602 && !is_group_variant_absent(
603 ctx,
604 field,
605 is_parent_group_required(
606 field,
607 &expr_eval,
608 ctx,
609 &workflow.ub_definitions,
610 ),
611 )
612 {
613 let mut issue = ValidationIssue::new(
614 Severity::Error,
615 IssueKind::MissingRequiredField {
616 field_name: field.name.clone(),
617 },
618 )
619 .with_field_path(&field.segment_path)
620 .with_rule(&field.ahb_status)
621 .with_mig_number(field.mig_number.as_deref());
622 if let Some(first_code) = field.codes.first() {
626 issue.expected_value = Some(first_code.value.clone());
627 }
628 report.add_issue(issue);
629 }
630 }
631 ConditionResult::False => {
632 if mandatory && is_field_present(ctx, field) {
638 let failed = expr_eval.false_ids_with_ub(
639 &field.ahb_status,
640 ctx,
641 &workflow.ub_definitions,
642 );
643 report.add_issue(
644 ValidationIssue::new(
645 Severity::Error,
646 IssueKind::FieldConditionNotSatisfied {
647 field_name: field.name.clone(),
648 failed,
649 },
650 )
651 .with_field_path(&field.segment_path)
652 .with_rule(&field.ahb_status)
653 .with_mig_number(field.mig_number.as_deref()),
654 );
655 }
656 }
657 ConditionResult::Unknown => {
658 let mut external_ids = Vec::new();
663 let mut undetermined_ids = Vec::new();
664 let mut missing_ids = Vec::new();
665 for id in unknown_ids {
666 if self.evaluator.is_external(id) {
667 external_ids.push(id);
668 } else if self.evaluator.is_known(id) {
669 undetermined_ids.push(id);
670 } else {
671 missing_ids.push(id);
672 }
673 }
674
675 report.add_issue(
676 ValidationIssue::new(
677 Severity::Info,
678 IssueKind::ConditionUnknown {
679 field_name: field.name.clone(),
680 unresolved: UnresolvedConditions {
681 external: external_ids,
682 undetermined: undetermined_ids,
683 missing: missing_ids,
684 },
685 },
686 )
687 .with_field_path(&field.segment_path)
688 .with_rule(&field.ahb_status),
689 );
690 }
691 }
692 }
693
694 self.validate_codes_cross_field(workflow, ctx, report);
699 self.validate_answer_lists(workflow, ctx, report);
700
701 self.validate_package_cardinality(workflow, ctx, None, report);
704 }
705
706 fn validate_package_cardinality(
717 &self,
718 workflow: &AhbWorkflow,
719 ctx: &EvaluationContext,
720 views: Option<&TransactionViews<'_>>,
721 report: &mut ValidationReport,
722 ) {
723 struct PackageGroup {
726 min: u32,
727 max: u32,
728 max_sum: u32,
731 bounds: BTreeSet<(u32, u32)>,
732 code_values: Vec<String>,
733 element_index: usize,
734 component_index: usize,
735 }
736
737 let mut groups: HashMap<(String, Option<String>, u32, u32, u32), PackageGroup> =
746 HashMap::new();
747 let mut alternatives: HashMap<(String, Option<String>, u32), BTreeSet<u32>> =
751 HashMap::new();
752
753 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
754
755 let mut parent_group_skipped: HashMap<&str, bool> = HashMap::new();
762
763 for field in &workflow.fields {
764 let el_idx = field.element_index.unwrap_or(0);
765 let comp_idx = field.component_index.unwrap_or(0);
766
767 let code_packages: Vec<(&str, Vec<(u32, u32, u32)>)> = field
769 .codes
770 .iter()
771 .filter_map(|code| {
772 let expr = ConditionParser::parse(&code.ahb_status).ok()??;
773 let mut packages = Vec::new();
774 collect_packages(&expr, &mut packages);
775 (!packages.is_empty()).then_some((code.value.as_str(), packages))
776 })
777 .collect();
778 if code_packages.is_empty() {
779 continue;
780 }
781
782 let skipped = match field.parent_group_ahb_status.as_deref() {
784 Some(status) => *parent_group_skipped.entry(status).or_insert_with(|| {
785 should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions)
786 }),
787 None => false,
788 };
789 if skipped {
790 continue;
791 }
792
793 for (code_value, packages) in code_packages {
794 if packages.len() > 1 {
795 let ids: BTreeSet<u32> = packages.iter().map(|(id, _, _)| *id).collect();
796 for id in &ids {
797 alternatives
798 .entry((field.segment_path.clone(), field.mig_number.clone(), *id))
799 .or_default()
800 .extend(ids.iter().copied().filter(|other| other != id));
801 }
802 }
803
804 for (pkg_id, pkg_min, pkg_max) in packages {
805 let key = (
806 field.segment_path.clone(),
807 field.mig_number.clone(),
808 pkg_id,
809 0,
810 0,
811 );
812 let group = groups.entry(key).or_insert_with(|| PackageGroup {
813 min: pkg_min,
814 max: pkg_max,
815 max_sum: 0,
816 bounds: BTreeSet::new(),
817 code_values: Vec::new(),
818 element_index: el_idx,
819 component_index: comp_idx,
820 });
821 group.bounds.insert((pkg_min, pkg_max));
828 group.min = group.min.min(pkg_min);
829 if !group.code_values.iter().any(|v| v == code_value) {
830 group.max_sum = group.max_sum.saturating_add(pkg_max);
831 group.code_values.push(code_value.to_string());
832 }
833 group.max = group.max.max(pkg_max);
834 }
835 }
836 }
837
838 let mut field_packages: HashMap<(String, Option<String>), BTreeSet<u32>> = HashMap::new();
849 for (seg_path, mig_number, pkg_id, _, _) in groups.keys() {
850 field_packages
851 .entry((seg_path.clone(), mig_number.clone()))
852 .or_default()
853 .insert(*pkg_id);
854 }
855 let with_alternatives: Vec<(String, Option<String>)> = alternatives
856 .keys()
857 .map(|(p, m, _)| (p.clone(), m.clone()))
858 .collect();
859 for field in with_alternatives {
860 let Some(ids) = field_packages.get(&field) else {
861 continue;
862 };
863 for id in ids {
864 alternatives
865 .entry((field.0.clone(), field.1.clone(), *id))
866 .or_default()
867 .extend(ids.iter().copied().filter(|other| other != id));
868 }
869 }
870
871 type Counted = (Vec<usize>, Vec<bool>, usize, usize, Vec<String>);
873 let mut counted: HashMap<(String, Option<String>, u32, u32, u32), Counted> = HashMap::new();
874 let mut transaction_views: HashMap<(String, usize), TransactionSegmentsView> =
877 HashMap::new();
878 for ((seg_path, mig_number, pkg_id, _, _), group) in &groups {
879 let segment_id = extract_segment_id(seg_path);
880
881 let mut unique_codes: Vec<&str> =
885 group.code_values.iter().map(|s| s.as_str()).collect();
886 unique_codes.sort_unstable();
887 unique_codes.dedup();
888 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
889
890 let group_path_str = extract_group_path_key(seg_path);
891 let group_path: Vec<&str> = if group_path_str.is_empty() {
892 Vec::new()
893 } else {
894 group_path_str.split('/').collect()
895 };
896
897 let min = group.min as usize;
898 let max = if group.bounds.len() > 1 {
899 group.max_sum as usize
900 } else {
901 group.max as usize
902 };
903
904 type Instance<'p> = (usize, Option<Vec<(&'p str, usize)>>);
907 let per_instance_counts: Option<Vec<Instance>> =
908 match (ctx.navigator, group_path.is_empty()) {
909 (Some(nav), false)
917 if group_path.len() > 1
918 && !full_instance_paths(nav, &group_path).is_empty() =>
919 {
920 let paths = full_instance_paths(nav, &group_path);
921 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
929 .as_deref()
930 {
931 Some(m) => (0..group_path.len())
932 .filter_map(|level| {
933 let prefix = group_path[..=level].join("/");
934 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
935 })
936 .collect(),
937 None => Vec::new(),
938 };
939 if paths.is_empty() {
940 None
941 } else {
942 Some(
943 paths
944 .into_iter()
945 .filter(|path| match mig_number.as_deref() {
946 Some(m) => nav.instance_at_has_mig_number(path, m),
947 None => true,
948 })
949 .filter(|path| {
950 variant_entries.iter().all(|(level, entry)| {
951 nav.find_segments_in_instance(
952 &entry.tag,
953 &path[..=*level],
954 )
955 .first()
956 .and_then(|seg| {
957 seg.elements
958 .get(entry.element)
959 .and_then(|e| e.get(entry.component))
960 .cloned()
961 })
962 .map_or(true, |v| entry.codes.contains(v.as_str()))
963 })
964 })
965 .map(|path| {
966 let count = nav
967 .find_segments_in_instance(&segment_id, &path)
968 .iter()
969 .filter_map(|seg| {
970 seg.elements
971 .get(group.element_index)
972 .and_then(|e| e.get(group.component_index))
973 .filter(|v| !v.is_empty())
974 .cloned()
975 })
976 .filter(|v| code_set.contains(v.as_str()))
977 .count();
978 (count, Some(path))
979 })
980 .collect(),
981 )
982 }
983 }
984 (Some(nav), false) => {
985 let instance_count = nav.group_instance_count(&group_path);
986 if instance_count == 0 {
987 None
988 } else {
989 Some(
997 (0..instance_count)
998 .filter(|i| match mig_number.as_deref() {
999 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
1000 None => true,
1001 })
1002 .map(|i| {
1003 let count = nav
1004 .find_segments_in_group(&segment_id, &group_path, i)
1005 .iter()
1006 .filter_map(|seg| {
1007 seg.elements
1008 .get(group.element_index)
1009 .and_then(|e| e.get(group.component_index))
1010 .filter(|v| !v.is_empty())
1011 .cloned()
1012 })
1013 .filter(|v| code_set.contains(v.as_str()))
1014 .count();
1015 let path = (group_path.len() == 1)
1017 .then(|| vec![(group_path[0], i)]);
1018 (count, path)
1019 })
1020 .collect(),
1021 )
1022 }
1023 }
1024 _ => None,
1025 };
1026
1027 let instances = per_instance_counts.unwrap_or_else(|| {
1028 let segments = ctx.find_segments(&segment_id);
1029 let count = segments
1030 .iter()
1031 .filter_map(|seg| {
1032 seg.elements
1033 .get(group.element_index)
1034 .and_then(|e| e.get(group.component_index))
1035 .filter(|v| !v.is_empty())
1036 .map(|s| s.as_str())
1037 })
1038 .filter(|v| code_set.contains(v))
1039 .count();
1040 vec![(count, None)]
1041 });
1042 let applies: Vec<bool> = instances
1048 .iter()
1049 .map(|(_, path)| match workflow.package_conditions.get(pkg_id) {
1050 None => true,
1051 Some(condition) => {
1052 let result = match path {
1053 Some(path) => match path.first().and_then(|&(group_id, index)| {
1054 let key = (group_id.to_string(), index);
1055 if !transaction_views.contains_key(&key) {
1056 let view = views?.view(group_id, index)?;
1057 transaction_views.insert(key.clone(), view);
1058 }
1059 transaction_views.get(&key)
1060 }) {
1061 Some(view) => {
1062 let navigator = AssembledTreeNavigator::new(&view.tree);
1063 let mut in_view = path.clone();
1064 in_view[0].1 = 0;
1065 expr_eval.evaluate(
1066 condition,
1067 &ctx.with_view(&view.segments, &navigator)
1068 .with_scope(crate::eval::GroupScope { path: &in_view }),
1069 )
1070 }
1071 None => expr_eval.evaluate(
1072 condition,
1073 &ctx.with_scope(crate::eval::GroupScope { path }),
1074 ),
1075 },
1076 None => expr_eval.evaluate(condition, ctx),
1077 };
1078 result == ConditionResult::True
1079 }
1080 })
1081 .collect();
1082 let counts: Vec<usize> = instances.iter().map(|(c, _)| *c).collect();
1083
1084 counted.insert(
1085 (
1086 seg_path.clone(),
1087 mig_number.clone(),
1088 *pkg_id,
1089 group.min,
1090 group.max,
1091 ),
1092 (
1093 counts,
1094 applies,
1095 min,
1096 max,
1097 unique_codes.iter().map(|s| s.to_string()).collect(),
1098 ),
1099 );
1100 }
1101
1102 let mut keys: Vec<&(String, Option<String>, u32, u32, u32)> = counted.keys().collect();
1107 keys.sort();
1108 for key in keys {
1109 let (counts, applies, min, max, codes) = &counted[key];
1110 let (seg_path, mig_number, pkg_id, _, _) = key;
1111 let holds = |c: &[usize], a: &[bool], lo: usize, hi: usize, i: usize| {
1112 c.len() == counts.len() && a[i] && (lo..=hi).contains(&c[i]) && c[i] > 0
1113 };
1114 let alternative_holds = |i: usize| {
1115 alternatives
1116 .get(&(seg_path.clone(), mig_number.clone(), *pkg_id))
1117 .is_some_and(|alts| {
1118 counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
1119 p == seg_path
1120 && m == mig_number
1121 && alts.contains(id)
1122 && holds(c, a, *lo, *hi, i)
1123 })
1124 })
1125 };
1126 let applicable_alternative_holds = |i: usize| {
1131 *min > 0
1132 && counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
1133 p == seg_path
1134 && m == mig_number
1135 && id != pkg_id
1136 && *lo > 0
1137 && holds(c, a, *lo, *hi, i)
1138 })
1139 };
1140 let mut reported_counts: HashSet<usize> = HashSet::new();
1141 for (i, &present_count) in counts.iter().enumerate() {
1142 if applies[i]
1143 && (present_count < *min || present_count > *max)
1144 && !alternative_holds(i)
1145 && !(present_count < *min && applicable_alternative_holds(i))
1146 && reported_counts.insert(present_count)
1147 {
1148 report.add_issue(
1149 ValidationIssue::new(
1150 Severity::Error,
1151 IssueKind::PackageCardinality {
1152 package_id: pkg_id.to_string(),
1153 present: present_count,
1154 min: *min,
1155 max: *max,
1156 codes: codes.clone(),
1157 },
1158 )
1159 .with_field_path(seg_path)
1160 .with_expected(format!("{min}..{max}"))
1161 .with_actual(present_count.to_string()),
1162 );
1163 }
1164 }
1165 }
1166 }
1167
1168 fn validate_codes_cross_field(
1180 &self,
1181 workflow: &AhbWorkflow,
1182 ctx: &EvaluationContext,
1183 report: &mut ValidationReport,
1184 ) {
1185 if ctx.navigator.is_some() {
1186 self.validate_codes_group_scoped(workflow, ctx, report);
1187 } else {
1188 self.validate_codes_tag_scoped(workflow, ctx, report);
1189 }
1190 }
1191
1192 fn validate_codes_group_scoped(
1195 &self,
1196 workflow: &AhbWorkflow,
1197 ctx: &EvaluationContext,
1198 report: &mut ValidationReport,
1199 ) {
1200 let by_loc = partition_codes_by_mig(workflow);
1201 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
1202 let data_slots = data_slot_entries(workflow);
1203 let nav = ctx.navigator.unwrap();
1204
1205 for ((group_key, tag), migs) in &by_loc {
1206 let field_path = if group_key.is_empty() {
1207 format!("{tag}/qualifier")
1208 } else {
1209 format!("{group_key}/{tag}/qualifier")
1210 };
1211
1212 let group_path: Vec<&str> = if group_key.is_empty() {
1213 Vec::new()
1214 } else {
1215 group_key.split('/').collect()
1216 };
1217
1218 let tag_qualifiers = known_qualifiers.get(tag);
1219
1220 if group_path.is_empty() {
1221 Self::validate_segments_per_mig(
1222 &ctx.find_segments(tag),
1223 migs,
1224 tag_qualifiers,
1225 false,
1226 tag,
1227 &field_path,
1228 report,
1229 );
1230 } else {
1231 let instance_count = nav.group_instance_count(&group_path);
1232 let slots = data_slots.get(&(group_key.clone(), tag.clone()));
1233 for i in 0..instance_count {
1234 let owned = nav.find_segments_in_group(tag, &group_path, i);
1235 let refs: Vec<&OwnedSegment> = owned.iter().collect();
1236 let holds_data_slot = slots.is_some_and(|entries| {
1239 entries.iter().any(|(entry_tag, entry_mig)| {
1240 let Some(bucket) = by_loc
1241 .get(&(group_key.clone(), entry_tag.clone()))
1242 .and_then(|m| m.get(entry_mig))
1243 else {
1244 return false;
1245 };
1246 nav.find_segments_in_group(entry_tag, &group_path, i)
1247 .iter()
1248 .any(|s| matches_qualifier(s, bucket))
1249 })
1250 });
1251 Self::validate_segments_per_mig(
1252 &refs,
1253 migs,
1254 tag_qualifiers,
1255 holds_data_slot,
1256 tag,
1257 &field_path,
1258 report,
1259 );
1260 }
1261 }
1262 }
1263 }
1264
1265 fn validate_codes_tag_scoped(
1269 &self,
1270 workflow: &AhbWorkflow,
1271 ctx: &EvaluationContext,
1272 report: &mut ValidationReport,
1273 ) {
1274 let by_loc = partition_codes_by_mig(workflow);
1275 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
1276 let mut by_tag: BTreeMap<String, BTreeMap<Option<String>, MigCodeBucket>> = BTreeMap::new();
1278 for ((_group_key, tag), migs) in by_loc {
1279 let merged = by_tag.entry(tag).or_default();
1280 for (mig_key, bucket) in migs {
1281 let entry = merged.entry(mig_key).or_default();
1282 if entry.qualifier_position.is_none() {
1283 entry.qualifier_position = bucket.qualifier_position;
1284 }
1285 if entry.qualifier_position == bucket.qualifier_position {
1286 entry.qualifier_values.extend(&bucket.qualifier_values);
1287 }
1288 for (pos, codes) in bucket.codes {
1289 entry.codes.entry(pos).or_default().extend(codes);
1290 }
1291 }
1292 }
1293
1294 for (tag, migs) in &by_tag {
1295 let field_path = format!("{tag}/qualifier");
1296 Self::validate_segments_per_mig(
1297 &ctx.find_segments(tag),
1298 migs,
1299 known_qualifiers.get(tag),
1300 false,
1301 tag,
1302 &field_path,
1303 report,
1304 );
1305 }
1306 }
1307
1308 fn validate_segments_per_mig(
1321 segments: &[&OwnedSegment],
1322 migs: &BTreeMap<Option<String>, MigCodeBucket>,
1323 tag_qualifiers: Option<&BTreeMap<(usize, usize), HashSet<String>>>,
1324 holds_data_slot: bool,
1325 tag: &str,
1326 field_path: &str,
1327 report: &mut ValidationReport,
1328 ) {
1329 for seg in segments {
1330 match match_segment_to_mig(seg, migs) {
1331 Some(bucket) => {
1332 for ((el, c), allowed) in &bucket.codes {
1333 if allowed.is_empty() {
1334 continue;
1335 }
1336 Self::check_segments_against_codes(
1337 vec![*seg],
1338 allowed,
1339 tag,
1340 *el,
1341 *c,
1342 field_path,
1343 report,
1344 );
1345 }
1346 }
1347 None => {
1348 let Some(qualifiers) = tag_qualifiers.filter(|_| !holds_data_slot) else {
1349 continue;
1350 };
1351 for ((el, c), allowed) in qualifiers {
1356 let Some(actual) = seg
1357 .elements
1358 .get(*el)
1359 .and_then(|e| e.get(*c))
1360 .filter(|v| !v.is_empty())
1361 .map(|s| s.as_str())
1362 else {
1363 continue;
1364 };
1365 if allowed.iter().any(|v| v == actual) {
1366 break;
1368 }
1369 let bucket_values: HashSet<&str> = migs
1372 .values()
1373 .filter(|b| b.qualifier_position == Some((*el, *c)))
1374 .flat_map(|b| b.qualifier_values.iter().copied())
1375 .collect();
1376 if !bucket_values.is_empty() {
1377 Self::check_segments_against_codes(
1378 vec![*seg],
1379 &bucket_values,
1380 tag,
1381 *el,
1382 *c,
1383 field_path,
1384 report,
1385 );
1386 break;
1387 }
1388 }
1389 }
1390 }
1391 }
1392 }
1393
1394 fn validate_answer_lists(
1403 &self,
1404 workflow: &AhbWorkflow,
1405 ctx: &EvaluationContext,
1406 report: &mut ValidationReport,
1407 ) {
1408 let lists = AnswerCodeLists::embedded();
1409 let fv = self.evaluator.format_version();
1410 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1411 for list_rule in &workflow.fields {
1412 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1413 continue;
1414 };
1415 if list_rule.codes.is_empty()
1416 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1417 {
1418 continue;
1419 }
1420 let Some(answer_rule) = workflow
1421 .fields
1422 .iter()
1423 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1424 else {
1425 continue;
1426 };
1427 let at = |rule: &AhbFieldRule| {
1428 (
1429 rule.element_index.unwrap_or(0),
1430 rule.component_index.unwrap_or(0),
1431 )
1432 };
1433 let (list_el, list_c) = at(list_rule);
1434 let (answer_el, answer_c) = at(answer_rule);
1435 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1436 seg.elements
1437 .get(el)
1438 .and_then(|e| e.get(c))
1439 .filter(|v| !v.is_empty())
1440 .cloned()
1441 };
1442 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1443 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1444 continue;
1445 }
1446 let (Some(list), Some(answer)) =
1447 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1448 else {
1449 continue;
1450 };
1451 if !list_rule.codes.iter().any(|c| c.value == list) {
1452 continue;
1453 }
1454 if answer_rule.codes.iter().any(|c| c.value == answer) {
1458 continue;
1459 }
1460 let Some(codes) = lists.codes(fv, &list) else {
1461 continue;
1462 };
1463 if codes.contains(&answer) {
1464 continue;
1465 }
1466 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1467 allowed.sort_unstable();
1468 report.add_issue(
1469 ValidationIssue::new(
1470 Severity::Warning,
1471 IssueKind::AnswerNotInList {
1472 answer: answer.clone(),
1473 list: list.clone(),
1474 allowed: allowed.clone(),
1475 },
1476 )
1477 .with_field_path(&answer_rule.segment_path)
1478 .with_actual(&answer)
1479 .with_expected(allowed.join(", ")),
1480 );
1481 }
1482 }
1483 }
1484
1485 fn check_segments_against_codes(
1487 segments: Vec<&OwnedSegment>,
1488 allowed_codes: &HashSet<&str>,
1489 _tag: &str,
1490 el_idx: usize,
1491 comp_idx: usize,
1492 field_path: &str,
1493 report: &mut ValidationReport,
1494 ) {
1495 for segment in segments {
1496 if let Some(code_value) = segment
1497 .elements
1498 .get(el_idx)
1499 .and_then(|e| e.get(comp_idx))
1500 .filter(|v| !v.is_empty())
1501 {
1502 if !allowed_codes.contains(code_value.as_str()) {
1503 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1504 sorted_codes.sort_unstable();
1505 let kind = IssueKind::CodeNotAllowedForPid {
1506 value: code_value.to_string(),
1507 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1508 };
1509 let mut issue = ValidationIssue::new(Severity::Error, kind)
1510 .with_field_path(field_path)
1511 .with_actual(code_value)
1512 .with_expected(sorted_codes.join(", "))
1513 .with_field_position(el_idx as u32 + 2, Some(comp_idx as u32 + 1));
1514 if segment.segment_number > 0 {
1515 issue =
1516 issue.with_position(crate::SegmentPosition::at(segment.segment_number));
1517 }
1518 report.add_issue(issue);
1519 }
1520 }
1521 }
1522 }
1523}
1524
1525fn full_instance_paths<'p>(
1528 nav: &dyn GroupNavigator,
1529 group_path: &[&'p str],
1530) -> Vec<Vec<(&'p str, usize)>> {
1531 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1532 for g in group_path {
1533 paths = paths
1534 .into_iter()
1535 .flat_map(|parent| {
1536 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1537 let mut path = parent.clone();
1538 path.push((*g, i));
1539 path
1540 })
1541 })
1542 .collect();
1543 }
1544 paths
1545}
1546
1547struct VariantEntry<'a> {
1550 tag: String,
1551 element: usize,
1552 component: usize,
1553 codes: HashSet<&'a str>,
1554}
1555
1556fn merged_variant_entry<'a>(
1562 workflow: &'a AhbWorkflow,
1563 group_path: &str,
1564 mig: &str,
1565) -> Option<VariantEntry<'a>> {
1566 let mig: u32 = mig.parse().ok()?;
1567 let prefix = format!("{group_path}/");
1568 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1570 .fields
1571 .iter()
1572 .filter_map(|r| {
1573 let rest = r.segment_path.strip_prefix(&prefix)?;
1574 let tag = rest.split('/').next()?;
1575 if tag.starts_with("SG") {
1576 return None;
1577 }
1578 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1579 })
1580 .collect();
1581 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1583 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1584 .iter()
1585 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1586 .collect();
1587 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1588 if numbers.len() < 2 {
1589 return None;
1590 }
1591 let own = entries
1592 .iter()
1593 .filter(|(_, n, _)| *n <= mig)
1594 .map(|(_, n, _)| *n)
1595 .max()?;
1596 let (rule, _, tag) = entries
1598 .iter()
1599 .filter(|(_, n, _)| *n == own)
1600 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1601 Some(VariantEntry {
1602 tag: tag.to_string(),
1603 element: rule.element_index.unwrap_or(0),
1604 component: rule.component_index.unwrap_or(0),
1605 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1606 })
1607}
1608
1609fn value_length(format: &str, value: &str) -> usize {
1613 let format = format.trim();
1614 if format.starts_with('n') {
1615 value
1616 .chars()
1617 .filter(|c| !matches!(c, '.' | ',' | '-'))
1618 .count()
1619 } else {
1620 value.chars().count()
1621 }
1622}
1623
1624fn max_length(format: &str) -> Option<usize> {
1627 let digits = format
1628 .trim()
1629 .strip_prefix("an")
1630 .or_else(|| format.trim().strip_prefix('a'))
1631 .or_else(|| format.trim().strip_prefix('n'))?;
1632 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1633}
1634
1635fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1646 let derived_from_codes = field
1647 .codes
1648 .first()
1649 .is_some_and(|c| c.ahb_status == field.ahb_status);
1650 if !derived_from_codes && !codes_are_alternatives(field) {
1651 return &field.ahb_status;
1652 }
1653 value
1654 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1655 .map_or(&field.ahb_status, |c| &c.ahb_status)
1656}
1657
1658fn codes_are_alternatives(field: &AhbFieldRule) -> bool {
1669 !field.codes.is_empty()
1670 && field.ahb_status.contains('[')
1671 && field
1672 .codes
1673 .iter()
1674 .all(|c| c.ahb_status.contains('[') && c.ahb_status != field.ahb_status)
1675}
1676
1677fn should_skip_for_parent_group<E: ConditionEvaluator>(
1687 field: &AhbFieldRule,
1688 expr_eval: &ConditionExprEvaluator<E>,
1689 ctx: &EvaluationContext,
1690 ub_definitions: &BTreeMap<String, ConditionExpr>,
1691) -> bool {
1692 let Some(group_status) = field.parent_group_ahb_status.as_deref() else {
1693 return false;
1694 };
1695 if !group_status.contains('[') {
1696 return false;
1697 }
1698 match ConditionParser::parse_status_branches(group_status, ub_definitions) {
1699 Ok(branches) => !branches.iter().any(|branch| {
1700 branch.condition.as_ref().map_or(true, |c| {
1701 expr_eval.evaluate(c, ctx) == ConditionResult::True
1702 })
1703 }),
1704 Err(_) => true,
1705 }
1706}
1707
1708fn parent_group_refused<E: ConditionEvaluator>(
1712 field: &AhbFieldRule,
1713 expr_eval: &ConditionExprEvaluator<E>,
1714 ctx: &EvaluationContext,
1715 ub_definitions: &BTreeMap<String, ConditionExpr>,
1716) -> bool {
1717 let Some(group_status) = field.parent_group_ahb_status.as_deref() else {
1718 return false;
1719 };
1720 if !group_status.contains('[') {
1721 return false;
1722 }
1723 match ConditionParser::parse_status_branches(group_status, ub_definitions) {
1724 Ok(branches) => branches.iter().all(|branch| {
1725 branch
1726 .condition
1727 .as_ref()
1728 .is_some_and(|c| expr_eval.evaluate(c, ctx) == ConditionResult::False)
1729 }),
1730 Err(_) => true,
1731 }
1732}
1733
1734fn is_parent_group_required<E: ConditionEvaluator>(
1746 field: &AhbFieldRule,
1747 expr_eval: &ConditionExprEvaluator<E>,
1748 ctx: &EvaluationContext,
1749 ub_definitions: &BTreeMap<String, ConditionExpr>,
1750) -> bool {
1751 let required = |status: &str| {
1752 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1753 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1754 };
1755 field
1756 .parent_group_ahb_status
1757 .as_deref()
1758 .is_some_and(required)
1759 && field.group_ahb_status.as_deref().map_or(true, required)
1760}
1761
1762fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1771 let segment_id = extract_segment_id(&field.segment_path);
1772
1773 if !field.codes.is_empty() {
1779 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1780 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
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 if is_qualifier_field(&field.segment_path) {
1792 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1793 let el_idx = field.element_index.unwrap_or(0);
1794 let comp_idx = field.component_index.unwrap_or(0);
1795 let matching = ctx.find_segments(&segment_id);
1796 return matching.iter().any(|seg| {
1797 seg.elements
1798 .get(el_idx)
1799 .and_then(|e| e.get(comp_idx))
1800 .is_some_and(|v| required_codes.contains(&v.as_str()))
1801 });
1802 }
1803 }
1804
1805 ctx.has_segment(&segment_id)
1806}
1807
1808fn is_group_variant_absent(
1823 ctx: &EvaluationContext,
1824 field: &AhbFieldRule,
1825 is_group_mandatory: bool,
1826) -> bool {
1827 let group_path: Vec<&str> = field
1828 .segment_path
1829 .split('/')
1830 .take_while(|p| p.starts_with("SG"))
1831 .collect();
1832
1833 if group_path.is_empty() {
1834 return false;
1835 }
1836
1837 let nav = match ctx.navigator {
1838 Some(nav) => nav,
1839 None => return false,
1840 };
1841
1842 let instance_count = nav.group_instance_count(&group_path);
1843
1844 if instance_count == 0 {
1849 if !is_group_mandatory {
1850 return true;
1851 }
1852 return false;
1854 }
1855
1856 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1861 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1863 let segment_id = extract_segment_id(&field.segment_path);
1864 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1865
1866 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1867 nav.find_segments_in_group(&segment_id, &group_path, i)
1868 .iter()
1869 .any(|seg| {
1870 seg.elements
1871 .first()
1872 .and_then(|e| e.first())
1873 .is_some_and(|v| required_codes.contains(&v.as_str()))
1874 })
1875 });
1876
1877 if !any_instance_has_qualifier {
1878 return true; }
1880 }
1881 }
1882
1883 let segment_id = extract_segment_id(&field.segment_path);
1892 let segment_absent_from_all = (0..instance_count).all(|i| {
1893 nav.find_segments_in_group(&segment_id, &group_path, i)
1894 .is_empty()
1895 });
1896 if segment_absent_from_all {
1897 let group_has_other_segments =
1898 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1899 if group_has_other_segments {
1900 return true;
1901 }
1902 }
1903
1904 false
1905}
1906
1907fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1909 out.extend(group.fields.iter());
1910 for child in &group.children {
1911 collect_nodes_depth_first(child, out);
1912 }
1913}
1914
1915#[allow(clippy::too_many_arguments)]
1919fn evaluate_node<E: ConditionEvaluator>(
1920 node: &AhbNode,
1921 ctx: &EvaluationContext,
1922 expr_eval: &ConditionExprEvaluator<E>,
1923 evaluator: &E,
1924 ub_definitions: &BTreeMap<String, ConditionExpr>,
1925 tag_migs: &HashMap<String, HashSet<&str>>,
1926 instance_index: Option<usize>,
1927 slot_holds: bool,
1928 statuses_only: bool,
1929 report: &mut ValidationReport,
1930) {
1931 let field = node.rule;
1932 if statuses_only && node.value.is_some() {
1934 return;
1935 }
1936
1937 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1939
1940 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1945 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1946 if length > max {
1947 let mut issue = ValidationIssue::new(
1948 Severity::Error,
1949 IssueKind::ValueTooLong {
1950 field_name: field.name.clone(),
1951 length,
1952 max,
1953 },
1954 )
1955 .with_field_path(&field.segment_path)
1956 .with_actual(value)
1957 .with_expected(field.format.clone().unwrap_or_default());
1958 if let Some(idx) = instance_index {
1959 issue = issue.with_instance_index(idx);
1960 }
1961 if let Some(num) = node.matched_segment_number {
1962 issue = issue.with_position(crate::SegmentPosition::at(num));
1963 }
1964 report.add_issue(attach_field_position(issue, field));
1965 }
1966 }
1967
1968 if let (Some(value), Some(format)) = (node.value, field.format.as_deref()) {
1971 if slot_holds
1972 && format.trim().starts_with('n')
1973 && field.codes.is_empty()
1974 && !value.is_empty()
1975 && !crate::eval::format_validators::is_edifact_number(value)
1976 {
1977 let mut issue = ValidationIssue::new(
1978 Severity::Error,
1979 IssueKind::ValueNotNumeric {
1980 field_name: field.name.clone(),
1981 },
1982 )
1983 .with_field_path(&field.segment_path)
1984 .with_actual(value)
1985 .with_expected(format);
1986 if let Some(idx) = instance_index {
1987 issue = issue.with_instance_index(idx);
1988 }
1989 if let Some(num) = node.matched_segment_number {
1990 issue = issue.with_position(crate::SegmentPosition::at(num));
1991 }
1992 report.add_issue(attach_field_position(issue, field));
1993 }
1994 }
1995
1996 if let (Some(value), Some(elements)) = (node.value, node.segment_elements) {
2000 let format = elements
2001 .first()
2002 .and_then(|composite| composite.get(2))
2003 .map(String::as_str)
2004 .unwrap_or_default();
2005 if slot_holds
2006 && field.segment_path.ends_with("DTM/C507/2380")
2007 && !value.is_empty()
2008 && crate::eval::format_validators::fits_date_format(value, format) == Some(false)
2009 {
2010 let mut issue = ValidationIssue::new(
2011 Severity::Error,
2012 IssueKind::DateNotInFormat {
2013 field_name: field.name.clone(),
2014 format: format.to_string(),
2015 },
2016 )
2017 .with_field_path(&field.segment_path)
2018 .with_actual(value)
2019 .with_expected(format);
2020 if let Some(idx) = instance_index {
2021 issue = issue.with_instance_index(idx);
2022 }
2023 if let Some(num) = node.matched_segment_number {
2024 issue = issue.with_position(crate::SegmentPosition::at(num));
2025 }
2026 report.add_issue(attach_field_position(issue, field));
2027 }
2028 }
2029
2030 if parent_group_refused(field, expr_eval, ctx, ub_definitions) {
2034 return;
2035 }
2036
2037 let status = governing_status(field, node.value);
2040 let StatusResolution {
2041 result: condition_result,
2042 kind,
2043 unknown_ids,
2044 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
2045 let mandatory = kind == Some(StatusKind::Muss);
2046
2047 match condition_result {
2048 ConditionResult::True => {
2049 if mandatory && node.value.is_none() {
2058 let tag = extract_segment_id(&field.segment_path);
2059 let single_variant_present = node.segment_elements.is_none()
2060 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
2061 && is_field_present(ctx, field);
2062 let segment_optional_and_absent = node.segment_elements.is_none()
2067 && field
2068 .segment_ahb_status
2069 .as_deref()
2070 .is_some_and(is_optional_segment_status);
2071 let segment_not_required_and_absent = node.segment_elements.is_none()
2078 && field.segment_ahb_status.as_deref().is_some_and(|st| {
2079 let status = expr_eval.resolve_status(st, &node_ctx, ub_definitions);
2080 status.result != ConditionResult::True
2081 || status.kind != Some(StatusKind::Muss)
2082 });
2083 if !single_variant_present
2084 && !segment_optional_and_absent
2085 && !segment_not_required_and_absent
2086 {
2087 let mut issue = ValidationIssue::new(
2088 Severity::Error,
2089 IssueKind::MissingRequiredField {
2090 field_name: field.name.clone(),
2091 },
2092 )
2093 .with_field_path(&field.segment_path)
2094 .with_rule(&field.ahb_status);
2095 if let Some(first_code) = field.codes.first() {
2096 issue.expected_value = Some(first_code.value.clone());
2097 }
2098 if let Some(idx) = instance_index {
2099 issue = issue.with_instance_index(idx);
2100 }
2101 if let Some(num) = node.matched_segment_number {
2102 issue = issue.with_position(crate::SegmentPosition::at(num));
2103 }
2104 issue = attach_field_position(issue, field);
2105 report.add_issue(issue);
2106 }
2107 }
2108 }
2109 ConditionResult::False => {
2110 if mandatory && node.value.is_some() {
2113 let failed = expr_eval.false_ids_with_ub(status, &node_ctx, ub_definitions);
2114 let mut issue = ValidationIssue::new(
2115 Severity::Error,
2116 IssueKind::FieldConditionNotSatisfied {
2117 field_name: field.name.clone(),
2118 failed,
2119 },
2120 )
2121 .with_field_path(&field.segment_path)
2122 .with_rule(&field.ahb_status);
2123 if let Some(idx) = instance_index {
2124 issue = issue.with_instance_index(idx);
2125 }
2126 if let Some(num) = node.matched_segment_number {
2127 issue = issue.with_position(crate::SegmentPosition::at(num));
2128 }
2129 issue = attach_field_position(issue, field);
2130 report.add_issue(issue);
2131 }
2132 }
2133 ConditionResult::Unknown if statuses_only => {}
2134 ConditionResult::Unknown => {
2135 let mut external_ids = Vec::new();
2137 let mut undetermined_ids = Vec::new();
2138 let mut missing_ids = Vec::new();
2139 for id in unknown_ids {
2140 if evaluator.is_external(id) {
2141 external_ids.push(id);
2142 } else if evaluator.is_known(id) {
2143 undetermined_ids.push(id);
2144 } else {
2145 missing_ids.push(id);
2146 }
2147 }
2148
2149 let mut issue = ValidationIssue::new(
2150 Severity::Info,
2151 IssueKind::ConditionUnknown {
2152 field_name: field.name.clone(),
2153 unresolved: UnresolvedConditions {
2154 external: external_ids,
2155 undetermined: undetermined_ids,
2156 missing: missing_ids,
2157 },
2158 },
2159 )
2160 .with_field_path(&field.segment_path)
2161 .with_rule(&field.ahb_status);
2162 if let Some(idx) = instance_index {
2163 issue = issue.with_instance_index(idx);
2164 }
2165 if let Some(num) = node.matched_segment_number {
2166 issue = issue.with_position(crate::SegmentPosition::at(num));
2167 }
2168 issue = attach_field_position(issue, field);
2169 report.add_issue(issue);
2170 }
2171 }
2172}
2173
2174fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
2179 let issue = issue.with_mig_number(field.mig_number.as_deref());
2180 match field.element_index {
2181 Some(el) => {
2182 let element_pos = (el as u32) + 2;
2183 let component_pos = field.component_index.map(|c| (c as u32) + 1);
2184 issue.with_field_position(element_pos, component_pos)
2185 }
2186 None => issue,
2187 }
2188}
2189
2190fn report_repeated_in_transaction(
2197 transaction: &AhbGroupNode,
2198 transaction_index: usize,
2199 once: u32,
2200 ctx: &EvaluationContext,
2201 report: &mut ValidationReport,
2202) {
2203 let mut counts: BTreeMap<(String, String), (usize, String)> = BTreeMap::new();
2205 fn visit<'a>(
2206 node: &'a AhbGroupNode<'a>,
2207 path: &mut Vec<(&'a str, usize)>,
2208 once: u32,
2209 ctx: &EvaluationContext,
2210 counts: &mut BTreeMap<(String, String), (usize, String)>,
2211 ) {
2212 let ids: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2213 let mut seen: HashSet<Option<&str>> = HashSet::new();
2214 for n in &node.fields {
2215 let rule = n.rule;
2216 let Some(status) = rule.segment_ahb_status.as_deref() else {
2217 continue;
2218 };
2219 if Some(status) == node.ahb_status
2220 || rule.element_index.unwrap_or(0) != 0
2221 || rule.component_index.unwrap_or(0) != 0
2222 || rule.codes.is_empty()
2223 || !crate::eval::binds_once(status, once)
2224 || !seen.insert(rule.mig_number.as_deref())
2225 {
2226 continue;
2227 }
2228 let tag = extract_segment_id(&rule.segment_path);
2229 let sent = ctx.navigator().map_or(0, |nav| {
2230 nav.find_segments_in_instance(&tag, path)
2231 .iter()
2232 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2233 .count()
2234 });
2235 let entry = counts
2236 .entry((
2237 format!("{}/{tag}", ids.join("/")),
2238 rule.mig_number.clone().unwrap_or_default(),
2239 ))
2240 .or_insert((0, status.to_string()));
2241 entry.0 += sent;
2242 }
2243 for child in &node.children {
2244 if let Some(status) = child
2248 .own_status
2249 .or(child.ahb_status)
2250 .filter(|s| crate::eval::binds_once(s, once))
2251 {
2252 let variant = child
2253 .fields
2254 .first()
2255 .and_then(|n| n.rule.mig_number.clone())
2256 .unwrap_or_default();
2257 let entry = counts
2258 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
2259 .or_insert((0, status.to_string()));
2260 entry.0 += 1;
2261 }
2262 path.push((child.group_id, child.instance_index));
2263 visit(child, path, once, ctx, counts);
2264 path.pop();
2265 }
2266 }
2267 let mut path = vec![(transaction.group_id, transaction_index)];
2268 visit(transaction, &mut path, once, ctx, &mut counts);
2269 for ((path, _), (count, status)) in counts {
2270 if count > 1 {
2271 report.add_issue(
2272 ValidationIssue::new(
2273 Severity::Error,
2274 IssueKind::RepeatedInTransaction {
2275 path: path.clone(),
2276 count,
2277 },
2278 )
2279 .with_field_path(path)
2280 .with_rule(status)
2281 .with_instance_index(transaction_index),
2282 );
2283 }
2284 }
2285}
2286
2287#[allow(clippy::too_many_arguments)]
2294fn refuse_segments<E: ConditionEvaluator>(
2295 fields: &[AhbNode],
2296 group_status: Option<&str>,
2297 path: &[(&str, usize)],
2298 scoped_ctx: &EvaluationContext,
2299 evaluator: &E,
2300 ub_definitions: &BTreeMap<String, ConditionExpr>,
2301 instance_index: usize,
2302 report: &mut ValidationReport,
2303) {
2304 let mut seen: Vec<u32> = Vec::new();
2305 for node in fields {
2306 let rule = node.rule;
2307 let (Some(status), Some(value), Some(number)) = (
2308 rule.segment_ahb_status.as_deref(),
2309 node.value,
2310 node.matched_segment_number,
2311 ) else {
2312 continue;
2313 };
2314 let qualifier =
2317 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
2318 if !qualifier
2319 || seen.contains(&number)
2320 || Some(status) == group_status
2321 || !rule.codes.iter().any(|c| c.value == value)
2322 {
2323 continue;
2324 }
2325 seen.push(number);
2326 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2327 continue;
2328 }
2329 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2330 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
2331 let failed = ConditionExprEvaluator::new(evaluator).false_ids_with_ub(
2332 status,
2333 scoped_ctx,
2334 ub_definitions,
2335 );
2336 report.add_issue(
2337 ValidationIssue::new(
2338 Severity::Warning,
2339 IssueKind::SegmentNotAllowed {
2340 segment: segment.join("/"),
2341 failed,
2342 },
2343 )
2344 .with_field_path(segment.join("/"))
2345 .with_rule(status)
2346 .with_instance_index(instance_index)
2347 .with_position(crate::SegmentPosition::at(number)),
2348 );
2349 }
2350}
2351
2352#[allow(clippy::too_many_arguments)]
2363fn report_exclusive_alternatives<E: ConditionEvaluator>(
2364 fields: &[AhbNode],
2365 group_status: Option<&str>,
2366 children: &[AhbGroupNode],
2367 path: &[(&str, usize)],
2368 scoped_ctx: &EvaluationContext,
2369 evaluator: &E,
2370 ub_definitions: &BTreeMap<String, ConditionExpr>,
2371 instance_index: usize,
2372 report: &mut ValidationReport,
2373) {
2374 let pairs =
2375 crate::eval::exclusive_conditions(evaluator.format_version(), evaluator.message_type());
2376 if pairs.is_empty() {
2377 return;
2378 }
2379 let mut refused: Vec<(String, Vec<u32>, Option<u32>)> = Vec::new();
2382 let mut segments = ValidationReport::new("", crate::ValidationLevel::Full);
2383 refuse_segments(
2384 fields,
2385 group_status,
2386 path,
2387 scoped_ctx,
2388 evaluator,
2389 ub_definitions,
2390 instance_index,
2391 &mut segments,
2392 );
2393 for issue in segments.issues {
2394 if let IssueKind::SegmentNotAllowed { segment, failed } = issue.kind {
2395 let number = issue.segment_position.map(|p| p.segment_number);
2396 refused.push((segment, failed, number));
2397 }
2398 }
2399 for child in children {
2400 let Some(status) = child.ahb_status else {
2401 continue;
2402 };
2403 let mut child_path = path.to_vec();
2404 child_path.push((child.group_id, child.instance_index));
2405 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2406 if !crate::eval::refuses_presence(status, evaluator, &child_ctx, ub_definitions) {
2407 continue;
2408 }
2409 let failed = ConditionExprEvaluator::new(evaluator).false_ids_with_ub(
2410 status,
2411 &child_ctx,
2412 ub_definitions,
2413 );
2414 let name: Vec<&str> = child_path.iter().map(|(g, _)| *g).collect();
2415 let number = child.fields.iter().find_map(|n| n.matched_segment_number);
2416 refused.push((name.join("/"), failed, number));
2417 }
2418 for &(x, y) in pairs {
2419 let first = refused
2420 .iter()
2421 .position(|(_, failed, _)| failed.contains(&x));
2422 let second = refused
2423 .iter()
2424 .position(|(_, failed, _)| failed.contains(&y));
2425 let (Some(a), Some(b)) = (first, second) else {
2426 continue;
2427 };
2428 if a == b {
2429 continue;
2430 }
2431 let (a, b) = if refused[b].2 < refused[a].2 {
2432 (b, a)
2433 } else {
2434 (a, b)
2435 };
2436 let mut issue = ValidationIssue::new(
2437 Severity::Error,
2438 IssueKind::ExclusiveAlternatives {
2439 first: refused[a].0.clone(),
2440 second: refused[b].0.clone(),
2441 conditions: vec![x, y],
2442 },
2443 )
2444 .with_field_path(refused[b].0.clone())
2445 .with_instance_index(instance_index);
2446 if let Some(number) = refused[b].2 {
2447 issue = issue.with_position(crate::SegmentPosition::at(number));
2448 }
2449 report.add_issue(issue);
2450 }
2451}
2452
2453fn report_segment_counts<E: ConditionEvaluator>(
2460 group: &AhbGroupNode,
2461 path: &[(&str, usize)],
2462 scoped_ctx: &EvaluationContext,
2463 evaluator: &E,
2464 instance_index: usize,
2465 report: &mut ValidationReport,
2466) {
2467 if crate::eval::repetition_limits(evaluator.message_type()).is_empty() {
2468 return;
2469 }
2470 let mut seen: HashSet<Option<&str>> = HashSet::new();
2471 for node in &group.fields {
2472 let rule = node.rule;
2473 let Some(status) = rule.segment_ahb_status.as_deref() else {
2474 continue;
2475 };
2476 if rule.element_index.unwrap_or(0) != 0
2477 || rule.component_index.unwrap_or(0) != 0
2478 || rule.codes.is_empty()
2479 || !seen.insert(rule.mig_number.as_deref())
2480 {
2481 continue;
2482 }
2483 let tag = extract_segment_id(&rule.segment_path);
2484 let sent: Vec<_> = scoped_ctx
2485 .find_segments_at(path, &tag)
2486 .into_iter()
2487 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2488 .collect();
2489 if sent.is_empty()
2490 || !crate::eval::breaks_repetition_limit(status, evaluator, scoped_ctx, sent.len())
2491 {
2492 continue;
2493 }
2494 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2495 segment.push(&tag);
2496 let mut issue = ValidationIssue::new(
2497 Severity::Error,
2498 IssueKind::RepetitionCount {
2499 path: segment.join("/"),
2500 count: sent.len(),
2501 },
2502 )
2503 .with_field_path(segment.join("/"))
2504 .with_rule(status)
2505 .with_instance_index(instance_index);
2506 if let Some(num) = node.matched_segment_number {
2507 issue = issue.with_position(crate::SegmentPosition::at(num));
2508 }
2509 report.add_issue(issue);
2510 }
2511}
2512
2513fn report_off_count_per_id<E: ConditionEvaluator>(
2522 transaction: &AhbGroupNode,
2523 tx: usize,
2524 ctx: &EvaluationContext,
2525 evaluator: &E,
2526 ub_definitions: &BTreeMap<String, ConditionExpr>,
2527 report: &mut ValidationReport,
2528) {
2529 let mut judged: Vec<(&str, Vec<&str>)> = Vec::new();
2530 for child in &transaction.children {
2531 let Some(status) = child.ahb_status else {
2532 continue;
2533 };
2534 let per_package = status.contains("[2002]");
2535 if child.group_id != "SG8"
2536 || !(per_package || status.contains("[2004]") || status.contains("[2005]"))
2537 || crate::eval::refuses_presence(status, evaluator, ctx, ub_definitions)
2538 {
2539 continue;
2540 }
2541 let Some(codes) = child.fields.iter().find_map(|node| {
2542 let rule = node.rule;
2543 (extract_segment_id(&rule.segment_path) == "SEQ"
2544 && rule.element_index.unwrap_or(0) == 0
2545 && rule.component_index.unwrap_or(0) == 0
2546 && !rule.codes.is_empty())
2547 .then(|| {
2548 rule.codes
2549 .iter()
2550 .map(|c| c.value.as_str())
2551 .collect::<Vec<_>>()
2552 })
2553 }) else {
2554 continue;
2555 };
2556 if judged.iter().any(|(s, c)| *s == status && *c == codes) {
2557 continue;
2558 }
2559 let position = child.fields.iter().find_map(|n| n.matched_segment_number);
2560 let (off, label) = if per_package {
2561 let off =
2562 crate::eval::sg4_instances::sg8_off_count_per_produktpaket(ctx, "SG4", tx, &codes);
2563 (off, "Produktpaket-ID")
2564 } else {
2565 let off =
2566 crate::eval::periods::sg8_off_count_per_zeitraum(ctx, "SG4", tx, &codes, status);
2567 (off, "Zeitraum-ID")
2568 };
2569 for (id, count) in off {
2570 let mut issue = ValidationIssue::new(
2571 Severity::Error,
2572 IssueKind::RepetitionCount {
2573 path: "SG4/SG8".to_string(),
2574 count,
2575 },
2576 )
2577 .with_field_path("SG4/SG8")
2578 .with_rule(status)
2579 .with_instance_index(tx);
2580 issue.expected_value = Some(format!("{label} {id}"));
2581 if let Some(num) = position {
2582 issue = issue.with_position(crate::SegmentPosition::at(num));
2583 }
2584 report.add_issue(issue);
2585 }
2586 judged.push((status, codes));
2587 }
2588}
2589
2590fn report_repeated_in_message(
2597 groups: &[AhbGroupNode],
2598 message_type: &str,
2599 report: &mut ValidationReport,
2600) {
2601 for &(condition, group_id, kind) in crate::eval::message_repetition_limits(message_type) {
2602 let tag = format!("[{condition}]");
2603 let mut index: HashMap<&str, usize> = HashMap::new();
2604 let counted: Vec<(usize, &AhbGroupNode)> = groups
2605 .iter()
2606 .filter_map(|g| {
2607 let i = *index.entry(g.group_id).and_modify(|c| *c += 1).or_insert(0);
2608 (g.group_id == group_id).then_some((i, g))
2609 })
2610 .filter(|(_, g)| g.ahb_status.is_some_and(|s| s.contains(&tag)))
2611 .filter(|(_, g)| {
2612 kind.map_or(true, |code| {
2613 g.fields.iter().any(|n| {
2614 extract_segment_id(&n.rule.segment_path) == "LIN"
2615 && n.rule.element_index == Some(1)
2616 && n.value == Some(code)
2617 })
2618 })
2619 })
2620 .collect();
2621 if counted.len() < 2 {
2622 continue;
2623 }
2624 let (instance_index, extra) = counted[1];
2625 let status = extra.ahb_status.unwrap_or_default();
2626 let mut issue = ValidationIssue::new(
2627 Severity::Error,
2628 IssueKind::RepetitionCount {
2629 path: group_id.to_string(),
2630 count: counted.len(),
2631 },
2632 )
2633 .with_field_path(group_id)
2634 .with_rule(status)
2635 .with_instance_index(instance_index)
2636 .with_mig_number(extra.fields.first().and_then(|n| n.rule.mig_number.as_deref()));
2638 if let Some(num) = extra.fields.iter().find_map(|n| n.matched_segment_number) {
2639 issue = issue.with_position(crate::SegmentPosition::at(num));
2640 }
2641 report.add_issue(issue);
2642 }
2643}
2644
2645#[allow(clippy::too_many_arguments)]
2651fn report_repeated_in_group<E: ConditionEvaluator>(
2652 group: &AhbGroupNode,
2653 path: &[(&str, usize)],
2654 scoped_ctx: &EvaluationContext,
2655 evaluator: &E,
2656 ub_definitions: &BTreeMap<String, ConditionExpr>,
2657 instance_index: usize,
2658 report: &mut ValidationReport,
2659) {
2660 if crate::eval::counted_repetitions(evaluator.message_type()).is_empty() {
2661 return;
2662 }
2663 let mut reported: HashSet<&str> = HashSet::new();
2664 for child in &group.children {
2665 let Some(status) = child.ahb_status else {
2666 continue;
2667 };
2668 let count = group
2669 .children
2670 .iter()
2671 .filter(|c| c.group_id == child.group_id)
2672 .count();
2673 if count < 2 || reported.contains(child.group_id) {
2676 continue;
2677 }
2678 let mut child_path = path.to_vec();
2679 child_path.push((child.group_id, child.instance_index));
2680 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2681 if !crate::eval::refuses_count(status, evaluator, &child_ctx, ub_definitions) {
2682 continue;
2683 }
2684 reported.insert(child.group_id);
2685 let group_path: Vec<&str> = child_path.iter().map(|(g, _)| *g).collect();
2686 let mut issue = ValidationIssue::new(
2687 Severity::Error,
2688 IssueKind::RepetitionCount {
2689 path: group_path.join("/"),
2690 count,
2691 },
2692 )
2693 .with_field_path(group_path.join("/"))
2694 .with_rule(status)
2695 .with_instance_index(instance_index);
2696 if let Some(num) = child.fields.iter().find_map(|n| n.matched_segment_number) {
2697 issue = issue.with_position(crate::SegmentPosition::at(num));
2698 }
2699 report.add_issue(issue);
2700 }
2701}
2702
2703#[allow(clippy::too_many_arguments)]
2712fn walk_group_instance<E: ConditionEvaluator>(
2713 group: &AhbGroupNode,
2714 path: &[(&str, usize)],
2715 scoped_ctx: &EvaluationContext,
2716 expr_eval: &ConditionExprEvaluator<E>,
2717 evaluator: &E,
2718 ub_definitions: &BTreeMap<String, ConditionExpr>,
2719 tag_migs: &HashMap<String, HashSet<&str>>,
2720 instance_index: usize,
2721 report_segments: bool,
2722 statuses_only: bool,
2723 report: &mut ValidationReport,
2724) {
2725 let mut group_refused = false;
2728 if let Some(status) = group.ahb_status {
2729 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2730 group_refused = true;
2731 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2732 let failed = ConditionExprEvaluator::new(evaluator).false_ids_with_ub(
2733 status,
2734 scoped_ctx,
2735 ub_definitions,
2736 );
2737 let mut issue = ValidationIssue::new(
2738 Severity::Warning,
2739 IssueKind::GroupNotAllowed {
2740 group: group_path.join("/"),
2741 failed,
2742 },
2743 )
2744 .with_field_path(group_path.join("/"))
2745 .with_rule(status)
2746 .with_instance_index(instance_index);
2747 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
2748 issue = issue.with_position(crate::SegmentPosition::at(num));
2749 }
2750 report.add_issue(issue);
2751 }
2752 }
2753 if report_segments && !group_refused {
2754 refuse_segments(
2755 &group.fields,
2756 group.ahb_status,
2757 path,
2758 scoped_ctx,
2759 evaluator,
2760 ub_definitions,
2761 instance_index,
2762 report,
2763 );
2764 }
2765 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
2769 .fields
2770 .iter()
2771 .filter(|n| {
2772 n.rule.element_index.unwrap_or(0) == 0
2773 && n.rule.component_index.unwrap_or(0) == 0
2774 && !n.rule.codes.is_empty()
2775 })
2776 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
2777 .collect();
2778 let holds = |node: &AhbNode| {
2779 let (Some(codes), Some(elements)) = (
2780 slot_codes.get(&node.rule.mig_number.as_deref()),
2781 node.segment_elements,
2782 ) else {
2783 return true;
2784 };
2785 let qualifier = elements
2786 .first()
2787 .and_then(|e| e.first())
2788 .map(String::as_str)
2789 .unwrap_or_default();
2790 codes.iter().any(|c| c.value == qualifier)
2791 };
2792 for node in &group.fields {
2793 evaluate_node(
2794 node,
2795 scoped_ctx,
2796 expr_eval,
2797 evaluator,
2798 ub_definitions,
2799 tag_migs,
2800 Some(instance_index),
2801 holds(node),
2802 statuses_only,
2803 report,
2804 );
2805 }
2806 if !statuses_only && !group_refused {
2807 report_exclusive_alternatives(
2808 &group.fields,
2809 group.ahb_status,
2810 &group.children,
2811 path,
2812 scoped_ctx,
2813 evaluator,
2814 ub_definitions,
2815 instance_index,
2816 report,
2817 );
2818 }
2819 if !statuses_only {
2820 report_repeated_in_group(
2821 group,
2822 path,
2823 scoped_ctx,
2824 evaluator,
2825 ub_definitions,
2826 instance_index,
2827 report,
2828 );
2829 report_segment_counts(group, path, scoped_ctx, evaluator, instance_index, report);
2830 }
2831 for child in &group.children {
2832 let mut child_path = path.to_vec();
2833 child_path.push((child.group_id, child.instance_index));
2834 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2835 walk_group_instance(
2836 child,
2837 &child_path,
2838 &child_ctx,
2839 expr_eval,
2840 evaluator,
2841 ub_definitions,
2842 tag_migs,
2843 instance_index,
2844 report_segments,
2845 statuses_only,
2846 report,
2847 );
2848 }
2849}
2850
2851#[allow(clippy::too_many_arguments)]
2861fn report_absent_children<E: ConditionEvaluator>(
2862 group: &AhbGroupNode,
2863 path: &[(&str, usize)],
2864 scoped_ctx: &EvaluationContext,
2865 absent_eval: &ConditionExprEvaluator<crate::eval::AbsentTarget<'_, E>>,
2866 evaluator: &E,
2867 ub_definitions: &BTreeMap<String, ConditionExpr>,
2868 instance_index: usize,
2869 report: &mut ValidationReport,
2870) {
2871 if group.ahb_status.is_some_and(|status| {
2872 crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions)
2873 }) {
2874 return;
2875 }
2876 for rule in &group.absent_children {
2877 let rule: &AhbFieldRule = rule;
2883 let own;
2884 let rule = match &rule.group_ahb_status {
2885 Some(status) => {
2886 own = AhbFieldRule {
2887 parent_group_ahb_status: Some(status.clone()),
2888 ..rule.clone()
2889 };
2890 &own
2891 }
2892 None => rule,
2893 };
2894 let child_id = rule
2900 .segment_path
2901 .split('/')
2902 .nth(path.len())
2903 .filter(|id| id.starts_with("SG"));
2904 let mut child_path = path.to_vec();
2905 if let Some(id) = child_id {
2906 let sent = group.children.iter().filter(|c| c.group_id == id).count();
2907 child_path.push((id, sent));
2908 }
2909 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2910 let scoped_ctx = &child_ctx;
2911 if should_skip_for_parent_group(rule, absent_eval, scoped_ctx, ub_definitions)
2912 || !is_parent_group_required(rule, absent_eval, scoped_ctx, ub_definitions)
2913 {
2914 continue;
2915 }
2916 let required = |status: &str| {
2917 let resolved = absent_eval.resolve_status(status, scoped_ctx, ub_definitions);
2918 resolved.result == ConditionResult::True && resolved.kind == Some(StatusKind::Muss)
2919 };
2920 let package_choice = |status: &str| {
2924 ConditionParser::parse_status_branches(status, ub_definitions).is_ok_and(|branches| {
2925 matches!(branches.as_slice(), [branch]
2926 if branch.kind == Some(StatusKind::Muss)
2927 && branch
2928 .condition
2929 .as_ref()
2930 .is_some_and(crate::eval::packages_allow_none))
2931 })
2932 };
2933 let segment_required = match rule.segment_ahb_status.as_deref() {
2934 Some(status) => required(status),
2935 None => true,
2936 };
2937 if !segment_required
2938 || !(required(&rule.ahb_status)
2939 || (rule.segment_ahb_status.is_some() && package_choice(&rule.ahb_status)))
2940 {
2941 continue;
2942 }
2943 let mut issue = ValidationIssue::new(
2944 Severity::Error,
2945 IssueKind::MissingRequiredField {
2946 field_name: rule.name.clone(),
2947 },
2948 )
2949 .with_field_path(&rule.segment_path)
2950 .with_rule(&rule.ahb_status)
2951 .with_instance_index(instance_index);
2952 if let Some(first_code) = rule.codes.first() {
2953 issue.expected_value = Some(first_code.value.clone());
2954 }
2955 report.add_issue(attach_field_position(issue, rule));
2956 }
2957 for child in &group.children {
2958 let mut child_path = path.to_vec();
2959 child_path.push((child.group_id, child.instance_index));
2960 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2961 report_absent_children(
2962 child,
2963 &child_path,
2964 &child_ctx,
2965 absent_eval,
2966 evaluator,
2967 ub_definitions,
2968 instance_index,
2969 report,
2970 );
2971 }
2972}
2973
2974fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2975 match expr {
2976 ConditionExpr::Package { id, min, max } => {
2977 out.push((*id, *min, *max));
2978 }
2979 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2980 for e in exprs {
2981 collect_packages(e, out);
2982 }
2983 }
2984 ConditionExpr::Xor(left, right) => {
2985 collect_packages(left, out);
2986 collect_packages(right, out);
2987 }
2988 ConditionExpr::Not(inner) => {
2989 collect_packages(inner, out);
2990 }
2991 ConditionExpr::Ref(_) => {}
2992 }
2993}
2994
2995fn is_optional_segment_status(status: &str) -> bool {
3005 let trimmed = status.trim();
3006 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
3007}
3008
3009fn is_qualifier_field(path: &str) -> bool {
3019 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
3020 matches!(parts.len(), 2 | 3)
3022}
3023
3024#[derive(Default)]
3032struct MigCodeBucket<'a> {
3033 qualifier_position: Option<(usize, usize)>,
3036 qualifier_values: HashSet<&'a str>,
3039 codes: BTreeMap<(usize, usize), HashSet<&'a str>>,
3042}
3043
3044fn partition_codes_by_mig(
3049 workflow: &AhbWorkflow,
3050) -> BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket<'_>>> {
3051 let mut out: BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket>> =
3052 BTreeMap::new();
3053 for field in &workflow.fields {
3054 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
3055 continue;
3056 }
3057 if codes_are_alternatives(field) {
3060 continue;
3061 }
3062 let tag = extract_segment_id(&field.segment_path);
3063 let group_key = extract_group_path_key(&field.segment_path);
3064 let mig = field.mig_number.clone();
3065 let el = field.element_index.unwrap_or(0);
3066 let c = field.component_index.unwrap_or(0);
3067
3068 let bucket = out
3069 .entry((group_key, tag))
3070 .or_default()
3071 .entry(mig)
3072 .or_default();
3073
3074 let required: Vec<&str> = field
3081 .codes
3082 .iter()
3083 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
3084 .map(|code| code.value.as_str())
3085 .collect();
3086
3087 if !required.is_empty() {
3094 if bucket.qualifier_position.is_none() {
3095 bucket.qualifier_position = Some((el, c));
3096 }
3097 if bucket.qualifier_position == Some((el, c)) {
3098 bucket.qualifier_values.extend(required.iter().copied());
3099 }
3100 }
3101
3102 for v in required {
3103 bucket.codes.entry((el, c)).or_default().insert(v);
3104 }
3105 }
3106 out
3107}
3108
3109fn global_qualifiers_by_tag(
3115 by_loc: &BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket<'_>>>,
3116) -> BTreeMap<String, BTreeMap<(usize, usize), HashSet<String>>> {
3117 let mut out: BTreeMap<String, BTreeMap<(usize, usize), HashSet<String>>> = BTreeMap::new();
3118 for ((_group, tag), migs) in by_loc {
3119 let tag_entry = out.entry(tag.clone()).or_default();
3120 for bucket in migs.values() {
3121 if let Some(pos) = bucket.qualifier_position {
3122 tag_entry
3123 .entry(pos)
3124 .or_default()
3125 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
3126 }
3127 }
3128 }
3129 out
3130}
3131
3132fn matches_qualifier(seg: &OwnedSegment, bucket: &MigCodeBucket<'_>) -> bool {
3134 bucket.qualifier_position.is_some_and(|(el, c)| {
3135 seg.elements
3136 .get(el)
3137 .and_then(|e| e.get(c))
3138 .is_some_and(|v| bucket.qualifier_values.contains(v.as_str()))
3139 })
3140}
3141
3142fn data_slot_entries(
3148 workflow: &AhbWorkflow,
3149) -> HashMap<(String, String), Vec<(String, Option<String>)>> {
3150 let mut out: HashMap<(String, String), Vec<(String, Option<String>)>> = HashMap::new();
3151 let mut tags_by_mig: HashMap<String, BTreeMap<&str, Vec<String>>> = HashMap::new();
3156 for f in &workflow.fields {
3157 if let Some(n) = f.mig_number.as_deref() {
3158 let tags = tags_by_mig
3159 .entry(extract_group_path_key(&f.segment_path))
3160 .or_default()
3161 .entry(n)
3162 .or_default();
3163 let tag = extract_segment_id(&f.segment_path);
3164 if tags.last() != Some(&tag) {
3165 tags.push(tag);
3166 }
3167 }
3168 }
3169 let mut judged: HashSet<(&str, &str)> = HashSet::new();
3170 for field in &workflow.fields {
3171 let leading =
3172 field.element_index.unwrap_or(0) == 0 && field.component_index.unwrap_or(0) == 0;
3173 let Some(mig) = field.mig_number.as_deref() else {
3174 continue;
3175 };
3176 if !field.codes.is_empty() || !leading || !is_qualifier_field(&field.segment_path) {
3177 continue;
3178 }
3179 if !judged.insert((field.segment_path.as_str(), mig)) {
3180 continue;
3181 }
3182 let tag = extract_segment_id(&field.segment_path);
3183 let group_key = extract_group_path_key(&field.segment_path);
3184 if group_key.is_empty() {
3185 continue;
3186 }
3187 let entry = tags_by_mig.get(&group_key).and_then(|migs| {
3190 migs.range(..mig).rev().find_map(|(n, tags)| {
3191 tags.iter()
3192 .rev()
3193 .find(|t| **t != tag)
3194 .map(|t| (*n, t.clone()))
3195 })
3196 });
3197 if let Some((n, entry_tag)) = entry {
3198 let slot = (entry_tag, Some(n.to_string()));
3199 let entries = out.entry((group_key, tag)).or_default();
3200 if !entries.contains(&slot) {
3201 entries.push(slot);
3202 }
3203 }
3204 }
3205 out
3206}
3207
3208fn match_segment_to_mig<'a, 'b>(
3219 seg: &OwnedSegment,
3220 migs: &'a BTreeMap<Option<String>, MigCodeBucket<'b>>,
3221) -> Option<&'a MigCodeBucket<'b>> {
3222 let actual_at = |el: usize, c: usize| -> &str {
3223 seg.elements
3224 .get(el)
3225 .and_then(|e| e.get(c))
3226 .map(|s| s.as_str())
3227 .unwrap_or("")
3228 };
3229
3230 let mut best: Option<&MigCodeBucket> = None;
3231 let mut best_matches = 0usize;
3232
3233 for bucket in migs.values() {
3234 let Some((el, c)) = bucket.qualifier_position else {
3235 continue;
3236 };
3237 if !bucket.qualifier_values.contains(actual_at(el, c)) {
3238 continue;
3239 }
3240 let extra_matches = bucket
3241 .codes
3242 .iter()
3243 .filter(|(pos, _)| **pos != (el, c))
3244 .filter(|((e, k), allowed)| {
3245 let v = actual_at(*e, *k);
3246 !v.is_empty() && allowed.contains(v)
3247 })
3248 .count();
3249 if best.is_none() || extra_matches > best_matches {
3250 best = Some(bucket);
3251 best_matches = extra_matches;
3252 }
3253 }
3254 best
3255}
3256
3257fn extract_group_path_key(path: &str) -> String {
3262 let sg_parts: Vec<&str> = path
3263 .split('/')
3264 .take_while(|p| p.starts_with("SG"))
3265 .collect();
3266 sg_parts.join("/")
3267}
3268
3269fn extract_segment_id(path: &str) -> String {
3271 for part in path.split('/') {
3272 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
3274 continue;
3275 }
3276 if part.len() >= 3
3278 && part
3279 .chars()
3280 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
3281 {
3282 return part.to_string();
3283 }
3284 }
3285 path.split('/').next_back().unwrap_or(path).to_string()
3287}
3288
3289pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
3301 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
3303 if unh_count > 1 {
3304 return Some(ValidationIssue::new(
3305 Severity::Error,
3306 IssueKind::UntCountNotVerifiable { unh_count },
3307 ));
3308 }
3309
3310 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
3312 let declared: usize = unt.get_element(0).parse().ok()?;
3313
3314 let actual = segments
3316 .iter()
3317 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
3318 .count();
3319
3320 if declared != actual {
3321 Some(
3322 ValidationIssue::new(
3323 Severity::Error,
3324 IssueKind::UntSegmentCountMismatch { declared, actual },
3325 )
3326 .with_field_path("UNT/0074")
3327 .with_expected(actual.to_string())
3328 .with_actual(declared.to_string()),
3329 )
3330 } else {
3331 None
3332 }
3333}
3334
3335#[cfg(test)]
3336mod tests {
3337
3338 #[test]
3339 fn a_numeric_length_counts_digits_only() {
3340 assert_eq!(value_length("n..15", "-12.50"), 4);
3341 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
3342 assert_eq!(value_length("n5", "55001"), 5);
3343 assert_eq!(value_length("an..3", "Z.1"), 3);
3344 assert_eq!(value_length("an..35", "Müller"), 6);
3345 }
3346 use super::super::codes::ErrorCodes;
3347 use super::super::issue::ValidationCategory;
3348 use super::*;
3349 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
3350 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
3351 use std::collections::HashMap;
3352
3353 fn narrate(issue: &ValidationIssue) -> String {
3356 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
3357 }
3358
3359 struct MockEvaluator {
3361 results: HashMap<u32, CR>,
3362 }
3363
3364 impl MockEvaluator {
3365 fn new(results: Vec<(u32, CR)>) -> Self {
3366 Self {
3367 results: results.into_iter().collect(),
3368 }
3369 }
3370
3371 fn all_true(ids: &[u32]) -> Self {
3372 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
3373 }
3374 }
3375
3376 impl ConditionEvaluator for MockEvaluator {
3377 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
3378 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
3379 }
3380 fn is_external(&self, _condition: u32) -> bool {
3381 false
3382 }
3383 fn message_type(&self) -> &str {
3384 "UTILMD"
3385 }
3386 fn format_version(&self) -> &str {
3387 "FV2510"
3388 }
3389 }
3390
3391 #[test]
3394 fn a_present_code_is_judged_by_its_own_status() {
3395 let code = |value: &str, status: &str| AhbCodeRule {
3397 value: value.to_string(),
3398 description: String::new(),
3399 ahb_status: status.to_string(),
3400 };
3401 let field = AhbFieldRule {
3402 segment_path: "SG4/STS/C556/9013".to_string(),
3403 ahb_status: "X [192]".to_string(),
3404 codes: vec![code("E01", "X [192]"), code("E03", "X")],
3405 ..Default::default()
3406 };
3407 assert_eq!(governing_status(&field, Some("E03")), "X");
3408 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
3409 assert_eq!(governing_status(&field, None), "X [192]");
3410 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
3411
3412 let own = AhbFieldRule {
3414 ahb_status: "X [931]".to_string(),
3415 codes: vec![code("303", "X")],
3416 ..Default::default()
3417 };
3418 assert_eq!(governing_status(&own, Some("303")), "X [931]");
3419 assert!(!codes_are_alternatives(&own));
3420
3421 let alternatives = AhbFieldRule {
3424 ahb_status: "X [153] ∧ [538]".to_string(),
3425 codes: vec![
3426 code("Z74", "X [103] ∧ [537]"),
3427 code("Z75", "X [103] ∧ [537]"),
3428 ],
3429 ..Default::default()
3430 };
3431 assert!(codes_are_alternatives(&alternatives));
3432 assert_eq!(
3433 governing_status(&alternatives, Some("Z74")),
3434 "X [103] ∧ [537]"
3435 );
3436 assert_eq!(
3437 governing_status(&alternatives, Some("A01")),
3438 "X [153] ∧ [538]"
3439 );
3440 assert_eq!(governing_status(&alternatives, None), "X [153] ∧ [538]");
3441
3442 let restricting = AhbFieldRule {
3444 ahb_status: "X [2] ∨ [3]".to_string(),
3445 codes: vec![code("UNH", "X"), code("UNT", "X")],
3446 ..Default::default()
3447 };
3448 assert!(!codes_are_alternatives(&restricting));
3449 assert_eq!(governing_status(&restricting, Some("UNH")), "X [2] ∨ [3]");
3450 }
3451
3452 #[test]
3453 fn test_extract_segment_id_simple() {
3454 assert_eq!(extract_segment_id("NAD"), "NAD");
3455 }
3456
3457 #[test]
3458 fn test_extract_segment_id_with_sg_prefix() {
3459 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
3460 }
3461
3462 #[test]
3463 fn test_extract_segment_id_nested_sg() {
3464 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
3465 }
3466
3467 #[test]
3470 fn test_validate_missing_mandatory_field() {
3471 let evaluator = MockEvaluator::all_true(&[182, 152]);
3472 let validator = EdifactValidator::new(evaluator);
3473 let external = NoOpExternalProvider;
3474
3475 let workflow = AhbWorkflow {
3476 pruefidentifikator: "11001".to_string(),
3477 description: "Test".to_string(),
3478 communication_direction: None,
3479 fields: vec![AhbFieldRule {
3480 segment_path: "SG2/NAD/C082/3039".to_string(),
3481 name: "MP-ID des MSB".to_string(),
3482 ahb_status: "Muss [182] ∧ [152]".to_string(),
3483 codes: vec![],
3484 parent_group_ahb_status: None,
3485 segment_ahb_status: None,
3486 ..Default::default()
3487 }],
3488 ub_definitions: BTreeMap::new(),
3489 package_conditions: Default::default(),
3490 };
3491
3492 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3494
3495 assert!(!report.is_valid());
3497 let errors: Vec<_> = report.errors().collect();
3498 assert_eq!(errors.len(), 1);
3499 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3500 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
3501 }
3502
3503 #[test]
3504 fn test_validate_condition_false_no_error() {
3505 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
3507 let validator = EdifactValidator::new(evaluator);
3508 let external = NoOpExternalProvider;
3509
3510 let workflow = AhbWorkflow {
3511 pruefidentifikator: "11001".to_string(),
3512 description: "Test".to_string(),
3513 communication_direction: None,
3514 fields: vec![AhbFieldRule {
3515 segment_path: "NAD".to_string(),
3516 name: "Partnerrolle".to_string(),
3517 ahb_status: "Muss [182] ∧ [152]".to_string(),
3518 codes: vec![],
3519 parent_group_ahb_status: None,
3520 segment_ahb_status: None,
3521 ..Default::default()
3522 }],
3523 ub_definitions: BTreeMap::new(),
3524 package_conditions: Default::default(),
3525 };
3526
3527 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3528
3529 assert!(report.is_valid());
3531 }
3532
3533 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
3535 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3536 let workflow = AhbWorkflow {
3537 pruefidentifikator: "11001".to_string(),
3538 description: "Test".to_string(),
3539 communication_direction: None,
3540 fields: vec![AhbFieldRule {
3541 segment_path: "SG2/NAD/C082/3039".to_string(),
3542 name: "MP-ID".to_string(),
3543 ahb_status: status.to_string(),
3544 ..Default::default()
3545 }],
3546 ub_definitions: BTreeMap::new(),
3547 package_conditions: Default::default(),
3548 };
3549 validator.validate(
3550 &[],
3551 &workflow,
3552 &NoOpExternalProvider,
3553 ValidationLevel::Conditions,
3554 )
3555 }
3556
3557 #[test]
3558 fn a_later_soll_line_alone_does_not_require_the_field() {
3559 let report = validate_absent_field(
3561 "Muss [2119] \r\nSoll [130]",
3562 vec![(2119, CR::False), (130, CR::True)],
3563 );
3564 assert!(
3565 report.is_valid(),
3566 "{:?}",
3567 report.errors().collect::<Vec<_>>()
3568 );
3569 }
3570
3571 #[test]
3572 fn a_later_muss_line_requires_the_field() {
3573 let report = validate_absent_field(
3575 "Soll [47]\r\nMuss [46]",
3576 vec![(47, CR::False), (46, CR::True)],
3577 );
3578 let errors: Vec<_> = report.errors().collect();
3579 assert_eq!(errors.len(), 1);
3580 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3581 }
3582
3583 #[test]
3584 fn a_weaker_unknown_line_does_not_stop_a_muss() {
3585 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
3587 let errors: Vec<_> = report.errors().collect();
3588 assert_eq!(errors.len(), 1);
3589 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3590 }
3591
3592 #[test]
3593 fn a_stricter_unknown_line_leaves_the_status_undecided() {
3594 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
3595 assert!(report.is_valid());
3596 let infos: Vec<_> = report.infos().collect();
3597 assert_eq!(infos.len(), 1, "{infos:?}");
3598 match &infos[0].kind {
3599 IssueKind::ConditionUnknown { unresolved, .. } => {
3600 assert_eq!(unresolved.missing, vec![2119]);
3601 }
3602 other => panic!("expected ConditionUnknown, got {other:?}"),
3603 }
3604 }
3605
3606 #[test]
3607 fn test_validate_condition_unknown_adds_info() {
3608 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
3610 let validator = EdifactValidator::new(evaluator);
3612 let external = NoOpExternalProvider;
3613
3614 let workflow = AhbWorkflow {
3615 pruefidentifikator: "11001".to_string(),
3616 description: "Test".to_string(),
3617 communication_direction: None,
3618 fields: vec![AhbFieldRule {
3619 segment_path: "NAD".to_string(),
3620 name: "Partnerrolle".to_string(),
3621 ahb_status: "Muss [182] ∧ [152]".to_string(),
3622 codes: vec![],
3623 parent_group_ahb_status: None,
3624 segment_ahb_status: None,
3625 ..Default::default()
3626 }],
3627 ub_definitions: BTreeMap::new(),
3628 package_conditions: Default::default(),
3629 };
3630
3631 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3632
3633 assert!(report.is_valid());
3635 let infos: Vec<_> = report.infos().collect();
3636 assert_eq!(infos.len(), 1);
3637 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
3638 }
3639
3640 #[test]
3641 fn test_validate_structure_level_skips_conditions() {
3642 let evaluator = MockEvaluator::all_true(&[182, 152]);
3643 let validator = EdifactValidator::new(evaluator);
3644 let external = NoOpExternalProvider;
3645
3646 let workflow = AhbWorkflow {
3647 pruefidentifikator: "11001".to_string(),
3648 description: "Test".to_string(),
3649 communication_direction: None,
3650 fields: vec![AhbFieldRule {
3651 segment_path: "NAD".to_string(),
3652 name: "Partnerrolle".to_string(),
3653 ahb_status: "Muss [182] ∧ [152]".to_string(),
3654 codes: vec![],
3655 parent_group_ahb_status: None,
3656 segment_ahb_status: None,
3657 ..Default::default()
3658 }],
3659 ub_definitions: BTreeMap::new(),
3660 package_conditions: Default::default(),
3661 };
3662
3663 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
3665
3666 assert!(report.is_valid());
3668 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
3669 }
3670
3671 #[test]
3672 fn test_validate_empty_workflow_no_condition_errors() {
3673 let evaluator = MockEvaluator::all_true(&[]);
3674 let validator = EdifactValidator::new(evaluator);
3675 let external = NoOpExternalProvider;
3676
3677 let empty_workflow = AhbWorkflow {
3678 pruefidentifikator: String::new(),
3679 description: String::new(),
3680 communication_direction: None,
3681 fields: vec![],
3682 ub_definitions: BTreeMap::new(),
3683 package_conditions: Default::default(),
3684 };
3685
3686 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
3687
3688 assert!(report.is_valid());
3689 }
3690
3691 #[test]
3692 fn test_validate_bare_muss_always_required() {
3693 let evaluator = MockEvaluator::new(vec![]);
3694 let validator = EdifactValidator::new(evaluator);
3695 let external = NoOpExternalProvider;
3696
3697 let workflow = AhbWorkflow {
3698 pruefidentifikator: "55001".to_string(),
3699 description: "Test".to_string(),
3700 communication_direction: Some("NB an LF".to_string()),
3701 fields: vec![AhbFieldRule {
3702 segment_path: "SG2/NAD/3035".to_string(),
3703 name: "Partnerrolle".to_string(),
3704 ahb_status: "Muss".to_string(), codes: vec![],
3706 parent_group_ahb_status: None,
3707 segment_ahb_status: None,
3708 ..Default::default()
3709 }],
3710 ub_definitions: BTreeMap::new(),
3711 package_conditions: Default::default(),
3712 };
3713
3714 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3715
3716 assert!(!report.is_valid());
3718 assert_eq!(report.error_count(), 1);
3719 }
3720
3721 #[test]
3722 fn test_validate_x_status_is_mandatory() {
3723 let evaluator = MockEvaluator::new(vec![]);
3724 let validator = EdifactValidator::new(evaluator);
3725 let external = NoOpExternalProvider;
3726
3727 let workflow = AhbWorkflow {
3728 pruefidentifikator: "55001".to_string(),
3729 description: "Test".to_string(),
3730 communication_direction: None,
3731 fields: vec![AhbFieldRule {
3732 segment_path: "DTM".to_string(),
3733 name: "Datum".to_string(),
3734 ahb_status: "X".to_string(),
3735 codes: vec![],
3736 parent_group_ahb_status: None,
3737 segment_ahb_status: None,
3738 ..Default::default()
3739 }],
3740 ub_definitions: BTreeMap::new(),
3741 package_conditions: Default::default(),
3742 };
3743
3744 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3745
3746 assert!(!report.is_valid());
3747 let errors: Vec<_> = report.errors().collect();
3748 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3749 }
3750
3751 #[test]
3752 fn test_validate_soll_not_mandatory() {
3753 let evaluator = MockEvaluator::new(vec![]);
3754 let validator = EdifactValidator::new(evaluator);
3755 let external = NoOpExternalProvider;
3756
3757 let workflow = AhbWorkflow {
3758 pruefidentifikator: "55001".to_string(),
3759 description: "Test".to_string(),
3760 communication_direction: None,
3761 fields: vec![AhbFieldRule {
3762 segment_path: "DTM".to_string(),
3763 name: "Datum".to_string(),
3764 ahb_status: "Soll".to_string(),
3765 codes: vec![],
3766 parent_group_ahb_status: None,
3767 segment_ahb_status: None,
3768 ..Default::default()
3769 }],
3770 ub_definitions: BTreeMap::new(),
3771 package_conditions: Default::default(),
3772 };
3773
3774 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3775
3776 assert!(report.is_valid());
3778 }
3779
3780 #[test]
3781 fn test_report_includes_metadata() {
3782 let evaluator = MockEvaluator::new(vec![]);
3783 let validator = EdifactValidator::new(evaluator);
3784 let external = NoOpExternalProvider;
3785
3786 let workflow = AhbWorkflow {
3787 pruefidentifikator: "55001".to_string(),
3788 description: String::new(),
3789 communication_direction: None,
3790 fields: vec![],
3791 ub_definitions: BTreeMap::new(),
3792 package_conditions: Default::default(),
3793 };
3794
3795 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
3796
3797 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
3798 assert_eq!(report.level, ValidationLevel::Full);
3799 assert_eq!(report.message_type, "UTILMD");
3800 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
3801 }
3802
3803 #[test]
3804 fn test_validate_with_navigator_returns_report() {
3805 let evaluator = MockEvaluator::all_true(&[]);
3806 let validator = EdifactValidator::new(evaluator);
3807 let external = NoOpExternalProvider;
3808 let nav = crate::eval::NoOpGroupNavigator;
3809
3810 let workflow = AhbWorkflow {
3811 pruefidentifikator: "55001".to_string(),
3812 description: "Test".to_string(),
3813 communication_direction: None,
3814 fields: vec![],
3815 ub_definitions: BTreeMap::new(),
3816 package_conditions: Default::default(),
3817 };
3818
3819 let report = validator.validate_with_navigator(
3820 &[],
3821 &workflow,
3822 &external,
3823 ValidationLevel::Full,
3824 &nav,
3825 );
3826 assert!(report.is_valid());
3827 }
3828
3829 #[test]
3830 fn test_code_validation_composite_paths_valid_codes() {
3831 let evaluator = MockEvaluator::new(vec![]);
3835 let validator = EdifactValidator::new(evaluator);
3836 let external = NoOpExternalProvider;
3837
3838 let unh_segment = OwnedSegment {
3839 id: "UNH".to_string(),
3840 elements: vec![
3841 vec!["ALEXANDE951842".to_string()],
3842 vec![
3843 "UTILMD".to_string(),
3844 "D".to_string(),
3845 "11A".to_string(),
3846 "UN".to_string(),
3847 "S2.1".to_string(),
3848 ],
3849 ],
3850 segment_number: 1,
3851 };
3852
3853 let workflow = AhbWorkflow {
3854 pruefidentifikator: "55001".to_string(),
3855 description: "Test".to_string(),
3856 communication_direction: None,
3857 fields: vec![
3858 AhbFieldRule {
3859 segment_path: "UNH/S009/0065".to_string(),
3860 name: "Nachrichtentyp".to_string(),
3861 ahb_status: "X".to_string(),
3862 codes: vec![AhbCodeRule {
3863 value: "UTILMD".to_string(),
3864 description: "Stammdaten".to_string(),
3865 ahb_status: "X".to_string(),
3866 }],
3867 parent_group_ahb_status: None,
3868 segment_ahb_status: None,
3869 element_index: Some(1),
3870 component_index: Some(0),
3871 ..Default::default()
3872 },
3873 AhbFieldRule {
3874 segment_path: "UNH/S009/0052".to_string(),
3875 name: "Version".to_string(),
3876 ahb_status: "X".to_string(),
3877 codes: vec![AhbCodeRule {
3878 value: "D".to_string(),
3879 description: "Draft".to_string(),
3880 ahb_status: "X".to_string(),
3881 }],
3882 parent_group_ahb_status: None,
3883 segment_ahb_status: None,
3884 element_index: Some(1),
3885 component_index: Some(1),
3886 ..Default::default()
3887 },
3888 ],
3889 ub_definitions: BTreeMap::new(),
3890 package_conditions: Default::default(),
3891 };
3892
3893 let report = validator.validate(
3894 &[unh_segment],
3895 &workflow,
3896 &external,
3897 ValidationLevel::Conditions,
3898 );
3899
3900 let code_errors: Vec<_> = report
3901 .by_category(ValidationCategory::Code)
3902 .filter(|i| i.severity == Severity::Error)
3903 .collect();
3904 assert!(
3905 code_errors.is_empty(),
3906 "Expected no code errors when composite values match allowed codes, got: {:?}",
3907 code_errors
3908 );
3909 }
3910
3911 #[test]
3912 fn test_code_validation_partitions_by_mig_number() {
3913 let evaluator = MockEvaluator::new(vec![]);
3917 let validator = EdifactValidator::new(evaluator);
3918 let external = NoOpExternalProvider;
3919
3920 let sts_7 = OwnedSegment {
3921 id: "STS".to_string(),
3922 elements: vec![
3923 vec!["7".to_string()],
3924 vec![String::new()],
3925 vec!["GH02".to_string()],
3926 vec!["ZW4".to_string()],
3927 ],
3928 segment_number: 1,
3929 };
3930 let sts_e01 = OwnedSegment {
3931 id: "STS".to_string(),
3932 elements: vec![
3933 vec!["E01".to_string()],
3934 vec![String::new()],
3935 vec!["A99".to_string(), "E_0614".to_string()],
3936 ],
3937 segment_number: 2,
3938 };
3939
3940 let workflow = AhbWorkflow {
3941 pruefidentifikator: "55018".to_string(),
3942 description: "Test".to_string(),
3943 communication_direction: None,
3944 fields: vec![
3945 AhbFieldRule {
3947 group_ahb_status: None,
3948 segment_path: "SG4/STS/C601/9015".to_string(),
3949 name: "Statuskategorie".to_string(),
3950 ahb_status: "X".to_string(),
3951 codes: vec![AhbCodeRule {
3952 value: "7".to_string(),
3953 description: "Transaktionsgrund".to_string(),
3954 ahb_status: "X".to_string(),
3955 }],
3956 parent_group_ahb_status: None,
3957 segment_ahb_status: None,
3958 element_index: Some(0),
3959 component_index: Some(0),
3960 mig_number: Some("00035".to_string()),
3961 format: None,
3962 },
3963 AhbFieldRule {
3964 group_ahb_status: None,
3965 segment_path: "SG4/STS/C556/9013".to_string(),
3966 name: "Statusanlaß".to_string(),
3967 ahb_status: "X".to_string(),
3968 codes: vec![AhbCodeRule {
3969 value: "E03".to_string(),
3970 description: "Transaktionsgrund".to_string(),
3971 ahb_status: "X".to_string(),
3972 }],
3973 parent_group_ahb_status: None,
3974 segment_ahb_status: None,
3975 element_index: Some(2),
3976 component_index: Some(0),
3977 mig_number: Some("00035".to_string()),
3978 format: None,
3979 },
3980 AhbFieldRule {
3982 group_ahb_status: None,
3983 segment_path: "SG4/STS/C601/9015".to_string(),
3984 name: "Statuskategorie".to_string(),
3985 ahb_status: "X".to_string(),
3986 codes: vec![AhbCodeRule {
3987 value: "E01".to_string(),
3988 description: "Antwort".to_string(),
3989 ahb_status: "X".to_string(),
3990 }],
3991 parent_group_ahb_status: None,
3992 segment_ahb_status: None,
3993 element_index: Some(0),
3994 component_index: Some(0),
3995 mig_number: Some("00036".to_string()),
3996 format: None,
3997 },
3998 ],
3999 ub_definitions: BTreeMap::new(),
4000 package_conditions: Default::default(),
4001 };
4002
4003 let report = validator.validate(
4004 &[sts_7, sts_e01],
4005 &workflow,
4006 &external,
4007 ValidationLevel::Conditions,
4008 );
4009
4010 let code_errors: Vec<_> = report
4011 .by_category(ValidationCategory::Code)
4012 .filter(|i| {
4013 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4014 })
4015 .collect();
4016 assert_eq!(
4017 code_errors.len(),
4018 1,
4019 "Expected one COD002 (for GH02 only), got: {:?}",
4020 code_errors
4021 );
4022 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
4023 }
4024
4025 #[test]
4026 fn test_code_validation_composite_paths_detects_invalid_code() {
4027 let evaluator = MockEvaluator::new(vec![]);
4030 let validator = EdifactValidator::new(evaluator);
4031 let external = NoOpExternalProvider;
4032
4033 let sts_segment = OwnedSegment {
4034 id: "STS".to_string(),
4035 elements: vec![
4036 vec!["7".to_string()],
4037 vec![String::new()],
4038 vec!["GH02".to_string()],
4039 vec!["ZW4".to_string()],
4040 ],
4041 segment_number: 1,
4042 };
4043
4044 let workflow = AhbWorkflow {
4045 pruefidentifikator: "55018".to_string(),
4046 description: "Test".to_string(),
4047 communication_direction: None,
4048 fields: vec![AhbFieldRule {
4049 segment_path: "SG4/STS/C556/9013".to_string(),
4050 name: "Statusanlaß".to_string(),
4051 ahb_status: "X".to_string(),
4052 codes: vec![AhbCodeRule {
4053 value: "E03".to_string(),
4054 description: "Transaktionsgrund".to_string(),
4055 ahb_status: "X".to_string(),
4056 }],
4057 parent_group_ahb_status: None,
4058 segment_ahb_status: None,
4059 element_index: Some(2),
4060 component_index: Some(0),
4061 ..Default::default()
4062 }],
4063 ub_definitions: BTreeMap::new(),
4064 package_conditions: Default::default(),
4065 };
4066
4067 let report = validator.validate(
4068 &[sts_segment],
4069 &workflow,
4070 &external,
4071 ValidationLevel::Conditions,
4072 );
4073
4074 let code_errors: Vec<_> = report
4075 .by_category(ValidationCategory::Code)
4076 .filter(|i| {
4077 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4078 })
4079 .collect();
4080 assert_eq!(
4081 code_errors.len(),
4082 1,
4083 "Expected COD002 for GH02, got: {:?}",
4084 code_errors
4085 );
4086 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
4087 }
4088
4089 #[test]
4090 fn test_cross_field_code_validation_valid_qualifiers() {
4091 let evaluator = MockEvaluator::new(vec![]);
4094 let validator = EdifactValidator::new(evaluator);
4095 let external = NoOpExternalProvider;
4096
4097 let nad_ms = OwnedSegment {
4098 id: "NAD".to_string(),
4099 elements: vec![vec!["MS".to_string()]],
4100 segment_number: 4,
4101 };
4102 let nad_mr = OwnedSegment {
4103 id: "NAD".to_string(),
4104 elements: vec![vec!["MR".to_string()]],
4105 segment_number: 5,
4106 };
4107
4108 let workflow = AhbWorkflow {
4109 pruefidentifikator: "55001".to_string(),
4110 description: "Test".to_string(),
4111 communication_direction: None,
4112 fields: vec![
4113 AhbFieldRule {
4114 segment_path: "SG2/NAD/3035".to_string(),
4115 name: "Absender".to_string(),
4116 ahb_status: "X".to_string(),
4117 codes: vec![AhbCodeRule {
4118 value: "MS".to_string(),
4119 description: "Absender".to_string(),
4120 ahb_status: "X".to_string(),
4121 }],
4122 parent_group_ahb_status: None,
4123 segment_ahb_status: None,
4124 ..Default::default()
4125 },
4126 AhbFieldRule {
4127 segment_path: "SG2/NAD/3035".to_string(),
4128 name: "Empfaenger".to_string(),
4129 ahb_status: "X".to_string(),
4130 codes: vec![AhbCodeRule {
4131 value: "MR".to_string(),
4132 description: "Empfaenger".to_string(),
4133 ahb_status: "X".to_string(),
4134 }],
4135 parent_group_ahb_status: None,
4136 segment_ahb_status: None,
4137 ..Default::default()
4138 },
4139 ],
4140 ub_definitions: BTreeMap::new(),
4141 package_conditions: Default::default(),
4142 };
4143
4144 let report = validator.validate(
4145 &[nad_ms, nad_mr],
4146 &workflow,
4147 &external,
4148 ValidationLevel::Conditions,
4149 );
4150
4151 let code_errors: Vec<_> = report
4152 .by_category(ValidationCategory::Code)
4153 .filter(|i| i.severity == Severity::Error)
4154 .collect();
4155 assert!(
4156 code_errors.is_empty(),
4157 "Expected no code errors for valid qualifiers, got: {:?}",
4158 code_errors
4159 );
4160 }
4161
4162 #[test]
4163 fn test_cross_field_code_validation_catches_invalid_qualifier() {
4164 let evaluator = MockEvaluator::new(vec![]);
4166 let validator = EdifactValidator::new(evaluator);
4167 let external = NoOpExternalProvider;
4168
4169 let nad_ms = OwnedSegment {
4170 id: "NAD".to_string(),
4171 elements: vec![vec!["MS".to_string()]],
4172 segment_number: 4,
4173 };
4174 let nad_mt = OwnedSegment {
4175 id: "NAD".to_string(),
4176 elements: vec![vec!["MT".to_string()]], segment_number: 5,
4178 };
4179
4180 let workflow = AhbWorkflow {
4181 pruefidentifikator: "55001".to_string(),
4182 description: "Test".to_string(),
4183 communication_direction: None,
4184 fields: vec![
4185 AhbFieldRule {
4186 segment_path: "SG2/NAD/3035".to_string(),
4187 name: "Absender".to_string(),
4188 ahb_status: "X".to_string(),
4189 codes: vec![AhbCodeRule {
4190 value: "MS".to_string(),
4191 description: "Absender".to_string(),
4192 ahb_status: "X".to_string(),
4193 }],
4194 parent_group_ahb_status: None,
4195 segment_ahb_status: None,
4196 ..Default::default()
4197 },
4198 AhbFieldRule {
4199 segment_path: "SG2/NAD/3035".to_string(),
4200 name: "Empfaenger".to_string(),
4201 ahb_status: "X".to_string(),
4202 codes: vec![AhbCodeRule {
4203 value: "MR".to_string(),
4204 description: "Empfaenger".to_string(),
4205 ahb_status: "X".to_string(),
4206 }],
4207 parent_group_ahb_status: None,
4208 segment_ahb_status: None,
4209 ..Default::default()
4210 },
4211 ],
4212 ub_definitions: BTreeMap::new(),
4213 package_conditions: Default::default(),
4214 };
4215
4216 let report = validator.validate(
4217 &[nad_ms, nad_mt],
4218 &workflow,
4219 &external,
4220 ValidationLevel::Conditions,
4221 );
4222
4223 let code_errors: Vec<_> = report
4224 .by_category(ValidationCategory::Code)
4225 .filter(|i| i.severity == Severity::Error)
4226 .collect();
4227 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
4228 assert!(narrate(code_errors[0]).contains("MT"));
4229 assert!(narrate(code_errors[0]).contains("MR"));
4230 assert!(narrate(code_errors[0]).contains("MS"));
4231 }
4232
4233 #[test]
4234 fn test_cross_field_code_validation_unions_across_groups() {
4235 let evaluator = MockEvaluator::new(vec![]);
4239 let validator = EdifactValidator::new(evaluator);
4240 let external = NoOpExternalProvider;
4241
4242 let segments = vec![
4243 OwnedSegment {
4244 id: "NAD".to_string(),
4245 elements: vec![vec!["MS".to_string()]],
4246 segment_number: 3,
4247 },
4248 OwnedSegment {
4249 id: "NAD".to_string(),
4250 elements: vec![vec!["MR".to_string()]],
4251 segment_number: 4,
4252 },
4253 OwnedSegment {
4254 id: "NAD".to_string(),
4255 elements: vec![vec!["Z04".to_string()]],
4256 segment_number: 20,
4257 },
4258 OwnedSegment {
4259 id: "NAD".to_string(),
4260 elements: vec![vec!["Z09".to_string()]],
4261 segment_number: 21,
4262 },
4263 OwnedSegment {
4264 id: "NAD".to_string(),
4265 elements: vec![vec!["MT".to_string()]], segment_number: 22,
4267 },
4268 ];
4269
4270 let workflow = AhbWorkflow {
4271 pruefidentifikator: "55001".to_string(),
4272 description: "Test".to_string(),
4273 communication_direction: None,
4274 fields: vec![
4275 AhbFieldRule {
4276 segment_path: "SG2/NAD/3035".to_string(),
4277 name: "Absender".to_string(),
4278 ahb_status: "X".to_string(),
4279 codes: vec![AhbCodeRule {
4280 value: "MS".to_string(),
4281 description: "Absender".to_string(),
4282 ahb_status: "X".to_string(),
4283 }],
4284 parent_group_ahb_status: None,
4285 segment_ahb_status: None,
4286 ..Default::default()
4287 },
4288 AhbFieldRule {
4289 segment_path: "SG2/NAD/3035".to_string(),
4290 name: "Empfaenger".to_string(),
4291 ahb_status: "X".to_string(),
4292 codes: vec![AhbCodeRule {
4293 value: "MR".to_string(),
4294 description: "Empfaenger".to_string(),
4295 ahb_status: "X".to_string(),
4296 }],
4297 parent_group_ahb_status: None,
4298 segment_ahb_status: None,
4299 ..Default::default()
4300 },
4301 AhbFieldRule {
4302 segment_path: "SG4/SG12/NAD/3035".to_string(),
4303 name: "Anschlussnutzer".to_string(),
4304 ahb_status: "X".to_string(),
4305 codes: vec![AhbCodeRule {
4306 value: "Z04".to_string(),
4307 description: "Anschlussnutzer".to_string(),
4308 ahb_status: "X".to_string(),
4309 }],
4310 parent_group_ahb_status: None,
4311 segment_ahb_status: None,
4312 ..Default::default()
4313 },
4314 AhbFieldRule {
4315 segment_path: "SG4/SG12/NAD/3035".to_string(),
4316 name: "Korrespondenzanschrift".to_string(),
4317 ahb_status: "X".to_string(),
4318 codes: vec![AhbCodeRule {
4319 value: "Z09".to_string(),
4320 description: "Korrespondenzanschrift".to_string(),
4321 ahb_status: "X".to_string(),
4322 }],
4323 parent_group_ahb_status: None,
4324 segment_ahb_status: None,
4325 ..Default::default()
4326 },
4327 ],
4328 ub_definitions: BTreeMap::new(),
4329 package_conditions: Default::default(),
4330 };
4331
4332 let report =
4333 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
4334
4335 let code_errors: Vec<_> = report
4336 .by_category(ValidationCategory::Code)
4337 .filter(|i| i.severity == Severity::Error)
4338 .collect();
4339 assert_eq!(
4340 code_errors.len(),
4341 1,
4342 "Expected exactly one COD002 error for MT, got: {:?}",
4343 code_errors
4344 );
4345 assert!(narrate(code_errors[0]).contains("MT"));
4346 }
4347
4348 #[test]
4349 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
4350 let evaluator = MockEvaluator::new(vec![]);
4359 let validator = EdifactValidator::new(evaluator);
4360 let external = NoOpExternalProvider;
4361
4362 let qty_67 = OwnedSegment {
4363 id: "QTY".to_string(),
4364 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
4365 segment_number: 10,
4366 };
4367
4368 let workflow = AhbWorkflow {
4369 pruefidentifikator: "13025".to_string(),
4370 description: "Test".to_string(),
4371 communication_direction: None,
4372 fields: vec![AhbFieldRule {
4373 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
4374 name: "Menge, Qualifier".to_string(),
4375 ahb_status: "X".to_string(),
4376 codes: vec![
4377 AhbCodeRule {
4378 value: "220".to_string(),
4379 description: "Wahrer Wert".to_string(),
4380 ahb_status: "X".to_string(),
4381 },
4382 AhbCodeRule {
4383 value: "67".to_string(),
4384 description: "Ersatzwert".to_string(),
4385 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
4386 },
4387 AhbCodeRule {
4388 value: "Z18".to_string(),
4389 description: "Vorläufiger Wert".to_string(),
4390 ahb_status: "X [35]".to_string(),
4391 },
4392 ],
4393 parent_group_ahb_status: None,
4394 segment_ahb_status: None,
4395 element_index: Some(0),
4396 component_index: Some(0),
4397 ..Default::default()
4398 }],
4399 ub_definitions: BTreeMap::new(),
4400 package_conditions: Default::default(),
4401 };
4402
4403 let report =
4404 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
4405
4406 let code_errors: Vec<_> = report
4407 .by_category(ValidationCategory::Code)
4408 .filter(|i| i.severity == Severity::Error)
4409 .collect();
4410 assert!(
4411 code_errors.is_empty(),
4412 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
4413 code_errors
4414 );
4415 }
4416
4417 #[test]
4418 fn test_is_qualifier_field_simple_paths() {
4419 assert!(is_qualifier_field("NAD/3035"));
4420 assert!(is_qualifier_field("SG2/NAD/3035"));
4421 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
4422 assert!(is_qualifier_field("LOC/3227"));
4423 }
4424
4425 #[test]
4426 fn test_is_qualifier_field_composite_paths() {
4427 assert!(is_qualifier_field("UNH/S009/0065"));
4431 assert!(is_qualifier_field("NAD/C082/3039"));
4432 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
4433 assert!(is_qualifier_field("SG4/STS/C556/9013"));
4434 }
4435
4436 #[test]
4437 fn test_is_qualifier_field_bare_segment() {
4438 assert!(!is_qualifier_field("NAD"));
4439 assert!(!is_qualifier_field("SG2/NAD"));
4440 }
4441
4442 #[test]
4443 fn test_is_qualifier_field_rejects_deep_paths() {
4444 assert!(!is_qualifier_field("SEG/A/B/C/D"));
4446 }
4447
4448 #[test]
4449 fn test_missing_qualifier_instance_is_detected() {
4450 let evaluator = MockEvaluator::new(vec![]);
4453 let validator = EdifactValidator::new(evaluator);
4454 let external = NoOpExternalProvider;
4455
4456 let nad_ms = OwnedSegment {
4457 id: "NAD".to_string(),
4458 elements: vec![vec!["MS".to_string()]],
4459 segment_number: 3,
4460 };
4461
4462 let workflow = AhbWorkflow {
4463 pruefidentifikator: "55001".to_string(),
4464 description: "Test".to_string(),
4465 communication_direction: None,
4466 fields: vec![
4467 AhbFieldRule {
4468 segment_path: "SG2/NAD/3035".to_string(),
4469 name: "Absender".to_string(),
4470 ahb_status: "X".to_string(),
4471 codes: vec![AhbCodeRule {
4472 value: "MS".to_string(),
4473 description: "Absender".to_string(),
4474 ahb_status: "X".to_string(),
4475 }],
4476 parent_group_ahb_status: None,
4477 segment_ahb_status: None,
4478 ..Default::default()
4479 },
4480 AhbFieldRule {
4481 segment_path: "SG2/NAD/3035".to_string(),
4482 name: "Empfaenger".to_string(),
4483 ahb_status: "Muss".to_string(),
4484 codes: vec![AhbCodeRule {
4485 value: "MR".to_string(),
4486 description: "Empfaenger".to_string(),
4487 ahb_status: "X".to_string(),
4488 }],
4489 parent_group_ahb_status: None,
4490 segment_ahb_status: None,
4491 ..Default::default()
4492 },
4493 ],
4494 ub_definitions: BTreeMap::new(),
4495 package_conditions: Default::default(),
4496 };
4497
4498 let report =
4499 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
4500
4501 let ahb_errors: Vec<_> = report
4502 .by_category(ValidationCategory::Ahb)
4503 .filter(|i| i.severity == Severity::Error)
4504 .collect();
4505 assert_eq!(
4506 ahb_errors.len(),
4507 1,
4508 "Expected AHB001 for missing NAD+MR, got: {:?}",
4509 ahb_errors
4510 );
4511 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4512 }
4513
4514 #[test]
4515 fn test_present_qualifier_instance_no_error() {
4516 let evaluator = MockEvaluator::new(vec![]);
4518 let validator = EdifactValidator::new(evaluator);
4519 let external = NoOpExternalProvider;
4520
4521 let segments = vec![
4522 OwnedSegment {
4523 id: "NAD".to_string(),
4524 elements: vec![vec!["MS".to_string()]],
4525 segment_number: 3,
4526 },
4527 OwnedSegment {
4528 id: "NAD".to_string(),
4529 elements: vec![vec!["MR".to_string()]],
4530 segment_number: 4,
4531 },
4532 ];
4533
4534 let workflow = AhbWorkflow {
4535 pruefidentifikator: "55001".to_string(),
4536 description: "Test".to_string(),
4537 communication_direction: None,
4538 fields: vec![
4539 AhbFieldRule {
4540 segment_path: "SG2/NAD/3035".to_string(),
4541 name: "Absender".to_string(),
4542 ahb_status: "Muss".to_string(),
4543 codes: vec![AhbCodeRule {
4544 value: "MS".to_string(),
4545 description: "Absender".to_string(),
4546 ahb_status: "X".to_string(),
4547 }],
4548 parent_group_ahb_status: None,
4549 segment_ahb_status: None,
4550 ..Default::default()
4551 },
4552 AhbFieldRule {
4553 segment_path: "SG2/NAD/3035".to_string(),
4554 name: "Empfaenger".to_string(),
4555 ahb_status: "Muss".to_string(),
4556 codes: vec![AhbCodeRule {
4557 value: "MR".to_string(),
4558 description: "Empfaenger".to_string(),
4559 ahb_status: "X".to_string(),
4560 }],
4561 parent_group_ahb_status: None,
4562 segment_ahb_status: None,
4563 ..Default::default()
4564 },
4565 ],
4566 ub_definitions: BTreeMap::new(),
4567 package_conditions: Default::default(),
4568 };
4569
4570 let report =
4571 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
4572
4573 let ahb_errors: Vec<_> = report
4574 .by_category(ValidationCategory::Ahb)
4575 .filter(|i| i.severity == Severity::Error)
4576 .collect();
4577 assert!(
4578 ahb_errors.is_empty(),
4579 "Expected no AHB001 errors, got: {:?}",
4580 ahb_errors
4581 );
4582 }
4583
4584 #[test]
4585 fn test_extract_group_path_key() {
4586 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
4587 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
4588 assert_eq!(extract_group_path_key("NAD/3035"), "");
4589 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
4590 }
4591
4592 fn validate_field_of_absent_group(
4594 group_status: &str,
4595 conditions: Vec<(u32, CR)>,
4596 ) -> ValidationReport {
4597 use mig_types::navigator::GroupNavigator;
4598
4599 struct NoSg9;
4600 impl GroupNavigator for NoSg9 {
4601 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4602 vec![]
4603 }
4604 fn find_segments_with_qualifier_in_group(
4605 &self,
4606 _: &str,
4607 _: usize,
4608 _: &str,
4609 _: &[&str],
4610 _: usize,
4611 ) -> Vec<OwnedSegment> {
4612 vec![]
4613 }
4614 fn group_instance_count(&self, _: &[&str]) -> usize {
4615 0
4616 }
4617 }
4618
4619 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
4620 let workflow = AhbWorkflow {
4621 pruefidentifikator: "44001".to_string(),
4622 description: "Test".to_string(),
4623 communication_direction: None,
4624 fields: vec![AhbFieldRule {
4625 segment_path: "SG9/QTY/C186/6060".to_string(),
4626 name: "Menge".to_string(),
4627 ahb_status: "X".to_string(),
4628 parent_group_ahb_status: Some(group_status.to_string()),
4629 ..Default::default()
4630 }],
4631 ub_definitions: BTreeMap::new(),
4632 package_conditions: Default::default(),
4633 };
4634 validator.validate_with_navigator(
4635 &[],
4636 &workflow,
4637 &NoOpExternalProvider,
4638 ValidationLevel::Conditions,
4639 &NoSg9,
4640 )
4641 }
4642
4643 #[test]
4644 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
4645 let report = validate_field_of_absent_group(
4647 "Soll [47]\r\nMuss [46]",
4648 vec![(47, CR::True), (46, CR::False)],
4649 );
4650 assert!(
4651 report.is_valid(),
4652 "{:?}",
4653 report.errors().collect::<Vec<_>>()
4654 );
4655 }
4656
4657 #[test]
4658 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
4659 let report = validate_field_of_absent_group(
4660 "Soll [47]\r\nMuss [46]",
4661 vec![(47, CR::False), (46, CR::True)],
4662 );
4663 let errors: Vec<_> = report.errors().collect();
4664 assert_eq!(errors.len(), 1, "{errors:?}");
4665 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
4666 }
4667
4668 #[test]
4669 fn test_absent_optional_group_no_missing_field_error() {
4670 use mig_types::navigator::GroupNavigator;
4673
4674 struct NavWithoutSG3;
4675 impl GroupNavigator for NavWithoutSG3 {
4676 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4677 vec![]
4678 }
4679 fn find_segments_with_qualifier_in_group(
4680 &self,
4681 _: &str,
4682 _: usize,
4683 _: &str,
4684 _: &[&str],
4685 _: usize,
4686 ) -> Vec<OwnedSegment> {
4687 vec![]
4688 }
4689 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4690 match group_path {
4691 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
4694 }
4695 }
4696 }
4697
4698 let evaluator = MockEvaluator::new(vec![]);
4699 let validator = EdifactValidator::new(evaluator);
4700 let external = NoOpExternalProvider;
4701 let nav = NavWithoutSG3;
4702
4703 let segments = vec![
4705 OwnedSegment {
4706 id: "NAD".into(),
4707 elements: vec![vec!["MS".into()]],
4708 segment_number: 3,
4709 },
4710 OwnedSegment {
4711 id: "NAD".into(),
4712 elements: vec![vec!["MR".into()]],
4713 segment_number: 4,
4714 },
4715 ];
4716
4717 let workflow = AhbWorkflow {
4718 pruefidentifikator: "55001".to_string(),
4719 description: "Test".to_string(),
4720 communication_direction: None,
4721 fields: vec![
4722 AhbFieldRule {
4723 segment_path: "SG2/SG3/CTA/3139".to_string(),
4724 name: "Funktion des Ansprechpartners, Code".to_string(),
4725 ahb_status: "Muss".to_string(),
4726 codes: vec![],
4727 parent_group_ahb_status: None,
4728 segment_ahb_status: None,
4729 ..Default::default()
4730 },
4731 AhbFieldRule {
4732 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
4733 name: "Name vom Ansprechpartner".to_string(),
4734 ahb_status: "X".to_string(),
4735 codes: vec![],
4736 parent_group_ahb_status: None,
4737 segment_ahb_status: None,
4738 ..Default::default()
4739 },
4740 ],
4741 ub_definitions: BTreeMap::new(),
4742 package_conditions: Default::default(),
4743 };
4744
4745 let report = validator.validate_with_navigator(
4746 &segments,
4747 &workflow,
4748 &external,
4749 ValidationLevel::Conditions,
4750 &nav,
4751 );
4752
4753 let ahb_errors: Vec<_> = report
4754 .by_category(ValidationCategory::Ahb)
4755 .filter(|i| i.severity == Severity::Error)
4756 .collect();
4757 assert!(
4758 ahb_errors.is_empty(),
4759 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
4760 ahb_errors
4761 );
4762 }
4763
4764 #[test]
4765 fn test_present_group_still_checks_mandatory_fields() {
4766 use mig_types::navigator::GroupNavigator;
4768
4769 struct NavWithSG3;
4770 impl GroupNavigator for NavWithSG3 {
4771 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4772 vec![]
4773 }
4774 fn find_segments_with_qualifier_in_group(
4775 &self,
4776 _: &str,
4777 _: usize,
4778 _: &str,
4779 _: &[&str],
4780 _: usize,
4781 ) -> Vec<OwnedSegment> {
4782 vec![]
4783 }
4784 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4785 match group_path {
4786 ["SG2"] => 1,
4787 ["SG2", "SG3"] => 1, _ => 0,
4789 }
4790 }
4791 }
4792
4793 let evaluator = MockEvaluator::new(vec![]);
4794 let validator = EdifactValidator::new(evaluator);
4795 let external = NoOpExternalProvider;
4796 let nav = NavWithSG3;
4797
4798 let segments = vec![OwnedSegment {
4800 id: "NAD".into(),
4801 elements: vec![vec!["MS".into()]],
4802 segment_number: 3,
4803 }];
4804
4805 let workflow = AhbWorkflow {
4806 pruefidentifikator: "55001".to_string(),
4807 description: "Test".to_string(),
4808 communication_direction: None,
4809 fields: vec![AhbFieldRule {
4810 segment_path: "SG2/SG3/CTA/3139".to_string(),
4811 name: "Funktion des Ansprechpartners, Code".to_string(),
4812 ahb_status: "Muss".to_string(),
4813 codes: vec![],
4814 parent_group_ahb_status: None,
4815 segment_ahb_status: None,
4816 ..Default::default()
4817 }],
4818 ub_definitions: BTreeMap::new(),
4819 package_conditions: Default::default(),
4820 };
4821
4822 let report = validator.validate_with_navigator(
4823 &segments,
4824 &workflow,
4825 &external,
4826 ValidationLevel::Conditions,
4827 &nav,
4828 );
4829
4830 let ahb_errors: Vec<_> = report
4831 .by_category(ValidationCategory::Ahb)
4832 .filter(|i| i.severity == Severity::Error)
4833 .collect();
4834 assert_eq!(
4835 ahb_errors.len(),
4836 1,
4837 "Expected AHB001 error when SG3 is present but CTA missing"
4838 );
4839 assert!(narrate(ahb_errors[0]).contains("CTA"));
4840 }
4841
4842 #[test]
4843 fn test_missing_qualifier_with_navigator_is_detected() {
4844 use mig_types::navigator::GroupNavigator;
4847
4848 struct NavWithSG2;
4849 impl GroupNavigator for NavWithSG2 {
4850 fn find_segments_in_group(
4851 &self,
4852 segment_id: &str,
4853 group_path: &[&str],
4854 instance_index: usize,
4855 ) -> Vec<OwnedSegment> {
4856 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
4857 vec![OwnedSegment {
4858 id: "NAD".into(),
4859 elements: vec![vec!["MS".into()]],
4860 segment_number: 3,
4861 }]
4862 } else {
4863 vec![]
4864 }
4865 }
4866 fn find_segments_with_qualifier_in_group(
4867 &self,
4868 _: &str,
4869 _: usize,
4870 _: &str,
4871 _: &[&str],
4872 _: usize,
4873 ) -> Vec<OwnedSegment> {
4874 vec![]
4875 }
4876 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4877 match group_path {
4878 ["SG2"] => 1,
4879 _ => 0,
4880 }
4881 }
4882 }
4883
4884 let evaluator = MockEvaluator::new(vec![]);
4885 let validator = EdifactValidator::new(evaluator);
4886 let external = NoOpExternalProvider;
4887 let nav = NavWithSG2;
4888
4889 let segments = vec![OwnedSegment {
4890 id: "NAD".into(),
4891 elements: vec![vec!["MS".into()]],
4892 segment_number: 3,
4893 }];
4894
4895 let workflow = AhbWorkflow {
4896 pruefidentifikator: "55001".to_string(),
4897 description: "Test".to_string(),
4898 communication_direction: None,
4899 fields: vec![
4900 AhbFieldRule {
4901 segment_path: "SG2/NAD/3035".to_string(),
4902 name: "Absender".to_string(),
4903 ahb_status: "X".to_string(),
4904 codes: vec![AhbCodeRule {
4905 value: "MS".to_string(),
4906 description: "Absender".to_string(),
4907 ahb_status: "X".to_string(),
4908 }],
4909 parent_group_ahb_status: None,
4910 segment_ahb_status: None,
4911 ..Default::default()
4912 },
4913 AhbFieldRule {
4914 segment_path: "SG2/NAD/3035".to_string(),
4915 name: "Empfaenger".to_string(),
4916 ahb_status: "Muss".to_string(),
4917 codes: vec![AhbCodeRule {
4918 value: "MR".to_string(),
4919 description: "Empfaenger".to_string(),
4920 ahb_status: "X".to_string(),
4921 }],
4922 parent_group_ahb_status: None,
4923 segment_ahb_status: None,
4924 ..Default::default()
4925 },
4926 ],
4927 ub_definitions: BTreeMap::new(),
4928 package_conditions: Default::default(),
4929 };
4930
4931 let report = validator.validate_with_navigator(
4932 &segments,
4933 &workflow,
4934 &external,
4935 ValidationLevel::Conditions,
4936 &nav,
4937 );
4938
4939 let ahb_errors: Vec<_> = report
4940 .by_category(ValidationCategory::Ahb)
4941 .filter(|i| i.severity == Severity::Error)
4942 .collect();
4943 assert_eq!(
4944 ahb_errors.len(),
4945 1,
4946 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
4947 ahb_errors
4948 );
4949 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4950 }
4951
4952 #[test]
4953 fn test_optional_group_variant_absent_no_error() {
4954 use mig_types::navigator::GroupNavigator;
4959
4960 struct TestNav;
4961 impl GroupNavigator for TestNav {
4962 fn find_segments_in_group(
4963 &self,
4964 segment_id: &str,
4965 group_path: &[&str],
4966 instance_index: usize,
4967 ) -> Vec<OwnedSegment> {
4968 match (segment_id, group_path, instance_index) {
4969 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
4970 id: "LOC".into(),
4971 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4972 segment_number: 10,
4973 }],
4974 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
4975 id: "NAD".into(),
4976 elements: vec![vec!["MS".into()]],
4977 segment_number: 3,
4978 }],
4979 _ => vec![],
4980 }
4981 }
4982 fn find_segments_with_qualifier_in_group(
4983 &self,
4984 _: &str,
4985 _: usize,
4986 _: &str,
4987 _: &[&str],
4988 _: usize,
4989 ) -> Vec<OwnedSegment> {
4990 vec![]
4991 }
4992 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4993 match group_path {
4994 ["SG2"] => 1,
4995 ["SG4"] => 1,
4996 ["SG4", "SG5"] => 1, _ => 0,
4998 }
4999 }
5000 }
5001
5002 let evaluator = MockEvaluator::new(vec![]);
5003 let validator = EdifactValidator::new(evaluator);
5004 let external = NoOpExternalProvider;
5005 let nav = TestNav;
5006
5007 let segments = vec![
5008 OwnedSegment {
5009 id: "NAD".into(),
5010 elements: vec![vec!["MS".into()]],
5011 segment_number: 3,
5012 },
5013 OwnedSegment {
5014 id: "LOC".into(),
5015 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
5016 segment_number: 10,
5017 },
5018 ];
5019
5020 let workflow = AhbWorkflow {
5021 pruefidentifikator: "55001".to_string(),
5022 description: "Test".to_string(),
5023 communication_direction: None,
5024 fields: vec![
5025 AhbFieldRule {
5027 segment_path: "SG2/NAD/3035".to_string(),
5028 name: "Absender".to_string(),
5029 ahb_status: "X".to_string(),
5030 codes: vec![AhbCodeRule {
5031 value: "MS".to_string(),
5032 description: "Absender".to_string(),
5033 ahb_status: "X".to_string(),
5034 }],
5035 parent_group_ahb_status: Some("Muss".to_string()),
5036 segment_ahb_status: None,
5037 ..Default::default()
5038 },
5039 AhbFieldRule {
5040 segment_path: "SG2/NAD/3035".to_string(),
5041 name: "Empfaenger".to_string(),
5042 ahb_status: "Muss".to_string(),
5043 codes: vec![AhbCodeRule {
5044 value: "MR".to_string(),
5045 description: "Empfaenger".to_string(),
5046 ahb_status: "X".to_string(),
5047 }],
5048 parent_group_ahb_status: Some("Muss".to_string()),
5049 segment_ahb_status: None,
5050 ..Default::default()
5051 },
5052 AhbFieldRule {
5054 segment_path: "SG4/SG5/LOC/3227".to_string(),
5055 name: "Ortsangabe, Qualifier (Z16)".to_string(),
5056 ahb_status: "X".to_string(),
5057 codes: vec![AhbCodeRule {
5058 value: "Z16".to_string(),
5059 description: "Marktlokation".to_string(),
5060 ahb_status: "X".to_string(),
5061 }],
5062 parent_group_ahb_status: Some("Kann".to_string()),
5063 segment_ahb_status: None,
5064 ..Default::default()
5065 },
5066 AhbFieldRule {
5067 segment_path: "SG4/SG5/LOC/3227".to_string(),
5068 name: "Ortsangabe, Qualifier (Z17)".to_string(),
5069 ahb_status: "Muss".to_string(),
5070 codes: vec![AhbCodeRule {
5071 value: "Z17".to_string(),
5072 description: "Messlokation".to_string(),
5073 ahb_status: "X".to_string(),
5074 }],
5075 parent_group_ahb_status: Some("Kann".to_string()),
5076 segment_ahb_status: None,
5077 ..Default::default()
5078 },
5079 ],
5080 ub_definitions: BTreeMap::new(),
5081 package_conditions: Default::default(),
5082 };
5083
5084 let report = validator.validate_with_navigator(
5085 &segments,
5086 &workflow,
5087 &external,
5088 ValidationLevel::Conditions,
5089 &nav,
5090 );
5091
5092 let ahb_errors: Vec<_> = report
5093 .by_category(ValidationCategory::Ahb)
5094 .filter(|i| i.severity == Severity::Error)
5095 .collect();
5096
5097 assert_eq!(
5100 ahb_errors.len(),
5101 1,
5102 "Expected only AHB001 for missing NAD+MR, got: {:?}",
5103 ahb_errors
5104 );
5105 assert!(
5106 narrate(ahb_errors[0]).contains("Empfaenger"),
5107 "Error should be for missing NAD+MR (Empfaenger)"
5108 );
5109 }
5110
5111 #[test]
5115 fn test_optional_group_variant_with_true_condition_absent_no_error() {
5116 use mig_types::navigator::GroupNavigator;
5117
5118 struct TestNav;
5119 impl GroupNavigator for TestNav {
5120 fn find_segments_in_group(
5121 &self,
5122 segment_id: &str,
5123 group_path: &[&str],
5124 instance_index: usize,
5125 ) -> Vec<OwnedSegment> {
5126 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
5127 vec![OwnedSegment {
5128 id: "SEQ".into(),
5129 elements: vec![vec!["Z03".into()]],
5130 segment_number: 10,
5131 }]
5132 } else {
5133 vec![]
5134 }
5135 }
5136 fn find_segments_with_qualifier_in_group(
5137 &self,
5138 _: &str,
5139 _: usize,
5140 _: &str,
5141 _: &[&str],
5142 _: usize,
5143 ) -> Vec<OwnedSegment> {
5144 vec![]
5145 }
5146 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5147 match group_path {
5148 ["SG4"] => 1,
5149 ["SG4", "SG8"] => 1,
5150 _ => 0,
5151 }
5152 }
5153 }
5154
5155 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
5156 let validator = EdifactValidator::new(evaluator);
5157 let segments = vec![OwnedSegment {
5158 id: "SEQ".into(),
5159 elements: vec![vec!["Z03".into()]],
5160 segment_number: 10,
5161 }];
5162 let seq = |code: &str, group: &str| AhbFieldRule {
5163 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5164 name: format!("Handlung, Code ({code})"),
5165 ahb_status: "X".to_string(),
5166 codes: vec![AhbCodeRule {
5167 value: code.to_string(),
5168 description: String::new(),
5169 ahb_status: "X".to_string(),
5170 }],
5171 parent_group_ahb_status: Some(group.to_string()),
5172 segment_ahb_status: None,
5173 ..Default::default()
5174 };
5175 let workflow = AhbWorkflow {
5176 pruefidentifikator: "44143".to_string(),
5177 description: "Test".to_string(),
5178 communication_direction: None,
5179 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
5180 ub_definitions: BTreeMap::new(),
5181 package_conditions: Default::default(),
5182 };
5183
5184 let report = validator.validate_with_navigator(
5185 &segments,
5186 &workflow,
5187 &NoOpExternalProvider,
5188 ValidationLevel::Conditions,
5189 &TestNav,
5190 );
5191 let missing: Vec<_> = report
5192 .issues
5193 .iter()
5194 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
5195 .collect();
5196 assert!(missing.is_empty(), "{missing:#?}");
5197 }
5198
5199 #[test]
5200 fn test_conditional_group_variant_absent_no_error() {
5201 use mig_types::navigator::GroupNavigator;
5206
5207 struct TestNav;
5208 impl GroupNavigator for TestNav {
5209 fn find_segments_in_group(
5210 &self,
5211 segment_id: &str,
5212 group_path: &[&str],
5213 instance_index: usize,
5214 ) -> Vec<OwnedSegment> {
5215 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
5216 vec![OwnedSegment {
5217 id: "LOC".into(),
5218 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
5219 segment_number: 10,
5220 }]
5221 } else {
5222 vec![]
5223 }
5224 }
5225 fn find_segments_with_qualifier_in_group(
5226 &self,
5227 _: &str,
5228 _: usize,
5229 _: &str,
5230 _: &[&str],
5231 _: usize,
5232 ) -> Vec<OwnedSegment> {
5233 vec![]
5234 }
5235 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5236 match group_path {
5237 ["SG4"] => 1,
5238 ["SG4", "SG5"] => 1, _ => 0,
5240 }
5241 }
5242 }
5243
5244 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
5247 let validator = EdifactValidator::new(evaluator);
5248 let external = NoOpExternalProvider;
5249 let nav = TestNav;
5250
5251 let segments = vec![OwnedSegment {
5252 id: "LOC".into(),
5253 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
5254 segment_number: 10,
5255 }];
5256
5257 let workflow = AhbWorkflow {
5258 pruefidentifikator: "55001".to_string(),
5259 description: "Test".to_string(),
5260 communication_direction: None,
5261 fields: vec![
5262 AhbFieldRule {
5264 segment_path: "SG4/SG5/LOC/3227".to_string(),
5265 name: "Ortsangabe, Qualifier (Z16)".to_string(),
5266 ahb_status: "X".to_string(),
5267 codes: vec![AhbCodeRule {
5268 value: "Z16".to_string(),
5269 description: "Marktlokation".to_string(),
5270 ahb_status: "X".to_string(),
5271 }],
5272 parent_group_ahb_status: Some("Muss [2061]".to_string()),
5273 segment_ahb_status: None,
5274 ..Default::default()
5275 },
5276 AhbFieldRule {
5278 segment_path: "SG4/SG5/LOC/3227".to_string(),
5279 name: "Ortsangabe, Qualifier (Z17)".to_string(),
5280 ahb_status: "X".to_string(),
5281 codes: vec![AhbCodeRule {
5282 value: "Z17".to_string(),
5283 description: "Messlokation".to_string(),
5284 ahb_status: "X".to_string(),
5285 }],
5286 parent_group_ahb_status: Some("Soll [165]".to_string()),
5287 segment_ahb_status: None,
5288 ..Default::default()
5289 },
5290 ],
5291 ub_definitions: BTreeMap::new(),
5292 package_conditions: Default::default(),
5293 };
5294
5295 let report = validator.validate_with_navigator(
5296 &segments,
5297 &workflow,
5298 &external,
5299 ValidationLevel::Conditions,
5300 &nav,
5301 );
5302
5303 let ahb_errors: Vec<_> = report
5304 .by_category(ValidationCategory::Ahb)
5305 .filter(|i| i.severity == Severity::Error)
5306 .collect();
5307
5308 assert!(
5310 ahb_errors.is_empty(),
5311 "Expected no errors when conditional group variant [165]=False, got: {:?}",
5312 ahb_errors
5313 );
5314 }
5315
5316 #[test]
5317 fn test_conditional_group_variant_unknown_no_error() {
5318 let evaluator = MockEvaluator::new(vec![]);
5324 let validator = EdifactValidator::new(evaluator);
5325 let external = NoOpExternalProvider;
5326
5327 let workflow = AhbWorkflow {
5328 pruefidentifikator: "55001".to_string(),
5329 description: "Test".to_string(),
5330 communication_direction: None,
5331 fields: vec![AhbFieldRule {
5332 segment_path: "SG4/SG5/LOC/3227".to_string(),
5333 name: "Ortsangabe, Qualifier (Z17)".to_string(),
5334 ahb_status: "X".to_string(),
5335 codes: vec![AhbCodeRule {
5336 value: "Z17".to_string(),
5337 description: "Messlokation".to_string(),
5338 ahb_status: "X".to_string(),
5339 }],
5340 parent_group_ahb_status: Some("Soll [165]".to_string()),
5341 segment_ahb_status: None,
5342 ..Default::default()
5343 }],
5344 ub_definitions: BTreeMap::new(),
5345 package_conditions: Default::default(),
5346 };
5347
5348 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
5349
5350 let ahb_errors: Vec<_> = report
5351 .by_category(ValidationCategory::Ahb)
5352 .filter(|i| i.severity == Severity::Error)
5353 .collect();
5354
5355 assert!(
5357 ahb_errors.is_empty(),
5358 "Expected no errors when parent group condition is Unknown, got: {:?}",
5359 ahb_errors
5360 );
5361 }
5362
5363 #[test]
5364 fn test_segment_absent_within_present_group_no_error() {
5365 use mig_types::navigator::GroupNavigator;
5369
5370 struct TestNav;
5371 impl GroupNavigator for TestNav {
5372 fn find_segments_in_group(
5373 &self,
5374 segment_id: &str,
5375 group_path: &[&str],
5376 instance_index: usize,
5377 ) -> Vec<OwnedSegment> {
5378 if segment_id == "QTY"
5380 && group_path == ["SG5", "SG6", "SG9", "SG10"]
5381 && instance_index == 0
5382 {
5383 vec![OwnedSegment {
5384 id: "QTY".into(),
5385 elements: vec![vec!["220".into(), "0".into()]],
5386 segment_number: 14,
5387 }]
5388 } else {
5389 vec![]
5390 }
5391 }
5392 fn find_segments_with_qualifier_in_group(
5393 &self,
5394 _: &str,
5395 _: usize,
5396 _: &str,
5397 _: &[&str],
5398 _: usize,
5399 ) -> Vec<OwnedSegment> {
5400 vec![]
5401 }
5402 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5403 match group_path {
5404 ["SG5"] => 1,
5405 ["SG5", "SG6"] => 1,
5406 ["SG5", "SG6", "SG9"] => 1,
5407 ["SG5", "SG6", "SG9", "SG10"] => 1,
5408 _ => 0,
5409 }
5410 }
5411 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
5412 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
5414 }
5415 }
5416
5417 let evaluator = MockEvaluator::all_true(&[]);
5418 let validator = EdifactValidator::new(evaluator);
5419 let external = NoOpExternalProvider;
5420 let nav = TestNav;
5421
5422 let segments = vec![OwnedSegment {
5423 id: "QTY".into(),
5424 elements: vec![vec!["220".into(), "0".into()]],
5425 segment_number: 14,
5426 }];
5427
5428 let workflow = AhbWorkflow {
5429 pruefidentifikator: "13017".to_string(),
5430 description: "Test".to_string(),
5431 communication_direction: None,
5432 fields: vec![
5433 AhbFieldRule {
5435 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
5436 name: "Statuskategorie, Code".to_string(),
5437 ahb_status: "X".to_string(),
5438 codes: vec![],
5439 parent_group_ahb_status: Some("Muss".to_string()),
5440 segment_ahb_status: None,
5441 ..Default::default()
5442 },
5443 AhbFieldRule {
5445 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5446 name: "Statusanlaß, Code".to_string(),
5447 ahb_status: "X [5]".to_string(),
5448 codes: vec![],
5449 parent_group_ahb_status: Some("Muss".to_string()),
5450 segment_ahb_status: None,
5451 ..Default::default()
5452 },
5453 ],
5454 ub_definitions: BTreeMap::new(),
5455 package_conditions: Default::default(),
5456 };
5457
5458 let report = validator.validate_with_navigator(
5459 &segments,
5460 &workflow,
5461 &external,
5462 ValidationLevel::Conditions,
5463 &nav,
5464 );
5465
5466 let ahb_errors: Vec<_> = report
5467 .by_category(ValidationCategory::Ahb)
5468 .filter(|i| i.severity == Severity::Error)
5469 .collect();
5470
5471 assert!(
5472 ahb_errors.is_empty(),
5473 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
5474 ahb_errors
5475 );
5476 }
5477
5478 #[test]
5479 fn test_group_scoped_code_validation_with_navigator() {
5480 use mig_types::navigator::GroupNavigator;
5484
5485 struct TestNav;
5486 impl GroupNavigator for TestNav {
5487 fn find_segments_in_group(
5488 &self,
5489 segment_id: &str,
5490 group_path: &[&str],
5491 _instance_index: usize,
5492 ) -> Vec<OwnedSegment> {
5493 if segment_id != "NAD" {
5494 return vec![];
5495 }
5496 match group_path {
5497 ["SG2"] => vec![
5498 OwnedSegment {
5499 id: "NAD".into(),
5500 elements: vec![vec!["MS".into()]],
5501 segment_number: 3,
5502 },
5503 OwnedSegment {
5504 id: "NAD".into(),
5505 elements: vec![vec!["MT".into()]], segment_number: 4,
5507 },
5508 ],
5509 ["SG4", "SG12"] => vec![
5510 OwnedSegment {
5511 id: "NAD".into(),
5512 elements: vec![vec!["Z04".into()]],
5513 segment_number: 20,
5514 },
5515 OwnedSegment {
5516 id: "NAD".into(),
5517 elements: vec![vec!["Z09".into()]],
5518 segment_number: 21,
5519 },
5520 ],
5521 _ => vec![],
5522 }
5523 }
5524 fn find_segments_with_qualifier_in_group(
5525 &self,
5526 _: &str,
5527 _: usize,
5528 _: &str,
5529 _: &[&str],
5530 _: usize,
5531 ) -> Vec<OwnedSegment> {
5532 vec![]
5533 }
5534 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5535 match group_path {
5536 ["SG2"] | ["SG4", "SG12"] => 1,
5537 _ => 0,
5538 }
5539 }
5540 }
5541
5542 let evaluator = MockEvaluator::new(vec![]);
5543 let validator = EdifactValidator::new(evaluator);
5544 let external = NoOpExternalProvider;
5545 let nav = TestNav;
5546
5547 let workflow = AhbWorkflow {
5548 pruefidentifikator: "55001".to_string(),
5549 description: "Test".to_string(),
5550 communication_direction: None,
5551 fields: vec![
5552 AhbFieldRule {
5553 segment_path: "SG2/NAD/3035".to_string(),
5554 name: "Absender".to_string(),
5555 ahb_status: "X".to_string(),
5556 codes: vec![AhbCodeRule {
5557 value: "MS".to_string(),
5558 description: "Absender".to_string(),
5559 ahb_status: "X".to_string(),
5560 }],
5561 parent_group_ahb_status: None,
5562 segment_ahb_status: None,
5563 ..Default::default()
5564 },
5565 AhbFieldRule {
5566 segment_path: "SG2/NAD/3035".to_string(),
5567 name: "Empfaenger".to_string(),
5568 ahb_status: "X".to_string(),
5569 codes: vec![AhbCodeRule {
5570 value: "MR".to_string(),
5571 description: "Empfaenger".to_string(),
5572 ahb_status: "X".to_string(),
5573 }],
5574 parent_group_ahb_status: None,
5575 segment_ahb_status: None,
5576 ..Default::default()
5577 },
5578 AhbFieldRule {
5579 segment_path: "SG4/SG12/NAD/3035".to_string(),
5580 name: "Anschlussnutzer".to_string(),
5581 ahb_status: "X".to_string(),
5582 codes: vec![AhbCodeRule {
5583 value: "Z04".to_string(),
5584 description: "Anschlussnutzer".to_string(),
5585 ahb_status: "X".to_string(),
5586 }],
5587 parent_group_ahb_status: None,
5588 segment_ahb_status: None,
5589 ..Default::default()
5590 },
5591 AhbFieldRule {
5592 segment_path: "SG4/SG12/NAD/3035".to_string(),
5593 name: "Korrespondenzanschrift".to_string(),
5594 ahb_status: "X".to_string(),
5595 codes: vec![AhbCodeRule {
5596 value: "Z09".to_string(),
5597 description: "Korrespondenzanschrift".to_string(),
5598 ahb_status: "X".to_string(),
5599 }],
5600 parent_group_ahb_status: None,
5601 segment_ahb_status: None,
5602 ..Default::default()
5603 },
5604 ],
5605 ub_definitions: BTreeMap::new(),
5606 package_conditions: Default::default(),
5607 };
5608
5609 let all_segments = vec![
5611 OwnedSegment {
5612 id: "NAD".into(),
5613 elements: vec![vec!["MS".into()]],
5614 segment_number: 3,
5615 },
5616 OwnedSegment {
5617 id: "NAD".into(),
5618 elements: vec![vec!["MT".into()]],
5619 segment_number: 4,
5620 },
5621 OwnedSegment {
5622 id: "NAD".into(),
5623 elements: vec![vec!["Z04".into()]],
5624 segment_number: 20,
5625 },
5626 OwnedSegment {
5627 id: "NAD".into(),
5628 elements: vec![vec!["Z09".into()]],
5629 segment_number: 21,
5630 },
5631 ];
5632
5633 let report = validator.validate_with_navigator(
5634 &all_segments,
5635 &workflow,
5636 &external,
5637 ValidationLevel::Conditions,
5638 &nav,
5639 );
5640
5641 let code_errors: Vec<_> = report
5642 .by_category(ValidationCategory::Code)
5643 .filter(|i| i.severity == Severity::Error)
5644 .collect();
5645
5646 assert_eq!(
5649 code_errors.len(),
5650 1,
5651 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
5652 code_errors
5653 );
5654 assert!(narrate(code_errors[0]).contains("MT"));
5655 assert!(narrate(code_errors[0]).contains("MR"));
5657 assert!(narrate(code_errors[0]).contains("MS"));
5658 assert!(
5659 !narrate(code_errors[0]).contains("Z04"),
5660 "SG4/SG12 codes should not leak into SG2 error"
5661 );
5662 assert!(
5664 code_errors[0]
5665 .field_path
5666 .as_deref()
5667 .unwrap_or("")
5668 .contains("SG2"),
5669 "Error field_path should reference SG2, got: {:?}",
5670 code_errors[0].field_path
5671 );
5672 }
5673
5674 #[test]
5677 fn test_package_cardinality_within_bounds() {
5678 let evaluator = MockEvaluator::all_true(&[]);
5680 let validator = EdifactValidator::new(evaluator);
5681 let external = NoOpExternalProvider;
5682
5683 let segments = vec![OwnedSegment {
5684 id: "STS".into(),
5685 elements: vec![
5686 vec!["Z33".into()], vec![], vec!["E01".into()], ],
5690 segment_number: 5,
5691 }];
5692
5693 let workflow = AhbWorkflow {
5694 pruefidentifikator: "13017".to_string(),
5695 description: "Test".to_string(),
5696 communication_direction: None,
5697 ub_definitions: BTreeMap::new(),
5698 package_conditions: Default::default(),
5699 fields: vec![AhbFieldRule {
5700 group_ahb_status: None,
5701 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5702 name: "Statusanlaß, Code".to_string(),
5703 ahb_status: "X".to_string(),
5704 element_index: Some(2),
5705 component_index: Some(0),
5706 codes: vec![
5707 AhbCodeRule {
5708 value: "E01".into(),
5709 description: "Code 1".into(),
5710 ahb_status: "X [4P0..1]".into(),
5711 },
5712 AhbCodeRule {
5713 value: "E02".into(),
5714 description: "Code 2".into(),
5715 ahb_status: "X [4P0..1]".into(),
5716 },
5717 ],
5718 parent_group_ahb_status: Some("Muss".to_string()),
5719 segment_ahb_status: None,
5720 mig_number: None,
5721 format: None,
5722 }],
5723 };
5724
5725 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5726 let pkg_errors: Vec<_> = report
5727 .by_category(ValidationCategory::Ahb)
5728 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5729 .collect();
5730 assert!(
5731 pkg_errors.is_empty(),
5732 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
5733 pkg_errors
5734 );
5735 }
5736
5737 #[test]
5738 fn test_package_cardinality_zero_present_min_zero() {
5739 let evaluator = MockEvaluator::all_true(&[]);
5741 let validator = EdifactValidator::new(evaluator);
5742 let external = NoOpExternalProvider;
5743
5744 let segments = vec![OwnedSegment {
5745 id: "STS".into(),
5746 elements: vec![
5747 vec!["Z33".into()],
5748 vec![],
5749 vec!["X99".into()], ],
5751 segment_number: 5,
5752 }];
5753
5754 let workflow = AhbWorkflow {
5755 pruefidentifikator: "13017".to_string(),
5756 description: "Test".to_string(),
5757 communication_direction: None,
5758 ub_definitions: BTreeMap::new(),
5759 package_conditions: Default::default(),
5760 fields: vec![AhbFieldRule {
5761 group_ahb_status: None,
5762 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5763 name: "Statusanlaß, Code".to_string(),
5764 ahb_status: "X".to_string(),
5765 element_index: Some(2),
5766 component_index: Some(0),
5767 codes: vec![
5768 AhbCodeRule {
5769 value: "E01".into(),
5770 description: "Code 1".into(),
5771 ahb_status: "X [4P0..1]".into(),
5772 },
5773 AhbCodeRule {
5774 value: "E02".into(),
5775 description: "Code 2".into(),
5776 ahb_status: "X [4P0..1]".into(),
5777 },
5778 ],
5779 parent_group_ahb_status: Some("Muss".to_string()),
5780 segment_ahb_status: None,
5781 mig_number: None,
5782 format: None,
5783 }],
5784 };
5785
5786 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5787 let pkg_errors: Vec<_> = report
5788 .by_category(ValidationCategory::Ahb)
5789 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5790 .collect();
5791 assert!(
5792 pkg_errors.is_empty(),
5793 "0 codes, min=0 — no error expected, got: {:?}",
5794 pkg_errors
5795 );
5796 }
5797
5798 #[test]
5799 fn test_package_cardinality_too_many() {
5800 let evaluator = MockEvaluator::all_true(&[]);
5802 let validator = EdifactValidator::new(evaluator);
5803 let external = NoOpExternalProvider;
5804
5805 let segments = vec![
5807 OwnedSegment {
5808 id: "STS".into(),
5809 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
5810 segment_number: 5,
5811 },
5812 OwnedSegment {
5813 id: "STS".into(),
5814 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
5815 segment_number: 6,
5816 },
5817 ];
5818
5819 let workflow = AhbWorkflow {
5820 pruefidentifikator: "13017".to_string(),
5821 description: "Test".to_string(),
5822 communication_direction: None,
5823 ub_definitions: BTreeMap::new(),
5824 package_conditions: Default::default(),
5825 fields: vec![AhbFieldRule {
5826 group_ahb_status: None,
5827 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5828 name: "Statusanlaß, Code".to_string(),
5829 ahb_status: "X".to_string(),
5830 element_index: Some(2),
5831 component_index: Some(0),
5832 codes: vec![
5833 AhbCodeRule {
5834 value: "E01".into(),
5835 description: "Code 1".into(),
5836 ahb_status: "X [4P0..1]".into(),
5837 },
5838 AhbCodeRule {
5839 value: "E02".into(),
5840 description: "Code 2".into(),
5841 ahb_status: "X [4P0..1]".into(),
5842 },
5843 ],
5844 parent_group_ahb_status: Some("Muss".to_string()),
5845 segment_ahb_status: None,
5846 mig_number: None,
5847 format: None,
5848 }],
5849 };
5850
5851 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5852 let pkg_errors: Vec<_> = report
5853 .by_category(ValidationCategory::Ahb)
5854 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5855 .collect();
5856 assert_eq!(
5857 pkg_errors.len(),
5858 1,
5859 "2 codes present, max=1 — expected 1 error, got: {:?}",
5860 pkg_errors
5861 );
5862 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
5863 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
5864 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
5865 }
5866
5867 #[test]
5868 fn test_package_cardinality_too_few() {
5869 let evaluator = MockEvaluator::all_true(&[]);
5871 let validator = EdifactValidator::new(evaluator);
5872 let external = NoOpExternalProvider;
5873
5874 let segments = vec![OwnedSegment {
5875 id: "STS".into(),
5876 elements: vec![
5877 vec!["Z33".into()],
5878 vec![],
5879 vec!["X99".into()], ],
5881 segment_number: 5,
5882 }];
5883
5884 let workflow = AhbWorkflow {
5885 pruefidentifikator: "13017".to_string(),
5886 description: "Test".to_string(),
5887 communication_direction: None,
5888 ub_definitions: BTreeMap::new(),
5889 package_conditions: Default::default(),
5890 fields: vec![AhbFieldRule {
5891 group_ahb_status: None,
5892 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5893 name: "Statusanlaß, Code".to_string(),
5894 ahb_status: "X".to_string(),
5895 element_index: Some(2),
5896 component_index: Some(0),
5897 codes: vec![
5898 AhbCodeRule {
5899 value: "E01".into(),
5900 description: "Code 1".into(),
5901 ahb_status: "X [5P1..3]".into(),
5902 },
5903 AhbCodeRule {
5904 value: "E02".into(),
5905 description: "Code 2".into(),
5906 ahb_status: "X [5P1..3]".into(),
5907 },
5908 AhbCodeRule {
5909 value: "E03".into(),
5910 description: "Code 3".into(),
5911 ahb_status: "X [5P1..3]".into(),
5912 },
5913 ],
5914 parent_group_ahb_status: Some("Muss".to_string()),
5915 segment_ahb_status: None,
5916 mig_number: None,
5917 format: None,
5918 }],
5919 };
5920
5921 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5922 let pkg_errors: Vec<_> = report
5923 .by_category(ValidationCategory::Ahb)
5924 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5925 .collect();
5926 assert_eq!(
5927 pkg_errors.len(),
5928 1,
5929 "0 codes present, min=1 — expected 1 error, got: {:?}",
5930 pkg_errors
5931 );
5932 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
5933 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
5934 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
5935 }
5936
5937 #[test]
5938 fn test_package_cardinality_no_packages_in_workflow() {
5939 let evaluator = MockEvaluator::all_true(&[]);
5941 let validator = EdifactValidator::new(evaluator);
5942 let external = NoOpExternalProvider;
5943
5944 let segments = vec![OwnedSegment {
5945 id: "STS".into(),
5946 elements: vec![vec!["E01".into()]],
5947 segment_number: 5,
5948 }];
5949
5950 let workflow = AhbWorkflow {
5951 pruefidentifikator: "13017".to_string(),
5952 description: "Test".to_string(),
5953 communication_direction: None,
5954 ub_definitions: BTreeMap::new(),
5955 package_conditions: Default::default(),
5956 fields: vec![AhbFieldRule {
5957 segment_path: "STS/9015".to_string(),
5958 name: "Status Code".to_string(),
5959 ahb_status: "X".to_string(),
5960 codes: vec![AhbCodeRule {
5961 value: "E01".into(),
5962 description: "Code 1".into(),
5963 ahb_status: "X".into(),
5964 }],
5965 parent_group_ahb_status: Some("Muss".to_string()),
5966 segment_ahb_status: None,
5967 ..Default::default()
5968 }],
5969 };
5970
5971 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5972 let pkg_errors: Vec<_> = report
5973 .by_category(ValidationCategory::Ahb)
5974 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5975 .collect();
5976 assert!(
5977 pkg_errors.is_empty(),
5978 "No packages in workflow — no errors expected"
5979 );
5980 }
5981
5982 #[test]
5983 fn test_package_cardinality_with_condition_and_package() {
5984 let evaluator = MockEvaluator::all_true(&[901]);
5986 let validator = EdifactValidator::new(evaluator);
5987 let external = NoOpExternalProvider;
5988
5989 let segments = vec![OwnedSegment {
5990 id: "STS".into(),
5991 elements: vec![vec![], vec![], vec!["E01".into()]],
5992 segment_number: 5,
5993 }];
5994
5995 let workflow = AhbWorkflow {
5996 pruefidentifikator: "13017".to_string(),
5997 description: "Test".to_string(),
5998 communication_direction: None,
5999 ub_definitions: BTreeMap::new(),
6000 package_conditions: Default::default(),
6001 fields: vec![AhbFieldRule {
6002 group_ahb_status: None,
6003 segment_path: "SG10/STS/C556/9013".to_string(),
6004 name: "Code".to_string(),
6005 ahb_status: "X".to_string(),
6006 element_index: Some(2),
6007 component_index: Some(0),
6008 codes: vec![
6009 AhbCodeRule {
6010 value: "E01".into(),
6011 description: "Code 1".into(),
6012 ahb_status: "X [901] [4P0..1]".into(),
6013 },
6014 AhbCodeRule {
6015 value: "E02".into(),
6016 description: "Code 2".into(),
6017 ahb_status: "X [901] [4P0..1]".into(),
6018 },
6019 ],
6020 parent_group_ahb_status: Some("Muss".to_string()),
6021 segment_ahb_status: None,
6022 mig_number: None,
6023 format: None,
6024 }],
6025 };
6026
6027 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
6028 let pkg_errors: Vec<_> = report
6029 .by_category(ValidationCategory::Ahb)
6030 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
6031 .collect();
6032 assert!(
6033 pkg_errors.is_empty(),
6034 "1 code within [4P0..1] bounds — no error, got: {:?}",
6035 pkg_errors
6036 );
6037 }
6038
6039 #[test]
6040 fn test_package_binds_only_where_its_condition_holds() {
6041 let report_for = |package_condition: CR| {
6045 let validator =
6046 EdifactValidator::new(MockEvaluator::new(vec![(902, package_condition)]));
6047 let segments = vec![OwnedSegment {
6048 id: "STS".into(),
6049 elements: vec![vec![], vec![], vec!["E09".into()]],
6050 segment_number: 5,
6051 }];
6052 let workflow = AhbWorkflow {
6053 pruefidentifikator: "13017".to_string(),
6054 description: "Test".to_string(),
6055 communication_direction: None,
6056 ub_definitions: BTreeMap::new(),
6057 package_conditions: BTreeMap::from([(5, ConditionExpr::Ref(902))]),
6058 fields: vec![AhbFieldRule {
6059 group_ahb_status: None,
6060 segment_path: "SG10/STS/C556/9013".to_string(),
6061 name: "Code".to_string(),
6062 ahb_status: "X".to_string(),
6063 element_index: Some(2),
6064 component_index: Some(0),
6065 codes: vec![
6066 AhbCodeRule {
6067 value: "E01".into(),
6068 description: "Code 1".into(),
6069 ahb_status: "X [5P1..1]".into(),
6070 },
6071 AhbCodeRule {
6072 value: "E09".into(),
6073 description: "Code 9".into(),
6074 ahb_status: "X".into(),
6075 },
6076 ],
6077 parent_group_ahb_status: Some("Muss".to_string()),
6078 segment_ahb_status: None,
6079 mig_number: None,
6080 format: None,
6081 }],
6082 };
6083 validator
6084 .validate(
6085 &segments,
6086 &workflow,
6087 &NoOpExternalProvider,
6088 ValidationLevel::Full,
6089 )
6090 .by_category(ValidationCategory::Ahb)
6091 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
6092 .count()
6093 };
6094 assert_eq!(report_for(CR::False), 0, "[902] false: [5P] does not bind");
6095 assert_eq!(report_for(CR::True), 1, "[902] true: [5P1..1] binds");
6096 }
6097
6098 #[test]
6099 fn test_package_cardinality_scoped_per_group_instance() {
6100 use mig_types::navigator::GroupNavigator;
6108
6109 struct TwoSg10s {
6110 sts_a: OwnedSegment,
6111 sts_b: OwnedSegment,
6112 }
6113 impl GroupNavigator for TwoSg10s {
6114 fn find_segments_in_group(
6115 &self,
6116 segment_id: &str,
6117 group_path: &[&str],
6118 instance_index: usize,
6119 ) -> Vec<OwnedSegment> {
6120 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
6121 match instance_index {
6122 0 => vec![self.sts_a.clone()],
6123 1 => vec![self.sts_b.clone()],
6124 _ => vec![],
6125 }
6126 } else {
6127 vec![]
6128 }
6129 }
6130 fn find_segments_with_qualifier_in_group(
6131 &self,
6132 _: &str,
6133 _: usize,
6134 _: &str,
6135 _: &[&str],
6136 _: usize,
6137 ) -> Vec<OwnedSegment> {
6138 vec![]
6139 }
6140 fn group_instance_count(&self, group_path: &[&str]) -> usize {
6141 match group_path {
6142 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
6143 ["SG5", "SG6", "SG9", "SG10"] => 2,
6144 _ => 0,
6145 }
6146 }
6147 }
6148
6149 let sts_a = OwnedSegment {
6150 id: "STS".into(),
6151 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
6152 segment_number: 10,
6153 };
6154 let sts_b = OwnedSegment {
6155 id: "STS".into(),
6156 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
6157 segment_number: 15,
6158 };
6159 let nav = TwoSg10s {
6160 sts_a: sts_a.clone(),
6161 sts_b: sts_b.clone(),
6162 };
6163
6164 let evaluator = MockEvaluator::all_true(&[]);
6165 let validator = EdifactValidator::new(evaluator);
6166 let external = NoOpExternalProvider;
6167
6168 let workflow = AhbWorkflow {
6169 pruefidentifikator: "13025".to_string(),
6170 description: "Test".to_string(),
6171 communication_direction: None,
6172 ub_definitions: BTreeMap::new(),
6173 package_conditions: Default::default(),
6174 fields: vec![AhbFieldRule {
6175 group_ahb_status: None,
6176 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
6177 name: "Statusanlaß, Code".to_string(),
6178 ahb_status: "X".to_string(),
6179 element_index: Some(2),
6180 component_index: Some(0),
6181 codes: vec![
6182 AhbCodeRule {
6183 value: "E01".into(),
6184 description: "Code 1".into(),
6185 ahb_status: "X [4P0..1]".into(),
6186 },
6187 AhbCodeRule {
6188 value: "E02".into(),
6189 description: "Code 2".into(),
6190 ahb_status: "X [4P0..1]".into(),
6191 },
6192 ],
6193 parent_group_ahb_status: Some("Muss".to_string()),
6194 segment_ahb_status: None,
6195 mig_number: None,
6196 format: None,
6197 }],
6198 };
6199
6200 let report = validator.validate_with_navigator(
6201 &[sts_a, sts_b],
6202 &workflow,
6203 &external,
6204 ValidationLevel::Full,
6205 &nav,
6206 );
6207 let pkg_errors: Vec<_> = report
6208 .by_category(ValidationCategory::Ahb)
6209 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
6210 .collect();
6211 assert!(
6212 pkg_errors.is_empty(),
6213 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
6214 pkg_errors
6215 );
6216 }
6217
6218 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
6219 OwnedSegment {
6220 id: id.to_string(),
6221 elements: elements
6222 .into_iter()
6223 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
6224 .collect(),
6225 segment_number: 0,
6226 }
6227 }
6228
6229 #[test]
6240 fn test_package_cardinality_scoped_to_rule_mig_variant() {
6241 use mig_types::navigator::GroupNavigator;
6242
6243 struct TwoSg8Variants {
6244 seq_z01: OwnedSegment,
6245 seq_z45: OwnedSegment,
6246 }
6247 impl GroupNavigator for TwoSg8Variants {
6248 fn find_segments_in_group(
6249 &self,
6250 segment_id: &str,
6251 group_path: &[&str],
6252 instance_index: usize,
6253 ) -> Vec<OwnedSegment> {
6254 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
6255 match instance_index {
6256 0 => vec![self.seq_z01.clone()],
6257 1 => vec![self.seq_z45.clone()],
6258 _ => vec![],
6259 }
6260 } else {
6261 vec![]
6262 }
6263 }
6264 fn find_segments_with_qualifier_in_group(
6265 &self,
6266 _: &str,
6267 _: usize,
6268 _: &str,
6269 _: &[&str],
6270 _: usize,
6271 ) -> Vec<OwnedSegment> {
6272 vec![]
6273 }
6274 fn group_instance_count(&self, group_path: &[&str]) -> usize {
6275 match group_path {
6276 ["SG4"] => 1,
6277 ["SG4", "SG8"] => 2,
6278 _ => 0,
6279 }
6280 }
6281 fn instance_has_mig_number(
6282 &self,
6283 group_path: &[&str],
6284 instance_index: usize,
6285 mig_number: &str,
6286 ) -> bool {
6287 if group_path != ["SG4", "SG8"] {
6288 return true;
6289 }
6290 match (instance_index, mig_number) {
6291 (0, "00115") => true,
6292 (0, _) => false,
6293 (1, "00171") => true,
6294 (1, _) => false,
6295 _ => false,
6296 }
6297 }
6298 }
6299
6300 let seq_z01 = OwnedSegment {
6301 id: "SEQ".into(),
6302 elements: vec![vec!["Z01".into()], vec!["1".into()]],
6303 segment_number: 10,
6304 };
6305 let seq_z45 = OwnedSegment {
6306 id: "SEQ".into(),
6307 elements: vec![vec!["Z45".into()], vec!["1".into()]],
6308 segment_number: 20,
6309 };
6310 let nav = TwoSg8Variants {
6311 seq_z01: seq_z01.clone(),
6312 seq_z45: seq_z45.clone(),
6313 };
6314
6315 let evaluator = MockEvaluator::all_true(&[]);
6316 let validator = EdifactValidator::new(evaluator);
6317 let external = NoOpExternalProvider;
6318
6319 let workflow = AhbWorkflow {
6320 pruefidentifikator: "55218".to_string(),
6321 description: "Test".to_string(),
6322 communication_direction: None,
6323 ub_definitions: BTreeMap::new(),
6324 package_conditions: Default::default(),
6325 fields: vec![AhbFieldRule {
6326 group_ahb_status: None,
6327 segment_path: "SG4/SG8/SEQ/1229".to_string(),
6328 name: "Handlung, Code".to_string(),
6329 ahb_status: "X".to_string(),
6330 element_index: Some(0),
6331 component_index: Some(0),
6332 codes: vec![
6333 AhbCodeRule {
6334 value: "Z45".into(),
6335 description: "NNA".into(),
6336 ahb_status: "X [1P1..4294967295]".into(),
6337 },
6338 AhbCodeRule {
6339 value: "Z84".into(),
6340 description: "Differenz-NNA".into(),
6341 ahb_status: "X [1P0..4294967295]".into(),
6342 },
6343 ],
6344 parent_group_ahb_status: Some("Muss".to_string()),
6345 segment_ahb_status: None,
6346 mig_number: Some("00171".to_string()),
6347 format: None,
6348 }],
6349 };
6350
6351 let report = validator.validate_with_navigator(
6352 &[seq_z01, seq_z45],
6353 &workflow,
6354 &external,
6355 ValidationLevel::Full,
6356 &nav,
6357 );
6358 let pkg_errors: Vec<_> = report
6359 .by_category(ValidationCategory::Ahb)
6360 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
6361 .collect();
6362 assert!(
6363 pkg_errors.is_empty(),
6364 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
6365 pkg_errors
6366 );
6367 }
6368
6369 #[test]
6370 fn test_unt_count_correct() {
6371 let segments = vec![
6373 make_segment("UNH", vec![vec!["001"]]),
6374 make_segment("BGM", vec![vec!["E01"]]),
6375 make_segment("DTM", vec![vec!["137", "20250401"]]),
6376 make_segment("UNT", vec![vec!["4", "001"]]),
6377 ];
6378 assert!(
6379 validate_unt_segment_count(&segments).is_none(),
6380 "Correct count should produce no issue"
6381 );
6382 }
6383
6384 #[test]
6385 fn test_unt_count_mismatch() {
6386 let segments = vec![
6388 make_segment("UNH", vec![vec!["001"]]),
6389 make_segment("BGM", vec![vec!["E01"]]),
6390 make_segment("UNT", vec![vec!["5", "001"]]),
6391 ];
6392 let issue =
6393 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
6394 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
6395 assert_eq!(issue.severity, Severity::Error);
6396 assert!(narrate(&issue).contains("declared 5"));
6397 assert!(narrate(&issue).contains("actual 3"));
6398 }
6399
6400 #[test]
6401 fn test_unt_count_excludes_envelope() {
6402 let segments = vec![
6404 make_segment("UNA", vec![]),
6405 make_segment("UNB", vec![vec!["UNOC", "3"]]),
6406 make_segment("UNH", vec![vec!["001"]]),
6407 make_segment("BGM", vec![vec!["E01"]]),
6408 make_segment("UNT", vec![vec!["3", "001"]]),
6409 make_segment("UNZ", vec![vec!["1"]]),
6410 ];
6411 assert!(
6412 validate_unt_segment_count(&segments).is_none(),
6413 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
6414 );
6415 }
6416
6417 #[test]
6418 fn test_unt_count_no_unt_returns_none() {
6419 let segments = vec![
6420 make_segment("UNH", vec![vec!["001"]]),
6421 make_segment("BGM", vec![vec!["E01"]]),
6422 ];
6423 assert!(
6424 validate_unt_segment_count(&segments).is_none(),
6425 "No UNT segment should return None (not our problem)"
6426 );
6427 }
6428
6429 #[test]
6430 fn test_unt_count_rejects_multi_message_input() {
6431 let segments = vec![
6433 make_segment("UNH", vec![vec!["001"]]),
6434 make_segment("BGM", vec![vec!["E01"]]),
6435 make_segment("UNT", vec![vec!["3", "001"]]),
6436 make_segment("UNH", vec![vec!["002"]]),
6437 make_segment("BGM", vec![vec!["E02"]]),
6438 make_segment("UNT", vec![vec!["3", "002"]]),
6439 ];
6440 let issue = validate_unt_segment_count(&segments)
6441 .expect("Multi-message input should produce an error");
6442 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
6443 assert!(
6444 narrate(&issue).contains("2 UNH"),
6445 "Should mention UNH count: {}",
6446 narrate(&issue)
6447 );
6448 }
6449
6450 #[test]
6451 fn test_code_validation_accepts_multi_code_variant_qualifier() {
6452 let evaluator = MockEvaluator::new(vec![]);
6459 let validator = EdifactValidator::new(evaluator);
6460 let external = NoOpExternalProvider;
6461
6462 let rff_z39 = OwnedSegment {
6463 id: "RFF".to_string(),
6464 elements: vec![
6465 vec!["RFF".to_string()],
6466 vec!["Z39".to_string(), "REF1".to_string()],
6467 ],
6468 segment_number: 1,
6469 };
6470
6471 let workflow = AhbWorkflow {
6472 pruefidentifikator: "55035".to_string(),
6473 description: "Test".to_string(),
6474 communication_direction: None,
6475 fields: vec![
6476 AhbFieldRule {
6478 group_ahb_status: None,
6479 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6480 name: "Referenznummer Qualifier".to_string(),
6481 ahb_status: "Muss".to_string(),
6482 codes: vec![
6483 AhbCodeRule {
6484 value: "Z31".to_string(),
6485 description: "".to_string(),
6486 ahb_status: "X".to_string(),
6487 },
6488 AhbCodeRule {
6489 value: "Z39".to_string(),
6490 description: "".to_string(),
6491 ahb_status: "X".to_string(),
6492 },
6493 ],
6494 parent_group_ahb_status: None,
6495 segment_ahb_status: None,
6496 element_index: Some(1),
6497 component_index: Some(0),
6498 mig_number: Some("00075".to_string()),
6499 format: None,
6500 },
6501 AhbFieldRule {
6503 group_ahb_status: None,
6504 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6505 name: "Referenznummer Qualifier".to_string(),
6506 ahb_status: "Muss".to_string(),
6507 codes: vec![AhbCodeRule {
6508 value: "Z33".to_string(),
6509 description: "".to_string(),
6510 ahb_status: "X".to_string(),
6511 }],
6512 parent_group_ahb_status: None,
6513 segment_ahb_status: None,
6514 element_index: Some(1),
6515 component_index: Some(0),
6516 mig_number: Some("00078".to_string()),
6517 format: None,
6518 },
6519 ],
6520 ub_definitions: BTreeMap::new(),
6521 package_conditions: Default::default(),
6522 };
6523
6524 let report = validator.validate(
6525 &[rff_z39],
6526 &workflow,
6527 &external,
6528 ValidationLevel::Conditions,
6529 );
6530
6531 let code_errors: Vec<_> = report
6532 .by_category(ValidationCategory::Code)
6533 .filter(|i| {
6534 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6535 })
6536 .collect();
6537 assert!(
6538 code_errors.is_empty(),
6539 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
6540 code_errors
6541 );
6542 }
6543
6544 #[test]
6545 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
6546 let evaluator = MockEvaluator::new(vec![]);
6552 let validator = EdifactValidator::new(evaluator);
6553 let external = NoOpExternalProvider;
6554
6555 let pia_5_z12 = OwnedSegment {
6559 id: "PIA".to_string(),
6560 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
6561 segment_number: 1,
6562 };
6563 let pia_5_srw = OwnedSegment {
6564 id: "PIA".to_string(),
6565 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
6566 segment_number: 2,
6567 };
6568
6569 let make_rules = |mig: &str, composite_code: &str| {
6570 vec![
6571 AhbFieldRule {
6572 group_ahb_status: None,
6573 segment_path: "SG4/SG8/PIA/4347".to_string(),
6574 name: "Produkt-ID-Funktion".to_string(),
6575 ahb_status: "Muss".to_string(),
6576 codes: vec![AhbCodeRule {
6577 value: "5".to_string(),
6578 description: "".to_string(),
6579 ahb_status: "X".to_string(),
6580 }],
6581 parent_group_ahb_status: None,
6582 segment_ahb_status: None,
6583 element_index: Some(0),
6584 component_index: Some(0),
6585 mig_number: Some(mig.to_string()),
6586 format: None,
6587 },
6588 AhbFieldRule {
6589 group_ahb_status: None,
6590 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
6591 name: "Artikel/Dienstleistung-Identifikator".to_string(),
6592 ahb_status: "Muss".to_string(),
6593 codes: vec![AhbCodeRule {
6594 value: composite_code.to_string(),
6595 description: "".to_string(),
6596 ahb_status: "X".to_string(),
6597 }],
6598 parent_group_ahb_status: None,
6599 segment_ahb_status: None,
6600 element_index: Some(1),
6601 component_index: Some(0),
6602 mig_number: Some(mig.to_string()),
6603 format: None,
6604 },
6605 ]
6606 };
6607
6608 let mut fields = make_rules("00108", "Z12");
6609 fields.extend(make_rules("00197", "SRW"));
6610
6611 let workflow = AhbWorkflow {
6612 pruefidentifikator: "55035".to_string(),
6613 description: "Test".to_string(),
6614 communication_direction: None,
6615 fields,
6616 ub_definitions: BTreeMap::new(),
6617 package_conditions: Default::default(),
6618 };
6619
6620 let report = validator.validate(
6621 &[pia_5_z12, pia_5_srw],
6622 &workflow,
6623 &external,
6624 ValidationLevel::Conditions,
6625 );
6626
6627 let code_errors: Vec<_> = report
6628 .by_category(ValidationCategory::Code)
6629 .filter(|i| {
6630 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6631 })
6632 .collect();
6633 assert!(
6634 code_errors.is_empty(),
6635 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
6636 code_errors
6637 );
6638 }
6639}