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(
229 validated_tree.pruefidentifikator,
230 external,
231 segments,
232 ),
233 };
234
235 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
236
237 let mut all_nodes: Vec<&AhbNode> = Vec::new();
239 all_nodes.extend(validated_tree.root_fields.iter());
240 for group in &validated_tree.groups {
241 collect_nodes_depth_first(group, &mut all_nodes);
242 }
243
244 for node in &all_nodes {
246 let field = node.rule;
247
248 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
250
251 if let Some(ref group_status) = field.parent_group_ahb_status {
254 if group_status.contains('[') {
255 let group_result = expr_eval.evaluate_status_with_ub(
256 group_status,
257 &ctx,
258 validated_tree.ub_definitions,
259 );
260 if matches!(
261 group_result,
262 ConditionResult::False | ConditionResult::Unknown
263 ) {
264 continue;
265 }
266 }
267 }
268
269 let (condition_result, unknown_ids) = expr_eval.evaluate_status_detailed_with_ub(
272 &field.ahb_status,
273 &node_ctx,
274 validated_tree.ub_definitions,
275 );
276
277 match condition_result {
278 ConditionResult::True => {
279 if is_mandatory_status(&field.ahb_status) && node.value.is_none() {
283 let mut issue = ValidationIssue::new(
284 Severity::Error,
285 ValidationCategory::Ahb,
286 ErrorCodes::MISSING_REQUIRED_FIELD,
287 format!(
288 "Required field '{}' at {} is missing",
289 field.name, field.segment_path
290 ),
291 )
292 .with_field_path(&field.segment_path)
293 .with_rule(&field.ahb_status);
294 if let Some(first_code) = field.codes.first() {
295 issue.expected_value = Some(first_code.value.clone());
296 }
297 report.add_issue(issue);
298 }
299 }
300 ConditionResult::False => {
301 if is_mandatory_status(&field.ahb_status) && node.value.is_some() {
304 report.add_issue(
305 ValidationIssue::new(
306 Severity::Error,
307 ValidationCategory::Ahb,
308 ErrorCodes::CONDITIONAL_RULE_VIOLATION,
309 format!(
310 "Field '{}' at {} is present but does not satisfy condition: {}",
311 field.name, field.segment_path, field.ahb_status
312 ),
313 )
314 .with_field_path(&field.segment_path)
315 .with_rule(&field.ahb_status),
316 );
317 }
318 }
319 ConditionResult::Unknown => {
320 let mut external_ids = Vec::new();
322 let mut undetermined_ids = Vec::new();
323 let mut missing_ids = Vec::new();
324 for id in unknown_ids {
325 if self.evaluator.is_external(id) {
326 external_ids.push(id);
327 } else if self.evaluator.is_known(id) {
328 undetermined_ids.push(id);
329 } else {
330 missing_ids.push(id);
331 }
332 }
333
334 let mut parts = Vec::new();
335 if !external_ids.is_empty() {
336 let ids: Vec<String> =
337 external_ids.iter().map(|id| format!("[{id}]")).collect();
338 parts.push(format!(
339 "external conditions require provider: {}",
340 ids.join(", ")
341 ));
342 }
343 if !undetermined_ids.is_empty() {
344 let ids: Vec<String> = undetermined_ids
345 .iter()
346 .map(|id| format!("[{id}]"))
347 .collect();
348 parts.push(format!(
349 "conditions could not be determined from message data: {}",
350 ids.join(", ")
351 ));
352 }
353 if !missing_ids.is_empty() {
354 let ids: Vec<String> =
355 missing_ids.iter().map(|id| format!("[{id}]")).collect();
356 parts.push(format!("missing conditions: {}", ids.join(", ")));
357 }
358 let detail = if parts.is_empty() {
359 String::new()
360 } else {
361 format!(" ({})", parts.join("; "))
362 };
363 report.add_issue(
364 ValidationIssue::new(
365 Severity::Info,
366 ValidationCategory::Ahb,
367 ErrorCodes::CONDITION_UNKNOWN,
368 format!(
369 "Condition for field '{}' could not be fully evaluated{}",
370 field.name, detail
371 ),
372 )
373 .with_field_path(&field.segment_path)
374 .with_rule(&field.ahb_status),
375 );
376 }
377 }
378 }
379
380 report
387 }
388
389 fn validate_conditions(
391 &self,
392 workflow: &AhbWorkflow,
393 ctx: &EvaluationContext,
394 report: &mut ValidationReport,
395 ) {
396 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
397
398 for field in &workflow.fields {
399 if let Some(ref group_status) = field.parent_group_ahb_status {
406 if group_status.contains('[') {
407 let group_result = expr_eval.evaluate_status_with_ub(
408 group_status,
409 ctx,
410 &workflow.ub_definitions,
411 );
412 if matches!(
413 group_result,
414 ConditionResult::False | ConditionResult::Unknown
415 ) {
416 continue;
417 }
418 }
419 }
420
421 let (condition_result, unknown_ids) = expr_eval.evaluate_status_detailed_with_ub(
424 &field.ahb_status,
425 ctx,
426 &workflow.ub_definitions,
427 );
428
429 match condition_result {
430 ConditionResult::True => {
431 if is_mandatory_status(&field.ahb_status)
433 && !is_field_present(ctx, field)
434 && !is_group_variant_absent(ctx, field)
435 {
436 let mut issue = ValidationIssue::new(
437 Severity::Error,
438 ValidationCategory::Ahb,
439 ErrorCodes::MISSING_REQUIRED_FIELD,
440 format!(
441 "Required field '{}' at {} is missing",
442 field.name, field.segment_path
443 ),
444 )
445 .with_field_path(&field.segment_path)
446 .with_rule(&field.ahb_status);
447 if let Some(first_code) = field.codes.first() {
451 issue.expected_value = Some(first_code.value.clone());
452 }
453 report.add_issue(issue);
454 }
455 }
456 ConditionResult::False => {
457 if is_mandatory_status(&field.ahb_status)
463 && is_field_present(ctx, field)
464 {
465 report.add_issue(
466 ValidationIssue::new(
467 Severity::Error,
468 ValidationCategory::Ahb,
469 ErrorCodes::CONDITIONAL_RULE_VIOLATION,
470 format!(
471 "Field '{}' at {} is present but does not satisfy condition: {}",
472 field.name, field.segment_path, field.ahb_status
473 ),
474 )
475 .with_field_path(&field.segment_path)
476 .with_rule(&field.ahb_status),
477 );
478 }
479 }
480 ConditionResult::Unknown => {
481 let mut external_ids = Vec::new();
486 let mut undetermined_ids = Vec::new();
487 let mut missing_ids = Vec::new();
488 for id in unknown_ids {
489 if self.evaluator.is_external(id) {
490 external_ids.push(id);
491 } else if self.evaluator.is_known(id) {
492 undetermined_ids.push(id);
493 } else {
494 missing_ids.push(id);
495 }
496 }
497
498 let mut parts = Vec::new();
499 if !external_ids.is_empty() {
500 let ids: Vec<String> =
501 external_ids.iter().map(|id| format!("[{id}]")).collect();
502 parts.push(format!(
503 "external conditions require provider: {}",
504 ids.join(", ")
505 ));
506 }
507 if !undetermined_ids.is_empty() {
508 let ids: Vec<String> = undetermined_ids
509 .iter()
510 .map(|id| format!("[{id}]"))
511 .collect();
512 parts.push(format!(
513 "conditions could not be determined from message data: {}",
514 ids.join(", ")
515 ));
516 }
517 if !missing_ids.is_empty() {
518 let ids: Vec<String> =
519 missing_ids.iter().map(|id| format!("[{id}]")).collect();
520 parts.push(format!("missing conditions: {}", ids.join(", ")));
521 }
522 let detail = if parts.is_empty() {
523 String::new()
524 } else {
525 format!(" ({})", parts.join("; "))
526 };
527 report.add_issue(
528 ValidationIssue::new(
529 Severity::Info,
530 ValidationCategory::Ahb,
531 ErrorCodes::CONDITION_UNKNOWN,
532 format!(
533 "Condition for field '{}' could not be fully evaluated{}",
534 field.name, detail
535 ),
536 )
537 .with_field_path(&field.segment_path)
538 .with_rule(&field.ahb_status),
539 );
540 }
541 }
542 }
543
544 self.validate_codes_cross_field(workflow, ctx, report);
549
550 self.validate_package_cardinality(workflow, ctx, report);
553 }
554
555 fn validate_package_cardinality(
562 &self,
563 workflow: &AhbWorkflow,
564 ctx: &EvaluationContext,
565 report: &mut ValidationReport,
566 ) {
567 struct PackageGroup {
570 min: u32,
571 max: u32,
572 code_values: Vec<String>,
573 element_index: usize,
574 component_index: usize,
575 }
576
577 let mut groups: HashMap<(String, u32), PackageGroup> = HashMap::new();
579
580 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
581
582 for field in &workflow.fields {
583 let el_idx = field.element_index.unwrap_or(0);
584 let comp_idx = field.component_index.unwrap_or(0);
585
586 if let Some(ref group_status) = field.parent_group_ahb_status {
590 if group_status.contains('[') {
591 let group_result = expr_eval.evaluate_status_with_ub(
592 group_status,
593 ctx,
594 &workflow.ub_definitions,
595 );
596 if matches!(
597 group_result,
598 ConditionResult::False | ConditionResult::Unknown
599 ) {
600 continue;
601 }
602 }
603 }
604
605 for code in &field.codes {
606 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
608 let mut packages = Vec::new();
610 collect_packages(&expr, &mut packages);
611
612 for (pkg_id, pkg_min, pkg_max) in packages {
613 let key = (field.segment_path.clone(), pkg_id);
614 let group = groups.entry(key).or_insert_with(|| PackageGroup {
615 min: pkg_min,
616 max: pkg_max,
617 code_values: Vec::new(),
618 element_index: el_idx,
619 component_index: comp_idx,
620 });
621 group.min = group.min.max(pkg_min);
624 group.max = group.max.min(pkg_max);
625 group.code_values.push(code.value.clone());
626 }
627 }
628 }
629 }
630
631 for ((seg_path, pkg_id), group) in &groups {
633 let segment_id = extract_segment_id(seg_path);
634 let segments = ctx.find_segments(&segment_id);
635
636 let mut present_count: usize = 0;
638 for seg in &segments {
639 if let Some(value) = seg
640 .elements
641 .get(group.element_index)
642 .and_then(|e| e.get(group.component_index))
643 .filter(|v| !v.is_empty())
644 {
645 if group.code_values.contains(value) {
646 present_count += 1;
647 }
648 }
649 }
650
651 let min = group.min as usize;
652 let max = group.max as usize;
653
654 if present_count < min || present_count > max {
655 let code_list = group.code_values.join(", ");
656 report.add_issue(
657 ValidationIssue::new(
658 Severity::Error,
659 ValidationCategory::Ahb,
660 ErrorCodes::PACKAGE_CARDINALITY_VIOLATION,
661 format!(
662 "Package [{}P{}..{}] at {}: {} code(s) present (allowed {}..{}). Codes in package: [{}]",
663 pkg_id, group.min, group.max, seg_path, present_count, group.min, group.max, code_list
664 ),
665 )
666 .with_field_path(seg_path)
667 .with_expected(format!("{}..{}", group.min, group.max))
668 .with_actual(present_count.to_string()),
669 );
670 }
671 }
672 }
673
674 fn validate_codes_cross_field(
686 &self,
687 workflow: &AhbWorkflow,
688 ctx: &EvaluationContext,
689 report: &mut ValidationReport,
690 ) {
691 if ctx.navigator.is_some() {
692 self.validate_codes_group_scoped(workflow, ctx, report);
693 } else {
694 self.validate_codes_tag_scoped(workflow, ctx, report);
695 }
696 }
697
698 fn validate_codes_group_scoped(
701 &self,
702 workflow: &AhbWorkflow,
703 ctx: &EvaluationContext,
704 report: &mut ValidationReport,
705 ) {
706 type CodeKey = (String, String, usize, usize);
709 let mut codes_by_group: HashMap<CodeKey, HashSet<&str>> = HashMap::new();
710
711 for field in &workflow.fields {
712 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
713 continue;
714 }
715 let tag = extract_segment_id(&field.segment_path);
716 let group_key = extract_group_path_key(&field.segment_path);
717 let el_idx = field.element_index.unwrap_or(0);
718 let comp_idx = field.component_index.unwrap_or(0);
719 let entry = codes_by_group
720 .entry((group_key, tag, el_idx, comp_idx))
721 .or_default();
722 for code in &field.codes {
723 if code.ahb_status == "X" || code.ahb_status.starts_with("Muss") {
724 entry.insert(&code.value);
725 }
726 }
727 }
728
729 let nav = ctx.navigator.unwrap();
730
731 for ((group_key, tag, el_idx, comp_idx), allowed_codes) in &codes_by_group {
732 if allowed_codes.is_empty() {
733 continue;
734 }
735
736 let group_path: Vec<&str> = if group_key.is_empty() {
737 Vec::new()
738 } else {
739 group_key.split('/').collect()
740 };
741
742 if group_path.is_empty() {
744 Self::check_segments_against_codes(
746 ctx.find_segments(tag),
747 allowed_codes,
748 tag,
749 *el_idx,
750 *comp_idx,
751 &format!("{tag}/qualifier"),
752 report,
753 );
754 } else {
755 let instance_count = nav.group_instance_count(&group_path);
756 for i in 0..instance_count {
757 let segments = nav.find_segments_in_group(tag, &group_path, i);
758 let refs: Vec<&OwnedSegment> = segments.iter().collect();
759 Self::check_segments_against_codes(
760 refs,
761 allowed_codes,
762 tag,
763 *el_idx,
764 *comp_idx,
765 &format!("{group_key}/{tag}/qualifier"),
766 report,
767 );
768 }
769 }
770 }
771 }
772
773 fn validate_codes_tag_scoped(
776 &self,
777 workflow: &AhbWorkflow,
778 ctx: &EvaluationContext,
779 report: &mut ValidationReport,
780 ) {
781 let mut codes_by_tag: HashMap<(String, usize, usize), HashSet<&str>> = HashMap::new();
783
784 for field in &workflow.fields {
785 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
786 continue;
787 }
788 let tag = extract_segment_id(&field.segment_path);
789 let el_idx = field.element_index.unwrap_or(0);
790 let comp_idx = field.component_index.unwrap_or(0);
791 let entry = codes_by_tag.entry((tag, el_idx, comp_idx)).or_default();
792 for code in &field.codes {
793 if code.ahb_status == "X" || code.ahb_status.starts_with("Muss") {
794 entry.insert(&code.value);
795 }
796 }
797 }
798
799 for ((tag, el_idx, comp_idx), allowed_codes) in &codes_by_tag {
800 if allowed_codes.is_empty() {
801 continue;
802 }
803 Self::check_segments_against_codes(
804 ctx.find_segments(tag),
805 allowed_codes,
806 tag,
807 *el_idx,
808 *comp_idx,
809 &format!("{tag}/qualifier"),
810 report,
811 );
812 }
813 }
814
815 fn check_segments_against_codes(
817 segments: Vec<&OwnedSegment>,
818 allowed_codes: &HashSet<&str>,
819 _tag: &str,
820 el_idx: usize,
821 comp_idx: usize,
822 field_path: &str,
823 report: &mut ValidationReport,
824 ) {
825 for segment in segments {
826 if let Some(code_value) = segment
827 .elements
828 .get(el_idx)
829 .and_then(|e| e.get(comp_idx))
830 .filter(|v| !v.is_empty())
831 {
832 if !allowed_codes.contains(code_value.as_str()) {
833 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
834 sorted_codes.sort_unstable();
835 report.add_issue(
836 ValidationIssue::new(
837 Severity::Error,
838 ValidationCategory::Code,
839 ErrorCodes::CODE_NOT_ALLOWED_FOR_PID,
840 format!(
841 "Code '{}' is not allowed for this PID. Allowed: [{}]",
842 code_value,
843 sorted_codes.join(", ")
844 ),
845 )
846 .with_field_path(field_path)
847 .with_actual(code_value)
848 .with_expected(sorted_codes.join(", ")),
849 );
850 }
851 }
852 }
853 }
854}
855
856fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
865 let segment_id = extract_segment_id(&field.segment_path);
866
867 if !field.codes.is_empty() {
873 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
874 let required_codes: Vec<&str> =
875 field.codes.iter().map(|c| c.value.as_str()).collect();
876 let matching = ctx.find_segments(&segment_id);
877 return matching.iter().any(|seg| {
878 seg.elements
879 .get(el_idx)
880 .and_then(|e| e.get(comp_idx))
881 .is_some_and(|v| required_codes.contains(&v.as_str()))
882 });
883 }
884 if is_qualifier_field(&field.segment_path) {
887 let required_codes: Vec<&str> =
888 field.codes.iter().map(|c| c.value.as_str()).collect();
889 let el_idx = field.element_index.unwrap_or(0);
890 let comp_idx = field.component_index.unwrap_or(0);
891 let matching = ctx.find_segments(&segment_id);
892 return matching.iter().any(|seg| {
893 seg.elements
894 .get(el_idx)
895 .and_then(|e| e.get(comp_idx))
896 .is_some_and(|v| required_codes.contains(&v.as_str()))
897 });
898 }
899 }
900
901 ctx.has_segment(&segment_id)
902}
903
904fn is_group_variant_absent(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
919 let group_path: Vec<&str> = field
920 .segment_path
921 .split('/')
922 .take_while(|p| p.starts_with("SG"))
923 .collect();
924
925 if group_path.is_empty() {
926 return false;
927 }
928
929 let nav = match ctx.navigator {
930 Some(nav) => nav,
931 None => return false,
932 };
933
934 let instance_count = nav.group_instance_count(&group_path);
935
936 if instance_count == 0 {
941 let is_group_mandatory = field
942 .parent_group_ahb_status
943 .as_deref()
944 .is_some_and(is_mandatory_status);
945 if !is_group_mandatory {
946 return true;
947 }
948 return false;
950 }
951
952 if let Some(ref group_status) = field.parent_group_ahb_status {
956 if !is_mandatory_status(group_status) && !group_status.contains('[') {
957 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
960 let segment_id = extract_segment_id(&field.segment_path);
961 let required_codes: Vec<&str> =
962 field.codes.iter().map(|c| c.value.as_str()).collect();
963
964 let any_instance_has_qualifier = (0..instance_count).any(|i| {
965 nav.find_segments_in_group(&segment_id, &group_path, i)
966 .iter()
967 .any(|seg| {
968 seg.elements
969 .first()
970 .and_then(|e| e.first())
971 .is_some_and(|v| required_codes.contains(&v.as_str()))
972 })
973 });
974
975 if !any_instance_has_qualifier {
976 return true; }
978 }
979 }
980 }
981
982 let segment_id = extract_segment_id(&field.segment_path);
991 let segment_absent_from_all = (0..instance_count).all(|i| {
992 nav.find_segments_in_group(&segment_id, &group_path, i)
993 .is_empty()
994 });
995 if segment_absent_from_all {
996 let group_has_other_segments =
997 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
998 if group_has_other_segments {
999 return true;
1000 }
1001 }
1002
1003 false
1004}
1005
1006fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1008 out.extend(group.fields.iter());
1009 for child in &group.children {
1010 collect_nodes_depth_first(child, out);
1011 }
1012}
1013
1014fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
1016 match expr {
1017 ConditionExpr::Package { id, min, max } => {
1018 out.push((*id, *min, *max));
1019 }
1020 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1021 for e in exprs {
1022 collect_packages(e, out);
1023 }
1024 }
1025 ConditionExpr::Xor(left, right) => {
1026 collect_packages(left, out);
1027 collect_packages(right, out);
1028 }
1029 ConditionExpr::Not(inner) => {
1030 collect_packages(inner, out);
1031 }
1032 ConditionExpr::Ref(_) => {}
1033 }
1034}
1035
1036fn is_mandatory_status(status: &str) -> bool {
1038 let trimmed = status.trim();
1039 trimmed.starts_with("Muss") || trimmed.starts_with('X')
1040}
1041
1042fn is_qualifier_field(path: &str) -> bool {
1051 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
1052 parts.len() == 2
1055}
1056
1057fn extract_group_path_key(path: &str) -> String {
1062 let sg_parts: Vec<&str> = path
1063 .split('/')
1064 .take_while(|p| p.starts_with("SG"))
1065 .collect();
1066 sg_parts.join("/")
1067}
1068
1069fn extract_segment_id(path: &str) -> String {
1071 for part in path.split('/') {
1072 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
1074 continue;
1075 }
1076 if part.len() >= 3
1078 && part
1079 .chars()
1080 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
1081 {
1082 return part.to_string();
1083 }
1084 }
1085 path.split('/').next_back().unwrap_or(path).to_string()
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
1093 use std::collections::HashMap;
1094
1095 struct MockEvaluator {
1097 results: HashMap<u32, CR>,
1098 }
1099
1100 impl MockEvaluator {
1101 fn new(results: Vec<(u32, CR)>) -> Self {
1102 Self {
1103 results: results.into_iter().collect(),
1104 }
1105 }
1106
1107 fn all_true(ids: &[u32]) -> Self {
1108 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
1109 }
1110 }
1111
1112 impl ConditionEvaluator for MockEvaluator {
1113 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
1114 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
1115 }
1116 fn is_external(&self, _condition: u32) -> bool {
1117 false
1118 }
1119 fn message_type(&self) -> &str {
1120 "UTILMD"
1121 }
1122 fn format_version(&self) -> &str {
1123 "FV2510"
1124 }
1125 }
1126
1127 #[test]
1130 fn test_is_mandatory_status() {
1131 assert!(is_mandatory_status("Muss"));
1132 assert!(is_mandatory_status("Muss [182] ∧ [152]"));
1133 assert!(is_mandatory_status("X"));
1134 assert!(is_mandatory_status("X [567]"));
1135 assert!(!is_mandatory_status("Soll [1]"));
1136 assert!(!is_mandatory_status("Kann [1]"));
1137 assert!(!is_mandatory_status(""));
1138 }
1139
1140 #[test]
1141 fn test_extract_segment_id_simple() {
1142 assert_eq!(extract_segment_id("NAD"), "NAD");
1143 }
1144
1145 #[test]
1146 fn test_extract_segment_id_with_sg_prefix() {
1147 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
1148 }
1149
1150 #[test]
1151 fn test_extract_segment_id_nested_sg() {
1152 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
1153 }
1154
1155 #[test]
1158 fn test_validate_missing_mandatory_field() {
1159 let evaluator = MockEvaluator::all_true(&[182, 152]);
1160 let validator = EdifactValidator::new(evaluator);
1161 let external = NoOpExternalProvider;
1162
1163 let workflow = AhbWorkflow {
1164 pruefidentifikator: "11001".to_string(),
1165 description: "Test".to_string(),
1166 communication_direction: None,
1167 fields: vec![AhbFieldRule {
1168 segment_path: "SG2/NAD/C082/3039".to_string(),
1169 name: "MP-ID des MSB".to_string(),
1170 ahb_status: "Muss [182] ∧ [152]".to_string(),
1171 codes: vec![],
1172 parent_group_ahb_status: None,
1173 ..Default::default()
1174 }],
1175 ub_definitions: HashMap::new(),
1176 };
1177
1178 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1180
1181 assert!(!report.is_valid());
1183 let errors: Vec<_> = report.errors().collect();
1184 assert_eq!(errors.len(), 1);
1185 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1186 assert!(errors[0].message.contains("MP-ID des MSB"));
1187 }
1188
1189 #[test]
1190 fn test_validate_condition_false_no_error() {
1191 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
1193 let validator = EdifactValidator::new(evaluator);
1194 let external = NoOpExternalProvider;
1195
1196 let workflow = AhbWorkflow {
1197 pruefidentifikator: "11001".to_string(),
1198 description: "Test".to_string(),
1199 communication_direction: None,
1200 fields: vec![AhbFieldRule {
1201 segment_path: "NAD".to_string(),
1202 name: "Partnerrolle".to_string(),
1203 ahb_status: "Muss [182] ∧ [152]".to_string(),
1204 codes: vec![],
1205 parent_group_ahb_status: None,
1206 ..Default::default()
1207 }],
1208 ub_definitions: HashMap::new(),
1209 };
1210
1211 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1212
1213 assert!(report.is_valid());
1215 }
1216
1217 #[test]
1218 fn test_validate_condition_unknown_adds_info() {
1219 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
1221 let validator = EdifactValidator::new(evaluator);
1223 let external = NoOpExternalProvider;
1224
1225 let workflow = AhbWorkflow {
1226 pruefidentifikator: "11001".to_string(),
1227 description: "Test".to_string(),
1228 communication_direction: None,
1229 fields: vec![AhbFieldRule {
1230 segment_path: "NAD".to_string(),
1231 name: "Partnerrolle".to_string(),
1232 ahb_status: "Muss [182] ∧ [152]".to_string(),
1233 codes: vec![],
1234 parent_group_ahb_status: None,
1235 ..Default::default()
1236 }],
1237 ub_definitions: HashMap::new(),
1238 };
1239
1240 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1241
1242 assert!(report.is_valid());
1244 let infos: Vec<_> = report.infos().collect();
1245 assert_eq!(infos.len(), 1);
1246 assert_eq!(infos[0].code, ErrorCodes::CONDITION_UNKNOWN);
1247 }
1248
1249 #[test]
1250 fn test_validate_structure_level_skips_conditions() {
1251 let evaluator = MockEvaluator::all_true(&[182, 152]);
1252 let validator = EdifactValidator::new(evaluator);
1253 let external = NoOpExternalProvider;
1254
1255 let workflow = AhbWorkflow {
1256 pruefidentifikator: "11001".to_string(),
1257 description: "Test".to_string(),
1258 communication_direction: None,
1259 fields: vec![AhbFieldRule {
1260 segment_path: "NAD".to_string(),
1261 name: "Partnerrolle".to_string(),
1262 ahb_status: "Muss [182] ∧ [152]".to_string(),
1263 codes: vec![],
1264 parent_group_ahb_status: None,
1265 ..Default::default()
1266 }],
1267 ub_definitions: HashMap::new(),
1268 };
1269
1270 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
1272
1273 assert!(report.is_valid());
1275 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
1276 }
1277
1278 #[test]
1279 fn test_validate_empty_workflow_no_condition_errors() {
1280 let evaluator = MockEvaluator::all_true(&[]);
1281 let validator = EdifactValidator::new(evaluator);
1282 let external = NoOpExternalProvider;
1283
1284 let empty_workflow = AhbWorkflow {
1285 pruefidentifikator: String::new(),
1286 description: String::new(),
1287 communication_direction: None,
1288 fields: vec![],
1289 ub_definitions: HashMap::new(),
1290 };
1291
1292 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
1293
1294 assert!(report.is_valid());
1295 }
1296
1297 #[test]
1298 fn test_validate_bare_muss_always_required() {
1299 let evaluator = MockEvaluator::new(vec![]);
1300 let validator = EdifactValidator::new(evaluator);
1301 let external = NoOpExternalProvider;
1302
1303 let workflow = AhbWorkflow {
1304 pruefidentifikator: "55001".to_string(),
1305 description: "Test".to_string(),
1306 communication_direction: Some("NB an LF".to_string()),
1307 fields: vec![AhbFieldRule {
1308 segment_path: "SG2/NAD/3035".to_string(),
1309 name: "Partnerrolle".to_string(),
1310 ahb_status: "Muss".to_string(), codes: vec![],
1312 parent_group_ahb_status: None,
1313 ..Default::default()
1314 }],
1315 ub_definitions: HashMap::new(),
1316 };
1317
1318 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1319
1320 assert!(!report.is_valid());
1322 assert_eq!(report.error_count(), 1);
1323 }
1324
1325 #[test]
1326 fn test_validate_x_status_is_mandatory() {
1327 let evaluator = MockEvaluator::new(vec![]);
1328 let validator = EdifactValidator::new(evaluator);
1329 let external = NoOpExternalProvider;
1330
1331 let workflow = AhbWorkflow {
1332 pruefidentifikator: "55001".to_string(),
1333 description: "Test".to_string(),
1334 communication_direction: None,
1335 fields: vec![AhbFieldRule {
1336 segment_path: "DTM".to_string(),
1337 name: "Datum".to_string(),
1338 ahb_status: "X".to_string(),
1339 codes: vec![],
1340 parent_group_ahb_status: None,
1341 ..Default::default()
1342 }],
1343 ub_definitions: HashMap::new(),
1344 };
1345
1346 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1347
1348 assert!(!report.is_valid());
1349 let errors: Vec<_> = report.errors().collect();
1350 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1351 }
1352
1353 #[test]
1354 fn test_validate_soll_not_mandatory() {
1355 let evaluator = MockEvaluator::new(vec![]);
1356 let validator = EdifactValidator::new(evaluator);
1357 let external = NoOpExternalProvider;
1358
1359 let workflow = AhbWorkflow {
1360 pruefidentifikator: "55001".to_string(),
1361 description: "Test".to_string(),
1362 communication_direction: None,
1363 fields: vec![AhbFieldRule {
1364 segment_path: "DTM".to_string(),
1365 name: "Datum".to_string(),
1366 ahb_status: "Soll".to_string(),
1367 codes: vec![],
1368 parent_group_ahb_status: None,
1369 ..Default::default()
1370 }],
1371 ub_definitions: HashMap::new(),
1372 };
1373
1374 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1375
1376 assert!(report.is_valid());
1378 }
1379
1380 #[test]
1381 fn test_report_includes_metadata() {
1382 let evaluator = MockEvaluator::new(vec![]);
1383 let validator = EdifactValidator::new(evaluator);
1384 let external = NoOpExternalProvider;
1385
1386 let workflow = AhbWorkflow {
1387 pruefidentifikator: "55001".to_string(),
1388 description: String::new(),
1389 communication_direction: None,
1390 fields: vec![],
1391 ub_definitions: HashMap::new(),
1392 };
1393
1394 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
1395
1396 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
1397 assert_eq!(report.level, ValidationLevel::Full);
1398 assert_eq!(report.message_type, "UTILMD");
1399 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
1400 }
1401
1402 #[test]
1403 fn test_validate_with_navigator_returns_report() {
1404 let evaluator = MockEvaluator::all_true(&[]);
1405 let validator = EdifactValidator::new(evaluator);
1406 let external = NoOpExternalProvider;
1407 let nav = crate::eval::NoOpGroupNavigator;
1408
1409 let workflow = AhbWorkflow {
1410 pruefidentifikator: "55001".to_string(),
1411 description: "Test".to_string(),
1412 communication_direction: None,
1413 fields: vec![],
1414 ub_definitions: HashMap::new(),
1415 };
1416
1417 let report = validator.validate_with_navigator(
1418 &[],
1419 &workflow,
1420 &external,
1421 ValidationLevel::Full,
1422 &nav,
1423 );
1424 assert!(report.is_valid());
1425 }
1426
1427 #[test]
1428 fn test_code_validation_skips_composite_paths() {
1429 let evaluator = MockEvaluator::new(vec![]);
1433 let validator = EdifactValidator::new(evaluator);
1434 let external = NoOpExternalProvider;
1435
1436 let unh_segment = OwnedSegment {
1437 id: "UNH".to_string(),
1438 elements: vec![
1439 vec!["ALEXANDE951842".to_string()], vec![
1441 "UTILMD".to_string(),
1442 "D".to_string(),
1443 "11A".to_string(),
1444 "UN".to_string(),
1445 "S2.1".to_string(),
1446 ],
1447 ],
1448 segment_number: 1,
1449 };
1450
1451 let workflow = AhbWorkflow {
1452 pruefidentifikator: "55001".to_string(),
1453 description: "Test".to_string(),
1454 communication_direction: None,
1455 fields: vec![
1456 AhbFieldRule {
1457 segment_path: "UNH/S009/0065".to_string(),
1458 name: "Nachrichtentyp".to_string(),
1459 ahb_status: "X".to_string(),
1460 codes: vec![AhbCodeRule {
1461 value: "UTILMD".to_string(),
1462 description: "Stammdaten".to_string(),
1463 ahb_status: "X".to_string(),
1464 }],
1465 parent_group_ahb_status: None,
1466 ..Default::default()
1467 },
1468 AhbFieldRule {
1469 segment_path: "UNH/S009/0052".to_string(),
1470 name: "Version".to_string(),
1471 ahb_status: "X".to_string(),
1472 codes: vec![AhbCodeRule {
1473 value: "D".to_string(),
1474 description: "Draft".to_string(),
1475 ahb_status: "X".to_string(),
1476 }],
1477 parent_group_ahb_status: None,
1478 ..Default::default()
1479 },
1480 ],
1481 ub_definitions: HashMap::new(),
1482 };
1483
1484 let report = validator.validate(
1485 &[unh_segment],
1486 &workflow,
1487 &external,
1488 ValidationLevel::Conditions,
1489 );
1490
1491 let code_errors: Vec<_> = report
1493 .by_category(ValidationCategory::Code)
1494 .filter(|i| i.severity == Severity::Error)
1495 .collect();
1496 assert!(
1497 code_errors.is_empty(),
1498 "Expected no code errors for composite paths, got: {:?}",
1499 code_errors
1500 );
1501 }
1502
1503 #[test]
1504 fn test_cross_field_code_validation_valid_qualifiers() {
1505 let evaluator = MockEvaluator::new(vec![]);
1508 let validator = EdifactValidator::new(evaluator);
1509 let external = NoOpExternalProvider;
1510
1511 let nad_ms = OwnedSegment {
1512 id: "NAD".to_string(),
1513 elements: vec![vec!["MS".to_string()]],
1514 segment_number: 4,
1515 };
1516 let nad_mr = OwnedSegment {
1517 id: "NAD".to_string(),
1518 elements: vec![vec!["MR".to_string()]],
1519 segment_number: 5,
1520 };
1521
1522 let workflow = AhbWorkflow {
1523 pruefidentifikator: "55001".to_string(),
1524 description: "Test".to_string(),
1525 communication_direction: None,
1526 fields: vec![
1527 AhbFieldRule {
1528 segment_path: "SG2/NAD/3035".to_string(),
1529 name: "Absender".to_string(),
1530 ahb_status: "X".to_string(),
1531 codes: vec![AhbCodeRule {
1532 value: "MS".to_string(),
1533 description: "Absender".to_string(),
1534 ahb_status: "X".to_string(),
1535 }],
1536 parent_group_ahb_status: None,
1537 ..Default::default()
1538 },
1539 AhbFieldRule {
1540 segment_path: "SG2/NAD/3035".to_string(),
1541 name: "Empfaenger".to_string(),
1542 ahb_status: "X".to_string(),
1543 codes: vec![AhbCodeRule {
1544 value: "MR".to_string(),
1545 description: "Empfaenger".to_string(),
1546 ahb_status: "X".to_string(),
1547 }],
1548 parent_group_ahb_status: None,
1549 ..Default::default()
1550 },
1551 ],
1552 ub_definitions: HashMap::new(),
1553 };
1554
1555 let report = validator.validate(
1556 &[nad_ms, nad_mr],
1557 &workflow,
1558 &external,
1559 ValidationLevel::Conditions,
1560 );
1561
1562 let code_errors: Vec<_> = report
1563 .by_category(ValidationCategory::Code)
1564 .filter(|i| i.severity == Severity::Error)
1565 .collect();
1566 assert!(
1567 code_errors.is_empty(),
1568 "Expected no code errors for valid qualifiers, got: {:?}",
1569 code_errors
1570 );
1571 }
1572
1573 #[test]
1574 fn test_cross_field_code_validation_catches_invalid_qualifier() {
1575 let evaluator = MockEvaluator::new(vec![]);
1577 let validator = EdifactValidator::new(evaluator);
1578 let external = NoOpExternalProvider;
1579
1580 let nad_ms = OwnedSegment {
1581 id: "NAD".to_string(),
1582 elements: vec![vec!["MS".to_string()]],
1583 segment_number: 4,
1584 };
1585 let nad_mt = OwnedSegment {
1586 id: "NAD".to_string(),
1587 elements: vec![vec!["MT".to_string()]], segment_number: 5,
1589 };
1590
1591 let workflow = AhbWorkflow {
1592 pruefidentifikator: "55001".to_string(),
1593 description: "Test".to_string(),
1594 communication_direction: None,
1595 fields: vec![
1596 AhbFieldRule {
1597 segment_path: "SG2/NAD/3035".to_string(),
1598 name: "Absender".to_string(),
1599 ahb_status: "X".to_string(),
1600 codes: vec![AhbCodeRule {
1601 value: "MS".to_string(),
1602 description: "Absender".to_string(),
1603 ahb_status: "X".to_string(),
1604 }],
1605 parent_group_ahb_status: None,
1606 ..Default::default()
1607 },
1608 AhbFieldRule {
1609 segment_path: "SG2/NAD/3035".to_string(),
1610 name: "Empfaenger".to_string(),
1611 ahb_status: "X".to_string(),
1612 codes: vec![AhbCodeRule {
1613 value: "MR".to_string(),
1614 description: "Empfaenger".to_string(),
1615 ahb_status: "X".to_string(),
1616 }],
1617 parent_group_ahb_status: None,
1618 ..Default::default()
1619 },
1620 ],
1621 ub_definitions: HashMap::new(),
1622 };
1623
1624 let report = validator.validate(
1625 &[nad_ms, nad_mt],
1626 &workflow,
1627 &external,
1628 ValidationLevel::Conditions,
1629 );
1630
1631 let code_errors: Vec<_> = report
1632 .by_category(ValidationCategory::Code)
1633 .filter(|i| i.severity == Severity::Error)
1634 .collect();
1635 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
1636 assert!(code_errors[0].message.contains("MT"));
1637 assert!(code_errors[0].message.contains("MR"));
1638 assert!(code_errors[0].message.contains("MS"));
1639 }
1640
1641 #[test]
1642 fn test_cross_field_code_validation_unions_across_groups() {
1643 let evaluator = MockEvaluator::new(vec![]);
1647 let validator = EdifactValidator::new(evaluator);
1648 let external = NoOpExternalProvider;
1649
1650 let segments = vec![
1651 OwnedSegment {
1652 id: "NAD".to_string(),
1653 elements: vec![vec!["MS".to_string()]],
1654 segment_number: 3,
1655 },
1656 OwnedSegment {
1657 id: "NAD".to_string(),
1658 elements: vec![vec!["MR".to_string()]],
1659 segment_number: 4,
1660 },
1661 OwnedSegment {
1662 id: "NAD".to_string(),
1663 elements: vec![vec!["Z04".to_string()]],
1664 segment_number: 20,
1665 },
1666 OwnedSegment {
1667 id: "NAD".to_string(),
1668 elements: vec![vec!["Z09".to_string()]],
1669 segment_number: 21,
1670 },
1671 OwnedSegment {
1672 id: "NAD".to_string(),
1673 elements: vec![vec!["MT".to_string()]], segment_number: 22,
1675 },
1676 ];
1677
1678 let workflow = AhbWorkflow {
1679 pruefidentifikator: "55001".to_string(),
1680 description: "Test".to_string(),
1681 communication_direction: None,
1682 fields: vec![
1683 AhbFieldRule {
1684 segment_path: "SG2/NAD/3035".to_string(),
1685 name: "Absender".to_string(),
1686 ahb_status: "X".to_string(),
1687 codes: vec![AhbCodeRule {
1688 value: "MS".to_string(),
1689 description: "Absender".to_string(),
1690 ahb_status: "X".to_string(),
1691 }],
1692 parent_group_ahb_status: None,
1693 ..Default::default()
1694 },
1695 AhbFieldRule {
1696 segment_path: "SG2/NAD/3035".to_string(),
1697 name: "Empfaenger".to_string(),
1698 ahb_status: "X".to_string(),
1699 codes: vec![AhbCodeRule {
1700 value: "MR".to_string(),
1701 description: "Empfaenger".to_string(),
1702 ahb_status: "X".to_string(),
1703 }],
1704 parent_group_ahb_status: None,
1705 ..Default::default()
1706 },
1707 AhbFieldRule {
1708 segment_path: "SG4/SG12/NAD/3035".to_string(),
1709 name: "Anschlussnutzer".to_string(),
1710 ahb_status: "X".to_string(),
1711 codes: vec![AhbCodeRule {
1712 value: "Z04".to_string(),
1713 description: "Anschlussnutzer".to_string(),
1714 ahb_status: "X".to_string(),
1715 }],
1716 parent_group_ahb_status: None,
1717 ..Default::default()
1718 },
1719 AhbFieldRule {
1720 segment_path: "SG4/SG12/NAD/3035".to_string(),
1721 name: "Korrespondenzanschrift".to_string(),
1722 ahb_status: "X".to_string(),
1723 codes: vec![AhbCodeRule {
1724 value: "Z09".to_string(),
1725 description: "Korrespondenzanschrift".to_string(),
1726 ahb_status: "X".to_string(),
1727 }],
1728 parent_group_ahb_status: None,
1729 ..Default::default()
1730 },
1731 ],
1732 ub_definitions: HashMap::new(),
1733 };
1734
1735 let report =
1736 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
1737
1738 let code_errors: Vec<_> = report
1739 .by_category(ValidationCategory::Code)
1740 .filter(|i| i.severity == Severity::Error)
1741 .collect();
1742 assert_eq!(
1743 code_errors.len(),
1744 1,
1745 "Expected exactly one COD002 error for MT, got: {:?}",
1746 code_errors
1747 );
1748 assert!(code_errors[0].message.contains("MT"));
1749 }
1750
1751 #[test]
1752 fn test_is_qualifier_field_simple_paths() {
1753 assert!(is_qualifier_field("NAD/3035"));
1754 assert!(is_qualifier_field("SG2/NAD/3035"));
1755 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
1756 assert!(is_qualifier_field("LOC/3227"));
1757 }
1758
1759 #[test]
1760 fn test_is_qualifier_field_composite_paths() {
1761 assert!(!is_qualifier_field("UNH/S009/0065"));
1762 assert!(!is_qualifier_field("NAD/C082/3039"));
1763 assert!(!is_qualifier_field("SG2/NAD/C082/3039"));
1764 }
1765
1766 #[test]
1767 fn test_is_qualifier_field_bare_segment() {
1768 assert!(!is_qualifier_field("NAD"));
1769 assert!(!is_qualifier_field("SG2/NAD"));
1770 }
1771
1772 #[test]
1773 fn test_missing_qualifier_instance_is_detected() {
1774 let evaluator = MockEvaluator::new(vec![]);
1777 let validator = EdifactValidator::new(evaluator);
1778 let external = NoOpExternalProvider;
1779
1780 let nad_ms = OwnedSegment {
1781 id: "NAD".to_string(),
1782 elements: vec![vec!["MS".to_string()]],
1783 segment_number: 3,
1784 };
1785
1786 let workflow = AhbWorkflow {
1787 pruefidentifikator: "55001".to_string(),
1788 description: "Test".to_string(),
1789 communication_direction: None,
1790 fields: vec![
1791 AhbFieldRule {
1792 segment_path: "SG2/NAD/3035".to_string(),
1793 name: "Absender".to_string(),
1794 ahb_status: "X".to_string(),
1795 codes: vec![AhbCodeRule {
1796 value: "MS".to_string(),
1797 description: "Absender".to_string(),
1798 ahb_status: "X".to_string(),
1799 }],
1800 parent_group_ahb_status: None,
1801 ..Default::default()
1802 },
1803 AhbFieldRule {
1804 segment_path: "SG2/NAD/3035".to_string(),
1805 name: "Empfaenger".to_string(),
1806 ahb_status: "Muss".to_string(),
1807 codes: vec![AhbCodeRule {
1808 value: "MR".to_string(),
1809 description: "Empfaenger".to_string(),
1810 ahb_status: "X".to_string(),
1811 }],
1812 parent_group_ahb_status: None,
1813 ..Default::default()
1814 },
1815 ],
1816 ub_definitions: HashMap::new(),
1817 };
1818
1819 let report =
1820 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
1821
1822 let ahb_errors: Vec<_> = report
1823 .by_category(ValidationCategory::Ahb)
1824 .filter(|i| i.severity == Severity::Error)
1825 .collect();
1826 assert_eq!(
1827 ahb_errors.len(),
1828 1,
1829 "Expected AHB001 for missing NAD+MR, got: {:?}",
1830 ahb_errors
1831 );
1832 assert!(ahb_errors[0].message.contains("Empfaenger"));
1833 }
1834
1835 #[test]
1836 fn test_present_qualifier_instance_no_error() {
1837 let evaluator = MockEvaluator::new(vec![]);
1839 let validator = EdifactValidator::new(evaluator);
1840 let external = NoOpExternalProvider;
1841
1842 let segments = vec![
1843 OwnedSegment {
1844 id: "NAD".to_string(),
1845 elements: vec![vec!["MS".to_string()]],
1846 segment_number: 3,
1847 },
1848 OwnedSegment {
1849 id: "NAD".to_string(),
1850 elements: vec![vec!["MR".to_string()]],
1851 segment_number: 4,
1852 },
1853 ];
1854
1855 let workflow = AhbWorkflow {
1856 pruefidentifikator: "55001".to_string(),
1857 description: "Test".to_string(),
1858 communication_direction: None,
1859 fields: vec![
1860 AhbFieldRule {
1861 segment_path: "SG2/NAD/3035".to_string(),
1862 name: "Absender".to_string(),
1863 ahb_status: "Muss".to_string(),
1864 codes: vec![AhbCodeRule {
1865 value: "MS".to_string(),
1866 description: "Absender".to_string(),
1867 ahb_status: "X".to_string(),
1868 }],
1869 parent_group_ahb_status: None,
1870 ..Default::default()
1871 },
1872 AhbFieldRule {
1873 segment_path: "SG2/NAD/3035".to_string(),
1874 name: "Empfaenger".to_string(),
1875 ahb_status: "Muss".to_string(),
1876 codes: vec![AhbCodeRule {
1877 value: "MR".to_string(),
1878 description: "Empfaenger".to_string(),
1879 ahb_status: "X".to_string(),
1880 }],
1881 parent_group_ahb_status: None,
1882 ..Default::default()
1883 },
1884 ],
1885 ub_definitions: HashMap::new(),
1886 };
1887
1888 let report =
1889 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
1890
1891 let ahb_errors: Vec<_> = report
1892 .by_category(ValidationCategory::Ahb)
1893 .filter(|i| i.severity == Severity::Error)
1894 .collect();
1895 assert!(
1896 ahb_errors.is_empty(),
1897 "Expected no AHB001 errors, got: {:?}",
1898 ahb_errors
1899 );
1900 }
1901
1902 #[test]
1903 fn test_extract_group_path_key() {
1904 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
1905 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
1906 assert_eq!(extract_group_path_key("NAD/3035"), "");
1907 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
1908 }
1909
1910 #[test]
1911 fn test_absent_optional_group_no_missing_field_error() {
1912 use mig_types::navigator::GroupNavigator;
1915
1916 struct NavWithoutSG3;
1917 impl GroupNavigator for NavWithoutSG3 {
1918 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
1919 vec![]
1920 }
1921 fn find_segments_with_qualifier_in_group(
1922 &self,
1923 _: &str,
1924 _: usize,
1925 _: &str,
1926 _: &[&str],
1927 _: usize,
1928 ) -> Vec<OwnedSegment> {
1929 vec![]
1930 }
1931 fn group_instance_count(&self, group_path: &[&str]) -> usize {
1932 match group_path {
1933 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
1936 }
1937 }
1938 }
1939
1940 let evaluator = MockEvaluator::new(vec![]);
1941 let validator = EdifactValidator::new(evaluator);
1942 let external = NoOpExternalProvider;
1943 let nav = NavWithoutSG3;
1944
1945 let segments = vec![
1947 OwnedSegment {
1948 id: "NAD".into(),
1949 elements: vec![vec!["MS".into()]],
1950 segment_number: 3,
1951 },
1952 OwnedSegment {
1953 id: "NAD".into(),
1954 elements: vec![vec!["MR".into()]],
1955 segment_number: 4,
1956 },
1957 ];
1958
1959 let workflow = AhbWorkflow {
1960 pruefidentifikator: "55001".to_string(),
1961 description: "Test".to_string(),
1962 communication_direction: None,
1963 fields: vec![
1964 AhbFieldRule {
1965 segment_path: "SG2/SG3/CTA/3139".to_string(),
1966 name: "Funktion des Ansprechpartners, Code".to_string(),
1967 ahb_status: "Muss".to_string(),
1968 codes: vec![],
1969 parent_group_ahb_status: None,
1970 ..Default::default()
1971 },
1972 AhbFieldRule {
1973 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
1974 name: "Name vom Ansprechpartner".to_string(),
1975 ahb_status: "X".to_string(),
1976 codes: vec![],
1977 parent_group_ahb_status: None,
1978 ..Default::default()
1979 },
1980 ],
1981 ub_definitions: HashMap::new(),
1982 };
1983
1984 let report = validator.validate_with_navigator(
1985 &segments,
1986 &workflow,
1987 &external,
1988 ValidationLevel::Conditions,
1989 &nav,
1990 );
1991
1992 let ahb_errors: Vec<_> = report
1993 .by_category(ValidationCategory::Ahb)
1994 .filter(|i| i.severity == Severity::Error)
1995 .collect();
1996 assert!(
1997 ahb_errors.is_empty(),
1998 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
1999 ahb_errors
2000 );
2001 }
2002
2003 #[test]
2004 fn test_present_group_still_checks_mandatory_fields() {
2005 use mig_types::navigator::GroupNavigator;
2007
2008 struct NavWithSG3;
2009 impl GroupNavigator for NavWithSG3 {
2010 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2011 vec![]
2012 }
2013 fn find_segments_with_qualifier_in_group(
2014 &self,
2015 _: &str,
2016 _: usize,
2017 _: &str,
2018 _: &[&str],
2019 _: usize,
2020 ) -> Vec<OwnedSegment> {
2021 vec![]
2022 }
2023 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2024 match group_path {
2025 ["SG2"] => 1,
2026 ["SG2", "SG3"] => 1, _ => 0,
2028 }
2029 }
2030 }
2031
2032 let evaluator = MockEvaluator::new(vec![]);
2033 let validator = EdifactValidator::new(evaluator);
2034 let external = NoOpExternalProvider;
2035 let nav = NavWithSG3;
2036
2037 let segments = vec![OwnedSegment {
2039 id: "NAD".into(),
2040 elements: vec![vec!["MS".into()]],
2041 segment_number: 3,
2042 }];
2043
2044 let workflow = AhbWorkflow {
2045 pruefidentifikator: "55001".to_string(),
2046 description: "Test".to_string(),
2047 communication_direction: None,
2048 fields: vec![AhbFieldRule {
2049 segment_path: "SG2/SG3/CTA/3139".to_string(),
2050 name: "Funktion des Ansprechpartners, Code".to_string(),
2051 ahb_status: "Muss".to_string(),
2052 codes: vec![],
2053 parent_group_ahb_status: None,
2054 ..Default::default()
2055 }],
2056 ub_definitions: HashMap::new(),
2057 };
2058
2059 let report = validator.validate_with_navigator(
2060 &segments,
2061 &workflow,
2062 &external,
2063 ValidationLevel::Conditions,
2064 &nav,
2065 );
2066
2067 let ahb_errors: Vec<_> = report
2068 .by_category(ValidationCategory::Ahb)
2069 .filter(|i| i.severity == Severity::Error)
2070 .collect();
2071 assert_eq!(
2072 ahb_errors.len(),
2073 1,
2074 "Expected AHB001 error when SG3 is present but CTA missing"
2075 );
2076 assert!(ahb_errors[0].message.contains("CTA"));
2077 }
2078
2079 #[test]
2080 fn test_missing_qualifier_with_navigator_is_detected() {
2081 use mig_types::navigator::GroupNavigator;
2084
2085 struct NavWithSG2;
2086 impl GroupNavigator for NavWithSG2 {
2087 fn find_segments_in_group(
2088 &self,
2089 segment_id: &str,
2090 group_path: &[&str],
2091 instance_index: usize,
2092 ) -> Vec<OwnedSegment> {
2093 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
2094 vec![OwnedSegment {
2095 id: "NAD".into(),
2096 elements: vec![vec!["MS".into()]],
2097 segment_number: 3,
2098 }]
2099 } else {
2100 vec![]
2101 }
2102 }
2103 fn find_segments_with_qualifier_in_group(
2104 &self,
2105 _: &str,
2106 _: usize,
2107 _: &str,
2108 _: &[&str],
2109 _: usize,
2110 ) -> Vec<OwnedSegment> {
2111 vec![]
2112 }
2113 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2114 match group_path {
2115 ["SG2"] => 1,
2116 _ => 0,
2117 }
2118 }
2119 }
2120
2121 let evaluator = MockEvaluator::new(vec![]);
2122 let validator = EdifactValidator::new(evaluator);
2123 let external = NoOpExternalProvider;
2124 let nav = NavWithSG2;
2125
2126 let segments = vec![OwnedSegment {
2127 id: "NAD".into(),
2128 elements: vec![vec!["MS".into()]],
2129 segment_number: 3,
2130 }];
2131
2132 let workflow = AhbWorkflow {
2133 pruefidentifikator: "55001".to_string(),
2134 description: "Test".to_string(),
2135 communication_direction: None,
2136 fields: vec![
2137 AhbFieldRule {
2138 segment_path: "SG2/NAD/3035".to_string(),
2139 name: "Absender".to_string(),
2140 ahb_status: "X".to_string(),
2141 codes: vec![AhbCodeRule {
2142 value: "MS".to_string(),
2143 description: "Absender".to_string(),
2144 ahb_status: "X".to_string(),
2145 }],
2146 parent_group_ahb_status: None,
2147 ..Default::default()
2148 },
2149 AhbFieldRule {
2150 segment_path: "SG2/NAD/3035".to_string(),
2151 name: "Empfaenger".to_string(),
2152 ahb_status: "Muss".to_string(),
2153 codes: vec![AhbCodeRule {
2154 value: "MR".to_string(),
2155 description: "Empfaenger".to_string(),
2156 ahb_status: "X".to_string(),
2157 }],
2158 parent_group_ahb_status: None,
2159 ..Default::default()
2160 },
2161 ],
2162 ub_definitions: HashMap::new(),
2163 };
2164
2165 let report = validator.validate_with_navigator(
2166 &segments,
2167 &workflow,
2168 &external,
2169 ValidationLevel::Conditions,
2170 &nav,
2171 );
2172
2173 let ahb_errors: Vec<_> = report
2174 .by_category(ValidationCategory::Ahb)
2175 .filter(|i| i.severity == Severity::Error)
2176 .collect();
2177 assert_eq!(
2178 ahb_errors.len(),
2179 1,
2180 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
2181 ahb_errors
2182 );
2183 assert!(ahb_errors[0].message.contains("Empfaenger"));
2184 }
2185
2186 #[test]
2187 fn test_optional_group_variant_absent_no_error() {
2188 use mig_types::navigator::GroupNavigator;
2193
2194 struct TestNav;
2195 impl GroupNavigator for TestNav {
2196 fn find_segments_in_group(
2197 &self,
2198 segment_id: &str,
2199 group_path: &[&str],
2200 instance_index: usize,
2201 ) -> Vec<OwnedSegment> {
2202 match (segment_id, group_path, instance_index) {
2203 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
2204 id: "LOC".into(),
2205 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2206 segment_number: 10,
2207 }],
2208 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
2209 id: "NAD".into(),
2210 elements: vec![vec!["MS".into()]],
2211 segment_number: 3,
2212 }],
2213 _ => vec![],
2214 }
2215 }
2216 fn find_segments_with_qualifier_in_group(
2217 &self,
2218 _: &str,
2219 _: usize,
2220 _: &str,
2221 _: &[&str],
2222 _: usize,
2223 ) -> Vec<OwnedSegment> {
2224 vec![]
2225 }
2226 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2227 match group_path {
2228 ["SG2"] => 1,
2229 ["SG4"] => 1,
2230 ["SG4", "SG5"] => 1, _ => 0,
2232 }
2233 }
2234 }
2235
2236 let evaluator = MockEvaluator::new(vec![]);
2237 let validator = EdifactValidator::new(evaluator);
2238 let external = NoOpExternalProvider;
2239 let nav = TestNav;
2240
2241 let segments = vec![
2242 OwnedSegment {
2243 id: "NAD".into(),
2244 elements: vec![vec!["MS".into()]],
2245 segment_number: 3,
2246 },
2247 OwnedSegment {
2248 id: "LOC".into(),
2249 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2250 segment_number: 10,
2251 },
2252 ];
2253
2254 let workflow = AhbWorkflow {
2255 pruefidentifikator: "55001".to_string(),
2256 description: "Test".to_string(),
2257 communication_direction: None,
2258 fields: vec![
2259 AhbFieldRule {
2261 segment_path: "SG2/NAD/3035".to_string(),
2262 name: "Absender".to_string(),
2263 ahb_status: "X".to_string(),
2264 codes: vec![AhbCodeRule {
2265 value: "MS".to_string(),
2266 description: "Absender".to_string(),
2267 ahb_status: "X".to_string(),
2268 }],
2269 parent_group_ahb_status: Some("Muss".to_string()),
2270 ..Default::default()
2271 },
2272 AhbFieldRule {
2273 segment_path: "SG2/NAD/3035".to_string(),
2274 name: "Empfaenger".to_string(),
2275 ahb_status: "Muss".to_string(),
2276 codes: vec![AhbCodeRule {
2277 value: "MR".to_string(),
2278 description: "Empfaenger".to_string(),
2279 ahb_status: "X".to_string(),
2280 }],
2281 parent_group_ahb_status: Some("Muss".to_string()),
2282 ..Default::default()
2283 },
2284 AhbFieldRule {
2286 segment_path: "SG4/SG5/LOC/3227".to_string(),
2287 name: "Ortsangabe, Qualifier (Z16)".to_string(),
2288 ahb_status: "X".to_string(),
2289 codes: vec![AhbCodeRule {
2290 value: "Z16".to_string(),
2291 description: "Marktlokation".to_string(),
2292 ahb_status: "X".to_string(),
2293 }],
2294 parent_group_ahb_status: Some("Kann".to_string()),
2295 ..Default::default()
2296 },
2297 AhbFieldRule {
2298 segment_path: "SG4/SG5/LOC/3227".to_string(),
2299 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2300 ahb_status: "Muss".to_string(),
2301 codes: vec![AhbCodeRule {
2302 value: "Z17".to_string(),
2303 description: "Messlokation".to_string(),
2304 ahb_status: "X".to_string(),
2305 }],
2306 parent_group_ahb_status: Some("Kann".to_string()),
2307 ..Default::default()
2308 },
2309 ],
2310 ub_definitions: HashMap::new(),
2311 };
2312
2313 let report = validator.validate_with_navigator(
2314 &segments,
2315 &workflow,
2316 &external,
2317 ValidationLevel::Conditions,
2318 &nav,
2319 );
2320
2321 let ahb_errors: Vec<_> = report
2322 .by_category(ValidationCategory::Ahb)
2323 .filter(|i| i.severity == Severity::Error)
2324 .collect();
2325
2326 assert_eq!(
2329 ahb_errors.len(),
2330 1,
2331 "Expected only AHB001 for missing NAD+MR, got: {:?}",
2332 ahb_errors
2333 );
2334 assert!(
2335 ahb_errors[0].message.contains("Empfaenger"),
2336 "Error should be for missing NAD+MR (Empfaenger)"
2337 );
2338 }
2339
2340 #[test]
2341 fn test_conditional_group_variant_absent_no_error() {
2342 use mig_types::navigator::GroupNavigator;
2347
2348 struct TestNav;
2349 impl GroupNavigator for TestNav {
2350 fn find_segments_in_group(
2351 &self,
2352 segment_id: &str,
2353 group_path: &[&str],
2354 instance_index: usize,
2355 ) -> Vec<OwnedSegment> {
2356 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
2357 vec![OwnedSegment {
2358 id: "LOC".into(),
2359 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2360 segment_number: 10,
2361 }]
2362 } else {
2363 vec![]
2364 }
2365 }
2366 fn find_segments_with_qualifier_in_group(
2367 &self,
2368 _: &str,
2369 _: usize,
2370 _: &str,
2371 _: &[&str],
2372 _: usize,
2373 ) -> Vec<OwnedSegment> {
2374 vec![]
2375 }
2376 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2377 match group_path {
2378 ["SG4"] => 1,
2379 ["SG4", "SG5"] => 1, _ => 0,
2381 }
2382 }
2383 }
2384
2385 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
2388 let validator = EdifactValidator::new(evaluator);
2389 let external = NoOpExternalProvider;
2390 let nav = TestNav;
2391
2392 let segments = vec![OwnedSegment {
2393 id: "LOC".into(),
2394 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2395 segment_number: 10,
2396 }];
2397
2398 let workflow = AhbWorkflow {
2399 pruefidentifikator: "55001".to_string(),
2400 description: "Test".to_string(),
2401 communication_direction: None,
2402 fields: vec![
2403 AhbFieldRule {
2405 segment_path: "SG4/SG5/LOC/3227".to_string(),
2406 name: "Ortsangabe, Qualifier (Z16)".to_string(),
2407 ahb_status: "X".to_string(),
2408 codes: vec![AhbCodeRule {
2409 value: "Z16".to_string(),
2410 description: "Marktlokation".to_string(),
2411 ahb_status: "X".to_string(),
2412 }],
2413 parent_group_ahb_status: Some("Muss [2061]".to_string()),
2414 ..Default::default()
2415 },
2416 AhbFieldRule {
2418 segment_path: "SG4/SG5/LOC/3227".to_string(),
2419 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2420 ahb_status: "X".to_string(),
2421 codes: vec![AhbCodeRule {
2422 value: "Z17".to_string(),
2423 description: "Messlokation".to_string(),
2424 ahb_status: "X".to_string(),
2425 }],
2426 parent_group_ahb_status: Some("Soll [165]".to_string()),
2427 ..Default::default()
2428 },
2429 ],
2430 ub_definitions: HashMap::new(),
2431 };
2432
2433 let report = validator.validate_with_navigator(
2434 &segments,
2435 &workflow,
2436 &external,
2437 ValidationLevel::Conditions,
2438 &nav,
2439 );
2440
2441 let ahb_errors: Vec<_> = report
2442 .by_category(ValidationCategory::Ahb)
2443 .filter(|i| i.severity == Severity::Error)
2444 .collect();
2445
2446 assert!(
2448 ahb_errors.is_empty(),
2449 "Expected no errors when conditional group variant [165]=False, got: {:?}",
2450 ahb_errors
2451 );
2452 }
2453
2454 #[test]
2455 fn test_conditional_group_variant_unknown_no_error() {
2456 let evaluator = MockEvaluator::new(vec![]);
2462 let validator = EdifactValidator::new(evaluator);
2463 let external = NoOpExternalProvider;
2464
2465 let workflow = AhbWorkflow {
2466 pruefidentifikator: "55001".to_string(),
2467 description: "Test".to_string(),
2468 communication_direction: None,
2469 fields: vec![AhbFieldRule {
2470 segment_path: "SG4/SG5/LOC/3227".to_string(),
2471 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2472 ahb_status: "X".to_string(),
2473 codes: vec![AhbCodeRule {
2474 value: "Z17".to_string(),
2475 description: "Messlokation".to_string(),
2476 ahb_status: "X".to_string(),
2477 }],
2478 parent_group_ahb_status: Some("Soll [165]".to_string()),
2479 ..Default::default()
2480 }],
2481 ub_definitions: HashMap::new(),
2482 };
2483
2484 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2485
2486 let ahb_errors: Vec<_> = report
2487 .by_category(ValidationCategory::Ahb)
2488 .filter(|i| i.severity == Severity::Error)
2489 .collect();
2490
2491 assert!(
2493 ahb_errors.is_empty(),
2494 "Expected no errors when parent group condition is Unknown, got: {:?}",
2495 ahb_errors
2496 );
2497 }
2498
2499 #[test]
2500 fn test_segment_absent_within_present_group_no_error() {
2501 use mig_types::navigator::GroupNavigator;
2505
2506 struct TestNav;
2507 impl GroupNavigator for TestNav {
2508 fn find_segments_in_group(
2509 &self,
2510 segment_id: &str,
2511 group_path: &[&str],
2512 instance_index: usize,
2513 ) -> Vec<OwnedSegment> {
2514 if segment_id == "QTY"
2516 && group_path == ["SG5", "SG6", "SG9", "SG10"]
2517 && instance_index == 0
2518 {
2519 vec![OwnedSegment {
2520 id: "QTY".into(),
2521 elements: vec![vec!["220".into(), "0".into()]],
2522 segment_number: 14,
2523 }]
2524 } else {
2525 vec![]
2526 }
2527 }
2528 fn find_segments_with_qualifier_in_group(
2529 &self,
2530 _: &str,
2531 _: usize,
2532 _: &str,
2533 _: &[&str],
2534 _: usize,
2535 ) -> Vec<OwnedSegment> {
2536 vec![]
2537 }
2538 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2539 match group_path {
2540 ["SG5"] => 1,
2541 ["SG5", "SG6"] => 1,
2542 ["SG5", "SG6", "SG9"] => 1,
2543 ["SG5", "SG6", "SG9", "SG10"] => 1,
2544 _ => 0,
2545 }
2546 }
2547 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
2548 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
2550 }
2551 }
2552
2553 let evaluator = MockEvaluator::all_true(&[]);
2554 let validator = EdifactValidator::new(evaluator);
2555 let external = NoOpExternalProvider;
2556 let nav = TestNav;
2557
2558 let segments = vec![OwnedSegment {
2559 id: "QTY".into(),
2560 elements: vec![vec!["220".into(), "0".into()]],
2561 segment_number: 14,
2562 }];
2563
2564 let workflow = AhbWorkflow {
2565 pruefidentifikator: "13017".to_string(),
2566 description: "Test".to_string(),
2567 communication_direction: None,
2568 fields: vec![
2569 AhbFieldRule {
2571 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
2572 name: "Statuskategorie, Code".to_string(),
2573 ahb_status: "X".to_string(),
2574 codes: vec![],
2575 parent_group_ahb_status: Some("Muss".to_string()),
2576 ..Default::default()
2577 },
2578 AhbFieldRule {
2580 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2581 name: "Statusanlaß, Code".to_string(),
2582 ahb_status: "X [5]".to_string(),
2583 codes: vec![],
2584 parent_group_ahb_status: Some("Muss".to_string()),
2585 ..Default::default()
2586 },
2587 ],
2588 ub_definitions: HashMap::new(),
2589 };
2590
2591 let report = validator.validate_with_navigator(
2592 &segments,
2593 &workflow,
2594 &external,
2595 ValidationLevel::Conditions,
2596 &nav,
2597 );
2598
2599 let ahb_errors: Vec<_> = report
2600 .by_category(ValidationCategory::Ahb)
2601 .filter(|i| i.severity == Severity::Error)
2602 .collect();
2603
2604 assert!(
2605 ahb_errors.is_empty(),
2606 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
2607 ahb_errors
2608 );
2609 }
2610
2611 #[test]
2612 fn test_group_scoped_code_validation_with_navigator() {
2613 use mig_types::navigator::GroupNavigator;
2617
2618 struct TestNav;
2619 impl GroupNavigator for TestNav {
2620 fn find_segments_in_group(
2621 &self,
2622 segment_id: &str,
2623 group_path: &[&str],
2624 _instance_index: usize,
2625 ) -> Vec<OwnedSegment> {
2626 if segment_id != "NAD" {
2627 return vec![];
2628 }
2629 match group_path {
2630 ["SG2"] => vec![
2631 OwnedSegment {
2632 id: "NAD".into(),
2633 elements: vec![vec!["MS".into()]],
2634 segment_number: 3,
2635 },
2636 OwnedSegment {
2637 id: "NAD".into(),
2638 elements: vec![vec!["MT".into()]], segment_number: 4,
2640 },
2641 ],
2642 ["SG4", "SG12"] => vec![
2643 OwnedSegment {
2644 id: "NAD".into(),
2645 elements: vec![vec!["Z04".into()]],
2646 segment_number: 20,
2647 },
2648 OwnedSegment {
2649 id: "NAD".into(),
2650 elements: vec![vec!["Z09".into()]],
2651 segment_number: 21,
2652 },
2653 ],
2654 _ => vec![],
2655 }
2656 }
2657 fn find_segments_with_qualifier_in_group(
2658 &self,
2659 _: &str,
2660 _: usize,
2661 _: &str,
2662 _: &[&str],
2663 _: usize,
2664 ) -> Vec<OwnedSegment> {
2665 vec![]
2666 }
2667 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2668 match group_path {
2669 ["SG2"] | ["SG4", "SG12"] => 1,
2670 _ => 0,
2671 }
2672 }
2673 }
2674
2675 let evaluator = MockEvaluator::new(vec![]);
2676 let validator = EdifactValidator::new(evaluator);
2677 let external = NoOpExternalProvider;
2678 let nav = TestNav;
2679
2680 let workflow = AhbWorkflow {
2681 pruefidentifikator: "55001".to_string(),
2682 description: "Test".to_string(),
2683 communication_direction: None,
2684 fields: vec![
2685 AhbFieldRule {
2686 segment_path: "SG2/NAD/3035".to_string(),
2687 name: "Absender".to_string(),
2688 ahb_status: "X".to_string(),
2689 codes: vec![AhbCodeRule {
2690 value: "MS".to_string(),
2691 description: "Absender".to_string(),
2692 ahb_status: "X".to_string(),
2693 }],
2694 parent_group_ahb_status: None,
2695 ..Default::default()
2696 },
2697 AhbFieldRule {
2698 segment_path: "SG2/NAD/3035".to_string(),
2699 name: "Empfaenger".to_string(),
2700 ahb_status: "X".to_string(),
2701 codes: vec![AhbCodeRule {
2702 value: "MR".to_string(),
2703 description: "Empfaenger".to_string(),
2704 ahb_status: "X".to_string(),
2705 }],
2706 parent_group_ahb_status: None,
2707 ..Default::default()
2708 },
2709 AhbFieldRule {
2710 segment_path: "SG4/SG12/NAD/3035".to_string(),
2711 name: "Anschlussnutzer".to_string(),
2712 ahb_status: "X".to_string(),
2713 codes: vec![AhbCodeRule {
2714 value: "Z04".to_string(),
2715 description: "Anschlussnutzer".to_string(),
2716 ahb_status: "X".to_string(),
2717 }],
2718 parent_group_ahb_status: None,
2719 ..Default::default()
2720 },
2721 AhbFieldRule {
2722 segment_path: "SG4/SG12/NAD/3035".to_string(),
2723 name: "Korrespondenzanschrift".to_string(),
2724 ahb_status: "X".to_string(),
2725 codes: vec![AhbCodeRule {
2726 value: "Z09".to_string(),
2727 description: "Korrespondenzanschrift".to_string(),
2728 ahb_status: "X".to_string(),
2729 }],
2730 parent_group_ahb_status: None,
2731 ..Default::default()
2732 },
2733 ],
2734 ub_definitions: HashMap::new(),
2735 };
2736
2737 let all_segments = vec![
2739 OwnedSegment {
2740 id: "NAD".into(),
2741 elements: vec![vec!["MS".into()]],
2742 segment_number: 3,
2743 },
2744 OwnedSegment {
2745 id: "NAD".into(),
2746 elements: vec![vec!["MT".into()]],
2747 segment_number: 4,
2748 },
2749 OwnedSegment {
2750 id: "NAD".into(),
2751 elements: vec![vec!["Z04".into()]],
2752 segment_number: 20,
2753 },
2754 OwnedSegment {
2755 id: "NAD".into(),
2756 elements: vec![vec!["Z09".into()]],
2757 segment_number: 21,
2758 },
2759 ];
2760
2761 let report = validator.validate_with_navigator(
2762 &all_segments,
2763 &workflow,
2764 &external,
2765 ValidationLevel::Conditions,
2766 &nav,
2767 );
2768
2769 let code_errors: Vec<_> = report
2770 .by_category(ValidationCategory::Code)
2771 .filter(|i| i.severity == Severity::Error)
2772 .collect();
2773
2774 assert_eq!(
2777 code_errors.len(),
2778 1,
2779 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
2780 code_errors
2781 );
2782 assert!(code_errors[0].message.contains("MT"));
2783 assert!(code_errors[0].message.contains("MR"));
2785 assert!(code_errors[0].message.contains("MS"));
2786 assert!(
2787 !code_errors[0].message.contains("Z04"),
2788 "SG4/SG12 codes should not leak into SG2 error"
2789 );
2790 assert!(
2792 code_errors[0]
2793 .field_path
2794 .as_deref()
2795 .unwrap_or("")
2796 .contains("SG2"),
2797 "Error field_path should reference SG2, got: {:?}",
2798 code_errors[0].field_path
2799 );
2800 }
2801
2802 #[test]
2805 fn test_package_cardinality_within_bounds() {
2806 let evaluator = MockEvaluator::all_true(&[]);
2808 let validator = EdifactValidator::new(evaluator);
2809 let external = NoOpExternalProvider;
2810
2811 let segments = vec![OwnedSegment {
2812 id: "STS".into(),
2813 elements: vec![
2814 vec!["Z33".into()], vec![], vec!["E01".into()], ],
2818 segment_number: 5,
2819 }];
2820
2821 let workflow = AhbWorkflow {
2822 pruefidentifikator: "13017".to_string(),
2823 description: "Test".to_string(),
2824 communication_direction: None,
2825 ub_definitions: HashMap::new(),
2826 fields: vec![AhbFieldRule {
2827 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2828 name: "Statusanlaß, Code".to_string(),
2829 ahb_status: "X".to_string(),
2830 element_index: Some(2),
2831 component_index: Some(0),
2832 codes: vec![
2833 AhbCodeRule {
2834 value: "E01".into(),
2835 description: "Code 1".into(),
2836 ahb_status: "X [4P0..1]".into(),
2837 },
2838 AhbCodeRule {
2839 value: "E02".into(),
2840 description: "Code 2".into(),
2841 ahb_status: "X [4P0..1]".into(),
2842 },
2843 ],
2844 parent_group_ahb_status: Some("Muss".to_string()),
2845 mig_number: None,
2846 }],
2847 };
2848
2849 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2850 let pkg_errors: Vec<_> = report
2851 .by_category(ValidationCategory::Ahb)
2852 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2853 .collect();
2854 assert!(
2855 pkg_errors.is_empty(),
2856 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
2857 pkg_errors
2858 );
2859 }
2860
2861 #[test]
2862 fn test_package_cardinality_zero_present_min_zero() {
2863 let evaluator = MockEvaluator::all_true(&[]);
2865 let validator = EdifactValidator::new(evaluator);
2866 let external = NoOpExternalProvider;
2867
2868 let segments = vec![OwnedSegment {
2869 id: "STS".into(),
2870 elements: vec![
2871 vec!["Z33".into()],
2872 vec![],
2873 vec!["X99".into()], ],
2875 segment_number: 5,
2876 }];
2877
2878 let workflow = AhbWorkflow {
2879 pruefidentifikator: "13017".to_string(),
2880 description: "Test".to_string(),
2881 communication_direction: None,
2882 ub_definitions: HashMap::new(),
2883 fields: vec![AhbFieldRule {
2884 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2885 name: "Statusanlaß, Code".to_string(),
2886 ahb_status: "X".to_string(),
2887 element_index: Some(2),
2888 component_index: Some(0),
2889 codes: vec![
2890 AhbCodeRule {
2891 value: "E01".into(),
2892 description: "Code 1".into(),
2893 ahb_status: "X [4P0..1]".into(),
2894 },
2895 AhbCodeRule {
2896 value: "E02".into(),
2897 description: "Code 2".into(),
2898 ahb_status: "X [4P0..1]".into(),
2899 },
2900 ],
2901 parent_group_ahb_status: Some("Muss".to_string()),
2902 mig_number: None,
2903 }],
2904 };
2905
2906 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2907 let pkg_errors: Vec<_> = report
2908 .by_category(ValidationCategory::Ahb)
2909 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2910 .collect();
2911 assert!(
2912 pkg_errors.is_empty(),
2913 "0 codes, min=0 — no error expected, got: {:?}",
2914 pkg_errors
2915 );
2916 }
2917
2918 #[test]
2919 fn test_package_cardinality_too_many() {
2920 let evaluator = MockEvaluator::all_true(&[]);
2922 let validator = EdifactValidator::new(evaluator);
2923 let external = NoOpExternalProvider;
2924
2925 let segments = vec![
2927 OwnedSegment {
2928 id: "STS".into(),
2929 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
2930 segment_number: 5,
2931 },
2932 OwnedSegment {
2933 id: "STS".into(),
2934 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
2935 segment_number: 6,
2936 },
2937 ];
2938
2939 let workflow = AhbWorkflow {
2940 pruefidentifikator: "13017".to_string(),
2941 description: "Test".to_string(),
2942 communication_direction: None,
2943 ub_definitions: HashMap::new(),
2944 fields: vec![AhbFieldRule {
2945 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2946 name: "Statusanlaß, Code".to_string(),
2947 ahb_status: "X".to_string(),
2948 element_index: Some(2),
2949 component_index: Some(0),
2950 codes: vec![
2951 AhbCodeRule {
2952 value: "E01".into(),
2953 description: "Code 1".into(),
2954 ahb_status: "X [4P0..1]".into(),
2955 },
2956 AhbCodeRule {
2957 value: "E02".into(),
2958 description: "Code 2".into(),
2959 ahb_status: "X [4P0..1]".into(),
2960 },
2961 ],
2962 parent_group_ahb_status: Some("Muss".to_string()),
2963 mig_number: None,
2964 }],
2965 };
2966
2967 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2968 let pkg_errors: Vec<_> = report
2969 .by_category(ValidationCategory::Ahb)
2970 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2971 .collect();
2972 assert_eq!(
2973 pkg_errors.len(),
2974 1,
2975 "2 codes present, max=1 — expected 1 error, got: {:?}",
2976 pkg_errors
2977 );
2978 assert!(pkg_errors[0].message.contains("[4P0..1]"));
2979 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
2980 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
2981 }
2982
2983 #[test]
2984 fn test_package_cardinality_too_few() {
2985 let evaluator = MockEvaluator::all_true(&[]);
2987 let validator = EdifactValidator::new(evaluator);
2988 let external = NoOpExternalProvider;
2989
2990 let segments = vec![OwnedSegment {
2991 id: "STS".into(),
2992 elements: vec![
2993 vec!["Z33".into()],
2994 vec![],
2995 vec!["X99".into()], ],
2997 segment_number: 5,
2998 }];
2999
3000 let workflow = AhbWorkflow {
3001 pruefidentifikator: "13017".to_string(),
3002 description: "Test".to_string(),
3003 communication_direction: None,
3004 ub_definitions: HashMap::new(),
3005 fields: vec![AhbFieldRule {
3006 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3007 name: "Statusanlaß, Code".to_string(),
3008 ahb_status: "X".to_string(),
3009 element_index: Some(2),
3010 component_index: Some(0),
3011 codes: vec![
3012 AhbCodeRule {
3013 value: "E01".into(),
3014 description: "Code 1".into(),
3015 ahb_status: "X [5P1..3]".into(),
3016 },
3017 AhbCodeRule {
3018 value: "E02".into(),
3019 description: "Code 2".into(),
3020 ahb_status: "X [5P1..3]".into(),
3021 },
3022 AhbCodeRule {
3023 value: "E03".into(),
3024 description: "Code 3".into(),
3025 ahb_status: "X [5P1..3]".into(),
3026 },
3027 ],
3028 parent_group_ahb_status: Some("Muss".to_string()),
3029 mig_number: None,
3030 }],
3031 };
3032
3033 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3034 let pkg_errors: Vec<_> = report
3035 .by_category(ValidationCategory::Ahb)
3036 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3037 .collect();
3038 assert_eq!(
3039 pkg_errors.len(),
3040 1,
3041 "0 codes present, min=1 — expected 1 error, got: {:?}",
3042 pkg_errors
3043 );
3044 assert!(pkg_errors[0].message.contains("[5P1..3]"));
3045 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
3046 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
3047 }
3048
3049 #[test]
3050 fn test_package_cardinality_no_packages_in_workflow() {
3051 let evaluator = MockEvaluator::all_true(&[]);
3053 let validator = EdifactValidator::new(evaluator);
3054 let external = NoOpExternalProvider;
3055
3056 let segments = vec![OwnedSegment {
3057 id: "STS".into(),
3058 elements: vec![vec!["E01".into()]],
3059 segment_number: 5,
3060 }];
3061
3062 let workflow = AhbWorkflow {
3063 pruefidentifikator: "13017".to_string(),
3064 description: "Test".to_string(),
3065 communication_direction: None,
3066 ub_definitions: HashMap::new(),
3067 fields: vec![AhbFieldRule {
3068 segment_path: "STS/9015".to_string(),
3069 name: "Status Code".to_string(),
3070 ahb_status: "X".to_string(),
3071 codes: vec![AhbCodeRule {
3072 value: "E01".into(),
3073 description: "Code 1".into(),
3074 ahb_status: "X".into(),
3075 }],
3076 parent_group_ahb_status: Some("Muss".to_string()),
3077 ..Default::default()
3078 }],
3079 };
3080
3081 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3082 let pkg_errors: Vec<_> = report
3083 .by_category(ValidationCategory::Ahb)
3084 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3085 .collect();
3086 assert!(
3087 pkg_errors.is_empty(),
3088 "No packages in workflow — no errors expected"
3089 );
3090 }
3091
3092 #[test]
3093 fn test_package_cardinality_with_condition_and_package() {
3094 let evaluator = MockEvaluator::all_true(&[901]);
3096 let validator = EdifactValidator::new(evaluator);
3097 let external = NoOpExternalProvider;
3098
3099 let segments = vec![OwnedSegment {
3100 id: "STS".into(),
3101 elements: vec![vec![], vec![], vec!["E01".into()]],
3102 segment_number: 5,
3103 }];
3104
3105 let workflow = AhbWorkflow {
3106 pruefidentifikator: "13017".to_string(),
3107 description: "Test".to_string(),
3108 communication_direction: None,
3109 ub_definitions: HashMap::new(),
3110 fields: vec![AhbFieldRule {
3111 segment_path: "SG10/STS/C556/9013".to_string(),
3112 name: "Code".to_string(),
3113 ahb_status: "X".to_string(),
3114 element_index: Some(2),
3115 component_index: Some(0),
3116 codes: vec![
3117 AhbCodeRule {
3118 value: "E01".into(),
3119 description: "Code 1".into(),
3120 ahb_status: "X [901] [4P0..1]".into(),
3121 },
3122 AhbCodeRule {
3123 value: "E02".into(),
3124 description: "Code 2".into(),
3125 ahb_status: "X [901] [4P0..1]".into(),
3126 },
3127 ],
3128 parent_group_ahb_status: Some("Muss".to_string()),
3129 mig_number: None,
3130 }],
3131 };
3132
3133 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3134 let pkg_errors: Vec<_> = report
3135 .by_category(ValidationCategory::Ahb)
3136 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3137 .collect();
3138 assert!(
3139 pkg_errors.is_empty(),
3140 "1 code within [4P0..1] bounds — no error, got: {:?}",
3141 pkg_errors
3142 );
3143 }
3144}