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