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 }
374
375 if let Some((once, transaction)) =
377 crate::eval::once_per_transaction(self.evaluator.message_type())
378 {
379 let mut index: HashMap<&str, usize> = HashMap::new();
380 for group in &validated_tree.groups {
381 let i = *index
382 .entry(group.group_id)
383 .and_modify(|c| *c += 1)
384 .or_insert(0);
385 if group.group_id == transaction {
386 let first_new = report.issues.len();
387 report_repeated_in_transaction(group, i, once, &ctx, &mut report);
388 for issue in &mut report.issues[first_new..] {
389 issue.transaction_index = Some(i);
390 }
391 }
392 }
393 }
394
395 if self.evaluator.message_type() == "UTILMD_Strom" {
398 let mut index: HashMap<&str, usize> = HashMap::new();
399 for group in &validated_tree.groups {
400 let i = *index
401 .entry(group.group_id)
402 .and_modify(|c| *c += 1)
403 .or_insert(0);
404 if group.group_id == "SG4" {
405 let first_new = report.issues.len();
406 report_off_count_per_zeitraum(
407 group,
408 i,
409 &ctx,
410 &self.evaluator,
411 validated_tree.ub_definitions,
412 &mut report,
413 );
414 for issue in &mut report.issues[first_new..] {
415 issue.transaction_index = Some(i);
416 }
417 }
418 }
419 }
420
421 for field in &validated_tree.unmatched_rules {
431 if field
432 .mig_number
433 .as_deref()
434 .is_some_and(|n| per_transaction.contains(n))
435 {
436 continue;
437 }
438 if should_skip_for_parent_group(
439 field,
440 &absent_eval,
441 &ctx,
442 validated_tree.ub_definitions,
443 ) {
444 continue;
445 }
446
447 let status =
448 absent_eval.resolve_status(&field.ahb_status, &ctx, validated_tree.ub_definitions);
449
450 if status.result == ConditionResult::True
451 && status.kind == Some(StatusKind::Muss)
452 && !is_field_present(&ctx, field)
453 && !is_group_variant_absent(
454 &ctx,
455 field,
456 is_parent_group_required(
457 field,
458 &absent_eval,
459 &ctx,
460 validated_tree.ub_definitions,
461 ),
462 )
463 {
464 let mut issue = ValidationIssue::new(
465 Severity::Error,
466 IssueKind::MissingRequiredField {
467 field_name: field.name.clone(),
468 },
469 )
470 .with_field_path(&field.segment_path)
471 .with_rule(&field.ahb_status);
472 if let Some(first_code) = field.codes.first() {
473 issue.expected_value = Some(first_code.value.clone());
474 }
475 issue = attach_field_position(issue, field);
476 report.add_issue(issue);
477 }
478 }
479
480 if matches!(level, ValidationLevel::Full) {
484 let mut seen: HashSet<*const AhbFieldRule> = HashSet::new();
488 let all_fields: Vec<AhbFieldRule> = all_nodes
489 .iter()
490 .map(|node| node.rule)
491 .chain(validated_tree.unmatched_rules.iter().copied())
492 .filter(|rule| seen.insert(*rule as *const AhbFieldRule))
493 .cloned()
494 .collect();
495 let synthetic_workflow = AhbWorkflow {
496 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
497 description: String::new(),
498 communication_direction: None,
499 fields: all_fields,
500 ub_definitions: validated_tree.ub_definitions.clone(),
501 package_conditions: validated_tree.package_conditions.clone(),
502 };
503 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
504 self.validate_answer_lists(&synthetic_workflow, &ctx, &mut report);
505 self.validate_package_cardinality(
506 &synthetic_workflow,
507 &ctx,
508 views.as_ref(),
509 &mut report,
510 );
511 }
512
513 report
514 }
515
516 fn validate_conditions(
518 &self,
519 workflow: &AhbWorkflow,
520 ctx: &EvaluationContext,
521 report: &mut ValidationReport,
522 ) {
523 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
524
525 for field in &workflow.fields {
526 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
528 continue;
529 }
530
531 let StatusResolution {
534 result: condition_result,
535 kind,
536 unknown_ids,
537 } = expr_eval.resolve_status(&field.ahb_status, ctx, &workflow.ub_definitions);
538 let mandatory = kind == Some(StatusKind::Muss);
539
540 match condition_result {
541 ConditionResult::True => {
542 if mandatory
544 && !is_field_present(ctx, field)
545 && !is_group_variant_absent(
546 ctx,
547 field,
548 is_parent_group_required(
549 field,
550 &expr_eval,
551 ctx,
552 &workflow.ub_definitions,
553 ),
554 )
555 {
556 let mut issue = ValidationIssue::new(
557 Severity::Error,
558 IssueKind::MissingRequiredField {
559 field_name: field.name.clone(),
560 },
561 )
562 .with_field_path(&field.segment_path)
563 .with_rule(&field.ahb_status)
564 .with_mig_number(field.mig_number.as_deref());
565 if let Some(first_code) = field.codes.first() {
569 issue.expected_value = Some(first_code.value.clone());
570 }
571 report.add_issue(issue);
572 }
573 }
574 ConditionResult::False => {
575 if mandatory && is_field_present(ctx, field) {
581 let failed = expr_eval.false_ids_with_ub(
582 &field.ahb_status,
583 ctx,
584 &workflow.ub_definitions,
585 );
586 report.add_issue(
587 ValidationIssue::new(
588 Severity::Error,
589 IssueKind::FieldConditionNotSatisfied {
590 field_name: field.name.clone(),
591 failed,
592 },
593 )
594 .with_field_path(&field.segment_path)
595 .with_rule(&field.ahb_status)
596 .with_mig_number(field.mig_number.as_deref()),
597 );
598 }
599 }
600 ConditionResult::Unknown => {
601 let mut external_ids = Vec::new();
606 let mut undetermined_ids = Vec::new();
607 let mut missing_ids = Vec::new();
608 for id in unknown_ids {
609 if self.evaluator.is_external(id) {
610 external_ids.push(id);
611 } else if self.evaluator.is_known(id) {
612 undetermined_ids.push(id);
613 } else {
614 missing_ids.push(id);
615 }
616 }
617
618 report.add_issue(
619 ValidationIssue::new(
620 Severity::Info,
621 IssueKind::ConditionUnknown {
622 field_name: field.name.clone(),
623 unresolved: UnresolvedConditions {
624 external: external_ids,
625 undetermined: undetermined_ids,
626 missing: missing_ids,
627 },
628 },
629 )
630 .with_field_path(&field.segment_path)
631 .with_rule(&field.ahb_status),
632 );
633 }
634 }
635 }
636
637 self.validate_codes_cross_field(workflow, ctx, report);
642 self.validate_answer_lists(workflow, ctx, report);
643
644 self.validate_package_cardinality(workflow, ctx, None, report);
647 }
648
649 fn validate_package_cardinality(
660 &self,
661 workflow: &AhbWorkflow,
662 ctx: &EvaluationContext,
663 views: Option<&TransactionViews<'_>>,
664 report: &mut ValidationReport,
665 ) {
666 struct PackageGroup {
669 min: u32,
670 max: u32,
671 max_sum: u32,
674 bounds: BTreeSet<(u32, u32)>,
675 code_values: Vec<String>,
676 element_index: usize,
677 component_index: usize,
678 }
679
680 let mut groups: HashMap<(String, Option<String>, u32, u32, u32), PackageGroup> =
689 HashMap::new();
690 let mut alternatives: HashMap<(String, Option<String>, u32), BTreeSet<u32>> =
694 HashMap::new();
695
696 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
697
698 let mut parent_group_skipped: HashMap<&str, bool> = HashMap::new();
705
706 for field in &workflow.fields {
707 let el_idx = field.element_index.unwrap_or(0);
708 let comp_idx = field.component_index.unwrap_or(0);
709
710 let code_packages: Vec<(&str, Vec<(u32, u32, u32)>)> = field
712 .codes
713 .iter()
714 .filter_map(|code| {
715 let expr = ConditionParser::parse(&code.ahb_status).ok()??;
716 let mut packages = Vec::new();
717 collect_packages(&expr, &mut packages);
718 (!packages.is_empty()).then_some((code.value.as_str(), packages))
719 })
720 .collect();
721 if code_packages.is_empty() {
722 continue;
723 }
724
725 let skipped = match field.parent_group_ahb_status.as_deref() {
727 Some(status) => *parent_group_skipped.entry(status).or_insert_with(|| {
728 should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions)
729 }),
730 None => false,
731 };
732 if skipped {
733 continue;
734 }
735
736 for (code_value, packages) in code_packages {
737 if packages.len() > 1 {
738 let ids: BTreeSet<u32> = packages.iter().map(|(id, _, _)| *id).collect();
739 for id in &ids {
740 alternatives
741 .entry((field.segment_path.clone(), field.mig_number.clone(), *id))
742 .or_default()
743 .extend(ids.iter().copied().filter(|other| other != id));
744 }
745 }
746
747 for (pkg_id, pkg_min, pkg_max) in packages {
748 let key = (
749 field.segment_path.clone(),
750 field.mig_number.clone(),
751 pkg_id,
752 0,
753 0,
754 );
755 let group = groups.entry(key).or_insert_with(|| PackageGroup {
756 min: pkg_min,
757 max: pkg_max,
758 max_sum: 0,
759 bounds: BTreeSet::new(),
760 code_values: Vec::new(),
761 element_index: el_idx,
762 component_index: comp_idx,
763 });
764 group.bounds.insert((pkg_min, pkg_max));
771 group.min = group.min.min(pkg_min);
772 if !group.code_values.iter().any(|v| v == code_value) {
773 group.max_sum = group.max_sum.saturating_add(pkg_max);
774 group.code_values.push(code_value.to_string());
775 }
776 group.max = group.max.max(pkg_max);
777 }
778 }
779 }
780
781 let mut field_packages: HashMap<(String, Option<String>), BTreeSet<u32>> = HashMap::new();
792 for (seg_path, mig_number, pkg_id, _, _) in groups.keys() {
793 field_packages
794 .entry((seg_path.clone(), mig_number.clone()))
795 .or_default()
796 .insert(*pkg_id);
797 }
798 let with_alternatives: Vec<(String, Option<String>)> = alternatives
799 .keys()
800 .map(|(p, m, _)| (p.clone(), m.clone()))
801 .collect();
802 for field in with_alternatives {
803 let Some(ids) = field_packages.get(&field) else {
804 continue;
805 };
806 for id in ids {
807 alternatives
808 .entry((field.0.clone(), field.1.clone(), *id))
809 .or_default()
810 .extend(ids.iter().copied().filter(|other| other != id));
811 }
812 }
813
814 type Counted = (Vec<usize>, Vec<bool>, usize, usize, Vec<String>);
816 let mut counted: HashMap<(String, Option<String>, u32, u32, u32), Counted> = HashMap::new();
817 let mut transaction_views: HashMap<(String, usize), TransactionSegmentsView> =
820 HashMap::new();
821 for ((seg_path, mig_number, pkg_id, _, _), group) in &groups {
822 let segment_id = extract_segment_id(seg_path);
823
824 let mut unique_codes: Vec<&str> =
828 group.code_values.iter().map(|s| s.as_str()).collect();
829 unique_codes.sort_unstable();
830 unique_codes.dedup();
831 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
832
833 let group_path_str = extract_group_path_key(seg_path);
834 let group_path: Vec<&str> = if group_path_str.is_empty() {
835 Vec::new()
836 } else {
837 group_path_str.split('/').collect()
838 };
839
840 let min = group.min as usize;
841 let max = if group.bounds.len() > 1 {
842 group.max_sum as usize
843 } else {
844 group.max as usize
845 };
846
847 type Instance<'p> = (usize, Option<Vec<(&'p str, usize)>>);
850 let per_instance_counts: Option<Vec<Instance>> =
851 match (ctx.navigator, group_path.is_empty()) {
852 (Some(nav), false)
860 if group_path.len() > 1
861 && !full_instance_paths(nav, &group_path).is_empty() =>
862 {
863 let paths = full_instance_paths(nav, &group_path);
864 let variant_entries: Vec<(usize, VariantEntry)> = match mig_number
872 .as_deref()
873 {
874 Some(m) => (0..group_path.len())
875 .filter_map(|level| {
876 let prefix = group_path[..=level].join("/");
877 merged_variant_entry(workflow, &prefix, m).map(|e| (level, e))
878 })
879 .collect(),
880 None => Vec::new(),
881 };
882 if paths.is_empty() {
883 None
884 } else {
885 Some(
886 paths
887 .into_iter()
888 .filter(|path| match mig_number.as_deref() {
889 Some(m) => nav.instance_at_has_mig_number(path, m),
890 None => true,
891 })
892 .filter(|path| {
893 variant_entries.iter().all(|(level, entry)| {
894 nav.find_segments_in_instance(
895 &entry.tag,
896 &path[..=*level],
897 )
898 .first()
899 .and_then(|seg| {
900 seg.elements
901 .get(entry.element)
902 .and_then(|e| e.get(entry.component))
903 .cloned()
904 })
905 .map_or(true, |v| entry.codes.contains(v.as_str()))
906 })
907 })
908 .map(|path| {
909 let count = nav
910 .find_segments_in_instance(&segment_id, &path)
911 .iter()
912 .filter_map(|seg| {
913 seg.elements
914 .get(group.element_index)
915 .and_then(|e| e.get(group.component_index))
916 .filter(|v| !v.is_empty())
917 .cloned()
918 })
919 .filter(|v| code_set.contains(v.as_str()))
920 .count();
921 (count, Some(path))
922 })
923 .collect(),
924 )
925 }
926 }
927 (Some(nav), false) => {
928 let instance_count = nav.group_instance_count(&group_path);
929 if instance_count == 0 {
930 None
931 } else {
932 Some(
940 (0..instance_count)
941 .filter(|i| match mig_number.as_deref() {
942 Some(m) => nav.instance_has_mig_number(&group_path, *i, m),
943 None => true,
944 })
945 .map(|i| {
946 let count = nav
947 .find_segments_in_group(&segment_id, &group_path, i)
948 .iter()
949 .filter_map(|seg| {
950 seg.elements
951 .get(group.element_index)
952 .and_then(|e| e.get(group.component_index))
953 .filter(|v| !v.is_empty())
954 .cloned()
955 })
956 .filter(|v| code_set.contains(v.as_str()))
957 .count();
958 let path = (group_path.len() == 1)
960 .then(|| vec![(group_path[0], i)]);
961 (count, path)
962 })
963 .collect(),
964 )
965 }
966 }
967 _ => None,
968 };
969
970 let instances = per_instance_counts.unwrap_or_else(|| {
971 let segments = ctx.find_segments(&segment_id);
972 let count = segments
973 .iter()
974 .filter_map(|seg| {
975 seg.elements
976 .get(group.element_index)
977 .and_then(|e| e.get(group.component_index))
978 .filter(|v| !v.is_empty())
979 .map(|s| s.as_str())
980 })
981 .filter(|v| code_set.contains(v))
982 .count();
983 vec![(count, None)]
984 });
985 let applies: Vec<bool> = instances
991 .iter()
992 .map(|(_, path)| match workflow.package_conditions.get(pkg_id) {
993 None => true,
994 Some(condition) => {
995 let result = match path {
996 Some(path) => match path.first().and_then(|&(group_id, index)| {
997 let key = (group_id.to_string(), index);
998 if !transaction_views.contains_key(&key) {
999 let view = views?.view(group_id, index)?;
1000 transaction_views.insert(key.clone(), view);
1001 }
1002 transaction_views.get(&key)
1003 }) {
1004 Some(view) => {
1005 let navigator = AssembledTreeNavigator::new(&view.tree);
1006 let mut in_view = path.clone();
1007 in_view[0].1 = 0;
1008 expr_eval.evaluate(
1009 condition,
1010 &ctx.with_view(&view.segments, &navigator)
1011 .with_scope(crate::eval::GroupScope { path: &in_view }),
1012 )
1013 }
1014 None => expr_eval.evaluate(
1015 condition,
1016 &ctx.with_scope(crate::eval::GroupScope { path }),
1017 ),
1018 },
1019 None => expr_eval.evaluate(condition, ctx),
1020 };
1021 result == ConditionResult::True
1022 }
1023 })
1024 .collect();
1025 let counts: Vec<usize> = instances.iter().map(|(c, _)| *c).collect();
1026
1027 counted.insert(
1028 (
1029 seg_path.clone(),
1030 mig_number.clone(),
1031 *pkg_id,
1032 group.min,
1033 group.max,
1034 ),
1035 (
1036 counts,
1037 applies,
1038 min,
1039 max,
1040 unique_codes.iter().map(|s| s.to_string()).collect(),
1041 ),
1042 );
1043 }
1044
1045 let mut keys: Vec<&(String, Option<String>, u32, u32, u32)> = counted.keys().collect();
1050 keys.sort();
1051 for key in keys {
1052 let (counts, applies, min, max, codes) = &counted[key];
1053 let (seg_path, mig_number, pkg_id, _, _) = key;
1054 let holds = |c: &[usize], a: &[bool], lo: usize, hi: usize, i: usize| {
1055 c.len() == counts.len() && a[i] && (lo..=hi).contains(&c[i]) && c[i] > 0
1056 };
1057 let alternative_holds = |i: usize| {
1058 alternatives
1059 .get(&(seg_path.clone(), mig_number.clone(), *pkg_id))
1060 .is_some_and(|alts| {
1061 counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
1062 p == seg_path
1063 && m == mig_number
1064 && alts.contains(id)
1065 && holds(c, a, *lo, *hi, i)
1066 })
1067 })
1068 };
1069 let applicable_alternative_holds = |i: usize| {
1074 *min > 0
1075 && counted.iter().any(|((p, m, id, _, _), (c, a, lo, hi, _))| {
1076 p == seg_path
1077 && m == mig_number
1078 && id != pkg_id
1079 && *lo > 0
1080 && holds(c, a, *lo, *hi, i)
1081 })
1082 };
1083 let mut reported_counts: HashSet<usize> = HashSet::new();
1084 for (i, &present_count) in counts.iter().enumerate() {
1085 if applies[i]
1086 && (present_count < *min || present_count > *max)
1087 && !alternative_holds(i)
1088 && !(present_count < *min && applicable_alternative_holds(i))
1089 && reported_counts.insert(present_count)
1090 {
1091 report.add_issue(
1092 ValidationIssue::new(
1093 Severity::Error,
1094 IssueKind::PackageCardinality {
1095 package_id: pkg_id.to_string(),
1096 present: present_count,
1097 min: *min,
1098 max: *max,
1099 codes: codes.clone(),
1100 },
1101 )
1102 .with_field_path(seg_path)
1103 .with_expected(format!("{min}..{max}"))
1104 .with_actual(present_count.to_string()),
1105 );
1106 }
1107 }
1108 }
1109 }
1110
1111 fn validate_codes_cross_field(
1123 &self,
1124 workflow: &AhbWorkflow,
1125 ctx: &EvaluationContext,
1126 report: &mut ValidationReport,
1127 ) {
1128 if ctx.navigator.is_some() {
1129 self.validate_codes_group_scoped(workflow, ctx, report);
1130 } else {
1131 self.validate_codes_tag_scoped(workflow, ctx, report);
1132 }
1133 }
1134
1135 fn validate_codes_group_scoped(
1138 &self,
1139 workflow: &AhbWorkflow,
1140 ctx: &EvaluationContext,
1141 report: &mut ValidationReport,
1142 ) {
1143 let by_loc = partition_codes_by_mig(workflow);
1144 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
1145 let data_slots = data_slot_entries(workflow);
1146 let nav = ctx.navigator.unwrap();
1147
1148 for ((group_key, tag), migs) in &by_loc {
1149 let field_path = if group_key.is_empty() {
1150 format!("{tag}/qualifier")
1151 } else {
1152 format!("{group_key}/{tag}/qualifier")
1153 };
1154
1155 let group_path: Vec<&str> = if group_key.is_empty() {
1156 Vec::new()
1157 } else {
1158 group_key.split('/').collect()
1159 };
1160
1161 let tag_qualifiers = known_qualifiers.get(tag);
1162
1163 if group_path.is_empty() {
1164 Self::validate_segments_per_mig(
1165 &ctx.find_segments(tag),
1166 migs,
1167 tag_qualifiers,
1168 false,
1169 tag,
1170 &field_path,
1171 report,
1172 );
1173 } else {
1174 let instance_count = nav.group_instance_count(&group_path);
1175 let slots = data_slots.get(&(group_key.clone(), tag.clone()));
1176 for i in 0..instance_count {
1177 let owned = nav.find_segments_in_group(tag, &group_path, i);
1178 let refs: Vec<&OwnedSegment> = owned.iter().collect();
1179 let holds_data_slot = slots.is_some_and(|entries| {
1182 entries.iter().any(|(entry_tag, entry_mig)| {
1183 let Some(bucket) = by_loc
1184 .get(&(group_key.clone(), entry_tag.clone()))
1185 .and_then(|m| m.get(entry_mig))
1186 else {
1187 return false;
1188 };
1189 nav.find_segments_in_group(entry_tag, &group_path, i)
1190 .iter()
1191 .any(|s| matches_qualifier(s, bucket))
1192 })
1193 });
1194 Self::validate_segments_per_mig(
1195 &refs,
1196 migs,
1197 tag_qualifiers,
1198 holds_data_slot,
1199 tag,
1200 &field_path,
1201 report,
1202 );
1203 }
1204 }
1205 }
1206 }
1207
1208 fn validate_codes_tag_scoped(
1212 &self,
1213 workflow: &AhbWorkflow,
1214 ctx: &EvaluationContext,
1215 report: &mut ValidationReport,
1216 ) {
1217 let by_loc = partition_codes_by_mig(workflow);
1218 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
1219 let mut by_tag: BTreeMap<String, BTreeMap<Option<String>, MigCodeBucket>> = BTreeMap::new();
1221 for ((_group_key, tag), migs) in by_loc {
1222 let merged = by_tag.entry(tag).or_default();
1223 for (mig_key, bucket) in migs {
1224 let entry = merged.entry(mig_key).or_default();
1225 if entry.qualifier_position.is_none() {
1226 entry.qualifier_position = bucket.qualifier_position;
1227 }
1228 if entry.qualifier_position == bucket.qualifier_position {
1229 entry.qualifier_values.extend(&bucket.qualifier_values);
1230 }
1231 for (pos, codes) in bucket.codes {
1232 entry.codes.entry(pos).or_default().extend(codes);
1233 }
1234 }
1235 }
1236
1237 for (tag, migs) in &by_tag {
1238 let field_path = format!("{tag}/qualifier");
1239 Self::validate_segments_per_mig(
1240 &ctx.find_segments(tag),
1241 migs,
1242 known_qualifiers.get(tag),
1243 false,
1244 tag,
1245 &field_path,
1246 report,
1247 );
1248 }
1249 }
1250
1251 fn validate_segments_per_mig(
1264 segments: &[&OwnedSegment],
1265 migs: &BTreeMap<Option<String>, MigCodeBucket>,
1266 tag_qualifiers: Option<&BTreeMap<(usize, usize), HashSet<String>>>,
1267 holds_data_slot: bool,
1268 tag: &str,
1269 field_path: &str,
1270 report: &mut ValidationReport,
1271 ) {
1272 for seg in segments {
1273 match match_segment_to_mig(seg, migs) {
1274 Some(bucket) => {
1275 for ((el, c), allowed) in &bucket.codes {
1276 if allowed.is_empty() {
1277 continue;
1278 }
1279 Self::check_segments_against_codes(
1280 vec![*seg],
1281 allowed,
1282 tag,
1283 *el,
1284 *c,
1285 field_path,
1286 report,
1287 );
1288 }
1289 }
1290 None => {
1291 let Some(qualifiers) = tag_qualifiers.filter(|_| !holds_data_slot) else {
1292 continue;
1293 };
1294 for ((el, c), allowed) in qualifiers {
1299 let Some(actual) = seg
1300 .elements
1301 .get(*el)
1302 .and_then(|e| e.get(*c))
1303 .filter(|v| !v.is_empty())
1304 .map(|s| s.as_str())
1305 else {
1306 continue;
1307 };
1308 if allowed.iter().any(|v| v == actual) {
1309 break;
1311 }
1312 let bucket_values: HashSet<&str> = migs
1315 .values()
1316 .filter(|b| b.qualifier_position == Some((*el, *c)))
1317 .flat_map(|b| b.qualifier_values.iter().copied())
1318 .collect();
1319 if !bucket_values.is_empty() {
1320 Self::check_segments_against_codes(
1321 vec![*seg],
1322 &bucket_values,
1323 tag,
1324 *el,
1325 *c,
1326 field_path,
1327 report,
1328 );
1329 break;
1330 }
1331 }
1332 }
1333 }
1334 }
1335 }
1336
1337 fn validate_answer_lists(
1346 &self,
1347 workflow: &AhbWorkflow,
1348 ctx: &EvaluationContext,
1349 report: &mut ValidationReport,
1350 ) {
1351 let lists = AnswerCodeLists::embedded();
1352 let fv = self.evaluator.format_version();
1353 let mut seen: HashSet<(*const OwnedSegment, usize, usize)> = HashSet::new();
1354 for list_rule in &workflow.fields {
1355 let Some(answer_path) = answer_path(&list_rule.segment_path) else {
1356 continue;
1357 };
1358 if list_rule.codes.is_empty()
1359 || !list_rule.codes.iter().all(|c| is_answer_list_id(&c.value))
1360 {
1361 continue;
1362 }
1363 let Some(answer_rule) = workflow
1364 .fields
1365 .iter()
1366 .find(|f| f.segment_path == answer_path && f.mig_number == list_rule.mig_number)
1367 else {
1368 continue;
1369 };
1370 let at = |rule: &AhbFieldRule| {
1371 (
1372 rule.element_index.unwrap_or(0),
1373 rule.component_index.unwrap_or(0),
1374 )
1375 };
1376 let (list_el, list_c) = at(list_rule);
1377 let (answer_el, answer_c) = at(answer_rule);
1378 let value = |seg: &OwnedSegment, el: usize, c: usize| {
1379 seg.elements
1380 .get(el)
1381 .and_then(|e| e.get(c))
1382 .filter(|v| !v.is_empty())
1383 .cloned()
1384 };
1385 for seg in ctx.find_segments(&extract_segment_id(&list_rule.segment_path)) {
1386 if !seen.insert((seg as *const OwnedSegment, list_el, list_c)) {
1387 continue;
1388 }
1389 let (Some(list), Some(answer)) =
1390 (value(seg, list_el, list_c), value(seg, answer_el, answer_c))
1391 else {
1392 continue;
1393 };
1394 if !list_rule.codes.iter().any(|c| c.value == list) {
1395 continue;
1396 }
1397 if answer_rule.codes.iter().any(|c| c.value == answer) {
1401 continue;
1402 }
1403 let Some(codes) = lists.codes(fv, &list) else {
1404 continue;
1405 };
1406 if codes.contains(&answer) {
1407 continue;
1408 }
1409 let mut allowed: Vec<String> = codes.iter().cloned().collect();
1410 allowed.sort_unstable();
1411 report.add_issue(
1412 ValidationIssue::new(
1413 Severity::Warning,
1414 IssueKind::AnswerNotInList {
1415 answer: answer.clone(),
1416 list: list.clone(),
1417 allowed: allowed.clone(),
1418 },
1419 )
1420 .with_field_path(&answer_rule.segment_path)
1421 .with_actual(&answer)
1422 .with_expected(allowed.join(", ")),
1423 );
1424 }
1425 }
1426 }
1427
1428 fn check_segments_against_codes(
1430 segments: Vec<&OwnedSegment>,
1431 allowed_codes: &HashSet<&str>,
1432 _tag: &str,
1433 el_idx: usize,
1434 comp_idx: usize,
1435 field_path: &str,
1436 report: &mut ValidationReport,
1437 ) {
1438 for segment in segments {
1439 if let Some(code_value) = segment
1440 .elements
1441 .get(el_idx)
1442 .and_then(|e| e.get(comp_idx))
1443 .filter(|v| !v.is_empty())
1444 {
1445 if !allowed_codes.contains(code_value.as_str()) {
1446 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1447 sorted_codes.sort_unstable();
1448 let kind = IssueKind::CodeNotAllowedForPid {
1449 value: code_value.to_string(),
1450 allowed: sorted_codes.iter().map(|s| (*s).to_string()).collect(),
1451 };
1452 let mut issue = ValidationIssue::new(Severity::Error, kind)
1453 .with_field_path(field_path)
1454 .with_actual(code_value)
1455 .with_expected(sorted_codes.join(", "))
1456 .with_field_position(el_idx as u32 + 2, Some(comp_idx as u32 + 1));
1457 if segment.segment_number > 0 {
1458 issue =
1459 issue.with_position(crate::SegmentPosition::at(segment.segment_number));
1460 }
1461 report.add_issue(issue);
1462 }
1463 }
1464 }
1465 }
1466}
1467
1468fn full_instance_paths<'p>(
1471 nav: &dyn GroupNavigator,
1472 group_path: &[&'p str],
1473) -> Vec<Vec<(&'p str, usize)>> {
1474 let mut paths: Vec<Vec<(&'p str, usize)>> = vec![Vec::new()];
1475 for g in group_path {
1476 paths = paths
1477 .into_iter()
1478 .flat_map(|parent| {
1479 (0..nav.child_instance_count(&parent, g)).map(move |i| {
1480 let mut path = parent.clone();
1481 path.push((*g, i));
1482 path
1483 })
1484 })
1485 .collect();
1486 }
1487 paths
1488}
1489
1490struct VariantEntry<'a> {
1493 tag: String,
1494 element: usize,
1495 component: usize,
1496 codes: HashSet<&'a str>,
1497}
1498
1499fn merged_variant_entry<'a>(
1505 workflow: &'a AhbWorkflow,
1506 group_path: &str,
1507 mig: &str,
1508) -> Option<VariantEntry<'a>> {
1509 let mig: u32 = mig.parse().ok()?;
1510 let prefix = format!("{group_path}/");
1511 let direct: Vec<(&AhbFieldRule, u32, &str)> = workflow
1513 .fields
1514 .iter()
1515 .filter_map(|r| {
1516 let rest = r.segment_path.strip_prefix(&prefix)?;
1517 let tag = rest.split('/').next()?;
1518 if tag.starts_with("SG") {
1519 return None;
1520 }
1521 Some((r, r.mig_number.as_deref()?.parse::<u32>().ok()?, tag))
1522 })
1523 .collect();
1524 let (_, _, entry_tag) = *direct.iter().min_by_key(|(_, n, _)| *n)?;
1526 let entries: Vec<&(&AhbFieldRule, u32, &str)> = direct
1527 .iter()
1528 .filter(|(r, _, tag)| *tag == entry_tag && !r.codes.is_empty())
1529 .collect();
1530 let numbers: HashSet<u32> = entries.iter().map(|(_, n, _)| *n).collect();
1531 if numbers.len() < 2 {
1532 return None;
1533 }
1534 let own = entries
1535 .iter()
1536 .filter(|(_, n, _)| *n <= mig)
1537 .map(|(_, n, _)| *n)
1538 .max()?;
1539 let (rule, _, tag) = entries
1541 .iter()
1542 .filter(|(_, n, _)| *n == own)
1543 .min_by_key(|(r, _, _)| (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))?;
1544 Some(VariantEntry {
1545 tag: tag.to_string(),
1546 element: rule.element_index.unwrap_or(0),
1547 component: rule.component_index.unwrap_or(0),
1548 codes: rule.codes.iter().map(|c| c.value.as_str()).collect(),
1549 })
1550}
1551
1552fn value_length(format: &str, value: &str) -> usize {
1556 let format = format.trim();
1557 if format.starts_with('n') {
1558 value
1559 .chars()
1560 .filter(|c| !matches!(c, '.' | ',' | '-'))
1561 .count()
1562 } else {
1563 value.chars().count()
1564 }
1565}
1566
1567fn max_length(format: &str) -> Option<usize> {
1570 let digits = format
1571 .trim()
1572 .strip_prefix("an")
1573 .or_else(|| format.trim().strip_prefix('a'))
1574 .or_else(|| format.trim().strip_prefix('n'))?;
1575 digits.strip_prefix("..").unwrap_or(digits).parse().ok()
1576}
1577
1578fn governing_status<'a>(field: &'a AhbFieldRule, value: Option<&str>) -> &'a str {
1589 let derived_from_codes = field
1590 .codes
1591 .first()
1592 .is_some_and(|c| c.ahb_status == field.ahb_status);
1593 if !derived_from_codes && !codes_are_alternatives(field) {
1594 return &field.ahb_status;
1595 }
1596 value
1597 .and_then(|v| field.codes.iter().find(|c| c.value == v))
1598 .map_or(&field.ahb_status, |c| &c.ahb_status)
1599}
1600
1601fn codes_are_alternatives(field: &AhbFieldRule) -> bool {
1612 !field.codes.is_empty()
1613 && field.ahb_status.contains('[')
1614 && field
1615 .codes
1616 .iter()
1617 .all(|c| c.ahb_status.contains('[') && c.ahb_status != field.ahb_status)
1618}
1619
1620fn should_skip_for_parent_group<E: ConditionEvaluator>(
1630 field: &AhbFieldRule,
1631 expr_eval: &ConditionExprEvaluator<E>,
1632 ctx: &EvaluationContext,
1633 ub_definitions: &BTreeMap<String, ConditionExpr>,
1634) -> bool {
1635 let Some(group_status) = field.parent_group_ahb_status.as_deref() else {
1636 return false;
1637 };
1638 if !group_status.contains('[') {
1639 return false;
1640 }
1641 match ConditionParser::parse_status_branches(group_status, ub_definitions) {
1642 Ok(branches) => !branches.iter().any(|branch| {
1643 branch.condition.as_ref().map_or(true, |c| {
1644 expr_eval.evaluate(c, ctx) == ConditionResult::True
1645 })
1646 }),
1647 Err(_) => true,
1648 }
1649}
1650
1651fn parent_group_refused<E: ConditionEvaluator>(
1655 field: &AhbFieldRule,
1656 expr_eval: &ConditionExprEvaluator<E>,
1657 ctx: &EvaluationContext,
1658 ub_definitions: &BTreeMap<String, ConditionExpr>,
1659) -> bool {
1660 let Some(group_status) = field.parent_group_ahb_status.as_deref() else {
1661 return false;
1662 };
1663 if !group_status.contains('[') {
1664 return false;
1665 }
1666 match ConditionParser::parse_status_branches(group_status, ub_definitions) {
1667 Ok(branches) => branches.iter().all(|branch| {
1668 branch
1669 .condition
1670 .as_ref()
1671 .is_some_and(|c| expr_eval.evaluate(c, ctx) == ConditionResult::False)
1672 }),
1673 Err(_) => true,
1674 }
1675}
1676
1677fn is_parent_group_required<E: ConditionEvaluator>(
1684 field: &AhbFieldRule,
1685 expr_eval: &ConditionExprEvaluator<E>,
1686 ctx: &EvaluationContext,
1687 ub_definitions: &BTreeMap<String, ConditionExpr>,
1688) -> bool {
1689 field
1690 .parent_group_ahb_status
1691 .as_deref()
1692 .is_some_and(|status| {
1693 let status = expr_eval.resolve_status(status, ctx, ub_definitions);
1694 status.result == ConditionResult::True && status.kind == Some(StatusKind::Muss)
1695 })
1696}
1697
1698fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1707 let segment_id = extract_segment_id(&field.segment_path);
1708
1709 if !field.codes.is_empty() {
1715 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1716 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1717 let matching = ctx.find_segments(&segment_id);
1718 return matching.iter().any(|seg| {
1719 seg.elements
1720 .get(el_idx)
1721 .and_then(|e| e.get(comp_idx))
1722 .is_some_and(|v| required_codes.contains(&v.as_str()))
1723 });
1724 }
1725 if is_qualifier_field(&field.segment_path) {
1728 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1729 let el_idx = field.element_index.unwrap_or(0);
1730 let comp_idx = field.component_index.unwrap_or(0);
1731 let matching = ctx.find_segments(&segment_id);
1732 return matching.iter().any(|seg| {
1733 seg.elements
1734 .get(el_idx)
1735 .and_then(|e| e.get(comp_idx))
1736 .is_some_and(|v| required_codes.contains(&v.as_str()))
1737 });
1738 }
1739 }
1740
1741 ctx.has_segment(&segment_id)
1742}
1743
1744fn is_group_variant_absent(
1759 ctx: &EvaluationContext,
1760 field: &AhbFieldRule,
1761 is_group_mandatory: bool,
1762) -> bool {
1763 let group_path: Vec<&str> = field
1764 .segment_path
1765 .split('/')
1766 .take_while(|p| p.starts_with("SG"))
1767 .collect();
1768
1769 if group_path.is_empty() {
1770 return false;
1771 }
1772
1773 let nav = match ctx.navigator {
1774 Some(nav) => nav,
1775 None => return false,
1776 };
1777
1778 let instance_count = nav.group_instance_count(&group_path);
1779
1780 if instance_count == 0 {
1785 if !is_group_mandatory {
1786 return true;
1787 }
1788 return false;
1790 }
1791
1792 if field.parent_group_ahb_status.is_some() && !is_group_mandatory {
1797 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1799 let segment_id = extract_segment_id(&field.segment_path);
1800 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1801
1802 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1803 nav.find_segments_in_group(&segment_id, &group_path, i)
1804 .iter()
1805 .any(|seg| {
1806 seg.elements
1807 .first()
1808 .and_then(|e| e.first())
1809 .is_some_and(|v| required_codes.contains(&v.as_str()))
1810 })
1811 });
1812
1813 if !any_instance_has_qualifier {
1814 return true; }
1816 }
1817 }
1818
1819 let segment_id = extract_segment_id(&field.segment_path);
1828 let segment_absent_from_all = (0..instance_count).all(|i| {
1829 nav.find_segments_in_group(&segment_id, &group_path, i)
1830 .is_empty()
1831 });
1832 if segment_absent_from_all {
1833 let group_has_other_segments =
1834 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1835 if group_has_other_segments {
1836 return true;
1837 }
1838 }
1839
1840 false
1841}
1842
1843fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1845 out.extend(group.fields.iter());
1846 for child in &group.children {
1847 collect_nodes_depth_first(child, out);
1848 }
1849}
1850
1851#[allow(clippy::too_many_arguments)]
1855fn evaluate_node<E: ConditionEvaluator>(
1856 node: &AhbNode,
1857 ctx: &EvaluationContext,
1858 expr_eval: &ConditionExprEvaluator<E>,
1859 evaluator: &E,
1860 ub_definitions: &BTreeMap<String, ConditionExpr>,
1861 tag_migs: &HashMap<String, HashSet<&str>>,
1862 instance_index: Option<usize>,
1863 slot_holds: bool,
1864 report: &mut ValidationReport,
1865) {
1866 let field = node.rule;
1867
1868 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
1870
1871 if let (Some(value), Some(max)) = (node.value, field.format.as_deref().and_then(max_length)) {
1876 let length = value_length(field.format.as_deref().unwrap_or_default(), value);
1877 if length > max {
1878 let mut issue = ValidationIssue::new(
1879 Severity::Error,
1880 IssueKind::ValueTooLong {
1881 field_name: field.name.clone(),
1882 length,
1883 max,
1884 },
1885 )
1886 .with_field_path(&field.segment_path)
1887 .with_actual(value)
1888 .with_expected(field.format.clone().unwrap_or_default());
1889 if let Some(idx) = instance_index {
1890 issue = issue.with_instance_index(idx);
1891 }
1892 if let Some(num) = node.matched_segment_number {
1893 issue = issue.with_position(crate::SegmentPosition::at(num));
1894 }
1895 report.add_issue(attach_field_position(issue, field));
1896 }
1897 }
1898
1899 if let (Some(value), Some(format)) = (node.value, field.format.as_deref()) {
1902 if slot_holds
1903 && format.trim().starts_with('n')
1904 && field.codes.is_empty()
1905 && !value.is_empty()
1906 && !crate::eval::format_validators::is_edifact_number(value)
1907 {
1908 let mut issue = ValidationIssue::new(
1909 Severity::Error,
1910 IssueKind::ValueNotNumeric {
1911 field_name: field.name.clone(),
1912 },
1913 )
1914 .with_field_path(&field.segment_path)
1915 .with_actual(value)
1916 .with_expected(format);
1917 if let Some(idx) = instance_index {
1918 issue = issue.with_instance_index(idx);
1919 }
1920 if let Some(num) = node.matched_segment_number {
1921 issue = issue.with_position(crate::SegmentPosition::at(num));
1922 }
1923 report.add_issue(attach_field_position(issue, field));
1924 }
1925 }
1926
1927 if parent_group_refused(field, expr_eval, ctx, ub_definitions) {
1931 return;
1932 }
1933
1934 let status = governing_status(field, node.value);
1937 let StatusResolution {
1938 result: condition_result,
1939 kind,
1940 unknown_ids,
1941 } = expr_eval.resolve_status(status, &node_ctx, ub_definitions);
1942 let mandatory = kind == Some(StatusKind::Muss);
1943
1944 match condition_result {
1945 ConditionResult::True => {
1946 if mandatory && node.value.is_none() {
1955 let tag = extract_segment_id(&field.segment_path);
1956 let single_variant_present = node.segment_elements.is_none()
1957 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
1958 && is_field_present(ctx, field);
1959 let segment_optional_and_absent = node.segment_elements.is_none()
1964 && field
1965 .segment_ahb_status
1966 .as_deref()
1967 .is_some_and(is_optional_segment_status);
1968 let segment_not_required_and_absent = node.segment_elements.is_none()
1975 && field.segment_ahb_status.as_deref().is_some_and(|st| {
1976 let status = expr_eval.resolve_status(st, &node_ctx, ub_definitions);
1977 status.result != ConditionResult::True
1978 || status.kind != Some(StatusKind::Muss)
1979 });
1980 if !single_variant_present
1981 && !segment_optional_and_absent
1982 && !segment_not_required_and_absent
1983 {
1984 let mut issue = ValidationIssue::new(
1985 Severity::Error,
1986 IssueKind::MissingRequiredField {
1987 field_name: field.name.clone(),
1988 },
1989 )
1990 .with_field_path(&field.segment_path)
1991 .with_rule(&field.ahb_status);
1992 if let Some(first_code) = field.codes.first() {
1993 issue.expected_value = Some(first_code.value.clone());
1994 }
1995 if let Some(idx) = instance_index {
1996 issue = issue.with_instance_index(idx);
1997 }
1998 if let Some(num) = node.matched_segment_number {
1999 issue = issue.with_position(crate::SegmentPosition::at(num));
2000 }
2001 issue = attach_field_position(issue, field);
2002 report.add_issue(issue);
2003 }
2004 }
2005 }
2006 ConditionResult::False => {
2007 if mandatory && node.value.is_some() {
2010 let failed = expr_eval.false_ids_with_ub(status, &node_ctx, ub_definitions);
2011 let mut issue = ValidationIssue::new(
2012 Severity::Error,
2013 IssueKind::FieldConditionNotSatisfied {
2014 field_name: field.name.clone(),
2015 failed,
2016 },
2017 )
2018 .with_field_path(&field.segment_path)
2019 .with_rule(&field.ahb_status);
2020 if let Some(idx) = instance_index {
2021 issue = issue.with_instance_index(idx);
2022 }
2023 if let Some(num) = node.matched_segment_number {
2024 issue = issue.with_position(crate::SegmentPosition::at(num));
2025 }
2026 issue = attach_field_position(issue, field);
2027 report.add_issue(issue);
2028 }
2029 }
2030 ConditionResult::Unknown => {
2031 let mut external_ids = Vec::new();
2033 let mut undetermined_ids = Vec::new();
2034 let mut missing_ids = Vec::new();
2035 for id in unknown_ids {
2036 if evaluator.is_external(id) {
2037 external_ids.push(id);
2038 } else if evaluator.is_known(id) {
2039 undetermined_ids.push(id);
2040 } else {
2041 missing_ids.push(id);
2042 }
2043 }
2044
2045 let mut issue = ValidationIssue::new(
2046 Severity::Info,
2047 IssueKind::ConditionUnknown {
2048 field_name: field.name.clone(),
2049 unresolved: UnresolvedConditions {
2050 external: external_ids,
2051 undetermined: undetermined_ids,
2052 missing: missing_ids,
2053 },
2054 },
2055 )
2056 .with_field_path(&field.segment_path)
2057 .with_rule(&field.ahb_status);
2058 if let Some(idx) = instance_index {
2059 issue = issue.with_instance_index(idx);
2060 }
2061 if let Some(num) = node.matched_segment_number {
2062 issue = issue.with_position(crate::SegmentPosition::at(num));
2063 }
2064 issue = attach_field_position(issue, field);
2065 report.add_issue(issue);
2066 }
2067 }
2068}
2069
2070fn attach_field_position(issue: ValidationIssue, field: &AhbFieldRule) -> ValidationIssue {
2075 let issue = issue.with_mig_number(field.mig_number.as_deref());
2076 match field.element_index {
2077 Some(el) => {
2078 let element_pos = (el as u32) + 2;
2079 let component_pos = field.component_index.map(|c| (c as u32) + 1);
2080 issue.with_field_position(element_pos, component_pos)
2081 }
2082 None => issue,
2083 }
2084}
2085
2086fn report_repeated_in_transaction(
2093 transaction: &AhbGroupNode,
2094 transaction_index: usize,
2095 once: u32,
2096 ctx: &EvaluationContext,
2097 report: &mut ValidationReport,
2098) {
2099 let mut counts: BTreeMap<(String, String), (usize, String)> = BTreeMap::new();
2101 fn visit<'a>(
2102 node: &'a AhbGroupNode<'a>,
2103 path: &mut Vec<(&'a str, usize)>,
2104 once: u32,
2105 ctx: &EvaluationContext,
2106 counts: &mut BTreeMap<(String, String), (usize, String)>,
2107 ) {
2108 let ids: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2109 let mut seen: HashSet<Option<&str>> = HashSet::new();
2110 for n in &node.fields {
2111 let rule = n.rule;
2112 let Some(status) = rule.segment_ahb_status.as_deref() else {
2113 continue;
2114 };
2115 if Some(status) == node.ahb_status
2116 || rule.element_index.unwrap_or(0) != 0
2117 || rule.component_index.unwrap_or(0) != 0
2118 || rule.codes.is_empty()
2119 || !crate::eval::binds_once(status, once)
2120 || !seen.insert(rule.mig_number.as_deref())
2121 {
2122 continue;
2123 }
2124 let tag = extract_segment_id(&rule.segment_path);
2125 let sent = ctx.navigator().map_or(0, |nav| {
2126 nav.find_segments_in_instance(&tag, path)
2127 .iter()
2128 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2129 .count()
2130 });
2131 let entry = counts
2132 .entry((
2133 format!("{}/{tag}", ids.join("/")),
2134 rule.mig_number.clone().unwrap_or_default(),
2135 ))
2136 .or_insert((0, status.to_string()));
2137 entry.0 += sent;
2138 }
2139 for child in &node.children {
2140 if let Some(status) = child
2141 .ahb_status
2142 .filter(|s| crate::eval::binds_once(s, once))
2143 {
2144 let variant = child
2145 .fields
2146 .first()
2147 .and_then(|n| n.rule.mig_number.clone())
2148 .unwrap_or_default();
2149 let entry = counts
2150 .entry((format!("{}/{}", ids.join("/"), child.group_id), variant))
2151 .or_insert((0, status.to_string()));
2152 entry.0 += 1;
2153 }
2154 path.push((child.group_id, child.instance_index));
2155 visit(child, path, once, ctx, counts);
2156 path.pop();
2157 }
2158 }
2159 let mut path = vec![(transaction.group_id, transaction_index)];
2160 visit(transaction, &mut path, once, ctx, &mut counts);
2161 for ((path, _), (count, status)) in counts {
2162 if count > 1 {
2163 report.add_issue(
2164 ValidationIssue::new(
2165 Severity::Error,
2166 IssueKind::RepeatedInTransaction {
2167 path: path.clone(),
2168 count,
2169 },
2170 )
2171 .with_field_path(path)
2172 .with_rule(status)
2173 .with_instance_index(transaction_index),
2174 );
2175 }
2176 }
2177}
2178
2179#[allow(clippy::too_many_arguments)]
2186fn refuse_segments<E: ConditionEvaluator>(
2187 fields: &[AhbNode],
2188 group_status: Option<&str>,
2189 path: &[(&str, usize)],
2190 scoped_ctx: &EvaluationContext,
2191 evaluator: &E,
2192 ub_definitions: &BTreeMap<String, ConditionExpr>,
2193 instance_index: usize,
2194 report: &mut ValidationReport,
2195) {
2196 let mut seen: Vec<u32> = Vec::new();
2197 for node in fields {
2198 let rule = node.rule;
2199 let (Some(status), Some(value), Some(number)) = (
2200 rule.segment_ahb_status.as_deref(),
2201 node.value,
2202 node.matched_segment_number,
2203 ) else {
2204 continue;
2205 };
2206 let qualifier =
2209 rule.element_index.unwrap_or(0) == 0 && rule.component_index.unwrap_or(0) == 0;
2210 if !qualifier
2211 || seen.contains(&number)
2212 || Some(status) == group_status
2213 || !rule.codes.iter().any(|c| c.value == value)
2214 {
2215 continue;
2216 }
2217 seen.push(number);
2218 if !crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2219 continue;
2220 }
2221 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2222 segment.extend(rule.segment_path.split('/').find(|p| !p.starts_with("SG")));
2223 report.add_issue(
2224 ValidationIssue::new(
2225 Severity::Warning,
2226 IssueKind::SegmentNotAllowed {
2227 segment: segment.join("/"),
2228 },
2229 )
2230 .with_field_path(segment.join("/"))
2231 .with_rule(status)
2232 .with_instance_index(instance_index)
2233 .with_position(crate::SegmentPosition::at(number)),
2234 );
2235 }
2236}
2237
2238fn report_segment_counts<E: ConditionEvaluator>(
2245 group: &AhbGroupNode,
2246 path: &[(&str, usize)],
2247 scoped_ctx: &EvaluationContext,
2248 evaluator: &E,
2249 instance_index: usize,
2250 report: &mut ValidationReport,
2251) {
2252 if crate::eval::repetition_limits(evaluator.message_type()).is_empty() {
2253 return;
2254 }
2255 let mut seen: HashSet<Option<&str>> = HashSet::new();
2256 for node in &group.fields {
2257 let rule = node.rule;
2258 let Some(status) = rule.segment_ahb_status.as_deref() else {
2259 continue;
2260 };
2261 if rule.element_index.unwrap_or(0) != 0
2262 || rule.component_index.unwrap_or(0) != 0
2263 || rule.codes.is_empty()
2264 || !seen.insert(rule.mig_number.as_deref())
2265 {
2266 continue;
2267 }
2268 let tag = extract_segment_id(&rule.segment_path);
2269 let sent: Vec<_> = scoped_ctx
2270 .find_segments_at(path, &tag)
2271 .into_iter()
2272 .filter(|s| rule.codes.iter().any(|c| c.value == s.get_component(0, 0)))
2273 .collect();
2274 if sent.is_empty()
2275 || !crate::eval::breaks_repetition_limit(status, evaluator, scoped_ctx, sent.len())
2276 {
2277 continue;
2278 }
2279 let mut segment: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2280 segment.push(&tag);
2281 let mut issue = ValidationIssue::new(
2282 Severity::Error,
2283 IssueKind::RepetitionCount {
2284 path: segment.join("/"),
2285 count: sent.len(),
2286 },
2287 )
2288 .with_field_path(segment.join("/"))
2289 .with_rule(status)
2290 .with_instance_index(instance_index);
2291 if let Some(num) = node.matched_segment_number {
2292 issue = issue.with_position(crate::SegmentPosition::at(num));
2293 }
2294 report.add_issue(issue);
2295 }
2296}
2297
2298fn report_off_count_per_zeitraum<E: ConditionEvaluator>(
2305 transaction: &AhbGroupNode,
2306 tx: usize,
2307 ctx: &EvaluationContext,
2308 evaluator: &E,
2309 ub_definitions: &BTreeMap<String, ConditionExpr>,
2310 report: &mut ValidationReport,
2311) {
2312 let mut judged: Vec<(&str, Vec<&str>)> = Vec::new();
2313 for child in &transaction.children {
2314 let Some(status) = child.ahb_status else {
2315 continue;
2316 };
2317 if child.group_id != "SG8"
2318 || !(status.contains("[2004]") || status.contains("[2005]"))
2319 || crate::eval::refuses_presence(status, evaluator, ctx, ub_definitions)
2320 {
2321 continue;
2322 }
2323 let Some(codes) = child.fields.iter().find_map(|node| {
2324 let rule = node.rule;
2325 (extract_segment_id(&rule.segment_path) == "SEQ"
2326 && rule.element_index.unwrap_or(0) == 0
2327 && rule.component_index.unwrap_or(0) == 0
2328 && !rule.codes.is_empty())
2329 .then(|| {
2330 rule.codes
2331 .iter()
2332 .map(|c| c.value.as_str())
2333 .collect::<Vec<_>>()
2334 })
2335 }) else {
2336 continue;
2337 };
2338 if judged.iter().any(|(s, c)| *s == status && *c == codes) {
2339 continue;
2340 }
2341 let position = child.fields.iter().find_map(|n| n.matched_segment_number);
2342 for (id, count) in
2343 crate::eval::periods::sg8_off_count_per_zeitraum(ctx, "SG4", tx, &codes, status)
2344 {
2345 let mut issue = ValidationIssue::new(
2346 Severity::Error,
2347 IssueKind::RepetitionCount {
2348 path: "SG4/SG8".to_string(),
2349 count,
2350 },
2351 )
2352 .with_field_path("SG4/SG8")
2353 .with_rule(status)
2354 .with_instance_index(tx);
2355 issue.expected_value = Some(format!("Zeitraum-ID {id}"));
2356 if let Some(num) = position {
2357 issue = issue.with_position(crate::SegmentPosition::at(num));
2358 }
2359 report.add_issue(issue);
2360 }
2361 judged.push((status, codes));
2362 }
2363}
2364
2365#[allow(clippy::too_many_arguments)]
2371fn report_repeated_in_group<E: ConditionEvaluator>(
2372 group: &AhbGroupNode,
2373 path: &[(&str, usize)],
2374 scoped_ctx: &EvaluationContext,
2375 evaluator: &E,
2376 ub_definitions: &BTreeMap<String, ConditionExpr>,
2377 instance_index: usize,
2378 report: &mut ValidationReport,
2379) {
2380 if crate::eval::counted_repetitions(evaluator.message_type()).is_empty() {
2381 return;
2382 }
2383 let mut reported: HashSet<&str> = HashSet::new();
2384 for child in &group.children {
2385 let Some(status) = child.ahb_status else {
2386 continue;
2387 };
2388 let count = group
2389 .children
2390 .iter()
2391 .filter(|c| c.group_id == child.group_id)
2392 .count();
2393 if count < 2 || reported.contains(child.group_id) {
2396 continue;
2397 }
2398 let mut child_path = path.to_vec();
2399 child_path.push((child.group_id, child.instance_index));
2400 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2401 if !crate::eval::refuses_count(status, evaluator, &child_ctx, ub_definitions) {
2402 continue;
2403 }
2404 reported.insert(child.group_id);
2405 let group_path: Vec<&str> = child_path.iter().map(|(g, _)| *g).collect();
2406 let mut issue = ValidationIssue::new(
2407 Severity::Error,
2408 IssueKind::RepetitionCount {
2409 path: group_path.join("/"),
2410 count,
2411 },
2412 )
2413 .with_field_path(group_path.join("/"))
2414 .with_rule(status)
2415 .with_instance_index(instance_index);
2416 if let Some(num) = child.fields.iter().find_map(|n| n.matched_segment_number) {
2417 issue = issue.with_position(crate::SegmentPosition::at(num));
2418 }
2419 report.add_issue(issue);
2420 }
2421}
2422
2423#[allow(clippy::too_many_arguments)]
2432fn walk_group_instance<E: ConditionEvaluator>(
2433 group: &AhbGroupNode,
2434 path: &[(&str, usize)],
2435 scoped_ctx: &EvaluationContext,
2436 expr_eval: &ConditionExprEvaluator<E>,
2437 evaluator: &E,
2438 ub_definitions: &BTreeMap<String, ConditionExpr>,
2439 tag_migs: &HashMap<String, HashSet<&str>>,
2440 instance_index: usize,
2441 report_segments: bool,
2442 report: &mut ValidationReport,
2443) {
2444 let mut group_refused = false;
2447 if let Some(status) = group.ahb_status {
2448 if crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions) {
2449 group_refused = true;
2450 let group_path: Vec<&str> = path.iter().map(|(g, _)| *g).collect();
2451 let mut issue = ValidationIssue::new(
2452 Severity::Warning,
2453 IssueKind::GroupNotAllowed {
2454 group: group_path.join("/"),
2455 },
2456 )
2457 .with_field_path(group_path.join("/"))
2458 .with_rule(status)
2459 .with_instance_index(instance_index);
2460 if let Some(num) = group.fields.iter().find_map(|n| n.matched_segment_number) {
2461 issue = issue.with_position(crate::SegmentPosition::at(num));
2462 }
2463 report.add_issue(issue);
2464 }
2465 }
2466 if report_segments && !group_refused {
2467 refuse_segments(
2468 &group.fields,
2469 group.ahb_status,
2470 path,
2471 scoped_ctx,
2472 evaluator,
2473 ub_definitions,
2474 instance_index,
2475 report,
2476 );
2477 }
2478 let slot_codes: HashMap<Option<&str>, &[AhbCodeRule]> = group
2482 .fields
2483 .iter()
2484 .filter(|n| {
2485 n.rule.element_index.unwrap_or(0) == 0
2486 && n.rule.component_index.unwrap_or(0) == 0
2487 && !n.rule.codes.is_empty()
2488 })
2489 .map(|n| (n.rule.mig_number.as_deref(), n.rule.codes.as_slice()))
2490 .collect();
2491 let holds = |node: &AhbNode| {
2492 let (Some(codes), Some(elements)) = (
2493 slot_codes.get(&node.rule.mig_number.as_deref()),
2494 node.segment_elements,
2495 ) else {
2496 return true;
2497 };
2498 let qualifier = elements
2499 .first()
2500 .and_then(|e| e.first())
2501 .map(String::as_str)
2502 .unwrap_or_default();
2503 codes.iter().any(|c| c.value == qualifier)
2504 };
2505 for node in &group.fields {
2506 evaluate_node(
2507 node,
2508 scoped_ctx,
2509 expr_eval,
2510 evaluator,
2511 ub_definitions,
2512 tag_migs,
2513 Some(instance_index),
2514 holds(node),
2515 report,
2516 );
2517 }
2518 report_repeated_in_group(
2519 group,
2520 path,
2521 scoped_ctx,
2522 evaluator,
2523 ub_definitions,
2524 instance_index,
2525 report,
2526 );
2527 report_segment_counts(group, path, scoped_ctx, evaluator, instance_index, report);
2528 for child in &group.children {
2529 let mut child_path = path.to_vec();
2530 child_path.push((child.group_id, child.instance_index));
2531 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2532 walk_group_instance(
2533 child,
2534 &child_path,
2535 &child_ctx,
2536 expr_eval,
2537 evaluator,
2538 ub_definitions,
2539 tag_migs,
2540 instance_index,
2541 report_segments,
2542 report,
2543 );
2544 }
2545}
2546
2547#[allow(clippy::too_many_arguments)]
2557fn report_absent_children<E: ConditionEvaluator>(
2558 group: &AhbGroupNode,
2559 path: &[(&str, usize)],
2560 scoped_ctx: &EvaluationContext,
2561 absent_eval: &ConditionExprEvaluator<crate::eval::AbsentTarget<'_, E>>,
2562 evaluator: &E,
2563 ub_definitions: &BTreeMap<String, ConditionExpr>,
2564 instance_index: usize,
2565 report: &mut ValidationReport,
2566) {
2567 if group.ahb_status.is_some_and(|status| {
2568 crate::eval::refuses_presence(status, evaluator, scoped_ctx, ub_definitions)
2569 }) {
2570 return;
2571 }
2572 for rule in &group.absent_children {
2573 if should_skip_for_parent_group(rule, absent_eval, scoped_ctx, ub_definitions)
2574 || !is_parent_group_required(rule, absent_eval, scoped_ctx, ub_definitions)
2575 {
2576 continue;
2577 }
2578 let required = |status: &str| {
2579 let resolved = absent_eval.resolve_status(status, scoped_ctx, ub_definitions);
2580 resolved.result == ConditionResult::True && resolved.kind == Some(StatusKind::Muss)
2581 };
2582 if !required(&rule.ahb_status)
2583 || rule
2584 .segment_ahb_status
2585 .as_deref()
2586 .is_some_and(|status| !required(status))
2587 {
2588 continue;
2589 }
2590 let mut issue = ValidationIssue::new(
2591 Severity::Error,
2592 IssueKind::MissingRequiredField {
2593 field_name: rule.name.clone(),
2594 },
2595 )
2596 .with_field_path(&rule.segment_path)
2597 .with_rule(&rule.ahb_status)
2598 .with_instance_index(instance_index);
2599 if let Some(first_code) = rule.codes.first() {
2600 issue.expected_value = Some(first_code.value.clone());
2601 }
2602 report.add_issue(attach_field_position(issue, rule));
2603 }
2604 for child in &group.children {
2605 let mut child_path = path.to_vec();
2606 child_path.push((child.group_id, child.instance_index));
2607 let child_ctx = scoped_ctx.with_scope(GroupScope { path: &child_path });
2608 report_absent_children(
2609 child,
2610 &child_path,
2611 &child_ctx,
2612 absent_eval,
2613 evaluator,
2614 ub_definitions,
2615 instance_index,
2616 report,
2617 );
2618 }
2619}
2620
2621fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
2622 match expr {
2623 ConditionExpr::Package { id, min, max } => {
2624 out.push((*id, *min, *max));
2625 }
2626 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
2627 for e in exprs {
2628 collect_packages(e, out);
2629 }
2630 }
2631 ConditionExpr::Xor(left, right) => {
2632 collect_packages(left, out);
2633 collect_packages(right, out);
2634 }
2635 ConditionExpr::Not(inner) => {
2636 collect_packages(inner, out);
2637 }
2638 ConditionExpr::Ref(_) => {}
2639 }
2640}
2641
2642fn is_optional_segment_status(status: &str) -> bool {
2652 let trimmed = status.trim();
2653 trimmed.starts_with("Kann") || trimmed.starts_with("Soll")
2654}
2655
2656fn is_qualifier_field(path: &str) -> bool {
2666 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
2667 matches!(parts.len(), 2 | 3)
2669}
2670
2671#[derive(Default)]
2679struct MigCodeBucket<'a> {
2680 qualifier_position: Option<(usize, usize)>,
2683 qualifier_values: HashSet<&'a str>,
2686 codes: BTreeMap<(usize, usize), HashSet<&'a str>>,
2689}
2690
2691fn partition_codes_by_mig(
2696 workflow: &AhbWorkflow,
2697) -> BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket<'_>>> {
2698 let mut out: BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket>> =
2699 BTreeMap::new();
2700 for field in &workflow.fields {
2701 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
2702 continue;
2703 }
2704 if codes_are_alternatives(field) {
2707 continue;
2708 }
2709 let tag = extract_segment_id(&field.segment_path);
2710 let group_key = extract_group_path_key(&field.segment_path);
2711 let mig = field.mig_number.clone();
2712 let el = field.element_index.unwrap_or(0);
2713 let c = field.component_index.unwrap_or(0);
2714
2715 let bucket = out
2716 .entry((group_key, tag))
2717 .or_default()
2718 .entry(mig)
2719 .or_default();
2720
2721 let required: Vec<&str> = field
2728 .codes
2729 .iter()
2730 .filter(|code| code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss"))
2731 .map(|code| code.value.as_str())
2732 .collect();
2733
2734 if !required.is_empty() {
2741 if bucket.qualifier_position.is_none() {
2742 bucket.qualifier_position = Some((el, c));
2743 }
2744 if bucket.qualifier_position == Some((el, c)) {
2745 bucket.qualifier_values.extend(required.iter().copied());
2746 }
2747 }
2748
2749 for v in required {
2750 bucket.codes.entry((el, c)).or_default().insert(v);
2751 }
2752 }
2753 out
2754}
2755
2756fn global_qualifiers_by_tag(
2762 by_loc: &BTreeMap<(String, String), BTreeMap<Option<String>, MigCodeBucket<'_>>>,
2763) -> BTreeMap<String, BTreeMap<(usize, usize), HashSet<String>>> {
2764 let mut out: BTreeMap<String, BTreeMap<(usize, usize), HashSet<String>>> = BTreeMap::new();
2765 for ((_group, tag), migs) in by_loc {
2766 let tag_entry = out.entry(tag.clone()).or_default();
2767 for bucket in migs.values() {
2768 if let Some(pos) = bucket.qualifier_position {
2769 tag_entry
2770 .entry(pos)
2771 .or_default()
2772 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
2773 }
2774 }
2775 }
2776 out
2777}
2778
2779fn matches_qualifier(seg: &OwnedSegment, bucket: &MigCodeBucket<'_>) -> bool {
2781 bucket.qualifier_position.is_some_and(|(el, c)| {
2782 seg.elements
2783 .get(el)
2784 .and_then(|e| e.get(c))
2785 .is_some_and(|v| bucket.qualifier_values.contains(v.as_str()))
2786 })
2787}
2788
2789fn data_slot_entries(
2795 workflow: &AhbWorkflow,
2796) -> HashMap<(String, String), Vec<(String, Option<String>)>> {
2797 let mut out: HashMap<(String, String), Vec<(String, Option<String>)>> = HashMap::new();
2798 let mut tags_by_mig: HashMap<String, BTreeMap<&str, Vec<String>>> = HashMap::new();
2803 for f in &workflow.fields {
2804 if let Some(n) = f.mig_number.as_deref() {
2805 let tags = tags_by_mig
2806 .entry(extract_group_path_key(&f.segment_path))
2807 .or_default()
2808 .entry(n)
2809 .or_default();
2810 let tag = extract_segment_id(&f.segment_path);
2811 if tags.last() != Some(&tag) {
2812 tags.push(tag);
2813 }
2814 }
2815 }
2816 let mut judged: HashSet<(&str, &str)> = HashSet::new();
2817 for field in &workflow.fields {
2818 let leading =
2819 field.element_index.unwrap_or(0) == 0 && field.component_index.unwrap_or(0) == 0;
2820 let Some(mig) = field.mig_number.as_deref() else {
2821 continue;
2822 };
2823 if !field.codes.is_empty() || !leading || !is_qualifier_field(&field.segment_path) {
2824 continue;
2825 }
2826 if !judged.insert((field.segment_path.as_str(), mig)) {
2827 continue;
2828 }
2829 let tag = extract_segment_id(&field.segment_path);
2830 let group_key = extract_group_path_key(&field.segment_path);
2831 if group_key.is_empty() {
2832 continue;
2833 }
2834 let entry = tags_by_mig.get(&group_key).and_then(|migs| {
2837 migs.range(..mig).rev().find_map(|(n, tags)| {
2838 tags.iter()
2839 .rev()
2840 .find(|t| **t != tag)
2841 .map(|t| (*n, t.clone()))
2842 })
2843 });
2844 if let Some((n, entry_tag)) = entry {
2845 let slot = (entry_tag, Some(n.to_string()));
2846 let entries = out.entry((group_key, tag)).or_default();
2847 if !entries.contains(&slot) {
2848 entries.push(slot);
2849 }
2850 }
2851 }
2852 out
2853}
2854
2855fn match_segment_to_mig<'a, 'b>(
2866 seg: &OwnedSegment,
2867 migs: &'a BTreeMap<Option<String>, MigCodeBucket<'b>>,
2868) -> Option<&'a MigCodeBucket<'b>> {
2869 let actual_at = |el: usize, c: usize| -> &str {
2870 seg.elements
2871 .get(el)
2872 .and_then(|e| e.get(c))
2873 .map(|s| s.as_str())
2874 .unwrap_or("")
2875 };
2876
2877 let mut best: Option<&MigCodeBucket> = None;
2878 let mut best_matches = 0usize;
2879
2880 for bucket in migs.values() {
2881 let Some((el, c)) = bucket.qualifier_position else {
2882 continue;
2883 };
2884 if !bucket.qualifier_values.contains(actual_at(el, c)) {
2885 continue;
2886 }
2887 let extra_matches = bucket
2888 .codes
2889 .iter()
2890 .filter(|(pos, _)| **pos != (el, c))
2891 .filter(|((e, k), allowed)| {
2892 let v = actual_at(*e, *k);
2893 !v.is_empty() && allowed.contains(v)
2894 })
2895 .count();
2896 if best.is_none() || extra_matches > best_matches {
2897 best = Some(bucket);
2898 best_matches = extra_matches;
2899 }
2900 }
2901 best
2902}
2903
2904fn extract_group_path_key(path: &str) -> String {
2909 let sg_parts: Vec<&str> = path
2910 .split('/')
2911 .take_while(|p| p.starts_with("SG"))
2912 .collect();
2913 sg_parts.join("/")
2914}
2915
2916fn extract_segment_id(path: &str) -> String {
2918 for part in path.split('/') {
2919 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
2921 continue;
2922 }
2923 if part.len() >= 3
2925 && part
2926 .chars()
2927 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
2928 {
2929 return part.to_string();
2930 }
2931 }
2932 path.split('/').next_back().unwrap_or(path).to_string()
2934}
2935
2936pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
2948 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
2950 if unh_count > 1 {
2951 return Some(ValidationIssue::new(
2952 Severity::Error,
2953 IssueKind::UntCountNotVerifiable { unh_count },
2954 ));
2955 }
2956
2957 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
2959 let declared: usize = unt.get_element(0).parse().ok()?;
2960
2961 let actual = segments
2963 .iter()
2964 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
2965 .count();
2966
2967 if declared != actual {
2968 Some(
2969 ValidationIssue::new(
2970 Severity::Error,
2971 IssueKind::UntSegmentCountMismatch { declared, actual },
2972 )
2973 .with_field_path("UNT/0074")
2974 .with_expected(actual.to_string())
2975 .with_actual(declared.to_string()),
2976 )
2977 } else {
2978 None
2979 }
2980}
2981
2982#[cfg(test)]
2983mod tests {
2984
2985 #[test]
2986 fn a_numeric_length_counts_digits_only() {
2987 assert_eq!(value_length("n..15", "-12.50"), 4);
2988 assert_eq!(value_length("n..6", "JMMHAYJXGIJT"), 12);
2989 assert_eq!(value_length("n5", "55001"), 5);
2990 assert_eq!(value_length("an..3", "Z.1"), 3);
2991 assert_eq!(value_length("an..35", "Müller"), 6);
2992 }
2993 use super::super::codes::ErrorCodes;
2994 use super::super::issue::ValidationCategory;
2995 use super::*;
2996 use crate::display::{EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
2997 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
2998 use std::collections::HashMap;
2999
3000 fn narrate(issue: &ValidationIssue) -> String {
3003 TechnicalNarrator.describe(issue, EdifactView.location(issue).as_deref())
3004 }
3005
3006 struct MockEvaluator {
3008 results: HashMap<u32, CR>,
3009 }
3010
3011 impl MockEvaluator {
3012 fn new(results: Vec<(u32, CR)>) -> Self {
3013 Self {
3014 results: results.into_iter().collect(),
3015 }
3016 }
3017
3018 fn all_true(ids: &[u32]) -> Self {
3019 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
3020 }
3021 }
3022
3023 impl ConditionEvaluator for MockEvaluator {
3024 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
3025 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
3026 }
3027 fn is_external(&self, _condition: u32) -> bool {
3028 false
3029 }
3030 fn message_type(&self) -> &str {
3031 "UTILMD"
3032 }
3033 fn format_version(&self) -> &str {
3034 "FV2510"
3035 }
3036 }
3037
3038 #[test]
3041 fn a_present_code_is_judged_by_its_own_status() {
3042 let code = |value: &str, status: &str| AhbCodeRule {
3044 value: value.to_string(),
3045 description: String::new(),
3046 ahb_status: status.to_string(),
3047 };
3048 let field = AhbFieldRule {
3049 segment_path: "SG4/STS/C556/9013".to_string(),
3050 ahb_status: "X [192]".to_string(),
3051 codes: vec![code("E01", "X [192]"), code("E03", "X")],
3052 ..Default::default()
3053 };
3054 assert_eq!(governing_status(&field, Some("E03")), "X");
3055 assert_eq!(governing_status(&field, Some("E01")), "X [192]");
3056 assert_eq!(governing_status(&field, None), "X [192]");
3057 assert_eq!(governing_status(&field, Some("ZZZ")), "X [192]");
3058
3059 let own = AhbFieldRule {
3061 ahb_status: "X [931]".to_string(),
3062 codes: vec![code("303", "X")],
3063 ..Default::default()
3064 };
3065 assert_eq!(governing_status(&own, Some("303")), "X [931]");
3066 assert!(!codes_are_alternatives(&own));
3067
3068 let alternatives = AhbFieldRule {
3071 ahb_status: "X [153] ∧ [538]".to_string(),
3072 codes: vec![
3073 code("Z74", "X [103] ∧ [537]"),
3074 code("Z75", "X [103] ∧ [537]"),
3075 ],
3076 ..Default::default()
3077 };
3078 assert!(codes_are_alternatives(&alternatives));
3079 assert_eq!(
3080 governing_status(&alternatives, Some("Z74")),
3081 "X [103] ∧ [537]"
3082 );
3083 assert_eq!(
3084 governing_status(&alternatives, Some("A01")),
3085 "X [153] ∧ [538]"
3086 );
3087 assert_eq!(governing_status(&alternatives, None), "X [153] ∧ [538]");
3088
3089 let restricting = AhbFieldRule {
3091 ahb_status: "X [2] ∨ [3]".to_string(),
3092 codes: vec![code("UNH", "X"), code("UNT", "X")],
3093 ..Default::default()
3094 };
3095 assert!(!codes_are_alternatives(&restricting));
3096 assert_eq!(governing_status(&restricting, Some("UNH")), "X [2] ∨ [3]");
3097 }
3098
3099 #[test]
3100 fn test_extract_segment_id_simple() {
3101 assert_eq!(extract_segment_id("NAD"), "NAD");
3102 }
3103
3104 #[test]
3105 fn test_extract_segment_id_with_sg_prefix() {
3106 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
3107 }
3108
3109 #[test]
3110 fn test_extract_segment_id_nested_sg() {
3111 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
3112 }
3113
3114 #[test]
3117 fn test_validate_missing_mandatory_field() {
3118 let evaluator = MockEvaluator::all_true(&[182, 152]);
3119 let validator = EdifactValidator::new(evaluator);
3120 let external = NoOpExternalProvider;
3121
3122 let workflow = AhbWorkflow {
3123 pruefidentifikator: "11001".to_string(),
3124 description: "Test".to_string(),
3125 communication_direction: None,
3126 fields: vec![AhbFieldRule {
3127 segment_path: "SG2/NAD/C082/3039".to_string(),
3128 name: "MP-ID des MSB".to_string(),
3129 ahb_status: "Muss [182] ∧ [152]".to_string(),
3130 codes: vec![],
3131 parent_group_ahb_status: None,
3132 segment_ahb_status: None,
3133 ..Default::default()
3134 }],
3135 ub_definitions: BTreeMap::new(),
3136 package_conditions: Default::default(),
3137 };
3138
3139 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3141
3142 assert!(!report.is_valid());
3144 let errors: Vec<_> = report.errors().collect();
3145 assert_eq!(errors.len(), 1);
3146 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3147 assert!(narrate(errors[0]).contains("MP-ID des MSB"));
3148 }
3149
3150 #[test]
3151 fn test_validate_condition_false_no_error() {
3152 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
3154 let validator = EdifactValidator::new(evaluator);
3155 let external = NoOpExternalProvider;
3156
3157 let workflow = AhbWorkflow {
3158 pruefidentifikator: "11001".to_string(),
3159 description: "Test".to_string(),
3160 communication_direction: None,
3161 fields: vec![AhbFieldRule {
3162 segment_path: "NAD".to_string(),
3163 name: "Partnerrolle".to_string(),
3164 ahb_status: "Muss [182] ∧ [152]".to_string(),
3165 codes: vec![],
3166 parent_group_ahb_status: None,
3167 segment_ahb_status: None,
3168 ..Default::default()
3169 }],
3170 ub_definitions: BTreeMap::new(),
3171 package_conditions: Default::default(),
3172 };
3173
3174 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3175
3176 assert!(report.is_valid());
3178 }
3179
3180 fn validate_absent_field(status: &str, conditions: Vec<(u32, CR)>) -> ValidationReport {
3182 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
3183 let workflow = AhbWorkflow {
3184 pruefidentifikator: "11001".to_string(),
3185 description: "Test".to_string(),
3186 communication_direction: None,
3187 fields: vec![AhbFieldRule {
3188 segment_path: "SG2/NAD/C082/3039".to_string(),
3189 name: "MP-ID".to_string(),
3190 ahb_status: status.to_string(),
3191 ..Default::default()
3192 }],
3193 ub_definitions: BTreeMap::new(),
3194 package_conditions: Default::default(),
3195 };
3196 validator.validate(
3197 &[],
3198 &workflow,
3199 &NoOpExternalProvider,
3200 ValidationLevel::Conditions,
3201 )
3202 }
3203
3204 #[test]
3205 fn a_later_soll_line_alone_does_not_require_the_field() {
3206 let report = validate_absent_field(
3208 "Muss [2119] \r\nSoll [130]",
3209 vec![(2119, CR::False), (130, CR::True)],
3210 );
3211 assert!(
3212 report.is_valid(),
3213 "{:?}",
3214 report.errors().collect::<Vec<_>>()
3215 );
3216 }
3217
3218 #[test]
3219 fn a_later_muss_line_requires_the_field() {
3220 let report = validate_absent_field(
3222 "Soll [47]\r\nMuss [46]",
3223 vec![(47, CR::False), (46, CR::True)],
3224 );
3225 let errors: Vec<_> = report.errors().collect();
3226 assert_eq!(errors.len(), 1);
3227 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3228 }
3229
3230 #[test]
3231 fn a_weaker_unknown_line_does_not_stop_a_muss() {
3232 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(2119, CR::True)]);
3234 let errors: Vec<_> = report.errors().collect();
3235 assert_eq!(errors.len(), 1);
3236 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3237 }
3238
3239 #[test]
3240 fn a_stricter_unknown_line_leaves_the_status_undecided() {
3241 let report = validate_absent_field("Muss [2119]\nSoll [130]", vec![(130, CR::True)]);
3242 assert!(report.is_valid());
3243 let infos: Vec<_> = report.infos().collect();
3244 assert_eq!(infos.len(), 1, "{infos:?}");
3245 match &infos[0].kind {
3246 IssueKind::ConditionUnknown { unresolved, .. } => {
3247 assert_eq!(unresolved.missing, vec![2119]);
3248 }
3249 other => panic!("expected ConditionUnknown, got {other:?}"),
3250 }
3251 }
3252
3253 #[test]
3254 fn test_validate_condition_unknown_adds_info() {
3255 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
3257 let validator = EdifactValidator::new(evaluator);
3259 let external = NoOpExternalProvider;
3260
3261 let workflow = AhbWorkflow {
3262 pruefidentifikator: "11001".to_string(),
3263 description: "Test".to_string(),
3264 communication_direction: None,
3265 fields: vec![AhbFieldRule {
3266 segment_path: "NAD".to_string(),
3267 name: "Partnerrolle".to_string(),
3268 ahb_status: "Muss [182] ∧ [152]".to_string(),
3269 codes: vec![],
3270 parent_group_ahb_status: None,
3271 segment_ahb_status: None,
3272 ..Default::default()
3273 }],
3274 ub_definitions: BTreeMap::new(),
3275 package_conditions: Default::default(),
3276 };
3277
3278 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3279
3280 assert!(report.is_valid());
3282 let infos: Vec<_> = report.infos().collect();
3283 assert_eq!(infos.len(), 1);
3284 assert_eq!(infos[0].code(), ErrorCodes::CONDITION_UNKNOWN);
3285 }
3286
3287 #[test]
3288 fn test_validate_structure_level_skips_conditions() {
3289 let evaluator = MockEvaluator::all_true(&[182, 152]);
3290 let validator = EdifactValidator::new(evaluator);
3291 let external = NoOpExternalProvider;
3292
3293 let workflow = AhbWorkflow {
3294 pruefidentifikator: "11001".to_string(),
3295 description: "Test".to_string(),
3296 communication_direction: None,
3297 fields: vec![AhbFieldRule {
3298 segment_path: "NAD".to_string(),
3299 name: "Partnerrolle".to_string(),
3300 ahb_status: "Muss [182] ∧ [152]".to_string(),
3301 codes: vec![],
3302 parent_group_ahb_status: None,
3303 segment_ahb_status: None,
3304 ..Default::default()
3305 }],
3306 ub_definitions: BTreeMap::new(),
3307 package_conditions: Default::default(),
3308 };
3309
3310 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
3312
3313 assert!(report.is_valid());
3315 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
3316 }
3317
3318 #[test]
3319 fn test_validate_empty_workflow_no_condition_errors() {
3320 let evaluator = MockEvaluator::all_true(&[]);
3321 let validator = EdifactValidator::new(evaluator);
3322 let external = NoOpExternalProvider;
3323
3324 let empty_workflow = AhbWorkflow {
3325 pruefidentifikator: String::new(),
3326 description: String::new(),
3327 communication_direction: None,
3328 fields: vec![],
3329 ub_definitions: BTreeMap::new(),
3330 package_conditions: Default::default(),
3331 };
3332
3333 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
3334
3335 assert!(report.is_valid());
3336 }
3337
3338 #[test]
3339 fn test_validate_bare_muss_always_required() {
3340 let evaluator = MockEvaluator::new(vec![]);
3341 let validator = EdifactValidator::new(evaluator);
3342 let external = NoOpExternalProvider;
3343
3344 let workflow = AhbWorkflow {
3345 pruefidentifikator: "55001".to_string(),
3346 description: "Test".to_string(),
3347 communication_direction: Some("NB an LF".to_string()),
3348 fields: vec![AhbFieldRule {
3349 segment_path: "SG2/NAD/3035".to_string(),
3350 name: "Partnerrolle".to_string(),
3351 ahb_status: "Muss".to_string(), codes: vec![],
3353 parent_group_ahb_status: None,
3354 segment_ahb_status: None,
3355 ..Default::default()
3356 }],
3357 ub_definitions: BTreeMap::new(),
3358 package_conditions: Default::default(),
3359 };
3360
3361 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3362
3363 assert!(!report.is_valid());
3365 assert_eq!(report.error_count(), 1);
3366 }
3367
3368 #[test]
3369 fn test_validate_x_status_is_mandatory() {
3370 let evaluator = MockEvaluator::new(vec![]);
3371 let validator = EdifactValidator::new(evaluator);
3372 let external = NoOpExternalProvider;
3373
3374 let workflow = AhbWorkflow {
3375 pruefidentifikator: "55001".to_string(),
3376 description: "Test".to_string(),
3377 communication_direction: None,
3378 fields: vec![AhbFieldRule {
3379 segment_path: "DTM".to_string(),
3380 name: "Datum".to_string(),
3381 ahb_status: "X".to_string(),
3382 codes: vec![],
3383 parent_group_ahb_status: None,
3384 segment_ahb_status: None,
3385 ..Default::default()
3386 }],
3387 ub_definitions: BTreeMap::new(),
3388 package_conditions: Default::default(),
3389 };
3390
3391 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3392
3393 assert!(!report.is_valid());
3394 let errors: Vec<_> = report.errors().collect();
3395 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
3396 }
3397
3398 #[test]
3399 fn test_validate_soll_not_mandatory() {
3400 let evaluator = MockEvaluator::new(vec![]);
3401 let validator = EdifactValidator::new(evaluator);
3402 let external = NoOpExternalProvider;
3403
3404 let workflow = AhbWorkflow {
3405 pruefidentifikator: "55001".to_string(),
3406 description: "Test".to_string(),
3407 communication_direction: None,
3408 fields: vec![AhbFieldRule {
3409 segment_path: "DTM".to_string(),
3410 name: "Datum".to_string(),
3411 ahb_status: "Soll".to_string(),
3412 codes: vec![],
3413 parent_group_ahb_status: None,
3414 segment_ahb_status: None,
3415 ..Default::default()
3416 }],
3417 ub_definitions: BTreeMap::new(),
3418 package_conditions: Default::default(),
3419 };
3420
3421 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3422
3423 assert!(report.is_valid());
3425 }
3426
3427 #[test]
3428 fn test_report_includes_metadata() {
3429 let evaluator = MockEvaluator::new(vec![]);
3430 let validator = EdifactValidator::new(evaluator);
3431 let external = NoOpExternalProvider;
3432
3433 let workflow = AhbWorkflow {
3434 pruefidentifikator: "55001".to_string(),
3435 description: String::new(),
3436 communication_direction: None,
3437 fields: vec![],
3438 ub_definitions: BTreeMap::new(),
3439 package_conditions: Default::default(),
3440 };
3441
3442 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
3443
3444 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
3445 assert_eq!(report.level, ValidationLevel::Full);
3446 assert_eq!(report.message_type, "UTILMD");
3447 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
3448 }
3449
3450 #[test]
3451 fn test_validate_with_navigator_returns_report() {
3452 let evaluator = MockEvaluator::all_true(&[]);
3453 let validator = EdifactValidator::new(evaluator);
3454 let external = NoOpExternalProvider;
3455 let nav = crate::eval::NoOpGroupNavigator;
3456
3457 let workflow = AhbWorkflow {
3458 pruefidentifikator: "55001".to_string(),
3459 description: "Test".to_string(),
3460 communication_direction: None,
3461 fields: vec![],
3462 ub_definitions: BTreeMap::new(),
3463 package_conditions: Default::default(),
3464 };
3465
3466 let report = validator.validate_with_navigator(
3467 &[],
3468 &workflow,
3469 &external,
3470 ValidationLevel::Full,
3471 &nav,
3472 );
3473 assert!(report.is_valid());
3474 }
3475
3476 #[test]
3477 fn test_code_validation_composite_paths_valid_codes() {
3478 let evaluator = MockEvaluator::new(vec![]);
3482 let validator = EdifactValidator::new(evaluator);
3483 let external = NoOpExternalProvider;
3484
3485 let unh_segment = OwnedSegment {
3486 id: "UNH".to_string(),
3487 elements: vec![
3488 vec!["ALEXANDE951842".to_string()],
3489 vec![
3490 "UTILMD".to_string(),
3491 "D".to_string(),
3492 "11A".to_string(),
3493 "UN".to_string(),
3494 "S2.1".to_string(),
3495 ],
3496 ],
3497 segment_number: 1,
3498 };
3499
3500 let workflow = AhbWorkflow {
3501 pruefidentifikator: "55001".to_string(),
3502 description: "Test".to_string(),
3503 communication_direction: None,
3504 fields: vec![
3505 AhbFieldRule {
3506 segment_path: "UNH/S009/0065".to_string(),
3507 name: "Nachrichtentyp".to_string(),
3508 ahb_status: "X".to_string(),
3509 codes: vec![AhbCodeRule {
3510 value: "UTILMD".to_string(),
3511 description: "Stammdaten".to_string(),
3512 ahb_status: "X".to_string(),
3513 }],
3514 parent_group_ahb_status: None,
3515 segment_ahb_status: None,
3516 element_index: Some(1),
3517 component_index: Some(0),
3518 ..Default::default()
3519 },
3520 AhbFieldRule {
3521 segment_path: "UNH/S009/0052".to_string(),
3522 name: "Version".to_string(),
3523 ahb_status: "X".to_string(),
3524 codes: vec![AhbCodeRule {
3525 value: "D".to_string(),
3526 description: "Draft".to_string(),
3527 ahb_status: "X".to_string(),
3528 }],
3529 parent_group_ahb_status: None,
3530 segment_ahb_status: None,
3531 element_index: Some(1),
3532 component_index: Some(1),
3533 ..Default::default()
3534 },
3535 ],
3536 ub_definitions: BTreeMap::new(),
3537 package_conditions: Default::default(),
3538 };
3539
3540 let report = validator.validate(
3541 &[unh_segment],
3542 &workflow,
3543 &external,
3544 ValidationLevel::Conditions,
3545 );
3546
3547 let code_errors: Vec<_> = report
3548 .by_category(ValidationCategory::Code)
3549 .filter(|i| i.severity == Severity::Error)
3550 .collect();
3551 assert!(
3552 code_errors.is_empty(),
3553 "Expected no code errors when composite values match allowed codes, got: {:?}",
3554 code_errors
3555 );
3556 }
3557
3558 #[test]
3559 fn test_code_validation_partitions_by_mig_number() {
3560 let evaluator = MockEvaluator::new(vec![]);
3564 let validator = EdifactValidator::new(evaluator);
3565 let external = NoOpExternalProvider;
3566
3567 let sts_7 = OwnedSegment {
3568 id: "STS".to_string(),
3569 elements: vec![
3570 vec!["7".to_string()],
3571 vec![String::new()],
3572 vec!["GH02".to_string()],
3573 vec!["ZW4".to_string()],
3574 ],
3575 segment_number: 1,
3576 };
3577 let sts_e01 = OwnedSegment {
3578 id: "STS".to_string(),
3579 elements: vec![
3580 vec!["E01".to_string()],
3581 vec![String::new()],
3582 vec!["A99".to_string(), "E_0614".to_string()],
3583 ],
3584 segment_number: 2,
3585 };
3586
3587 let workflow = AhbWorkflow {
3588 pruefidentifikator: "55018".to_string(),
3589 description: "Test".to_string(),
3590 communication_direction: None,
3591 fields: vec![
3592 AhbFieldRule {
3594 segment_path: "SG4/STS/C601/9015".to_string(),
3595 name: "Statuskategorie".to_string(),
3596 ahb_status: "X".to_string(),
3597 codes: vec![AhbCodeRule {
3598 value: "7".to_string(),
3599 description: "Transaktionsgrund".to_string(),
3600 ahb_status: "X".to_string(),
3601 }],
3602 parent_group_ahb_status: None,
3603 segment_ahb_status: None,
3604 element_index: Some(0),
3605 component_index: Some(0),
3606 mig_number: Some("00035".to_string()),
3607 format: None,
3608 },
3609 AhbFieldRule {
3610 segment_path: "SG4/STS/C556/9013".to_string(),
3611 name: "Statusanlaß".to_string(),
3612 ahb_status: "X".to_string(),
3613 codes: vec![AhbCodeRule {
3614 value: "E03".to_string(),
3615 description: "Transaktionsgrund".to_string(),
3616 ahb_status: "X".to_string(),
3617 }],
3618 parent_group_ahb_status: None,
3619 segment_ahb_status: None,
3620 element_index: Some(2),
3621 component_index: Some(0),
3622 mig_number: Some("00035".to_string()),
3623 format: None,
3624 },
3625 AhbFieldRule {
3627 segment_path: "SG4/STS/C601/9015".to_string(),
3628 name: "Statuskategorie".to_string(),
3629 ahb_status: "X".to_string(),
3630 codes: vec![AhbCodeRule {
3631 value: "E01".to_string(),
3632 description: "Antwort".to_string(),
3633 ahb_status: "X".to_string(),
3634 }],
3635 parent_group_ahb_status: None,
3636 segment_ahb_status: None,
3637 element_index: Some(0),
3638 component_index: Some(0),
3639 mig_number: Some("00036".to_string()),
3640 format: None,
3641 },
3642 ],
3643 ub_definitions: BTreeMap::new(),
3644 package_conditions: Default::default(),
3645 };
3646
3647 let report = validator.validate(
3648 &[sts_7, sts_e01],
3649 &workflow,
3650 &external,
3651 ValidationLevel::Conditions,
3652 );
3653
3654 let code_errors: Vec<_> = report
3655 .by_category(ValidationCategory::Code)
3656 .filter(|i| {
3657 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3658 })
3659 .collect();
3660 assert_eq!(
3661 code_errors.len(),
3662 1,
3663 "Expected one COD002 (for GH02 only), got: {:?}",
3664 code_errors
3665 );
3666 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3667 }
3668
3669 #[test]
3670 fn test_code_validation_composite_paths_detects_invalid_code() {
3671 let evaluator = MockEvaluator::new(vec![]);
3674 let validator = EdifactValidator::new(evaluator);
3675 let external = NoOpExternalProvider;
3676
3677 let sts_segment = OwnedSegment {
3678 id: "STS".to_string(),
3679 elements: vec![
3680 vec!["7".to_string()],
3681 vec![String::new()],
3682 vec!["GH02".to_string()],
3683 vec!["ZW4".to_string()],
3684 ],
3685 segment_number: 1,
3686 };
3687
3688 let workflow = AhbWorkflow {
3689 pruefidentifikator: "55018".to_string(),
3690 description: "Test".to_string(),
3691 communication_direction: None,
3692 fields: vec![AhbFieldRule {
3693 segment_path: "SG4/STS/C556/9013".to_string(),
3694 name: "Statusanlaß".to_string(),
3695 ahb_status: "X".to_string(),
3696 codes: vec![AhbCodeRule {
3697 value: "E03".to_string(),
3698 description: "Transaktionsgrund".to_string(),
3699 ahb_status: "X".to_string(),
3700 }],
3701 parent_group_ahb_status: None,
3702 segment_ahb_status: None,
3703 element_index: Some(2),
3704 component_index: Some(0),
3705 ..Default::default()
3706 }],
3707 ub_definitions: BTreeMap::new(),
3708 package_conditions: Default::default(),
3709 };
3710
3711 let report = validator.validate(
3712 &[sts_segment],
3713 &workflow,
3714 &external,
3715 ValidationLevel::Conditions,
3716 );
3717
3718 let code_errors: Vec<_> = report
3719 .by_category(ValidationCategory::Code)
3720 .filter(|i| {
3721 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
3722 })
3723 .collect();
3724 assert_eq!(
3725 code_errors.len(),
3726 1,
3727 "Expected COD002 for GH02, got: {:?}",
3728 code_errors
3729 );
3730 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
3731 }
3732
3733 #[test]
3734 fn test_cross_field_code_validation_valid_qualifiers() {
3735 let evaluator = MockEvaluator::new(vec![]);
3738 let validator = EdifactValidator::new(evaluator);
3739 let external = NoOpExternalProvider;
3740
3741 let nad_ms = OwnedSegment {
3742 id: "NAD".to_string(),
3743 elements: vec![vec!["MS".to_string()]],
3744 segment_number: 4,
3745 };
3746 let nad_mr = OwnedSegment {
3747 id: "NAD".to_string(),
3748 elements: vec![vec!["MR".to_string()]],
3749 segment_number: 5,
3750 };
3751
3752 let workflow = AhbWorkflow {
3753 pruefidentifikator: "55001".to_string(),
3754 description: "Test".to_string(),
3755 communication_direction: None,
3756 fields: vec![
3757 AhbFieldRule {
3758 segment_path: "SG2/NAD/3035".to_string(),
3759 name: "Absender".to_string(),
3760 ahb_status: "X".to_string(),
3761 codes: vec![AhbCodeRule {
3762 value: "MS".to_string(),
3763 description: "Absender".to_string(),
3764 ahb_status: "X".to_string(),
3765 }],
3766 parent_group_ahb_status: None,
3767 segment_ahb_status: None,
3768 ..Default::default()
3769 },
3770 AhbFieldRule {
3771 segment_path: "SG2/NAD/3035".to_string(),
3772 name: "Empfaenger".to_string(),
3773 ahb_status: "X".to_string(),
3774 codes: vec![AhbCodeRule {
3775 value: "MR".to_string(),
3776 description: "Empfaenger".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 ],
3784 ub_definitions: BTreeMap::new(),
3785 package_conditions: Default::default(),
3786 };
3787
3788 let report = validator.validate(
3789 &[nad_ms, nad_mr],
3790 &workflow,
3791 &external,
3792 ValidationLevel::Conditions,
3793 );
3794
3795 let code_errors: Vec<_> = report
3796 .by_category(ValidationCategory::Code)
3797 .filter(|i| i.severity == Severity::Error)
3798 .collect();
3799 assert!(
3800 code_errors.is_empty(),
3801 "Expected no code errors for valid qualifiers, got: {:?}",
3802 code_errors
3803 );
3804 }
3805
3806 #[test]
3807 fn test_cross_field_code_validation_catches_invalid_qualifier() {
3808 let evaluator = MockEvaluator::new(vec![]);
3810 let validator = EdifactValidator::new(evaluator);
3811 let external = NoOpExternalProvider;
3812
3813 let nad_ms = OwnedSegment {
3814 id: "NAD".to_string(),
3815 elements: vec![vec!["MS".to_string()]],
3816 segment_number: 4,
3817 };
3818 let nad_mt = OwnedSegment {
3819 id: "NAD".to_string(),
3820 elements: vec![vec!["MT".to_string()]], segment_number: 5,
3822 };
3823
3824 let workflow = AhbWorkflow {
3825 pruefidentifikator: "55001".to_string(),
3826 description: "Test".to_string(),
3827 communication_direction: None,
3828 fields: vec![
3829 AhbFieldRule {
3830 segment_path: "SG2/NAD/3035".to_string(),
3831 name: "Absender".to_string(),
3832 ahb_status: "X".to_string(),
3833 codes: vec![AhbCodeRule {
3834 value: "MS".to_string(),
3835 description: "Absender".to_string(),
3836 ahb_status: "X".to_string(),
3837 }],
3838 parent_group_ahb_status: None,
3839 segment_ahb_status: None,
3840 ..Default::default()
3841 },
3842 AhbFieldRule {
3843 segment_path: "SG2/NAD/3035".to_string(),
3844 name: "Empfaenger".to_string(),
3845 ahb_status: "X".to_string(),
3846 codes: vec![AhbCodeRule {
3847 value: "MR".to_string(),
3848 description: "Empfaenger".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 ],
3856 ub_definitions: BTreeMap::new(),
3857 package_conditions: Default::default(),
3858 };
3859
3860 let report = validator.validate(
3861 &[nad_ms, nad_mt],
3862 &workflow,
3863 &external,
3864 ValidationLevel::Conditions,
3865 );
3866
3867 let code_errors: Vec<_> = report
3868 .by_category(ValidationCategory::Code)
3869 .filter(|i| i.severity == Severity::Error)
3870 .collect();
3871 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
3872 assert!(narrate(code_errors[0]).contains("MT"));
3873 assert!(narrate(code_errors[0]).contains("MR"));
3874 assert!(narrate(code_errors[0]).contains("MS"));
3875 }
3876
3877 #[test]
3878 fn test_cross_field_code_validation_unions_across_groups() {
3879 let evaluator = MockEvaluator::new(vec![]);
3883 let validator = EdifactValidator::new(evaluator);
3884 let external = NoOpExternalProvider;
3885
3886 let segments = vec![
3887 OwnedSegment {
3888 id: "NAD".to_string(),
3889 elements: vec![vec!["MS".to_string()]],
3890 segment_number: 3,
3891 },
3892 OwnedSegment {
3893 id: "NAD".to_string(),
3894 elements: vec![vec!["MR".to_string()]],
3895 segment_number: 4,
3896 },
3897 OwnedSegment {
3898 id: "NAD".to_string(),
3899 elements: vec![vec!["Z04".to_string()]],
3900 segment_number: 20,
3901 },
3902 OwnedSegment {
3903 id: "NAD".to_string(),
3904 elements: vec![vec!["Z09".to_string()]],
3905 segment_number: 21,
3906 },
3907 OwnedSegment {
3908 id: "NAD".to_string(),
3909 elements: vec![vec!["MT".to_string()]], segment_number: 22,
3911 },
3912 ];
3913
3914 let workflow = AhbWorkflow {
3915 pruefidentifikator: "55001".to_string(),
3916 description: "Test".to_string(),
3917 communication_direction: None,
3918 fields: vec![
3919 AhbFieldRule {
3920 segment_path: "SG2/NAD/3035".to_string(),
3921 name: "Absender".to_string(),
3922 ahb_status: "X".to_string(),
3923 codes: vec![AhbCodeRule {
3924 value: "MS".to_string(),
3925 description: "Absender".to_string(),
3926 ahb_status: "X".to_string(),
3927 }],
3928 parent_group_ahb_status: None,
3929 segment_ahb_status: None,
3930 ..Default::default()
3931 },
3932 AhbFieldRule {
3933 segment_path: "SG2/NAD/3035".to_string(),
3934 name: "Empfaenger".to_string(),
3935 ahb_status: "X".to_string(),
3936 codes: vec![AhbCodeRule {
3937 value: "MR".to_string(),
3938 description: "Empfaenger".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: "SG4/SG12/NAD/3035".to_string(),
3947 name: "Anschlussnutzer".to_string(),
3948 ahb_status: "X".to_string(),
3949 codes: vec![AhbCodeRule {
3950 value: "Z04".to_string(),
3951 description: "Anschlussnutzer".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: "Korrespondenzanschrift".to_string(),
3961 ahb_status: "X".to_string(),
3962 codes: vec![AhbCodeRule {
3963 value: "Z09".to_string(),
3964 description: "Korrespondenzanschrift".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 ],
3972 ub_definitions: BTreeMap::new(),
3973 package_conditions: Default::default(),
3974 };
3975
3976 let report =
3977 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
3978
3979 let code_errors: Vec<_> = report
3980 .by_category(ValidationCategory::Code)
3981 .filter(|i| i.severity == Severity::Error)
3982 .collect();
3983 assert_eq!(
3984 code_errors.len(),
3985 1,
3986 "Expected exactly one COD002 error for MT, got: {:?}",
3987 code_errors
3988 );
3989 assert!(narrate(code_errors[0]).contains("MT"));
3990 }
3991
3992 #[test]
3993 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
3994 let evaluator = MockEvaluator::new(vec![]);
4003 let validator = EdifactValidator::new(evaluator);
4004 let external = NoOpExternalProvider;
4005
4006 let qty_67 = OwnedSegment {
4007 id: "QTY".to_string(),
4008 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
4009 segment_number: 10,
4010 };
4011
4012 let workflow = AhbWorkflow {
4013 pruefidentifikator: "13025".to_string(),
4014 description: "Test".to_string(),
4015 communication_direction: None,
4016 fields: vec![AhbFieldRule {
4017 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
4018 name: "Menge, Qualifier".to_string(),
4019 ahb_status: "X".to_string(),
4020 codes: vec![
4021 AhbCodeRule {
4022 value: "220".to_string(),
4023 description: "Wahrer Wert".to_string(),
4024 ahb_status: "X".to_string(),
4025 },
4026 AhbCodeRule {
4027 value: "67".to_string(),
4028 description: "Ersatzwert".to_string(),
4029 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
4030 },
4031 AhbCodeRule {
4032 value: "Z18".to_string(),
4033 description: "Vorläufiger Wert".to_string(),
4034 ahb_status: "X [35]".to_string(),
4035 },
4036 ],
4037 parent_group_ahb_status: None,
4038 segment_ahb_status: None,
4039 element_index: Some(0),
4040 component_index: Some(0),
4041 ..Default::default()
4042 }],
4043 ub_definitions: BTreeMap::new(),
4044 package_conditions: Default::default(),
4045 };
4046
4047 let report =
4048 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
4049
4050 let code_errors: Vec<_> = report
4051 .by_category(ValidationCategory::Code)
4052 .filter(|i| i.severity == Severity::Error)
4053 .collect();
4054 assert!(
4055 code_errors.is_empty(),
4056 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
4057 code_errors
4058 );
4059 }
4060
4061 #[test]
4062 fn test_is_qualifier_field_simple_paths() {
4063 assert!(is_qualifier_field("NAD/3035"));
4064 assert!(is_qualifier_field("SG2/NAD/3035"));
4065 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
4066 assert!(is_qualifier_field("LOC/3227"));
4067 }
4068
4069 #[test]
4070 fn test_is_qualifier_field_composite_paths() {
4071 assert!(is_qualifier_field("UNH/S009/0065"));
4075 assert!(is_qualifier_field("NAD/C082/3039"));
4076 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
4077 assert!(is_qualifier_field("SG4/STS/C556/9013"));
4078 }
4079
4080 #[test]
4081 fn test_is_qualifier_field_bare_segment() {
4082 assert!(!is_qualifier_field("NAD"));
4083 assert!(!is_qualifier_field("SG2/NAD"));
4084 }
4085
4086 #[test]
4087 fn test_is_qualifier_field_rejects_deep_paths() {
4088 assert!(!is_qualifier_field("SEG/A/B/C/D"));
4090 }
4091
4092 #[test]
4093 fn test_missing_qualifier_instance_is_detected() {
4094 let evaluator = MockEvaluator::new(vec![]);
4097 let validator = EdifactValidator::new(evaluator);
4098 let external = NoOpExternalProvider;
4099
4100 let nad_ms = OwnedSegment {
4101 id: "NAD".to_string(),
4102 elements: vec![vec!["MS".to_string()]],
4103 segment_number: 3,
4104 };
4105
4106 let workflow = AhbWorkflow {
4107 pruefidentifikator: "55001".to_string(),
4108 description: "Test".to_string(),
4109 communication_direction: None,
4110 fields: vec![
4111 AhbFieldRule {
4112 segment_path: "SG2/NAD/3035".to_string(),
4113 name: "Absender".to_string(),
4114 ahb_status: "X".to_string(),
4115 codes: vec![AhbCodeRule {
4116 value: "MS".to_string(),
4117 description: "Absender".to_string(),
4118 ahb_status: "X".to_string(),
4119 }],
4120 parent_group_ahb_status: None,
4121 segment_ahb_status: None,
4122 ..Default::default()
4123 },
4124 AhbFieldRule {
4125 segment_path: "SG2/NAD/3035".to_string(),
4126 name: "Empfaenger".to_string(),
4127 ahb_status: "Muss".to_string(),
4128 codes: vec![AhbCodeRule {
4129 value: "MR".to_string(),
4130 description: "Empfaenger".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 ],
4138 ub_definitions: BTreeMap::new(),
4139 package_conditions: Default::default(),
4140 };
4141
4142 let report =
4143 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
4144
4145 let ahb_errors: Vec<_> = report
4146 .by_category(ValidationCategory::Ahb)
4147 .filter(|i| i.severity == Severity::Error)
4148 .collect();
4149 assert_eq!(
4150 ahb_errors.len(),
4151 1,
4152 "Expected AHB001 for missing NAD+MR, got: {:?}",
4153 ahb_errors
4154 );
4155 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4156 }
4157
4158 #[test]
4159 fn test_present_qualifier_instance_no_error() {
4160 let evaluator = MockEvaluator::new(vec![]);
4162 let validator = EdifactValidator::new(evaluator);
4163 let external = NoOpExternalProvider;
4164
4165 let segments = vec![
4166 OwnedSegment {
4167 id: "NAD".to_string(),
4168 elements: vec![vec!["MS".to_string()]],
4169 segment_number: 3,
4170 },
4171 OwnedSegment {
4172 id: "NAD".to_string(),
4173 elements: vec![vec!["MR".to_string()]],
4174 segment_number: 4,
4175 },
4176 ];
4177
4178 let workflow = AhbWorkflow {
4179 pruefidentifikator: "55001".to_string(),
4180 description: "Test".to_string(),
4181 communication_direction: None,
4182 fields: vec![
4183 AhbFieldRule {
4184 segment_path: "SG2/NAD/3035".to_string(),
4185 name: "Absender".to_string(),
4186 ahb_status: "Muss".to_string(),
4187 codes: vec![AhbCodeRule {
4188 value: "MS".to_string(),
4189 description: "Absender".to_string(),
4190 ahb_status: "X".to_string(),
4191 }],
4192 parent_group_ahb_status: None,
4193 segment_ahb_status: None,
4194 ..Default::default()
4195 },
4196 AhbFieldRule {
4197 segment_path: "SG2/NAD/3035".to_string(),
4198 name: "Empfaenger".to_string(),
4199 ahb_status: "Muss".to_string(),
4200 codes: vec![AhbCodeRule {
4201 value: "MR".to_string(),
4202 description: "Empfaenger".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 ],
4210 ub_definitions: BTreeMap::new(),
4211 package_conditions: Default::default(),
4212 };
4213
4214 let report =
4215 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
4216
4217 let ahb_errors: Vec<_> = report
4218 .by_category(ValidationCategory::Ahb)
4219 .filter(|i| i.severity == Severity::Error)
4220 .collect();
4221 assert!(
4222 ahb_errors.is_empty(),
4223 "Expected no AHB001 errors, got: {:?}",
4224 ahb_errors
4225 );
4226 }
4227
4228 #[test]
4229 fn test_extract_group_path_key() {
4230 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
4231 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
4232 assert_eq!(extract_group_path_key("NAD/3035"), "");
4233 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
4234 }
4235
4236 fn validate_field_of_absent_group(
4238 group_status: &str,
4239 conditions: Vec<(u32, CR)>,
4240 ) -> ValidationReport {
4241 use mig_types::navigator::GroupNavigator;
4242
4243 struct NoSg9;
4244 impl GroupNavigator for NoSg9 {
4245 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4246 vec![]
4247 }
4248 fn find_segments_with_qualifier_in_group(
4249 &self,
4250 _: &str,
4251 _: usize,
4252 _: &str,
4253 _: &[&str],
4254 _: usize,
4255 ) -> Vec<OwnedSegment> {
4256 vec![]
4257 }
4258 fn group_instance_count(&self, _: &[&str]) -> usize {
4259 0
4260 }
4261 }
4262
4263 let validator = EdifactValidator::new(MockEvaluator::new(conditions));
4264 let workflow = AhbWorkflow {
4265 pruefidentifikator: "44001".to_string(),
4266 description: "Test".to_string(),
4267 communication_direction: None,
4268 fields: vec![AhbFieldRule {
4269 segment_path: "SG9/QTY/C186/6060".to_string(),
4270 name: "Menge".to_string(),
4271 ahb_status: "X".to_string(),
4272 parent_group_ahb_status: Some(group_status.to_string()),
4273 ..Default::default()
4274 }],
4275 ub_definitions: BTreeMap::new(),
4276 package_conditions: Default::default(),
4277 };
4278 validator.validate_with_navigator(
4279 &[],
4280 &workflow,
4281 &NoOpExternalProvider,
4282 ValidationLevel::Conditions,
4283 &NoSg9,
4284 )
4285 }
4286
4287 #[test]
4288 fn an_absent_group_whose_holding_line_is_soll_is_not_missing() {
4289 let report = validate_field_of_absent_group(
4291 "Soll [47]\r\nMuss [46]",
4292 vec![(47, CR::True), (46, CR::False)],
4293 );
4294 assert!(
4295 report.is_valid(),
4296 "{:?}",
4297 report.errors().collect::<Vec<_>>()
4298 );
4299 }
4300
4301 #[test]
4302 fn an_absent_group_whose_holding_line_is_muss_is_missing() {
4303 let report = validate_field_of_absent_group(
4304 "Soll [47]\r\nMuss [46]",
4305 vec![(47, CR::False), (46, CR::True)],
4306 );
4307 let errors: Vec<_> = report.errors().collect();
4308 assert_eq!(errors.len(), 1, "{errors:?}");
4309 assert_eq!(errors[0].code(), ErrorCodes::MISSING_REQUIRED_FIELD);
4310 }
4311
4312 #[test]
4313 fn test_absent_optional_group_no_missing_field_error() {
4314 use mig_types::navigator::GroupNavigator;
4317
4318 struct NavWithoutSG3;
4319 impl GroupNavigator for NavWithoutSG3 {
4320 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4321 vec![]
4322 }
4323 fn find_segments_with_qualifier_in_group(
4324 &self,
4325 _: &str,
4326 _: usize,
4327 _: &str,
4328 _: &[&str],
4329 _: usize,
4330 ) -> Vec<OwnedSegment> {
4331 vec![]
4332 }
4333 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4334 match group_path {
4335 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
4338 }
4339 }
4340 }
4341
4342 let evaluator = MockEvaluator::new(vec![]);
4343 let validator = EdifactValidator::new(evaluator);
4344 let external = NoOpExternalProvider;
4345 let nav = NavWithoutSG3;
4346
4347 let segments = vec![
4349 OwnedSegment {
4350 id: "NAD".into(),
4351 elements: vec![vec!["MS".into()]],
4352 segment_number: 3,
4353 },
4354 OwnedSegment {
4355 id: "NAD".into(),
4356 elements: vec![vec!["MR".into()]],
4357 segment_number: 4,
4358 },
4359 ];
4360
4361 let workflow = AhbWorkflow {
4362 pruefidentifikator: "55001".to_string(),
4363 description: "Test".to_string(),
4364 communication_direction: None,
4365 fields: vec![
4366 AhbFieldRule {
4367 segment_path: "SG2/SG3/CTA/3139".to_string(),
4368 name: "Funktion des Ansprechpartners, Code".to_string(),
4369 ahb_status: "Muss".to_string(),
4370 codes: vec![],
4371 parent_group_ahb_status: None,
4372 segment_ahb_status: None,
4373 ..Default::default()
4374 },
4375 AhbFieldRule {
4376 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
4377 name: "Name vom Ansprechpartner".to_string(),
4378 ahb_status: "X".to_string(),
4379 codes: vec![],
4380 parent_group_ahb_status: None,
4381 segment_ahb_status: None,
4382 ..Default::default()
4383 },
4384 ],
4385 ub_definitions: BTreeMap::new(),
4386 package_conditions: Default::default(),
4387 };
4388
4389 let report = validator.validate_with_navigator(
4390 &segments,
4391 &workflow,
4392 &external,
4393 ValidationLevel::Conditions,
4394 &nav,
4395 );
4396
4397 let ahb_errors: Vec<_> = report
4398 .by_category(ValidationCategory::Ahb)
4399 .filter(|i| i.severity == Severity::Error)
4400 .collect();
4401 assert!(
4402 ahb_errors.is_empty(),
4403 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
4404 ahb_errors
4405 );
4406 }
4407
4408 #[test]
4409 fn test_present_group_still_checks_mandatory_fields() {
4410 use mig_types::navigator::GroupNavigator;
4412
4413 struct NavWithSG3;
4414 impl GroupNavigator for NavWithSG3 {
4415 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
4416 vec![]
4417 }
4418 fn find_segments_with_qualifier_in_group(
4419 &self,
4420 _: &str,
4421 _: usize,
4422 _: &str,
4423 _: &[&str],
4424 _: usize,
4425 ) -> Vec<OwnedSegment> {
4426 vec![]
4427 }
4428 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4429 match group_path {
4430 ["SG2"] => 1,
4431 ["SG2", "SG3"] => 1, _ => 0,
4433 }
4434 }
4435 }
4436
4437 let evaluator = MockEvaluator::new(vec![]);
4438 let validator = EdifactValidator::new(evaluator);
4439 let external = NoOpExternalProvider;
4440 let nav = NavWithSG3;
4441
4442 let segments = vec![OwnedSegment {
4444 id: "NAD".into(),
4445 elements: vec![vec!["MS".into()]],
4446 segment_number: 3,
4447 }];
4448
4449 let workflow = AhbWorkflow {
4450 pruefidentifikator: "55001".to_string(),
4451 description: "Test".to_string(),
4452 communication_direction: None,
4453 fields: vec![AhbFieldRule {
4454 segment_path: "SG2/SG3/CTA/3139".to_string(),
4455 name: "Funktion des Ansprechpartners, Code".to_string(),
4456 ahb_status: "Muss".to_string(),
4457 codes: vec![],
4458 parent_group_ahb_status: None,
4459 segment_ahb_status: None,
4460 ..Default::default()
4461 }],
4462 ub_definitions: BTreeMap::new(),
4463 package_conditions: Default::default(),
4464 };
4465
4466 let report = validator.validate_with_navigator(
4467 &segments,
4468 &workflow,
4469 &external,
4470 ValidationLevel::Conditions,
4471 &nav,
4472 );
4473
4474 let ahb_errors: Vec<_> = report
4475 .by_category(ValidationCategory::Ahb)
4476 .filter(|i| i.severity == Severity::Error)
4477 .collect();
4478 assert_eq!(
4479 ahb_errors.len(),
4480 1,
4481 "Expected AHB001 error when SG3 is present but CTA missing"
4482 );
4483 assert!(narrate(ahb_errors[0]).contains("CTA"));
4484 }
4485
4486 #[test]
4487 fn test_missing_qualifier_with_navigator_is_detected() {
4488 use mig_types::navigator::GroupNavigator;
4491
4492 struct NavWithSG2;
4493 impl GroupNavigator for NavWithSG2 {
4494 fn find_segments_in_group(
4495 &self,
4496 segment_id: &str,
4497 group_path: &[&str],
4498 instance_index: usize,
4499 ) -> Vec<OwnedSegment> {
4500 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
4501 vec![OwnedSegment {
4502 id: "NAD".into(),
4503 elements: vec![vec!["MS".into()]],
4504 segment_number: 3,
4505 }]
4506 } else {
4507 vec![]
4508 }
4509 }
4510 fn find_segments_with_qualifier_in_group(
4511 &self,
4512 _: &str,
4513 _: usize,
4514 _: &str,
4515 _: &[&str],
4516 _: usize,
4517 ) -> Vec<OwnedSegment> {
4518 vec![]
4519 }
4520 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4521 match group_path {
4522 ["SG2"] => 1,
4523 _ => 0,
4524 }
4525 }
4526 }
4527
4528 let evaluator = MockEvaluator::new(vec![]);
4529 let validator = EdifactValidator::new(evaluator);
4530 let external = NoOpExternalProvider;
4531 let nav = NavWithSG2;
4532
4533 let segments = vec![OwnedSegment {
4534 id: "NAD".into(),
4535 elements: vec![vec!["MS".into()]],
4536 segment_number: 3,
4537 }];
4538
4539 let workflow = AhbWorkflow {
4540 pruefidentifikator: "55001".to_string(),
4541 description: "Test".to_string(),
4542 communication_direction: None,
4543 fields: vec![
4544 AhbFieldRule {
4545 segment_path: "SG2/NAD/3035".to_string(),
4546 name: "Absender".to_string(),
4547 ahb_status: "X".to_string(),
4548 codes: vec![AhbCodeRule {
4549 value: "MS".to_string(),
4550 description: "Absender".to_string(),
4551 ahb_status: "X".to_string(),
4552 }],
4553 parent_group_ahb_status: None,
4554 segment_ahb_status: None,
4555 ..Default::default()
4556 },
4557 AhbFieldRule {
4558 segment_path: "SG2/NAD/3035".to_string(),
4559 name: "Empfaenger".to_string(),
4560 ahb_status: "Muss".to_string(),
4561 codes: vec![AhbCodeRule {
4562 value: "MR".to_string(),
4563 description: "Empfaenger".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 ],
4571 ub_definitions: BTreeMap::new(),
4572 package_conditions: Default::default(),
4573 };
4574
4575 let report = validator.validate_with_navigator(
4576 &segments,
4577 &workflow,
4578 &external,
4579 ValidationLevel::Conditions,
4580 &nav,
4581 );
4582
4583 let ahb_errors: Vec<_> = report
4584 .by_category(ValidationCategory::Ahb)
4585 .filter(|i| i.severity == Severity::Error)
4586 .collect();
4587 assert_eq!(
4588 ahb_errors.len(),
4589 1,
4590 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
4591 ahb_errors
4592 );
4593 assert!(narrate(ahb_errors[0]).contains("Empfaenger"));
4594 }
4595
4596 #[test]
4597 fn test_optional_group_variant_absent_no_error() {
4598 use mig_types::navigator::GroupNavigator;
4603
4604 struct TestNav;
4605 impl GroupNavigator for TestNav {
4606 fn find_segments_in_group(
4607 &self,
4608 segment_id: &str,
4609 group_path: &[&str],
4610 instance_index: usize,
4611 ) -> Vec<OwnedSegment> {
4612 match (segment_id, group_path, instance_index) {
4613 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
4614 id: "LOC".into(),
4615 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4616 segment_number: 10,
4617 }],
4618 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
4619 id: "NAD".into(),
4620 elements: vec![vec!["MS".into()]],
4621 segment_number: 3,
4622 }],
4623 _ => vec![],
4624 }
4625 }
4626 fn find_segments_with_qualifier_in_group(
4627 &self,
4628 _: &str,
4629 _: usize,
4630 _: &str,
4631 _: &[&str],
4632 _: usize,
4633 ) -> Vec<OwnedSegment> {
4634 vec![]
4635 }
4636 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4637 match group_path {
4638 ["SG2"] => 1,
4639 ["SG4"] => 1,
4640 ["SG4", "SG5"] => 1, _ => 0,
4642 }
4643 }
4644 }
4645
4646 let evaluator = MockEvaluator::new(vec![]);
4647 let validator = EdifactValidator::new(evaluator);
4648 let external = NoOpExternalProvider;
4649 let nav = TestNav;
4650
4651 let segments = vec![
4652 OwnedSegment {
4653 id: "NAD".into(),
4654 elements: vec![vec!["MS".into()]],
4655 segment_number: 3,
4656 },
4657 OwnedSegment {
4658 id: "LOC".into(),
4659 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4660 segment_number: 10,
4661 },
4662 ];
4663
4664 let workflow = AhbWorkflow {
4665 pruefidentifikator: "55001".to_string(),
4666 description: "Test".to_string(),
4667 communication_direction: None,
4668 fields: vec![
4669 AhbFieldRule {
4671 segment_path: "SG2/NAD/3035".to_string(),
4672 name: "Absender".to_string(),
4673 ahb_status: "X".to_string(),
4674 codes: vec![AhbCodeRule {
4675 value: "MS".to_string(),
4676 description: "Absender".to_string(),
4677 ahb_status: "X".to_string(),
4678 }],
4679 parent_group_ahb_status: Some("Muss".to_string()),
4680 segment_ahb_status: None,
4681 ..Default::default()
4682 },
4683 AhbFieldRule {
4684 segment_path: "SG2/NAD/3035".to_string(),
4685 name: "Empfaenger".to_string(),
4686 ahb_status: "Muss".to_string(),
4687 codes: vec![AhbCodeRule {
4688 value: "MR".to_string(),
4689 description: "Empfaenger".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 {
4698 segment_path: "SG4/SG5/LOC/3227".to_string(),
4699 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4700 ahb_status: "X".to_string(),
4701 codes: vec![AhbCodeRule {
4702 value: "Z16".to_string(),
4703 description: "Marktlokation".to_string(),
4704 ahb_status: "X".to_string(),
4705 }],
4706 parent_group_ahb_status: Some("Kann".to_string()),
4707 segment_ahb_status: None,
4708 ..Default::default()
4709 },
4710 AhbFieldRule {
4711 segment_path: "SG4/SG5/LOC/3227".to_string(),
4712 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4713 ahb_status: "Muss".to_string(),
4714 codes: vec![AhbCodeRule {
4715 value: "Z17".to_string(),
4716 description: "Messlokation".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 ],
4724 ub_definitions: BTreeMap::new(),
4725 package_conditions: Default::default(),
4726 };
4727
4728 let report = validator.validate_with_navigator(
4729 &segments,
4730 &workflow,
4731 &external,
4732 ValidationLevel::Conditions,
4733 &nav,
4734 );
4735
4736 let ahb_errors: Vec<_> = report
4737 .by_category(ValidationCategory::Ahb)
4738 .filter(|i| i.severity == Severity::Error)
4739 .collect();
4740
4741 assert_eq!(
4744 ahb_errors.len(),
4745 1,
4746 "Expected only AHB001 for missing NAD+MR, got: {:?}",
4747 ahb_errors
4748 );
4749 assert!(
4750 narrate(ahb_errors[0]).contains("Empfaenger"),
4751 "Error should be for missing NAD+MR (Empfaenger)"
4752 );
4753 }
4754
4755 #[test]
4759 fn test_optional_group_variant_with_true_condition_absent_no_error() {
4760 use mig_types::navigator::GroupNavigator;
4761
4762 struct TestNav;
4763 impl GroupNavigator for TestNav {
4764 fn find_segments_in_group(
4765 &self,
4766 segment_id: &str,
4767 group_path: &[&str],
4768 instance_index: usize,
4769 ) -> Vec<OwnedSegment> {
4770 if segment_id == "SEQ" && group_path == ["SG4", "SG8"] && instance_index == 0 {
4771 vec![OwnedSegment {
4772 id: "SEQ".into(),
4773 elements: vec![vec!["Z03".into()]],
4774 segment_number: 10,
4775 }]
4776 } else {
4777 vec![]
4778 }
4779 }
4780 fn find_segments_with_qualifier_in_group(
4781 &self,
4782 _: &str,
4783 _: usize,
4784 _: &str,
4785 _: &[&str],
4786 _: usize,
4787 ) -> Vec<OwnedSegment> {
4788 vec![]
4789 }
4790 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4791 match group_path {
4792 ["SG4"] => 1,
4793 ["SG4", "SG8"] => 1,
4794 _ => 0,
4795 }
4796 }
4797 }
4798
4799 let evaluator = MockEvaluator::new(vec![(345, CR::True)]);
4800 let validator = EdifactValidator::new(evaluator);
4801 let segments = vec![OwnedSegment {
4802 id: "SEQ".into(),
4803 elements: vec![vec!["Z03".into()]],
4804 segment_number: 10,
4805 }];
4806 let seq = |code: &str, group: &str| AhbFieldRule {
4807 segment_path: "SG4/SG8/SEQ/1229".to_string(),
4808 name: format!("Handlung, Code ({code})"),
4809 ahb_status: "X".to_string(),
4810 codes: vec![AhbCodeRule {
4811 value: code.to_string(),
4812 description: String::new(),
4813 ahb_status: "X".to_string(),
4814 }],
4815 parent_group_ahb_status: Some(group.to_string()),
4816 segment_ahb_status: None,
4817 ..Default::default()
4818 };
4819 let workflow = AhbWorkflow {
4820 pruefidentifikator: "44143".to_string(),
4821 description: "Test".to_string(),
4822 communication_direction: None,
4823 fields: vec![seq("Z03", "Kann"), seq("Z50", "Kann [345]")],
4824 ub_definitions: BTreeMap::new(),
4825 package_conditions: Default::default(),
4826 };
4827
4828 let report = validator.validate_with_navigator(
4829 &segments,
4830 &workflow,
4831 &NoOpExternalProvider,
4832 ValidationLevel::Conditions,
4833 &TestNav,
4834 );
4835 let missing: Vec<_> = report
4836 .issues
4837 .iter()
4838 .filter(|i| matches!(i.kind, IssueKind::MissingRequiredField { .. }))
4839 .collect();
4840 assert!(missing.is_empty(), "{missing:#?}");
4841 }
4842
4843 #[test]
4844 fn test_conditional_group_variant_absent_no_error() {
4845 use mig_types::navigator::GroupNavigator;
4850
4851 struct TestNav;
4852 impl GroupNavigator for TestNav {
4853 fn find_segments_in_group(
4854 &self,
4855 segment_id: &str,
4856 group_path: &[&str],
4857 instance_index: usize,
4858 ) -> Vec<OwnedSegment> {
4859 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
4860 vec![OwnedSegment {
4861 id: "LOC".into(),
4862 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4863 segment_number: 10,
4864 }]
4865 } else {
4866 vec![]
4867 }
4868 }
4869 fn find_segments_with_qualifier_in_group(
4870 &self,
4871 _: &str,
4872 _: usize,
4873 _: &str,
4874 _: &[&str],
4875 _: usize,
4876 ) -> Vec<OwnedSegment> {
4877 vec![]
4878 }
4879 fn group_instance_count(&self, group_path: &[&str]) -> usize {
4880 match group_path {
4881 ["SG4"] => 1,
4882 ["SG4", "SG5"] => 1, _ => 0,
4884 }
4885 }
4886 }
4887
4888 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
4891 let validator = EdifactValidator::new(evaluator);
4892 let external = NoOpExternalProvider;
4893 let nav = TestNav;
4894
4895 let segments = vec![OwnedSegment {
4896 id: "LOC".into(),
4897 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
4898 segment_number: 10,
4899 }];
4900
4901 let workflow = AhbWorkflow {
4902 pruefidentifikator: "55001".to_string(),
4903 description: "Test".to_string(),
4904 communication_direction: None,
4905 fields: vec![
4906 AhbFieldRule {
4908 segment_path: "SG4/SG5/LOC/3227".to_string(),
4909 name: "Ortsangabe, Qualifier (Z16)".to_string(),
4910 ahb_status: "X".to_string(),
4911 codes: vec![AhbCodeRule {
4912 value: "Z16".to_string(),
4913 description: "Marktlokation".to_string(),
4914 ahb_status: "X".to_string(),
4915 }],
4916 parent_group_ahb_status: Some("Muss [2061]".to_string()),
4917 segment_ahb_status: None,
4918 ..Default::default()
4919 },
4920 AhbFieldRule {
4922 segment_path: "SG4/SG5/LOC/3227".to_string(),
4923 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4924 ahb_status: "X".to_string(),
4925 codes: vec![AhbCodeRule {
4926 value: "Z17".to_string(),
4927 description: "Messlokation".to_string(),
4928 ahb_status: "X".to_string(),
4929 }],
4930 parent_group_ahb_status: Some("Soll [165]".to_string()),
4931 segment_ahb_status: None,
4932 ..Default::default()
4933 },
4934 ],
4935 ub_definitions: BTreeMap::new(),
4936 package_conditions: Default::default(),
4937 };
4938
4939 let report = validator.validate_with_navigator(
4940 &segments,
4941 &workflow,
4942 &external,
4943 ValidationLevel::Conditions,
4944 &nav,
4945 );
4946
4947 let ahb_errors: Vec<_> = report
4948 .by_category(ValidationCategory::Ahb)
4949 .filter(|i| i.severity == Severity::Error)
4950 .collect();
4951
4952 assert!(
4954 ahb_errors.is_empty(),
4955 "Expected no errors when conditional group variant [165]=False, got: {:?}",
4956 ahb_errors
4957 );
4958 }
4959
4960 #[test]
4961 fn test_conditional_group_variant_unknown_no_error() {
4962 let evaluator = MockEvaluator::new(vec![]);
4968 let validator = EdifactValidator::new(evaluator);
4969 let external = NoOpExternalProvider;
4970
4971 let workflow = AhbWorkflow {
4972 pruefidentifikator: "55001".to_string(),
4973 description: "Test".to_string(),
4974 communication_direction: None,
4975 fields: vec![AhbFieldRule {
4976 segment_path: "SG4/SG5/LOC/3227".to_string(),
4977 name: "Ortsangabe, Qualifier (Z17)".to_string(),
4978 ahb_status: "X".to_string(),
4979 codes: vec![AhbCodeRule {
4980 value: "Z17".to_string(),
4981 description: "Messlokation".to_string(),
4982 ahb_status: "X".to_string(),
4983 }],
4984 parent_group_ahb_status: Some("Soll [165]".to_string()),
4985 segment_ahb_status: None,
4986 ..Default::default()
4987 }],
4988 ub_definitions: BTreeMap::new(),
4989 package_conditions: Default::default(),
4990 };
4991
4992 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
4993
4994 let ahb_errors: Vec<_> = report
4995 .by_category(ValidationCategory::Ahb)
4996 .filter(|i| i.severity == Severity::Error)
4997 .collect();
4998
4999 assert!(
5001 ahb_errors.is_empty(),
5002 "Expected no errors when parent group condition is Unknown, got: {:?}",
5003 ahb_errors
5004 );
5005 }
5006
5007 #[test]
5008 fn test_segment_absent_within_present_group_no_error() {
5009 use mig_types::navigator::GroupNavigator;
5013
5014 struct TestNav;
5015 impl GroupNavigator for TestNav {
5016 fn find_segments_in_group(
5017 &self,
5018 segment_id: &str,
5019 group_path: &[&str],
5020 instance_index: usize,
5021 ) -> Vec<OwnedSegment> {
5022 if segment_id == "QTY"
5024 && group_path == ["SG5", "SG6", "SG9", "SG10"]
5025 && instance_index == 0
5026 {
5027 vec![OwnedSegment {
5028 id: "QTY".into(),
5029 elements: vec![vec!["220".into(), "0".into()]],
5030 segment_number: 14,
5031 }]
5032 } else {
5033 vec![]
5034 }
5035 }
5036 fn find_segments_with_qualifier_in_group(
5037 &self,
5038 _: &str,
5039 _: usize,
5040 _: &str,
5041 _: &[&str],
5042 _: usize,
5043 ) -> Vec<OwnedSegment> {
5044 vec![]
5045 }
5046 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5047 match group_path {
5048 ["SG5"] => 1,
5049 ["SG5", "SG6"] => 1,
5050 ["SG5", "SG6", "SG9"] => 1,
5051 ["SG5", "SG6", "SG9", "SG10"] => 1,
5052 _ => 0,
5053 }
5054 }
5055 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
5056 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
5058 }
5059 }
5060
5061 let evaluator = MockEvaluator::all_true(&[]);
5062 let validator = EdifactValidator::new(evaluator);
5063 let external = NoOpExternalProvider;
5064 let nav = TestNav;
5065
5066 let segments = vec![OwnedSegment {
5067 id: "QTY".into(),
5068 elements: vec![vec!["220".into(), "0".into()]],
5069 segment_number: 14,
5070 }];
5071
5072 let workflow = AhbWorkflow {
5073 pruefidentifikator: "13017".to_string(),
5074 description: "Test".to_string(),
5075 communication_direction: None,
5076 fields: vec![
5077 AhbFieldRule {
5079 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
5080 name: "Statuskategorie, Code".to_string(),
5081 ahb_status: "X".to_string(),
5082 codes: vec![],
5083 parent_group_ahb_status: Some("Muss".to_string()),
5084 segment_ahb_status: None,
5085 ..Default::default()
5086 },
5087 AhbFieldRule {
5089 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5090 name: "Statusanlaß, Code".to_string(),
5091 ahb_status: "X [5]".to_string(),
5092 codes: vec![],
5093 parent_group_ahb_status: Some("Muss".to_string()),
5094 segment_ahb_status: None,
5095 ..Default::default()
5096 },
5097 ],
5098 ub_definitions: BTreeMap::new(),
5099 package_conditions: Default::default(),
5100 };
5101
5102 let report = validator.validate_with_navigator(
5103 &segments,
5104 &workflow,
5105 &external,
5106 ValidationLevel::Conditions,
5107 &nav,
5108 );
5109
5110 let ahb_errors: Vec<_> = report
5111 .by_category(ValidationCategory::Ahb)
5112 .filter(|i| i.severity == Severity::Error)
5113 .collect();
5114
5115 assert!(
5116 ahb_errors.is_empty(),
5117 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
5118 ahb_errors
5119 );
5120 }
5121
5122 #[test]
5123 fn test_group_scoped_code_validation_with_navigator() {
5124 use mig_types::navigator::GroupNavigator;
5128
5129 struct TestNav;
5130 impl GroupNavigator for TestNav {
5131 fn find_segments_in_group(
5132 &self,
5133 segment_id: &str,
5134 group_path: &[&str],
5135 _instance_index: usize,
5136 ) -> Vec<OwnedSegment> {
5137 if segment_id != "NAD" {
5138 return vec![];
5139 }
5140 match group_path {
5141 ["SG2"] => vec![
5142 OwnedSegment {
5143 id: "NAD".into(),
5144 elements: vec![vec!["MS".into()]],
5145 segment_number: 3,
5146 },
5147 OwnedSegment {
5148 id: "NAD".into(),
5149 elements: vec![vec!["MT".into()]], segment_number: 4,
5151 },
5152 ],
5153 ["SG4", "SG12"] => vec![
5154 OwnedSegment {
5155 id: "NAD".into(),
5156 elements: vec![vec!["Z04".into()]],
5157 segment_number: 20,
5158 },
5159 OwnedSegment {
5160 id: "NAD".into(),
5161 elements: vec![vec!["Z09".into()]],
5162 segment_number: 21,
5163 },
5164 ],
5165 _ => vec![],
5166 }
5167 }
5168 fn find_segments_with_qualifier_in_group(
5169 &self,
5170 _: &str,
5171 _: usize,
5172 _: &str,
5173 _: &[&str],
5174 _: usize,
5175 ) -> Vec<OwnedSegment> {
5176 vec![]
5177 }
5178 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5179 match group_path {
5180 ["SG2"] | ["SG4", "SG12"] => 1,
5181 _ => 0,
5182 }
5183 }
5184 }
5185
5186 let evaluator = MockEvaluator::new(vec![]);
5187 let validator = EdifactValidator::new(evaluator);
5188 let external = NoOpExternalProvider;
5189 let nav = TestNav;
5190
5191 let workflow = AhbWorkflow {
5192 pruefidentifikator: "55001".to_string(),
5193 description: "Test".to_string(),
5194 communication_direction: None,
5195 fields: vec![
5196 AhbFieldRule {
5197 segment_path: "SG2/NAD/3035".to_string(),
5198 name: "Absender".to_string(),
5199 ahb_status: "X".to_string(),
5200 codes: vec![AhbCodeRule {
5201 value: "MS".to_string(),
5202 description: "Absender".to_string(),
5203 ahb_status: "X".to_string(),
5204 }],
5205 parent_group_ahb_status: None,
5206 segment_ahb_status: None,
5207 ..Default::default()
5208 },
5209 AhbFieldRule {
5210 segment_path: "SG2/NAD/3035".to_string(),
5211 name: "Empfaenger".to_string(),
5212 ahb_status: "X".to_string(),
5213 codes: vec![AhbCodeRule {
5214 value: "MR".to_string(),
5215 description: "Empfaenger".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: "SG4/SG12/NAD/3035".to_string(),
5224 name: "Anschlussnutzer".to_string(),
5225 ahb_status: "X".to_string(),
5226 codes: vec![AhbCodeRule {
5227 value: "Z04".to_string(),
5228 description: "Anschlussnutzer".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: "Korrespondenzanschrift".to_string(),
5238 ahb_status: "X".to_string(),
5239 codes: vec![AhbCodeRule {
5240 value: "Z09".to_string(),
5241 description: "Korrespondenzanschrift".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 ],
5249 ub_definitions: BTreeMap::new(),
5250 package_conditions: Default::default(),
5251 };
5252
5253 let all_segments = vec![
5255 OwnedSegment {
5256 id: "NAD".into(),
5257 elements: vec![vec!["MS".into()]],
5258 segment_number: 3,
5259 },
5260 OwnedSegment {
5261 id: "NAD".into(),
5262 elements: vec![vec!["MT".into()]],
5263 segment_number: 4,
5264 },
5265 OwnedSegment {
5266 id: "NAD".into(),
5267 elements: vec![vec!["Z04".into()]],
5268 segment_number: 20,
5269 },
5270 OwnedSegment {
5271 id: "NAD".into(),
5272 elements: vec![vec!["Z09".into()]],
5273 segment_number: 21,
5274 },
5275 ];
5276
5277 let report = validator.validate_with_navigator(
5278 &all_segments,
5279 &workflow,
5280 &external,
5281 ValidationLevel::Conditions,
5282 &nav,
5283 );
5284
5285 let code_errors: Vec<_> = report
5286 .by_category(ValidationCategory::Code)
5287 .filter(|i| i.severity == Severity::Error)
5288 .collect();
5289
5290 assert_eq!(
5293 code_errors.len(),
5294 1,
5295 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
5296 code_errors
5297 );
5298 assert!(narrate(code_errors[0]).contains("MT"));
5299 assert!(narrate(code_errors[0]).contains("MR"));
5301 assert!(narrate(code_errors[0]).contains("MS"));
5302 assert!(
5303 !narrate(code_errors[0]).contains("Z04"),
5304 "SG4/SG12 codes should not leak into SG2 error"
5305 );
5306 assert!(
5308 code_errors[0]
5309 .field_path
5310 .as_deref()
5311 .unwrap_or("")
5312 .contains("SG2"),
5313 "Error field_path should reference SG2, got: {:?}",
5314 code_errors[0].field_path
5315 );
5316 }
5317
5318 #[test]
5321 fn test_package_cardinality_within_bounds() {
5322 let evaluator = MockEvaluator::all_true(&[]);
5324 let validator = EdifactValidator::new(evaluator);
5325 let external = NoOpExternalProvider;
5326
5327 let segments = vec![OwnedSegment {
5328 id: "STS".into(),
5329 elements: vec![
5330 vec!["Z33".into()], vec![], vec!["E01".into()], ],
5334 segment_number: 5,
5335 }];
5336
5337 let workflow = AhbWorkflow {
5338 pruefidentifikator: "13017".to_string(),
5339 description: "Test".to_string(),
5340 communication_direction: None,
5341 ub_definitions: BTreeMap::new(),
5342 package_conditions: Default::default(),
5343 fields: vec![AhbFieldRule {
5344 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5345 name: "Statusanlaß, Code".to_string(),
5346 ahb_status: "X".to_string(),
5347 element_index: Some(2),
5348 component_index: Some(0),
5349 codes: vec![
5350 AhbCodeRule {
5351 value: "E01".into(),
5352 description: "Code 1".into(),
5353 ahb_status: "X [4P0..1]".into(),
5354 },
5355 AhbCodeRule {
5356 value: "E02".into(),
5357 description: "Code 2".into(),
5358 ahb_status: "X [4P0..1]".into(),
5359 },
5360 ],
5361 parent_group_ahb_status: Some("Muss".to_string()),
5362 segment_ahb_status: None,
5363 mig_number: None,
5364 format: None,
5365 }],
5366 };
5367
5368 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5369 let pkg_errors: Vec<_> = report
5370 .by_category(ValidationCategory::Ahb)
5371 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5372 .collect();
5373 assert!(
5374 pkg_errors.is_empty(),
5375 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
5376 pkg_errors
5377 );
5378 }
5379
5380 #[test]
5381 fn test_package_cardinality_zero_present_min_zero() {
5382 let evaluator = MockEvaluator::all_true(&[]);
5384 let validator = EdifactValidator::new(evaluator);
5385 let external = NoOpExternalProvider;
5386
5387 let segments = vec![OwnedSegment {
5388 id: "STS".into(),
5389 elements: vec![
5390 vec!["Z33".into()],
5391 vec![],
5392 vec!["X99".into()], ],
5394 segment_number: 5,
5395 }];
5396
5397 let workflow = AhbWorkflow {
5398 pruefidentifikator: "13017".to_string(),
5399 description: "Test".to_string(),
5400 communication_direction: None,
5401 ub_definitions: BTreeMap::new(),
5402 package_conditions: Default::default(),
5403 fields: vec![AhbFieldRule {
5404 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5405 name: "Statusanlaß, Code".to_string(),
5406 ahb_status: "X".to_string(),
5407 element_index: Some(2),
5408 component_index: Some(0),
5409 codes: vec![
5410 AhbCodeRule {
5411 value: "E01".into(),
5412 description: "Code 1".into(),
5413 ahb_status: "X [4P0..1]".into(),
5414 },
5415 AhbCodeRule {
5416 value: "E02".into(),
5417 description: "Code 2".into(),
5418 ahb_status: "X [4P0..1]".into(),
5419 },
5420 ],
5421 parent_group_ahb_status: Some("Muss".to_string()),
5422 segment_ahb_status: None,
5423 mig_number: None,
5424 format: None,
5425 }],
5426 };
5427
5428 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5429 let pkg_errors: Vec<_> = report
5430 .by_category(ValidationCategory::Ahb)
5431 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5432 .collect();
5433 assert!(
5434 pkg_errors.is_empty(),
5435 "0 codes, min=0 — no error expected, got: {:?}",
5436 pkg_errors
5437 );
5438 }
5439
5440 #[test]
5441 fn test_package_cardinality_too_many() {
5442 let evaluator = MockEvaluator::all_true(&[]);
5444 let validator = EdifactValidator::new(evaluator);
5445 let external = NoOpExternalProvider;
5446
5447 let segments = vec![
5449 OwnedSegment {
5450 id: "STS".into(),
5451 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
5452 segment_number: 5,
5453 },
5454 OwnedSegment {
5455 id: "STS".into(),
5456 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
5457 segment_number: 6,
5458 },
5459 ];
5460
5461 let workflow = AhbWorkflow {
5462 pruefidentifikator: "13017".to_string(),
5463 description: "Test".to_string(),
5464 communication_direction: None,
5465 ub_definitions: BTreeMap::new(),
5466 package_conditions: Default::default(),
5467 fields: vec![AhbFieldRule {
5468 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5469 name: "Statusanlaß, Code".to_string(),
5470 ahb_status: "X".to_string(),
5471 element_index: Some(2),
5472 component_index: Some(0),
5473 codes: vec![
5474 AhbCodeRule {
5475 value: "E01".into(),
5476 description: "Code 1".into(),
5477 ahb_status: "X [4P0..1]".into(),
5478 },
5479 AhbCodeRule {
5480 value: "E02".into(),
5481 description: "Code 2".into(),
5482 ahb_status: "X [4P0..1]".into(),
5483 },
5484 ],
5485 parent_group_ahb_status: Some("Muss".to_string()),
5486 segment_ahb_status: None,
5487 mig_number: None,
5488 format: None,
5489 }],
5490 };
5491
5492 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5493 let pkg_errors: Vec<_> = report
5494 .by_category(ValidationCategory::Ahb)
5495 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5496 .collect();
5497 assert_eq!(
5498 pkg_errors.len(),
5499 1,
5500 "2 codes present, max=1 — expected 1 error, got: {:?}",
5501 pkg_errors
5502 );
5503 assert!(narrate(pkg_errors[0]).contains("[4P0..1]"));
5504 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
5505 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
5506 }
5507
5508 #[test]
5509 fn test_package_cardinality_too_few() {
5510 let evaluator = MockEvaluator::all_true(&[]);
5512 let validator = EdifactValidator::new(evaluator);
5513 let external = NoOpExternalProvider;
5514
5515 let segments = vec![OwnedSegment {
5516 id: "STS".into(),
5517 elements: vec![
5518 vec!["Z33".into()],
5519 vec![],
5520 vec!["X99".into()], ],
5522 segment_number: 5,
5523 }];
5524
5525 let workflow = AhbWorkflow {
5526 pruefidentifikator: "13017".to_string(),
5527 description: "Test".to_string(),
5528 communication_direction: None,
5529 ub_definitions: BTreeMap::new(),
5530 package_conditions: Default::default(),
5531 fields: vec![AhbFieldRule {
5532 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5533 name: "Statusanlaß, Code".to_string(),
5534 ahb_status: "X".to_string(),
5535 element_index: Some(2),
5536 component_index: Some(0),
5537 codes: vec![
5538 AhbCodeRule {
5539 value: "E01".into(),
5540 description: "Code 1".into(),
5541 ahb_status: "X [5P1..3]".into(),
5542 },
5543 AhbCodeRule {
5544 value: "E02".into(),
5545 description: "Code 2".into(),
5546 ahb_status: "X [5P1..3]".into(),
5547 },
5548 AhbCodeRule {
5549 value: "E03".into(),
5550 description: "Code 3".into(),
5551 ahb_status: "X [5P1..3]".into(),
5552 },
5553 ],
5554 parent_group_ahb_status: Some("Muss".to_string()),
5555 segment_ahb_status: None,
5556 mig_number: None,
5557 format: None,
5558 }],
5559 };
5560
5561 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5562 let pkg_errors: Vec<_> = report
5563 .by_category(ValidationCategory::Ahb)
5564 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5565 .collect();
5566 assert_eq!(
5567 pkg_errors.len(),
5568 1,
5569 "0 codes present, min=1 — expected 1 error, got: {:?}",
5570 pkg_errors
5571 );
5572 assert!(narrate(pkg_errors[0]).contains("[5P1..3]"));
5573 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
5574 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
5575 }
5576
5577 #[test]
5578 fn test_package_cardinality_no_packages_in_workflow() {
5579 let evaluator = MockEvaluator::all_true(&[]);
5581 let validator = EdifactValidator::new(evaluator);
5582 let external = NoOpExternalProvider;
5583
5584 let segments = vec![OwnedSegment {
5585 id: "STS".into(),
5586 elements: vec![vec!["E01".into()]],
5587 segment_number: 5,
5588 }];
5589
5590 let workflow = AhbWorkflow {
5591 pruefidentifikator: "13017".to_string(),
5592 description: "Test".to_string(),
5593 communication_direction: None,
5594 ub_definitions: BTreeMap::new(),
5595 package_conditions: Default::default(),
5596 fields: vec![AhbFieldRule {
5597 segment_path: "STS/9015".to_string(),
5598 name: "Status Code".to_string(),
5599 ahb_status: "X".to_string(),
5600 codes: vec![AhbCodeRule {
5601 value: "E01".into(),
5602 description: "Code 1".into(),
5603 ahb_status: "X".into(),
5604 }],
5605 parent_group_ahb_status: Some("Muss".to_string()),
5606 segment_ahb_status: None,
5607 ..Default::default()
5608 }],
5609 };
5610
5611 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5612 let pkg_errors: Vec<_> = report
5613 .by_category(ValidationCategory::Ahb)
5614 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5615 .collect();
5616 assert!(
5617 pkg_errors.is_empty(),
5618 "No packages in workflow — no errors expected"
5619 );
5620 }
5621
5622 #[test]
5623 fn test_package_cardinality_with_condition_and_package() {
5624 let evaluator = MockEvaluator::all_true(&[901]);
5626 let validator = EdifactValidator::new(evaluator);
5627 let external = NoOpExternalProvider;
5628
5629 let segments = vec![OwnedSegment {
5630 id: "STS".into(),
5631 elements: vec![vec![], vec![], vec!["E01".into()]],
5632 segment_number: 5,
5633 }];
5634
5635 let workflow = AhbWorkflow {
5636 pruefidentifikator: "13017".to_string(),
5637 description: "Test".to_string(),
5638 communication_direction: None,
5639 ub_definitions: BTreeMap::new(),
5640 package_conditions: Default::default(),
5641 fields: vec![AhbFieldRule {
5642 segment_path: "SG10/STS/C556/9013".to_string(),
5643 name: "Code".to_string(),
5644 ahb_status: "X".to_string(),
5645 element_index: Some(2),
5646 component_index: Some(0),
5647 codes: vec![
5648 AhbCodeRule {
5649 value: "E01".into(),
5650 description: "Code 1".into(),
5651 ahb_status: "X [901] [4P0..1]".into(),
5652 },
5653 AhbCodeRule {
5654 value: "E02".into(),
5655 description: "Code 2".into(),
5656 ahb_status: "X [901] [4P0..1]".into(),
5657 },
5658 ],
5659 parent_group_ahb_status: Some("Muss".to_string()),
5660 segment_ahb_status: None,
5661 mig_number: None,
5662 format: None,
5663 }],
5664 };
5665
5666 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
5667 let pkg_errors: Vec<_> = report
5668 .by_category(ValidationCategory::Ahb)
5669 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5670 .collect();
5671 assert!(
5672 pkg_errors.is_empty(),
5673 "1 code within [4P0..1] bounds — no error, got: {:?}",
5674 pkg_errors
5675 );
5676 }
5677
5678 #[test]
5679 fn test_package_binds_only_where_its_condition_holds() {
5680 let report_for = |package_condition: CR| {
5684 let validator =
5685 EdifactValidator::new(MockEvaluator::new(vec![(902, package_condition)]));
5686 let segments = vec![OwnedSegment {
5687 id: "STS".into(),
5688 elements: vec![vec![], vec![], vec!["E09".into()]],
5689 segment_number: 5,
5690 }];
5691 let workflow = AhbWorkflow {
5692 pruefidentifikator: "13017".to_string(),
5693 description: "Test".to_string(),
5694 communication_direction: None,
5695 ub_definitions: BTreeMap::new(),
5696 package_conditions: BTreeMap::from([(5, ConditionExpr::Ref(902))]),
5697 fields: vec![AhbFieldRule {
5698 segment_path: "SG10/STS/C556/9013".to_string(),
5699 name: "Code".to_string(),
5700 ahb_status: "X".to_string(),
5701 element_index: Some(2),
5702 component_index: Some(0),
5703 codes: vec![
5704 AhbCodeRule {
5705 value: "E01".into(),
5706 description: "Code 1".into(),
5707 ahb_status: "X [5P1..1]".into(),
5708 },
5709 AhbCodeRule {
5710 value: "E09".into(),
5711 description: "Code 9".into(),
5712 ahb_status: "X".into(),
5713 },
5714 ],
5715 parent_group_ahb_status: Some("Muss".to_string()),
5716 segment_ahb_status: None,
5717 mig_number: None,
5718 format: None,
5719 }],
5720 };
5721 validator
5722 .validate(
5723 &segments,
5724 &workflow,
5725 &NoOpExternalProvider,
5726 ValidationLevel::Full,
5727 )
5728 .by_category(ValidationCategory::Ahb)
5729 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5730 .count()
5731 };
5732 assert_eq!(report_for(CR::False), 0, "[902] false: [5P] does not bind");
5733 assert_eq!(report_for(CR::True), 1, "[902] true: [5P1..1] binds");
5734 }
5735
5736 #[test]
5737 fn test_package_cardinality_scoped_per_group_instance() {
5738 use mig_types::navigator::GroupNavigator;
5746
5747 struct TwoSg10s {
5748 sts_a: OwnedSegment,
5749 sts_b: OwnedSegment,
5750 }
5751 impl GroupNavigator for TwoSg10s {
5752 fn find_segments_in_group(
5753 &self,
5754 segment_id: &str,
5755 group_path: &[&str],
5756 instance_index: usize,
5757 ) -> Vec<OwnedSegment> {
5758 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
5759 match instance_index {
5760 0 => vec![self.sts_a.clone()],
5761 1 => vec![self.sts_b.clone()],
5762 _ => vec![],
5763 }
5764 } else {
5765 vec![]
5766 }
5767 }
5768 fn find_segments_with_qualifier_in_group(
5769 &self,
5770 _: &str,
5771 _: usize,
5772 _: &str,
5773 _: &[&str],
5774 _: usize,
5775 ) -> Vec<OwnedSegment> {
5776 vec![]
5777 }
5778 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5779 match group_path {
5780 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
5781 ["SG5", "SG6", "SG9", "SG10"] => 2,
5782 _ => 0,
5783 }
5784 }
5785 }
5786
5787 let sts_a = OwnedSegment {
5788 id: "STS".into(),
5789 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5790 segment_number: 10,
5791 };
5792 let sts_b = OwnedSegment {
5793 id: "STS".into(),
5794 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
5795 segment_number: 15,
5796 };
5797 let nav = TwoSg10s {
5798 sts_a: sts_a.clone(),
5799 sts_b: sts_b.clone(),
5800 };
5801
5802 let evaluator = MockEvaluator::all_true(&[]);
5803 let validator = EdifactValidator::new(evaluator);
5804 let external = NoOpExternalProvider;
5805
5806 let workflow = AhbWorkflow {
5807 pruefidentifikator: "13025".to_string(),
5808 description: "Test".to_string(),
5809 communication_direction: None,
5810 ub_definitions: BTreeMap::new(),
5811 package_conditions: Default::default(),
5812 fields: vec![AhbFieldRule {
5813 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
5814 name: "Statusanlaß, Code".to_string(),
5815 ahb_status: "X".to_string(),
5816 element_index: Some(2),
5817 component_index: Some(0),
5818 codes: vec![
5819 AhbCodeRule {
5820 value: "E01".into(),
5821 description: "Code 1".into(),
5822 ahb_status: "X [4P0..1]".into(),
5823 },
5824 AhbCodeRule {
5825 value: "E02".into(),
5826 description: "Code 2".into(),
5827 ahb_status: "X [4P0..1]".into(),
5828 },
5829 ],
5830 parent_group_ahb_status: Some("Muss".to_string()),
5831 segment_ahb_status: None,
5832 mig_number: None,
5833 format: None,
5834 }],
5835 };
5836
5837 let report = validator.validate_with_navigator(
5838 &[sts_a, sts_b],
5839 &workflow,
5840 &external,
5841 ValidationLevel::Full,
5842 &nav,
5843 );
5844 let pkg_errors: Vec<_> = report
5845 .by_category(ValidationCategory::Ahb)
5846 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5847 .collect();
5848 assert!(
5849 pkg_errors.is_empty(),
5850 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
5851 pkg_errors
5852 );
5853 }
5854
5855 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
5856 OwnedSegment {
5857 id: id.to_string(),
5858 elements: elements
5859 .into_iter()
5860 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
5861 .collect(),
5862 segment_number: 0,
5863 }
5864 }
5865
5866 #[test]
5877 fn test_package_cardinality_scoped_to_rule_mig_variant() {
5878 use mig_types::navigator::GroupNavigator;
5879
5880 struct TwoSg8Variants {
5881 seq_z01: OwnedSegment,
5882 seq_z45: OwnedSegment,
5883 }
5884 impl GroupNavigator for TwoSg8Variants {
5885 fn find_segments_in_group(
5886 &self,
5887 segment_id: &str,
5888 group_path: &[&str],
5889 instance_index: usize,
5890 ) -> Vec<OwnedSegment> {
5891 if group_path == ["SG4", "SG8"] && segment_id == "SEQ" {
5892 match instance_index {
5893 0 => vec![self.seq_z01.clone()],
5894 1 => vec![self.seq_z45.clone()],
5895 _ => vec![],
5896 }
5897 } else {
5898 vec![]
5899 }
5900 }
5901 fn find_segments_with_qualifier_in_group(
5902 &self,
5903 _: &str,
5904 _: usize,
5905 _: &str,
5906 _: &[&str],
5907 _: usize,
5908 ) -> Vec<OwnedSegment> {
5909 vec![]
5910 }
5911 fn group_instance_count(&self, group_path: &[&str]) -> usize {
5912 match group_path {
5913 ["SG4"] => 1,
5914 ["SG4", "SG8"] => 2,
5915 _ => 0,
5916 }
5917 }
5918 fn instance_has_mig_number(
5919 &self,
5920 group_path: &[&str],
5921 instance_index: usize,
5922 mig_number: &str,
5923 ) -> bool {
5924 if group_path != ["SG4", "SG8"] {
5925 return true;
5926 }
5927 match (instance_index, mig_number) {
5928 (0, "00115") => true,
5929 (0, _) => false,
5930 (1, "00171") => true,
5931 (1, _) => false,
5932 _ => false,
5933 }
5934 }
5935 }
5936
5937 let seq_z01 = OwnedSegment {
5938 id: "SEQ".into(),
5939 elements: vec![vec!["Z01".into()], vec!["1".into()]],
5940 segment_number: 10,
5941 };
5942 let seq_z45 = OwnedSegment {
5943 id: "SEQ".into(),
5944 elements: vec![vec!["Z45".into()], vec!["1".into()]],
5945 segment_number: 20,
5946 };
5947 let nav = TwoSg8Variants {
5948 seq_z01: seq_z01.clone(),
5949 seq_z45: seq_z45.clone(),
5950 };
5951
5952 let evaluator = MockEvaluator::all_true(&[]);
5953 let validator = EdifactValidator::new(evaluator);
5954 let external = NoOpExternalProvider;
5955
5956 let workflow = AhbWorkflow {
5957 pruefidentifikator: "55218".to_string(),
5958 description: "Test".to_string(),
5959 communication_direction: None,
5960 ub_definitions: BTreeMap::new(),
5961 package_conditions: Default::default(),
5962 fields: vec![AhbFieldRule {
5963 segment_path: "SG4/SG8/SEQ/1229".to_string(),
5964 name: "Handlung, Code".to_string(),
5965 ahb_status: "X".to_string(),
5966 element_index: Some(0),
5967 component_index: Some(0),
5968 codes: vec![
5969 AhbCodeRule {
5970 value: "Z45".into(),
5971 description: "NNA".into(),
5972 ahb_status: "X [1P1..4294967295]".into(),
5973 },
5974 AhbCodeRule {
5975 value: "Z84".into(),
5976 description: "Differenz-NNA".into(),
5977 ahb_status: "X [1P0..4294967295]".into(),
5978 },
5979 ],
5980 parent_group_ahb_status: Some("Muss".to_string()),
5981 segment_ahb_status: None,
5982 mig_number: Some("00171".to_string()),
5983 format: None,
5984 }],
5985 };
5986
5987 let report = validator.validate_with_navigator(
5988 &[seq_z01, seq_z45],
5989 &workflow,
5990 &external,
5991 ValidationLevel::Full,
5992 &nav,
5993 );
5994 let pkg_errors: Vec<_> = report
5995 .by_category(ValidationCategory::Ahb)
5996 .filter(|i| i.code() == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
5997 .collect();
5998 assert!(
5999 pkg_errors.is_empty(),
6000 "Package rule with mig=00171 must only count the Z45 instance (which has 1 code), not the sibling Z01 variant. Got: {:?}",
6001 pkg_errors
6002 );
6003 }
6004
6005 #[test]
6006 fn test_unt_count_correct() {
6007 let segments = vec![
6009 make_segment("UNH", vec![vec!["001"]]),
6010 make_segment("BGM", vec![vec!["E01"]]),
6011 make_segment("DTM", vec![vec!["137", "20250401"]]),
6012 make_segment("UNT", vec![vec!["4", "001"]]),
6013 ];
6014 assert!(
6015 validate_unt_segment_count(&segments).is_none(),
6016 "Correct count should produce no issue"
6017 );
6018 }
6019
6020 #[test]
6021 fn test_unt_count_mismatch() {
6022 let segments = vec![
6024 make_segment("UNH", vec![vec!["001"]]),
6025 make_segment("BGM", vec![vec!["E01"]]),
6026 make_segment("UNT", vec![vec!["5", "001"]]),
6027 ];
6028 let issue =
6029 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
6030 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
6031 assert_eq!(issue.severity, Severity::Error);
6032 assert!(narrate(&issue).contains("declared 5"));
6033 assert!(narrate(&issue).contains("actual 3"));
6034 }
6035
6036 #[test]
6037 fn test_unt_count_excludes_envelope() {
6038 let segments = vec![
6040 make_segment("UNA", vec![]),
6041 make_segment("UNB", vec![vec!["UNOC", "3"]]),
6042 make_segment("UNH", vec![vec!["001"]]),
6043 make_segment("BGM", vec![vec!["E01"]]),
6044 make_segment("UNT", vec![vec!["3", "001"]]),
6045 make_segment("UNZ", vec![vec!["1"]]),
6046 ];
6047 assert!(
6048 validate_unt_segment_count(&segments).is_none(),
6049 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
6050 );
6051 }
6052
6053 #[test]
6054 fn test_unt_count_no_unt_returns_none() {
6055 let segments = vec![
6056 make_segment("UNH", vec![vec!["001"]]),
6057 make_segment("BGM", vec![vec!["E01"]]),
6058 ];
6059 assert!(
6060 validate_unt_segment_count(&segments).is_none(),
6061 "No UNT segment should return None (not our problem)"
6062 );
6063 }
6064
6065 #[test]
6066 fn test_unt_count_rejects_multi_message_input() {
6067 let segments = vec![
6069 make_segment("UNH", vec![vec!["001"]]),
6070 make_segment("BGM", vec![vec!["E01"]]),
6071 make_segment("UNT", vec![vec!["3", "001"]]),
6072 make_segment("UNH", vec![vec!["002"]]),
6073 make_segment("BGM", vec![vec!["E02"]]),
6074 make_segment("UNT", vec![vec!["3", "002"]]),
6075 ];
6076 let issue = validate_unt_segment_count(&segments)
6077 .expect("Multi-message input should produce an error");
6078 assert_eq!(issue.code(), ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
6079 assert!(
6080 narrate(&issue).contains("2 UNH"),
6081 "Should mention UNH count: {}",
6082 narrate(&issue)
6083 );
6084 }
6085
6086 #[test]
6087 fn test_code_validation_accepts_multi_code_variant_qualifier() {
6088 let evaluator = MockEvaluator::new(vec![]);
6095 let validator = EdifactValidator::new(evaluator);
6096 let external = NoOpExternalProvider;
6097
6098 let rff_z39 = OwnedSegment {
6099 id: "RFF".to_string(),
6100 elements: vec![
6101 vec!["RFF".to_string()],
6102 vec!["Z39".to_string(), "REF1".to_string()],
6103 ],
6104 segment_number: 1,
6105 };
6106
6107 let workflow = AhbWorkflow {
6108 pruefidentifikator: "55035".to_string(),
6109 description: "Test".to_string(),
6110 communication_direction: None,
6111 fields: vec![
6112 AhbFieldRule {
6114 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6115 name: "Referenznummer Qualifier".to_string(),
6116 ahb_status: "Muss".to_string(),
6117 codes: vec![
6118 AhbCodeRule {
6119 value: "Z31".to_string(),
6120 description: "".to_string(),
6121 ahb_status: "X".to_string(),
6122 },
6123 AhbCodeRule {
6124 value: "Z39".to_string(),
6125 description: "".to_string(),
6126 ahb_status: "X".to_string(),
6127 },
6128 ],
6129 parent_group_ahb_status: None,
6130 segment_ahb_status: None,
6131 element_index: Some(1),
6132 component_index: Some(0),
6133 mig_number: Some("00075".to_string()),
6134 format: None,
6135 },
6136 AhbFieldRule {
6138 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
6139 name: "Referenznummer Qualifier".to_string(),
6140 ahb_status: "Muss".to_string(),
6141 codes: vec![AhbCodeRule {
6142 value: "Z33".to_string(),
6143 description: "".to_string(),
6144 ahb_status: "X".to_string(),
6145 }],
6146 parent_group_ahb_status: None,
6147 segment_ahb_status: None,
6148 element_index: Some(1),
6149 component_index: Some(0),
6150 mig_number: Some("00078".to_string()),
6151 format: None,
6152 },
6153 ],
6154 ub_definitions: BTreeMap::new(),
6155 package_conditions: Default::default(),
6156 };
6157
6158 let report = validator.validate(
6159 &[rff_z39],
6160 &workflow,
6161 &external,
6162 ValidationLevel::Conditions,
6163 );
6164
6165 let code_errors: Vec<_> = report
6166 .by_category(ValidationCategory::Code)
6167 .filter(|i| {
6168 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6169 })
6170 .collect();
6171 assert!(
6172 code_errors.is_empty(),
6173 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
6174 code_errors
6175 );
6176 }
6177
6178 #[test]
6179 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
6180 let evaluator = MockEvaluator::new(vec![]);
6186 let validator = EdifactValidator::new(evaluator);
6187 let external = NoOpExternalProvider;
6188
6189 let pia_5_z12 = OwnedSegment {
6193 id: "PIA".to_string(),
6194 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
6195 segment_number: 1,
6196 };
6197 let pia_5_srw = OwnedSegment {
6198 id: "PIA".to_string(),
6199 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
6200 segment_number: 2,
6201 };
6202
6203 let make_rules = |mig: &str, composite_code: &str| {
6204 vec![
6205 AhbFieldRule {
6206 segment_path: "SG4/SG8/PIA/4347".to_string(),
6207 name: "Produkt-ID-Funktion".to_string(),
6208 ahb_status: "Muss".to_string(),
6209 codes: vec![AhbCodeRule {
6210 value: "5".to_string(),
6211 description: "".to_string(),
6212 ahb_status: "X".to_string(),
6213 }],
6214 parent_group_ahb_status: None,
6215 segment_ahb_status: None,
6216 element_index: Some(0),
6217 component_index: Some(0),
6218 mig_number: Some(mig.to_string()),
6219 format: None,
6220 },
6221 AhbFieldRule {
6222 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
6223 name: "Artikel/Dienstleistung-Identifikator".to_string(),
6224 ahb_status: "Muss".to_string(),
6225 codes: vec![AhbCodeRule {
6226 value: composite_code.to_string(),
6227 description: "".to_string(),
6228 ahb_status: "X".to_string(),
6229 }],
6230 parent_group_ahb_status: None,
6231 segment_ahb_status: None,
6232 element_index: Some(1),
6233 component_index: Some(0),
6234 mig_number: Some(mig.to_string()),
6235 format: None,
6236 },
6237 ]
6238 };
6239
6240 let mut fields = make_rules("00108", "Z12");
6241 fields.extend(make_rules("00197", "SRW"));
6242
6243 let workflow = AhbWorkflow {
6244 pruefidentifikator: "55035".to_string(),
6245 description: "Test".to_string(),
6246 communication_direction: None,
6247 fields,
6248 ub_definitions: BTreeMap::new(),
6249 package_conditions: Default::default(),
6250 };
6251
6252 let report = validator.validate(
6253 &[pia_5_z12, pia_5_srw],
6254 &workflow,
6255 &external,
6256 ValidationLevel::Conditions,
6257 );
6258
6259 let code_errors: Vec<_> = report
6260 .by_category(ValidationCategory::Code)
6261 .filter(|i| {
6262 i.severity == Severity::Error && i.code() == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
6263 })
6264 .collect();
6265 assert!(
6266 code_errors.is_empty(),
6267 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
6268 code_errors
6269 );
6270 }
6271}