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