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::codes::ErrorCodes;
15use super::issue::{Severity, ValidationCategory, ValidationIssue};
16use super::level::ValidationLevel;
17use super::report::ValidationReport;
18
19#[derive(Debug, Clone, Default)]
24pub struct AhbFieldRule {
25 pub segment_path: String,
27
28 pub name: String,
30
31 pub ahb_status: String,
33
34 pub codes: Vec<AhbCodeRule>,
36
37 pub parent_group_ahb_status: Option<String>,
42
43 pub element_index: Option<usize>,
46
47 pub component_index: Option<usize>,
50}
51
52#[derive(Debug, Clone, Default)]
54pub struct AhbCodeRule {
55 pub value: String,
57
58 pub description: String,
60
61 pub ahb_status: String,
63}
64
65#[derive(Debug, Clone)]
67pub struct AhbWorkflow {
68 pub pruefidentifikator: String,
70
71 pub description: String,
73
74 pub communication_direction: Option<String>,
76
77 pub fields: Vec<AhbFieldRule>,
79
80 pub ub_definitions: HashMap<String, ConditionExpr>,
86}
87
88pub struct EdifactValidator<E: ConditionEvaluator> {
121 evaluator: E,
122}
123
124impl<E: ConditionEvaluator> EdifactValidator<E> {
125 pub fn new(evaluator: E) -> Self {
127 Self { evaluator }
128 }
129
130 pub fn validate(
143 &self,
144 segments: &[OwnedSegment],
145 workflow: &AhbWorkflow,
146 external: &dyn ExternalConditionProvider,
147 level: ValidationLevel,
148 ) -> ValidationReport {
149 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
150 .with_format_version(self.evaluator.format_version())
151 .with_pruefidentifikator(&workflow.pruefidentifikator);
152
153 let ctx = EvaluationContext::new(&workflow.pruefidentifikator, external, segments);
154
155 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
156 self.validate_conditions(workflow, &ctx, &mut report);
157 }
158
159 report
160 }
161
162 pub fn validate_with_navigator(
168 &self,
169 segments: &[OwnedSegment],
170 workflow: &AhbWorkflow,
171 external: &dyn ExternalConditionProvider,
172 level: ValidationLevel,
173 navigator: &dyn GroupNavigator,
174 ) -> ValidationReport {
175 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
176 .with_format_version(self.evaluator.format_version())
177 .with_pruefidentifikator(&workflow.pruefidentifikator);
178
179 let ctx = EvaluationContext::with_navigator(
180 &workflow.pruefidentifikator,
181 external,
182 segments,
183 navigator,
184 );
185
186 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
187 self.validate_conditions(workflow, &ctx, &mut report);
188 }
189
190 report
191 }
192
193 fn validate_conditions(
195 &self,
196 workflow: &AhbWorkflow,
197 ctx: &EvaluationContext,
198 report: &mut ValidationReport,
199 ) {
200 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
201
202 for field in &workflow.fields {
203 if let Some(ref group_status) = field.parent_group_ahb_status {
210 if group_status.contains('[') {
211 let group_result = expr_eval.evaluate_status_with_ub(
212 group_status,
213 ctx,
214 &workflow.ub_definitions,
215 );
216 if matches!(
217 group_result,
218 ConditionResult::False | ConditionResult::Unknown
219 ) {
220 continue;
221 }
222 }
223 }
224
225 let (condition_result, unknown_ids) = expr_eval.evaluate_status_detailed_with_ub(
228 &field.ahb_status,
229 ctx,
230 &workflow.ub_definitions,
231 );
232
233 match condition_result {
234 ConditionResult::True => {
235 if is_mandatory_status(&field.ahb_status)
237 && !is_field_present(ctx, field)
238 && !is_group_variant_absent(ctx, field)
239 {
240 let mut issue = ValidationIssue::new(
241 Severity::Error,
242 ValidationCategory::Ahb,
243 ErrorCodes::MISSING_REQUIRED_FIELD,
244 format!(
245 "Required field '{}' at {} is missing",
246 field.name, field.segment_path
247 ),
248 )
249 .with_field_path(&field.segment_path)
250 .with_rule(&field.ahb_status);
251 if let Some(first_code) = field.codes.first() {
255 issue.expected_value = Some(first_code.value.clone());
256 }
257 report.add_issue(issue);
258 }
259 }
260 ConditionResult::False => {
261 if is_mandatory_status(&field.ahb_status)
267 && is_field_present(ctx, field)
268 {
269 report.add_issue(
270 ValidationIssue::new(
271 Severity::Error,
272 ValidationCategory::Ahb,
273 ErrorCodes::CONDITIONAL_RULE_VIOLATION,
274 format!(
275 "Field '{}' at {} is present but does not satisfy condition: {}",
276 field.name, field.segment_path, field.ahb_status
277 ),
278 )
279 .with_field_path(&field.segment_path)
280 .with_rule(&field.ahb_status),
281 );
282 }
283 }
284 ConditionResult::Unknown => {
285 let mut external_ids = Vec::new();
290 let mut undetermined_ids = Vec::new();
291 let mut missing_ids = Vec::new();
292 for id in unknown_ids {
293 if self.evaluator.is_external(id) {
294 external_ids.push(id);
295 } else if self.evaluator.is_known(id) {
296 undetermined_ids.push(id);
297 } else {
298 missing_ids.push(id);
299 }
300 }
301
302 let mut parts = Vec::new();
303 if !external_ids.is_empty() {
304 let ids: Vec<String> =
305 external_ids.iter().map(|id| format!("[{id}]")).collect();
306 parts.push(format!(
307 "external conditions require provider: {}",
308 ids.join(", ")
309 ));
310 }
311 if !undetermined_ids.is_empty() {
312 let ids: Vec<String> = undetermined_ids
313 .iter()
314 .map(|id| format!("[{id}]"))
315 .collect();
316 parts.push(format!(
317 "conditions could not be determined from message data: {}",
318 ids.join(", ")
319 ));
320 }
321 if !missing_ids.is_empty() {
322 let ids: Vec<String> =
323 missing_ids.iter().map(|id| format!("[{id}]")).collect();
324 parts.push(format!("missing conditions: {}", ids.join(", ")));
325 }
326 let detail = if parts.is_empty() {
327 String::new()
328 } else {
329 format!(" ({})", parts.join("; "))
330 };
331 report.add_issue(
332 ValidationIssue::new(
333 Severity::Info,
334 ValidationCategory::Ahb,
335 ErrorCodes::CONDITION_UNKNOWN,
336 format!(
337 "Condition for field '{}' could not be fully evaluated{}",
338 field.name, detail
339 ),
340 )
341 .with_field_path(&field.segment_path)
342 .with_rule(&field.ahb_status),
343 );
344 }
345 }
346 }
347
348 self.validate_codes_cross_field(workflow, ctx, report);
353
354 self.validate_package_cardinality(workflow, ctx, report);
357 }
358
359 fn validate_package_cardinality(
366 &self,
367 workflow: &AhbWorkflow,
368 ctx: &EvaluationContext,
369 report: &mut ValidationReport,
370 ) {
371 struct PackageGroup {
374 min: u32,
375 max: u32,
376 code_values: Vec<String>,
377 element_index: usize,
378 component_index: usize,
379 }
380
381 let mut groups: HashMap<(String, u32), PackageGroup> = HashMap::new();
383
384 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
385
386 for field in &workflow.fields {
387 let el_idx = field.element_index.unwrap_or(0);
388 let comp_idx = field.component_index.unwrap_or(0);
389
390 if let Some(ref group_status) = field.parent_group_ahb_status {
394 if group_status.contains('[') {
395 let group_result = expr_eval.evaluate_status_with_ub(
396 group_status,
397 ctx,
398 &workflow.ub_definitions,
399 );
400 if matches!(
401 group_result,
402 ConditionResult::False | ConditionResult::Unknown
403 ) {
404 continue;
405 }
406 }
407 }
408
409 for code in &field.codes {
410 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
412 let mut packages = Vec::new();
414 collect_packages(&expr, &mut packages);
415
416 for (pkg_id, pkg_min, pkg_max) in packages {
417 let key = (field.segment_path.clone(), pkg_id);
418 let group = groups.entry(key).or_insert_with(|| PackageGroup {
419 min: pkg_min,
420 max: pkg_max,
421 code_values: Vec::new(),
422 element_index: el_idx,
423 component_index: comp_idx,
424 });
425 group.min = group.min.max(pkg_min);
428 group.max = group.max.min(pkg_max);
429 group.code_values.push(code.value.clone());
430 }
431 }
432 }
433 }
434
435 for ((seg_path, pkg_id), group) in &groups {
437 let segment_id = extract_segment_id(seg_path);
438 let segments = ctx.find_segments(&segment_id);
439
440 let mut present_count: usize = 0;
442 for seg in &segments {
443 if let Some(value) = seg
444 .elements
445 .get(group.element_index)
446 .and_then(|e| e.get(group.component_index))
447 .filter(|v| !v.is_empty())
448 {
449 if group.code_values.contains(value) {
450 present_count += 1;
451 }
452 }
453 }
454
455 let min = group.min as usize;
456 let max = group.max as usize;
457
458 if present_count < min || present_count > max {
459 let code_list = group.code_values.join(", ");
460 report.add_issue(
461 ValidationIssue::new(
462 Severity::Error,
463 ValidationCategory::Ahb,
464 ErrorCodes::PACKAGE_CARDINALITY_VIOLATION,
465 format!(
466 "Package [{}P{}..{}] at {}: {} code(s) present (allowed {}..{}). Codes in package: [{}]",
467 pkg_id, group.min, group.max, seg_path, present_count, group.min, group.max, code_list
468 ),
469 )
470 .with_field_path(seg_path)
471 .with_expected(format!("{}..{}", group.min, group.max))
472 .with_actual(present_count.to_string()),
473 );
474 }
475 }
476 }
477
478 fn validate_codes_cross_field(
490 &self,
491 workflow: &AhbWorkflow,
492 ctx: &EvaluationContext,
493 report: &mut ValidationReport,
494 ) {
495 if ctx.navigator.is_some() {
496 self.validate_codes_group_scoped(workflow, ctx, report);
497 } else {
498 self.validate_codes_tag_scoped(workflow, ctx, report);
499 }
500 }
501
502 fn validate_codes_group_scoped(
505 &self,
506 workflow: &AhbWorkflow,
507 ctx: &EvaluationContext,
508 report: &mut ValidationReport,
509 ) {
510 type CodeKey = (String, String, usize, usize);
513 let mut codes_by_group: HashMap<CodeKey, HashSet<&str>> = HashMap::new();
514
515 for field in &workflow.fields {
516 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
517 continue;
518 }
519 let tag = extract_segment_id(&field.segment_path);
520 let group_key = extract_group_path_key(&field.segment_path);
521 let el_idx = field.element_index.unwrap_or(0);
522 let comp_idx = field.component_index.unwrap_or(0);
523 let entry = codes_by_group
524 .entry((group_key, tag, el_idx, comp_idx))
525 .or_default();
526 for code in &field.codes {
527 if code.ahb_status == "X" || code.ahb_status.starts_with("Muss") {
528 entry.insert(&code.value);
529 }
530 }
531 }
532
533 let nav = ctx.navigator.unwrap();
534
535 for ((group_key, tag, el_idx, comp_idx), allowed_codes) in &codes_by_group {
536 if allowed_codes.is_empty() {
537 continue;
538 }
539
540 let group_path: Vec<&str> = if group_key.is_empty() {
541 Vec::new()
542 } else {
543 group_key.split('/').collect()
544 };
545
546 if group_path.is_empty() {
548 Self::check_segments_against_codes(
550 ctx.find_segments(tag),
551 allowed_codes,
552 tag,
553 *el_idx,
554 *comp_idx,
555 &format!("{tag}/qualifier"),
556 report,
557 );
558 } else {
559 let instance_count = nav.group_instance_count(&group_path);
560 for i in 0..instance_count {
561 let segments = nav.find_segments_in_group(tag, &group_path, i);
562 let refs: Vec<&OwnedSegment> = segments.iter().collect();
563 Self::check_segments_against_codes(
564 refs,
565 allowed_codes,
566 tag,
567 *el_idx,
568 *comp_idx,
569 &format!("{group_key}/{tag}/qualifier"),
570 report,
571 );
572 }
573 }
574 }
575 }
576
577 fn validate_codes_tag_scoped(
580 &self,
581 workflow: &AhbWorkflow,
582 ctx: &EvaluationContext,
583 report: &mut ValidationReport,
584 ) {
585 let mut codes_by_tag: HashMap<(String, usize, usize), HashSet<&str>> = HashMap::new();
587
588 for field in &workflow.fields {
589 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
590 continue;
591 }
592 let tag = extract_segment_id(&field.segment_path);
593 let el_idx = field.element_index.unwrap_or(0);
594 let comp_idx = field.component_index.unwrap_or(0);
595 let entry = codes_by_tag.entry((tag, el_idx, comp_idx)).or_default();
596 for code in &field.codes {
597 if code.ahb_status == "X" || code.ahb_status.starts_with("Muss") {
598 entry.insert(&code.value);
599 }
600 }
601 }
602
603 for ((tag, el_idx, comp_idx), allowed_codes) in &codes_by_tag {
604 if allowed_codes.is_empty() {
605 continue;
606 }
607 Self::check_segments_against_codes(
608 ctx.find_segments(tag),
609 allowed_codes,
610 tag,
611 *el_idx,
612 *comp_idx,
613 &format!("{tag}/qualifier"),
614 report,
615 );
616 }
617 }
618
619 fn check_segments_against_codes(
621 segments: Vec<&OwnedSegment>,
622 allowed_codes: &HashSet<&str>,
623 _tag: &str,
624 el_idx: usize,
625 comp_idx: usize,
626 field_path: &str,
627 report: &mut ValidationReport,
628 ) {
629 for segment in segments {
630 if let Some(code_value) = segment
631 .elements
632 .get(el_idx)
633 .and_then(|e| e.get(comp_idx))
634 .filter(|v| !v.is_empty())
635 {
636 if !allowed_codes.contains(code_value.as_str()) {
637 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
638 sorted_codes.sort_unstable();
639 report.add_issue(
640 ValidationIssue::new(
641 Severity::Error,
642 ValidationCategory::Code,
643 ErrorCodes::CODE_NOT_ALLOWED_FOR_PID,
644 format!(
645 "Code '{}' is not allowed for this PID. Allowed: [{}]",
646 code_value,
647 sorted_codes.join(", ")
648 ),
649 )
650 .with_field_path(field_path)
651 .with_actual(code_value)
652 .with_expected(sorted_codes.join(", ")),
653 );
654 }
655 }
656 }
657 }
658}
659
660fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
669 let segment_id = extract_segment_id(&field.segment_path);
670
671 if !field.codes.is_empty() {
677 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
678 let required_codes: Vec<&str> =
679 field.codes.iter().map(|c| c.value.as_str()).collect();
680 let matching = ctx.find_segments(&segment_id);
681 return matching.iter().any(|seg| {
682 seg.elements
683 .get(el_idx)
684 .and_then(|e| e.get(comp_idx))
685 .is_some_and(|v| required_codes.contains(&v.as_str()))
686 });
687 }
688 if is_qualifier_field(&field.segment_path) {
691 let required_codes: Vec<&str> =
692 field.codes.iter().map(|c| c.value.as_str()).collect();
693 let el_idx = field.element_index.unwrap_or(0);
694 let comp_idx = field.component_index.unwrap_or(0);
695 let matching = ctx.find_segments(&segment_id);
696 return matching.iter().any(|seg| {
697 seg.elements
698 .get(el_idx)
699 .and_then(|e| e.get(comp_idx))
700 .is_some_and(|v| required_codes.contains(&v.as_str()))
701 });
702 }
703 }
704
705 ctx.has_segment(&segment_id)
706}
707
708fn is_group_variant_absent(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
723 let group_path: Vec<&str> = field
724 .segment_path
725 .split('/')
726 .take_while(|p| p.starts_with("SG"))
727 .collect();
728
729 if group_path.is_empty() {
730 return false;
731 }
732
733 let nav = match ctx.navigator {
734 Some(nav) => nav,
735 None => return false,
736 };
737
738 let instance_count = nav.group_instance_count(&group_path);
739
740 if instance_count == 0 {
745 let is_group_mandatory = field
746 .parent_group_ahb_status
747 .as_deref()
748 .is_some_and(is_mandatory_status);
749 if !is_group_mandatory {
750 return true;
751 }
752 return false;
754 }
755
756 if let Some(ref group_status) = field.parent_group_ahb_status {
760 if !is_mandatory_status(group_status) && !group_status.contains('[') {
761 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
764 let segment_id = extract_segment_id(&field.segment_path);
765 let required_codes: Vec<&str> =
766 field.codes.iter().map(|c| c.value.as_str()).collect();
767
768 let any_instance_has_qualifier = (0..instance_count).any(|i| {
769 nav.find_segments_in_group(&segment_id, &group_path, i)
770 .iter()
771 .any(|seg| {
772 seg.elements
773 .first()
774 .and_then(|e| e.first())
775 .is_some_and(|v| required_codes.contains(&v.as_str()))
776 })
777 });
778
779 if !any_instance_has_qualifier {
780 return true; }
782 }
783 }
784 }
785
786 let segment_id = extract_segment_id(&field.segment_path);
795 let segment_absent_from_all = (0..instance_count).all(|i| {
796 nav.find_segments_in_group(&segment_id, &group_path, i)
797 .is_empty()
798 });
799 if segment_absent_from_all {
800 let group_has_other_segments =
801 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
802 if group_has_other_segments {
803 return true;
804 }
805 }
806
807 false
808}
809
810fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
812 match expr {
813 ConditionExpr::Package { id, min, max } => {
814 out.push((*id, *min, *max));
815 }
816 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
817 for e in exprs {
818 collect_packages(e, out);
819 }
820 }
821 ConditionExpr::Xor(left, right) => {
822 collect_packages(left, out);
823 collect_packages(right, out);
824 }
825 ConditionExpr::Not(inner) => {
826 collect_packages(inner, out);
827 }
828 ConditionExpr::Ref(_) => {}
829 }
830}
831
832fn is_mandatory_status(status: &str) -> bool {
834 let trimmed = status.trim();
835 trimmed.starts_with("Muss") || trimmed.starts_with('X')
836}
837
838fn is_qualifier_field(path: &str) -> bool {
847 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
848 parts.len() == 2
851}
852
853fn extract_group_path_key(path: &str) -> String {
858 let sg_parts: Vec<&str> = path
859 .split('/')
860 .take_while(|p| p.starts_with("SG"))
861 .collect();
862 sg_parts.join("/")
863}
864
865fn extract_segment_id(path: &str) -> String {
867 for part in path.split('/') {
868 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
870 continue;
871 }
872 if part.len() >= 3
874 && part
875 .chars()
876 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
877 {
878 return part.to_string();
879 }
880 }
881 path.split('/').next_back().unwrap_or(path).to_string()
883}
884
885#[cfg(test)]
886mod tests {
887 use super::*;
888 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
889 use std::collections::HashMap;
890
891 struct MockEvaluator {
893 results: HashMap<u32, CR>,
894 }
895
896 impl MockEvaluator {
897 fn new(results: Vec<(u32, CR)>) -> Self {
898 Self {
899 results: results.into_iter().collect(),
900 }
901 }
902
903 fn all_true(ids: &[u32]) -> Self {
904 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
905 }
906 }
907
908 impl ConditionEvaluator for MockEvaluator {
909 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
910 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
911 }
912 fn is_external(&self, _condition: u32) -> bool {
913 false
914 }
915 fn message_type(&self) -> &str {
916 "UTILMD"
917 }
918 fn format_version(&self) -> &str {
919 "FV2510"
920 }
921 }
922
923 #[test]
926 fn test_is_mandatory_status() {
927 assert!(is_mandatory_status("Muss"));
928 assert!(is_mandatory_status("Muss [182] ∧ [152]"));
929 assert!(is_mandatory_status("X"));
930 assert!(is_mandatory_status("X [567]"));
931 assert!(!is_mandatory_status("Soll [1]"));
932 assert!(!is_mandatory_status("Kann [1]"));
933 assert!(!is_mandatory_status(""));
934 }
935
936 #[test]
937 fn test_extract_segment_id_simple() {
938 assert_eq!(extract_segment_id("NAD"), "NAD");
939 }
940
941 #[test]
942 fn test_extract_segment_id_with_sg_prefix() {
943 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
944 }
945
946 #[test]
947 fn test_extract_segment_id_nested_sg() {
948 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
949 }
950
951 #[test]
954 fn test_validate_missing_mandatory_field() {
955 let evaluator = MockEvaluator::all_true(&[182, 152]);
956 let validator = EdifactValidator::new(evaluator);
957 let external = NoOpExternalProvider;
958
959 let workflow = AhbWorkflow {
960 pruefidentifikator: "11001".to_string(),
961 description: "Test".to_string(),
962 communication_direction: None,
963 fields: vec![AhbFieldRule {
964 segment_path: "SG2/NAD/C082/3039".to_string(),
965 name: "MP-ID des MSB".to_string(),
966 ahb_status: "Muss [182] ∧ [152]".to_string(),
967 codes: vec![],
968 parent_group_ahb_status: None,
969 ..Default::default()
970 }],
971 ub_definitions: HashMap::new(),
972 };
973
974 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
976
977 assert!(!report.is_valid());
979 let errors: Vec<_> = report.errors().collect();
980 assert_eq!(errors.len(), 1);
981 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
982 assert!(errors[0].message.contains("MP-ID des MSB"));
983 }
984
985 #[test]
986 fn test_validate_condition_false_no_error() {
987 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
989 let validator = EdifactValidator::new(evaluator);
990 let external = NoOpExternalProvider;
991
992 let workflow = AhbWorkflow {
993 pruefidentifikator: "11001".to_string(),
994 description: "Test".to_string(),
995 communication_direction: None,
996 fields: vec![AhbFieldRule {
997 segment_path: "NAD".to_string(),
998 name: "Partnerrolle".to_string(),
999 ahb_status: "Muss [182] ∧ [152]".to_string(),
1000 codes: vec![],
1001 parent_group_ahb_status: None,
1002 ..Default::default()
1003 }],
1004 ub_definitions: HashMap::new(),
1005 };
1006
1007 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1008
1009 assert!(report.is_valid());
1011 }
1012
1013 #[test]
1014 fn test_validate_condition_unknown_adds_info() {
1015 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
1017 let validator = EdifactValidator::new(evaluator);
1019 let external = NoOpExternalProvider;
1020
1021 let workflow = AhbWorkflow {
1022 pruefidentifikator: "11001".to_string(),
1023 description: "Test".to_string(),
1024 communication_direction: None,
1025 fields: vec![AhbFieldRule {
1026 segment_path: "NAD".to_string(),
1027 name: "Partnerrolle".to_string(),
1028 ahb_status: "Muss [182] ∧ [152]".to_string(),
1029 codes: vec![],
1030 parent_group_ahb_status: None,
1031 ..Default::default()
1032 }],
1033 ub_definitions: HashMap::new(),
1034 };
1035
1036 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1037
1038 assert!(report.is_valid());
1040 let infos: Vec<_> = report.infos().collect();
1041 assert_eq!(infos.len(), 1);
1042 assert_eq!(infos[0].code, ErrorCodes::CONDITION_UNKNOWN);
1043 }
1044
1045 #[test]
1046 fn test_validate_structure_level_skips_conditions() {
1047 let evaluator = MockEvaluator::all_true(&[182, 152]);
1048 let validator = EdifactValidator::new(evaluator);
1049 let external = NoOpExternalProvider;
1050
1051 let workflow = AhbWorkflow {
1052 pruefidentifikator: "11001".to_string(),
1053 description: "Test".to_string(),
1054 communication_direction: None,
1055 fields: vec![AhbFieldRule {
1056 segment_path: "NAD".to_string(),
1057 name: "Partnerrolle".to_string(),
1058 ahb_status: "Muss [182] ∧ [152]".to_string(),
1059 codes: vec![],
1060 parent_group_ahb_status: None,
1061 ..Default::default()
1062 }],
1063 ub_definitions: HashMap::new(),
1064 };
1065
1066 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
1068
1069 assert!(report.is_valid());
1071 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
1072 }
1073
1074 #[test]
1075 fn test_validate_empty_workflow_no_condition_errors() {
1076 let evaluator = MockEvaluator::all_true(&[]);
1077 let validator = EdifactValidator::new(evaluator);
1078 let external = NoOpExternalProvider;
1079
1080 let empty_workflow = AhbWorkflow {
1081 pruefidentifikator: String::new(),
1082 description: String::new(),
1083 communication_direction: None,
1084 fields: vec![],
1085 ub_definitions: HashMap::new(),
1086 };
1087
1088 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
1089
1090 assert!(report.is_valid());
1091 }
1092
1093 #[test]
1094 fn test_validate_bare_muss_always_required() {
1095 let evaluator = MockEvaluator::new(vec![]);
1096 let validator = EdifactValidator::new(evaluator);
1097 let external = NoOpExternalProvider;
1098
1099 let workflow = AhbWorkflow {
1100 pruefidentifikator: "55001".to_string(),
1101 description: "Test".to_string(),
1102 communication_direction: Some("NB an LF".to_string()),
1103 fields: vec![AhbFieldRule {
1104 segment_path: "SG2/NAD/3035".to_string(),
1105 name: "Partnerrolle".to_string(),
1106 ahb_status: "Muss".to_string(), codes: vec![],
1108 parent_group_ahb_status: None,
1109 ..Default::default()
1110 }],
1111 ub_definitions: HashMap::new(),
1112 };
1113
1114 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1115
1116 assert!(!report.is_valid());
1118 assert_eq!(report.error_count(), 1);
1119 }
1120
1121 #[test]
1122 fn test_validate_x_status_is_mandatory() {
1123 let evaluator = MockEvaluator::new(vec![]);
1124 let validator = EdifactValidator::new(evaluator);
1125 let external = NoOpExternalProvider;
1126
1127 let workflow = AhbWorkflow {
1128 pruefidentifikator: "55001".to_string(),
1129 description: "Test".to_string(),
1130 communication_direction: None,
1131 fields: vec![AhbFieldRule {
1132 segment_path: "DTM".to_string(),
1133 name: "Datum".to_string(),
1134 ahb_status: "X".to_string(),
1135 codes: vec![],
1136 parent_group_ahb_status: None,
1137 ..Default::default()
1138 }],
1139 ub_definitions: HashMap::new(),
1140 };
1141
1142 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1143
1144 assert!(!report.is_valid());
1145 let errors: Vec<_> = report.errors().collect();
1146 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1147 }
1148
1149 #[test]
1150 fn test_validate_soll_not_mandatory() {
1151 let evaluator = MockEvaluator::new(vec![]);
1152 let validator = EdifactValidator::new(evaluator);
1153 let external = NoOpExternalProvider;
1154
1155 let workflow = AhbWorkflow {
1156 pruefidentifikator: "55001".to_string(),
1157 description: "Test".to_string(),
1158 communication_direction: None,
1159 fields: vec![AhbFieldRule {
1160 segment_path: "DTM".to_string(),
1161 name: "Datum".to_string(),
1162 ahb_status: "Soll".to_string(),
1163 codes: vec![],
1164 parent_group_ahb_status: None,
1165 ..Default::default()
1166 }],
1167 ub_definitions: HashMap::new(),
1168 };
1169
1170 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1171
1172 assert!(report.is_valid());
1174 }
1175
1176 #[test]
1177 fn test_report_includes_metadata() {
1178 let evaluator = MockEvaluator::new(vec![]);
1179 let validator = EdifactValidator::new(evaluator);
1180 let external = NoOpExternalProvider;
1181
1182 let workflow = AhbWorkflow {
1183 pruefidentifikator: "55001".to_string(),
1184 description: String::new(),
1185 communication_direction: None,
1186 fields: vec![],
1187 ub_definitions: HashMap::new(),
1188 };
1189
1190 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
1191
1192 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
1193 assert_eq!(report.level, ValidationLevel::Full);
1194 assert_eq!(report.message_type, "UTILMD");
1195 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
1196 }
1197
1198 #[test]
1199 fn test_validate_with_navigator_returns_report() {
1200 let evaluator = MockEvaluator::all_true(&[]);
1201 let validator = EdifactValidator::new(evaluator);
1202 let external = NoOpExternalProvider;
1203 let nav = crate::eval::NoOpGroupNavigator;
1204
1205 let workflow = AhbWorkflow {
1206 pruefidentifikator: "55001".to_string(),
1207 description: "Test".to_string(),
1208 communication_direction: None,
1209 fields: vec![],
1210 ub_definitions: HashMap::new(),
1211 };
1212
1213 let report = validator.validate_with_navigator(
1214 &[],
1215 &workflow,
1216 &external,
1217 ValidationLevel::Full,
1218 &nav,
1219 );
1220 assert!(report.is_valid());
1221 }
1222
1223 #[test]
1224 fn test_code_validation_skips_composite_paths() {
1225 let evaluator = MockEvaluator::new(vec![]);
1229 let validator = EdifactValidator::new(evaluator);
1230 let external = NoOpExternalProvider;
1231
1232 let unh_segment = OwnedSegment {
1233 id: "UNH".to_string(),
1234 elements: vec![
1235 vec!["ALEXANDE951842".to_string()], vec![
1237 "UTILMD".to_string(),
1238 "D".to_string(),
1239 "11A".to_string(),
1240 "UN".to_string(),
1241 "S2.1".to_string(),
1242 ],
1243 ],
1244 segment_number: 1,
1245 };
1246
1247 let workflow = AhbWorkflow {
1248 pruefidentifikator: "55001".to_string(),
1249 description: "Test".to_string(),
1250 communication_direction: None,
1251 fields: vec![
1252 AhbFieldRule {
1253 segment_path: "UNH/S009/0065".to_string(),
1254 name: "Nachrichtentyp".to_string(),
1255 ahb_status: "X".to_string(),
1256 codes: vec![AhbCodeRule {
1257 value: "UTILMD".to_string(),
1258 description: "Stammdaten".to_string(),
1259 ahb_status: "X".to_string(),
1260 }],
1261 parent_group_ahb_status: None,
1262 ..Default::default()
1263 },
1264 AhbFieldRule {
1265 segment_path: "UNH/S009/0052".to_string(),
1266 name: "Version".to_string(),
1267 ahb_status: "X".to_string(),
1268 codes: vec![AhbCodeRule {
1269 value: "D".to_string(),
1270 description: "Draft".to_string(),
1271 ahb_status: "X".to_string(),
1272 }],
1273 parent_group_ahb_status: None,
1274 ..Default::default()
1275 },
1276 ],
1277 ub_definitions: HashMap::new(),
1278 };
1279
1280 let report = validator.validate(
1281 &[unh_segment],
1282 &workflow,
1283 &external,
1284 ValidationLevel::Conditions,
1285 );
1286
1287 let code_errors: Vec<_> = report
1289 .by_category(ValidationCategory::Code)
1290 .filter(|i| i.severity == Severity::Error)
1291 .collect();
1292 assert!(
1293 code_errors.is_empty(),
1294 "Expected no code errors for composite paths, got: {:?}",
1295 code_errors
1296 );
1297 }
1298
1299 #[test]
1300 fn test_cross_field_code_validation_valid_qualifiers() {
1301 let evaluator = MockEvaluator::new(vec![]);
1304 let validator = EdifactValidator::new(evaluator);
1305 let external = NoOpExternalProvider;
1306
1307 let nad_ms = OwnedSegment {
1308 id: "NAD".to_string(),
1309 elements: vec![vec!["MS".to_string()]],
1310 segment_number: 4,
1311 };
1312 let nad_mr = OwnedSegment {
1313 id: "NAD".to_string(),
1314 elements: vec![vec!["MR".to_string()]],
1315 segment_number: 5,
1316 };
1317
1318 let workflow = AhbWorkflow {
1319 pruefidentifikator: "55001".to_string(),
1320 description: "Test".to_string(),
1321 communication_direction: None,
1322 fields: vec![
1323 AhbFieldRule {
1324 segment_path: "SG2/NAD/3035".to_string(),
1325 name: "Absender".to_string(),
1326 ahb_status: "X".to_string(),
1327 codes: vec![AhbCodeRule {
1328 value: "MS".to_string(),
1329 description: "Absender".to_string(),
1330 ahb_status: "X".to_string(),
1331 }],
1332 parent_group_ahb_status: None,
1333 ..Default::default()
1334 },
1335 AhbFieldRule {
1336 segment_path: "SG2/NAD/3035".to_string(),
1337 name: "Empfaenger".to_string(),
1338 ahb_status: "X".to_string(),
1339 codes: vec![AhbCodeRule {
1340 value: "MR".to_string(),
1341 description: "Empfaenger".to_string(),
1342 ahb_status: "X".to_string(),
1343 }],
1344 parent_group_ahb_status: None,
1345 ..Default::default()
1346 },
1347 ],
1348 ub_definitions: HashMap::new(),
1349 };
1350
1351 let report = validator.validate(
1352 &[nad_ms, nad_mr],
1353 &workflow,
1354 &external,
1355 ValidationLevel::Conditions,
1356 );
1357
1358 let code_errors: Vec<_> = report
1359 .by_category(ValidationCategory::Code)
1360 .filter(|i| i.severity == Severity::Error)
1361 .collect();
1362 assert!(
1363 code_errors.is_empty(),
1364 "Expected no code errors for valid qualifiers, got: {:?}",
1365 code_errors
1366 );
1367 }
1368
1369 #[test]
1370 fn test_cross_field_code_validation_catches_invalid_qualifier() {
1371 let evaluator = MockEvaluator::new(vec![]);
1373 let validator = EdifactValidator::new(evaluator);
1374 let external = NoOpExternalProvider;
1375
1376 let nad_ms = OwnedSegment {
1377 id: "NAD".to_string(),
1378 elements: vec![vec!["MS".to_string()]],
1379 segment_number: 4,
1380 };
1381 let nad_mt = OwnedSegment {
1382 id: "NAD".to_string(),
1383 elements: vec![vec!["MT".to_string()]], segment_number: 5,
1385 };
1386
1387 let workflow = AhbWorkflow {
1388 pruefidentifikator: "55001".to_string(),
1389 description: "Test".to_string(),
1390 communication_direction: None,
1391 fields: vec![
1392 AhbFieldRule {
1393 segment_path: "SG2/NAD/3035".to_string(),
1394 name: "Absender".to_string(),
1395 ahb_status: "X".to_string(),
1396 codes: vec![AhbCodeRule {
1397 value: "MS".to_string(),
1398 description: "Absender".to_string(),
1399 ahb_status: "X".to_string(),
1400 }],
1401 parent_group_ahb_status: None,
1402 ..Default::default()
1403 },
1404 AhbFieldRule {
1405 segment_path: "SG2/NAD/3035".to_string(),
1406 name: "Empfaenger".to_string(),
1407 ahb_status: "X".to_string(),
1408 codes: vec![AhbCodeRule {
1409 value: "MR".to_string(),
1410 description: "Empfaenger".to_string(),
1411 ahb_status: "X".to_string(),
1412 }],
1413 parent_group_ahb_status: None,
1414 ..Default::default()
1415 },
1416 ],
1417 ub_definitions: HashMap::new(),
1418 };
1419
1420 let report = validator.validate(
1421 &[nad_ms, nad_mt],
1422 &workflow,
1423 &external,
1424 ValidationLevel::Conditions,
1425 );
1426
1427 let code_errors: Vec<_> = report
1428 .by_category(ValidationCategory::Code)
1429 .filter(|i| i.severity == Severity::Error)
1430 .collect();
1431 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
1432 assert!(code_errors[0].message.contains("MT"));
1433 assert!(code_errors[0].message.contains("MR"));
1434 assert!(code_errors[0].message.contains("MS"));
1435 }
1436
1437 #[test]
1438 fn test_cross_field_code_validation_unions_across_groups() {
1439 let evaluator = MockEvaluator::new(vec![]);
1443 let validator = EdifactValidator::new(evaluator);
1444 let external = NoOpExternalProvider;
1445
1446 let segments = vec![
1447 OwnedSegment {
1448 id: "NAD".to_string(),
1449 elements: vec![vec!["MS".to_string()]],
1450 segment_number: 3,
1451 },
1452 OwnedSegment {
1453 id: "NAD".to_string(),
1454 elements: vec![vec!["MR".to_string()]],
1455 segment_number: 4,
1456 },
1457 OwnedSegment {
1458 id: "NAD".to_string(),
1459 elements: vec![vec!["Z04".to_string()]],
1460 segment_number: 20,
1461 },
1462 OwnedSegment {
1463 id: "NAD".to_string(),
1464 elements: vec![vec!["Z09".to_string()]],
1465 segment_number: 21,
1466 },
1467 OwnedSegment {
1468 id: "NAD".to_string(),
1469 elements: vec![vec!["MT".to_string()]], segment_number: 22,
1471 },
1472 ];
1473
1474 let workflow = AhbWorkflow {
1475 pruefidentifikator: "55001".to_string(),
1476 description: "Test".to_string(),
1477 communication_direction: None,
1478 fields: vec![
1479 AhbFieldRule {
1480 segment_path: "SG2/NAD/3035".to_string(),
1481 name: "Absender".to_string(),
1482 ahb_status: "X".to_string(),
1483 codes: vec![AhbCodeRule {
1484 value: "MS".to_string(),
1485 description: "Absender".to_string(),
1486 ahb_status: "X".to_string(),
1487 }],
1488 parent_group_ahb_status: None,
1489 ..Default::default()
1490 },
1491 AhbFieldRule {
1492 segment_path: "SG2/NAD/3035".to_string(),
1493 name: "Empfaenger".to_string(),
1494 ahb_status: "X".to_string(),
1495 codes: vec![AhbCodeRule {
1496 value: "MR".to_string(),
1497 description: "Empfaenger".to_string(),
1498 ahb_status: "X".to_string(),
1499 }],
1500 parent_group_ahb_status: None,
1501 ..Default::default()
1502 },
1503 AhbFieldRule {
1504 segment_path: "SG4/SG12/NAD/3035".to_string(),
1505 name: "Anschlussnutzer".to_string(),
1506 ahb_status: "X".to_string(),
1507 codes: vec![AhbCodeRule {
1508 value: "Z04".to_string(),
1509 description: "Anschlussnutzer".to_string(),
1510 ahb_status: "X".to_string(),
1511 }],
1512 parent_group_ahb_status: None,
1513 ..Default::default()
1514 },
1515 AhbFieldRule {
1516 segment_path: "SG4/SG12/NAD/3035".to_string(),
1517 name: "Korrespondenzanschrift".to_string(),
1518 ahb_status: "X".to_string(),
1519 codes: vec![AhbCodeRule {
1520 value: "Z09".to_string(),
1521 description: "Korrespondenzanschrift".to_string(),
1522 ahb_status: "X".to_string(),
1523 }],
1524 parent_group_ahb_status: None,
1525 ..Default::default()
1526 },
1527 ],
1528 ub_definitions: HashMap::new(),
1529 };
1530
1531 let report =
1532 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
1533
1534 let code_errors: Vec<_> = report
1535 .by_category(ValidationCategory::Code)
1536 .filter(|i| i.severity == Severity::Error)
1537 .collect();
1538 assert_eq!(
1539 code_errors.len(),
1540 1,
1541 "Expected exactly one COD002 error for MT, got: {:?}",
1542 code_errors
1543 );
1544 assert!(code_errors[0].message.contains("MT"));
1545 }
1546
1547 #[test]
1548 fn test_is_qualifier_field_simple_paths() {
1549 assert!(is_qualifier_field("NAD/3035"));
1550 assert!(is_qualifier_field("SG2/NAD/3035"));
1551 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
1552 assert!(is_qualifier_field("LOC/3227"));
1553 }
1554
1555 #[test]
1556 fn test_is_qualifier_field_composite_paths() {
1557 assert!(!is_qualifier_field("UNH/S009/0065"));
1558 assert!(!is_qualifier_field("NAD/C082/3039"));
1559 assert!(!is_qualifier_field("SG2/NAD/C082/3039"));
1560 }
1561
1562 #[test]
1563 fn test_is_qualifier_field_bare_segment() {
1564 assert!(!is_qualifier_field("NAD"));
1565 assert!(!is_qualifier_field("SG2/NAD"));
1566 }
1567
1568 #[test]
1569 fn test_missing_qualifier_instance_is_detected() {
1570 let evaluator = MockEvaluator::new(vec![]);
1573 let validator = EdifactValidator::new(evaluator);
1574 let external = NoOpExternalProvider;
1575
1576 let nad_ms = OwnedSegment {
1577 id: "NAD".to_string(),
1578 elements: vec![vec!["MS".to_string()]],
1579 segment_number: 3,
1580 };
1581
1582 let workflow = AhbWorkflow {
1583 pruefidentifikator: "55001".to_string(),
1584 description: "Test".to_string(),
1585 communication_direction: None,
1586 fields: vec![
1587 AhbFieldRule {
1588 segment_path: "SG2/NAD/3035".to_string(),
1589 name: "Absender".to_string(),
1590 ahb_status: "X".to_string(),
1591 codes: vec![AhbCodeRule {
1592 value: "MS".to_string(),
1593 description: "Absender".to_string(),
1594 ahb_status: "X".to_string(),
1595 }],
1596 parent_group_ahb_status: None,
1597 ..Default::default()
1598 },
1599 AhbFieldRule {
1600 segment_path: "SG2/NAD/3035".to_string(),
1601 name: "Empfaenger".to_string(),
1602 ahb_status: "Muss".to_string(),
1603 codes: vec![AhbCodeRule {
1604 value: "MR".to_string(),
1605 description: "Empfaenger".to_string(),
1606 ahb_status: "X".to_string(),
1607 }],
1608 parent_group_ahb_status: None,
1609 ..Default::default()
1610 },
1611 ],
1612 ub_definitions: HashMap::new(),
1613 };
1614
1615 let report =
1616 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
1617
1618 let ahb_errors: Vec<_> = report
1619 .by_category(ValidationCategory::Ahb)
1620 .filter(|i| i.severity == Severity::Error)
1621 .collect();
1622 assert_eq!(
1623 ahb_errors.len(),
1624 1,
1625 "Expected AHB001 for missing NAD+MR, got: {:?}",
1626 ahb_errors
1627 );
1628 assert!(ahb_errors[0].message.contains("Empfaenger"));
1629 }
1630
1631 #[test]
1632 fn test_present_qualifier_instance_no_error() {
1633 let evaluator = MockEvaluator::new(vec![]);
1635 let validator = EdifactValidator::new(evaluator);
1636 let external = NoOpExternalProvider;
1637
1638 let segments = vec![
1639 OwnedSegment {
1640 id: "NAD".to_string(),
1641 elements: vec![vec!["MS".to_string()]],
1642 segment_number: 3,
1643 },
1644 OwnedSegment {
1645 id: "NAD".to_string(),
1646 elements: vec![vec!["MR".to_string()]],
1647 segment_number: 4,
1648 },
1649 ];
1650
1651 let workflow = AhbWorkflow {
1652 pruefidentifikator: "55001".to_string(),
1653 description: "Test".to_string(),
1654 communication_direction: None,
1655 fields: vec![
1656 AhbFieldRule {
1657 segment_path: "SG2/NAD/3035".to_string(),
1658 name: "Absender".to_string(),
1659 ahb_status: "Muss".to_string(),
1660 codes: vec![AhbCodeRule {
1661 value: "MS".to_string(),
1662 description: "Absender".to_string(),
1663 ahb_status: "X".to_string(),
1664 }],
1665 parent_group_ahb_status: None,
1666 ..Default::default()
1667 },
1668 AhbFieldRule {
1669 segment_path: "SG2/NAD/3035".to_string(),
1670 name: "Empfaenger".to_string(),
1671 ahb_status: "Muss".to_string(),
1672 codes: vec![AhbCodeRule {
1673 value: "MR".to_string(),
1674 description: "Empfaenger".to_string(),
1675 ahb_status: "X".to_string(),
1676 }],
1677 parent_group_ahb_status: None,
1678 ..Default::default()
1679 },
1680 ],
1681 ub_definitions: HashMap::new(),
1682 };
1683
1684 let report =
1685 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
1686
1687 let ahb_errors: Vec<_> = report
1688 .by_category(ValidationCategory::Ahb)
1689 .filter(|i| i.severity == Severity::Error)
1690 .collect();
1691 assert!(
1692 ahb_errors.is_empty(),
1693 "Expected no AHB001 errors, got: {:?}",
1694 ahb_errors
1695 );
1696 }
1697
1698 #[test]
1699 fn test_extract_group_path_key() {
1700 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
1701 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
1702 assert_eq!(extract_group_path_key("NAD/3035"), "");
1703 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
1704 }
1705
1706 #[test]
1707 fn test_absent_optional_group_no_missing_field_error() {
1708 use mig_types::navigator::GroupNavigator;
1711
1712 struct NavWithoutSG3;
1713 impl GroupNavigator for NavWithoutSG3 {
1714 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
1715 vec![]
1716 }
1717 fn find_segments_with_qualifier_in_group(
1718 &self,
1719 _: &str,
1720 _: usize,
1721 _: &str,
1722 _: &[&str],
1723 _: usize,
1724 ) -> Vec<OwnedSegment> {
1725 vec![]
1726 }
1727 fn group_instance_count(&self, group_path: &[&str]) -> usize {
1728 match group_path {
1729 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
1732 }
1733 }
1734 }
1735
1736 let evaluator = MockEvaluator::new(vec![]);
1737 let validator = EdifactValidator::new(evaluator);
1738 let external = NoOpExternalProvider;
1739 let nav = NavWithoutSG3;
1740
1741 let segments = vec![
1743 OwnedSegment {
1744 id: "NAD".into(),
1745 elements: vec![vec!["MS".into()]],
1746 segment_number: 3,
1747 },
1748 OwnedSegment {
1749 id: "NAD".into(),
1750 elements: vec![vec!["MR".into()]],
1751 segment_number: 4,
1752 },
1753 ];
1754
1755 let workflow = AhbWorkflow {
1756 pruefidentifikator: "55001".to_string(),
1757 description: "Test".to_string(),
1758 communication_direction: None,
1759 fields: vec![
1760 AhbFieldRule {
1761 segment_path: "SG2/SG3/CTA/3139".to_string(),
1762 name: "Funktion des Ansprechpartners, Code".to_string(),
1763 ahb_status: "Muss".to_string(),
1764 codes: vec![],
1765 parent_group_ahb_status: None,
1766 ..Default::default()
1767 },
1768 AhbFieldRule {
1769 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
1770 name: "Name vom Ansprechpartner".to_string(),
1771 ahb_status: "X".to_string(),
1772 codes: vec![],
1773 parent_group_ahb_status: None,
1774 ..Default::default()
1775 },
1776 ],
1777 ub_definitions: HashMap::new(),
1778 };
1779
1780 let report = validator.validate_with_navigator(
1781 &segments,
1782 &workflow,
1783 &external,
1784 ValidationLevel::Conditions,
1785 &nav,
1786 );
1787
1788 let ahb_errors: Vec<_> = report
1789 .by_category(ValidationCategory::Ahb)
1790 .filter(|i| i.severity == Severity::Error)
1791 .collect();
1792 assert!(
1793 ahb_errors.is_empty(),
1794 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
1795 ahb_errors
1796 );
1797 }
1798
1799 #[test]
1800 fn test_present_group_still_checks_mandatory_fields() {
1801 use mig_types::navigator::GroupNavigator;
1803
1804 struct NavWithSG3;
1805 impl GroupNavigator for NavWithSG3 {
1806 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
1807 vec![]
1808 }
1809 fn find_segments_with_qualifier_in_group(
1810 &self,
1811 _: &str,
1812 _: usize,
1813 _: &str,
1814 _: &[&str],
1815 _: usize,
1816 ) -> Vec<OwnedSegment> {
1817 vec![]
1818 }
1819 fn group_instance_count(&self, group_path: &[&str]) -> usize {
1820 match group_path {
1821 ["SG2"] => 1,
1822 ["SG2", "SG3"] => 1, _ => 0,
1824 }
1825 }
1826 }
1827
1828 let evaluator = MockEvaluator::new(vec![]);
1829 let validator = EdifactValidator::new(evaluator);
1830 let external = NoOpExternalProvider;
1831 let nav = NavWithSG3;
1832
1833 let segments = vec![OwnedSegment {
1835 id: "NAD".into(),
1836 elements: vec![vec!["MS".into()]],
1837 segment_number: 3,
1838 }];
1839
1840 let workflow = AhbWorkflow {
1841 pruefidentifikator: "55001".to_string(),
1842 description: "Test".to_string(),
1843 communication_direction: None,
1844 fields: vec![AhbFieldRule {
1845 segment_path: "SG2/SG3/CTA/3139".to_string(),
1846 name: "Funktion des Ansprechpartners, Code".to_string(),
1847 ahb_status: "Muss".to_string(),
1848 codes: vec![],
1849 parent_group_ahb_status: None,
1850 ..Default::default()
1851 }],
1852 ub_definitions: HashMap::new(),
1853 };
1854
1855 let report = validator.validate_with_navigator(
1856 &segments,
1857 &workflow,
1858 &external,
1859 ValidationLevel::Conditions,
1860 &nav,
1861 );
1862
1863 let ahb_errors: Vec<_> = report
1864 .by_category(ValidationCategory::Ahb)
1865 .filter(|i| i.severity == Severity::Error)
1866 .collect();
1867 assert_eq!(
1868 ahb_errors.len(),
1869 1,
1870 "Expected AHB001 error when SG3 is present but CTA missing"
1871 );
1872 assert!(ahb_errors[0].message.contains("CTA"));
1873 }
1874
1875 #[test]
1876 fn test_missing_qualifier_with_navigator_is_detected() {
1877 use mig_types::navigator::GroupNavigator;
1880
1881 struct NavWithSG2;
1882 impl GroupNavigator for NavWithSG2 {
1883 fn find_segments_in_group(
1884 &self,
1885 segment_id: &str,
1886 group_path: &[&str],
1887 instance_index: usize,
1888 ) -> Vec<OwnedSegment> {
1889 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
1890 vec![OwnedSegment {
1891 id: "NAD".into(),
1892 elements: vec![vec!["MS".into()]],
1893 segment_number: 3,
1894 }]
1895 } else {
1896 vec![]
1897 }
1898 }
1899 fn find_segments_with_qualifier_in_group(
1900 &self,
1901 _: &str,
1902 _: usize,
1903 _: &str,
1904 _: &[&str],
1905 _: usize,
1906 ) -> Vec<OwnedSegment> {
1907 vec![]
1908 }
1909 fn group_instance_count(&self, group_path: &[&str]) -> usize {
1910 match group_path {
1911 ["SG2"] => 1,
1912 _ => 0,
1913 }
1914 }
1915 }
1916
1917 let evaluator = MockEvaluator::new(vec![]);
1918 let validator = EdifactValidator::new(evaluator);
1919 let external = NoOpExternalProvider;
1920 let nav = NavWithSG2;
1921
1922 let segments = vec![OwnedSegment {
1923 id: "NAD".into(),
1924 elements: vec![vec!["MS".into()]],
1925 segment_number: 3,
1926 }];
1927
1928 let workflow = AhbWorkflow {
1929 pruefidentifikator: "55001".to_string(),
1930 description: "Test".to_string(),
1931 communication_direction: None,
1932 fields: vec![
1933 AhbFieldRule {
1934 segment_path: "SG2/NAD/3035".to_string(),
1935 name: "Absender".to_string(),
1936 ahb_status: "X".to_string(),
1937 codes: vec![AhbCodeRule {
1938 value: "MS".to_string(),
1939 description: "Absender".to_string(),
1940 ahb_status: "X".to_string(),
1941 }],
1942 parent_group_ahb_status: None,
1943 ..Default::default()
1944 },
1945 AhbFieldRule {
1946 segment_path: "SG2/NAD/3035".to_string(),
1947 name: "Empfaenger".to_string(),
1948 ahb_status: "Muss".to_string(),
1949 codes: vec![AhbCodeRule {
1950 value: "MR".to_string(),
1951 description: "Empfaenger".to_string(),
1952 ahb_status: "X".to_string(),
1953 }],
1954 parent_group_ahb_status: None,
1955 ..Default::default()
1956 },
1957 ],
1958 ub_definitions: HashMap::new(),
1959 };
1960
1961 let report = validator.validate_with_navigator(
1962 &segments,
1963 &workflow,
1964 &external,
1965 ValidationLevel::Conditions,
1966 &nav,
1967 );
1968
1969 let ahb_errors: Vec<_> = report
1970 .by_category(ValidationCategory::Ahb)
1971 .filter(|i| i.severity == Severity::Error)
1972 .collect();
1973 assert_eq!(
1974 ahb_errors.len(),
1975 1,
1976 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
1977 ahb_errors
1978 );
1979 assert!(ahb_errors[0].message.contains("Empfaenger"));
1980 }
1981
1982 #[test]
1983 fn test_optional_group_variant_absent_no_error() {
1984 use mig_types::navigator::GroupNavigator;
1989
1990 struct TestNav;
1991 impl GroupNavigator for TestNav {
1992 fn find_segments_in_group(
1993 &self,
1994 segment_id: &str,
1995 group_path: &[&str],
1996 instance_index: usize,
1997 ) -> Vec<OwnedSegment> {
1998 match (segment_id, group_path, instance_index) {
1999 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
2000 id: "LOC".into(),
2001 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2002 segment_number: 10,
2003 }],
2004 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
2005 id: "NAD".into(),
2006 elements: vec![vec!["MS".into()]],
2007 segment_number: 3,
2008 }],
2009 _ => vec![],
2010 }
2011 }
2012 fn find_segments_with_qualifier_in_group(
2013 &self,
2014 _: &str,
2015 _: usize,
2016 _: &str,
2017 _: &[&str],
2018 _: usize,
2019 ) -> Vec<OwnedSegment> {
2020 vec![]
2021 }
2022 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2023 match group_path {
2024 ["SG2"] => 1,
2025 ["SG4"] => 1,
2026 ["SG4", "SG5"] => 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 = TestNav;
2036
2037 let segments = vec![
2038 OwnedSegment {
2039 id: "NAD".into(),
2040 elements: vec![vec!["MS".into()]],
2041 segment_number: 3,
2042 },
2043 OwnedSegment {
2044 id: "LOC".into(),
2045 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2046 segment_number: 10,
2047 },
2048 ];
2049
2050 let workflow = AhbWorkflow {
2051 pruefidentifikator: "55001".to_string(),
2052 description: "Test".to_string(),
2053 communication_direction: None,
2054 fields: vec![
2055 AhbFieldRule {
2057 segment_path: "SG2/NAD/3035".to_string(),
2058 name: "Absender".to_string(),
2059 ahb_status: "X".to_string(),
2060 codes: vec![AhbCodeRule {
2061 value: "MS".to_string(),
2062 description: "Absender".to_string(),
2063 ahb_status: "X".to_string(),
2064 }],
2065 parent_group_ahb_status: Some("Muss".to_string()),
2066 ..Default::default()
2067 },
2068 AhbFieldRule {
2069 segment_path: "SG2/NAD/3035".to_string(),
2070 name: "Empfaenger".to_string(),
2071 ahb_status: "Muss".to_string(),
2072 codes: vec![AhbCodeRule {
2073 value: "MR".to_string(),
2074 description: "Empfaenger".to_string(),
2075 ahb_status: "X".to_string(),
2076 }],
2077 parent_group_ahb_status: Some("Muss".to_string()),
2078 ..Default::default()
2079 },
2080 AhbFieldRule {
2082 segment_path: "SG4/SG5/LOC/3227".to_string(),
2083 name: "Ortsangabe, Qualifier (Z16)".to_string(),
2084 ahb_status: "X".to_string(),
2085 codes: vec![AhbCodeRule {
2086 value: "Z16".to_string(),
2087 description: "Marktlokation".to_string(),
2088 ahb_status: "X".to_string(),
2089 }],
2090 parent_group_ahb_status: Some("Kann".to_string()),
2091 ..Default::default()
2092 },
2093 AhbFieldRule {
2094 segment_path: "SG4/SG5/LOC/3227".to_string(),
2095 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2096 ahb_status: "Muss".to_string(),
2097 codes: vec![AhbCodeRule {
2098 value: "Z17".to_string(),
2099 description: "Messlokation".to_string(),
2100 ahb_status: "X".to_string(),
2101 }],
2102 parent_group_ahb_status: Some("Kann".to_string()),
2103 ..Default::default()
2104 },
2105 ],
2106 ub_definitions: HashMap::new(),
2107 };
2108
2109 let report = validator.validate_with_navigator(
2110 &segments,
2111 &workflow,
2112 &external,
2113 ValidationLevel::Conditions,
2114 &nav,
2115 );
2116
2117 let ahb_errors: Vec<_> = report
2118 .by_category(ValidationCategory::Ahb)
2119 .filter(|i| i.severity == Severity::Error)
2120 .collect();
2121
2122 assert_eq!(
2125 ahb_errors.len(),
2126 1,
2127 "Expected only AHB001 for missing NAD+MR, got: {:?}",
2128 ahb_errors
2129 );
2130 assert!(
2131 ahb_errors[0].message.contains("Empfaenger"),
2132 "Error should be for missing NAD+MR (Empfaenger)"
2133 );
2134 }
2135
2136 #[test]
2137 fn test_conditional_group_variant_absent_no_error() {
2138 use mig_types::navigator::GroupNavigator;
2143
2144 struct TestNav;
2145 impl GroupNavigator for TestNav {
2146 fn find_segments_in_group(
2147 &self,
2148 segment_id: &str,
2149 group_path: &[&str],
2150 instance_index: usize,
2151 ) -> Vec<OwnedSegment> {
2152 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
2153 vec![OwnedSegment {
2154 id: "LOC".into(),
2155 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2156 segment_number: 10,
2157 }]
2158 } else {
2159 vec![]
2160 }
2161 }
2162 fn find_segments_with_qualifier_in_group(
2163 &self,
2164 _: &str,
2165 _: usize,
2166 _: &str,
2167 _: &[&str],
2168 _: usize,
2169 ) -> Vec<OwnedSegment> {
2170 vec![]
2171 }
2172 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2173 match group_path {
2174 ["SG4"] => 1,
2175 ["SG4", "SG5"] => 1, _ => 0,
2177 }
2178 }
2179 }
2180
2181 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
2184 let validator = EdifactValidator::new(evaluator);
2185 let external = NoOpExternalProvider;
2186 let nav = TestNav;
2187
2188 let segments = vec![OwnedSegment {
2189 id: "LOC".into(),
2190 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2191 segment_number: 10,
2192 }];
2193
2194 let workflow = AhbWorkflow {
2195 pruefidentifikator: "55001".to_string(),
2196 description: "Test".to_string(),
2197 communication_direction: None,
2198 fields: vec![
2199 AhbFieldRule {
2201 segment_path: "SG4/SG5/LOC/3227".to_string(),
2202 name: "Ortsangabe, Qualifier (Z16)".to_string(),
2203 ahb_status: "X".to_string(),
2204 codes: vec![AhbCodeRule {
2205 value: "Z16".to_string(),
2206 description: "Marktlokation".to_string(),
2207 ahb_status: "X".to_string(),
2208 }],
2209 parent_group_ahb_status: Some("Muss [2061]".to_string()),
2210 ..Default::default()
2211 },
2212 AhbFieldRule {
2214 segment_path: "SG4/SG5/LOC/3227".to_string(),
2215 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2216 ahb_status: "X".to_string(),
2217 codes: vec![AhbCodeRule {
2218 value: "Z17".to_string(),
2219 description: "Messlokation".to_string(),
2220 ahb_status: "X".to_string(),
2221 }],
2222 parent_group_ahb_status: Some("Soll [165]".to_string()),
2223 ..Default::default()
2224 },
2225 ],
2226 ub_definitions: HashMap::new(),
2227 };
2228
2229 let report = validator.validate_with_navigator(
2230 &segments,
2231 &workflow,
2232 &external,
2233 ValidationLevel::Conditions,
2234 &nav,
2235 );
2236
2237 let ahb_errors: Vec<_> = report
2238 .by_category(ValidationCategory::Ahb)
2239 .filter(|i| i.severity == Severity::Error)
2240 .collect();
2241
2242 assert!(
2244 ahb_errors.is_empty(),
2245 "Expected no errors when conditional group variant [165]=False, got: {:?}",
2246 ahb_errors
2247 );
2248 }
2249
2250 #[test]
2251 fn test_conditional_group_variant_unknown_no_error() {
2252 let evaluator = MockEvaluator::new(vec![]);
2258 let validator = EdifactValidator::new(evaluator);
2259 let external = NoOpExternalProvider;
2260
2261 let workflow = AhbWorkflow {
2262 pruefidentifikator: "55001".to_string(),
2263 description: "Test".to_string(),
2264 communication_direction: None,
2265 fields: vec![AhbFieldRule {
2266 segment_path: "SG4/SG5/LOC/3227".to_string(),
2267 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2268 ahb_status: "X".to_string(),
2269 codes: vec![AhbCodeRule {
2270 value: "Z17".to_string(),
2271 description: "Messlokation".to_string(),
2272 ahb_status: "X".to_string(),
2273 }],
2274 parent_group_ahb_status: Some("Soll [165]".to_string()),
2275 ..Default::default()
2276 }],
2277 ub_definitions: HashMap::new(),
2278 };
2279
2280 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2281
2282 let ahb_errors: Vec<_> = report
2283 .by_category(ValidationCategory::Ahb)
2284 .filter(|i| i.severity == Severity::Error)
2285 .collect();
2286
2287 assert!(
2289 ahb_errors.is_empty(),
2290 "Expected no errors when parent group condition is Unknown, got: {:?}",
2291 ahb_errors
2292 );
2293 }
2294
2295 #[test]
2296 fn test_segment_absent_within_present_group_no_error() {
2297 use mig_types::navigator::GroupNavigator;
2301
2302 struct TestNav;
2303 impl GroupNavigator for TestNav {
2304 fn find_segments_in_group(
2305 &self,
2306 segment_id: &str,
2307 group_path: &[&str],
2308 instance_index: usize,
2309 ) -> Vec<OwnedSegment> {
2310 if segment_id == "QTY"
2312 && group_path == ["SG5", "SG6", "SG9", "SG10"]
2313 && instance_index == 0
2314 {
2315 vec![OwnedSegment {
2316 id: "QTY".into(),
2317 elements: vec![vec!["220".into(), "0".into()]],
2318 segment_number: 14,
2319 }]
2320 } else {
2321 vec![]
2322 }
2323 }
2324 fn find_segments_with_qualifier_in_group(
2325 &self,
2326 _: &str,
2327 _: usize,
2328 _: &str,
2329 _: &[&str],
2330 _: usize,
2331 ) -> Vec<OwnedSegment> {
2332 vec![]
2333 }
2334 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2335 match group_path {
2336 ["SG5"] => 1,
2337 ["SG5", "SG6"] => 1,
2338 ["SG5", "SG6", "SG9"] => 1,
2339 ["SG5", "SG6", "SG9", "SG10"] => 1,
2340 _ => 0,
2341 }
2342 }
2343 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
2344 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
2346 }
2347 }
2348
2349 let evaluator = MockEvaluator::all_true(&[]);
2350 let validator = EdifactValidator::new(evaluator);
2351 let external = NoOpExternalProvider;
2352 let nav = TestNav;
2353
2354 let segments = vec![OwnedSegment {
2355 id: "QTY".into(),
2356 elements: vec![vec!["220".into(), "0".into()]],
2357 segment_number: 14,
2358 }];
2359
2360 let workflow = AhbWorkflow {
2361 pruefidentifikator: "13017".to_string(),
2362 description: "Test".to_string(),
2363 communication_direction: None,
2364 fields: vec![
2365 AhbFieldRule {
2367 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
2368 name: "Statuskategorie, Code".to_string(),
2369 ahb_status: "X".to_string(),
2370 codes: vec![],
2371 parent_group_ahb_status: Some("Muss".to_string()),
2372 ..Default::default()
2373 },
2374 AhbFieldRule {
2376 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2377 name: "Statusanlaß, Code".to_string(),
2378 ahb_status: "X [5]".to_string(),
2379 codes: vec![],
2380 parent_group_ahb_status: Some("Muss".to_string()),
2381 ..Default::default()
2382 },
2383 ],
2384 ub_definitions: HashMap::new(),
2385 };
2386
2387 let report = validator.validate_with_navigator(
2388 &segments,
2389 &workflow,
2390 &external,
2391 ValidationLevel::Conditions,
2392 &nav,
2393 );
2394
2395 let ahb_errors: Vec<_> = report
2396 .by_category(ValidationCategory::Ahb)
2397 .filter(|i| i.severity == Severity::Error)
2398 .collect();
2399
2400 assert!(
2401 ahb_errors.is_empty(),
2402 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
2403 ahb_errors
2404 );
2405 }
2406
2407 #[test]
2408 fn test_group_scoped_code_validation_with_navigator() {
2409 use mig_types::navigator::GroupNavigator;
2413
2414 struct TestNav;
2415 impl GroupNavigator for TestNav {
2416 fn find_segments_in_group(
2417 &self,
2418 segment_id: &str,
2419 group_path: &[&str],
2420 _instance_index: usize,
2421 ) -> Vec<OwnedSegment> {
2422 if segment_id != "NAD" {
2423 return vec![];
2424 }
2425 match group_path {
2426 ["SG2"] => vec![
2427 OwnedSegment {
2428 id: "NAD".into(),
2429 elements: vec![vec!["MS".into()]],
2430 segment_number: 3,
2431 },
2432 OwnedSegment {
2433 id: "NAD".into(),
2434 elements: vec![vec!["MT".into()]], segment_number: 4,
2436 },
2437 ],
2438 ["SG4", "SG12"] => vec![
2439 OwnedSegment {
2440 id: "NAD".into(),
2441 elements: vec![vec!["Z04".into()]],
2442 segment_number: 20,
2443 },
2444 OwnedSegment {
2445 id: "NAD".into(),
2446 elements: vec![vec!["Z09".into()]],
2447 segment_number: 21,
2448 },
2449 ],
2450 _ => vec![],
2451 }
2452 }
2453 fn find_segments_with_qualifier_in_group(
2454 &self,
2455 _: &str,
2456 _: usize,
2457 _: &str,
2458 _: &[&str],
2459 _: usize,
2460 ) -> Vec<OwnedSegment> {
2461 vec![]
2462 }
2463 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2464 match group_path {
2465 ["SG2"] | ["SG4", "SG12"] => 1,
2466 _ => 0,
2467 }
2468 }
2469 }
2470
2471 let evaluator = MockEvaluator::new(vec![]);
2472 let validator = EdifactValidator::new(evaluator);
2473 let external = NoOpExternalProvider;
2474 let nav = TestNav;
2475
2476 let workflow = AhbWorkflow {
2477 pruefidentifikator: "55001".to_string(),
2478 description: "Test".to_string(),
2479 communication_direction: None,
2480 fields: vec![
2481 AhbFieldRule {
2482 segment_path: "SG2/NAD/3035".to_string(),
2483 name: "Absender".to_string(),
2484 ahb_status: "X".to_string(),
2485 codes: vec![AhbCodeRule {
2486 value: "MS".to_string(),
2487 description: "Absender".to_string(),
2488 ahb_status: "X".to_string(),
2489 }],
2490 parent_group_ahb_status: None,
2491 ..Default::default()
2492 },
2493 AhbFieldRule {
2494 segment_path: "SG2/NAD/3035".to_string(),
2495 name: "Empfaenger".to_string(),
2496 ahb_status: "X".to_string(),
2497 codes: vec![AhbCodeRule {
2498 value: "MR".to_string(),
2499 description: "Empfaenger".to_string(),
2500 ahb_status: "X".to_string(),
2501 }],
2502 parent_group_ahb_status: None,
2503 ..Default::default()
2504 },
2505 AhbFieldRule {
2506 segment_path: "SG4/SG12/NAD/3035".to_string(),
2507 name: "Anschlussnutzer".to_string(),
2508 ahb_status: "X".to_string(),
2509 codes: vec![AhbCodeRule {
2510 value: "Z04".to_string(),
2511 description: "Anschlussnutzer".to_string(),
2512 ahb_status: "X".to_string(),
2513 }],
2514 parent_group_ahb_status: None,
2515 ..Default::default()
2516 },
2517 AhbFieldRule {
2518 segment_path: "SG4/SG12/NAD/3035".to_string(),
2519 name: "Korrespondenzanschrift".to_string(),
2520 ahb_status: "X".to_string(),
2521 codes: vec![AhbCodeRule {
2522 value: "Z09".to_string(),
2523 description: "Korrespondenzanschrift".to_string(),
2524 ahb_status: "X".to_string(),
2525 }],
2526 parent_group_ahb_status: None,
2527 ..Default::default()
2528 },
2529 ],
2530 ub_definitions: HashMap::new(),
2531 };
2532
2533 let all_segments = vec![
2535 OwnedSegment {
2536 id: "NAD".into(),
2537 elements: vec![vec!["MS".into()]],
2538 segment_number: 3,
2539 },
2540 OwnedSegment {
2541 id: "NAD".into(),
2542 elements: vec![vec!["MT".into()]],
2543 segment_number: 4,
2544 },
2545 OwnedSegment {
2546 id: "NAD".into(),
2547 elements: vec![vec!["Z04".into()]],
2548 segment_number: 20,
2549 },
2550 OwnedSegment {
2551 id: "NAD".into(),
2552 elements: vec![vec!["Z09".into()]],
2553 segment_number: 21,
2554 },
2555 ];
2556
2557 let report = validator.validate_with_navigator(
2558 &all_segments,
2559 &workflow,
2560 &external,
2561 ValidationLevel::Conditions,
2562 &nav,
2563 );
2564
2565 let code_errors: Vec<_> = report
2566 .by_category(ValidationCategory::Code)
2567 .filter(|i| i.severity == Severity::Error)
2568 .collect();
2569
2570 assert_eq!(
2573 code_errors.len(),
2574 1,
2575 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
2576 code_errors
2577 );
2578 assert!(code_errors[0].message.contains("MT"));
2579 assert!(code_errors[0].message.contains("MR"));
2581 assert!(code_errors[0].message.contains("MS"));
2582 assert!(
2583 !code_errors[0].message.contains("Z04"),
2584 "SG4/SG12 codes should not leak into SG2 error"
2585 );
2586 assert!(
2588 code_errors[0]
2589 .field_path
2590 .as_deref()
2591 .unwrap_or("")
2592 .contains("SG2"),
2593 "Error field_path should reference SG2, got: {:?}",
2594 code_errors[0].field_path
2595 );
2596 }
2597
2598 #[test]
2601 fn test_package_cardinality_within_bounds() {
2602 let evaluator = MockEvaluator::all_true(&[]);
2604 let validator = EdifactValidator::new(evaluator);
2605 let external = NoOpExternalProvider;
2606
2607 let segments = vec![OwnedSegment {
2608 id: "STS".into(),
2609 elements: vec![
2610 vec!["Z33".into()], vec![], vec!["E01".into()], ],
2614 segment_number: 5,
2615 }];
2616
2617 let workflow = AhbWorkflow {
2618 pruefidentifikator: "13017".to_string(),
2619 description: "Test".to_string(),
2620 communication_direction: None,
2621 ub_definitions: HashMap::new(),
2622 fields: vec![AhbFieldRule {
2623 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2624 name: "Statusanlaß, Code".to_string(),
2625 ahb_status: "X".to_string(),
2626 element_index: Some(2),
2627 component_index: Some(0),
2628 codes: vec![
2629 AhbCodeRule {
2630 value: "E01".into(),
2631 description: "Code 1".into(),
2632 ahb_status: "X [4P0..1]".into(),
2633 },
2634 AhbCodeRule {
2635 value: "E02".into(),
2636 description: "Code 2".into(),
2637 ahb_status: "X [4P0..1]".into(),
2638 },
2639 ],
2640 parent_group_ahb_status: Some("Muss".to_string()),
2641 }],
2642 };
2643
2644 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2645 let pkg_errors: Vec<_> = report
2646 .by_category(ValidationCategory::Ahb)
2647 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2648 .collect();
2649 assert!(
2650 pkg_errors.is_empty(),
2651 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
2652 pkg_errors
2653 );
2654 }
2655
2656 #[test]
2657 fn test_package_cardinality_zero_present_min_zero() {
2658 let evaluator = MockEvaluator::all_true(&[]);
2660 let validator = EdifactValidator::new(evaluator);
2661 let external = NoOpExternalProvider;
2662
2663 let segments = vec![OwnedSegment {
2664 id: "STS".into(),
2665 elements: vec![
2666 vec!["Z33".into()],
2667 vec![],
2668 vec!["X99".into()], ],
2670 segment_number: 5,
2671 }];
2672
2673 let workflow = AhbWorkflow {
2674 pruefidentifikator: "13017".to_string(),
2675 description: "Test".to_string(),
2676 communication_direction: None,
2677 ub_definitions: HashMap::new(),
2678 fields: vec![AhbFieldRule {
2679 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2680 name: "Statusanlaß, Code".to_string(),
2681 ahb_status: "X".to_string(),
2682 element_index: Some(2),
2683 component_index: Some(0),
2684 codes: vec![
2685 AhbCodeRule {
2686 value: "E01".into(),
2687 description: "Code 1".into(),
2688 ahb_status: "X [4P0..1]".into(),
2689 },
2690 AhbCodeRule {
2691 value: "E02".into(),
2692 description: "Code 2".into(),
2693 ahb_status: "X [4P0..1]".into(),
2694 },
2695 ],
2696 parent_group_ahb_status: Some("Muss".to_string()),
2697 }],
2698 };
2699
2700 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2701 let pkg_errors: Vec<_> = report
2702 .by_category(ValidationCategory::Ahb)
2703 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2704 .collect();
2705 assert!(
2706 pkg_errors.is_empty(),
2707 "0 codes, min=0 — no error expected, got: {:?}",
2708 pkg_errors
2709 );
2710 }
2711
2712 #[test]
2713 fn test_package_cardinality_too_many() {
2714 let evaluator = MockEvaluator::all_true(&[]);
2716 let validator = EdifactValidator::new(evaluator);
2717 let external = NoOpExternalProvider;
2718
2719 let segments = vec![
2721 OwnedSegment {
2722 id: "STS".into(),
2723 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
2724 segment_number: 5,
2725 },
2726 OwnedSegment {
2727 id: "STS".into(),
2728 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
2729 segment_number: 6,
2730 },
2731 ];
2732
2733 let workflow = AhbWorkflow {
2734 pruefidentifikator: "13017".to_string(),
2735 description: "Test".to_string(),
2736 communication_direction: None,
2737 ub_definitions: HashMap::new(),
2738 fields: vec![AhbFieldRule {
2739 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2740 name: "Statusanlaß, Code".to_string(),
2741 ahb_status: "X".to_string(),
2742 element_index: Some(2),
2743 component_index: Some(0),
2744 codes: vec![
2745 AhbCodeRule {
2746 value: "E01".into(),
2747 description: "Code 1".into(),
2748 ahb_status: "X [4P0..1]".into(),
2749 },
2750 AhbCodeRule {
2751 value: "E02".into(),
2752 description: "Code 2".into(),
2753 ahb_status: "X [4P0..1]".into(),
2754 },
2755 ],
2756 parent_group_ahb_status: Some("Muss".to_string()),
2757 }],
2758 };
2759
2760 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2761 let pkg_errors: Vec<_> = report
2762 .by_category(ValidationCategory::Ahb)
2763 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2764 .collect();
2765 assert_eq!(
2766 pkg_errors.len(),
2767 1,
2768 "2 codes present, max=1 — expected 1 error, got: {:?}",
2769 pkg_errors
2770 );
2771 assert!(pkg_errors[0].message.contains("[4P0..1]"));
2772 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
2773 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
2774 }
2775
2776 #[test]
2777 fn test_package_cardinality_too_few() {
2778 let evaluator = MockEvaluator::all_true(&[]);
2780 let validator = EdifactValidator::new(evaluator);
2781 let external = NoOpExternalProvider;
2782
2783 let segments = vec![OwnedSegment {
2784 id: "STS".into(),
2785 elements: vec![
2786 vec!["Z33".into()],
2787 vec![],
2788 vec!["X99".into()], ],
2790 segment_number: 5,
2791 }];
2792
2793 let workflow = AhbWorkflow {
2794 pruefidentifikator: "13017".to_string(),
2795 description: "Test".to_string(),
2796 communication_direction: None,
2797 ub_definitions: HashMap::new(),
2798 fields: vec![AhbFieldRule {
2799 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2800 name: "Statusanlaß, Code".to_string(),
2801 ahb_status: "X".to_string(),
2802 element_index: Some(2),
2803 component_index: Some(0),
2804 codes: vec![
2805 AhbCodeRule {
2806 value: "E01".into(),
2807 description: "Code 1".into(),
2808 ahb_status: "X [5P1..3]".into(),
2809 },
2810 AhbCodeRule {
2811 value: "E02".into(),
2812 description: "Code 2".into(),
2813 ahb_status: "X [5P1..3]".into(),
2814 },
2815 AhbCodeRule {
2816 value: "E03".into(),
2817 description: "Code 3".into(),
2818 ahb_status: "X [5P1..3]".into(),
2819 },
2820 ],
2821 parent_group_ahb_status: Some("Muss".to_string()),
2822 }],
2823 };
2824
2825 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2826 let pkg_errors: Vec<_> = report
2827 .by_category(ValidationCategory::Ahb)
2828 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2829 .collect();
2830 assert_eq!(
2831 pkg_errors.len(),
2832 1,
2833 "0 codes present, min=1 — expected 1 error, got: {:?}",
2834 pkg_errors
2835 );
2836 assert!(pkg_errors[0].message.contains("[5P1..3]"));
2837 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
2838 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
2839 }
2840
2841 #[test]
2842 fn test_package_cardinality_no_packages_in_workflow() {
2843 let evaluator = MockEvaluator::all_true(&[]);
2845 let validator = EdifactValidator::new(evaluator);
2846 let external = NoOpExternalProvider;
2847
2848 let segments = vec![OwnedSegment {
2849 id: "STS".into(),
2850 elements: vec![vec!["E01".into()]],
2851 segment_number: 5,
2852 }];
2853
2854 let workflow = AhbWorkflow {
2855 pruefidentifikator: "13017".to_string(),
2856 description: "Test".to_string(),
2857 communication_direction: None,
2858 ub_definitions: HashMap::new(),
2859 fields: vec![AhbFieldRule {
2860 segment_path: "STS/9015".to_string(),
2861 name: "Status Code".to_string(),
2862 ahb_status: "X".to_string(),
2863 codes: vec![AhbCodeRule {
2864 value: "E01".into(),
2865 description: "Code 1".into(),
2866 ahb_status: "X".into(),
2867 }],
2868 parent_group_ahb_status: Some("Muss".to_string()),
2869 ..Default::default()
2870 }],
2871 };
2872
2873 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2874 let pkg_errors: Vec<_> = report
2875 .by_category(ValidationCategory::Ahb)
2876 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2877 .collect();
2878 assert!(
2879 pkg_errors.is_empty(),
2880 "No packages in workflow — no errors expected"
2881 );
2882 }
2883
2884 #[test]
2885 fn test_package_cardinality_with_condition_and_package() {
2886 let evaluator = MockEvaluator::all_true(&[901]);
2888 let validator = EdifactValidator::new(evaluator);
2889 let external = NoOpExternalProvider;
2890
2891 let segments = vec![OwnedSegment {
2892 id: "STS".into(),
2893 elements: vec![vec![], vec![], vec!["E01".into()]],
2894 segment_number: 5,
2895 }];
2896
2897 let workflow = AhbWorkflow {
2898 pruefidentifikator: "13017".to_string(),
2899 description: "Test".to_string(),
2900 communication_direction: None,
2901 ub_definitions: HashMap::new(),
2902 fields: vec![AhbFieldRule {
2903 segment_path: "SG10/STS/C556/9013".to_string(),
2904 name: "Code".to_string(),
2905 ahb_status: "X".to_string(),
2906 element_index: Some(2),
2907 component_index: Some(0),
2908 codes: vec![
2909 AhbCodeRule {
2910 value: "E01".into(),
2911 description: "Code 1".into(),
2912 ahb_status: "X [901] [4P0..1]".into(),
2913 },
2914 AhbCodeRule {
2915 value: "E02".into(),
2916 description: "Code 2".into(),
2917 ahb_status: "X [901] [4P0..1]".into(),
2918 },
2919 ],
2920 parent_group_ahb_status: Some("Muss".to_string()),
2921 }],
2922 };
2923
2924 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2925 let pkg_errors: Vec<_> = report
2926 .by_category(ValidationCategory::Ahb)
2927 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2928 .collect();
2929 assert!(
2930 pkg_errors.is_empty(),
2931 "1 code within [4P0..1] bounds — no error, got: {:?}",
2932 pkg_errors
2933 );
2934 }
2935}