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
2214 .ahb_status
2215 .filter(|s| crate::eval::binds_once(s, once))
2216 {
2217 let variant = child
2218 .fields
2219 .first()
2220 .and_then(|n| n.rule.mig_number.clone())
2221 .unwrap_or_default();
2222 let entry = counts
2223 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
2224 .or_insert((0, status.to_string()));
2225 entry.0 += 1;
2226 }
2227 path.push((child.group_id, child.instance_index));
2228 visit(child, path, once, ctx, counts);
2229 path.pop();
2230 }
2231 }
2232 let mut path = vec![(transaction.group_id, transaction_index)];
2233 visit(transaction, &mut path, once, ctx, &mut counts);
2234 for ((path, _), (count, status)) in counts {
2235 if count > 1 {
2236 report.add_issue(
2237 ValidationIssue::new(
2238 Severity::Error,
2239 IssueKind::RepeatedInTransaction {
2240 path: path.clone(),
2241 count,
2242 },
2243 )
2244 .with_field_path(path)
2245 .with_rule(status)
2246 .with_instance_index(transaction_index),
2247 );
2248 }
2249 }
2250}
2251
2252#[allow(clippy::too_many_arguments)]
2259fn refuse_segments<E: ConditionEvaluator>(
2260 fields: &[AhbNode],
2261 group_status: Option<&str>,
2262 path: &[(&str, usize)],
2263 scoped_ctx: &EvaluationContext,
2264 evaluator: &E,
2265 ub_definitions: &BTreeMap<String, ConditionExpr>,
2266 instance_index: usize,
2267 report: &mut ValidationReport,
2268) {
2269 let mut seen: Vec<u32> = Vec::new();
2270 for node in fields {
2271 let rule = node.rule;
2272 let (Some(status), Some(value), Some(number)) = (
2273 rule.segment_ahb_status.as_deref(),
2274 node.value,
2275 node.matched_segment_number,
2276 ) else {
2277 continue;
2278 };
2279 let qualifier =
2282 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
2283 if !qualifier
2284 || seen.contains(&number)
2285 || Some(status) == group_status
2286 || !rule.codes.iter().any(|c| c.value == value)
2287 {
2288 continue;
2289 }
2290 seen.push(number);
2291 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2292 continue;
2293 }
2294 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2295 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
2296 let failed = ConditionExprEvaluator::new(evaluator).false_ids_with_ub(
2297 status,
2298 scoped_ctx,
2299 ub_definitions,
2300 );
2301 report.add_issue(
2302 ValidationIssue::new(
2303 Severity::Warning,
2304 IssueKind::SegmentNotAllowed {
2305 segment: segment.join("/"),
2306 failed,
2307 },
2308 )
2309 .with_field_path(segment.join("/"))
2310 .with_rule(status)
2311 .with_instance_index(instance_index)
2312 .with_position(crate::SegmentPosition::at(number)),
2313 );
2314 }
2315}
2316
2317fn report_segment_counts<E: ConditionEvaluator>(
2324 group: &AhbGroupNode,
2325 path: &[(&str, usize)],
2326 scoped_ctx: &EvaluationContext,
2327 evaluator: &E,
2328 instance_index: usize,
2329 report: &mut ValidationReport,
2330) {
2331 if crate::eval::repetition_limits(evaluator.message_type()).is_empty() {
2332 return;
2333 }
2334 let mut seen: HashSet<Option<&str>> = HashSet::new();
2335 for node in &group.fields {
2336 let rule = node.rule;
2337 let Some(status) = rule.segment_ahb_status.as_deref() else {
2338 continue;
2339 };
2340 if rule.element_index.unwrap_or(0) != 0
2341 || rule.component_index.unwrap_or(0) != 0
2342 || rule.codes.is_empty()
2343 || !seen.insert(rule.mig_number.as_deref())
2344 {
2345 continue;
2346 }
2347 let tag = extract_segment_id(&rule.segment_path);
2348 let sent: Vec<_> = scoped_ctx
2349 .find_segments_at(path, &tag)
2350 .into_iter()
2351 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2352 .collect();
2353 if sent.is_empty()
2354 || !crate::eval::breaks_repetition_limit(status, evaluator, scoped_ctx, sent.len())
2355 {
2356 continue;
2357 }
2358 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2359 segment.push(&tag);
2360 let mut issue = ValidationIssue::new(
2361 Severity::Error,
2362 IssueKind::RepetitionCount {
2363 path: segment.join("/"),
2364 count: sent.len(),
2365 },
2366 )
2367 .with_field_path(segment.join("/"))
2368 .with_rule(status)
2369 .with_instance_index(instance_index);
2370 if let Some(num) = node.matched_segment_number {
2371 issue = issue.with_position(crate::SegmentPosition::at(num));
2372 }
2373 report.add_issue(issue);
2374 }
2375}
2376
2377fn report_off_count_per_id<E: ConditionEvaluator>(
2386 transaction: &AhbGroupNode,
2387 tx: usize,
2388 ctx: &EvaluationContext,
2389 evaluator: &E,
2390 ub_definitions: &BTreeMap<String, ConditionExpr>,
2391 report: &mut ValidationReport,
2392) {
2393 let mut judged: Vec<(&str, Vec<&str>)> = Vec::new();
2394 for child in &transaction.children {
2395 let Some(status) = child.ahb_status else {
2396 continue;
2397 };
2398 let per_package = status.contains("[2002]");
2399 if child.group_id != "SG8"
2400 || !(per_package || status.contains("[2004]") || status.contains("[2005]"))
2401 || crate::eval::refuses_presence(status, evaluator, ctx, ub_definitions)
2402 {
2403 continue;
2404 }
2405 let Some(codes) = child.fields.iter().find_map(|node| {
2406 let rule = node.rule;
2407 (extract_segment_id(&rule.segment_path) == "SEQ"
2408 && rule.element_index.unwrap_or(0) == 0
2409 && rule.component_index.unwrap_or(0) == 0
2410 && !rule.codes.is_empty())
2411 .then(|| {
2412 rule.codes
2413 .iter()
2414 .map(|c| c.value.as_str())
2415 .collect::<Vec<_>>()
2416 })
2417 }) else {
2418 continue;
2419 };
2420 if judged.iter().any(|(s, c)| *s == status && *c == codes) {
2421 continue;
2422 }
2423 let position = child.fields.iter().find_map(|n| n.matched_segment_number);
2424 let (off, label) = if per_package {
2425 let off =
2426 crate::eval::sg4_instances::sg8_off_count_per_produktpaket(ctx, "SG4", tx, &codes);
2427 (off, "Produktpaket-ID")
2428 } else {
2429 let off =
2430 crate::eval::periods::sg8_off_count_per_zeitraum(ctx, "SG4", tx, &codes, status);
2431 (off, "Zeitraum-ID")
2432 };
2433 for (id, count) in off {
2434 let mut issue = ValidationIssue::new(
2435 Severity::Error,
2436 IssueKind::RepetitionCount {
2437 path: "SG4/SG8".to_string(),
2438 count,
2439 },
2440 )
2441 .with_field_path("SG4/SG8")
2442 .with_rule(status)
2443 .with_instance_index(tx);
2444 issue.expected_value = Some(format!("{label} {id}"));
2445 if let Some(num) = position {
2446 issue = issue.with_position(crate::SegmentPosition::at(num));
2447 }
2448 report.add_issue(issue);
2449 }
2450 judged.push((status, codes));
2451 }
2452}
2453
2454#[allow(clippy::too_many_arguments)]
2460fn report_repeated_in_group<E: ConditionEvaluator>(
2461 group: &AhbGroupNode,
2462 path: &[(&str, usize)],
2463 scoped_ctx: &EvaluationContext,
2464 evaluator: &E,
2465 ub_definitions: &BTreeMap<String, ConditionExpr>,
2466 instance_index: usize,
2467 report: &mut ValidationReport,
2468) {
2469 if crate::eval::counted_repetitions(evaluator.message_type()).is_empty() {
2470 return;
2471 }
2472 let mut reported: HashSet<&str> = HashSet::new();
2473 for child in &group.children {
2474 let Some(status) = child.ahb_status else {
2475 continue;
2476 };
2477 let count = group
2478 .children
2479 .iter()
2480 .filter(|c| c.group_id == child.group_id)
2481 .count();
2482 if count < 2 || reported.contains(child.group_id) {
2485 continue;
2486 }
2487 let mut child_path = path.to_vec();
2488 child_path.push((child.group_id, child.instance_index));
2489 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2490 if !crate::eval::refuses_count(status, evaluator, &child_ctx, ub_definitions) {
2491 continue;
2492 }
2493 reported.insert(child.group_id);
2494 let group_path: Vec<&str> = child_path.iter().map(|(g, _)| *g).collect();
2495 let mut issue = ValidationIssue::new(
2496 Severity::Error,
2497 IssueKind::RepetitionCount {
2498 path: group_path.join("/"),
2499 count,
2500 },
2501 )
2502 .with_field_path(group_path.join("/"))
2503 .with_rule(status)
2504 .with_instance_index(instance_index);
2505 if let Some(num) = child.fields.iter().find_map(|n| n.matched_segment_number) {
2506 issue = issue.with_position(crate::SegmentPosition::at(num));
2507 }
2508 report.add_issue(issue);
2509 }
2510}
2511
2512#[allow(clippy::too_many_arguments)]
2521fn walk_group_instance<E: ConditionEvaluator>(
2522 group: &AhbGroupNode,
2523 path: &[(&str, usize)],
2524 scoped_ctx: &EvaluationContext,
2525 expr_eval: &ConditionExprEvaluator<E>,
2526 evaluator: &E,
2527 ub_definitions: &BTreeMap<String, ConditionExpr>,
2528 tag_migs: &HashMap<String, HashSet<&str>>,
2529 instance_index: usize,
2530 report_segments: bool,
2531 statuses_only: bool,
2532 report: &mut ValidationReport,
2533) {
2534 let mut group_refused = false;
2537 if let Some(status) = group.ahb_status {
2538 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2539 group_refused = true;
2540 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2541 let failed = ConditionExprEvaluator::new(evaluator).false_ids_with_ub(
2542 status,
2543 scoped_ctx,
2544 ub_definitions,
2545 );
2546 let mut issue = ValidationIssue::new(
2547 Severity::Warning,
2548 IssueKind::GroupNotAllowed {
2549 group: group_path.join("/"),
2550 failed,
2551 },
2552 )
2553 .with_field_path(group_path.join("/"))
2554 .with_rule(status)
2555 .with_instance_index(instance_index);
2556 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
2557 issue = issue.with_position(crate::SegmentPosition::at(num));
2558 }
2559 report.add_issue(issue);
2560 }
2561 }
2562 if report_segments && !group_refused {
2563 refuse_segments(
2564 &group.fields,
2565 group.ahb_status,
2566 path,
2567 scoped_ctx,
2568 evaluator,
2569 ub_definitions,
2570 instance_index,
2571 report,
2572 );
2573 }
2574 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
2578 .fields
2579 .iter()
2580 .filter(|n| {
2581 n.rule.element_index.unwrap_or(0) == 0
2582 && n.rule.component_index.unwrap_or(0) == 0
2583 && !n.rule.codes.is_empty()
2584 })
2585 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
2586 .collect();
2587 let holds = |node: &AhbNode| {
2588 let (Some(codes), Some(elements)) = (
2589 slot_codes.get(&node.rule.mig_number.as_deref()),
2590 node.segment_elements,
2591 ) else {
2592 return true;
2593 };
2594 let qualifier = elements
2595 .first()
2596 .and_then(|e| e.first())
2597 .map(String::as_str)
2598 .unwrap_or_default();
2599 codes.iter().any(|c| c.value == qualifier)
2600 };
2601 for node in &group.fields {
2602 evaluate_node(
2603 node,
2604 scoped_ctx,
2605 expr_eval,
2606 evaluator,
2607 ub_definitions,
2608 tag_migs,
2609 Some(instance_index),
2610 holds(node),
2611 statuses_only,
2612 report,
2613 );
2614 }
2615 if !statuses_only {
2616 report_repeated_in_group(
2617 group,
2618 path,
2619 scoped_ctx,
2620 evaluator,
2621 ub_definitions,
2622 instance_index,
2623 report,
2624 );
2625 report_segment_counts(group, path, scoped_ctx, evaluator, instance_index, report);
2626 }
2627 for child in &group.children {
2628 let mut child_path = path.to_vec();
2629 child_path.push((child.group_id, child.instance_index));
2630 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2631 walk_group_instance(
2632 child,
2633 &child_path,
2634 &child_ctx,
2635 expr_eval,
2636 evaluator,
2637 ub_definitions,
2638 tag_migs,
2639 instance_index,
2640 report_segments,
2641 statuses_only,
2642 report,
2643 );
2644 }
2645}
2646
2647#[allow(clippy::too_many_arguments)]
2657fn report_absent_children<E: ConditionEvaluator>(
2658 group: &AhbGroupNode,
2659 path: &[(&str, usize)],
2660 scoped_ctx: &EvaluationContext,
2661 absent_eval: &ConditionExprEvaluator<crate::eval::AbsentTarget<'_, E>>,
2662 evaluator: &E,
2663 ub_definitions: &BTreeMap<String, ConditionExpr>,
2664 instance_index: usize,
2665 report: &mut ValidationReport,
2666) {
2667 if group.ahb_status.is_some_and(|status| {
2668 crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions)
2669 }) {
2670 return;
2671 }
2672 for rule in &group.absent_children {
2673 if should_skip_for_parent_group(rule, absent_eval, scoped_ctx, ub_definitions)
2674 || !is_parent_group_required(rule, absent_eval, scoped_ctx, ub_definitions)
2675 {
2676 continue;
2677 }
2678 let required = |status: &str| {
2679 let resolved = absent_eval.resolve_status(status, scoped_ctx, ub_definitions);
2680 resolved.result == ConditionResult::True && resolved.kind == Some(StatusKind::Muss)
2681 };
2682 let package_choice = |status: &str| {
2686 ConditionParser::parse_status_branches(status, ub_definitions).is_ok_and(|branches| {
2687 matches!(branches.as_slice(), [branch]
2688 if branch.kind == Some(StatusKind::Muss)
2689 && branch
2690 .condition
2691 .as_ref()
2692 .is_some_and(crate::eval::packages_allow_none))
2693 })
2694 };
2695 let segment_required = match rule.segment_ahb_status.as_deref() {
2696 Some(status) => required(status),
2697 None => true,
2698 };
2699 if !segment_required
2700 || !(required(&rule.ahb_status)
2701 || (rule.segment_ahb_status.is_some() && package_choice(&rule.ahb_status)))
2702 {
2703 continue;
2704 }
2705 let mut issue = ValidationIssue::new(
2706 Severity::Error,
2707 IssueKind::MissingRequiredField {
2708 field_name: rule.name.clone(),
2709 },
2710 )
2711 .with_field_path(&rule.segment_path)
2712 .with_rule(&rule.ahb_status)
2713 .with_instance_index(instance_index);
2714 if let Some(first_code) = rule.codes.first() {
2715 issue.expected_value = Some(first_code.value.clone());
2716 }
2717 report.add_issue(attach_field_position(issue, rule));
2718 }
2719 for child in &group.children {
2720 let mut child_path = path.to_vec();
2721 child_path.push((child.group_id, child.instance_index));
2722 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2723 report_absent_children(
2724 child,
2725 &child_path,
2726 &child_ctx,
2727 absent_eval,
2728 evaluator,
2729 ub_definitions,
2730 instance_index,
2731 report,
2732 );
2733 }
2734}
2735
2736fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2737 match expr {
2738 ConditionExpr::Package { id, min, max } => {
2739 out.push((*id, *min, *max));
2740 }
2741 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2742 for e in exprs {
2743 collect_packages(e, out);
2744 }
2745 }
2746 ConditionExpr::Xor(left, right) => {
2747 collect_packages(left, out);
2748 collect_packages(right, out);
2749 }
2750 ConditionExpr::Not(inner) => {
2751 collect_packages(inner, out);
2752 }
2753 ConditionExpr::Ref(_) => {}
2754 }
2755}
2756
2757fn is_optional_segment_status(status: &str) -> bool {
2767 let trimmed = status.trim();
2768 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
2769}
2770
2771fn is_qualifier_field(path: &str) -> bool {
2781 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
2782 matches!(parts.len(), 2 | 3)
2784}
2785
2786#[derive(Default)]
2794struct MigCodeBucket<'a> {
2795 qualifier_position: Option<(usize, usize)>,
2798 qualifier_values: HashSet<&'a str>,
2801 codes: BTreeMap<(usize, usize), HashSet<&'a str>>,
2804}
2805
2806fn partition_codes_by_mig(
2811 workflow: &AhbWorkflow,
2812) -> BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket<'_>>> {
2813 let mut out: BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket>> =
2814 BTreeMap::new();
2815 for field in &workflow.fields {
2816 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2817 continue;
2818 }
2819 if codes_are_alternatives(field) {
2822 continue;
2823 }
2824 let tag = extract_segment_id(&field.segment_path);
2825 let group_key = extract_group_path_key(&field.segment_path);
2826 let mig = field.mig_number.clone();
2827 let el = field.element_index.unwrap_or(0);
2828 let c = field.component_index.unwrap_or(0);
2829
2830 let bucket = out
2831 .entry((group_key, tag))
2832 .or_default()
2833 .entry(mig)
2834 .or_default();
2835
2836 let required: Vec<&str> = field
2843 .codes
2844 .iter()
2845 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2846 .map(|code| code.value.as_str())
2847 .collect();
2848
2849 if !required.is_empty() {
2856 if bucket.qualifier_position.is_none() {
2857 bucket.qualifier_position = Some((el, c));
2858 }
2859 if bucket.qualifier_position == Some((el, c)) {
2860 bucket.qualifier_values.extend(required.iter().copied());
2861 }
2862 }
2863
2864 for v in required {
2865 bucket.codes.entry((el, c)).or_default().insert(v);
2866 }
2867 }
2868 out
2869}
2870
2871fn global_qualifiers_by_tag(
2877 by_loc: &BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket<'_>>>,
2878) -> BTreeMap<String, BTreeMap<(usize, usize), HashSet<String>>> {
2879 let mut out: BTreeMap<String, BTreeMap<(usize, usize), HashSet<String>>> = BTreeMap::new();
2880 for ((_group, tag), migs) in by_loc {
2881 let tag_entry = out.entry(tag.clone()).or_default();
2882 for bucket in migs.values() {
2883 if let Some(pos) = bucket.qualifier_position {
2884 tag_entry
2885 .entry(pos)
2886 .or_default()
2887 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2888 }
2889 }
2890 }
2891 out
2892}
2893
2894fn matches_qualifier(seg: &OwnedSegment, bucket: &MigCodeBucket<'_>) -> bool {
2896 bucket.qualifier_position.is_some_and(|(el, c)| {
2897 seg.elements
2898 .get(el)
2899 .and_then(|e| e.get(c))
2900 .is_some_and(|v| bucket.qualifier_values.contains(v.as_str()))
2901 })
2902}
2903
2904fn data_slot_entries(
2910 workflow: &AhbWorkflow,
2911) -> HashMap<(String, String), Vec<(String, Option<String>)>> {
2912 let mut out: HashMap<(String, String), Vec<(String, Option<String>)>> = HashMap::new();
2913 let mut tags_by_mig: HashMap<String, BTreeMap<&str, Vec<String>>> = HashMap::new();
2918 for f in &workflow.fields {
2919 if let Some(n) = f.mig_number.as_deref() {
2920 let tags = tags_by_mig
2921 .entry(extract_group_path_key(&f.segment_path))
2922 .or_default()
2923 .entry(n)
2924 .or_default();
2925 let tag = extract_segment_id(&f.segment_path);
2926 if tags.last() != Some(&tag) {
2927 tags.push(tag);
2928 }
2929 }
2930 }
2931 let mut judged: HashSet<(&str, &str)> = HashSet::new();
2932 for field in &workflow.fields {
2933 let leading =
2934 field.element_index.unwrap_or(0) == 0 && field.component_index.unwrap_or(0) == 0;
2935 let Some(mig) = field.mig_number.as_deref() else {
2936 continue;
2937 };
2938 if !field.codes.is_empty() || !leading || !is_qualifier_field(&field.segment_path) {
2939 continue;
2940 }
2941 if !judged.insert((field.segment_path.as_str(), mig)) {
2942 continue;
2943 }
2944 let tag = extract_segment_id(&field.segment_path);
2945 let group_key = extract_group_path_key(&field.segment_path);
2946 if group_key.is_empty() {
2947 continue;
2948 }
2949 let entry = tags_by_mig.get(&group_key).and_then(|migs| {
2952 migs.range(..mig).rev().find_map(|(n, tags)| {
2953 tags.iter()
2954 .rev()
2955 .find(|t| **t != tag)
2956 .map(|t| (*n, t.clone()))
2957 })
2958 });
2959 if let Some((n, entry_tag)) = entry {
2960 let slot = (entry_tag, Some(n.to_string()));
2961 let entries = out.entry((group_key, tag)).or_default();
2962 if !entries.contains(&slot) {
2963 entries.push(slot);
2964 }
2965 }
2966 }
2967 out
2968}
2969
2970fn match_segment_to_mig<'a, 'b>(
2981 seg: &OwnedSegment,
2982 migs: &'a BTreeMap<Option<String>, MigCodeBucket<'b>>,
2983) -> Option<&'a MigCodeBucket<'b>> {
2984 let actual_at = |el: usize, c: usize| -> &str {
2985 seg.elements
2986 .get(el)
2987 .and_then(|e| e.get(c))
2988 .map(|s| s.as_str())
2989 .unwrap_or("")
2990 };
2991
2992 let mut best: Option<&MigCodeBucket> = None;
2993 let mut best_matches = 0usize;
2994
2995 for bucket in migs.values() {
2996 let Some((el, c)) = bucket.qualifier_position else {
2997 continue;
2998 };
2999 if !bucket.qualifier_values.contains(actual_at(el, c)) {
3000 continue;
3001 }
3002 let extra_matches = bucket
3003 .codes
3004 .iter()
3005 .filter(|(pos, _)| **pos != (el, c))
3006 .filter(|((e, k), allowed)| {
3007 let v = actual_at(*e, *k);
3008 !v.is_empty() && allowed.contains(v)
3009 })
3010 .count();
3011 if best.is_none() || extra_matches > best_matches {
3012 best = Some(bucket);
3013 best_matches = extra_matches;
3014 }
3015 }
3016 best
3017}
3018
3019fn extract_group_path_key(path: &str) -> String {
3024 let sg_parts: Vec<&str> = path
3025 .split('/')
3026 .take_while(|p| p.starts_with("SG"))
3027 .collect();
3028 sg_parts.join("/")
3029}
3030
3031fn extract_segment_id(path: &str) -> String {
3033 for part in path.split('/') {
3034 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
3036 continue;
3037 }
3038 if part.len() >= 3
3040 && part
3041 .chars()
3042 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
3043 {
3044 return part.to_string();
3045 }
3046 }
3047 path.split('/').next_back().unwrap_or(path).to_string()
3049}
3050
3051pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
3063 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
3065 if unh_count > 1 {
3066 return Some(ValidationIssue::new(
3067 Severity::Error,
3068 IssueKind::UntCountNotVerifiable { unh_count },
3069 ));
3070 }
3071
3072 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
3074 let declared: usize = unt.get_element(0).parse().ok()?;
3075
3076 let actual = segments
3078 .iter()
3079 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
3080 .count();
3081
3082 if declared != actual {
3083 Some(
3084 ValidationIssue::new(
3085 Severity::Error,
3086 IssueKind::UntSegmentCountMismatch { declared, actual },
3087 )
3088 .with_field_path("UNT/0074")
3089 .with_expected(actual.to_string())
3090 .with_actual(declared.to_string()),
3091 )
3092 } else {
3093 None
3094 }
3095}
3096
3097#[cfg(test)]
3098mod tests {
3099
3100 #[test]
3101 fn a_numeric_length_counts_digits_only() {
3102 assert_eq!(value_length("n..15", "-12.50"), 4);
3103 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
3104 assert_eq!(value_length("n5", "55001"), 5);
3105 assert_eq!(value_length("an..3", "Z.1"), 3);
3106 assert_eq!(value_length("an..35", "Müller"), 6);
3107 }
3108 use super::super::codes::ErrorCodes;
3109 use super::super::issue::ValidationCategory;
3110 use super::*;
3111 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
3112 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
3113 use std::collections::HashMap;
3114
3115 fn narrate(issue: &ValidationIssue) -> String {
3118 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
3119 }
3120
3121 struct MockEvaluator {
3123 results: HashMap<u32, CR>,
3124 }
3125
3126 impl MockEvaluator {
3127 fn new(results: Vec<(u32, CR)>) -> Self {
3128 Self {
3129 results: results.into_iter().collect(),
3130 }
3131 }
3132
3133 fn all_true(ids: &[u32]) -> Self {
3134 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
3135 }
3136 }
3137
3138 impl ConditionEvaluator for MockEvaluator {
3139 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
3140 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
3141 }
3142 fn is_external(&self, _condition: u32) -> bool {
3143 false
3144 }
3145 fn message_type(&self) -> &str {
3146 "UTILMD"
3147 }
3148 fn format_version(&self) -> &str {
3149 "FV2510"
3150 }
3151 }
3152
3153 #[test]
3156 fn a_present_code_is_judged_by_its_own_status() {
3157 let code = |value: &str, status: &str| AhbCodeRule {
3159 value: value.to_string(),
3160 description: String::new(),
3161 ahb_status: status.to_string(),
3162 };
3163 let field = AhbFieldRule {
3164 segment_path: "SG4/STS/C556/9013".to_string(),
3165 ahb_status: "X [192]".to_string(),
3166 codes: vec![code("E01", "X [192]"), code("E03", "X")],
3167 ..Default::default()
3168 };
3169 assert_eq!(governing_status(&field, Some("E03")), "X");
3170 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
3171 assert_eq!(governing_status(&field, None), "X [192]");
3172 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
3173
3174 let own = AhbFieldRule {
3176 ahb_status: "X [931]".to_string(),
3177 codes: vec![code("303", "X")],
3178 ..Default::default()
3179 };
3180 assert_eq!(governing_status(&own, Some("303")), "X [931]");
3181 assert!(!codes_are_alternatives(&own));
3182
3183 let alternatives = AhbFieldRule {
3186 ahb_status: "X [153] ∧ [538]".to_string(),
3187 codes: vec![
3188 code("Z74", "X [103] ∧ [537]"),
3189 code("Z75", "X [103] ∧ [537]"),
3190 ],
3191 ..Default::default()
3192 };
3193 assert!(codes_are_alternatives(&alternatives));
3194 assert_eq!(
3195 governing_status(&alternatives, Some("Z74")),
3196 "X [103] ∧ [537]"
3197 );
3198 assert_eq!(
3199 governing_status(&alternatives, Some("A01")),
3200 "X [153] ∧ [538]"
3201 );
3202 assert_eq!(governing_status(&alternatives, None), "X [153] ∧ [538]");
3203
3204 let restricting = AhbFieldRule {
3206 ahb_status: "X [2] ∨ [3]".to_string(),
3207 codes: vec![code("UNH", "X"), code("UNT", "X")],
3208 ..Default::default()
3209 };
3210 assert!(!codes_are_alternatives(&restricting));
3211 assert_eq!(governing_status(&restricting, Some("UNH")), "X [2] ∨ [3]");
3212 }
3213
3214 #[test]
3215 fn test_extract_segment_id_simple() {
3216 assert_eq!(extract_segment_id("NAD"), "NAD");
3217 }
3218
3219 #[test]
3220 fn test_extract_segment_id_with_sg_prefix() {
3221 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
3222 }
3223
3224 #[test]
3225 fn test_extract_segment_id_nested_sg() {
3226 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
3227 }
3228
3229 #[test]
3232 fn test_validate_missing_mandatory_field() {
3233 let evaluator = MockEvaluator::all_true(&[182, 152]);
3234 let validator = EdifactValidator::new(evaluator);
3235 let external = NoOpExternalProvider;
3236
3237 let workflow = AhbWorkflow {
3238 pruefidentifikator: "11001".to_string(),
3239 description: "Test".to_string(),
3240 communication_direction: None,
3241 fields: vec![AhbFieldRule {
3242 segment_path: "SG2/NAD/C082/3039".to_string(),
3243 name: "MP-ID des MSB".to_string(),
3244 ahb_status: "Muss [182] ∧ [152]".to_string(),
3245 codes: vec![],
3246 parent_group_ahb_status: None,
3247 segment_ahb_status: None,
3248 ..Default::default()
3249 }],
3250 ub_definitions: BTreeMap::new(),
3251 package_conditions: Default::default(),
3252 };
3253
3254 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3256
3257 assert!(!report.is_valid());
3259 let errors: Vec<_> = report.errors().collect();
3260 assert_eq!(errors.len(), 1);
3261 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3262 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
3263 }
3264
3265 #[test]
3266 fn test_validate_condition_false_no_error() {
3267 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
3269 let validator = EdifactValidator::new(evaluator);
3270 let external = NoOpExternalProvider;
3271
3272 let workflow = AhbWorkflow {
3273 pruefidentifikator: "11001".to_string(),
3274 description: "Test".to_string(),
3275 communication_direction: None,
3276 fields: vec![AhbFieldRule {
3277 segment_path: "NAD".to_string(),
3278 name: "Partnerrolle".to_string(),
3279 ahb_status: "Muss [182] ∧ [152]".to_string(),
3280 codes: vec![],
3281 parent_group_ahb_status: None,
3282 segment_ahb_status: None,
3283 ..Default::default()
3284 }],
3285 ub_definitions: BTreeMap::new(),
3286 package_conditions: Default::default(),
3287 };
3288
3289 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3290
3291 assert!(report.is_valid());
3293 }
3294
3295 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
3297 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3298 let workflow = AhbWorkflow {
3299 pruefidentifikator: "11001".to_string(),
3300 description: "Test".to_string(),
3301 communication_direction: None,
3302 fields: vec![AhbFieldRule {
3303 segment_path: "SG2/NAD/C082/3039".to_string(),
3304 name: "MP-ID".to_string(),
3305 ahb_status: status.to_string(),
3306 ..Default::default()
3307 }],
3308 ub_definitions: BTreeMap::new(),
3309 package_conditions: Default::default(),
3310 };
3311 validator.validate(
3312 &[],
3313 &workflow,
3314 &NoOpExternalProvider,
3315 ValidationLevel::Conditions,
3316 )
3317 }
3318
3319 #[test]
3320 fn a_later_soll_line_alone_does_not_require_the_field() {
3321 let report = validate_absent_field(
3323 "Muss [2119] \r\nSoll [130]",
3324 vec![(2119, CR::False), (130, CR::True)],
3325 );
3326 assert!(
3327 report.is_valid(),
3328 "{:?}",
3329 report.errors().collect::<Vec<_>>()
3330 );
3331 }
3332
3333 #[test]
3334 fn a_later_muss_line_requires_the_field() {
3335 let report = validate_absent_field(
3337 "Soll [47]\r\nMuss [46]",
3338 vec![(47, CR::False), (46, CR::True)],
3339 );
3340 let errors: Vec<_> = report.errors().collect();
3341 assert_eq!(errors.len(), 1);
3342 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3343 }
3344
3345 #[test]
3346 fn a_weaker_unknown_line_does_not_stop_a_muss() {
3347 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
3349 let errors: Vec<_> = report.errors().collect();
3350 assert_eq!(errors.len(), 1);
3351 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3352 }
3353
3354 #[test]
3355 fn a_stricter_unknown_line_leaves_the_status_undecided() {
3356 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
3357 assert!(report.is_valid());
3358 let infos: Vec<_> = report.infos().collect();
3359 assert_eq!(infos.len(), 1, "{infos:?}");
3360 match &infos[0].kind {
3361 IssueKind::ConditionUnknown { unresolved, .. } => {
3362 assert_eq!(unresolved.missing, vec![2119]);
3363 }
3364 other => panic!("expected ConditionUnknown, got {other:?}"),
3365 }
3366 }
3367
3368 #[test]
3369 fn test_validate_condition_unknown_adds_info() {
3370 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
3372 let validator = EdifactValidator::new(evaluator);
3374 let external = NoOpExternalProvider;
3375
3376 let workflow = AhbWorkflow {
3377 pruefidentifikator: "11001".to_string(),
3378 description: "Test".to_string(),
3379 communication_direction: None,
3380 fields: vec![AhbFieldRule {
3381 segment_path: "NAD".to_string(),
3382 name: "Partnerrolle".to_string(),
3383 ahb_status: "Muss [182] ∧ [152]".to_string(),
3384 codes: vec![],
3385 parent_group_ahb_status: None,
3386 segment_ahb_status: None,
3387 ..Default::default()
3388 }],
3389 ub_definitions: BTreeMap::new(),
3390 package_conditions: Default::default(),
3391 };
3392
3393 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3394
3395 assert!(report.is_valid());
3397 let infos: Vec<_> = report.infos().collect();
3398 assert_eq!(infos.len(), 1);
3399 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
3400 }
3401
3402 #[test]
3403 fn test_validate_structure_level_skips_conditions() {
3404 let evaluator = MockEvaluator::all_true(&[182, 152]);
3405 let validator = EdifactValidator::new(evaluator);
3406 let external = NoOpExternalProvider;
3407
3408 let workflow = AhbWorkflow {
3409 pruefidentifikator: "11001".to_string(),
3410 description: "Test".to_string(),
3411 communication_direction: None,
3412 fields: vec![AhbFieldRule {
3413 segment_path: "NAD".to_string(),
3414 name: "Partnerrolle".to_string(),
3415 ahb_status: "Muss [182] ∧ [152]".to_string(),
3416 codes: vec![],
3417 parent_group_ahb_status: None,
3418 segment_ahb_status: None,
3419 ..Default::default()
3420 }],
3421 ub_definitions: BTreeMap::new(),
3422 package_conditions: Default::default(),
3423 };
3424
3425 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
3427
3428 assert!(report.is_valid());
3430 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
3431 }
3432
3433 #[test]
3434 fn test_validate_empty_workflow_no_condition_errors() {
3435 let evaluator = MockEvaluator::all_true(&[]);
3436 let validator = EdifactValidator::new(evaluator);
3437 let external = NoOpExternalProvider;
3438
3439 let empty_workflow = AhbWorkflow {
3440 pruefidentifikator: String::new(),
3441 description: String::new(),
3442 communication_direction: None,
3443 fields: vec![],
3444 ub_definitions: BTreeMap::new(),
3445 package_conditions: Default::default(),
3446 };
3447
3448 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
3449
3450 assert!(report.is_valid());
3451 }
3452
3453 #[test]
3454 fn test_validate_bare_muss_always_required() {
3455 let evaluator = MockEvaluator::new(vec![]);
3456 let validator = EdifactValidator::new(evaluator);
3457 let external = NoOpExternalProvider;
3458
3459 let workflow = AhbWorkflow {
3460 pruefidentifikator: "55001".to_string(),
3461 description: "Test".to_string(),
3462 communication_direction: Some("NB an LF".to_string()),
3463 fields: vec![AhbFieldRule {
3464 segment_path: "SG2/NAD/3035".to_string(),
3465 name: "Partnerrolle".to_string(),
3466 ahb_status: "Muss".to_string(), codes: vec![],
3468 parent_group_ahb_status: None,
3469 segment_ahb_status: None,
3470 ..Default::default()
3471 }],
3472 ub_definitions: BTreeMap::new(),
3473 package_conditions: Default::default(),
3474 };
3475
3476 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3477
3478 assert!(!report.is_valid());
3480 assert_eq!(report.error_count(), 1);
3481 }
3482
3483 #[test]
3484 fn test_validate_x_status_is_mandatory() {
3485 let evaluator = MockEvaluator::new(vec![]);
3486 let validator = EdifactValidator::new(evaluator);
3487 let external = NoOpExternalProvider;
3488
3489 let workflow = AhbWorkflow {
3490 pruefidentifikator: "55001".to_string(),
3491 description: "Test".to_string(),
3492 communication_direction: None,
3493 fields: vec![AhbFieldRule {
3494 segment_path: "DTM".to_string(),
3495 name: "Datum".to_string(),
3496 ahb_status: "X".to_string(),
3497 codes: vec![],
3498 parent_group_ahb_status: None,
3499 segment_ahb_status: None,
3500 ..Default::default()
3501 }],
3502 ub_definitions: BTreeMap::new(),
3503 package_conditions: Default::default(),
3504 };
3505
3506 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3507
3508 assert!(!report.is_valid());
3509 let errors: Vec<_> = report.errors().collect();
3510 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3511 }
3512
3513 #[test]
3514 fn test_validate_soll_not_mandatory() {
3515 let evaluator = MockEvaluator::new(vec![]);
3516 let validator = EdifactValidator::new(evaluator);
3517 let external = NoOpExternalProvider;
3518
3519 let workflow = AhbWorkflow {
3520 pruefidentifikator: "55001".to_string(),
3521 description: "Test".to_string(),
3522 communication_direction: None,
3523 fields: vec![AhbFieldRule {
3524 segment_path: "DTM".to_string(),
3525 name: "Datum".to_string(),
3526 ahb_status: "Soll".to_string(),
3527 codes: vec![],
3528 parent_group_ahb_status: None,
3529 segment_ahb_status: None,
3530 ..Default::default()
3531 }],
3532 ub_definitions: BTreeMap::new(),
3533 package_conditions: Default::default(),
3534 };
3535
3536 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3537
3538 assert!(report.is_valid());
3540 }
3541
3542 #[test]
3543 fn test_report_includes_metadata() {
3544 let evaluator = MockEvaluator::new(vec![]);
3545 let validator = EdifactValidator::new(evaluator);
3546 let external = NoOpExternalProvider;
3547
3548 let workflow = AhbWorkflow {
3549 pruefidentifikator: "55001".to_string(),
3550 description: String::new(),
3551 communication_direction: None,
3552 fields: vec![],
3553 ub_definitions: BTreeMap::new(),
3554 package_conditions: Default::default(),
3555 };
3556
3557 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
3558
3559 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
3560 assert_eq!(report.level, ValidationLevel::Full);
3561 assert_eq!(report.message_type, "UTILMD");
3562 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
3563 }
3564
3565 #[test]
3566 fn test_validate_with_navigator_returns_report() {
3567 let evaluator = MockEvaluator::all_true(&[]);
3568 let validator = EdifactValidator::new(evaluator);
3569 let external = NoOpExternalProvider;
3570 let nav = crate::eval::NoOpGroupNavigator;
3571
3572 let workflow = AhbWorkflow {
3573 pruefidentifikator: "55001".to_string(),
3574 description: "Test".to_string(),
3575 communication_direction: None,
3576 fields: vec![],
3577 ub_definitions: BTreeMap::new(),
3578 package_conditions: Default::default(),
3579 };
3580
3581 let report = validator.validate_with_navigator(
3582 &[],
3583 &workflow,
3584 &external,
3585 ValidationLevel::Full,
3586 &nav,
3587 );
3588 assert!(report.is_valid());
3589 }
3590
3591 #[test]
3592 fn test_code_validation_composite_paths_valid_codes() {
3593 let evaluator = MockEvaluator::new(vec![]);
3597 let validator = EdifactValidator::new(evaluator);
3598 let external = NoOpExternalProvider;
3599
3600 let unh_segment = OwnedSegment {
3601 id: "UNH".to_string(),
3602 elements: vec![
3603 vec!["ALEXANDE951842".to_string()],
3604 vec![
3605 "UTILMD".to_string(),
3606 "D".to_string(),
3607 "11A".to_string(),
3608 "UN".to_string(),
3609 "S2.1".to_string(),
3610 ],
3611 ],
3612 segment_number: 1,
3613 };
3614
3615 let workflow = AhbWorkflow {
3616 pruefidentifikator: "55001".to_string(),
3617 description: "Test".to_string(),
3618 communication_direction: None,
3619 fields: vec![
3620 AhbFieldRule {
3621 segment_path: "UNH/S009/0065".to_string(),
3622 name: "Nachrichtentyp".to_string(),
3623 ahb_status: "X".to_string(),
3624 codes: vec![AhbCodeRule {
3625 value: "UTILMD".to_string(),
3626 description: "Stammdaten".to_string(),
3627 ahb_status: "X".to_string(),
3628 }],
3629 parent_group_ahb_status: None,
3630 segment_ahb_status: None,
3631 element_index: Some(1),
3632 component_index: Some(0),
3633 ..Default::default()
3634 },
3635 AhbFieldRule {
3636 segment_path: "UNH/S009/0052".to_string(),
3637 name: "Version".to_string(),
3638 ahb_status: "X".to_string(),
3639 codes: vec![AhbCodeRule {
3640 value: "D".to_string(),
3641 description: "Draft".to_string(),
3642 ahb_status: "X".to_string(),
3643 }],
3644 parent_group_ahb_status: None,
3645 segment_ahb_status: None,
3646 element_index: Some(1),
3647 component_index: Some(1),
3648 ..Default::default()
3649 },
3650 ],
3651 ub_definitions: BTreeMap::new(),
3652 package_conditions: Default::default(),
3653 };
3654
3655 let report = validator.validate(
3656 &[unh_segment],
3657 &workflow,
3658 &external,
3659 ValidationLevel::Conditions,
3660 );
3661
3662 let code_errors: Vec<_> = report
3663 .by_category(ValidationCategory::Code)
3664 .filter(|i| i.severity == Severity::Error)
3665 .collect();
3666 assert!(
3667 code_errors.is_empty(),
3668 "Expected no code errors when composite values match allowed codes, got: {:?}",
3669 code_errors
3670 );
3671 }
3672
3673 #[test]
3674 fn test_code_validation_partitions_by_mig_number() {
3675 let evaluator = MockEvaluator::new(vec![]);
3679 let validator = EdifactValidator::new(evaluator);
3680 let external = NoOpExternalProvider;
3681
3682 let sts_7 = OwnedSegment {
3683 id: "STS".to_string(),
3684 elements: vec![
3685 vec!["7".to_string()],
3686 vec![String::new()],
3687 vec!["GH02".to_string()],
3688 vec!["ZW4".to_string()],
3689 ],
3690 segment_number: 1,
3691 };
3692 let sts_e01 = OwnedSegment {
3693 id: "STS".to_string(),
3694 elements: vec![
3695 vec!["E01".to_string()],
3696 vec![String::new()],
3697 vec!["A99".to_string(), "E_0614".to_string()],
3698 ],
3699 segment_number: 2,
3700 };
3701
3702 let workflow = AhbWorkflow {
3703 pruefidentifikator: "55018".to_string(),
3704 description: "Test".to_string(),
3705 communication_direction: None,
3706 fields: vec![
3707 AhbFieldRule {
3709 segment_path: "SG4/STS/C601/9015".to_string(),
3710 name: "Statuskategorie".to_string(),
3711 ahb_status: "X".to_string(),
3712 codes: vec![AhbCodeRule {
3713 value: "7".to_string(),
3714 description: "Transaktionsgrund".to_string(),
3715 ahb_status: "X".to_string(),
3716 }],
3717 parent_group_ahb_status: None,
3718 segment_ahb_status: None,
3719 element_index: Some(0),
3720 component_index: Some(0),
3721 mig_number: Some("00035".to_string()),
3722 format: None,
3723 },
3724 AhbFieldRule {
3725 segment_path: "SG4/STS/C556/9013".to_string(),
3726 name: "Statusanlaß".to_string(),
3727 ahb_status: "X".to_string(),
3728 codes: vec![AhbCodeRule {
3729 value: "E03".to_string(),
3730 description: "Transaktionsgrund".to_string(),
3731 ahb_status: "X".to_string(),
3732 }],
3733 parent_group_ahb_status: None,
3734 segment_ahb_status: None,
3735 element_index: Some(2),
3736 component_index: Some(0),
3737 mig_number: Some("00035".to_string()),
3738 format: None,
3739 },
3740 AhbFieldRule {
3742 segment_path: "SG4/STS/C601/9015".to_string(),
3743 name: "Statuskategorie".to_string(),
3744 ahb_status: "X".to_string(),
3745 codes: vec![AhbCodeRule {
3746 value: "E01".to_string(),
3747 description: "Antwort".to_string(),
3748 ahb_status: "X".to_string(),
3749 }],
3750 parent_group_ahb_status: None,
3751 segment_ahb_status: None,
3752 element_index: Some(0),
3753 component_index: Some(0),
3754 mig_number: Some("00036".to_string()),
3755 format: None,
3756 },
3757 ],
3758 ub_definitions: BTreeMap::new(),
3759 package_conditions: Default::default(),
3760 };
3761
3762 let report = validator.validate(
3763 &[sts_7, sts_e01],
3764 &workflow,
3765 &external,
3766 ValidationLevel::Conditions,
3767 );
3768
3769 let code_errors: Vec<_> = report
3770 .by_category(ValidationCategory::Code)
3771 .filter(|i| {
3772 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3773 })
3774 .collect();
3775 assert_eq!(
3776 code_errors.len(),
3777 1,
3778 "Expected one COD002 (for GH02 only), got: {:?}",
3779 code_errors
3780 );
3781 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3782 }
3783
3784 #[test]
3785 fn test_code_validation_composite_paths_detects_invalid_code() {
3786 let evaluator = MockEvaluator::new(vec![]);
3789 let validator = EdifactValidator::new(evaluator);
3790 let external = NoOpExternalProvider;
3791
3792 let sts_segment = OwnedSegment {
3793 id: "STS".to_string(),
3794 elements: vec![
3795 vec!["7".to_string()],
3796 vec![String::new()],
3797 vec!["GH02".to_string()],
3798 vec!["ZW4".to_string()],
3799 ],
3800 segment_number: 1,
3801 };
3802
3803 let workflow = AhbWorkflow {
3804 pruefidentifikator: "55018".to_string(),
3805 description: "Test".to_string(),
3806 communication_direction: None,
3807 fields: vec![AhbFieldRule {
3808 segment_path: "SG4/STS/C556/9013".to_string(),
3809 name: "Statusanlaß".to_string(),
3810 ahb_status: "X".to_string(),
3811 codes: vec![AhbCodeRule {
3812 value: "E03".to_string(),
3813 description: "Transaktionsgrund".to_string(),
3814 ahb_status: "X".to_string(),
3815 }],
3816 parent_group_ahb_status: None,
3817 segment_ahb_status: None,
3818 element_index: Some(2),
3819 component_index: Some(0),
3820 ..Default::default()
3821 }],
3822 ub_definitions: BTreeMap::new(),
3823 package_conditions: Default::default(),
3824 };
3825
3826 let report = validator.validate(
3827 &[sts_segment],
3828 &workflow,
3829 &external,
3830 ValidationLevel::Conditions,
3831 );
3832
3833 let code_errors: Vec<_> = report
3834 .by_category(ValidationCategory::Code)
3835 .filter(|i| {
3836 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3837 })
3838 .collect();
3839 assert_eq!(
3840 code_errors.len(),
3841 1,
3842 "Expected COD002 for GH02, got: {:?}",
3843 code_errors
3844 );
3845 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3846 }
3847
3848 #[test]
3849 fn test_cross_field_code_validation_valid_qualifiers() {
3850 let evaluator = MockEvaluator::new(vec![]);
3853 let validator = EdifactValidator::new(evaluator);
3854 let external = NoOpExternalProvider;
3855
3856 let nad_ms = OwnedSegment {
3857 id: "NAD".to_string(),
3858 elements: vec![vec!["MS".to_string()]],
3859 segment_number: 4,
3860 };
3861 let nad_mr = OwnedSegment {
3862 id: "NAD".to_string(),
3863 elements: vec![vec!["MR".to_string()]],
3864 segment_number: 5,
3865 };
3866
3867 let workflow = AhbWorkflow {
3868 pruefidentifikator: "55001".to_string(),
3869 description: "Test".to_string(),
3870 communication_direction: None,
3871 fields: vec![
3872 AhbFieldRule {
3873 segment_path: "SG2/NAD/3035".to_string(),
3874 name: "Absender".to_string(),
3875 ahb_status: "X".to_string(),
3876 codes: vec![AhbCodeRule {
3877 value: "MS".to_string(),
3878 description: "Absender".to_string(),
3879 ahb_status: "X".to_string(),
3880 }],
3881 parent_group_ahb_status: None,
3882 segment_ahb_status: None,
3883 ..Default::default()
3884 },
3885 AhbFieldRule {
3886 segment_path: "SG2/NAD/3035".to_string(),
3887 name: "Empfaenger".to_string(),
3888 ahb_status: "X".to_string(),
3889 codes: vec![AhbCodeRule {
3890 value: "MR".to_string(),
3891 description: "Empfaenger".to_string(),
3892 ahb_status: "X".to_string(),
3893 }],
3894 parent_group_ahb_status: None,
3895 segment_ahb_status: None,
3896 ..Default::default()
3897 },
3898 ],
3899 ub_definitions: BTreeMap::new(),
3900 package_conditions: Default::default(),
3901 };
3902
3903 let report = validator.validate(
3904 &[nad_ms, nad_mr],
3905 &workflow,
3906 &external,
3907 ValidationLevel::Conditions,
3908 );
3909
3910 let code_errors: Vec<_> = report
3911 .by_category(ValidationCategory::Code)
3912 .filter(|i| i.severity == Severity::Error)
3913 .collect();
3914 assert!(
3915 code_errors.is_empty(),
3916 "Expected no code errors for valid qualifiers, got: {:?}",
3917 code_errors
3918 );
3919 }
3920
3921 #[test]
3922 fn test_cross_field_code_validation_catches_invalid_qualifier() {
3923 let evaluator = MockEvaluator::new(vec![]);
3925 let validator = EdifactValidator::new(evaluator);
3926 let external = NoOpExternalProvider;
3927
3928 let nad_ms = OwnedSegment {
3929 id: "NAD".to_string(),
3930 elements: vec![vec!["MS".to_string()]],
3931 segment_number: 4,
3932 };
3933 let nad_mt = OwnedSegment {
3934 id: "NAD".to_string(),
3935 elements: vec![vec!["MT".to_string()]], segment_number: 5,
3937 };
3938
3939 let workflow = AhbWorkflow {
3940 pruefidentifikator: "55001".to_string(),
3941 description: "Test".to_string(),
3942 communication_direction: None,
3943 fields: vec![
3944 AhbFieldRule {
3945 segment_path: "SG2/NAD/3035".to_string(),
3946 name: "Absender".to_string(),
3947 ahb_status: "X".to_string(),
3948 codes: vec![AhbCodeRule {
3949 value: "MS".to_string(),
3950 description: "Absender".to_string(),
3951 ahb_status: "X".to_string(),
3952 }],
3953 parent_group_ahb_status: None,
3954 segment_ahb_status: None,
3955 ..Default::default()
3956 },
3957 AhbFieldRule {
3958 segment_path: "SG2/NAD/3035".to_string(),
3959 name: "Empfaenger".to_string(),
3960 ahb_status: "X".to_string(),
3961 codes: vec![AhbCodeRule {
3962 value: "MR".to_string(),
3963 description: "Empfaenger".to_string(),
3964 ahb_status: "X".to_string(),
3965 }],
3966 parent_group_ahb_status: None,
3967 segment_ahb_status: None,
3968 ..Default::default()
3969 },
3970 ],
3971 ub_definitions: BTreeMap::new(),
3972 package_conditions: Default::default(),
3973 };
3974
3975 let report = validator.validate(
3976 &[nad_ms, nad_mt],
3977 &workflow,
3978 &external,
3979 ValidationLevel::Conditions,
3980 );
3981
3982 let code_errors: Vec<_> = report
3983 .by_category(ValidationCategory::Code)
3984 .filter(|i| i.severity == Severity::Error)
3985 .collect();
3986 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
3987 assert!(narrate(code_errors[0]).contains("MT"));
3988 assert!(narrate(code_errors[0]).contains("MR"));
3989 assert!(narrate(code_errors[0]).contains("MS"));
3990 }
3991
3992 #[test]
3993 fn test_cross_field_code_validation_unions_across_groups() {
3994 let evaluator = MockEvaluator::new(vec![]);
3998 let validator = EdifactValidator::new(evaluator);
3999 let external = NoOpExternalProvider;
4000
4001 let segments = vec![
4002 OwnedSegment {
4003 id: "NAD".to_string(),
4004 elements: vec![vec!["MS".to_string()]],
4005 segment_number: 3,
4006 },
4007 OwnedSegment {
4008 id: "NAD".to_string(),
4009 elements: vec![vec!["MR".to_string()]],
4010 segment_number: 4,
4011 },
4012 OwnedSegment {
4013 id: "NAD".to_string(),
4014 elements: vec![vec!["Z04".to_string()]],
4015 segment_number: 20,
4016 },
4017 OwnedSegment {
4018 id: "NAD".to_string(),
4019 elements: vec![vec!["Z09".to_string()]],
4020 segment_number: 21,
4021 },
4022 OwnedSegment {
4023 id: "NAD".to_string(),
4024 elements: vec![vec!["MT".to_string()]], segment_number: 22,
4026 },
4027 ];
4028
4029 let workflow = AhbWorkflow {
4030 pruefidentifikator: "55001".to_string(),
4031 description: "Test".to_string(),
4032 communication_direction: None,
4033 fields: vec![
4034 AhbFieldRule {
4035 segment_path: "SG2/NAD/3035".to_string(),
4036 name: "Absender".to_string(),
4037 ahb_status: "X".to_string(),
4038 codes: vec![AhbCodeRule {
4039 value: "MS".to_string(),
4040 description: "Absender".to_string(),
4041 ahb_status: "X".to_string(),
4042 }],
4043 parent_group_ahb_status: None,
4044 segment_ahb_status: None,
4045 ..Default::default()
4046 },
4047 AhbFieldRule {
4048 segment_path: "SG2/NAD/3035".to_string(),
4049 name: "Empfaenger".to_string(),
4050 ahb_status: "X".to_string(),
4051 codes: vec![AhbCodeRule {
4052 value: "MR".to_string(),
4053 description: "Empfaenger".to_string(),
4054 ahb_status: "X".to_string(),
4055 }],
4056 parent_group_ahb_status: None,
4057 segment_ahb_status: None,
4058 ..Default::default()
4059 },
4060 AhbFieldRule {
4061 segment_path: "SG4/SG12/NAD/3035".to_string(),
4062 name: "Anschlussnutzer".to_string(),
4063 ahb_status: "X".to_string(),
4064 codes: vec![AhbCodeRule {
4065 value: "Z04".to_string(),
4066 description: "Anschlussnutzer".to_string(),
4067 ahb_status: "X".to_string(),
4068 }],
4069 parent_group_ahb_status: None,
4070 segment_ahb_status: None,
4071 ..Default::default()
4072 },
4073 AhbFieldRule {
4074 segment_path: "SG4/SG12/NAD/3035".to_string(),
4075 name: "Korrespondenzanschrift".to_string(),
4076 ahb_status: "X".to_string(),
4077 codes: vec![AhbCodeRule {
4078 value: "Z09".to_string(),
4079 description: "Korrespondenzanschrift".to_string(),
4080 ahb_status: "X".to_string(),
4081 }],
4082 parent_group_ahb_status: None,
4083 segment_ahb_status: None,
4084 ..Default::default()
4085 },
4086 ],
4087 ub_definitions: BTreeMap::new(),
4088 package_conditions: Default::default(),
4089 };
4090
4091 let report =
4092 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
4093
4094 let code_errors: Vec<_> = report
4095 .by_category(ValidationCategory::Code)
4096 .filter(|i| i.severity == Severity::Error)
4097 .collect();
4098 assert_eq!(
4099 code_errors.len(),
4100 1,
4101 "Expected exactly one COD002 error for MT, got: {:?}",
4102 code_errors
4103 );
4104 assert!(narrate(code_errors[0]).contains("MT"));
4105 }
4106
4107 #[test]
4108 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
4109 let evaluator = MockEvaluator::new(vec![]);
4118 let validator = EdifactValidator::new(evaluator);
4119 let external = NoOpExternalProvider;
4120
4121 let qty_67 = OwnedSegment {
4122 id: "QTY".to_string(),
4123 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
4124 segment_number: 10,
4125 };
4126
4127 let workflow = AhbWorkflow {
4128 pruefidentifikator: "13025".to_string(),
4129 description: "Test".to_string(),
4130 communication_direction: None,
4131 fields: vec![AhbFieldRule {
4132 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
4133 name: "Menge, Qualifier".to_string(),
4134 ahb_status: "X".to_string(),
4135 codes: vec![
4136 AhbCodeRule {
4137 value: "220".to_string(),
4138 description: "Wahrer Wert".to_string(),
4139 ahb_status: "X".to_string(),
4140 },
4141 AhbCodeRule {
4142 value: "67".to_string(),
4143 description: "Ersatzwert".to_string(),
4144 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
4145 },
4146 AhbCodeRule {
4147 value: "Z18".to_string(),
4148 description: "Vorläufiger Wert".to_string(),
4149 ahb_status: "X [35]".to_string(),
4150 },
4151 ],
4152 parent_group_ahb_status: None,
4153 segment_ahb_status: None,
4154 element_index: Some(0),
4155 component_index: Some(0),
4156 ..Default::default()
4157 }],
4158 ub_definitions: BTreeMap::new(),
4159 package_conditions: Default::default(),
4160 };
4161
4162 let report =
4163 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
4164
4165 let code_errors: Vec<_> = report
4166 .by_category(ValidationCategory::Code)
4167 .filter(|i| i.severity == Severity::Error)
4168 .collect();
4169 assert!(
4170 code_errors.is_empty(),
4171 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
4172 code_errors
4173 );
4174 }
4175
4176 #[test]
4177 fn test_is_qualifier_field_simple_paths() {
4178 assert!(is_qualifier_field("NAD/3035"));
4179 assert!(is_qualifier_field("SG2/NAD/3035"));
4180 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
4181 assert!(is_qualifier_field("LOC/3227"));
4182 }
4183
4184 #[test]
4185 fn test_is_qualifier_field_composite_paths() {
4186 assert!(is_qualifier_field("UNH/S009/0065"));
4190 assert!(is_qualifier_field("NAD/C082/3039"));
4191 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
4192 assert!(is_qualifier_field("SG4/STS/C556/9013"));
4193 }
4194
4195 #[test]
4196 fn test_is_qualifier_field_bare_segment() {
4197 assert!(!is_qualifier_field("NAD"));
4198 assert!(!is_qualifier_field("SG2/NAD"));
4199 }
4200
4201 #[test]
4202 fn test_is_qualifier_field_rejects_deep_paths() {
4203 assert!(!is_qualifier_field("SEG/A/B/C/D"));
4205 }
4206
4207 #[test]
4208 fn test_missing_qualifier_instance_is_detected() {
4209 let evaluator = MockEvaluator::new(vec![]);
4212 let validator = EdifactValidator::new(evaluator);
4213 let external = NoOpExternalProvider;
4214
4215 let nad_ms = OwnedSegment {
4216 id: "NAD".to_string(),
4217 elements: vec![vec!["MS".to_string()]],
4218 segment_number: 3,
4219 };
4220
4221 let workflow = AhbWorkflow {
4222 pruefidentifikator: "55001".to_string(),
4223 description: "Test".to_string(),
4224 communication_direction: None,
4225 fields: vec![
4226 AhbFieldRule {
4227 segment_path: "SG2/NAD/3035".to_string(),
4228 name: "Absender".to_string(),
4229 ahb_status: "X".to_string(),
4230 codes: vec![AhbCodeRule {
4231 value: "MS".to_string(),
4232 description: "Absender".to_string(),
4233 ahb_status: "X".to_string(),
4234 }],
4235 parent_group_ahb_status: None,
4236 segment_ahb_status: None,
4237 ..Default::default()
4238 },
4239 AhbFieldRule {
4240 segment_path: "SG2/NAD/3035".to_string(),
4241 name: "Empfaenger".to_string(),
4242 ahb_status: "Muss".to_string(),
4243 codes: vec![AhbCodeRule {
4244 value: "MR".to_string(),
4245 description: "Empfaenger".to_string(),
4246 ahb_status: "X".to_string(),
4247 }],
4248 parent_group_ahb_status: None,
4249 segment_ahb_status: None,
4250 ..Default::default()
4251 },
4252 ],
4253 ub_definitions: BTreeMap::new(),
4254 package_conditions: Default::default(),
4255 };
4256
4257 let report =
4258 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
4259
4260 let ahb_errors: Vec<_> = report
4261 .by_category(ValidationCategory::Ahb)
4262 .filter(|i| i.severity == Severity::Error)
4263 .collect();
4264 assert_eq!(
4265 ahb_errors.len(),
4266 1,
4267 "Expected AHB001 for missing NAD+MR, got: {:?}",
4268 ahb_errors
4269 );
4270 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4271 }
4272
4273 #[test]
4274 fn test_present_qualifier_instance_no_error() {
4275 let evaluator = MockEvaluator::new(vec![]);
4277 let validator = EdifactValidator::new(evaluator);
4278 let external = NoOpExternalProvider;
4279
4280 let segments = vec![
4281 OwnedSegment {
4282 id: "NAD".to_string(),
4283 elements: vec![vec!["MS".to_string()]],
4284 segment_number: 3,
4285 },
4286 OwnedSegment {
4287 id: "NAD".to_string(),
4288 elements: vec![vec!["MR".to_string()]],
4289 segment_number: 4,
4290 },
4291 ];
4292
4293 let workflow = AhbWorkflow {
4294 pruefidentifikator: "55001".to_string(),
4295 description: "Test".to_string(),
4296 communication_direction: None,
4297 fields: vec![
4298 AhbFieldRule {
4299 segment_path: "SG2/NAD/3035".to_string(),
4300 name: "Absender".to_string(),
4301 ahb_status: "Muss".to_string(),
4302 codes: vec![AhbCodeRule {
4303 value: "MS".to_string(),
4304 description: "Absender".to_string(),
4305 ahb_status: "X".to_string(),
4306 }],
4307 parent_group_ahb_status: None,
4308 segment_ahb_status: None,
4309 ..Default::default()
4310 },
4311 AhbFieldRule {
4312 segment_path: "SG2/NAD/3035".to_string(),
4313 name: "Empfaenger".to_string(),
4314 ahb_status: "Muss".to_string(),
4315 codes: vec![AhbCodeRule {
4316 value: "MR".to_string(),
4317 description: "Empfaenger".to_string(),
4318 ahb_status: "X".to_string(),
4319 }],
4320 parent_group_ahb_status: None,
4321 segment_ahb_status: None,
4322 ..Default::default()
4323 },
4324 ],
4325 ub_definitions: BTreeMap::new(),
4326 package_conditions: Default::default(),
4327 };
4328
4329 let report =
4330 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
4331
4332 let ahb_errors: Vec<_> = report
4333 .by_category(ValidationCategory::Ahb)
4334 .filter(|i| i.severity == Severity::Error)
4335 .collect();
4336 assert!(
4337 ahb_errors.is_empty(),
4338 "Expected no AHB001 errors, got: {:?}",
4339 ahb_errors
4340 );
4341 }
4342
4343 #[test]
4344 fn test_extract_group_path_key() {
4345 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
4346 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
4347 assert_eq!(extract_group_path_key("NAD/3035"), "");
4348 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
4349 }
4350
4351 fn validate_field_of_absent_group(
4353 group_status: &str,
4354 conditions: Vec<(u32, CR)>,
4355 ) -> ValidationReport {
4356 use mig_types::navigator::GroupNavigator;
4357
4358 struct NoSg9;
4359 impl GroupNavigator for NoSg9 {
4360 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4361 vec![]
4362 }
4363 fn find_segments_with_qualifier_in_group(
4364 &self,
4365 _: &str,
4366 _: usize,
4367 _: &str,
4368 _: &[&str],
4369 _: usize,
4370 ) -> Vec<OwnedSegment> {
4371 vec![]
4372 }
4373 fn group_instance_count(&self, _: &[&str]) -> usize {
4374 0
4375 }
4376 }
4377
4378 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
4379 let workflow = AhbWorkflow {
4380 pruefidentifikator: "44001".to_string(),
4381 description: "Test".to_string(),
4382 communication_direction: None,
4383 fields: vec![AhbFieldRule {
4384 segment_path: "SG9/QTY/C186/6060".to_string(),
4385 name: "Menge".to_string(),
4386 ahb_status: "X".to_string(),
4387 parent_group_ahb_status: Some(group_status.to_string()),
4388 ..Default::default()
4389 }],
4390 ub_definitions: BTreeMap::new(),
4391 package_conditions: Default::default(),
4392 };
4393 validator.validate_with_navigator(
4394 &[],
4395 &workflow,
4396 &NoOpExternalProvider,
4397 ValidationLevel::Conditions,
4398 &NoSg9,
4399 )
4400 }
4401
4402 #[test]
4403 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
4404 let report = validate_field_of_absent_group(
4406 "Soll [47]\r\nMuss [46]",
4407 vec![(47, CR::True), (46, CR::False)],
4408 );
4409 assert!(
4410 report.is_valid(),
4411 "{:?}",
4412 report.errors().collect::<Vec<_>>()
4413 );
4414 }
4415
4416 #[test]
4417 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
4418 let report = validate_field_of_absent_group(
4419 "Soll [47]\r\nMuss [46]",
4420 vec![(47, CR::False), (46, CR::True)],
4421 );
4422 let errors: Vec<_> = report.errors().collect();
4423 assert_eq!(errors.len(), 1, "{errors:?}");
4424 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
4425 }
4426
4427 #[test]
4428 fn test_absent_optional_group_no_missing_field_error() {
4429 use mig_types::navigator::GroupNavigator;
4432
4433 struct NavWithoutSG3;
4434 impl GroupNavigator for NavWithoutSG3 {
4435 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4436 vec![]
4437 }
4438 fn find_segments_with_qualifier_in_group(
4439 &self,
4440 _: &str,
4441 _: usize,
4442 _: &str,
4443 _: &[&str],
4444 _: usize,
4445 ) -> Vec<OwnedSegment> {
4446 vec![]
4447 }
4448 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4449 match group_path {
4450 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
4453 }
4454 }
4455 }
4456
4457 let evaluator = MockEvaluator::new(vec![]);
4458 let validator = EdifactValidator::new(evaluator);
4459 let external = NoOpExternalProvider;
4460 let nav = NavWithoutSG3;
4461
4462 let segments = vec![
4464 OwnedSegment {
4465 id: "NAD".into(),
4466 elements: vec![vec!["MS".into()]],
4467 segment_number: 3,
4468 },
4469 OwnedSegment {
4470 id: "NAD".into(),
4471 elements: vec![vec!["MR".into()]],
4472 segment_number: 4,
4473 },
4474 ];
4475
4476 let workflow = AhbWorkflow {
4477 pruefidentifikator: "55001".to_string(),
4478 description: "Test".to_string(),
4479 communication_direction: None,
4480 fields: vec![
4481 AhbFieldRule {
4482 segment_path: "SG2/SG3/CTA/3139".to_string(),
4483 name: "Funktion des Ansprechpartners, Code".to_string(),
4484 ahb_status: "Muss".to_string(),
4485 codes: vec![],
4486 parent_group_ahb_status: None,
4487 segment_ahb_status: None,
4488 ..Default::default()
4489 },
4490 AhbFieldRule {
4491 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
4492 name: "Name vom Ansprechpartner".to_string(),
4493 ahb_status: "X".to_string(),
4494 codes: vec![],
4495 parent_group_ahb_status: None,
4496 segment_ahb_status: None,
4497 ..Default::default()
4498 },
4499 ],
4500 ub_definitions: BTreeMap::new(),
4501 package_conditions: Default::default(),
4502 };
4503
4504 let report = validator.validate_with_navigator(
4505 &segments,
4506 &workflow,
4507 &external,
4508 ValidationLevel::Conditions,
4509 &nav,
4510 );
4511
4512 let ahb_errors: Vec<_> = report
4513 .by_category(ValidationCategory::Ahb)
4514 .filter(|i| i.severity == Severity::Error)
4515 .collect();
4516 assert!(
4517 ahb_errors.is_empty(),
4518 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
4519 ahb_errors
4520 );
4521 }
4522
4523 #[test]
4524 fn test_present_group_still_checks_mandatory_fields() {
4525 use mig_types::navigator::GroupNavigator;
4527
4528 struct NavWithSG3;
4529 impl GroupNavigator for NavWithSG3 {
4530 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4531 vec![]
4532 }
4533 fn find_segments_with_qualifier_in_group(
4534 &self,
4535 _: &str,
4536 _: usize,
4537 _: &str,
4538 _: &[&str],
4539 _: usize,
4540 ) -> Vec<OwnedSegment> {
4541 vec![]
4542 }
4543 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4544 match group_path {
4545 ["SG2"] => 1,
4546 ["SG2", "SG3"] => 1, _ => 0,
4548 }
4549 }
4550 }
4551
4552 let evaluator = MockEvaluator::new(vec![]);
4553 let validator = EdifactValidator::new(evaluator);
4554 let external = NoOpExternalProvider;
4555 let nav = NavWithSG3;
4556
4557 let segments = vec![OwnedSegment {
4559 id: "NAD".into(),
4560 elements: vec![vec!["MS".into()]],
4561 segment_number: 3,
4562 }];
4563
4564 let workflow = AhbWorkflow {
4565 pruefidentifikator: "55001".to_string(),
4566 description: "Test".to_string(),
4567 communication_direction: None,
4568 fields: vec![AhbFieldRule {
4569 segment_path: "SG2/SG3/CTA/3139".to_string(),
4570 name: "Funktion des Ansprechpartners, Code".to_string(),
4571 ahb_status: "Muss".to_string(),
4572 codes: vec![],
4573 parent_group_ahb_status: None,
4574 segment_ahb_status: None,
4575 ..Default::default()
4576 }],
4577 ub_definitions: BTreeMap::new(),
4578 package_conditions: Default::default(),
4579 };
4580
4581 let report = validator.validate_with_navigator(
4582 &segments,
4583 &workflow,
4584 &external,
4585 ValidationLevel::Conditions,
4586 &nav,
4587 );
4588
4589 let ahb_errors: Vec<_> = report
4590 .by_category(ValidationCategory::Ahb)
4591 .filter(|i| i.severity == Severity::Error)
4592 .collect();
4593 assert_eq!(
4594 ahb_errors.len(),
4595 1,
4596 "Expected AHB001 error when SG3 is present but CTA missing"
4597 );
4598 assert!(narrate(ahb_errors[0]).contains("CTA"));
4599 }
4600
4601 #[test]
4602 fn test_missing_qualifier_with_navigator_is_detected() {
4603 use mig_types::navigator::GroupNavigator;
4606
4607 struct NavWithSG2;
4608 impl GroupNavigator for NavWithSG2 {
4609 fn find_segments_in_group(
4610 &self,
4611 segment_id: &str,
4612 group_path: &[&str],
4613 instance_index: usize,
4614 ) -> Vec<OwnedSegment> {
4615 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
4616 vec![OwnedSegment {
4617 id: "NAD".into(),
4618 elements: vec![vec!["MS".into()]],
4619 segment_number: 3,
4620 }]
4621 } else {
4622 vec![]
4623 }
4624 }
4625 fn find_segments_with_qualifier_in_group(
4626 &self,
4627 _: &str,
4628 _: usize,
4629 _: &str,
4630 _: &[&str],
4631 _: usize,
4632 ) -> Vec<OwnedSegment> {
4633 vec![]
4634 }
4635 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4636 match group_path {
4637 ["SG2"] => 1,
4638 _ => 0,
4639 }
4640 }
4641 }
4642
4643 let evaluator = MockEvaluator::new(vec![]);
4644 let validator = EdifactValidator::new(evaluator);
4645 let external = NoOpExternalProvider;
4646 let nav = NavWithSG2;
4647
4648 let segments = vec![OwnedSegment {
4649 id: "NAD".into(),
4650 elements: vec![vec!["MS".into()]],
4651 segment_number: 3,
4652 }];
4653
4654 let workflow = AhbWorkflow {
4655 pruefidentifikator: "55001".to_string(),
4656 description: "Test".to_string(),
4657 communication_direction: None,
4658 fields: vec![
4659 AhbFieldRule {
4660 segment_path: "SG2/NAD/3035".to_string(),
4661 name: "Absender".to_string(),
4662 ahb_status: "X".to_string(),
4663 codes: vec![AhbCodeRule {
4664 value: "MS".to_string(),
4665 description: "Absender".to_string(),
4666 ahb_status: "X".to_string(),
4667 }],
4668 parent_group_ahb_status: None,
4669 segment_ahb_status: None,
4670 ..Default::default()
4671 },
4672 AhbFieldRule {
4673 segment_path: "SG2/NAD/3035".to_string(),
4674 name: "Empfaenger".to_string(),
4675 ahb_status: "Muss".to_string(),
4676 codes: vec![AhbCodeRule {
4677 value: "MR".to_string(),
4678 description: "Empfaenger".to_string(),
4679 ahb_status: "X".to_string(),
4680 }],
4681 parent_group_ahb_status: None,
4682 segment_ahb_status: None,
4683 ..Default::default()
4684 },
4685 ],
4686 ub_definitions: BTreeMap::new(),
4687 package_conditions: Default::default(),
4688 };
4689
4690 let report = validator.validate_with_navigator(
4691 &segments,
4692 &workflow,
4693 &external,
4694 ValidationLevel::Conditions,
4695 &nav,
4696 );
4697
4698 let ahb_errors: Vec<_> = report
4699 .by_category(ValidationCategory::Ahb)
4700 .filter(|i| i.severity == Severity::Error)
4701 .collect();
4702 assert_eq!(
4703 ahb_errors.len(),
4704 1,
4705 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
4706 ahb_errors
4707 );
4708 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4709 }
4710
4711 #[test]
4712 fn test_optional_group_variant_absent_no_error() {
4713 use mig_types::navigator::GroupNavigator;
4718
4719 struct TestNav;
4720 impl GroupNavigator for TestNav {
4721 fn find_segments_in_group(
4722 &self,
4723 segment_id: &str,
4724 group_path: &[&str],
4725 instance_index: usize,
4726 ) -> Vec<OwnedSegment> {
4727 match (segment_id, group_path, instance_index) {
4728 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
4729 id: "LOC".into(),
4730 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4731 segment_number: 10,
4732 }],
4733 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
4734 id: "NAD".into(),
4735 elements: vec![vec!["MS".into()]],
4736 segment_number: 3,
4737 }],
4738 _ => vec![],
4739 }
4740 }
4741 fn find_segments_with_qualifier_in_group(
4742 &self,
4743 _: &str,
4744 _: usize,
4745 _: &str,
4746 _: &[&str],
4747 _: usize,
4748 ) -> Vec<OwnedSegment> {
4749 vec![]
4750 }
4751 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4752 match group_path {
4753 ["SG2"] => 1,
4754 ["SG4"] => 1,
4755 ["SG4", "SG5"] => 1, _ => 0,
4757 }
4758 }
4759 }
4760
4761 let evaluator = MockEvaluator::new(vec![]);
4762 let validator = EdifactValidator::new(evaluator);
4763 let external = NoOpExternalProvider;
4764 let nav = TestNav;
4765
4766 let segments = vec![
4767 OwnedSegment {
4768 id: "NAD".into(),
4769 elements: vec![vec!["MS".into()]],
4770 segment_number: 3,
4771 },
4772 OwnedSegment {
4773 id: "LOC".into(),
4774 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4775 segment_number: 10,
4776 },
4777 ];
4778
4779 let workflow = AhbWorkflow {
4780 pruefidentifikator: "55001".to_string(),
4781 description: "Test".to_string(),
4782 communication_direction: None,
4783 fields: vec![
4784 AhbFieldRule {
4786 segment_path: "SG2/NAD/3035".to_string(),
4787 name: "Absender".to_string(),
4788 ahb_status: "X".to_string(),
4789 codes: vec![AhbCodeRule {
4790 value: "MS".to_string(),
4791 description: "Absender".to_string(),
4792 ahb_status: "X".to_string(),
4793 }],
4794 parent_group_ahb_status: Some("Muss".to_string()),
4795 segment_ahb_status: None,
4796 ..Default::default()
4797 },
4798 AhbFieldRule {
4799 segment_path: "SG2/NAD/3035".to_string(),
4800 name: "Empfaenger".to_string(),
4801 ahb_status: "Muss".to_string(),
4802 codes: vec![AhbCodeRule {
4803 value: "MR".to_string(),
4804 description: "Empfaenger".to_string(),
4805 ahb_status: "X".to_string(),
4806 }],
4807 parent_group_ahb_status: Some("Muss".to_string()),
4808 segment_ahb_status: None,
4809 ..Default::default()
4810 },
4811 AhbFieldRule {
4813 segment_path: "SG4/SG5/LOC/3227".to_string(),
4814 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4815 ahb_status: "X".to_string(),
4816 codes: vec![AhbCodeRule {
4817 value: "Z16".to_string(),
4818 description: "Marktlokation".to_string(),
4819 ahb_status: "X".to_string(),
4820 }],
4821 parent_group_ahb_status: Some("Kann".to_string()),
4822 segment_ahb_status: None,
4823 ..Default::default()
4824 },
4825 AhbFieldRule {
4826 segment_path: "SG4/SG5/LOC/3227".to_string(),
4827 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4828 ahb_status: "Muss".to_string(),
4829 codes: vec![AhbCodeRule {
4830 value: "Z17".to_string(),
4831 description: "Messlokation".to_string(),
4832 ahb_status: "X".to_string(),
4833 }],
4834 parent_group_ahb_status: Some("Kann".to_string()),
4835 segment_ahb_status: None,
4836 ..Default::default()
4837 },
4838 ],
4839 ub_definitions: BTreeMap::new(),
4840 package_conditions: Default::default(),
4841 };
4842
4843 let report = validator.validate_with_navigator(
4844 &segments,
4845 &workflow,
4846 &external,
4847 ValidationLevel::Conditions,
4848 &nav,
4849 );
4850
4851 let ahb_errors: Vec<_> = report
4852 .by_category(ValidationCategory::Ahb)
4853 .filter(|i| i.severity == Severity::Error)
4854 .collect();
4855
4856 assert_eq!(
4859 ahb_errors.len(),
4860 1,
4861 "Expected only AHB001 for missing NAD+MR, got: {:?}",
4862 ahb_errors
4863 );
4864 assert!(
4865 narrate(ahb_errors[0]).contains("Empfaenger"),
4866 "Error should be for missing NAD+MR (Empfaenger)"
4867 );
4868 }
4869
4870 #[test]
4874 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4875 use mig_types::navigator::GroupNavigator;
4876
4877 struct TestNav;
4878 impl GroupNavigator for TestNav {
4879 fn find_segments_in_group(
4880 &self,
4881 segment_id: &str,
4882 group_path: &[&str],
4883 instance_index: usize,
4884 ) -> Vec<OwnedSegment> {
4885 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4886 vec![OwnedSegment {
4887 id: "SEQ".into(),
4888 elements: vec![vec!["Z03".into()]],
4889 segment_number: 10,
4890 }]
4891 } else {
4892 vec![]
4893 }
4894 }
4895 fn find_segments_with_qualifier_in_group(
4896 &self,
4897 _: &str,
4898 _: usize,
4899 _: &str,
4900 _: &[&str],
4901 _: usize,
4902 ) -> Vec<OwnedSegment> {
4903 vec![]
4904 }
4905 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4906 match group_path {
4907 ["SG4"] => 1,
4908 ["SG4", "SG8"] => 1,
4909 _ => 0,
4910 }
4911 }
4912 }
4913
4914 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
4915 let validator = EdifactValidator::new(evaluator);
4916 let segments = vec![OwnedSegment {
4917 id: "SEQ".into(),
4918 elements: vec![vec!["Z03".into()]],
4919 segment_number: 10,
4920 }];
4921 let seq = |code: &str, group: &str| AhbFieldRule {
4922 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4923 name: format!("Handlung, Code ({code})"),
4924 ahb_status: "X".to_string(),
4925 codes: vec![AhbCodeRule {
4926 value: code.to_string(),
4927 description: String::new(),
4928 ahb_status: "X".to_string(),
4929 }],
4930 parent_group_ahb_status: Some(group.to_string()),
4931 segment_ahb_status: None,
4932 ..Default::default()
4933 };
4934 let workflow = AhbWorkflow {
4935 pruefidentifikator: "44143".to_string(),
4936 description: "Test".to_string(),
4937 communication_direction: None,
4938 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
4939 ub_definitions: BTreeMap::new(),
4940 package_conditions: Default::default(),
4941 };
4942
4943 let report = validator.validate_with_navigator(
4944 &segments,
4945 &workflow,
4946 &NoOpExternalProvider,
4947 ValidationLevel::Conditions,
4948 &TestNav,
4949 );
4950 let missing: Vec<_> = report
4951 .issues
4952 .iter()
4953 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
4954 .collect();
4955 assert!(missing.is_empty(), "{missing:#?}");
4956 }
4957
4958 #[test]
4959 fn test_conditional_group_variant_absent_no_error() {
4960 use mig_types::navigator::GroupNavigator;
4965
4966 struct TestNav;
4967 impl GroupNavigator for TestNav {
4968 fn find_segments_in_group(
4969 &self,
4970 segment_id: &str,
4971 group_path: &[&str],
4972 instance_index: usize,
4973 ) -> Vec<OwnedSegment> {
4974 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
4975 vec![OwnedSegment {
4976 id: "LOC".into(),
4977 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4978 segment_number: 10,
4979 }]
4980 } else {
4981 vec![]
4982 }
4983 }
4984 fn find_segments_with_qualifier_in_group(
4985 &self,
4986 _: &str,
4987 _: usize,
4988 _: &str,
4989 _: &[&str],
4990 _: usize,
4991 ) -> Vec<OwnedSegment> {
4992 vec![]
4993 }
4994 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4995 match group_path {
4996 ["SG4"] => 1,
4997 ["SG4", "SG5"] => 1, _ => 0,
4999 }
5000 }
5001 }
5002
5003 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
5006 let validator = EdifactValidator::new(evaluator);
5007 let external = NoOpExternalProvider;
5008 let nav = TestNav;
5009
5010 let segments = vec![OwnedSegment {
5011 id: "LOC".into(),
5012 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
5013 segment_number: 10,
5014 }];
5015
5016 let workflow = AhbWorkflow {
5017 pruefidentifikator: "55001".to_string(),
5018 description: "Test".to_string(),
5019 communication_direction: None,
5020 fields: vec![
5021 AhbFieldRule {
5023 segment_path: "SG4/SG5/LOC/3227".to_string(),
5024 name: "Ortsangabe, Qualifier (Z16)".to_string(),
5025 ahb_status: "X".to_string(),
5026 codes: vec![AhbCodeRule {
5027 value: "Z16".to_string(),
5028 description: "Marktlokation".to_string(),
5029 ahb_status: "X".to_string(),
5030 }],
5031 parent_group_ahb_status: Some("Muss [2061]".to_string()),
5032 segment_ahb_status: None,
5033 ..Default::default()
5034 },
5035 AhbFieldRule {
5037 segment_path: "SG4/SG5/LOC/3227".to_string(),
5038 name: "Ortsangabe, Qualifier (Z17)".to_string(),
5039 ahb_status: "X".to_string(),
5040 codes: vec![AhbCodeRule {
5041 value: "Z17".to_string(),
5042 description: "Messlokation".to_string(),
5043 ahb_status: "X".to_string(),
5044 }],
5045 parent_group_ahb_status: Some("Soll [165]".to_string()),
5046 segment_ahb_status: None,
5047 ..Default::default()
5048 },
5049 ],
5050 ub_definitions: BTreeMap::new(),
5051 package_conditions: Default::default(),
5052 };
5053
5054 let report = validator.validate_with_navigator(
5055 &segments,
5056 &workflow,
5057 &external,
5058 ValidationLevel::Conditions,
5059 &nav,
5060 );
5061
5062 let ahb_errors: Vec<_> = report
5063 .by_category(ValidationCategory::Ahb)
5064 .filter(|i| i.severity == Severity::Error)
5065 .collect();
5066
5067 assert!(
5069 ahb_errors.is_empty(),
5070 "Expected no errors when conditional group variant [165]=False, got: {:?}",
5071 ahb_errors
5072 );
5073 }
5074
5075 #[test]
5076 fn test_conditional_group_variant_unknown_no_error() {
5077 let evaluator = MockEvaluator::new(vec![]);
5083 let validator = EdifactValidator::new(evaluator);
5084 let external = NoOpExternalProvider;
5085
5086 let workflow = AhbWorkflow {
5087 pruefidentifikator: "55001".to_string(),
5088 description: "Test".to_string(),
5089 communication_direction: None,
5090 fields: vec![AhbFieldRule {
5091 segment_path: "SG4/SG5/LOC/3227".to_string(),
5092 name: "Ortsangabe, Qualifier (Z17)".to_string(),
5093 ahb_status: "X".to_string(),
5094 codes: vec![AhbCodeRule {
5095 value: "Z17".to_string(),
5096 description: "Messlokation".to_string(),
5097 ahb_status: "X".to_string(),
5098 }],
5099 parent_group_ahb_status: Some("Soll [165]".to_string()),
5100 segment_ahb_status: None,
5101 ..Default::default()
5102 }],
5103 ub_definitions: BTreeMap::new(),
5104 package_conditions: Default::default(),
5105 };
5106
5107 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
5108
5109 let ahb_errors: Vec<_> = report
5110 .by_category(ValidationCategory::Ahb)
5111 .filter(|i| i.severity == Severity::Error)
5112 .collect();
5113
5114 assert!(
5116 ahb_errors.is_empty(),
5117 "Expected no errors when parent group condition is Unknown, got: {:?}",
5118 ahb_errors
5119 );
5120 }
5121
5122 #[test]
5123 fn test_segment_absent_within_present_group_no_error() {
5124 use mig_types::navigator::GroupNavigator;
5128
5129 struct TestNav;
5130 impl GroupNavigator for TestNav {
5131 fn find_segments_in_group(
5132 &self,
5133 segment_id: &str,
5134 group_path: &[&str],
5135 instance_index: usize,
5136 ) -> Vec<OwnedSegment> {
5137 if segment_id == "QTY"
5139 && group_path == ["SG5", "SG6", "SG9", "SG10"]
5140 && instance_index == 0
5141 {
5142 vec![OwnedSegment {
5143 id: "QTY".into(),
5144 elements: vec![vec!["220".into(), "0".into()]],
5145 segment_number: 14,
5146 }]
5147 } else {
5148 vec![]
5149 }
5150 }
5151 fn find_segments_with_qualifier_in_group(
5152 &self,
5153 _: &str,
5154 _: usize,
5155 _: &str,
5156 _: &[&str],
5157 _: usize,
5158 ) -> Vec<OwnedSegment> {
5159 vec![]
5160 }
5161 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5162 match group_path {
5163 ["SG5"] => 1,
5164 ["SG5", "SG6"] => 1,
5165 ["SG5", "SG6", "SG9"] => 1,
5166 ["SG5", "SG6", "SG9", "SG10"] => 1,
5167 _ => 0,
5168 }
5169 }
5170 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
5171 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
5173 }
5174 }
5175
5176 let evaluator = MockEvaluator::all_true(&[]);
5177 let validator = EdifactValidator::new(evaluator);
5178 let external = NoOpExternalProvider;
5179 let nav = TestNav;
5180
5181 let segments = vec![OwnedSegment {
5182 id: "QTY".into(),
5183 elements: vec![vec!["220".into(), "0".into()]],
5184 segment_number: 14,
5185 }];
5186
5187 let workflow = AhbWorkflow {
5188 pruefidentifikator: "13017".to_string(),
5189 description: "Test".to_string(),
5190 communication_direction: None,
5191 fields: vec![
5192 AhbFieldRule {
5194 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
5195 name: "Statuskategorie, Code".to_string(),
5196 ahb_status: "X".to_string(),
5197 codes: vec![],
5198 parent_group_ahb_status: Some("Muss".to_string()),
5199 segment_ahb_status: None,
5200 ..Default::default()
5201 },
5202 AhbFieldRule {
5204 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5205 name: "Statusanlaß, Code".to_string(),
5206 ahb_status: "X [5]".to_string(),
5207 codes: vec![],
5208 parent_group_ahb_status: Some("Muss".to_string()),
5209 segment_ahb_status: None,
5210 ..Default::default()
5211 },
5212 ],
5213 ub_definitions: BTreeMap::new(),
5214 package_conditions: Default::default(),
5215 };
5216
5217 let report = validator.validate_with_navigator(
5218 &segments,
5219 &workflow,
5220 &external,
5221 ValidationLevel::Conditions,
5222 &nav,
5223 );
5224
5225 let ahb_errors: Vec<_> = report
5226 .by_category(ValidationCategory::Ahb)
5227 .filter(|i| i.severity == Severity::Error)
5228 .collect();
5229
5230 assert!(
5231 ahb_errors.is_empty(),
5232 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
5233 ahb_errors
5234 );
5235 }
5236
5237 #[test]
5238 fn test_group_scoped_code_validation_with_navigator() {
5239 use mig_types::navigator::GroupNavigator;
5243
5244 struct TestNav;
5245 impl GroupNavigator for TestNav {
5246 fn find_segments_in_group(
5247 &self,
5248 segment_id: &str,
5249 group_path: &[&str],
5250 _instance_index: usize,
5251 ) -> Vec<OwnedSegment> {
5252 if segment_id != "NAD" {
5253 return vec![];
5254 }
5255 match group_path {
5256 ["SG2"] => vec![
5257 OwnedSegment {
5258 id: "NAD".into(),
5259 elements: vec![vec!["MS".into()]],
5260 segment_number: 3,
5261 },
5262 OwnedSegment {
5263 id: "NAD".into(),
5264 elements: vec![vec!["MT".into()]], segment_number: 4,
5266 },
5267 ],
5268 ["SG4", "SG12"] => vec![
5269 OwnedSegment {
5270 id: "NAD".into(),
5271 elements: vec![vec!["Z04".into()]],
5272 segment_number: 20,
5273 },
5274 OwnedSegment {
5275 id: "NAD".into(),
5276 elements: vec![vec!["Z09".into()]],
5277 segment_number: 21,
5278 },
5279 ],
5280 _ => vec![],
5281 }
5282 }
5283 fn find_segments_with_qualifier_in_group(
5284 &self,
5285 _: &str,
5286 _: usize,
5287 _: &str,
5288 _: &[&str],
5289 _: usize,
5290 ) -> Vec<OwnedSegment> {
5291 vec![]
5292 }
5293 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5294 match group_path {
5295 ["SG2"] | ["SG4", "SG12"] => 1,
5296 _ => 0,
5297 }
5298 }
5299 }
5300
5301 let evaluator = MockEvaluator::new(vec![]);
5302 let validator = EdifactValidator::new(evaluator);
5303 let external = NoOpExternalProvider;
5304 let nav = TestNav;
5305
5306 let workflow = AhbWorkflow {
5307 pruefidentifikator: "55001".to_string(),
5308 description: "Test".to_string(),
5309 communication_direction: None,
5310 fields: vec![
5311 AhbFieldRule {
5312 segment_path: "SG2/NAD/3035".to_string(),
5313 name: "Absender".to_string(),
5314 ahb_status: "X".to_string(),
5315 codes: vec![AhbCodeRule {
5316 value: "MS".to_string(),
5317 description: "Absender".to_string(),
5318 ahb_status: "X".to_string(),
5319 }],
5320 parent_group_ahb_status: None,
5321 segment_ahb_status: None,
5322 ..Default::default()
5323 },
5324 AhbFieldRule {
5325 segment_path: "SG2/NAD/3035".to_string(),
5326 name: "Empfaenger".to_string(),
5327 ahb_status: "X".to_string(),
5328 codes: vec![AhbCodeRule {
5329 value: "MR".to_string(),
5330 description: "Empfaenger".to_string(),
5331 ahb_status: "X".to_string(),
5332 }],
5333 parent_group_ahb_status: None,
5334 segment_ahb_status: None,
5335 ..Default::default()
5336 },
5337 AhbFieldRule {
5338 segment_path: "SG4/SG12/NAD/3035".to_string(),
5339 name: "Anschlussnutzer".to_string(),
5340 ahb_status: "X".to_string(),
5341 codes: vec![AhbCodeRule {
5342 value: "Z04".to_string(),
5343 description: "Anschlussnutzer".to_string(),
5344 ahb_status: "X".to_string(),
5345 }],
5346 parent_group_ahb_status: None,
5347 segment_ahb_status: None,
5348 ..Default::default()
5349 },
5350 AhbFieldRule {
5351 segment_path: "SG4/SG12/NAD/3035".to_string(),
5352 name: "Korrespondenzanschrift".to_string(),
5353 ahb_status: "X".to_string(),
5354 codes: vec![AhbCodeRule {
5355 value: "Z09".to_string(),
5356 description: "Korrespondenzanschrift".to_string(),
5357 ahb_status: "X".to_string(),
5358 }],
5359 parent_group_ahb_status: None,
5360 segment_ahb_status: None,
5361 ..Default::default()
5362 },
5363 ],
5364 ub_definitions: BTreeMap::new(),
5365 package_conditions: Default::default(),
5366 };
5367
5368 let all_segments = vec![
5370 OwnedSegment {
5371 id: "NAD".into(),
5372 elements: vec![vec!["MS".into()]],
5373 segment_number: 3,
5374 },
5375 OwnedSegment {
5376 id: "NAD".into(),
5377 elements: vec![vec!["MT".into()]],
5378 segment_number: 4,
5379 },
5380 OwnedSegment {
5381 id: "NAD".into(),
5382 elements: vec![vec!["Z04".into()]],
5383 segment_number: 20,
5384 },
5385 OwnedSegment {
5386 id: "NAD".into(),
5387 elements: vec![vec!["Z09".into()]],
5388 segment_number: 21,
5389 },
5390 ];
5391
5392 let report = validator.validate_with_navigator(
5393 &all_segments,
5394 &workflow,
5395 &external,
5396 ValidationLevel::Conditions,
5397 &nav,
5398 );
5399
5400 let code_errors: Vec<_> = report
5401 .by_category(ValidationCategory::Code)
5402 .filter(|i| i.severity == Severity::Error)
5403 .collect();
5404
5405 assert_eq!(
5408 code_errors.len(),
5409 1,
5410 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
5411 code_errors
5412 );
5413 assert!(narrate(code_errors[0]).contains("MT"));
5414 assert!(narrate(code_errors[0]).contains("MR"));
5416 assert!(narrate(code_errors[0]).contains("MS"));
5417 assert!(
5418 !narrate(code_errors[0]).contains("Z04"),
5419 "SG4/SG12 codes should not leak into SG2 error"
5420 );
5421 assert!(
5423 code_errors[0]
5424 .field_path
5425 .as_deref()
5426 .unwrap_or("")
5427 .contains("SG2"),
5428 "Error field_path should reference SG2, got: {:?}",
5429 code_errors[0].field_path
5430 );
5431 }
5432
5433 #[test]
5436 fn test_package_cardinality_within_bounds() {
5437 let evaluator = MockEvaluator::all_true(&[]);
5439 let validator = EdifactValidator::new(evaluator);
5440 let external = NoOpExternalProvider;
5441
5442 let segments = vec![OwnedSegment {
5443 id: "STS".into(),
5444 elements: vec![
5445 vec!["Z33".into()], vec![], vec!["E01".into()], ],
5449 segment_number: 5,
5450 }];
5451
5452 let workflow = AhbWorkflow {
5453 pruefidentifikator: "13017".to_string(),
5454 description: "Test".to_string(),
5455 communication_direction: None,
5456 ub_definitions: BTreeMap::new(),
5457 package_conditions: Default::default(),
5458 fields: vec![AhbFieldRule {
5459 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5460 name: "Statusanlaß, Code".to_string(),
5461 ahb_status: "X".to_string(),
5462 element_index: Some(2),
5463 component_index: Some(0),
5464 codes: vec![
5465 AhbCodeRule {
5466 value: "E01".into(),
5467 description: "Code 1".into(),
5468 ahb_status: "X [4P0..1]".into(),
5469 },
5470 AhbCodeRule {
5471 value: "E02".into(),
5472 description: "Code 2".into(),
5473 ahb_status: "X [4P0..1]".into(),
5474 },
5475 ],
5476 parent_group_ahb_status: Some("Muss".to_string()),
5477 segment_ahb_status: None,
5478 mig_number: None,
5479 format: None,
5480 }],
5481 };
5482
5483 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5484 let pkg_errors: Vec<_> = report
5485 .by_category(ValidationCategory::Ahb)
5486 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5487 .collect();
5488 assert!(
5489 pkg_errors.is_empty(),
5490 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
5491 pkg_errors
5492 );
5493 }
5494
5495 #[test]
5496 fn test_package_cardinality_zero_present_min_zero() {
5497 let evaluator = MockEvaluator::all_true(&[]);
5499 let validator = EdifactValidator::new(evaluator);
5500 let external = NoOpExternalProvider;
5501
5502 let segments = vec![OwnedSegment {
5503 id: "STS".into(),
5504 elements: vec![
5505 vec!["Z33".into()],
5506 vec![],
5507 vec!["X99".into()], ],
5509 segment_number: 5,
5510 }];
5511
5512 let workflow = AhbWorkflow {
5513 pruefidentifikator: "13017".to_string(),
5514 description: "Test".to_string(),
5515 communication_direction: None,
5516 ub_definitions: BTreeMap::new(),
5517 package_conditions: Default::default(),
5518 fields: vec![AhbFieldRule {
5519 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5520 name: "Statusanlaß, Code".to_string(),
5521 ahb_status: "X".to_string(),
5522 element_index: Some(2),
5523 component_index: Some(0),
5524 codes: vec![
5525 AhbCodeRule {
5526 value: "E01".into(),
5527 description: "Code 1".into(),
5528 ahb_status: "X [4P0..1]".into(),
5529 },
5530 AhbCodeRule {
5531 value: "E02".into(),
5532 description: "Code 2".into(),
5533 ahb_status: "X [4P0..1]".into(),
5534 },
5535 ],
5536 parent_group_ahb_status: Some("Muss".to_string()),
5537 segment_ahb_status: None,
5538 mig_number: None,
5539 format: None,
5540 }],
5541 };
5542
5543 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5544 let pkg_errors: Vec<_> = report
5545 .by_category(ValidationCategory::Ahb)
5546 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5547 .collect();
5548 assert!(
5549 pkg_errors.is_empty(),
5550 "0 codes, min=0 — no error expected, got: {:?}",
5551 pkg_errors
5552 );
5553 }
5554
5555 #[test]
5556 fn test_package_cardinality_too_many() {
5557 let evaluator = MockEvaluator::all_true(&[]);
5559 let validator = EdifactValidator::new(evaluator);
5560 let external = NoOpExternalProvider;
5561
5562 let segments = vec![
5564 OwnedSegment {
5565 id: "STS".into(),
5566 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
5567 segment_number: 5,
5568 },
5569 OwnedSegment {
5570 id: "STS".into(),
5571 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
5572 segment_number: 6,
5573 },
5574 ];
5575
5576 let workflow = AhbWorkflow {
5577 pruefidentifikator: "13017".to_string(),
5578 description: "Test".to_string(),
5579 communication_direction: None,
5580 ub_definitions: BTreeMap::new(),
5581 package_conditions: Default::default(),
5582 fields: vec![AhbFieldRule {
5583 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5584 name: "Statusanlaß, Code".to_string(),
5585 ahb_status: "X".to_string(),
5586 element_index: Some(2),
5587 component_index: Some(0),
5588 codes: vec![
5589 AhbCodeRule {
5590 value: "E01".into(),
5591 description: "Code 1".into(),
5592 ahb_status: "X [4P0..1]".into(),
5593 },
5594 AhbCodeRule {
5595 value: "E02".into(),
5596 description: "Code 2".into(),
5597 ahb_status: "X [4P0..1]".into(),
5598 },
5599 ],
5600 parent_group_ahb_status: Some("Muss".to_string()),
5601 segment_ahb_status: None,
5602 mig_number: None,
5603 format: None,
5604 }],
5605 };
5606
5607 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5608 let pkg_errors: Vec<_> = report
5609 .by_category(ValidationCategory::Ahb)
5610 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5611 .collect();
5612 assert_eq!(
5613 pkg_errors.len(),
5614 1,
5615 "2 codes present, max=1 — expected 1 error, got: {:?}",
5616 pkg_errors
5617 );
5618 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
5619 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
5620 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
5621 }
5622
5623 #[test]
5624 fn test_package_cardinality_too_few() {
5625 let evaluator = MockEvaluator::all_true(&[]);
5627 let validator = EdifactValidator::new(evaluator);
5628 let external = NoOpExternalProvider;
5629
5630 let segments = vec![OwnedSegment {
5631 id: "STS".into(),
5632 elements: vec![
5633 vec!["Z33".into()],
5634 vec![],
5635 vec!["X99".into()], ],
5637 segment_number: 5,
5638 }];
5639
5640 let workflow = AhbWorkflow {
5641 pruefidentifikator: "13017".to_string(),
5642 description: "Test".to_string(),
5643 communication_direction: None,
5644 ub_definitions: BTreeMap::new(),
5645 package_conditions: Default::default(),
5646 fields: vec![AhbFieldRule {
5647 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5648 name: "Statusanlaß, Code".to_string(),
5649 ahb_status: "X".to_string(),
5650 element_index: Some(2),
5651 component_index: Some(0),
5652 codes: vec![
5653 AhbCodeRule {
5654 value: "E01".into(),
5655 description: "Code 1".into(),
5656 ahb_status: "X [5P1..3]".into(),
5657 },
5658 AhbCodeRule {
5659 value: "E02".into(),
5660 description: "Code 2".into(),
5661 ahb_status: "X [5P1..3]".into(),
5662 },
5663 AhbCodeRule {
5664 value: "E03".into(),
5665 description: "Code 3".into(),
5666 ahb_status: "X [5P1..3]".into(),
5667 },
5668 ],
5669 parent_group_ahb_status: Some("Muss".to_string()),
5670 segment_ahb_status: None,
5671 mig_number: None,
5672 format: None,
5673 }],
5674 };
5675
5676 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5677 let pkg_errors: Vec<_> = report
5678 .by_category(ValidationCategory::Ahb)
5679 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5680 .collect();
5681 assert_eq!(
5682 pkg_errors.len(),
5683 1,
5684 "0 codes present, min=1 — expected 1 error, got: {:?}",
5685 pkg_errors
5686 );
5687 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
5688 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
5689 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
5690 }
5691
5692 #[test]
5693 fn test_package_cardinality_no_packages_in_workflow() {
5694 let evaluator = MockEvaluator::all_true(&[]);
5696 let validator = EdifactValidator::new(evaluator);
5697 let external = NoOpExternalProvider;
5698
5699 let segments = vec![OwnedSegment {
5700 id: "STS".into(),
5701 elements: vec![vec!["E01".into()]],
5702 segment_number: 5,
5703 }];
5704
5705 let workflow = AhbWorkflow {
5706 pruefidentifikator: "13017".to_string(),
5707 description: "Test".to_string(),
5708 communication_direction: None,
5709 ub_definitions: BTreeMap::new(),
5710 package_conditions: Default::default(),
5711 fields: vec![AhbFieldRule {
5712 segment_path: "STS/9015".to_string(),
5713 name: "Status Code".to_string(),
5714 ahb_status: "X".to_string(),
5715 codes: vec![AhbCodeRule {
5716 value: "E01".into(),
5717 description: "Code 1".into(),
5718 ahb_status: "X".into(),
5719 }],
5720 parent_group_ahb_status: Some("Muss".to_string()),
5721 segment_ahb_status: None,
5722 ..Default::default()
5723 }],
5724 };
5725
5726 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5727 let pkg_errors: Vec<_> = report
5728 .by_category(ValidationCategory::Ahb)
5729 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5730 .collect();
5731 assert!(
5732 pkg_errors.is_empty(),
5733 "No packages in workflow — no errors expected"
5734 );
5735 }
5736
5737 #[test]
5738 fn test_package_cardinality_with_condition_and_package() {
5739 let evaluator = MockEvaluator::all_true(&[901]);
5741 let validator = EdifactValidator::new(evaluator);
5742 let external = NoOpExternalProvider;
5743
5744 let segments = vec![OwnedSegment {
5745 id: "STS".into(),
5746 elements: vec![vec![], 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: "SG10/STS/C556/9013".to_string(),
5758 name: "Code".to_string(),
5759 ahb_status: "X".to_string(),
5760 element_index: Some(2),
5761 component_index: Some(0),
5762 codes: vec![
5763 AhbCodeRule {
5764 value: "E01".into(),
5765 description: "Code 1".into(),
5766 ahb_status: "X [901] [4P0..1]".into(),
5767 },
5768 AhbCodeRule {
5769 value: "E02".into(),
5770 description: "Code 2".into(),
5771 ahb_status: "X [901] [4P0..1]".into(),
5772 },
5773 ],
5774 parent_group_ahb_status: Some("Muss".to_string()),
5775 segment_ahb_status: None,
5776 mig_number: None,
5777 format: None,
5778 }],
5779 };
5780
5781 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5782 let pkg_errors: Vec<_> = report
5783 .by_category(ValidationCategory::Ahb)
5784 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5785 .collect();
5786 assert!(
5787 pkg_errors.is_empty(),
5788 "1 code within [4P0..1] bounds — no error, got: {:?}",
5789 pkg_errors
5790 );
5791 }
5792
5793 #[test]
5794 fn test_package_binds_only_where_its_condition_holds() {
5795 let report_for = |package_condition: CR| {
5799 let validator =
5800 EdifactValidator::new(MockEvaluator::new(vec![(902, package_condition)]));
5801 let segments = vec![OwnedSegment {
5802 id: "STS".into(),
5803 elements: vec![vec![], vec![], vec!["E09".into()]],
5804 segment_number: 5,
5805 }];
5806 let workflow = AhbWorkflow {
5807 pruefidentifikator: "13017".to_string(),
5808 description: "Test".to_string(),
5809 communication_direction: None,
5810 ub_definitions: BTreeMap::new(),
5811 package_conditions: BTreeMap::from([(5, ConditionExpr::Ref(902))]),
5812 fields: vec![AhbFieldRule {
5813 segment_path: "SG10/STS/C556/9013".to_string(),
5814 name: "Code".to_string(),
5815 ahb_status: "X".to_string(),
5816 element_index: Some(2),
5817 component_index: Some(0),
5818 codes: vec![
5819 AhbCodeRule {
5820 value: "E01".into(),
5821 description: "Code 1".into(),
5822 ahb_status: "X [5P1..1]".into(),
5823 },
5824 AhbCodeRule {
5825 value: "E09".into(),
5826 description: "Code 9".into(),
5827 ahb_status: "X".into(),
5828 },
5829 ],
5830 parent_group_ahb_status: Some("Muss".to_string()),
5831 segment_ahb_status: None,
5832 mig_number: None,
5833 format: None,
5834 }],
5835 };
5836 validator
5837 .validate(
5838 &segments,
5839 &workflow,
5840 &NoOpExternalProvider,
5841 ValidationLevel::Full,
5842 )
5843 .by_category(ValidationCategory::Ahb)
5844 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5845 .count()
5846 };
5847 assert_eq!(report_for(CR::False), 0, "[902] false: [5P] does not bind");
5848 assert_eq!(report_for(CR::True), 1, "[902] true: [5P1..1] binds");
5849 }
5850
5851 #[test]
5852 fn test_package_cardinality_scoped_per_group_instance() {
5853 use mig_types::navigator::GroupNavigator;
5861
5862 struct TwoSg10s {
5863 sts_a: OwnedSegment,
5864 sts_b: OwnedSegment,
5865 }
5866 impl GroupNavigator for TwoSg10s {
5867 fn find_segments_in_group(
5868 &self,
5869 segment_id: &str,
5870 group_path: &[&str],
5871 instance_index: usize,
5872 ) -> Vec<OwnedSegment> {
5873 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
5874 match instance_index {
5875 0 => vec![self.sts_a.clone()],
5876 1 => vec![self.sts_b.clone()],
5877 _ => vec![],
5878 }
5879 } else {
5880 vec![]
5881 }
5882 }
5883 fn find_segments_with_qualifier_in_group(
5884 &self,
5885 _: &str,
5886 _: usize,
5887 _: &str,
5888 _: &[&str],
5889 _: usize,
5890 ) -> Vec<OwnedSegment> {
5891 vec![]
5892 }
5893 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5894 match group_path {
5895 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
5896 ["SG5", "SG6", "SG9", "SG10"] => 2,
5897 _ => 0,
5898 }
5899 }
5900 }
5901
5902 let sts_a = OwnedSegment {
5903 id: "STS".into(),
5904 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5905 segment_number: 10,
5906 };
5907 let sts_b = OwnedSegment {
5908 id: "STS".into(),
5909 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5910 segment_number: 15,
5911 };
5912 let nav = TwoSg10s {
5913 sts_a: sts_a.clone(),
5914 sts_b: sts_b.clone(),
5915 };
5916
5917 let evaluator = MockEvaluator::all_true(&[]);
5918 let validator = EdifactValidator::new(evaluator);
5919 let external = NoOpExternalProvider;
5920
5921 let workflow = AhbWorkflow {
5922 pruefidentifikator: "13025".to_string(),
5923 description: "Test".to_string(),
5924 communication_direction: None,
5925 ub_definitions: BTreeMap::new(),
5926 package_conditions: Default::default(),
5927 fields: vec![AhbFieldRule {
5928 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5929 name: "Statusanlaß, Code".to_string(),
5930 ahb_status: "X".to_string(),
5931 element_index: Some(2),
5932 component_index: Some(0),
5933 codes: vec![
5934 AhbCodeRule {
5935 value: "E01".into(),
5936 description: "Code 1".into(),
5937 ahb_status: "X [4P0..1]".into(),
5938 },
5939 AhbCodeRule {
5940 value: "E02".into(),
5941 description: "Code 2".into(),
5942 ahb_status: "X [4P0..1]".into(),
5943 },
5944 ],
5945 parent_group_ahb_status: Some("Muss".to_string()),
5946 segment_ahb_status: None,
5947 mig_number: None,
5948 format: None,
5949 }],
5950 };
5951
5952 let report = validator.validate_with_navigator(
5953 &[sts_a, sts_b],
5954 &workflow,
5955 &external,
5956 ValidationLevel::Full,
5957 &nav,
5958 );
5959 let pkg_errors: Vec<_> = report
5960 .by_category(ValidationCategory::Ahb)
5961 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5962 .collect();
5963 assert!(
5964 pkg_errors.is_empty(),
5965 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
5966 pkg_errors
5967 );
5968 }
5969
5970 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
5971 OwnedSegment {
5972 id: id.to_string(),
5973 elements: elements
5974 .into_iter()
5975 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
5976 .collect(),
5977 segment_number: 0,
5978 }
5979 }
5980
5981 #[test]
5992 fn test_package_cardinality_scoped_to_rule_mig_variant() {
5993 use mig_types::navigator::GroupNavigator;
5994
5995 struct TwoSg8Variants {
5996 seq_z01: OwnedSegment,
5997 seq_z45: OwnedSegment,
5998 }
5999 impl GroupNavigator for TwoSg8Variants {
6000 fn find_segments_in_group(
6001 &self,
6002 segment_id: &str,
6003 group_path: &[&str],
6004 instance_index: usize,
6005 ) -> Vec<OwnedSegment> {
6006 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
6007 match instance_index {
6008 0 => vec![self.seq_z01.clone()],
6009 1 => vec![self.seq_z45.clone()],
6010 _ => vec![],
6011 }
6012 } else {
6013 vec![]
6014 }
6015 }
6016 fn find_segments_with_qualifier_in_group(
6017 &self,
6018 _: &str,
6019 _: usize,
6020 _: &str,
6021 _: &[&str],
6022 _: usize,
6023 ) -> Vec<OwnedSegment> {
6024 vec![]
6025 }
6026 fn group_instance_count(&self, group_path: &[&str]) -> usize {
6027 match group_path {
6028 ["SG4"] => 1,
6029 ["SG4", "SG8"] => 2,
6030 _ => 0,
6031 }
6032 }
6033 fn instance_has_mig_number(
6034 &self,
6035 group_path: &[&str],
6036 instance_index: usize,
6037 mig_number: &str,
6038 ) -> bool {
6039 if group_path != ["SG4", "SG8"] {
6040 return true;
6041 }
6042 match (instance_index, mig_number) {
6043 (0, "00115") => true,
6044 (0, _) => false,
6045 (1, "00171") => true,
6046 (1, _) => false,
6047 _ => false,
6048 }
6049 }
6050 }
6051
6052 let seq_z01 = OwnedSegment {
6053 id: "SEQ".into(),
6054 elements: vec![vec!["Z01".into()], vec!["1".into()]],
6055 segment_number: 10,
6056 };
6057 let seq_z45 = OwnedSegment {
6058 id: "SEQ".into(),
6059 elements: vec![vec!["Z45".into()], vec!["1".into()]],
6060 segment_number: 20,
6061 };
6062 let nav = TwoSg8Variants {
6063 seq_z01: seq_z01.clone(),
6064 seq_z45: seq_z45.clone(),
6065 };
6066
6067 let evaluator = MockEvaluator::all_true(&[]);
6068 let validator = EdifactValidator::new(evaluator);
6069 let external = NoOpExternalProvider;
6070
6071 let workflow = AhbWorkflow {
6072 pruefidentifikator: "55218".to_string(),
6073 description: "Test".to_string(),
6074 communication_direction: None,
6075 ub_definitions: BTreeMap::new(),
6076 package_conditions: Default::default(),
6077 fields: vec![AhbFieldRule {
6078 segment_path: "SG4/SG8/SEQ/1229".to_string(),
6079 name: "Handlung, Code".to_string(),
6080 ahb_status: "X".to_string(),
6081 element_index: Some(0),
6082 component_index: Some(0),
6083 codes: vec![
6084 AhbCodeRule {
6085 value: "Z45".into(),
6086 description: "NNA".into(),
6087 ahb_status: "X [1P1..4294967295]".into(),
6088 },
6089 AhbCodeRule {
6090 value: "Z84".into(),
6091 description: "Differenz-NNA".into(),
6092 ahb_status: "X [1P0..4294967295]".into(),
6093 },
6094 ],
6095 parent_group_ahb_status: Some("Muss".to_string()),
6096 segment_ahb_status: None,
6097 mig_number: Some("00171".to_string()),
6098 format: None,
6099 }],
6100 };
6101
6102 let report = validator.validate_with_navigator(
6103 &[seq_z01, seq_z45],
6104 &workflow,
6105 &external,
6106 ValidationLevel::Full,
6107 &nav,
6108 );
6109 let pkg_errors: Vec<_> = report
6110 .by_category(ValidationCategory::Ahb)
6111 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
6112 .collect();
6113 assert!(
6114 pkg_errors.is_empty(),
6115 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
6116 pkg_errors
6117 );
6118 }
6119
6120 #[test]
6121 fn test_unt_count_correct() {
6122 let segments = vec![
6124 make_segment("UNH", vec![vec!["001"]]),
6125 make_segment("BGM", vec![vec!["E01"]]),
6126 make_segment("DTM", vec![vec!["137", "20250401"]]),
6127 make_segment("UNT", vec![vec!["4", "001"]]),
6128 ];
6129 assert!(
6130 validate_unt_segment_count(&segments).is_none(),
6131 "Correct count should produce no issue"
6132 );
6133 }
6134
6135 #[test]
6136 fn test_unt_count_mismatch() {
6137 let segments = vec![
6139 make_segment("UNH", vec![vec!["001"]]),
6140 make_segment("BGM", vec![vec!["E01"]]),
6141 make_segment("UNT", vec![vec!["5", "001"]]),
6142 ];
6143 let issue =
6144 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
6145 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
6146 assert_eq!(issue.severity, Severity::Error);
6147 assert!(narrate(&issue).contains("declared 5"));
6148 assert!(narrate(&issue).contains("actual 3"));
6149 }
6150
6151 #[test]
6152 fn test_unt_count_excludes_envelope() {
6153 let segments = vec![
6155 make_segment("UNA", vec![]),
6156 make_segment("UNB", vec![vec!["UNOC", "3"]]),
6157 make_segment("UNH", vec![vec!["001"]]),
6158 make_segment("BGM", vec![vec!["E01"]]),
6159 make_segment("UNT", vec![vec!["3", "001"]]),
6160 make_segment("UNZ", vec![vec!["1"]]),
6161 ];
6162 assert!(
6163 validate_unt_segment_count(&segments).is_none(),
6164 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
6165 );
6166 }
6167
6168 #[test]
6169 fn test_unt_count_no_unt_returns_none() {
6170 let segments = vec![
6171 make_segment("UNH", vec![vec!["001"]]),
6172 make_segment("BGM", vec![vec!["E01"]]),
6173 ];
6174 assert!(
6175 validate_unt_segment_count(&segments).is_none(),
6176 "No UNT segment should return None (not our problem)"
6177 );
6178 }
6179
6180 #[test]
6181 fn test_unt_count_rejects_multi_message_input() {
6182 let segments = vec![
6184 make_segment("UNH", vec![vec!["001"]]),
6185 make_segment("BGM", vec![vec!["E01"]]),
6186 make_segment("UNT", vec![vec!["3", "001"]]),
6187 make_segment("UNH", vec![vec!["002"]]),
6188 make_segment("BGM", vec![vec!["E02"]]),
6189 make_segment("UNT", vec![vec!["3", "002"]]),
6190 ];
6191 let issue = validate_unt_segment_count(&segments)
6192 .expect("Multi-message input should produce an error");
6193 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
6194 assert!(
6195 narrate(&issue).contains("2 UNH"),
6196 "Should mention UNH count: {}",
6197 narrate(&issue)
6198 );
6199 }
6200
6201 #[test]
6202 fn test_code_validation_accepts_multi_code_variant_qualifier() {
6203 let evaluator = MockEvaluator::new(vec![]);
6210 let validator = EdifactValidator::new(evaluator);
6211 let external = NoOpExternalProvider;
6212
6213 let rff_z39 = OwnedSegment {
6214 id: "RFF".to_string(),
6215 elements: vec![
6216 vec!["RFF".to_string()],
6217 vec!["Z39".to_string(), "REF1".to_string()],
6218 ],
6219 segment_number: 1,
6220 };
6221
6222 let workflow = AhbWorkflow {
6223 pruefidentifikator: "55035".to_string(),
6224 description: "Test".to_string(),
6225 communication_direction: None,
6226 fields: vec![
6227 AhbFieldRule {
6229 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6230 name: "Referenznummer Qualifier".to_string(),
6231 ahb_status: "Muss".to_string(),
6232 codes: vec![
6233 AhbCodeRule {
6234 value: "Z31".to_string(),
6235 description: "".to_string(),
6236 ahb_status: "X".to_string(),
6237 },
6238 AhbCodeRule {
6239 value: "Z39".to_string(),
6240 description: "".to_string(),
6241 ahb_status: "X".to_string(),
6242 },
6243 ],
6244 parent_group_ahb_status: None,
6245 segment_ahb_status: None,
6246 element_index: Some(1),
6247 component_index: Some(0),
6248 mig_number: Some("00075".to_string()),
6249 format: None,
6250 },
6251 AhbFieldRule {
6253 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6254 name: "Referenznummer Qualifier".to_string(),
6255 ahb_status: "Muss".to_string(),
6256 codes: vec![AhbCodeRule {
6257 value: "Z33".to_string(),
6258 description: "".to_string(),
6259 ahb_status: "X".to_string(),
6260 }],
6261 parent_group_ahb_status: None,
6262 segment_ahb_status: None,
6263 element_index: Some(1),
6264 component_index: Some(0),
6265 mig_number: Some("00078".to_string()),
6266 format: None,
6267 },
6268 ],
6269 ub_definitions: BTreeMap::new(),
6270 package_conditions: Default::default(),
6271 };
6272
6273 let report = validator.validate(
6274 &[rff_z39],
6275 &workflow,
6276 &external,
6277 ValidationLevel::Conditions,
6278 );
6279
6280 let code_errors: Vec<_> = report
6281 .by_category(ValidationCategory::Code)
6282 .filter(|i| {
6283 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6284 })
6285 .collect();
6286 assert!(
6287 code_errors.is_empty(),
6288 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
6289 code_errors
6290 );
6291 }
6292
6293 #[test]
6294 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
6295 let evaluator = MockEvaluator::new(vec![]);
6301 let validator = EdifactValidator::new(evaluator);
6302 let external = NoOpExternalProvider;
6303
6304 let pia_5_z12 = OwnedSegment {
6308 id: "PIA".to_string(),
6309 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
6310 segment_number: 1,
6311 };
6312 let pia_5_srw = OwnedSegment {
6313 id: "PIA".to_string(),
6314 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
6315 segment_number: 2,
6316 };
6317
6318 let make_rules = |mig: &str, composite_code: &str| {
6319 vec![
6320 AhbFieldRule {
6321 segment_path: "SG4/SG8/PIA/4347".to_string(),
6322 name: "Produkt-ID-Funktion".to_string(),
6323 ahb_status: "Muss".to_string(),
6324 codes: vec![AhbCodeRule {
6325 value: "5".to_string(),
6326 description: "".to_string(),
6327 ahb_status: "X".to_string(),
6328 }],
6329 parent_group_ahb_status: None,
6330 segment_ahb_status: None,
6331 element_index: Some(0),
6332 component_index: Some(0),
6333 mig_number: Some(mig.to_string()),
6334 format: None,
6335 },
6336 AhbFieldRule {
6337 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
6338 name: "Artikel/Dienstleistung-Identifikator".to_string(),
6339 ahb_status: "Muss".to_string(),
6340 codes: vec![AhbCodeRule {
6341 value: composite_code.to_string(),
6342 description: "".to_string(),
6343 ahb_status: "X".to_string(),
6344 }],
6345 parent_group_ahb_status: None,
6346 segment_ahb_status: None,
6347 element_index: Some(1),
6348 component_index: Some(0),
6349 mig_number: Some(mig.to_string()),
6350 format: None,
6351 },
6352 ]
6353 };
6354
6355 let mut fields = make_rules("00108", "Z12");
6356 fields.extend(make_rules("00197", "SRW"));
6357
6358 let workflow = AhbWorkflow {
6359 pruefidentifikator: "55035".to_string(),
6360 description: "Test".to_string(),
6361 communication_direction: None,
6362 fields,
6363 ub_definitions: BTreeMap::new(),
6364 package_conditions: Default::default(),
6365 };
6366
6367 let report = validator.validate(
6368 &[pia_5_z12, pia_5_srw],
6369 &workflow,
6370 &external,
6371 ValidationLevel::Conditions,
6372 );
6373
6374 let code_errors: Vec<_> = report
6375 .by_category(ValidationCategory::Code)
6376 .filter(|i| {
6377 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6378 })
6379 .collect();
6380 assert!(
6381 code_errors.is_empty(),
6382 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
6383 code_errors
6384 );
6385 }
6386}