1use std::collections::{HashMap, HashSet};
4
5use crate::expr::{ConditionExpr, ConditionParser};
6
7use crate::eval::{
8 ConditionEvaluator, ConditionExprEvaluator, ConditionResult, EvaluationContext,
9 ExternalConditionProvider,
10};
11use mig_types::navigator::GroupNavigator;
12use mig_types::segment::OwnedSegment;
13
14use super::tree::{AhbGroupNode, AhbNode, ValidatedTree};
15
16use super::codes::ErrorCodes;
17use super::issue::{Severity, ValidationCategory, ValidationIssue};
18use super::level::ValidationLevel;
19use super::report::ValidationReport;
20
21#[derive(Debug, Clone, Default)]
26pub struct AhbFieldRule {
27 pub segment_path: String,
29
30 pub name: String,
32
33 pub ahb_status: String,
35
36 pub codes: Vec<AhbCodeRule>,
38
39 pub parent_group_ahb_status: Option<String>,
44
45 pub element_index: Option<usize>,
48
49 pub component_index: Option<usize>,
52
53 pub mig_number: Option<String>,
56}
57
58#[derive(Debug, Clone, Default)]
60pub struct AhbCodeRule {
61 pub value: String,
63
64 pub description: String,
66
67 pub ahb_status: String,
69}
70
71#[derive(Debug, Clone)]
73pub struct AhbWorkflow {
74 pub pruefidentifikator: String,
76
77 pub description: String,
79
80 pub communication_direction: Option<String>,
82
83 pub fields: Vec<AhbFieldRule>,
85
86 pub ub_definitions: HashMap<String, ConditionExpr>,
92}
93
94pub struct EdifactValidator<E: ConditionEvaluator> {
127 evaluator: E,
128}
129
130impl<E: ConditionEvaluator> EdifactValidator<E> {
131 pub fn new(evaluator: E) -> Self {
133 Self { evaluator }
134 }
135
136 pub fn validate(
149 &self,
150 segments: &[OwnedSegment],
151 workflow: &AhbWorkflow,
152 external: &dyn ExternalConditionProvider,
153 level: ValidationLevel,
154 ) -> ValidationReport {
155 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
156 .with_format_version(self.evaluator.format_version())
157 .with_pruefidentifikator(&workflow.pruefidentifikator);
158
159 let ctx = EvaluationContext::new(&workflow.pruefidentifikator, external, segments);
160
161 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
162 self.validate_conditions(workflow, &ctx, &mut report);
163 }
164
165 report
166 }
167
168 pub fn validate_with_navigator(
174 &self,
175 segments: &[OwnedSegment],
176 workflow: &AhbWorkflow,
177 external: &dyn ExternalConditionProvider,
178 level: ValidationLevel,
179 navigator: &dyn GroupNavigator,
180 ) -> ValidationReport {
181 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
182 .with_format_version(self.evaluator.format_version())
183 .with_pruefidentifikator(&workflow.pruefidentifikator);
184
185 let ctx = EvaluationContext::with_navigator(
186 &workflow.pruefidentifikator,
187 external,
188 segments,
189 navigator,
190 );
191
192 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
193 self.validate_conditions(workflow, &ctx, &mut report);
194 }
195
196 report
197 }
198
199 pub fn validate_tree(
206 &self,
207 validated_tree: &ValidatedTree,
208 segments: &[OwnedSegment],
209 external: &dyn ExternalConditionProvider,
210 level: ValidationLevel,
211 navigator: Option<&dyn GroupNavigator>,
212 ) -> ValidationReport {
213 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
214 .with_format_version(self.evaluator.format_version())
215 .with_pruefidentifikator(validated_tree.pruefidentifikator);
216
217 if !matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
218 return report;
219 }
220
221 let ctx = match navigator {
222 Some(nav) => EvaluationContext::with_navigator(
223 validated_tree.pruefidentifikator,
224 external,
225 segments,
226 nav,
227 ),
228 None => EvaluationContext::new(validated_tree.pruefidentifikator, external, segments),
229 };
230
231 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
232
233 let mut all_nodes: Vec<&AhbNode> = Vec::new();
235 all_nodes.extend(validated_tree.root_fields.iter());
236 for group in &validated_tree.groups {
237 collect_nodes_depth_first(group, &mut all_nodes);
238 }
239
240 let mut tag_migs: HashMap<String, HashSet<&str>> = HashMap::new();
247 for node in &all_nodes {
248 if let Some(ref m) = node.rule.mig_number {
249 tag_migs
250 .entry(extract_segment_id(&node.rule.segment_path))
251 .or_default()
252 .insert(m.as_str());
253 }
254 }
255 for rule in &validated_tree.unmatched_rules {
256 if let Some(ref m) = rule.mig_number {
257 tag_migs
258 .entry(extract_segment_id(&rule.segment_path))
259 .or_default()
260 .insert(m.as_str());
261 }
262 }
263
264 for node in &all_nodes {
266 let field = node.rule;
267
268 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
270
271 if should_skip_for_parent_group(field, &expr_eval, &ctx, validated_tree.ub_definitions)
273 {
274 continue;
275 }
276
277 let (condition_result, unknown_ids) = expr_eval.evaluate_status_detailed_with_ub(
280 &field.ahb_status,
281 &node_ctx,
282 validated_tree.ub_definitions,
283 );
284
285 match condition_result {
286 ConditionResult::True => {
287 if is_mandatory_status(&field.ahb_status) && node.value.is_none() {
296 let tag = extract_segment_id(&field.segment_path);
297 let single_variant_present = node.segment_elements.is_none()
298 && tag_migs.get(&tag).is_some_and(|ms| ms.len() == 1)
299 && is_field_present(&ctx, field);
300 if !single_variant_present {
301 let mut issue = ValidationIssue::new(
302 Severity::Error,
303 ValidationCategory::Ahb,
304 ErrorCodes::MISSING_REQUIRED_FIELD,
305 format!(
306 "Required field '{}' at {} is missing",
307 field.name, field.segment_path
308 ),
309 )
310 .with_field_path(&field.segment_path)
311 .with_rule(&field.ahb_status);
312 if let Some(first_code) = field.codes.first() {
313 issue.expected_value = Some(first_code.value.clone());
314 }
315 report.add_issue(issue);
316 }
317 }
318 }
319 ConditionResult::False => {
320 if is_mandatory_status(&field.ahb_status) && node.value.is_some() {
323 report.add_issue(
324 ValidationIssue::new(
325 Severity::Error,
326 ValidationCategory::Ahb,
327 ErrorCodes::CONDITIONAL_RULE_VIOLATION,
328 format!(
329 "Field '{}' at {} is present but does not satisfy condition: {}",
330 field.name, field.segment_path, field.ahb_status
331 ),
332 )
333 .with_field_path(&field.segment_path)
334 .with_rule(&field.ahb_status),
335 );
336 }
337 }
338 ConditionResult::Unknown => {
339 let mut external_ids = Vec::new();
341 let mut undetermined_ids = Vec::new();
342 let mut missing_ids = Vec::new();
343 for id in unknown_ids {
344 if self.evaluator.is_external(id) {
345 external_ids.push(id);
346 } else if self.evaluator.is_known(id) {
347 undetermined_ids.push(id);
348 } else {
349 missing_ids.push(id);
350 }
351 }
352
353 let mut parts = Vec::new();
354 if !external_ids.is_empty() {
355 let ids: Vec<String> =
356 external_ids.iter().map(|id| format!("[{id}]")).collect();
357 parts.push(format!(
358 "external conditions require provider: {}",
359 ids.join(", ")
360 ));
361 }
362 if !undetermined_ids.is_empty() {
363 let ids: Vec<String> = undetermined_ids
364 .iter()
365 .map(|id| format!("[{id}]"))
366 .collect();
367 parts.push(format!(
368 "conditions could not be determined from message data: {}",
369 ids.join(", ")
370 ));
371 }
372 if !missing_ids.is_empty() {
373 let ids: Vec<String> =
374 missing_ids.iter().map(|id| format!("[{id}]")).collect();
375 parts.push(format!("missing conditions: {}", ids.join(", ")));
376 }
377 let detail = if parts.is_empty() {
378 String::new()
379 } else {
380 format!(" ({})", parts.join("; "))
381 };
382 report.add_issue(
383 ValidationIssue::new(
384 Severity::Info,
385 ValidationCategory::Ahb,
386 ErrorCodes::CONDITION_UNKNOWN,
387 format!(
388 "Condition for field '{}' could not be fully evaluated{}",
389 field.name, detail
390 ),
391 )
392 .with_field_path(&field.segment_path)
393 .with_rule(&field.ahb_status),
394 );
395 }
396 }
397 }
398
399 for field in &validated_tree.unmatched_rules {
405 if should_skip_for_parent_group(field, &expr_eval, &ctx, validated_tree.ub_definitions)
406 {
407 continue;
408 }
409
410 let (condition_result, _) = expr_eval.evaluate_status_detailed_with_ub(
411 &field.ahb_status,
412 &ctx,
413 validated_tree.ub_definitions,
414 );
415
416 if matches!(condition_result, ConditionResult::True)
417 && is_mandatory_status(&field.ahb_status)
418 && !is_field_present(&ctx, field)
419 && !is_group_variant_absent(&ctx, field)
420 {
421 let mut issue = ValidationIssue::new(
422 Severity::Error,
423 ValidationCategory::Ahb,
424 ErrorCodes::MISSING_REQUIRED_FIELD,
425 format!(
426 "Required field '{}' at {} is missing",
427 field.name, field.segment_path
428 ),
429 )
430 .with_field_path(&field.segment_path)
431 .with_rule(&field.ahb_status);
432 if let Some(first_code) = field.codes.first() {
433 issue.expected_value = Some(first_code.value.clone());
434 }
435 report.add_issue(issue);
436 }
437 }
438
439 if matches!(level, ValidationLevel::Full) {
443 let mut all_fields: Vec<AhbFieldRule> = Vec::new();
444 for node in &all_nodes {
445 all_fields.push(node.rule.clone());
446 }
447 for rule in &validated_tree.unmatched_rules {
448 all_fields.push((*rule).clone());
449 }
450 let synthetic_workflow = AhbWorkflow {
451 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
452 description: String::new(),
453 communication_direction: None,
454 fields: all_fields,
455 ub_definitions: validated_tree.ub_definitions.clone(),
456 };
457 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
458 self.validate_package_cardinality(&synthetic_workflow, &ctx, &mut report);
459 }
460
461 report
462 }
463
464 fn validate_conditions(
466 &self,
467 workflow: &AhbWorkflow,
468 ctx: &EvaluationContext,
469 report: &mut ValidationReport,
470 ) {
471 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
472
473 for field in &workflow.fields {
474 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
476 continue;
477 }
478
479 let (condition_result, unknown_ids) = expr_eval.evaluate_status_detailed_with_ub(
482 &field.ahb_status,
483 ctx,
484 &workflow.ub_definitions,
485 );
486
487 match condition_result {
488 ConditionResult::True => {
489 if is_mandatory_status(&field.ahb_status)
491 && !is_field_present(ctx, field)
492 && !is_group_variant_absent(ctx, field)
493 {
494 let mut issue = ValidationIssue::new(
495 Severity::Error,
496 ValidationCategory::Ahb,
497 ErrorCodes::MISSING_REQUIRED_FIELD,
498 format!(
499 "Required field '{}' at {} is missing",
500 field.name, field.segment_path
501 ),
502 )
503 .with_field_path(&field.segment_path)
504 .with_rule(&field.ahb_status);
505 if let Some(first_code) = field.codes.first() {
509 issue.expected_value = Some(first_code.value.clone());
510 }
511 report.add_issue(issue);
512 }
513 }
514 ConditionResult::False => {
515 if is_mandatory_status(&field.ahb_status) && is_field_present(ctx, field) {
521 report.add_issue(
522 ValidationIssue::new(
523 Severity::Error,
524 ValidationCategory::Ahb,
525 ErrorCodes::CONDITIONAL_RULE_VIOLATION,
526 format!(
527 "Field '{}' at {} is present but does not satisfy condition: {}",
528 field.name, field.segment_path, field.ahb_status
529 ),
530 )
531 .with_field_path(&field.segment_path)
532 .with_rule(&field.ahb_status),
533 );
534 }
535 }
536 ConditionResult::Unknown => {
537 let mut external_ids = Vec::new();
542 let mut undetermined_ids = Vec::new();
543 let mut missing_ids = Vec::new();
544 for id in unknown_ids {
545 if self.evaluator.is_external(id) {
546 external_ids.push(id);
547 } else if self.evaluator.is_known(id) {
548 undetermined_ids.push(id);
549 } else {
550 missing_ids.push(id);
551 }
552 }
553
554 let mut parts = Vec::new();
555 if !external_ids.is_empty() {
556 let ids: Vec<String> =
557 external_ids.iter().map(|id| format!("[{id}]")).collect();
558 parts.push(format!(
559 "external conditions require provider: {}",
560 ids.join(", ")
561 ));
562 }
563 if !undetermined_ids.is_empty() {
564 let ids: Vec<String> = undetermined_ids
565 .iter()
566 .map(|id| format!("[{id}]"))
567 .collect();
568 parts.push(format!(
569 "conditions could not be determined from message data: {}",
570 ids.join(", ")
571 ));
572 }
573 if !missing_ids.is_empty() {
574 let ids: Vec<String> =
575 missing_ids.iter().map(|id| format!("[{id}]")).collect();
576 parts.push(format!("missing conditions: {}", ids.join(", ")));
577 }
578 let detail = if parts.is_empty() {
579 String::new()
580 } else {
581 format!(" ({})", parts.join("; "))
582 };
583 report.add_issue(
584 ValidationIssue::new(
585 Severity::Info,
586 ValidationCategory::Ahb,
587 ErrorCodes::CONDITION_UNKNOWN,
588 format!(
589 "Condition for field '{}' could not be fully evaluated{}",
590 field.name, detail
591 ),
592 )
593 .with_field_path(&field.segment_path)
594 .with_rule(&field.ahb_status),
595 );
596 }
597 }
598 }
599
600 self.validate_codes_cross_field(workflow, ctx, report);
605
606 self.validate_package_cardinality(workflow, ctx, report);
609 }
610
611 fn validate_package_cardinality(
618 &self,
619 workflow: &AhbWorkflow,
620 ctx: &EvaluationContext,
621 report: &mut ValidationReport,
622 ) {
623 struct PackageGroup {
626 min: u32,
627 max: u32,
628 code_values: Vec<String>,
629 element_index: usize,
630 component_index: usize,
631 }
632
633 let mut groups: HashMap<(String, Option<String>, u32), PackageGroup> = HashMap::new();
642
643 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
644
645 for field in &workflow.fields {
646 let el_idx = field.element_index.unwrap_or(0);
647 let comp_idx = field.component_index.unwrap_or(0);
648
649 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
651 continue;
652 }
653
654 for code in &field.codes {
655 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
657 let mut packages = Vec::new();
659 collect_packages(&expr, &mut packages);
660
661 for (pkg_id, pkg_min, pkg_max) in packages {
662 let key = (
663 field.segment_path.clone(),
664 field.mig_number.clone(),
665 pkg_id,
666 );
667 let group = groups.entry(key).or_insert_with(|| PackageGroup {
668 min: pkg_min,
669 max: pkg_max,
670 code_values: Vec::new(),
671 element_index: el_idx,
672 component_index: comp_idx,
673 });
674 group.min = group.min.max(pkg_min);
677 group.max = group.max.min(pkg_max);
678 group.code_values.push(code.value.clone());
679 }
680 }
681 }
682 }
683
684 for ((seg_path, _mig_number, pkg_id), group) in &groups {
692 let segment_id = extract_segment_id(seg_path);
693
694 let mut unique_codes: Vec<&str> =
698 group.code_values.iter().map(|s| s.as_str()).collect();
699 unique_codes.sort_unstable();
700 unique_codes.dedup();
701 let code_set: HashSet<&str> = unique_codes.iter().copied().collect();
702
703 let group_path_str = extract_group_path_key(seg_path);
704 let group_path: Vec<&str> = if group_path_str.is_empty() {
705 Vec::new()
706 } else {
707 group_path_str.split('/').collect()
708 };
709
710 let min = group.min as usize;
711 let max = group.max as usize;
712
713 let per_instance_counts: Option<Vec<usize>> = match (ctx.navigator, group_path.is_empty()) {
714 (Some(nav), false) => {
715 let instance_count = nav.group_instance_count(&group_path);
716 if instance_count == 0 {
717 None
718 } else {
719 Some(
720 (0..instance_count)
721 .map(|i| {
722 nav.find_segments_in_group(&segment_id, &group_path, i)
723 .iter()
724 .filter_map(|seg| {
725 seg.elements
726 .get(group.element_index)
727 .and_then(|e| e.get(group.component_index))
728 .filter(|v| !v.is_empty())
729 .cloned()
730 })
731 .filter(|v| code_set.contains(v.as_str()))
732 .count()
733 })
734 .collect(),
735 )
736 }
737 }
738 _ => None,
739 };
740
741 let counts: Vec<usize> = per_instance_counts.unwrap_or_else(|| {
742 let segments = ctx.find_segments(&segment_id);
743 let count = segments
744 .iter()
745 .filter_map(|seg| {
746 seg.elements
747 .get(group.element_index)
748 .and_then(|e| e.get(group.component_index))
749 .filter(|v| !v.is_empty())
750 .map(|s| s.as_str())
751 })
752 .filter(|v| code_set.contains(v))
753 .count();
754 vec![count]
755 });
756
757 let mut reported_counts: HashSet<usize> = HashSet::new();
761 for present_count in counts {
762 if (present_count < min || present_count > max)
763 && reported_counts.insert(present_count)
764 {
765 let code_list = unique_codes.join(", ");
766 report.add_issue(
767 ValidationIssue::new(
768 Severity::Error,
769 ValidationCategory::Ahb,
770 ErrorCodes::PACKAGE_CARDINALITY_VIOLATION,
771 format!(
772 "Package [{}P{}..{}] at {}: {} code(s) present (allowed {}..{}). Codes in package: [{}]",
773 pkg_id, group.min, group.max, seg_path, present_count, group.min, group.max, code_list
774 ),
775 )
776 .with_field_path(seg_path)
777 .with_expected(format!("{}..{}", group.min, group.max))
778 .with_actual(present_count.to_string()),
779 );
780 }
781 }
782 }
783 }
784
785 fn validate_codes_cross_field(
797 &self,
798 workflow: &AhbWorkflow,
799 ctx: &EvaluationContext,
800 report: &mut ValidationReport,
801 ) {
802 if ctx.navigator.is_some() {
803 self.validate_codes_group_scoped(workflow, ctx, report);
804 } else {
805 self.validate_codes_tag_scoped(workflow, ctx, report);
806 }
807 }
808
809 fn validate_codes_group_scoped(
812 &self,
813 workflow: &AhbWorkflow,
814 ctx: &EvaluationContext,
815 report: &mut ValidationReport,
816 ) {
817 let by_loc = partition_codes_by_mig(workflow);
818 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
819 let nav = ctx.navigator.unwrap();
820
821 for ((group_key, tag), migs) in &by_loc {
822 let field_path = if group_key.is_empty() {
823 format!("{tag}/qualifier")
824 } else {
825 format!("{group_key}/{tag}/qualifier")
826 };
827
828 let group_path: Vec<&str> = if group_key.is_empty() {
829 Vec::new()
830 } else {
831 group_key.split('/').collect()
832 };
833
834 let tag_qualifiers = known_qualifiers.get(tag);
835
836 if group_path.is_empty() {
837 Self::validate_segments_per_mig(
838 &ctx.find_segments(tag),
839 migs,
840 tag_qualifiers,
841 tag,
842 &field_path,
843 report,
844 );
845 } else {
846 let instance_count = nav.group_instance_count(&group_path);
847 for i in 0..instance_count {
848 let owned = nav.find_segments_in_group(tag, &group_path, i);
849 let refs: Vec<&OwnedSegment> = owned.iter().collect();
850 Self::validate_segments_per_mig(
851 &refs,
852 migs,
853 tag_qualifiers,
854 tag,
855 &field_path,
856 report,
857 );
858 }
859 }
860 }
861 }
862
863 fn validate_codes_tag_scoped(
867 &self,
868 workflow: &AhbWorkflow,
869 ctx: &EvaluationContext,
870 report: &mut ValidationReport,
871 ) {
872 let by_loc = partition_codes_by_mig(workflow);
873 let known_qualifiers = global_qualifiers_by_tag(&by_loc);
874 let mut by_tag: HashMap<String, HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
876 for ((_group_key, tag), migs) in by_loc {
877 let merged = by_tag.entry(tag).or_default();
878 for (mig_key, bucket) in migs {
879 let entry = merged.entry(mig_key).or_default();
880 if entry.qualifier_position.is_none() {
881 entry.qualifier_position = bucket.qualifier_position;
882 }
883 if entry.qualifier_position == bucket.qualifier_position {
884 entry.qualifier_values.extend(&bucket.qualifier_values);
885 }
886 for (pos, codes) in bucket.codes {
887 entry.codes.entry(pos).or_default().extend(codes);
888 }
889 }
890 }
891
892 for (tag, migs) in &by_tag {
893 let field_path = format!("{tag}/qualifier");
894 Self::validate_segments_per_mig(
895 &ctx.find_segments(tag),
896 migs,
897 known_qualifiers.get(tag),
898 tag,
899 &field_path,
900 report,
901 );
902 }
903 }
904
905 fn validate_segments_per_mig(
914 segments: &[&OwnedSegment],
915 migs: &HashMap<Option<String>, MigCodeBucket>,
916 tag_qualifiers: Option<&HashMap<(usize, usize), HashSet<String>>>,
917 tag: &str,
918 field_path: &str,
919 report: &mut ValidationReport,
920 ) {
921 for seg in segments {
922 match match_segment_to_mig(seg, migs) {
923 Some(bucket) => {
924 for ((el, c), allowed) in &bucket.codes {
925 if allowed.is_empty() {
926 continue;
927 }
928 Self::check_segments_against_codes(
929 vec![*seg],
930 allowed,
931 tag,
932 *el,
933 *c,
934 field_path,
935 report,
936 );
937 }
938 }
939 None => {
940 let Some(qualifiers) = tag_qualifiers else {
941 continue;
942 };
943 for ((el, c), allowed) in qualifiers {
948 let Some(actual) = seg
949 .elements
950 .get(*el)
951 .and_then(|e| e.get(*c))
952 .filter(|v| !v.is_empty())
953 .map(|s| s.as_str())
954 else {
955 continue;
956 };
957 if allowed.iter().any(|v| v == actual) {
958 break;
960 }
961 let bucket_values: HashSet<&str> = migs
964 .values()
965 .filter(|b| b.qualifier_position == Some((*el, *c)))
966 .flat_map(|b| b.qualifier_values.iter().copied())
967 .collect();
968 if !bucket_values.is_empty() {
969 Self::check_segments_against_codes(
970 vec![*seg],
971 &bucket_values,
972 tag,
973 *el,
974 *c,
975 field_path,
976 report,
977 );
978 break;
979 }
980 }
981 }
982 }
983 }
984 }
985
986 fn check_segments_against_codes(
988 segments: Vec<&OwnedSegment>,
989 allowed_codes: &HashSet<&str>,
990 _tag: &str,
991 el_idx: usize,
992 comp_idx: usize,
993 field_path: &str,
994 report: &mut ValidationReport,
995 ) {
996 for segment in segments {
997 if let Some(code_value) = segment
998 .elements
999 .get(el_idx)
1000 .and_then(|e| e.get(comp_idx))
1001 .filter(|v| !v.is_empty())
1002 {
1003 if !allowed_codes.contains(code_value.as_str()) {
1004 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
1005 sorted_codes.sort_unstable();
1006 report.add_issue(
1007 ValidationIssue::new(
1008 Severity::Error,
1009 ValidationCategory::Code,
1010 ErrorCodes::CODE_NOT_ALLOWED_FOR_PID,
1011 format!(
1012 "Code '{}' is not allowed for this PID. Allowed: [{}]",
1013 code_value,
1014 sorted_codes.join(", ")
1015 ),
1016 )
1017 .with_field_path(field_path)
1018 .with_actual(code_value)
1019 .with_expected(sorted_codes.join(", ")),
1020 );
1021 }
1022 }
1023 }
1024 }
1025}
1026
1027fn should_skip_for_parent_group<E: ConditionEvaluator>(
1033 field: &AhbFieldRule,
1034 expr_eval: &ConditionExprEvaluator<E>,
1035 ctx: &EvaluationContext,
1036 ub_definitions: &HashMap<String, ConditionExpr>,
1037) -> bool {
1038 if let Some(ref group_status) = field.parent_group_ahb_status {
1039 if group_status.contains('[') {
1040 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
1041 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
1042 }
1043 }
1044 false
1045}
1046
1047fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1056 let segment_id = extract_segment_id(&field.segment_path);
1057
1058 if !field.codes.is_empty() {
1064 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
1065 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1066 let matching = ctx.find_segments(&segment_id);
1067 return matching.iter().any(|seg| {
1068 seg.elements
1069 .get(el_idx)
1070 .and_then(|e| e.get(comp_idx))
1071 .is_some_and(|v| required_codes.contains(&v.as_str()))
1072 });
1073 }
1074 if is_qualifier_field(&field.segment_path) {
1077 let required_codes: Vec<&str> = field.codes.iter().map(|c| c.value.as_str()).collect();
1078 let el_idx = field.element_index.unwrap_or(0);
1079 let comp_idx = field.component_index.unwrap_or(0);
1080 let matching = ctx.find_segments(&segment_id);
1081 return matching.iter().any(|seg| {
1082 seg.elements
1083 .get(el_idx)
1084 .and_then(|e| e.get(comp_idx))
1085 .is_some_and(|v| required_codes.contains(&v.as_str()))
1086 });
1087 }
1088 }
1089
1090 ctx.has_segment(&segment_id)
1091}
1092
1093fn is_group_variant_absent(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
1108 let group_path: Vec<&str> = field
1109 .segment_path
1110 .split('/')
1111 .take_while(|p| p.starts_with("SG"))
1112 .collect();
1113
1114 if group_path.is_empty() {
1115 return false;
1116 }
1117
1118 let nav = match ctx.navigator {
1119 Some(nav) => nav,
1120 None => return false,
1121 };
1122
1123 let instance_count = nav.group_instance_count(&group_path);
1124
1125 if instance_count == 0 {
1130 let is_group_mandatory = field
1131 .parent_group_ahb_status
1132 .as_deref()
1133 .is_some_and(is_mandatory_status);
1134 if !is_group_mandatory {
1135 return true;
1136 }
1137 return false;
1139 }
1140
1141 if let Some(ref group_status) = field.parent_group_ahb_status {
1145 if !is_mandatory_status(group_status) && !group_status.contains('[') {
1146 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
1149 let segment_id = extract_segment_id(&field.segment_path);
1150 let required_codes: Vec<&str> =
1151 field.codes.iter().map(|c| c.value.as_str()).collect();
1152
1153 let any_instance_has_qualifier = (0..instance_count).any(|i| {
1154 nav.find_segments_in_group(&segment_id, &group_path, i)
1155 .iter()
1156 .any(|seg| {
1157 seg.elements
1158 .first()
1159 .and_then(|e| e.first())
1160 .is_some_and(|v| required_codes.contains(&v.as_str()))
1161 })
1162 });
1163
1164 if !any_instance_has_qualifier {
1165 return true; }
1167 }
1168 }
1169 }
1170
1171 let segment_id = extract_segment_id(&field.segment_path);
1180 let segment_absent_from_all = (0..instance_count).all(|i| {
1181 nav.find_segments_in_group(&segment_id, &group_path, i)
1182 .is_empty()
1183 });
1184 if segment_absent_from_all {
1185 let group_has_other_segments =
1186 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1187 if group_has_other_segments {
1188 return true;
1189 }
1190 }
1191
1192 false
1193}
1194
1195fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1197 out.extend(group.fields.iter());
1198 for child in &group.children {
1199 collect_nodes_depth_first(child, out);
1200 }
1201}
1202
1203fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
1205 match expr {
1206 ConditionExpr::Package { id, min, max } => {
1207 out.push((*id, *min, *max));
1208 }
1209 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1210 for e in exprs {
1211 collect_packages(e, out);
1212 }
1213 }
1214 ConditionExpr::Xor(left, right) => {
1215 collect_packages(left, out);
1216 collect_packages(right, out);
1217 }
1218 ConditionExpr::Not(inner) => {
1219 collect_packages(inner, out);
1220 }
1221 ConditionExpr::Ref(_) => {}
1222 }
1223}
1224
1225fn is_mandatory_status(status: &str) -> bool {
1227 let trimmed = status.trim();
1228 trimmed.starts_with("Muss") || trimmed.starts_with('X')
1229}
1230
1231fn is_qualifier_field(path: &str) -> bool {
1241 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
1242 matches!(parts.len(), 2 | 3)
1244}
1245
1246#[derive(Default)]
1254struct MigCodeBucket<'a> {
1255 qualifier_position: Option<(usize, usize)>,
1258 qualifier_values: HashSet<&'a str>,
1261 codes: HashMap<(usize, usize), HashSet<&'a str>>,
1264}
1265
1266fn partition_codes_by_mig(
1271 workflow: &AhbWorkflow,
1272) -> HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>> {
1273 let mut out: HashMap<(String, String), HashMap<Option<String>, MigCodeBucket>> = HashMap::new();
1274 for field in &workflow.fields {
1275 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
1276 continue;
1277 }
1278 let tag = extract_segment_id(&field.segment_path);
1279 let group_key = extract_group_path_key(&field.segment_path);
1280 let mig = field.mig_number.clone();
1281 let el = field.element_index.unwrap_or(0);
1282 let c = field.component_index.unwrap_or(0);
1283
1284 let bucket = out
1285 .entry((group_key, tag))
1286 .or_default()
1287 .entry(mig)
1288 .or_default();
1289
1290 let required: Vec<&str> = field
1297 .codes
1298 .iter()
1299 .filter(|code| {
1300 code.ahb_status.starts_with('X') || code.ahb_status.starts_with("Muss")
1301 })
1302 .map(|code| code.value.as_str())
1303 .collect();
1304
1305 if !required.is_empty() {
1312 if bucket.qualifier_position.is_none() {
1313 bucket.qualifier_position = Some((el, c));
1314 }
1315 if bucket.qualifier_position == Some((el, c)) {
1316 bucket.qualifier_values.extend(required.iter().copied());
1317 }
1318 }
1319
1320 for v in required {
1321 bucket.codes.entry((el, c)).or_default().insert(v);
1322 }
1323 }
1324 out
1325}
1326
1327fn global_qualifiers_by_tag(
1333 by_loc: &HashMap<(String, String), HashMap<Option<String>, MigCodeBucket<'_>>>,
1334) -> HashMap<String, HashMap<(usize, usize), HashSet<String>>> {
1335 let mut out: HashMap<String, HashMap<(usize, usize), HashSet<String>>> = HashMap::new();
1336 for ((_group, tag), migs) in by_loc {
1337 let tag_entry = out.entry(tag.clone()).or_default();
1338 for bucket in migs.values() {
1339 if let Some(pos) = bucket.qualifier_position {
1340 tag_entry
1341 .entry(pos)
1342 .or_default()
1343 .extend(bucket.qualifier_values.iter().map(|s| s.to_string()));
1344 }
1345 }
1346 }
1347 out
1348}
1349
1350fn match_segment_to_mig<'a, 'b>(
1361 seg: &OwnedSegment,
1362 migs: &'a HashMap<Option<String>, MigCodeBucket<'b>>,
1363) -> Option<&'a MigCodeBucket<'b>> {
1364 let actual_at = |el: usize, c: usize| -> &str {
1365 seg.elements
1366 .get(el)
1367 .and_then(|e| e.get(c))
1368 .map(|s| s.as_str())
1369 .unwrap_or("")
1370 };
1371
1372 let mut best: Option<&MigCodeBucket> = None;
1373 let mut best_matches = 0usize;
1374
1375 for bucket in migs.values() {
1376 let Some((el, c)) = bucket.qualifier_position else {
1377 continue;
1378 };
1379 if !bucket.qualifier_values.contains(actual_at(el, c)) {
1380 continue;
1381 }
1382 let extra_matches = bucket
1383 .codes
1384 .iter()
1385 .filter(|(pos, _)| **pos != (el, c))
1386 .filter(|((e, k), allowed)| {
1387 let v = actual_at(*e, *k);
1388 !v.is_empty() && allowed.contains(v)
1389 })
1390 .count();
1391 if best.is_none() || extra_matches > best_matches {
1392 best = Some(bucket);
1393 best_matches = extra_matches;
1394 }
1395 }
1396 best
1397}
1398
1399fn extract_group_path_key(path: &str) -> String {
1404 let sg_parts: Vec<&str> = path
1405 .split('/')
1406 .take_while(|p| p.starts_with("SG"))
1407 .collect();
1408 sg_parts.join("/")
1409}
1410
1411fn extract_segment_id(path: &str) -> String {
1413 for part in path.split('/') {
1414 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
1416 continue;
1417 }
1418 if part.len() >= 3
1420 && part
1421 .chars()
1422 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
1423 {
1424 return part.to_string();
1425 }
1426 }
1427 path.split('/').next_back().unwrap_or(path).to_string()
1429}
1430
1431pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
1443 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
1445 if unh_count > 1 {
1446 return Some(ValidationIssue::new(
1447 Severity::Error,
1448 ValidationCategory::Structure,
1449 ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH,
1450 format!("UNT validation requires per-message segments, found {unh_count} UNH segments"),
1451 ));
1452 }
1453
1454 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
1456 let declared: usize = unt.get_element(0).parse().ok()?;
1457
1458 let actual = segments
1460 .iter()
1461 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
1462 .count();
1463
1464 if declared != actual {
1465 Some(
1466 ValidationIssue::new(
1467 Severity::Error,
1468 ValidationCategory::Structure,
1469 ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH,
1470 format!("UNT segment count mismatch: declared {declared}, actual {actual}"),
1471 )
1472 .with_field_path("UNT/0074")
1473 .with_expected(actual.to_string())
1474 .with_actual(declared.to_string()),
1475 )
1476 } else {
1477 None
1478 }
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483 use super::*;
1484 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
1485 use std::collections::HashMap;
1486
1487 struct MockEvaluator {
1489 results: HashMap<u32, CR>,
1490 }
1491
1492 impl MockEvaluator {
1493 fn new(results: Vec<(u32, CR)>) -> Self {
1494 Self {
1495 results: results.into_iter().collect(),
1496 }
1497 }
1498
1499 fn all_true(ids: &[u32]) -> Self {
1500 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
1501 }
1502 }
1503
1504 impl ConditionEvaluator for MockEvaluator {
1505 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
1506 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
1507 }
1508 fn is_external(&self, _condition: u32) -> bool {
1509 false
1510 }
1511 fn message_type(&self) -> &str {
1512 "UTILMD"
1513 }
1514 fn format_version(&self) -> &str {
1515 "FV2510"
1516 }
1517 }
1518
1519 #[test]
1522 fn test_is_mandatory_status() {
1523 assert!(is_mandatory_status("Muss"));
1524 assert!(is_mandatory_status("Muss [182] ∧ [152]"));
1525 assert!(is_mandatory_status("X"));
1526 assert!(is_mandatory_status("X [567]"));
1527 assert!(!is_mandatory_status("Soll [1]"));
1528 assert!(!is_mandatory_status("Kann [1]"));
1529 assert!(!is_mandatory_status(""));
1530 }
1531
1532 #[test]
1533 fn test_extract_segment_id_simple() {
1534 assert_eq!(extract_segment_id("NAD"), "NAD");
1535 }
1536
1537 #[test]
1538 fn test_extract_segment_id_with_sg_prefix() {
1539 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
1540 }
1541
1542 #[test]
1543 fn test_extract_segment_id_nested_sg() {
1544 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
1545 }
1546
1547 #[test]
1550 fn test_validate_missing_mandatory_field() {
1551 let evaluator = MockEvaluator::all_true(&[182, 152]);
1552 let validator = EdifactValidator::new(evaluator);
1553 let external = NoOpExternalProvider;
1554
1555 let workflow = AhbWorkflow {
1556 pruefidentifikator: "11001".to_string(),
1557 description: "Test".to_string(),
1558 communication_direction: None,
1559 fields: vec![AhbFieldRule {
1560 segment_path: "SG2/NAD/C082/3039".to_string(),
1561 name: "MP-ID des MSB".to_string(),
1562 ahb_status: "Muss [182] ∧ [152]".to_string(),
1563 codes: vec![],
1564 parent_group_ahb_status: None,
1565 ..Default::default()
1566 }],
1567 ub_definitions: HashMap::new(),
1568 };
1569
1570 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1572
1573 assert!(!report.is_valid());
1575 let errors: Vec<_> = report.errors().collect();
1576 assert_eq!(errors.len(), 1);
1577 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1578 assert!(errors[0].message.contains("MP-ID des MSB"));
1579 }
1580
1581 #[test]
1582 fn test_validate_condition_false_no_error() {
1583 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
1585 let validator = EdifactValidator::new(evaluator);
1586 let external = NoOpExternalProvider;
1587
1588 let workflow = AhbWorkflow {
1589 pruefidentifikator: "11001".to_string(),
1590 description: "Test".to_string(),
1591 communication_direction: None,
1592 fields: vec![AhbFieldRule {
1593 segment_path: "NAD".to_string(),
1594 name: "Partnerrolle".to_string(),
1595 ahb_status: "Muss [182] ∧ [152]".to_string(),
1596 codes: vec![],
1597 parent_group_ahb_status: None,
1598 ..Default::default()
1599 }],
1600 ub_definitions: HashMap::new(),
1601 };
1602
1603 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1604
1605 assert!(report.is_valid());
1607 }
1608
1609 #[test]
1610 fn test_validate_condition_unknown_adds_info() {
1611 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
1613 let validator = EdifactValidator::new(evaluator);
1615 let external = NoOpExternalProvider;
1616
1617 let workflow = AhbWorkflow {
1618 pruefidentifikator: "11001".to_string(),
1619 description: "Test".to_string(),
1620 communication_direction: None,
1621 fields: vec![AhbFieldRule {
1622 segment_path: "NAD".to_string(),
1623 name: "Partnerrolle".to_string(),
1624 ahb_status: "Muss [182] ∧ [152]".to_string(),
1625 codes: vec![],
1626 parent_group_ahb_status: None,
1627 ..Default::default()
1628 }],
1629 ub_definitions: HashMap::new(),
1630 };
1631
1632 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1633
1634 assert!(report.is_valid());
1636 let infos: Vec<_> = report.infos().collect();
1637 assert_eq!(infos.len(), 1);
1638 assert_eq!(infos[0].code, ErrorCodes::CONDITION_UNKNOWN);
1639 }
1640
1641 #[test]
1642 fn test_validate_structure_level_skips_conditions() {
1643 let evaluator = MockEvaluator::all_true(&[182, 152]);
1644 let validator = EdifactValidator::new(evaluator);
1645 let external = NoOpExternalProvider;
1646
1647 let workflow = AhbWorkflow {
1648 pruefidentifikator: "11001".to_string(),
1649 description: "Test".to_string(),
1650 communication_direction: None,
1651 fields: vec![AhbFieldRule {
1652 segment_path: "NAD".to_string(),
1653 name: "Partnerrolle".to_string(),
1654 ahb_status: "Muss [182] ∧ [152]".to_string(),
1655 codes: vec![],
1656 parent_group_ahb_status: None,
1657 ..Default::default()
1658 }],
1659 ub_definitions: HashMap::new(),
1660 };
1661
1662 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
1664
1665 assert!(report.is_valid());
1667 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
1668 }
1669
1670 #[test]
1671 fn test_validate_empty_workflow_no_condition_errors() {
1672 let evaluator = MockEvaluator::all_true(&[]);
1673 let validator = EdifactValidator::new(evaluator);
1674 let external = NoOpExternalProvider;
1675
1676 let empty_workflow = AhbWorkflow {
1677 pruefidentifikator: String::new(),
1678 description: String::new(),
1679 communication_direction: None,
1680 fields: vec![],
1681 ub_definitions: HashMap::new(),
1682 };
1683
1684 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
1685
1686 assert!(report.is_valid());
1687 }
1688
1689 #[test]
1690 fn test_validate_bare_muss_always_required() {
1691 let evaluator = MockEvaluator::new(vec![]);
1692 let validator = EdifactValidator::new(evaluator);
1693 let external = NoOpExternalProvider;
1694
1695 let workflow = AhbWorkflow {
1696 pruefidentifikator: "55001".to_string(),
1697 description: "Test".to_string(),
1698 communication_direction: Some("NB an LF".to_string()),
1699 fields: vec![AhbFieldRule {
1700 segment_path: "SG2/NAD/3035".to_string(),
1701 name: "Partnerrolle".to_string(),
1702 ahb_status: "Muss".to_string(), codes: vec![],
1704 parent_group_ahb_status: None,
1705 ..Default::default()
1706 }],
1707 ub_definitions: HashMap::new(),
1708 };
1709
1710 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1711
1712 assert!(!report.is_valid());
1714 assert_eq!(report.error_count(), 1);
1715 }
1716
1717 #[test]
1718 fn test_validate_x_status_is_mandatory() {
1719 let evaluator = MockEvaluator::new(vec![]);
1720 let validator = EdifactValidator::new(evaluator);
1721 let external = NoOpExternalProvider;
1722
1723 let workflow = AhbWorkflow {
1724 pruefidentifikator: "55001".to_string(),
1725 description: "Test".to_string(),
1726 communication_direction: None,
1727 fields: vec![AhbFieldRule {
1728 segment_path: "DTM".to_string(),
1729 name: "Datum".to_string(),
1730 ahb_status: "X".to_string(),
1731 codes: vec![],
1732 parent_group_ahb_status: None,
1733 ..Default::default()
1734 }],
1735 ub_definitions: HashMap::new(),
1736 };
1737
1738 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1739
1740 assert!(!report.is_valid());
1741 let errors: Vec<_> = report.errors().collect();
1742 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1743 }
1744
1745 #[test]
1746 fn test_validate_soll_not_mandatory() {
1747 let evaluator = MockEvaluator::new(vec![]);
1748 let validator = EdifactValidator::new(evaluator);
1749 let external = NoOpExternalProvider;
1750
1751 let workflow = AhbWorkflow {
1752 pruefidentifikator: "55001".to_string(),
1753 description: "Test".to_string(),
1754 communication_direction: None,
1755 fields: vec![AhbFieldRule {
1756 segment_path: "DTM".to_string(),
1757 name: "Datum".to_string(),
1758 ahb_status: "Soll".to_string(),
1759 codes: vec![],
1760 parent_group_ahb_status: None,
1761 ..Default::default()
1762 }],
1763 ub_definitions: HashMap::new(),
1764 };
1765
1766 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1767
1768 assert!(report.is_valid());
1770 }
1771
1772 #[test]
1773 fn test_report_includes_metadata() {
1774 let evaluator = MockEvaluator::new(vec![]);
1775 let validator = EdifactValidator::new(evaluator);
1776 let external = NoOpExternalProvider;
1777
1778 let workflow = AhbWorkflow {
1779 pruefidentifikator: "55001".to_string(),
1780 description: String::new(),
1781 communication_direction: None,
1782 fields: vec![],
1783 ub_definitions: HashMap::new(),
1784 };
1785
1786 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
1787
1788 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
1789 assert_eq!(report.level, ValidationLevel::Full);
1790 assert_eq!(report.message_type, "UTILMD");
1791 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
1792 }
1793
1794 #[test]
1795 fn test_validate_with_navigator_returns_report() {
1796 let evaluator = MockEvaluator::all_true(&[]);
1797 let validator = EdifactValidator::new(evaluator);
1798 let external = NoOpExternalProvider;
1799 let nav = crate::eval::NoOpGroupNavigator;
1800
1801 let workflow = AhbWorkflow {
1802 pruefidentifikator: "55001".to_string(),
1803 description: "Test".to_string(),
1804 communication_direction: None,
1805 fields: vec![],
1806 ub_definitions: HashMap::new(),
1807 };
1808
1809 let report = validator.validate_with_navigator(
1810 &[],
1811 &workflow,
1812 &external,
1813 ValidationLevel::Full,
1814 &nav,
1815 );
1816 assert!(report.is_valid());
1817 }
1818
1819 #[test]
1820 fn test_code_validation_composite_paths_valid_codes() {
1821 let evaluator = MockEvaluator::new(vec![]);
1825 let validator = EdifactValidator::new(evaluator);
1826 let external = NoOpExternalProvider;
1827
1828 let unh_segment = OwnedSegment {
1829 id: "UNH".to_string(),
1830 elements: vec![
1831 vec!["ALEXANDE951842".to_string()],
1832 vec![
1833 "UTILMD".to_string(),
1834 "D".to_string(),
1835 "11A".to_string(),
1836 "UN".to_string(),
1837 "S2.1".to_string(),
1838 ],
1839 ],
1840 segment_number: 1,
1841 };
1842
1843 let workflow = AhbWorkflow {
1844 pruefidentifikator: "55001".to_string(),
1845 description: "Test".to_string(),
1846 communication_direction: None,
1847 fields: vec![
1848 AhbFieldRule {
1849 segment_path: "UNH/S009/0065".to_string(),
1850 name: "Nachrichtentyp".to_string(),
1851 ahb_status: "X".to_string(),
1852 codes: vec![AhbCodeRule {
1853 value: "UTILMD".to_string(),
1854 description: "Stammdaten".to_string(),
1855 ahb_status: "X".to_string(),
1856 }],
1857 parent_group_ahb_status: None,
1858 element_index: Some(1),
1859 component_index: Some(0),
1860 ..Default::default()
1861 },
1862 AhbFieldRule {
1863 segment_path: "UNH/S009/0052".to_string(),
1864 name: "Version".to_string(),
1865 ahb_status: "X".to_string(),
1866 codes: vec![AhbCodeRule {
1867 value: "D".to_string(),
1868 description: "Draft".to_string(),
1869 ahb_status: "X".to_string(),
1870 }],
1871 parent_group_ahb_status: None,
1872 element_index: Some(1),
1873 component_index: Some(1),
1874 ..Default::default()
1875 },
1876 ],
1877 ub_definitions: HashMap::new(),
1878 };
1879
1880 let report = validator.validate(
1881 &[unh_segment],
1882 &workflow,
1883 &external,
1884 ValidationLevel::Conditions,
1885 );
1886
1887 let code_errors: Vec<_> = report
1888 .by_category(ValidationCategory::Code)
1889 .filter(|i| i.severity == Severity::Error)
1890 .collect();
1891 assert!(
1892 code_errors.is_empty(),
1893 "Expected no code errors when composite values match allowed codes, got: {:?}",
1894 code_errors
1895 );
1896 }
1897
1898 #[test]
1899 fn test_code_validation_partitions_by_mig_number() {
1900 let evaluator = MockEvaluator::new(vec![]);
1904 let validator = EdifactValidator::new(evaluator);
1905 let external = NoOpExternalProvider;
1906
1907 let sts_7 = OwnedSegment {
1908 id: "STS".to_string(),
1909 elements: vec![
1910 vec!["7".to_string()],
1911 vec![String::new()],
1912 vec!["GH02".to_string()],
1913 vec!["ZW4".to_string()],
1914 ],
1915 segment_number: 1,
1916 };
1917 let sts_e01 = OwnedSegment {
1918 id: "STS".to_string(),
1919 elements: vec![
1920 vec!["E01".to_string()],
1921 vec![String::new()],
1922 vec!["A99".to_string(), "E_0614".to_string()],
1923 ],
1924 segment_number: 2,
1925 };
1926
1927 let workflow = AhbWorkflow {
1928 pruefidentifikator: "55018".to_string(),
1929 description: "Test".to_string(),
1930 communication_direction: None,
1931 fields: vec![
1932 AhbFieldRule {
1934 segment_path: "SG4/STS/C601/9015".to_string(),
1935 name: "Statuskategorie".to_string(),
1936 ahb_status: "X".to_string(),
1937 codes: vec![AhbCodeRule {
1938 value: "7".to_string(),
1939 description: "Transaktionsgrund".to_string(),
1940 ahb_status: "X".to_string(),
1941 }],
1942 parent_group_ahb_status: None,
1943 element_index: Some(0),
1944 component_index: Some(0),
1945 mig_number: Some("00035".to_string()),
1946 },
1947 AhbFieldRule {
1948 segment_path: "SG4/STS/C556/9013".to_string(),
1949 name: "Statusanlaß".to_string(),
1950 ahb_status: "X".to_string(),
1951 codes: vec![AhbCodeRule {
1952 value: "E03".to_string(),
1953 description: "Transaktionsgrund".to_string(),
1954 ahb_status: "X".to_string(),
1955 }],
1956 parent_group_ahb_status: None,
1957 element_index: Some(2),
1958 component_index: Some(0),
1959 mig_number: Some("00035".to_string()),
1960 },
1961 AhbFieldRule {
1963 segment_path: "SG4/STS/C601/9015".to_string(),
1964 name: "Statuskategorie".to_string(),
1965 ahb_status: "X".to_string(),
1966 codes: vec![AhbCodeRule {
1967 value: "E01".to_string(),
1968 description: "Antwort".to_string(),
1969 ahb_status: "X".to_string(),
1970 }],
1971 parent_group_ahb_status: None,
1972 element_index: Some(0),
1973 component_index: Some(0),
1974 mig_number: Some("00036".to_string()),
1975 },
1976 ],
1977 ub_definitions: HashMap::new(),
1978 };
1979
1980 let report = validator.validate(
1981 &[sts_7, sts_e01],
1982 &workflow,
1983 &external,
1984 ValidationLevel::Conditions,
1985 );
1986
1987 let code_errors: Vec<_> = report
1988 .by_category(ValidationCategory::Code)
1989 .filter(|i| {
1990 i.severity == Severity::Error && i.code == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
1991 })
1992 .collect();
1993 assert_eq!(
1994 code_errors.len(),
1995 1,
1996 "Expected one COD002 (for GH02 only), got: {:?}",
1997 code_errors
1998 );
1999 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2000 }
2001
2002 #[test]
2003 fn test_code_validation_composite_paths_detects_invalid_code() {
2004 let evaluator = MockEvaluator::new(vec![]);
2007 let validator = EdifactValidator::new(evaluator);
2008 let external = NoOpExternalProvider;
2009
2010 let sts_segment = OwnedSegment {
2011 id: "STS".to_string(),
2012 elements: vec![
2013 vec!["7".to_string()],
2014 vec![String::new()],
2015 vec!["GH02".to_string()],
2016 vec!["ZW4".to_string()],
2017 ],
2018 segment_number: 1,
2019 };
2020
2021 let workflow = AhbWorkflow {
2022 pruefidentifikator: "55018".to_string(),
2023 description: "Test".to_string(),
2024 communication_direction: None,
2025 fields: vec![AhbFieldRule {
2026 segment_path: "SG4/STS/C556/9013".to_string(),
2027 name: "Statusanlaß".to_string(),
2028 ahb_status: "X".to_string(),
2029 codes: vec![AhbCodeRule {
2030 value: "E03".to_string(),
2031 description: "Transaktionsgrund".to_string(),
2032 ahb_status: "X".to_string(),
2033 }],
2034 parent_group_ahb_status: None,
2035 element_index: Some(2),
2036 component_index: Some(0),
2037 ..Default::default()
2038 }],
2039 ub_definitions: HashMap::new(),
2040 };
2041
2042 let report = validator.validate(
2043 &[sts_segment],
2044 &workflow,
2045 &external,
2046 ValidationLevel::Conditions,
2047 );
2048
2049 let code_errors: Vec<_> = report
2050 .by_category(ValidationCategory::Code)
2051 .filter(|i| {
2052 i.severity == Severity::Error && i.code == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
2053 })
2054 .collect();
2055 assert_eq!(
2056 code_errors.len(),
2057 1,
2058 "Expected COD002 for GH02, got: {:?}",
2059 code_errors
2060 );
2061 assert_eq!(code_errors[0].actual_value.as_deref(), Some("GH02"));
2062 }
2063
2064 #[test]
2065 fn test_cross_field_code_validation_valid_qualifiers() {
2066 let evaluator = MockEvaluator::new(vec![]);
2069 let validator = EdifactValidator::new(evaluator);
2070 let external = NoOpExternalProvider;
2071
2072 let nad_ms = OwnedSegment {
2073 id: "NAD".to_string(),
2074 elements: vec![vec!["MS".to_string()]],
2075 segment_number: 4,
2076 };
2077 let nad_mr = OwnedSegment {
2078 id: "NAD".to_string(),
2079 elements: vec![vec!["MR".to_string()]],
2080 segment_number: 5,
2081 };
2082
2083 let workflow = AhbWorkflow {
2084 pruefidentifikator: "55001".to_string(),
2085 description: "Test".to_string(),
2086 communication_direction: None,
2087 fields: vec![
2088 AhbFieldRule {
2089 segment_path: "SG2/NAD/3035".to_string(),
2090 name: "Absender".to_string(),
2091 ahb_status: "X".to_string(),
2092 codes: vec![AhbCodeRule {
2093 value: "MS".to_string(),
2094 description: "Absender".to_string(),
2095 ahb_status: "X".to_string(),
2096 }],
2097 parent_group_ahb_status: None,
2098 ..Default::default()
2099 },
2100 AhbFieldRule {
2101 segment_path: "SG2/NAD/3035".to_string(),
2102 name: "Empfaenger".to_string(),
2103 ahb_status: "X".to_string(),
2104 codes: vec![AhbCodeRule {
2105 value: "MR".to_string(),
2106 description: "Empfaenger".to_string(),
2107 ahb_status: "X".to_string(),
2108 }],
2109 parent_group_ahb_status: None,
2110 ..Default::default()
2111 },
2112 ],
2113 ub_definitions: HashMap::new(),
2114 };
2115
2116 let report = validator.validate(
2117 &[nad_ms, nad_mr],
2118 &workflow,
2119 &external,
2120 ValidationLevel::Conditions,
2121 );
2122
2123 let code_errors: Vec<_> = report
2124 .by_category(ValidationCategory::Code)
2125 .filter(|i| i.severity == Severity::Error)
2126 .collect();
2127 assert!(
2128 code_errors.is_empty(),
2129 "Expected no code errors for valid qualifiers, got: {:?}",
2130 code_errors
2131 );
2132 }
2133
2134 #[test]
2135 fn test_cross_field_code_validation_catches_invalid_qualifier() {
2136 let evaluator = MockEvaluator::new(vec![]);
2138 let validator = EdifactValidator::new(evaluator);
2139 let external = NoOpExternalProvider;
2140
2141 let nad_ms = OwnedSegment {
2142 id: "NAD".to_string(),
2143 elements: vec![vec!["MS".to_string()]],
2144 segment_number: 4,
2145 };
2146 let nad_mt = OwnedSegment {
2147 id: "NAD".to_string(),
2148 elements: vec![vec!["MT".to_string()]], segment_number: 5,
2150 };
2151
2152 let workflow = AhbWorkflow {
2153 pruefidentifikator: "55001".to_string(),
2154 description: "Test".to_string(),
2155 communication_direction: None,
2156 fields: vec![
2157 AhbFieldRule {
2158 segment_path: "SG2/NAD/3035".to_string(),
2159 name: "Absender".to_string(),
2160 ahb_status: "X".to_string(),
2161 codes: vec![AhbCodeRule {
2162 value: "MS".to_string(),
2163 description: "Absender".to_string(),
2164 ahb_status: "X".to_string(),
2165 }],
2166 parent_group_ahb_status: None,
2167 ..Default::default()
2168 },
2169 AhbFieldRule {
2170 segment_path: "SG2/NAD/3035".to_string(),
2171 name: "Empfaenger".to_string(),
2172 ahb_status: "X".to_string(),
2173 codes: vec![AhbCodeRule {
2174 value: "MR".to_string(),
2175 description: "Empfaenger".to_string(),
2176 ahb_status: "X".to_string(),
2177 }],
2178 parent_group_ahb_status: None,
2179 ..Default::default()
2180 },
2181 ],
2182 ub_definitions: HashMap::new(),
2183 };
2184
2185 let report = validator.validate(
2186 &[nad_ms, nad_mt],
2187 &workflow,
2188 &external,
2189 ValidationLevel::Conditions,
2190 );
2191
2192 let code_errors: Vec<_> = report
2193 .by_category(ValidationCategory::Code)
2194 .filter(|i| i.severity == Severity::Error)
2195 .collect();
2196 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
2197 assert!(code_errors[0].message.contains("MT"));
2198 assert!(code_errors[0].message.contains("MR"));
2199 assert!(code_errors[0].message.contains("MS"));
2200 }
2201
2202 #[test]
2203 fn test_cross_field_code_validation_unions_across_groups() {
2204 let evaluator = MockEvaluator::new(vec![]);
2208 let validator = EdifactValidator::new(evaluator);
2209 let external = NoOpExternalProvider;
2210
2211 let segments = vec![
2212 OwnedSegment {
2213 id: "NAD".to_string(),
2214 elements: vec![vec!["MS".to_string()]],
2215 segment_number: 3,
2216 },
2217 OwnedSegment {
2218 id: "NAD".to_string(),
2219 elements: vec![vec!["MR".to_string()]],
2220 segment_number: 4,
2221 },
2222 OwnedSegment {
2223 id: "NAD".to_string(),
2224 elements: vec![vec!["Z04".to_string()]],
2225 segment_number: 20,
2226 },
2227 OwnedSegment {
2228 id: "NAD".to_string(),
2229 elements: vec![vec!["Z09".to_string()]],
2230 segment_number: 21,
2231 },
2232 OwnedSegment {
2233 id: "NAD".to_string(),
2234 elements: vec![vec!["MT".to_string()]], segment_number: 22,
2236 },
2237 ];
2238
2239 let workflow = AhbWorkflow {
2240 pruefidentifikator: "55001".to_string(),
2241 description: "Test".to_string(),
2242 communication_direction: None,
2243 fields: vec![
2244 AhbFieldRule {
2245 segment_path: "SG2/NAD/3035".to_string(),
2246 name: "Absender".to_string(),
2247 ahb_status: "X".to_string(),
2248 codes: vec![AhbCodeRule {
2249 value: "MS".to_string(),
2250 description: "Absender".to_string(),
2251 ahb_status: "X".to_string(),
2252 }],
2253 parent_group_ahb_status: None,
2254 ..Default::default()
2255 },
2256 AhbFieldRule {
2257 segment_path: "SG2/NAD/3035".to_string(),
2258 name: "Empfaenger".to_string(),
2259 ahb_status: "X".to_string(),
2260 codes: vec![AhbCodeRule {
2261 value: "MR".to_string(),
2262 description: "Empfaenger".to_string(),
2263 ahb_status: "X".to_string(),
2264 }],
2265 parent_group_ahb_status: None,
2266 ..Default::default()
2267 },
2268 AhbFieldRule {
2269 segment_path: "SG4/SG12/NAD/3035".to_string(),
2270 name: "Anschlussnutzer".to_string(),
2271 ahb_status: "X".to_string(),
2272 codes: vec![AhbCodeRule {
2273 value: "Z04".to_string(),
2274 description: "Anschlussnutzer".to_string(),
2275 ahb_status: "X".to_string(),
2276 }],
2277 parent_group_ahb_status: None,
2278 ..Default::default()
2279 },
2280 AhbFieldRule {
2281 segment_path: "SG4/SG12/NAD/3035".to_string(),
2282 name: "Korrespondenzanschrift".to_string(),
2283 ahb_status: "X".to_string(),
2284 codes: vec![AhbCodeRule {
2285 value: "Z09".to_string(),
2286 description: "Korrespondenzanschrift".to_string(),
2287 ahb_status: "X".to_string(),
2288 }],
2289 parent_group_ahb_status: None,
2290 ..Default::default()
2291 },
2292 ],
2293 ub_definitions: HashMap::new(),
2294 };
2295
2296 let report =
2297 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
2298
2299 let code_errors: Vec<_> = report
2300 .by_category(ValidationCategory::Code)
2301 .filter(|i| i.severity == Severity::Error)
2302 .collect();
2303 assert_eq!(
2304 code_errors.len(),
2305 1,
2306 "Expected exactly one COD002 error for MT, got: {:?}",
2307 code_errors
2308 );
2309 assert!(code_errors[0].message.contains("MT"));
2310 }
2311
2312 #[test]
2313 fn test_cross_field_code_validation_accepts_conditionally_allowed_codes() {
2314 let evaluator = MockEvaluator::new(vec![]);
2323 let validator = EdifactValidator::new(evaluator);
2324 let external = NoOpExternalProvider;
2325
2326 let qty_67 = OwnedSegment {
2327 id: "QTY".to_string(),
2328 elements: vec![vec!["67".to_string(), "0.185".to_string()]],
2329 segment_number: 10,
2330 };
2331
2332 let workflow = AhbWorkflow {
2333 pruefidentifikator: "13025".to_string(),
2334 description: "Test".to_string(),
2335 communication_direction: None,
2336 fields: vec![AhbFieldRule {
2337 segment_path: "SG5/SG6/SG9/SG10/QTY/qualifier".to_string(),
2338 name: "Menge, Qualifier".to_string(),
2339 ahb_status: "X".to_string(),
2340 codes: vec![
2341 AhbCodeRule {
2342 value: "220".to_string(),
2343 description: "Wahrer Wert".to_string(),
2344 ahb_status: "X".to_string(),
2345 },
2346 AhbCodeRule {
2347 value: "67".to_string(),
2348 description: "Ersatzwert".to_string(),
2349 ahb_status: "X [35] ∨ ([32] ∧ [77])".to_string(),
2350 },
2351 AhbCodeRule {
2352 value: "Z18".to_string(),
2353 description: "Vorläufiger Wert".to_string(),
2354 ahb_status: "X [35]".to_string(),
2355 },
2356 ],
2357 parent_group_ahb_status: None,
2358 element_index: Some(0),
2359 component_index: Some(0),
2360 ..Default::default()
2361 }],
2362 ub_definitions: HashMap::new(),
2363 };
2364
2365 let report =
2366 validator.validate(&[qty_67], &workflow, &external, ValidationLevel::Conditions);
2367
2368 let code_errors: Vec<_> = report
2369 .by_category(ValidationCategory::Code)
2370 .filter(|i| i.severity == Severity::Error)
2371 .collect();
2372 assert!(
2373 code_errors.is_empty(),
2374 "QTY+67 should be accepted because code '67' is conditionally allowed for this PID (X [35] ∨ ([32] ∧ [77])). Got errors: {:?}",
2375 code_errors
2376 );
2377 }
2378
2379 #[test]
2380 fn test_is_qualifier_field_simple_paths() {
2381 assert!(is_qualifier_field("NAD/3035"));
2382 assert!(is_qualifier_field("SG2/NAD/3035"));
2383 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
2384 assert!(is_qualifier_field("LOC/3227"));
2385 }
2386
2387 #[test]
2388 fn test_is_qualifier_field_composite_paths() {
2389 assert!(is_qualifier_field("UNH/S009/0065"));
2393 assert!(is_qualifier_field("NAD/C082/3039"));
2394 assert!(is_qualifier_field("SG2/NAD/C082/3039"));
2395 assert!(is_qualifier_field("SG4/STS/C556/9013"));
2396 }
2397
2398 #[test]
2399 fn test_is_qualifier_field_bare_segment() {
2400 assert!(!is_qualifier_field("NAD"));
2401 assert!(!is_qualifier_field("SG2/NAD"));
2402 }
2403
2404 #[test]
2405 fn test_is_qualifier_field_rejects_deep_paths() {
2406 assert!(!is_qualifier_field("SEG/A/B/C/D"));
2408 }
2409
2410 #[test]
2411 fn test_missing_qualifier_instance_is_detected() {
2412 let evaluator = MockEvaluator::new(vec![]);
2415 let validator = EdifactValidator::new(evaluator);
2416 let external = NoOpExternalProvider;
2417
2418 let nad_ms = OwnedSegment {
2419 id: "NAD".to_string(),
2420 elements: vec![vec!["MS".to_string()]],
2421 segment_number: 3,
2422 };
2423
2424 let workflow = AhbWorkflow {
2425 pruefidentifikator: "55001".to_string(),
2426 description: "Test".to_string(),
2427 communication_direction: None,
2428 fields: vec![
2429 AhbFieldRule {
2430 segment_path: "SG2/NAD/3035".to_string(),
2431 name: "Absender".to_string(),
2432 ahb_status: "X".to_string(),
2433 codes: vec![AhbCodeRule {
2434 value: "MS".to_string(),
2435 description: "Absender".to_string(),
2436 ahb_status: "X".to_string(),
2437 }],
2438 parent_group_ahb_status: None,
2439 ..Default::default()
2440 },
2441 AhbFieldRule {
2442 segment_path: "SG2/NAD/3035".to_string(),
2443 name: "Empfaenger".to_string(),
2444 ahb_status: "Muss".to_string(),
2445 codes: vec![AhbCodeRule {
2446 value: "MR".to_string(),
2447 description: "Empfaenger".to_string(),
2448 ahb_status: "X".to_string(),
2449 }],
2450 parent_group_ahb_status: None,
2451 ..Default::default()
2452 },
2453 ],
2454 ub_definitions: HashMap::new(),
2455 };
2456
2457 let report =
2458 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
2459
2460 let ahb_errors: Vec<_> = report
2461 .by_category(ValidationCategory::Ahb)
2462 .filter(|i| i.severity == Severity::Error)
2463 .collect();
2464 assert_eq!(
2465 ahb_errors.len(),
2466 1,
2467 "Expected AHB001 for missing NAD+MR, got: {:?}",
2468 ahb_errors
2469 );
2470 assert!(ahb_errors[0].message.contains("Empfaenger"));
2471 }
2472
2473 #[test]
2474 fn test_present_qualifier_instance_no_error() {
2475 let evaluator = MockEvaluator::new(vec![]);
2477 let validator = EdifactValidator::new(evaluator);
2478 let external = NoOpExternalProvider;
2479
2480 let segments = vec![
2481 OwnedSegment {
2482 id: "NAD".to_string(),
2483 elements: vec![vec!["MS".to_string()]],
2484 segment_number: 3,
2485 },
2486 OwnedSegment {
2487 id: "NAD".to_string(),
2488 elements: vec![vec!["MR".to_string()]],
2489 segment_number: 4,
2490 },
2491 ];
2492
2493 let workflow = AhbWorkflow {
2494 pruefidentifikator: "55001".to_string(),
2495 description: "Test".to_string(),
2496 communication_direction: None,
2497 fields: vec![
2498 AhbFieldRule {
2499 segment_path: "SG2/NAD/3035".to_string(),
2500 name: "Absender".to_string(),
2501 ahb_status: "Muss".to_string(),
2502 codes: vec![AhbCodeRule {
2503 value: "MS".to_string(),
2504 description: "Absender".to_string(),
2505 ahb_status: "X".to_string(),
2506 }],
2507 parent_group_ahb_status: None,
2508 ..Default::default()
2509 },
2510 AhbFieldRule {
2511 segment_path: "SG2/NAD/3035".to_string(),
2512 name: "Empfaenger".to_string(),
2513 ahb_status: "Muss".to_string(),
2514 codes: vec![AhbCodeRule {
2515 value: "MR".to_string(),
2516 description: "Empfaenger".to_string(),
2517 ahb_status: "X".to_string(),
2518 }],
2519 parent_group_ahb_status: None,
2520 ..Default::default()
2521 },
2522 ],
2523 ub_definitions: HashMap::new(),
2524 };
2525
2526 let report =
2527 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
2528
2529 let ahb_errors: Vec<_> = report
2530 .by_category(ValidationCategory::Ahb)
2531 .filter(|i| i.severity == Severity::Error)
2532 .collect();
2533 assert!(
2534 ahb_errors.is_empty(),
2535 "Expected no AHB001 errors, got: {:?}",
2536 ahb_errors
2537 );
2538 }
2539
2540 #[test]
2541 fn test_extract_group_path_key() {
2542 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
2543 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
2544 assert_eq!(extract_group_path_key("NAD/3035"), "");
2545 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
2546 }
2547
2548 #[test]
2549 fn test_absent_optional_group_no_missing_field_error() {
2550 use mig_types::navigator::GroupNavigator;
2553
2554 struct NavWithoutSG3;
2555 impl GroupNavigator for NavWithoutSG3 {
2556 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2557 vec![]
2558 }
2559 fn find_segments_with_qualifier_in_group(
2560 &self,
2561 _: &str,
2562 _: usize,
2563 _: &str,
2564 _: &[&str],
2565 _: usize,
2566 ) -> Vec<OwnedSegment> {
2567 vec![]
2568 }
2569 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2570 match group_path {
2571 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
2574 }
2575 }
2576 }
2577
2578 let evaluator = MockEvaluator::new(vec![]);
2579 let validator = EdifactValidator::new(evaluator);
2580 let external = NoOpExternalProvider;
2581 let nav = NavWithoutSG3;
2582
2583 let segments = vec![
2585 OwnedSegment {
2586 id: "NAD".into(),
2587 elements: vec![vec!["MS".into()]],
2588 segment_number: 3,
2589 },
2590 OwnedSegment {
2591 id: "NAD".into(),
2592 elements: vec![vec!["MR".into()]],
2593 segment_number: 4,
2594 },
2595 ];
2596
2597 let workflow = AhbWorkflow {
2598 pruefidentifikator: "55001".to_string(),
2599 description: "Test".to_string(),
2600 communication_direction: None,
2601 fields: vec![
2602 AhbFieldRule {
2603 segment_path: "SG2/SG3/CTA/3139".to_string(),
2604 name: "Funktion des Ansprechpartners, Code".to_string(),
2605 ahb_status: "Muss".to_string(),
2606 codes: vec![],
2607 parent_group_ahb_status: None,
2608 ..Default::default()
2609 },
2610 AhbFieldRule {
2611 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
2612 name: "Name vom Ansprechpartner".to_string(),
2613 ahb_status: "X".to_string(),
2614 codes: vec![],
2615 parent_group_ahb_status: None,
2616 ..Default::default()
2617 },
2618 ],
2619 ub_definitions: HashMap::new(),
2620 };
2621
2622 let report = validator.validate_with_navigator(
2623 &segments,
2624 &workflow,
2625 &external,
2626 ValidationLevel::Conditions,
2627 &nav,
2628 );
2629
2630 let ahb_errors: Vec<_> = report
2631 .by_category(ValidationCategory::Ahb)
2632 .filter(|i| i.severity == Severity::Error)
2633 .collect();
2634 assert!(
2635 ahb_errors.is_empty(),
2636 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
2637 ahb_errors
2638 );
2639 }
2640
2641 #[test]
2642 fn test_present_group_still_checks_mandatory_fields() {
2643 use mig_types::navigator::GroupNavigator;
2645
2646 struct NavWithSG3;
2647 impl GroupNavigator for NavWithSG3 {
2648 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2649 vec![]
2650 }
2651 fn find_segments_with_qualifier_in_group(
2652 &self,
2653 _: &str,
2654 _: usize,
2655 _: &str,
2656 _: &[&str],
2657 _: usize,
2658 ) -> Vec<OwnedSegment> {
2659 vec![]
2660 }
2661 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2662 match group_path {
2663 ["SG2"] => 1,
2664 ["SG2", "SG3"] => 1, _ => 0,
2666 }
2667 }
2668 }
2669
2670 let evaluator = MockEvaluator::new(vec![]);
2671 let validator = EdifactValidator::new(evaluator);
2672 let external = NoOpExternalProvider;
2673 let nav = NavWithSG3;
2674
2675 let segments = vec![OwnedSegment {
2677 id: "NAD".into(),
2678 elements: vec![vec!["MS".into()]],
2679 segment_number: 3,
2680 }];
2681
2682 let workflow = AhbWorkflow {
2683 pruefidentifikator: "55001".to_string(),
2684 description: "Test".to_string(),
2685 communication_direction: None,
2686 fields: vec![AhbFieldRule {
2687 segment_path: "SG2/SG3/CTA/3139".to_string(),
2688 name: "Funktion des Ansprechpartners, Code".to_string(),
2689 ahb_status: "Muss".to_string(),
2690 codes: vec![],
2691 parent_group_ahb_status: None,
2692 ..Default::default()
2693 }],
2694 ub_definitions: HashMap::new(),
2695 };
2696
2697 let report = validator.validate_with_navigator(
2698 &segments,
2699 &workflow,
2700 &external,
2701 ValidationLevel::Conditions,
2702 &nav,
2703 );
2704
2705 let ahb_errors: Vec<_> = report
2706 .by_category(ValidationCategory::Ahb)
2707 .filter(|i| i.severity == Severity::Error)
2708 .collect();
2709 assert_eq!(
2710 ahb_errors.len(),
2711 1,
2712 "Expected AHB001 error when SG3 is present but CTA missing"
2713 );
2714 assert!(ahb_errors[0].message.contains("CTA"));
2715 }
2716
2717 #[test]
2718 fn test_missing_qualifier_with_navigator_is_detected() {
2719 use mig_types::navigator::GroupNavigator;
2722
2723 struct NavWithSG2;
2724 impl GroupNavigator for NavWithSG2 {
2725 fn find_segments_in_group(
2726 &self,
2727 segment_id: &str,
2728 group_path: &[&str],
2729 instance_index: usize,
2730 ) -> Vec<OwnedSegment> {
2731 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
2732 vec![OwnedSegment {
2733 id: "NAD".into(),
2734 elements: vec![vec!["MS".into()]],
2735 segment_number: 3,
2736 }]
2737 } else {
2738 vec![]
2739 }
2740 }
2741 fn find_segments_with_qualifier_in_group(
2742 &self,
2743 _: &str,
2744 _: usize,
2745 _: &str,
2746 _: &[&str],
2747 _: usize,
2748 ) -> Vec<OwnedSegment> {
2749 vec![]
2750 }
2751 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2752 match group_path {
2753 ["SG2"] => 1,
2754 _ => 0,
2755 }
2756 }
2757 }
2758
2759 let evaluator = MockEvaluator::new(vec![]);
2760 let validator = EdifactValidator::new(evaluator);
2761 let external = NoOpExternalProvider;
2762 let nav = NavWithSG2;
2763
2764 let segments = vec![OwnedSegment {
2765 id: "NAD".into(),
2766 elements: vec![vec!["MS".into()]],
2767 segment_number: 3,
2768 }];
2769
2770 let workflow = AhbWorkflow {
2771 pruefidentifikator: "55001".to_string(),
2772 description: "Test".to_string(),
2773 communication_direction: None,
2774 fields: vec![
2775 AhbFieldRule {
2776 segment_path: "SG2/NAD/3035".to_string(),
2777 name: "Absender".to_string(),
2778 ahb_status: "X".to_string(),
2779 codes: vec![AhbCodeRule {
2780 value: "MS".to_string(),
2781 description: "Absender".to_string(),
2782 ahb_status: "X".to_string(),
2783 }],
2784 parent_group_ahb_status: None,
2785 ..Default::default()
2786 },
2787 AhbFieldRule {
2788 segment_path: "SG2/NAD/3035".to_string(),
2789 name: "Empfaenger".to_string(),
2790 ahb_status: "Muss".to_string(),
2791 codes: vec![AhbCodeRule {
2792 value: "MR".to_string(),
2793 description: "Empfaenger".to_string(),
2794 ahb_status: "X".to_string(),
2795 }],
2796 parent_group_ahb_status: None,
2797 ..Default::default()
2798 },
2799 ],
2800 ub_definitions: HashMap::new(),
2801 };
2802
2803 let report = validator.validate_with_navigator(
2804 &segments,
2805 &workflow,
2806 &external,
2807 ValidationLevel::Conditions,
2808 &nav,
2809 );
2810
2811 let ahb_errors: Vec<_> = report
2812 .by_category(ValidationCategory::Ahb)
2813 .filter(|i| i.severity == Severity::Error)
2814 .collect();
2815 assert_eq!(
2816 ahb_errors.len(),
2817 1,
2818 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
2819 ahb_errors
2820 );
2821 assert!(ahb_errors[0].message.contains("Empfaenger"));
2822 }
2823
2824 #[test]
2825 fn test_optional_group_variant_absent_no_error() {
2826 use mig_types::navigator::GroupNavigator;
2831
2832 struct TestNav;
2833 impl GroupNavigator for TestNav {
2834 fn find_segments_in_group(
2835 &self,
2836 segment_id: &str,
2837 group_path: &[&str],
2838 instance_index: usize,
2839 ) -> Vec<OwnedSegment> {
2840 match (segment_id, group_path, instance_index) {
2841 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
2842 id: "LOC".into(),
2843 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2844 segment_number: 10,
2845 }],
2846 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
2847 id: "NAD".into(),
2848 elements: vec![vec!["MS".into()]],
2849 segment_number: 3,
2850 }],
2851 _ => vec![],
2852 }
2853 }
2854 fn find_segments_with_qualifier_in_group(
2855 &self,
2856 _: &str,
2857 _: usize,
2858 _: &str,
2859 _: &[&str],
2860 _: usize,
2861 ) -> Vec<OwnedSegment> {
2862 vec![]
2863 }
2864 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2865 match group_path {
2866 ["SG2"] => 1,
2867 ["SG4"] => 1,
2868 ["SG4", "SG5"] => 1, _ => 0,
2870 }
2871 }
2872 }
2873
2874 let evaluator = MockEvaluator::new(vec![]);
2875 let validator = EdifactValidator::new(evaluator);
2876 let external = NoOpExternalProvider;
2877 let nav = TestNav;
2878
2879 let segments = vec![
2880 OwnedSegment {
2881 id: "NAD".into(),
2882 elements: vec![vec!["MS".into()]],
2883 segment_number: 3,
2884 },
2885 OwnedSegment {
2886 id: "LOC".into(),
2887 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2888 segment_number: 10,
2889 },
2890 ];
2891
2892 let workflow = AhbWorkflow {
2893 pruefidentifikator: "55001".to_string(),
2894 description: "Test".to_string(),
2895 communication_direction: None,
2896 fields: vec![
2897 AhbFieldRule {
2899 segment_path: "SG2/NAD/3035".to_string(),
2900 name: "Absender".to_string(),
2901 ahb_status: "X".to_string(),
2902 codes: vec![AhbCodeRule {
2903 value: "MS".to_string(),
2904 description: "Absender".to_string(),
2905 ahb_status: "X".to_string(),
2906 }],
2907 parent_group_ahb_status: Some("Muss".to_string()),
2908 ..Default::default()
2909 },
2910 AhbFieldRule {
2911 segment_path: "SG2/NAD/3035".to_string(),
2912 name: "Empfaenger".to_string(),
2913 ahb_status: "Muss".to_string(),
2914 codes: vec![AhbCodeRule {
2915 value: "MR".to_string(),
2916 description: "Empfaenger".to_string(),
2917 ahb_status: "X".to_string(),
2918 }],
2919 parent_group_ahb_status: Some("Muss".to_string()),
2920 ..Default::default()
2921 },
2922 AhbFieldRule {
2924 segment_path: "SG4/SG5/LOC/3227".to_string(),
2925 name: "Ortsangabe, Qualifier (Z16)".to_string(),
2926 ahb_status: "X".to_string(),
2927 codes: vec![AhbCodeRule {
2928 value: "Z16".to_string(),
2929 description: "Marktlokation".to_string(),
2930 ahb_status: "X".to_string(),
2931 }],
2932 parent_group_ahb_status: Some("Kann".to_string()),
2933 ..Default::default()
2934 },
2935 AhbFieldRule {
2936 segment_path: "SG4/SG5/LOC/3227".to_string(),
2937 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2938 ahb_status: "Muss".to_string(),
2939 codes: vec![AhbCodeRule {
2940 value: "Z17".to_string(),
2941 description: "Messlokation".to_string(),
2942 ahb_status: "X".to_string(),
2943 }],
2944 parent_group_ahb_status: Some("Kann".to_string()),
2945 ..Default::default()
2946 },
2947 ],
2948 ub_definitions: HashMap::new(),
2949 };
2950
2951 let report = validator.validate_with_navigator(
2952 &segments,
2953 &workflow,
2954 &external,
2955 ValidationLevel::Conditions,
2956 &nav,
2957 );
2958
2959 let ahb_errors: Vec<_> = report
2960 .by_category(ValidationCategory::Ahb)
2961 .filter(|i| i.severity == Severity::Error)
2962 .collect();
2963
2964 assert_eq!(
2967 ahb_errors.len(),
2968 1,
2969 "Expected only AHB001 for missing NAD+MR, got: {:?}",
2970 ahb_errors
2971 );
2972 assert!(
2973 ahb_errors[0].message.contains("Empfaenger"),
2974 "Error should be for missing NAD+MR (Empfaenger)"
2975 );
2976 }
2977
2978 #[test]
2979 fn test_conditional_group_variant_absent_no_error() {
2980 use mig_types::navigator::GroupNavigator;
2985
2986 struct TestNav;
2987 impl GroupNavigator for TestNav {
2988 fn find_segments_in_group(
2989 &self,
2990 segment_id: &str,
2991 group_path: &[&str],
2992 instance_index: usize,
2993 ) -> Vec<OwnedSegment> {
2994 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
2995 vec![OwnedSegment {
2996 id: "LOC".into(),
2997 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2998 segment_number: 10,
2999 }]
3000 } else {
3001 vec![]
3002 }
3003 }
3004 fn find_segments_with_qualifier_in_group(
3005 &self,
3006 _: &str,
3007 _: usize,
3008 _: &str,
3009 _: &[&str],
3010 _: usize,
3011 ) -> Vec<OwnedSegment> {
3012 vec![]
3013 }
3014 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3015 match group_path {
3016 ["SG4"] => 1,
3017 ["SG4", "SG5"] => 1, _ => 0,
3019 }
3020 }
3021 }
3022
3023 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
3026 let validator = EdifactValidator::new(evaluator);
3027 let external = NoOpExternalProvider;
3028 let nav = TestNav;
3029
3030 let segments = vec![OwnedSegment {
3031 id: "LOC".into(),
3032 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
3033 segment_number: 10,
3034 }];
3035
3036 let workflow = AhbWorkflow {
3037 pruefidentifikator: "55001".to_string(),
3038 description: "Test".to_string(),
3039 communication_direction: None,
3040 fields: vec![
3041 AhbFieldRule {
3043 segment_path: "SG4/SG5/LOC/3227".to_string(),
3044 name: "Ortsangabe, Qualifier (Z16)".to_string(),
3045 ahb_status: "X".to_string(),
3046 codes: vec![AhbCodeRule {
3047 value: "Z16".to_string(),
3048 description: "Marktlokation".to_string(),
3049 ahb_status: "X".to_string(),
3050 }],
3051 parent_group_ahb_status: Some("Muss [2061]".to_string()),
3052 ..Default::default()
3053 },
3054 AhbFieldRule {
3056 segment_path: "SG4/SG5/LOC/3227".to_string(),
3057 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3058 ahb_status: "X".to_string(),
3059 codes: vec![AhbCodeRule {
3060 value: "Z17".to_string(),
3061 description: "Messlokation".to_string(),
3062 ahb_status: "X".to_string(),
3063 }],
3064 parent_group_ahb_status: Some("Soll [165]".to_string()),
3065 ..Default::default()
3066 },
3067 ],
3068 ub_definitions: HashMap::new(),
3069 };
3070
3071 let report = validator.validate_with_navigator(
3072 &segments,
3073 &workflow,
3074 &external,
3075 ValidationLevel::Conditions,
3076 &nav,
3077 );
3078
3079 let ahb_errors: Vec<_> = report
3080 .by_category(ValidationCategory::Ahb)
3081 .filter(|i| i.severity == Severity::Error)
3082 .collect();
3083
3084 assert!(
3086 ahb_errors.is_empty(),
3087 "Expected no errors when conditional group variant [165]=False, got: {:?}",
3088 ahb_errors
3089 );
3090 }
3091
3092 #[test]
3093 fn test_conditional_group_variant_unknown_no_error() {
3094 let evaluator = MockEvaluator::new(vec![]);
3100 let validator = EdifactValidator::new(evaluator);
3101 let external = NoOpExternalProvider;
3102
3103 let workflow = AhbWorkflow {
3104 pruefidentifikator: "55001".to_string(),
3105 description: "Test".to_string(),
3106 communication_direction: None,
3107 fields: vec![AhbFieldRule {
3108 segment_path: "SG4/SG5/LOC/3227".to_string(),
3109 name: "Ortsangabe, Qualifier (Z17)".to_string(),
3110 ahb_status: "X".to_string(),
3111 codes: vec![AhbCodeRule {
3112 value: "Z17".to_string(),
3113 description: "Messlokation".to_string(),
3114 ahb_status: "X".to_string(),
3115 }],
3116 parent_group_ahb_status: Some("Soll [165]".to_string()),
3117 ..Default::default()
3118 }],
3119 ub_definitions: HashMap::new(),
3120 };
3121
3122 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
3123
3124 let ahb_errors: Vec<_> = report
3125 .by_category(ValidationCategory::Ahb)
3126 .filter(|i| i.severity == Severity::Error)
3127 .collect();
3128
3129 assert!(
3131 ahb_errors.is_empty(),
3132 "Expected no errors when parent group condition is Unknown, got: {:?}",
3133 ahb_errors
3134 );
3135 }
3136
3137 #[test]
3138 fn test_segment_absent_within_present_group_no_error() {
3139 use mig_types::navigator::GroupNavigator;
3143
3144 struct TestNav;
3145 impl GroupNavigator for TestNav {
3146 fn find_segments_in_group(
3147 &self,
3148 segment_id: &str,
3149 group_path: &[&str],
3150 instance_index: usize,
3151 ) -> Vec<OwnedSegment> {
3152 if segment_id == "QTY"
3154 && group_path == ["SG5", "SG6", "SG9", "SG10"]
3155 && instance_index == 0
3156 {
3157 vec![OwnedSegment {
3158 id: "QTY".into(),
3159 elements: vec![vec!["220".into(), "0".into()]],
3160 segment_number: 14,
3161 }]
3162 } else {
3163 vec![]
3164 }
3165 }
3166 fn find_segments_with_qualifier_in_group(
3167 &self,
3168 _: &str,
3169 _: usize,
3170 _: &str,
3171 _: &[&str],
3172 _: usize,
3173 ) -> Vec<OwnedSegment> {
3174 vec![]
3175 }
3176 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3177 match group_path {
3178 ["SG5"] => 1,
3179 ["SG5", "SG6"] => 1,
3180 ["SG5", "SG6", "SG9"] => 1,
3181 ["SG5", "SG6", "SG9", "SG10"] => 1,
3182 _ => 0,
3183 }
3184 }
3185 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
3186 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
3188 }
3189 }
3190
3191 let evaluator = MockEvaluator::all_true(&[]);
3192 let validator = EdifactValidator::new(evaluator);
3193 let external = NoOpExternalProvider;
3194 let nav = TestNav;
3195
3196 let segments = vec![OwnedSegment {
3197 id: "QTY".into(),
3198 elements: vec![vec!["220".into(), "0".into()]],
3199 segment_number: 14,
3200 }];
3201
3202 let workflow = AhbWorkflow {
3203 pruefidentifikator: "13017".to_string(),
3204 description: "Test".to_string(),
3205 communication_direction: None,
3206 fields: vec![
3207 AhbFieldRule {
3209 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
3210 name: "Statuskategorie, Code".to_string(),
3211 ahb_status: "X".to_string(),
3212 codes: vec![],
3213 parent_group_ahb_status: Some("Muss".to_string()),
3214 ..Default::default()
3215 },
3216 AhbFieldRule {
3218 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3219 name: "Statusanlaß, Code".to_string(),
3220 ahb_status: "X [5]".to_string(),
3221 codes: vec![],
3222 parent_group_ahb_status: Some("Muss".to_string()),
3223 ..Default::default()
3224 },
3225 ],
3226 ub_definitions: HashMap::new(),
3227 };
3228
3229 let report = validator.validate_with_navigator(
3230 &segments,
3231 &workflow,
3232 &external,
3233 ValidationLevel::Conditions,
3234 &nav,
3235 );
3236
3237 let ahb_errors: Vec<_> = report
3238 .by_category(ValidationCategory::Ahb)
3239 .filter(|i| i.severity == Severity::Error)
3240 .collect();
3241
3242 assert!(
3243 ahb_errors.is_empty(),
3244 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
3245 ahb_errors
3246 );
3247 }
3248
3249 #[test]
3250 fn test_group_scoped_code_validation_with_navigator() {
3251 use mig_types::navigator::GroupNavigator;
3255
3256 struct TestNav;
3257 impl GroupNavigator for TestNav {
3258 fn find_segments_in_group(
3259 &self,
3260 segment_id: &str,
3261 group_path: &[&str],
3262 _instance_index: usize,
3263 ) -> Vec<OwnedSegment> {
3264 if segment_id != "NAD" {
3265 return vec![];
3266 }
3267 match group_path {
3268 ["SG2"] => vec![
3269 OwnedSegment {
3270 id: "NAD".into(),
3271 elements: vec![vec!["MS".into()]],
3272 segment_number: 3,
3273 },
3274 OwnedSegment {
3275 id: "NAD".into(),
3276 elements: vec![vec!["MT".into()]], segment_number: 4,
3278 },
3279 ],
3280 ["SG4", "SG12"] => vec![
3281 OwnedSegment {
3282 id: "NAD".into(),
3283 elements: vec![vec!["Z04".into()]],
3284 segment_number: 20,
3285 },
3286 OwnedSegment {
3287 id: "NAD".into(),
3288 elements: vec![vec!["Z09".into()]],
3289 segment_number: 21,
3290 },
3291 ],
3292 _ => vec![],
3293 }
3294 }
3295 fn find_segments_with_qualifier_in_group(
3296 &self,
3297 _: &str,
3298 _: usize,
3299 _: &str,
3300 _: &[&str],
3301 _: usize,
3302 ) -> Vec<OwnedSegment> {
3303 vec![]
3304 }
3305 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3306 match group_path {
3307 ["SG2"] | ["SG4", "SG12"] => 1,
3308 _ => 0,
3309 }
3310 }
3311 }
3312
3313 let evaluator = MockEvaluator::new(vec![]);
3314 let validator = EdifactValidator::new(evaluator);
3315 let external = NoOpExternalProvider;
3316 let nav = TestNav;
3317
3318 let workflow = AhbWorkflow {
3319 pruefidentifikator: "55001".to_string(),
3320 description: "Test".to_string(),
3321 communication_direction: None,
3322 fields: vec![
3323 AhbFieldRule {
3324 segment_path: "SG2/NAD/3035".to_string(),
3325 name: "Absender".to_string(),
3326 ahb_status: "X".to_string(),
3327 codes: vec![AhbCodeRule {
3328 value: "MS".to_string(),
3329 description: "Absender".to_string(),
3330 ahb_status: "X".to_string(),
3331 }],
3332 parent_group_ahb_status: None,
3333 ..Default::default()
3334 },
3335 AhbFieldRule {
3336 segment_path: "SG2/NAD/3035".to_string(),
3337 name: "Empfaenger".to_string(),
3338 ahb_status: "X".to_string(),
3339 codes: vec![AhbCodeRule {
3340 value: "MR".to_string(),
3341 description: "Empfaenger".to_string(),
3342 ahb_status: "X".to_string(),
3343 }],
3344 parent_group_ahb_status: None,
3345 ..Default::default()
3346 },
3347 AhbFieldRule {
3348 segment_path: "SG4/SG12/NAD/3035".to_string(),
3349 name: "Anschlussnutzer".to_string(),
3350 ahb_status: "X".to_string(),
3351 codes: vec![AhbCodeRule {
3352 value: "Z04".to_string(),
3353 description: "Anschlussnutzer".to_string(),
3354 ahb_status: "X".to_string(),
3355 }],
3356 parent_group_ahb_status: None,
3357 ..Default::default()
3358 },
3359 AhbFieldRule {
3360 segment_path: "SG4/SG12/NAD/3035".to_string(),
3361 name: "Korrespondenzanschrift".to_string(),
3362 ahb_status: "X".to_string(),
3363 codes: vec![AhbCodeRule {
3364 value: "Z09".to_string(),
3365 description: "Korrespondenzanschrift".to_string(),
3366 ahb_status: "X".to_string(),
3367 }],
3368 parent_group_ahb_status: None,
3369 ..Default::default()
3370 },
3371 ],
3372 ub_definitions: HashMap::new(),
3373 };
3374
3375 let all_segments = vec![
3377 OwnedSegment {
3378 id: "NAD".into(),
3379 elements: vec![vec!["MS".into()]],
3380 segment_number: 3,
3381 },
3382 OwnedSegment {
3383 id: "NAD".into(),
3384 elements: vec![vec!["MT".into()]],
3385 segment_number: 4,
3386 },
3387 OwnedSegment {
3388 id: "NAD".into(),
3389 elements: vec![vec!["Z04".into()]],
3390 segment_number: 20,
3391 },
3392 OwnedSegment {
3393 id: "NAD".into(),
3394 elements: vec![vec!["Z09".into()]],
3395 segment_number: 21,
3396 },
3397 ];
3398
3399 let report = validator.validate_with_navigator(
3400 &all_segments,
3401 &workflow,
3402 &external,
3403 ValidationLevel::Conditions,
3404 &nav,
3405 );
3406
3407 let code_errors: Vec<_> = report
3408 .by_category(ValidationCategory::Code)
3409 .filter(|i| i.severity == Severity::Error)
3410 .collect();
3411
3412 assert_eq!(
3415 code_errors.len(),
3416 1,
3417 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
3418 code_errors
3419 );
3420 assert!(code_errors[0].message.contains("MT"));
3421 assert!(code_errors[0].message.contains("MR"));
3423 assert!(code_errors[0].message.contains("MS"));
3424 assert!(
3425 !code_errors[0].message.contains("Z04"),
3426 "SG4/SG12 codes should not leak into SG2 error"
3427 );
3428 assert!(
3430 code_errors[0]
3431 .field_path
3432 .as_deref()
3433 .unwrap_or("")
3434 .contains("SG2"),
3435 "Error field_path should reference SG2, got: {:?}",
3436 code_errors[0].field_path
3437 );
3438 }
3439
3440 #[test]
3443 fn test_package_cardinality_within_bounds() {
3444 let evaluator = MockEvaluator::all_true(&[]);
3446 let validator = EdifactValidator::new(evaluator);
3447 let external = NoOpExternalProvider;
3448
3449 let segments = vec![OwnedSegment {
3450 id: "STS".into(),
3451 elements: vec![
3452 vec!["Z33".into()], vec![], vec!["E01".into()], ],
3456 segment_number: 5,
3457 }];
3458
3459 let workflow = AhbWorkflow {
3460 pruefidentifikator: "13017".to_string(),
3461 description: "Test".to_string(),
3462 communication_direction: None,
3463 ub_definitions: HashMap::new(),
3464 fields: vec![AhbFieldRule {
3465 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3466 name: "Statusanlaß, Code".to_string(),
3467 ahb_status: "X".to_string(),
3468 element_index: Some(2),
3469 component_index: Some(0),
3470 codes: vec![
3471 AhbCodeRule {
3472 value: "E01".into(),
3473 description: "Code 1".into(),
3474 ahb_status: "X [4P0..1]".into(),
3475 },
3476 AhbCodeRule {
3477 value: "E02".into(),
3478 description: "Code 2".into(),
3479 ahb_status: "X [4P0..1]".into(),
3480 },
3481 ],
3482 parent_group_ahb_status: Some("Muss".to_string()),
3483 mig_number: None,
3484 }],
3485 };
3486
3487 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3488 let pkg_errors: Vec<_> = report
3489 .by_category(ValidationCategory::Ahb)
3490 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3491 .collect();
3492 assert!(
3493 pkg_errors.is_empty(),
3494 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
3495 pkg_errors
3496 );
3497 }
3498
3499 #[test]
3500 fn test_package_cardinality_zero_present_min_zero() {
3501 let evaluator = MockEvaluator::all_true(&[]);
3503 let validator = EdifactValidator::new(evaluator);
3504 let external = NoOpExternalProvider;
3505
3506 let segments = vec![OwnedSegment {
3507 id: "STS".into(),
3508 elements: vec![
3509 vec!["Z33".into()],
3510 vec![],
3511 vec!["X99".into()], ],
3513 segment_number: 5,
3514 }];
3515
3516 let workflow = AhbWorkflow {
3517 pruefidentifikator: "13017".to_string(),
3518 description: "Test".to_string(),
3519 communication_direction: None,
3520 ub_definitions: HashMap::new(),
3521 fields: vec![AhbFieldRule {
3522 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3523 name: "Statusanlaß, Code".to_string(),
3524 ahb_status: "X".to_string(),
3525 element_index: Some(2),
3526 component_index: Some(0),
3527 codes: vec![
3528 AhbCodeRule {
3529 value: "E01".into(),
3530 description: "Code 1".into(),
3531 ahb_status: "X [4P0..1]".into(),
3532 },
3533 AhbCodeRule {
3534 value: "E02".into(),
3535 description: "Code 2".into(),
3536 ahb_status: "X [4P0..1]".into(),
3537 },
3538 ],
3539 parent_group_ahb_status: Some("Muss".to_string()),
3540 mig_number: None,
3541 }],
3542 };
3543
3544 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3545 let pkg_errors: Vec<_> = report
3546 .by_category(ValidationCategory::Ahb)
3547 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3548 .collect();
3549 assert!(
3550 pkg_errors.is_empty(),
3551 "0 codes, min=0 — no error expected, got: {:?}",
3552 pkg_errors
3553 );
3554 }
3555
3556 #[test]
3557 fn test_package_cardinality_too_many() {
3558 let evaluator = MockEvaluator::all_true(&[]);
3560 let validator = EdifactValidator::new(evaluator);
3561 let external = NoOpExternalProvider;
3562
3563 let segments = vec![
3565 OwnedSegment {
3566 id: "STS".into(),
3567 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
3568 segment_number: 5,
3569 },
3570 OwnedSegment {
3571 id: "STS".into(),
3572 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
3573 segment_number: 6,
3574 },
3575 ];
3576
3577 let workflow = AhbWorkflow {
3578 pruefidentifikator: "13017".to_string(),
3579 description: "Test".to_string(),
3580 communication_direction: None,
3581 ub_definitions: HashMap::new(),
3582 fields: vec![AhbFieldRule {
3583 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3584 name: "Statusanlaß, Code".to_string(),
3585 ahb_status: "X".to_string(),
3586 element_index: Some(2),
3587 component_index: Some(0),
3588 codes: vec![
3589 AhbCodeRule {
3590 value: "E01".into(),
3591 description: "Code 1".into(),
3592 ahb_status: "X [4P0..1]".into(),
3593 },
3594 AhbCodeRule {
3595 value: "E02".into(),
3596 description: "Code 2".into(),
3597 ahb_status: "X [4P0..1]".into(),
3598 },
3599 ],
3600 parent_group_ahb_status: Some("Muss".to_string()),
3601 mig_number: None,
3602 }],
3603 };
3604
3605 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3606 let pkg_errors: Vec<_> = report
3607 .by_category(ValidationCategory::Ahb)
3608 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3609 .collect();
3610 assert_eq!(
3611 pkg_errors.len(),
3612 1,
3613 "2 codes present, max=1 — expected 1 error, got: {:?}",
3614 pkg_errors
3615 );
3616 assert!(pkg_errors[0].message.contains("[4P0..1]"));
3617 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
3618 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
3619 }
3620
3621 #[test]
3622 fn test_package_cardinality_too_few() {
3623 let evaluator = MockEvaluator::all_true(&[]);
3625 let validator = EdifactValidator::new(evaluator);
3626 let external = NoOpExternalProvider;
3627
3628 let segments = vec![OwnedSegment {
3629 id: "STS".into(),
3630 elements: vec![
3631 vec!["Z33".into()],
3632 vec![],
3633 vec!["X99".into()], ],
3635 segment_number: 5,
3636 }];
3637
3638 let workflow = AhbWorkflow {
3639 pruefidentifikator: "13017".to_string(),
3640 description: "Test".to_string(),
3641 communication_direction: None,
3642 ub_definitions: HashMap::new(),
3643 fields: vec![AhbFieldRule {
3644 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3645 name: "Statusanlaß, Code".to_string(),
3646 ahb_status: "X".to_string(),
3647 element_index: Some(2),
3648 component_index: Some(0),
3649 codes: vec![
3650 AhbCodeRule {
3651 value: "E01".into(),
3652 description: "Code 1".into(),
3653 ahb_status: "X [5P1..3]".into(),
3654 },
3655 AhbCodeRule {
3656 value: "E02".into(),
3657 description: "Code 2".into(),
3658 ahb_status: "X [5P1..3]".into(),
3659 },
3660 AhbCodeRule {
3661 value: "E03".into(),
3662 description: "Code 3".into(),
3663 ahb_status: "X [5P1..3]".into(),
3664 },
3665 ],
3666 parent_group_ahb_status: Some("Muss".to_string()),
3667 mig_number: None,
3668 }],
3669 };
3670
3671 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3672 let pkg_errors: Vec<_> = report
3673 .by_category(ValidationCategory::Ahb)
3674 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3675 .collect();
3676 assert_eq!(
3677 pkg_errors.len(),
3678 1,
3679 "0 codes present, min=1 — expected 1 error, got: {:?}",
3680 pkg_errors
3681 );
3682 assert!(pkg_errors[0].message.contains("[5P1..3]"));
3683 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
3684 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
3685 }
3686
3687 #[test]
3688 fn test_package_cardinality_no_packages_in_workflow() {
3689 let evaluator = MockEvaluator::all_true(&[]);
3691 let validator = EdifactValidator::new(evaluator);
3692 let external = NoOpExternalProvider;
3693
3694 let segments = vec![OwnedSegment {
3695 id: "STS".into(),
3696 elements: vec![vec!["E01".into()]],
3697 segment_number: 5,
3698 }];
3699
3700 let workflow = AhbWorkflow {
3701 pruefidentifikator: "13017".to_string(),
3702 description: "Test".to_string(),
3703 communication_direction: None,
3704 ub_definitions: HashMap::new(),
3705 fields: vec![AhbFieldRule {
3706 segment_path: "STS/9015".to_string(),
3707 name: "Status Code".to_string(),
3708 ahb_status: "X".to_string(),
3709 codes: vec![AhbCodeRule {
3710 value: "E01".into(),
3711 description: "Code 1".into(),
3712 ahb_status: "X".into(),
3713 }],
3714 parent_group_ahb_status: Some("Muss".to_string()),
3715 ..Default::default()
3716 }],
3717 };
3718
3719 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3720 let pkg_errors: Vec<_> = report
3721 .by_category(ValidationCategory::Ahb)
3722 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3723 .collect();
3724 assert!(
3725 pkg_errors.is_empty(),
3726 "No packages in workflow — no errors expected"
3727 );
3728 }
3729
3730 #[test]
3731 fn test_package_cardinality_with_condition_and_package() {
3732 let evaluator = MockEvaluator::all_true(&[901]);
3734 let validator = EdifactValidator::new(evaluator);
3735 let external = NoOpExternalProvider;
3736
3737 let segments = vec![OwnedSegment {
3738 id: "STS".into(),
3739 elements: vec![vec![], vec![], vec!["E01".into()]],
3740 segment_number: 5,
3741 }];
3742
3743 let workflow = AhbWorkflow {
3744 pruefidentifikator: "13017".to_string(),
3745 description: "Test".to_string(),
3746 communication_direction: None,
3747 ub_definitions: HashMap::new(),
3748 fields: vec![AhbFieldRule {
3749 segment_path: "SG10/STS/C556/9013".to_string(),
3750 name: "Code".to_string(),
3751 ahb_status: "X".to_string(),
3752 element_index: Some(2),
3753 component_index: Some(0),
3754 codes: vec![
3755 AhbCodeRule {
3756 value: "E01".into(),
3757 description: "Code 1".into(),
3758 ahb_status: "X [901] [4P0..1]".into(),
3759 },
3760 AhbCodeRule {
3761 value: "E02".into(),
3762 description: "Code 2".into(),
3763 ahb_status: "X [901] [4P0..1]".into(),
3764 },
3765 ],
3766 parent_group_ahb_status: Some("Muss".to_string()),
3767 mig_number: None,
3768 }],
3769 };
3770
3771 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3772 let pkg_errors: Vec<_> = report
3773 .by_category(ValidationCategory::Ahb)
3774 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3775 .collect();
3776 assert!(
3777 pkg_errors.is_empty(),
3778 "1 code within [4P0..1] bounds — no error, got: {:?}",
3779 pkg_errors
3780 );
3781 }
3782
3783 #[test]
3784 fn test_package_cardinality_scoped_per_group_instance() {
3785 use mig_types::navigator::GroupNavigator;
3793
3794 struct TwoSg10s {
3795 sts_a: OwnedSegment,
3796 sts_b: OwnedSegment,
3797 }
3798 impl GroupNavigator for TwoSg10s {
3799 fn find_segments_in_group(
3800 &self,
3801 segment_id: &str,
3802 group_path: &[&str],
3803 instance_index: usize,
3804 ) -> Vec<OwnedSegment> {
3805 if group_path == ["SG5", "SG6", "SG9", "SG10"] && segment_id == "STS" {
3806 match instance_index {
3807 0 => vec![self.sts_a.clone()],
3808 1 => vec![self.sts_b.clone()],
3809 _ => vec![],
3810 }
3811 } else {
3812 vec![]
3813 }
3814 }
3815 fn find_segments_with_qualifier_in_group(
3816 &self,
3817 _: &str,
3818 _: usize,
3819 _: &str,
3820 _: &[&str],
3821 _: usize,
3822 ) -> Vec<OwnedSegment> {
3823 vec![]
3824 }
3825 fn group_instance_count(&self, group_path: &[&str]) -> usize {
3826 match group_path {
3827 ["SG5"] | ["SG5", "SG6"] | ["SG5", "SG6", "SG9"] => 1,
3828 ["SG5", "SG6", "SG9", "SG10"] => 2,
3829 _ => 0,
3830 }
3831 }
3832 }
3833
3834 let sts_a = OwnedSegment {
3835 id: "STS".into(),
3836 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
3837 segment_number: 10,
3838 };
3839 let sts_b = OwnedSegment {
3840 id: "STS".into(),
3841 elements: vec![vec!["Z32".into()], vec![], vec!["E01".into()]],
3842 segment_number: 15,
3843 };
3844 let nav = TwoSg10s {
3845 sts_a: sts_a.clone(),
3846 sts_b: sts_b.clone(),
3847 };
3848
3849 let evaluator = MockEvaluator::all_true(&[]);
3850 let validator = EdifactValidator::new(evaluator);
3851 let external = NoOpExternalProvider;
3852
3853 let workflow = AhbWorkflow {
3854 pruefidentifikator: "13025".to_string(),
3855 description: "Test".to_string(),
3856 communication_direction: None,
3857 ub_definitions: HashMap::new(),
3858 fields: vec![AhbFieldRule {
3859 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3860 name: "Statusanlaß, Code".to_string(),
3861 ahb_status: "X".to_string(),
3862 element_index: Some(2),
3863 component_index: Some(0),
3864 codes: vec![
3865 AhbCodeRule {
3866 value: "E01".into(),
3867 description: "Code 1".into(),
3868 ahb_status: "X [4P0..1]".into(),
3869 },
3870 AhbCodeRule {
3871 value: "E02".into(),
3872 description: "Code 2".into(),
3873 ahb_status: "X [4P0..1]".into(),
3874 },
3875 ],
3876 parent_group_ahb_status: Some("Muss".to_string()),
3877 mig_number: None,
3878 }],
3879 };
3880
3881 let report = validator.validate_with_navigator(
3882 &[sts_a, sts_b],
3883 &workflow,
3884 &external,
3885 ValidationLevel::Full,
3886 &nav,
3887 );
3888 let pkg_errors: Vec<_> = report
3889 .by_category(ValidationCategory::Ahb)
3890 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3891 .collect();
3892 assert!(
3893 pkg_errors.is_empty(),
3894 "Package cardinality is per-instance: 1 code per SG10 rep is within [4P0..1]. Got: {:?}",
3895 pkg_errors
3896 );
3897 }
3898
3899 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
3900 OwnedSegment {
3901 id: id.to_string(),
3902 elements: elements
3903 .into_iter()
3904 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
3905 .collect(),
3906 segment_number: 0,
3907 }
3908 }
3909
3910 #[test]
3911 fn test_unt_count_correct() {
3912 let segments = vec![
3914 make_segment("UNH", vec![vec!["001"]]),
3915 make_segment("BGM", vec![vec!["E01"]]),
3916 make_segment("DTM", vec![vec!["137", "20250401"]]),
3917 make_segment("UNT", vec![vec!["4", "001"]]),
3918 ];
3919 assert!(
3920 validate_unt_segment_count(&segments).is_none(),
3921 "Correct count should produce no issue"
3922 );
3923 }
3924
3925 #[test]
3926 fn test_unt_count_mismatch() {
3927 let segments = vec![
3929 make_segment("UNH", vec![vec!["001"]]),
3930 make_segment("BGM", vec![vec!["E01"]]),
3931 make_segment("UNT", vec![vec!["5", "001"]]),
3932 ];
3933 let issue =
3934 validate_unt_segment_count(&segments).expect("Mismatch should produce an issue");
3935 assert_eq!(issue.code, ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
3936 assert_eq!(issue.severity, Severity::Error);
3937 assert!(issue.message.contains("declared 5"));
3938 assert!(issue.message.contains("actual 3"));
3939 }
3940
3941 #[test]
3942 fn test_unt_count_excludes_envelope() {
3943 let segments = vec![
3945 make_segment("UNA", vec![]),
3946 make_segment("UNB", vec![vec!["UNOC", "3"]]),
3947 make_segment("UNH", vec![vec!["001"]]),
3948 make_segment("BGM", vec![vec!["E01"]]),
3949 make_segment("UNT", vec![vec!["3", "001"]]),
3950 make_segment("UNZ", vec![vec!["1"]]),
3951 ];
3952 assert!(
3953 validate_unt_segment_count(&segments).is_none(),
3954 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
3955 );
3956 }
3957
3958 #[test]
3959 fn test_unt_count_no_unt_returns_none() {
3960 let segments = vec![
3961 make_segment("UNH", vec![vec!["001"]]),
3962 make_segment("BGM", vec![vec!["E01"]]),
3963 ];
3964 assert!(
3965 validate_unt_segment_count(&segments).is_none(),
3966 "No UNT segment should return None (not our problem)"
3967 );
3968 }
3969
3970 #[test]
3971 fn test_unt_count_rejects_multi_message_input() {
3972 let segments = vec![
3974 make_segment("UNH", vec![vec!["001"]]),
3975 make_segment("BGM", vec![vec!["E01"]]),
3976 make_segment("UNT", vec![vec!["3", "001"]]),
3977 make_segment("UNH", vec![vec!["002"]]),
3978 make_segment("BGM", vec![vec!["E02"]]),
3979 make_segment("UNT", vec![vec!["3", "002"]]),
3980 ];
3981 let issue = validate_unt_segment_count(&segments)
3982 .expect("Multi-message input should produce an error");
3983 assert_eq!(issue.code, ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
3984 assert!(
3985 issue.message.contains("2 UNH"),
3986 "Should mention UNH count: {}",
3987 issue.message
3988 );
3989 }
3990
3991 #[test]
3992 fn test_code_validation_accepts_multi_code_variant_qualifier() {
3993 let evaluator = MockEvaluator::new(vec![]);
4000 let validator = EdifactValidator::new(evaluator);
4001 let external = NoOpExternalProvider;
4002
4003 let rff_z39 = OwnedSegment {
4004 id: "RFF".to_string(),
4005 elements: vec![vec!["RFF".to_string()], vec!["Z39".to_string(), "REF1".to_string()]],
4006 segment_number: 1,
4007 };
4008
4009 let workflow = AhbWorkflow {
4010 pruefidentifikator: "55035".to_string(),
4011 description: "Test".to_string(),
4012 communication_direction: None,
4013 fields: vec![
4014 AhbFieldRule {
4016 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
4017 name: "Referenznummer Qualifier".to_string(),
4018 ahb_status: "Muss".to_string(),
4019 codes: vec![
4020 AhbCodeRule {
4021 value: "Z31".to_string(),
4022 description: "".to_string(),
4023 ahb_status: "X".to_string(),
4024 },
4025 AhbCodeRule {
4026 value: "Z39".to_string(),
4027 description: "".to_string(),
4028 ahb_status: "X".to_string(),
4029 },
4030 ],
4031 parent_group_ahb_status: None,
4032 element_index: Some(1),
4033 component_index: Some(0),
4034 mig_number: Some("00075".to_string()),
4035 },
4036 AhbFieldRule {
4038 segment_path: "SG4/SG8/RFF/C506/1153".to_string(),
4039 name: "Referenznummer Qualifier".to_string(),
4040 ahb_status: "Muss".to_string(),
4041 codes: vec![AhbCodeRule {
4042 value: "Z33".to_string(),
4043 description: "".to_string(),
4044 ahb_status: "X".to_string(),
4045 }],
4046 parent_group_ahb_status: None,
4047 element_index: Some(1),
4048 component_index: Some(0),
4049 mig_number: Some("00078".to_string()),
4050 },
4051 ],
4052 ub_definitions: HashMap::new(),
4053 };
4054
4055 let report = validator.validate(
4056 &[rff_z39],
4057 &workflow,
4058 &external,
4059 ValidationLevel::Conditions,
4060 );
4061
4062 let code_errors: Vec<_> = report
4063 .by_category(ValidationCategory::Code)
4064 .filter(|i| {
4065 i.severity == Severity::Error && i.code == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4066 })
4067 .collect();
4068 assert!(
4069 code_errors.is_empty(),
4070 "RFF+Z39 must be accepted (Z39 is valid for mig=00075). Got: {:?}",
4071 code_errors
4072 );
4073 }
4074
4075 #[test]
4076 fn test_code_validation_disambiguates_migs_by_full_code_profile() {
4077 let evaluator = MockEvaluator::new(vec![]);
4083 let validator = EdifactValidator::new(evaluator);
4084 let external = NoOpExternalProvider;
4085
4086 let pia_5_z12 = OwnedSegment {
4090 id: "PIA".to_string(),
4091 elements: vec![vec!["5".to_string()], vec!["Z12".to_string()]],
4092 segment_number: 1,
4093 };
4094 let pia_5_srw = OwnedSegment {
4095 id: "PIA".to_string(),
4096 elements: vec![vec!["5".to_string()], vec!["SRW".to_string()]],
4097 segment_number: 2,
4098 };
4099
4100 let make_rules = |mig: &str, composite_code: &str| {
4101 vec![
4102 AhbFieldRule {
4103 segment_path: "SG4/SG8/PIA/4347".to_string(),
4104 name: "Produkt-ID-Funktion".to_string(),
4105 ahb_status: "Muss".to_string(),
4106 codes: vec![AhbCodeRule {
4107 value: "5".to_string(),
4108 description: "".to_string(),
4109 ahb_status: "X".to_string(),
4110 }],
4111 parent_group_ahb_status: None,
4112 element_index: Some(0),
4113 component_index: Some(0),
4114 mig_number: Some(mig.to_string()),
4115 },
4116 AhbFieldRule {
4117 segment_path: "SG4/SG8/PIA/C212/7143".to_string(),
4118 name: "Artikel/Dienstleistung-Identifikator".to_string(),
4119 ahb_status: "Muss".to_string(),
4120 codes: vec![AhbCodeRule {
4121 value: composite_code.to_string(),
4122 description: "".to_string(),
4123 ahb_status: "X".to_string(),
4124 }],
4125 parent_group_ahb_status: None,
4126 element_index: Some(1),
4127 component_index: Some(0),
4128 mig_number: Some(mig.to_string()),
4129 },
4130 ]
4131 };
4132
4133 let mut fields = make_rules("00108", "Z12");
4134 fields.extend(make_rules("00197", "SRW"));
4135
4136 let workflow = AhbWorkflow {
4137 pruefidentifikator: "55035".to_string(),
4138 description: "Test".to_string(),
4139 communication_direction: None,
4140 fields,
4141 ub_definitions: HashMap::new(),
4142 };
4143
4144 let report = validator.validate(
4145 &[pia_5_z12, pia_5_srw],
4146 &workflow,
4147 &external,
4148 ValidationLevel::Conditions,
4149 );
4150
4151 let code_errors: Vec<_> = report
4152 .by_category(ValidationCategory::Code)
4153 .filter(|i| {
4154 i.severity == Severity::Error && i.code == ErrorCodes::CODE_NOT_ALLOWED_FOR_PID
4155 })
4156 .collect();
4157 assert!(
4158 code_errors.is_empty(),
4159 "Both PIA+5+Z12 (mig=00108) and PIA+5+SRW (mig=00197) must be accepted. Got: {:?}",
4160 code_errors
4161 );
4162 }
4163}