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 for field in &workflow.fields {
385 let el_idx = field.element_index.unwrap_or(0);
386 let comp_idx = field.component_index.unwrap_or(0);
387
388 for code in &field.codes {
389 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
391 let mut packages = Vec::new();
393 collect_packages(&expr, &mut packages);
394
395 for (pkg_id, pkg_min, pkg_max) in packages {
396 let key = (field.segment_path.clone(), pkg_id);
397 let group = groups.entry(key).or_insert_with(|| PackageGroup {
398 min: pkg_min,
399 max: pkg_max,
400 code_values: Vec::new(),
401 element_index: el_idx,
402 component_index: comp_idx,
403 });
404 group.min = group.min.max(pkg_min);
407 group.max = group.max.min(pkg_max);
408 group.code_values.push(code.value.clone());
409 }
410 }
411 }
412 }
413
414 for ((seg_path, pkg_id), group) in &groups {
416 let segment_id = extract_segment_id(seg_path);
417 let segments = ctx.find_segments(&segment_id);
418
419 let mut present_count: usize = 0;
421 for seg in &segments {
422 if let Some(value) = seg
423 .elements
424 .get(group.element_index)
425 .and_then(|e| e.get(group.component_index))
426 .filter(|v| !v.is_empty())
427 {
428 if group.code_values.contains(value) {
429 present_count += 1;
430 }
431 }
432 }
433
434 let min = group.min as usize;
435 let max = group.max as usize;
436
437 if present_count < min || present_count > max {
438 let code_list = group.code_values.join(", ");
439 report.add_issue(
440 ValidationIssue::new(
441 Severity::Error,
442 ValidationCategory::Ahb,
443 ErrorCodes::PACKAGE_CARDINALITY_VIOLATION,
444 format!(
445 "Package [{}P{}..{}] at {}: {} code(s) present (allowed {}..{}). Codes in package: [{}]",
446 pkg_id, group.min, group.max, seg_path, present_count, group.min, group.max, code_list
447 ),
448 )
449 .with_field_path(seg_path)
450 .with_expected(format!("{}..{}", group.min, group.max))
451 .with_actual(present_count.to_string()),
452 );
453 }
454 }
455 }
456
457 fn validate_codes_cross_field(
469 &self,
470 workflow: &AhbWorkflow,
471 ctx: &EvaluationContext,
472 report: &mut ValidationReport,
473 ) {
474 if ctx.navigator.is_some() {
475 self.validate_codes_group_scoped(workflow, ctx, report);
476 } else {
477 self.validate_codes_tag_scoped(workflow, ctx, report);
478 }
479 }
480
481 fn validate_codes_group_scoped(
484 &self,
485 workflow: &AhbWorkflow,
486 ctx: &EvaluationContext,
487 report: &mut ValidationReport,
488 ) {
489 type CodeKey = (String, String, usize, usize);
492 let mut codes_by_group: HashMap<CodeKey, HashSet<&str>> = HashMap::new();
493
494 for field in &workflow.fields {
495 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
496 continue;
497 }
498 let tag = extract_segment_id(&field.segment_path);
499 let group_key = extract_group_path_key(&field.segment_path);
500 let el_idx = field.element_index.unwrap_or(0);
501 let comp_idx = field.component_index.unwrap_or(0);
502 let entry = codes_by_group
503 .entry((group_key, tag, el_idx, comp_idx))
504 .or_default();
505 for code in &field.codes {
506 if code.ahb_status == "X" || code.ahb_status.starts_with("Muss") {
507 entry.insert(&code.value);
508 }
509 }
510 }
511
512 let nav = ctx.navigator.unwrap();
513
514 for ((group_key, tag, el_idx, comp_idx), allowed_codes) in &codes_by_group {
515 if allowed_codes.is_empty() {
516 continue;
517 }
518
519 let group_path: Vec<&str> = if group_key.is_empty() {
520 Vec::new()
521 } else {
522 group_key.split('/').collect()
523 };
524
525 if group_path.is_empty() {
527 Self::check_segments_against_codes(
529 ctx.find_segments(tag),
530 allowed_codes,
531 tag,
532 *el_idx,
533 *comp_idx,
534 &format!("{tag}/qualifier"),
535 report,
536 );
537 } else {
538 let instance_count = nav.group_instance_count(&group_path);
539 for i in 0..instance_count {
540 let segments = nav.find_segments_in_group(tag, &group_path, i);
541 let refs: Vec<&OwnedSegment> = segments.iter().collect();
542 Self::check_segments_against_codes(
543 refs,
544 allowed_codes,
545 tag,
546 *el_idx,
547 *comp_idx,
548 &format!("{group_key}/{tag}/qualifier"),
549 report,
550 );
551 }
552 }
553 }
554 }
555
556 fn validate_codes_tag_scoped(
559 &self,
560 workflow: &AhbWorkflow,
561 ctx: &EvaluationContext,
562 report: &mut ValidationReport,
563 ) {
564 let mut codes_by_tag: HashMap<(String, usize, usize), HashSet<&str>> = HashMap::new();
566
567 for field in &workflow.fields {
568 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
569 continue;
570 }
571 let tag = extract_segment_id(&field.segment_path);
572 let el_idx = field.element_index.unwrap_or(0);
573 let comp_idx = field.component_index.unwrap_or(0);
574 let entry = codes_by_tag.entry((tag, el_idx, comp_idx)).or_default();
575 for code in &field.codes {
576 if code.ahb_status == "X" || code.ahb_status.starts_with("Muss") {
577 entry.insert(&code.value);
578 }
579 }
580 }
581
582 for ((tag, el_idx, comp_idx), allowed_codes) in &codes_by_tag {
583 if allowed_codes.is_empty() {
584 continue;
585 }
586 Self::check_segments_against_codes(
587 ctx.find_segments(tag),
588 allowed_codes,
589 tag,
590 *el_idx,
591 *comp_idx,
592 &format!("{tag}/qualifier"),
593 report,
594 );
595 }
596 }
597
598 fn check_segments_against_codes(
600 segments: Vec<&OwnedSegment>,
601 allowed_codes: &HashSet<&str>,
602 _tag: &str,
603 el_idx: usize,
604 comp_idx: usize,
605 field_path: &str,
606 report: &mut ValidationReport,
607 ) {
608 for segment in segments {
609 if let Some(code_value) = segment
610 .elements
611 .get(el_idx)
612 .and_then(|e| e.get(comp_idx))
613 .filter(|v| !v.is_empty())
614 {
615 if !allowed_codes.contains(code_value.as_str()) {
616 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
617 sorted_codes.sort_unstable();
618 report.add_issue(
619 ValidationIssue::new(
620 Severity::Error,
621 ValidationCategory::Code,
622 ErrorCodes::CODE_NOT_ALLOWED_FOR_PID,
623 format!(
624 "Code '{}' is not allowed for this PID. Allowed: [{}]",
625 code_value,
626 sorted_codes.join(", ")
627 ),
628 )
629 .with_field_path(field_path)
630 .with_actual(code_value)
631 .with_expected(sorted_codes.join(", ")),
632 );
633 }
634 }
635 }
636 }
637}
638
639fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
648 let segment_id = extract_segment_id(&field.segment_path);
649
650 if !field.codes.is_empty() {
656 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
657 let required_codes: Vec<&str> =
658 field.codes.iter().map(|c| c.value.as_str()).collect();
659 let matching = ctx.find_segments(&segment_id);
660 return matching.iter().any(|seg| {
661 seg.elements
662 .get(el_idx)
663 .and_then(|e| e.get(comp_idx))
664 .is_some_and(|v| required_codes.contains(&v.as_str()))
665 });
666 }
667 if is_qualifier_field(&field.segment_path) {
670 let required_codes: Vec<&str> =
671 field.codes.iter().map(|c| c.value.as_str()).collect();
672 let el_idx = field.element_index.unwrap_or(0);
673 let comp_idx = field.component_index.unwrap_or(0);
674 let matching = ctx.find_segments(&segment_id);
675 return matching.iter().any(|seg| {
676 seg.elements
677 .get(el_idx)
678 .and_then(|e| e.get(comp_idx))
679 .is_some_and(|v| required_codes.contains(&v.as_str()))
680 });
681 }
682 }
683
684 ctx.has_segment(&segment_id)
685}
686
687fn is_group_variant_absent(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
702 let group_path: Vec<&str> = field
703 .segment_path
704 .split('/')
705 .take_while(|p| p.starts_with("SG"))
706 .collect();
707
708 if group_path.is_empty() {
709 return false;
710 }
711
712 let nav = match ctx.navigator {
713 Some(nav) => nav,
714 None => return false,
715 };
716
717 let instance_count = nav.group_instance_count(&group_path);
718
719 if instance_count == 0 {
724 let is_group_mandatory = field
725 .parent_group_ahb_status
726 .as_deref()
727 .is_some_and(is_mandatory_status);
728 if !is_group_mandatory {
729 return true;
730 }
731 return false;
733 }
734
735 if let Some(ref group_status) = field.parent_group_ahb_status {
739 if !is_mandatory_status(group_status) && !group_status.contains('[') {
740 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
743 let segment_id = extract_segment_id(&field.segment_path);
744 let required_codes: Vec<&str> =
745 field.codes.iter().map(|c| c.value.as_str()).collect();
746
747 let any_instance_has_qualifier = (0..instance_count).any(|i| {
748 nav.find_segments_in_group(&segment_id, &group_path, i)
749 .iter()
750 .any(|seg| {
751 seg.elements
752 .first()
753 .and_then(|e| e.first())
754 .is_some_and(|v| required_codes.contains(&v.as_str()))
755 })
756 });
757
758 if !any_instance_has_qualifier {
759 return true; }
761 }
762 }
763 }
764
765 let segment_id = extract_segment_id(&field.segment_path);
774 let segment_absent_from_all = (0..instance_count).all(|i| {
775 nav.find_segments_in_group(&segment_id, &group_path, i)
776 .is_empty()
777 });
778 if segment_absent_from_all {
779 let group_has_other_segments =
780 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
781 if group_has_other_segments {
782 return true;
783 }
784 }
785
786 false
787}
788
789fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
791 match expr {
792 ConditionExpr::Package { id, min, max } => {
793 out.push((*id, *min, *max));
794 }
795 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
796 for e in exprs {
797 collect_packages(e, out);
798 }
799 }
800 ConditionExpr::Xor(left, right) => {
801 collect_packages(left, out);
802 collect_packages(right, out);
803 }
804 ConditionExpr::Not(inner) => {
805 collect_packages(inner, out);
806 }
807 ConditionExpr::Ref(_) => {}
808 }
809}
810
811fn is_mandatory_status(status: &str) -> bool {
813 let trimmed = status.trim();
814 trimmed.starts_with("Muss") || trimmed.starts_with('X')
815}
816
817fn is_qualifier_field(path: &str) -> bool {
826 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
827 parts.len() == 2
830}
831
832fn extract_group_path_key(path: &str) -> String {
837 let sg_parts: Vec<&str> = path
838 .split('/')
839 .take_while(|p| p.starts_with("SG"))
840 .collect();
841 sg_parts.join("/")
842}
843
844fn extract_segment_id(path: &str) -> String {
846 for part in path.split('/') {
847 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
849 continue;
850 }
851 if part.len() >= 3
853 && part
854 .chars()
855 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
856 {
857 return part.to_string();
858 }
859 }
860 path.split('/').next_back().unwrap_or(path).to_string()
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
868 use std::collections::HashMap;
869
870 struct MockEvaluator {
872 results: HashMap<u32, CR>,
873 }
874
875 impl MockEvaluator {
876 fn new(results: Vec<(u32, CR)>) -> Self {
877 Self {
878 results: results.into_iter().collect(),
879 }
880 }
881
882 fn all_true(ids: &[u32]) -> Self {
883 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
884 }
885 }
886
887 impl ConditionEvaluator for MockEvaluator {
888 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
889 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
890 }
891 fn is_external(&self, _condition: u32) -> bool {
892 false
893 }
894 fn message_type(&self) -> &str {
895 "UTILMD"
896 }
897 fn format_version(&self) -> &str {
898 "FV2510"
899 }
900 }
901
902 #[test]
905 fn test_is_mandatory_status() {
906 assert!(is_mandatory_status("Muss"));
907 assert!(is_mandatory_status("Muss [182] ∧ [152]"));
908 assert!(is_mandatory_status("X"));
909 assert!(is_mandatory_status("X [567]"));
910 assert!(!is_mandatory_status("Soll [1]"));
911 assert!(!is_mandatory_status("Kann [1]"));
912 assert!(!is_mandatory_status(""));
913 }
914
915 #[test]
916 fn test_extract_segment_id_simple() {
917 assert_eq!(extract_segment_id("NAD"), "NAD");
918 }
919
920 #[test]
921 fn test_extract_segment_id_with_sg_prefix() {
922 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
923 }
924
925 #[test]
926 fn test_extract_segment_id_nested_sg() {
927 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
928 }
929
930 #[test]
933 fn test_validate_missing_mandatory_field() {
934 let evaluator = MockEvaluator::all_true(&[182, 152]);
935 let validator = EdifactValidator::new(evaluator);
936 let external = NoOpExternalProvider;
937
938 let workflow = AhbWorkflow {
939 pruefidentifikator: "11001".to_string(),
940 description: "Test".to_string(),
941 communication_direction: None,
942 fields: vec![AhbFieldRule {
943 segment_path: "SG2/NAD/C082/3039".to_string(),
944 name: "MP-ID des MSB".to_string(),
945 ahb_status: "Muss [182] ∧ [152]".to_string(),
946 codes: vec![],
947 parent_group_ahb_status: None,
948 ..Default::default()
949 }],
950 ub_definitions: HashMap::new(),
951 };
952
953 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
955
956 assert!(!report.is_valid());
958 let errors: Vec<_> = report.errors().collect();
959 assert_eq!(errors.len(), 1);
960 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
961 assert!(errors[0].message.contains("MP-ID des MSB"));
962 }
963
964 #[test]
965 fn test_validate_condition_false_no_error() {
966 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
968 let validator = EdifactValidator::new(evaluator);
969 let external = NoOpExternalProvider;
970
971 let workflow = AhbWorkflow {
972 pruefidentifikator: "11001".to_string(),
973 description: "Test".to_string(),
974 communication_direction: None,
975 fields: vec![AhbFieldRule {
976 segment_path: "NAD".to_string(),
977 name: "Partnerrolle".to_string(),
978 ahb_status: "Muss [182] ∧ [152]".to_string(),
979 codes: vec![],
980 parent_group_ahb_status: None,
981 ..Default::default()
982 }],
983 ub_definitions: HashMap::new(),
984 };
985
986 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
987
988 assert!(report.is_valid());
990 }
991
992 #[test]
993 fn test_validate_condition_unknown_adds_info() {
994 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
996 let validator = EdifactValidator::new(evaluator);
998 let external = NoOpExternalProvider;
999
1000 let workflow = AhbWorkflow {
1001 pruefidentifikator: "11001".to_string(),
1002 description: "Test".to_string(),
1003 communication_direction: None,
1004 fields: vec![AhbFieldRule {
1005 segment_path: "NAD".to_string(),
1006 name: "Partnerrolle".to_string(),
1007 ahb_status: "Muss [182] ∧ [152]".to_string(),
1008 codes: vec![],
1009 parent_group_ahb_status: None,
1010 ..Default::default()
1011 }],
1012 ub_definitions: HashMap::new(),
1013 };
1014
1015 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1016
1017 assert!(report.is_valid());
1019 let infos: Vec<_> = report.infos().collect();
1020 assert_eq!(infos.len(), 1);
1021 assert_eq!(infos[0].code, ErrorCodes::CONDITION_UNKNOWN);
1022 }
1023
1024 #[test]
1025 fn test_validate_structure_level_skips_conditions() {
1026 let evaluator = MockEvaluator::all_true(&[182, 152]);
1027 let validator = EdifactValidator::new(evaluator);
1028 let external = NoOpExternalProvider;
1029
1030 let workflow = AhbWorkflow {
1031 pruefidentifikator: "11001".to_string(),
1032 description: "Test".to_string(),
1033 communication_direction: None,
1034 fields: vec![AhbFieldRule {
1035 segment_path: "NAD".to_string(),
1036 name: "Partnerrolle".to_string(),
1037 ahb_status: "Muss [182] ∧ [152]".to_string(),
1038 codes: vec![],
1039 parent_group_ahb_status: None,
1040 ..Default::default()
1041 }],
1042 ub_definitions: HashMap::new(),
1043 };
1044
1045 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
1047
1048 assert!(report.is_valid());
1050 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
1051 }
1052
1053 #[test]
1054 fn test_validate_empty_workflow_no_condition_errors() {
1055 let evaluator = MockEvaluator::all_true(&[]);
1056 let validator = EdifactValidator::new(evaluator);
1057 let external = NoOpExternalProvider;
1058
1059 let empty_workflow = AhbWorkflow {
1060 pruefidentifikator: String::new(),
1061 description: String::new(),
1062 communication_direction: None,
1063 fields: vec![],
1064 ub_definitions: HashMap::new(),
1065 };
1066
1067 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
1068
1069 assert!(report.is_valid());
1070 }
1071
1072 #[test]
1073 fn test_validate_bare_muss_always_required() {
1074 let evaluator = MockEvaluator::new(vec![]);
1075 let validator = EdifactValidator::new(evaluator);
1076 let external = NoOpExternalProvider;
1077
1078 let workflow = AhbWorkflow {
1079 pruefidentifikator: "55001".to_string(),
1080 description: "Test".to_string(),
1081 communication_direction: Some("NB an LF".to_string()),
1082 fields: vec![AhbFieldRule {
1083 segment_path: "SG2/NAD/3035".to_string(),
1084 name: "Partnerrolle".to_string(),
1085 ahb_status: "Muss".to_string(), codes: vec![],
1087 parent_group_ahb_status: None,
1088 ..Default::default()
1089 }],
1090 ub_definitions: HashMap::new(),
1091 };
1092
1093 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1094
1095 assert!(!report.is_valid());
1097 assert_eq!(report.error_count(), 1);
1098 }
1099
1100 #[test]
1101 fn test_validate_x_status_is_mandatory() {
1102 let evaluator = MockEvaluator::new(vec![]);
1103 let validator = EdifactValidator::new(evaluator);
1104 let external = NoOpExternalProvider;
1105
1106 let workflow = AhbWorkflow {
1107 pruefidentifikator: "55001".to_string(),
1108 description: "Test".to_string(),
1109 communication_direction: None,
1110 fields: vec![AhbFieldRule {
1111 segment_path: "DTM".to_string(),
1112 name: "Datum".to_string(),
1113 ahb_status: "X".to_string(),
1114 codes: vec![],
1115 parent_group_ahb_status: None,
1116 ..Default::default()
1117 }],
1118 ub_definitions: HashMap::new(),
1119 };
1120
1121 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1122
1123 assert!(!report.is_valid());
1124 let errors: Vec<_> = report.errors().collect();
1125 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1126 }
1127
1128 #[test]
1129 fn test_validate_soll_not_mandatory() {
1130 let evaluator = MockEvaluator::new(vec![]);
1131 let validator = EdifactValidator::new(evaluator);
1132 let external = NoOpExternalProvider;
1133
1134 let workflow = AhbWorkflow {
1135 pruefidentifikator: "55001".to_string(),
1136 description: "Test".to_string(),
1137 communication_direction: None,
1138 fields: vec![AhbFieldRule {
1139 segment_path: "DTM".to_string(),
1140 name: "Datum".to_string(),
1141 ahb_status: "Soll".to_string(),
1142 codes: vec![],
1143 parent_group_ahb_status: None,
1144 ..Default::default()
1145 }],
1146 ub_definitions: HashMap::new(),
1147 };
1148
1149 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1150
1151 assert!(report.is_valid());
1153 }
1154
1155 #[test]
1156 fn test_report_includes_metadata() {
1157 let evaluator = MockEvaluator::new(vec![]);
1158 let validator = EdifactValidator::new(evaluator);
1159 let external = NoOpExternalProvider;
1160
1161 let workflow = AhbWorkflow {
1162 pruefidentifikator: "55001".to_string(),
1163 description: String::new(),
1164 communication_direction: None,
1165 fields: vec![],
1166 ub_definitions: HashMap::new(),
1167 };
1168
1169 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
1170
1171 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
1172 assert_eq!(report.level, ValidationLevel::Full);
1173 assert_eq!(report.message_type, "UTILMD");
1174 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
1175 }
1176
1177 #[test]
1178 fn test_validate_with_navigator_returns_report() {
1179 let evaluator = MockEvaluator::all_true(&[]);
1180 let validator = EdifactValidator::new(evaluator);
1181 let external = NoOpExternalProvider;
1182 let nav = crate::eval::NoOpGroupNavigator;
1183
1184 let workflow = AhbWorkflow {
1185 pruefidentifikator: "55001".to_string(),
1186 description: "Test".to_string(),
1187 communication_direction: None,
1188 fields: vec![],
1189 ub_definitions: HashMap::new(),
1190 };
1191
1192 let report = validator.validate_with_navigator(
1193 &[],
1194 &workflow,
1195 &external,
1196 ValidationLevel::Full,
1197 &nav,
1198 );
1199 assert!(report.is_valid());
1200 }
1201
1202 #[test]
1203 fn test_code_validation_skips_composite_paths() {
1204 let evaluator = MockEvaluator::new(vec![]);
1208 let validator = EdifactValidator::new(evaluator);
1209 let external = NoOpExternalProvider;
1210
1211 let unh_segment = OwnedSegment {
1212 id: "UNH".to_string(),
1213 elements: vec![
1214 vec!["ALEXANDE951842".to_string()], vec![
1216 "UTILMD".to_string(),
1217 "D".to_string(),
1218 "11A".to_string(),
1219 "UN".to_string(),
1220 "S2.1".to_string(),
1221 ],
1222 ],
1223 segment_number: 1,
1224 };
1225
1226 let workflow = AhbWorkflow {
1227 pruefidentifikator: "55001".to_string(),
1228 description: "Test".to_string(),
1229 communication_direction: None,
1230 fields: vec![
1231 AhbFieldRule {
1232 segment_path: "UNH/S009/0065".to_string(),
1233 name: "Nachrichtentyp".to_string(),
1234 ahb_status: "X".to_string(),
1235 codes: vec![AhbCodeRule {
1236 value: "UTILMD".to_string(),
1237 description: "Stammdaten".to_string(),
1238 ahb_status: "X".to_string(),
1239 }],
1240 parent_group_ahb_status: None,
1241 ..Default::default()
1242 },
1243 AhbFieldRule {
1244 segment_path: "UNH/S009/0052".to_string(),
1245 name: "Version".to_string(),
1246 ahb_status: "X".to_string(),
1247 codes: vec![AhbCodeRule {
1248 value: "D".to_string(),
1249 description: "Draft".to_string(),
1250 ahb_status: "X".to_string(),
1251 }],
1252 parent_group_ahb_status: None,
1253 ..Default::default()
1254 },
1255 ],
1256 ub_definitions: HashMap::new(),
1257 };
1258
1259 let report = validator.validate(
1260 &[unh_segment],
1261 &workflow,
1262 &external,
1263 ValidationLevel::Conditions,
1264 );
1265
1266 let code_errors: Vec<_> = report
1268 .by_category(ValidationCategory::Code)
1269 .filter(|i| i.severity == Severity::Error)
1270 .collect();
1271 assert!(
1272 code_errors.is_empty(),
1273 "Expected no code errors for composite paths, got: {:?}",
1274 code_errors
1275 );
1276 }
1277
1278 #[test]
1279 fn test_cross_field_code_validation_valid_qualifiers() {
1280 let evaluator = MockEvaluator::new(vec![]);
1283 let validator = EdifactValidator::new(evaluator);
1284 let external = NoOpExternalProvider;
1285
1286 let nad_ms = OwnedSegment {
1287 id: "NAD".to_string(),
1288 elements: vec![vec!["MS".to_string()]],
1289 segment_number: 4,
1290 };
1291 let nad_mr = OwnedSegment {
1292 id: "NAD".to_string(),
1293 elements: vec![vec!["MR".to_string()]],
1294 segment_number: 5,
1295 };
1296
1297 let workflow = AhbWorkflow {
1298 pruefidentifikator: "55001".to_string(),
1299 description: "Test".to_string(),
1300 communication_direction: None,
1301 fields: vec![
1302 AhbFieldRule {
1303 segment_path: "SG2/NAD/3035".to_string(),
1304 name: "Absender".to_string(),
1305 ahb_status: "X".to_string(),
1306 codes: vec![AhbCodeRule {
1307 value: "MS".to_string(),
1308 description: "Absender".to_string(),
1309 ahb_status: "X".to_string(),
1310 }],
1311 parent_group_ahb_status: None,
1312 ..Default::default()
1313 },
1314 AhbFieldRule {
1315 segment_path: "SG2/NAD/3035".to_string(),
1316 name: "Empfaenger".to_string(),
1317 ahb_status: "X".to_string(),
1318 codes: vec![AhbCodeRule {
1319 value: "MR".to_string(),
1320 description: "Empfaenger".to_string(),
1321 ahb_status: "X".to_string(),
1322 }],
1323 parent_group_ahb_status: None,
1324 ..Default::default()
1325 },
1326 ],
1327 ub_definitions: HashMap::new(),
1328 };
1329
1330 let report = validator.validate(
1331 &[nad_ms, nad_mr],
1332 &workflow,
1333 &external,
1334 ValidationLevel::Conditions,
1335 );
1336
1337 let code_errors: Vec<_> = report
1338 .by_category(ValidationCategory::Code)
1339 .filter(|i| i.severity == Severity::Error)
1340 .collect();
1341 assert!(
1342 code_errors.is_empty(),
1343 "Expected no code errors for valid qualifiers, got: {:?}",
1344 code_errors
1345 );
1346 }
1347
1348 #[test]
1349 fn test_cross_field_code_validation_catches_invalid_qualifier() {
1350 let evaluator = MockEvaluator::new(vec![]);
1352 let validator = EdifactValidator::new(evaluator);
1353 let external = NoOpExternalProvider;
1354
1355 let nad_ms = OwnedSegment {
1356 id: "NAD".to_string(),
1357 elements: vec![vec!["MS".to_string()]],
1358 segment_number: 4,
1359 };
1360 let nad_mt = OwnedSegment {
1361 id: "NAD".to_string(),
1362 elements: vec![vec!["MT".to_string()]], segment_number: 5,
1364 };
1365
1366 let workflow = AhbWorkflow {
1367 pruefidentifikator: "55001".to_string(),
1368 description: "Test".to_string(),
1369 communication_direction: None,
1370 fields: vec![
1371 AhbFieldRule {
1372 segment_path: "SG2/NAD/3035".to_string(),
1373 name: "Absender".to_string(),
1374 ahb_status: "X".to_string(),
1375 codes: vec![AhbCodeRule {
1376 value: "MS".to_string(),
1377 description: "Absender".to_string(),
1378 ahb_status: "X".to_string(),
1379 }],
1380 parent_group_ahb_status: None,
1381 ..Default::default()
1382 },
1383 AhbFieldRule {
1384 segment_path: "SG2/NAD/3035".to_string(),
1385 name: "Empfaenger".to_string(),
1386 ahb_status: "X".to_string(),
1387 codes: vec![AhbCodeRule {
1388 value: "MR".to_string(),
1389 description: "Empfaenger".to_string(),
1390 ahb_status: "X".to_string(),
1391 }],
1392 parent_group_ahb_status: None,
1393 ..Default::default()
1394 },
1395 ],
1396 ub_definitions: HashMap::new(),
1397 };
1398
1399 let report = validator.validate(
1400 &[nad_ms, nad_mt],
1401 &workflow,
1402 &external,
1403 ValidationLevel::Conditions,
1404 );
1405
1406 let code_errors: Vec<_> = report
1407 .by_category(ValidationCategory::Code)
1408 .filter(|i| i.severity == Severity::Error)
1409 .collect();
1410 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
1411 assert!(code_errors[0].message.contains("MT"));
1412 assert!(code_errors[0].message.contains("MR"));
1413 assert!(code_errors[0].message.contains("MS"));
1414 }
1415
1416 #[test]
1417 fn test_cross_field_code_validation_unions_across_groups() {
1418 let evaluator = MockEvaluator::new(vec![]);
1422 let validator = EdifactValidator::new(evaluator);
1423 let external = NoOpExternalProvider;
1424
1425 let segments = vec![
1426 OwnedSegment {
1427 id: "NAD".to_string(),
1428 elements: vec![vec!["MS".to_string()]],
1429 segment_number: 3,
1430 },
1431 OwnedSegment {
1432 id: "NAD".to_string(),
1433 elements: vec![vec!["MR".to_string()]],
1434 segment_number: 4,
1435 },
1436 OwnedSegment {
1437 id: "NAD".to_string(),
1438 elements: vec![vec!["Z04".to_string()]],
1439 segment_number: 20,
1440 },
1441 OwnedSegment {
1442 id: "NAD".to_string(),
1443 elements: vec![vec!["Z09".to_string()]],
1444 segment_number: 21,
1445 },
1446 OwnedSegment {
1447 id: "NAD".to_string(),
1448 elements: vec![vec!["MT".to_string()]], segment_number: 22,
1450 },
1451 ];
1452
1453 let workflow = AhbWorkflow {
1454 pruefidentifikator: "55001".to_string(),
1455 description: "Test".to_string(),
1456 communication_direction: None,
1457 fields: vec![
1458 AhbFieldRule {
1459 segment_path: "SG2/NAD/3035".to_string(),
1460 name: "Absender".to_string(),
1461 ahb_status: "X".to_string(),
1462 codes: vec![AhbCodeRule {
1463 value: "MS".to_string(),
1464 description: "Absender".to_string(),
1465 ahb_status: "X".to_string(),
1466 }],
1467 parent_group_ahb_status: None,
1468 ..Default::default()
1469 },
1470 AhbFieldRule {
1471 segment_path: "SG2/NAD/3035".to_string(),
1472 name: "Empfaenger".to_string(),
1473 ahb_status: "X".to_string(),
1474 codes: vec![AhbCodeRule {
1475 value: "MR".to_string(),
1476 description: "Empfaenger".to_string(),
1477 ahb_status: "X".to_string(),
1478 }],
1479 parent_group_ahb_status: None,
1480 ..Default::default()
1481 },
1482 AhbFieldRule {
1483 segment_path: "SG4/SG12/NAD/3035".to_string(),
1484 name: "Anschlussnutzer".to_string(),
1485 ahb_status: "X".to_string(),
1486 codes: vec![AhbCodeRule {
1487 value: "Z04".to_string(),
1488 description: "Anschlussnutzer".to_string(),
1489 ahb_status: "X".to_string(),
1490 }],
1491 parent_group_ahb_status: None,
1492 ..Default::default()
1493 },
1494 AhbFieldRule {
1495 segment_path: "SG4/SG12/NAD/3035".to_string(),
1496 name: "Korrespondenzanschrift".to_string(),
1497 ahb_status: "X".to_string(),
1498 codes: vec![AhbCodeRule {
1499 value: "Z09".to_string(),
1500 description: "Korrespondenzanschrift".to_string(),
1501 ahb_status: "X".to_string(),
1502 }],
1503 parent_group_ahb_status: None,
1504 ..Default::default()
1505 },
1506 ],
1507 ub_definitions: HashMap::new(),
1508 };
1509
1510 let report =
1511 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
1512
1513 let code_errors: Vec<_> = report
1514 .by_category(ValidationCategory::Code)
1515 .filter(|i| i.severity == Severity::Error)
1516 .collect();
1517 assert_eq!(
1518 code_errors.len(),
1519 1,
1520 "Expected exactly one COD002 error for MT, got: {:?}",
1521 code_errors
1522 );
1523 assert!(code_errors[0].message.contains("MT"));
1524 }
1525
1526 #[test]
1527 fn test_is_qualifier_field_simple_paths() {
1528 assert!(is_qualifier_field("NAD/3035"));
1529 assert!(is_qualifier_field("SG2/NAD/3035"));
1530 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
1531 assert!(is_qualifier_field("LOC/3227"));
1532 }
1533
1534 #[test]
1535 fn test_is_qualifier_field_composite_paths() {
1536 assert!(!is_qualifier_field("UNH/S009/0065"));
1537 assert!(!is_qualifier_field("NAD/C082/3039"));
1538 assert!(!is_qualifier_field("SG2/NAD/C082/3039"));
1539 }
1540
1541 #[test]
1542 fn test_is_qualifier_field_bare_segment() {
1543 assert!(!is_qualifier_field("NAD"));
1544 assert!(!is_qualifier_field("SG2/NAD"));
1545 }
1546
1547 #[test]
1548 fn test_missing_qualifier_instance_is_detected() {
1549 let evaluator = MockEvaluator::new(vec![]);
1552 let validator = EdifactValidator::new(evaluator);
1553 let external = NoOpExternalProvider;
1554
1555 let nad_ms = OwnedSegment {
1556 id: "NAD".to_string(),
1557 elements: vec![vec!["MS".to_string()]],
1558 segment_number: 3,
1559 };
1560
1561 let workflow = AhbWorkflow {
1562 pruefidentifikator: "55001".to_string(),
1563 description: "Test".to_string(),
1564 communication_direction: None,
1565 fields: vec![
1566 AhbFieldRule {
1567 segment_path: "SG2/NAD/3035".to_string(),
1568 name: "Absender".to_string(),
1569 ahb_status: "X".to_string(),
1570 codes: vec![AhbCodeRule {
1571 value: "MS".to_string(),
1572 description: "Absender".to_string(),
1573 ahb_status: "X".to_string(),
1574 }],
1575 parent_group_ahb_status: None,
1576 ..Default::default()
1577 },
1578 AhbFieldRule {
1579 segment_path: "SG2/NAD/3035".to_string(),
1580 name: "Empfaenger".to_string(),
1581 ahb_status: "Muss".to_string(),
1582 codes: vec![AhbCodeRule {
1583 value: "MR".to_string(),
1584 description: "Empfaenger".to_string(),
1585 ahb_status: "X".to_string(),
1586 }],
1587 parent_group_ahb_status: None,
1588 ..Default::default()
1589 },
1590 ],
1591 ub_definitions: HashMap::new(),
1592 };
1593
1594 let report =
1595 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
1596
1597 let ahb_errors: Vec<_> = report
1598 .by_category(ValidationCategory::Ahb)
1599 .filter(|i| i.severity == Severity::Error)
1600 .collect();
1601 assert_eq!(
1602 ahb_errors.len(),
1603 1,
1604 "Expected AHB001 for missing NAD+MR, got: {:?}",
1605 ahb_errors
1606 );
1607 assert!(ahb_errors[0].message.contains("Empfaenger"));
1608 }
1609
1610 #[test]
1611 fn test_present_qualifier_instance_no_error() {
1612 let evaluator = MockEvaluator::new(vec![]);
1614 let validator = EdifactValidator::new(evaluator);
1615 let external = NoOpExternalProvider;
1616
1617 let segments = vec![
1618 OwnedSegment {
1619 id: "NAD".to_string(),
1620 elements: vec![vec!["MS".to_string()]],
1621 segment_number: 3,
1622 },
1623 OwnedSegment {
1624 id: "NAD".to_string(),
1625 elements: vec![vec!["MR".to_string()]],
1626 segment_number: 4,
1627 },
1628 ];
1629
1630 let workflow = AhbWorkflow {
1631 pruefidentifikator: "55001".to_string(),
1632 description: "Test".to_string(),
1633 communication_direction: None,
1634 fields: vec![
1635 AhbFieldRule {
1636 segment_path: "SG2/NAD/3035".to_string(),
1637 name: "Absender".to_string(),
1638 ahb_status: "Muss".to_string(),
1639 codes: vec![AhbCodeRule {
1640 value: "MS".to_string(),
1641 description: "Absender".to_string(),
1642 ahb_status: "X".to_string(),
1643 }],
1644 parent_group_ahb_status: None,
1645 ..Default::default()
1646 },
1647 AhbFieldRule {
1648 segment_path: "SG2/NAD/3035".to_string(),
1649 name: "Empfaenger".to_string(),
1650 ahb_status: "Muss".to_string(),
1651 codes: vec![AhbCodeRule {
1652 value: "MR".to_string(),
1653 description: "Empfaenger".to_string(),
1654 ahb_status: "X".to_string(),
1655 }],
1656 parent_group_ahb_status: None,
1657 ..Default::default()
1658 },
1659 ],
1660 ub_definitions: HashMap::new(),
1661 };
1662
1663 let report =
1664 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
1665
1666 let ahb_errors: Vec<_> = report
1667 .by_category(ValidationCategory::Ahb)
1668 .filter(|i| i.severity == Severity::Error)
1669 .collect();
1670 assert!(
1671 ahb_errors.is_empty(),
1672 "Expected no AHB001 errors, got: {:?}",
1673 ahb_errors
1674 );
1675 }
1676
1677 #[test]
1678 fn test_extract_group_path_key() {
1679 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
1680 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
1681 assert_eq!(extract_group_path_key("NAD/3035"), "");
1682 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
1683 }
1684
1685 #[test]
1686 fn test_absent_optional_group_no_missing_field_error() {
1687 use mig_types::navigator::GroupNavigator;
1690
1691 struct NavWithoutSG3;
1692 impl GroupNavigator for NavWithoutSG3 {
1693 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
1694 vec![]
1695 }
1696 fn find_segments_with_qualifier_in_group(
1697 &self,
1698 _: &str,
1699 _: usize,
1700 _: &str,
1701 _: &[&str],
1702 _: usize,
1703 ) -> Vec<OwnedSegment> {
1704 vec![]
1705 }
1706 fn group_instance_count(&self, group_path: &[&str]) -> usize {
1707 match group_path {
1708 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
1711 }
1712 }
1713 }
1714
1715 let evaluator = MockEvaluator::new(vec![]);
1716 let validator = EdifactValidator::new(evaluator);
1717 let external = NoOpExternalProvider;
1718 let nav = NavWithoutSG3;
1719
1720 let segments = vec![
1722 OwnedSegment {
1723 id: "NAD".into(),
1724 elements: vec![vec!["MS".into()]],
1725 segment_number: 3,
1726 },
1727 OwnedSegment {
1728 id: "NAD".into(),
1729 elements: vec![vec!["MR".into()]],
1730 segment_number: 4,
1731 },
1732 ];
1733
1734 let workflow = AhbWorkflow {
1735 pruefidentifikator: "55001".to_string(),
1736 description: "Test".to_string(),
1737 communication_direction: None,
1738 fields: vec![
1739 AhbFieldRule {
1740 segment_path: "SG2/SG3/CTA/3139".to_string(),
1741 name: "Funktion des Ansprechpartners, Code".to_string(),
1742 ahb_status: "Muss".to_string(),
1743 codes: vec![],
1744 parent_group_ahb_status: None,
1745 ..Default::default()
1746 },
1747 AhbFieldRule {
1748 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
1749 name: "Name vom Ansprechpartner".to_string(),
1750 ahb_status: "X".to_string(),
1751 codes: vec![],
1752 parent_group_ahb_status: None,
1753 ..Default::default()
1754 },
1755 ],
1756 ub_definitions: HashMap::new(),
1757 };
1758
1759 let report = validator.validate_with_navigator(
1760 &segments,
1761 &workflow,
1762 &external,
1763 ValidationLevel::Conditions,
1764 &nav,
1765 );
1766
1767 let ahb_errors: Vec<_> = report
1768 .by_category(ValidationCategory::Ahb)
1769 .filter(|i| i.severity == Severity::Error)
1770 .collect();
1771 assert!(
1772 ahb_errors.is_empty(),
1773 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
1774 ahb_errors
1775 );
1776 }
1777
1778 #[test]
1779 fn test_present_group_still_checks_mandatory_fields() {
1780 use mig_types::navigator::GroupNavigator;
1782
1783 struct NavWithSG3;
1784 impl GroupNavigator for NavWithSG3 {
1785 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
1786 vec![]
1787 }
1788 fn find_segments_with_qualifier_in_group(
1789 &self,
1790 _: &str,
1791 _: usize,
1792 _: &str,
1793 _: &[&str],
1794 _: usize,
1795 ) -> Vec<OwnedSegment> {
1796 vec![]
1797 }
1798 fn group_instance_count(&self, group_path: &[&str]) -> usize {
1799 match group_path {
1800 ["SG2"] => 1,
1801 ["SG2", "SG3"] => 1, _ => 0,
1803 }
1804 }
1805 }
1806
1807 let evaluator = MockEvaluator::new(vec![]);
1808 let validator = EdifactValidator::new(evaluator);
1809 let external = NoOpExternalProvider;
1810 let nav = NavWithSG3;
1811
1812 let segments = vec![OwnedSegment {
1814 id: "NAD".into(),
1815 elements: vec![vec!["MS".into()]],
1816 segment_number: 3,
1817 }];
1818
1819 let workflow = AhbWorkflow {
1820 pruefidentifikator: "55001".to_string(),
1821 description: "Test".to_string(),
1822 communication_direction: None,
1823 fields: vec![AhbFieldRule {
1824 segment_path: "SG2/SG3/CTA/3139".to_string(),
1825 name: "Funktion des Ansprechpartners, Code".to_string(),
1826 ahb_status: "Muss".to_string(),
1827 codes: vec![],
1828 parent_group_ahb_status: None,
1829 ..Default::default()
1830 }],
1831 ub_definitions: HashMap::new(),
1832 };
1833
1834 let report = validator.validate_with_navigator(
1835 &segments,
1836 &workflow,
1837 &external,
1838 ValidationLevel::Conditions,
1839 &nav,
1840 );
1841
1842 let ahb_errors: Vec<_> = report
1843 .by_category(ValidationCategory::Ahb)
1844 .filter(|i| i.severity == Severity::Error)
1845 .collect();
1846 assert_eq!(
1847 ahb_errors.len(),
1848 1,
1849 "Expected AHB001 error when SG3 is present but CTA missing"
1850 );
1851 assert!(ahb_errors[0].message.contains("CTA"));
1852 }
1853
1854 #[test]
1855 fn test_missing_qualifier_with_navigator_is_detected() {
1856 use mig_types::navigator::GroupNavigator;
1859
1860 struct NavWithSG2;
1861 impl GroupNavigator for NavWithSG2 {
1862 fn find_segments_in_group(
1863 &self,
1864 segment_id: &str,
1865 group_path: &[&str],
1866 instance_index: usize,
1867 ) -> Vec<OwnedSegment> {
1868 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
1869 vec![OwnedSegment {
1870 id: "NAD".into(),
1871 elements: vec![vec!["MS".into()]],
1872 segment_number: 3,
1873 }]
1874 } else {
1875 vec![]
1876 }
1877 }
1878 fn find_segments_with_qualifier_in_group(
1879 &self,
1880 _: &str,
1881 _: usize,
1882 _: &str,
1883 _: &[&str],
1884 _: usize,
1885 ) -> Vec<OwnedSegment> {
1886 vec![]
1887 }
1888 fn group_instance_count(&self, group_path: &[&str]) -> usize {
1889 match group_path {
1890 ["SG2"] => 1,
1891 _ => 0,
1892 }
1893 }
1894 }
1895
1896 let evaluator = MockEvaluator::new(vec![]);
1897 let validator = EdifactValidator::new(evaluator);
1898 let external = NoOpExternalProvider;
1899 let nav = NavWithSG2;
1900
1901 let segments = vec![OwnedSegment {
1902 id: "NAD".into(),
1903 elements: vec![vec!["MS".into()]],
1904 segment_number: 3,
1905 }];
1906
1907 let workflow = AhbWorkflow {
1908 pruefidentifikator: "55001".to_string(),
1909 description: "Test".to_string(),
1910 communication_direction: None,
1911 fields: vec![
1912 AhbFieldRule {
1913 segment_path: "SG2/NAD/3035".to_string(),
1914 name: "Absender".to_string(),
1915 ahb_status: "X".to_string(),
1916 codes: vec![AhbCodeRule {
1917 value: "MS".to_string(),
1918 description: "Absender".to_string(),
1919 ahb_status: "X".to_string(),
1920 }],
1921 parent_group_ahb_status: None,
1922 ..Default::default()
1923 },
1924 AhbFieldRule {
1925 segment_path: "SG2/NAD/3035".to_string(),
1926 name: "Empfaenger".to_string(),
1927 ahb_status: "Muss".to_string(),
1928 codes: vec![AhbCodeRule {
1929 value: "MR".to_string(),
1930 description: "Empfaenger".to_string(),
1931 ahb_status: "X".to_string(),
1932 }],
1933 parent_group_ahb_status: None,
1934 ..Default::default()
1935 },
1936 ],
1937 ub_definitions: HashMap::new(),
1938 };
1939
1940 let report = validator.validate_with_navigator(
1941 &segments,
1942 &workflow,
1943 &external,
1944 ValidationLevel::Conditions,
1945 &nav,
1946 );
1947
1948 let ahb_errors: Vec<_> = report
1949 .by_category(ValidationCategory::Ahb)
1950 .filter(|i| i.severity == Severity::Error)
1951 .collect();
1952 assert_eq!(
1953 ahb_errors.len(),
1954 1,
1955 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
1956 ahb_errors
1957 );
1958 assert!(ahb_errors[0].message.contains("Empfaenger"));
1959 }
1960
1961 #[test]
1962 fn test_optional_group_variant_absent_no_error() {
1963 use mig_types::navigator::GroupNavigator;
1968
1969 struct TestNav;
1970 impl GroupNavigator for TestNav {
1971 fn find_segments_in_group(
1972 &self,
1973 segment_id: &str,
1974 group_path: &[&str],
1975 instance_index: usize,
1976 ) -> Vec<OwnedSegment> {
1977 match (segment_id, group_path, instance_index) {
1978 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
1979 id: "LOC".into(),
1980 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
1981 segment_number: 10,
1982 }],
1983 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
1984 id: "NAD".into(),
1985 elements: vec![vec!["MS".into()]],
1986 segment_number: 3,
1987 }],
1988 _ => vec![],
1989 }
1990 }
1991 fn find_segments_with_qualifier_in_group(
1992 &self,
1993 _: &str,
1994 _: usize,
1995 _: &str,
1996 _: &[&str],
1997 _: usize,
1998 ) -> Vec<OwnedSegment> {
1999 vec![]
2000 }
2001 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2002 match group_path {
2003 ["SG2"] => 1,
2004 ["SG4"] => 1,
2005 ["SG4", "SG5"] => 1, _ => 0,
2007 }
2008 }
2009 }
2010
2011 let evaluator = MockEvaluator::new(vec![]);
2012 let validator = EdifactValidator::new(evaluator);
2013 let external = NoOpExternalProvider;
2014 let nav = TestNav;
2015
2016 let segments = vec![
2017 OwnedSegment {
2018 id: "NAD".into(),
2019 elements: vec![vec!["MS".into()]],
2020 segment_number: 3,
2021 },
2022 OwnedSegment {
2023 id: "LOC".into(),
2024 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2025 segment_number: 10,
2026 },
2027 ];
2028
2029 let workflow = AhbWorkflow {
2030 pruefidentifikator: "55001".to_string(),
2031 description: "Test".to_string(),
2032 communication_direction: None,
2033 fields: vec![
2034 AhbFieldRule {
2036 segment_path: "SG2/NAD/3035".to_string(),
2037 name: "Absender".to_string(),
2038 ahb_status: "X".to_string(),
2039 codes: vec![AhbCodeRule {
2040 value: "MS".to_string(),
2041 description: "Absender".to_string(),
2042 ahb_status: "X".to_string(),
2043 }],
2044 parent_group_ahb_status: Some("Muss".to_string()),
2045 ..Default::default()
2046 },
2047 AhbFieldRule {
2048 segment_path: "SG2/NAD/3035".to_string(),
2049 name: "Empfaenger".to_string(),
2050 ahb_status: "Muss".to_string(),
2051 codes: vec![AhbCodeRule {
2052 value: "MR".to_string(),
2053 description: "Empfaenger".to_string(),
2054 ahb_status: "X".to_string(),
2055 }],
2056 parent_group_ahb_status: Some("Muss".to_string()),
2057 ..Default::default()
2058 },
2059 AhbFieldRule {
2061 segment_path: "SG4/SG5/LOC/3227".to_string(),
2062 name: "Ortsangabe, Qualifier (Z16)".to_string(),
2063 ahb_status: "X".to_string(),
2064 codes: vec![AhbCodeRule {
2065 value: "Z16".to_string(),
2066 description: "Marktlokation".to_string(),
2067 ahb_status: "X".to_string(),
2068 }],
2069 parent_group_ahb_status: Some("Kann".to_string()),
2070 ..Default::default()
2071 },
2072 AhbFieldRule {
2073 segment_path: "SG4/SG5/LOC/3227".to_string(),
2074 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2075 ahb_status: "Muss".to_string(),
2076 codes: vec![AhbCodeRule {
2077 value: "Z17".to_string(),
2078 description: "Messlokation".to_string(),
2079 ahb_status: "X".to_string(),
2080 }],
2081 parent_group_ahb_status: Some("Kann".to_string()),
2082 ..Default::default()
2083 },
2084 ],
2085 ub_definitions: HashMap::new(),
2086 };
2087
2088 let report = validator.validate_with_navigator(
2089 &segments,
2090 &workflow,
2091 &external,
2092 ValidationLevel::Conditions,
2093 &nav,
2094 );
2095
2096 let ahb_errors: Vec<_> = report
2097 .by_category(ValidationCategory::Ahb)
2098 .filter(|i| i.severity == Severity::Error)
2099 .collect();
2100
2101 assert_eq!(
2104 ahb_errors.len(),
2105 1,
2106 "Expected only AHB001 for missing NAD+MR, got: {:?}",
2107 ahb_errors
2108 );
2109 assert!(
2110 ahb_errors[0].message.contains("Empfaenger"),
2111 "Error should be for missing NAD+MR (Empfaenger)"
2112 );
2113 }
2114
2115 #[test]
2116 fn test_conditional_group_variant_absent_no_error() {
2117 use mig_types::navigator::GroupNavigator;
2122
2123 struct TestNav;
2124 impl GroupNavigator for TestNav {
2125 fn find_segments_in_group(
2126 &self,
2127 segment_id: &str,
2128 group_path: &[&str],
2129 instance_index: usize,
2130 ) -> Vec<OwnedSegment> {
2131 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
2132 vec![OwnedSegment {
2133 id: "LOC".into(),
2134 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2135 segment_number: 10,
2136 }]
2137 } else {
2138 vec![]
2139 }
2140 }
2141 fn find_segments_with_qualifier_in_group(
2142 &self,
2143 _: &str,
2144 _: usize,
2145 _: &str,
2146 _: &[&str],
2147 _: usize,
2148 ) -> Vec<OwnedSegment> {
2149 vec![]
2150 }
2151 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2152 match group_path {
2153 ["SG4"] => 1,
2154 ["SG4", "SG5"] => 1, _ => 0,
2156 }
2157 }
2158 }
2159
2160 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
2163 let validator = EdifactValidator::new(evaluator);
2164 let external = NoOpExternalProvider;
2165 let nav = TestNav;
2166
2167 let segments = vec![OwnedSegment {
2168 id: "LOC".into(),
2169 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2170 segment_number: 10,
2171 }];
2172
2173 let workflow = AhbWorkflow {
2174 pruefidentifikator: "55001".to_string(),
2175 description: "Test".to_string(),
2176 communication_direction: None,
2177 fields: vec![
2178 AhbFieldRule {
2180 segment_path: "SG4/SG5/LOC/3227".to_string(),
2181 name: "Ortsangabe, Qualifier (Z16)".to_string(),
2182 ahb_status: "X".to_string(),
2183 codes: vec![AhbCodeRule {
2184 value: "Z16".to_string(),
2185 description: "Marktlokation".to_string(),
2186 ahb_status: "X".to_string(),
2187 }],
2188 parent_group_ahb_status: Some("Muss [2061]".to_string()),
2189 ..Default::default()
2190 },
2191 AhbFieldRule {
2193 segment_path: "SG4/SG5/LOC/3227".to_string(),
2194 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2195 ahb_status: "X".to_string(),
2196 codes: vec![AhbCodeRule {
2197 value: "Z17".to_string(),
2198 description: "Messlokation".to_string(),
2199 ahb_status: "X".to_string(),
2200 }],
2201 parent_group_ahb_status: Some("Soll [165]".to_string()),
2202 ..Default::default()
2203 },
2204 ],
2205 ub_definitions: HashMap::new(),
2206 };
2207
2208 let report = validator.validate_with_navigator(
2209 &segments,
2210 &workflow,
2211 &external,
2212 ValidationLevel::Conditions,
2213 &nav,
2214 );
2215
2216 let ahb_errors: Vec<_> = report
2217 .by_category(ValidationCategory::Ahb)
2218 .filter(|i| i.severity == Severity::Error)
2219 .collect();
2220
2221 assert!(
2223 ahb_errors.is_empty(),
2224 "Expected no errors when conditional group variant [165]=False, got: {:?}",
2225 ahb_errors
2226 );
2227 }
2228
2229 #[test]
2230 fn test_conditional_group_variant_unknown_no_error() {
2231 let evaluator = MockEvaluator::new(vec![]);
2237 let validator = EdifactValidator::new(evaluator);
2238 let external = NoOpExternalProvider;
2239
2240 let workflow = AhbWorkflow {
2241 pruefidentifikator: "55001".to_string(),
2242 description: "Test".to_string(),
2243 communication_direction: None,
2244 fields: vec![AhbFieldRule {
2245 segment_path: "SG4/SG5/LOC/3227".to_string(),
2246 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2247 ahb_status: "X".to_string(),
2248 codes: vec![AhbCodeRule {
2249 value: "Z17".to_string(),
2250 description: "Messlokation".to_string(),
2251 ahb_status: "X".to_string(),
2252 }],
2253 parent_group_ahb_status: Some("Soll [165]".to_string()),
2254 ..Default::default()
2255 }],
2256 ub_definitions: HashMap::new(),
2257 };
2258
2259 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2260
2261 let ahb_errors: Vec<_> = report
2262 .by_category(ValidationCategory::Ahb)
2263 .filter(|i| i.severity == Severity::Error)
2264 .collect();
2265
2266 assert!(
2268 ahb_errors.is_empty(),
2269 "Expected no errors when parent group condition is Unknown, got: {:?}",
2270 ahb_errors
2271 );
2272 }
2273
2274 #[test]
2275 fn test_segment_absent_within_present_group_no_error() {
2276 use mig_types::navigator::GroupNavigator;
2280
2281 struct TestNav;
2282 impl GroupNavigator for TestNav {
2283 fn find_segments_in_group(
2284 &self,
2285 segment_id: &str,
2286 group_path: &[&str],
2287 instance_index: usize,
2288 ) -> Vec<OwnedSegment> {
2289 if segment_id == "QTY"
2291 && group_path == ["SG5", "SG6", "SG9", "SG10"]
2292 && instance_index == 0
2293 {
2294 vec![OwnedSegment {
2295 id: "QTY".into(),
2296 elements: vec![vec!["220".into(), "0".into()]],
2297 segment_number: 14,
2298 }]
2299 } else {
2300 vec![]
2301 }
2302 }
2303 fn find_segments_with_qualifier_in_group(
2304 &self,
2305 _: &str,
2306 _: usize,
2307 _: &str,
2308 _: &[&str],
2309 _: usize,
2310 ) -> Vec<OwnedSegment> {
2311 vec![]
2312 }
2313 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2314 match group_path {
2315 ["SG5"] => 1,
2316 ["SG5", "SG6"] => 1,
2317 ["SG5", "SG6", "SG9"] => 1,
2318 ["SG5", "SG6", "SG9", "SG10"] => 1,
2319 _ => 0,
2320 }
2321 }
2322 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
2323 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
2325 }
2326 }
2327
2328 let evaluator = MockEvaluator::all_true(&[]);
2329 let validator = EdifactValidator::new(evaluator);
2330 let external = NoOpExternalProvider;
2331 let nav = TestNav;
2332
2333 let segments = vec![OwnedSegment {
2334 id: "QTY".into(),
2335 elements: vec![vec!["220".into(), "0".into()]],
2336 segment_number: 14,
2337 }];
2338
2339 let workflow = AhbWorkflow {
2340 pruefidentifikator: "13017".to_string(),
2341 description: "Test".to_string(),
2342 communication_direction: None,
2343 fields: vec![
2344 AhbFieldRule {
2346 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
2347 name: "Statuskategorie, Code".to_string(),
2348 ahb_status: "X".to_string(),
2349 codes: vec![],
2350 parent_group_ahb_status: Some("Muss".to_string()),
2351 ..Default::default()
2352 },
2353 AhbFieldRule {
2355 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2356 name: "Statusanlaß, Code".to_string(),
2357 ahb_status: "X [5]".to_string(),
2358 codes: vec![],
2359 parent_group_ahb_status: Some("Muss".to_string()),
2360 ..Default::default()
2361 },
2362 ],
2363 ub_definitions: HashMap::new(),
2364 };
2365
2366 let report = validator.validate_with_navigator(
2367 &segments,
2368 &workflow,
2369 &external,
2370 ValidationLevel::Conditions,
2371 &nav,
2372 );
2373
2374 let ahb_errors: Vec<_> = report
2375 .by_category(ValidationCategory::Ahb)
2376 .filter(|i| i.severity == Severity::Error)
2377 .collect();
2378
2379 assert!(
2380 ahb_errors.is_empty(),
2381 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
2382 ahb_errors
2383 );
2384 }
2385
2386 #[test]
2387 fn test_group_scoped_code_validation_with_navigator() {
2388 use mig_types::navigator::GroupNavigator;
2392
2393 struct TestNav;
2394 impl GroupNavigator for TestNav {
2395 fn find_segments_in_group(
2396 &self,
2397 segment_id: &str,
2398 group_path: &[&str],
2399 _instance_index: usize,
2400 ) -> Vec<OwnedSegment> {
2401 if segment_id != "NAD" {
2402 return vec![];
2403 }
2404 match group_path {
2405 ["SG2"] => vec![
2406 OwnedSegment {
2407 id: "NAD".into(),
2408 elements: vec![vec!["MS".into()]],
2409 segment_number: 3,
2410 },
2411 OwnedSegment {
2412 id: "NAD".into(),
2413 elements: vec![vec!["MT".into()]], segment_number: 4,
2415 },
2416 ],
2417 ["SG4", "SG12"] => vec![
2418 OwnedSegment {
2419 id: "NAD".into(),
2420 elements: vec![vec!["Z04".into()]],
2421 segment_number: 20,
2422 },
2423 OwnedSegment {
2424 id: "NAD".into(),
2425 elements: vec![vec!["Z09".into()]],
2426 segment_number: 21,
2427 },
2428 ],
2429 _ => vec![],
2430 }
2431 }
2432 fn find_segments_with_qualifier_in_group(
2433 &self,
2434 _: &str,
2435 _: usize,
2436 _: &str,
2437 _: &[&str],
2438 _: usize,
2439 ) -> Vec<OwnedSegment> {
2440 vec![]
2441 }
2442 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2443 match group_path {
2444 ["SG2"] | ["SG4", "SG12"] => 1,
2445 _ => 0,
2446 }
2447 }
2448 }
2449
2450 let evaluator = MockEvaluator::new(vec![]);
2451 let validator = EdifactValidator::new(evaluator);
2452 let external = NoOpExternalProvider;
2453 let nav = TestNav;
2454
2455 let workflow = AhbWorkflow {
2456 pruefidentifikator: "55001".to_string(),
2457 description: "Test".to_string(),
2458 communication_direction: None,
2459 fields: vec![
2460 AhbFieldRule {
2461 segment_path: "SG2/NAD/3035".to_string(),
2462 name: "Absender".to_string(),
2463 ahb_status: "X".to_string(),
2464 codes: vec![AhbCodeRule {
2465 value: "MS".to_string(),
2466 description: "Absender".to_string(),
2467 ahb_status: "X".to_string(),
2468 }],
2469 parent_group_ahb_status: None,
2470 ..Default::default()
2471 },
2472 AhbFieldRule {
2473 segment_path: "SG2/NAD/3035".to_string(),
2474 name: "Empfaenger".to_string(),
2475 ahb_status: "X".to_string(),
2476 codes: vec![AhbCodeRule {
2477 value: "MR".to_string(),
2478 description: "Empfaenger".to_string(),
2479 ahb_status: "X".to_string(),
2480 }],
2481 parent_group_ahb_status: None,
2482 ..Default::default()
2483 },
2484 AhbFieldRule {
2485 segment_path: "SG4/SG12/NAD/3035".to_string(),
2486 name: "Anschlussnutzer".to_string(),
2487 ahb_status: "X".to_string(),
2488 codes: vec![AhbCodeRule {
2489 value: "Z04".to_string(),
2490 description: "Anschlussnutzer".to_string(),
2491 ahb_status: "X".to_string(),
2492 }],
2493 parent_group_ahb_status: None,
2494 ..Default::default()
2495 },
2496 AhbFieldRule {
2497 segment_path: "SG4/SG12/NAD/3035".to_string(),
2498 name: "Korrespondenzanschrift".to_string(),
2499 ahb_status: "X".to_string(),
2500 codes: vec![AhbCodeRule {
2501 value: "Z09".to_string(),
2502 description: "Korrespondenzanschrift".to_string(),
2503 ahb_status: "X".to_string(),
2504 }],
2505 parent_group_ahb_status: None,
2506 ..Default::default()
2507 },
2508 ],
2509 ub_definitions: HashMap::new(),
2510 };
2511
2512 let all_segments = vec![
2514 OwnedSegment {
2515 id: "NAD".into(),
2516 elements: vec![vec!["MS".into()]],
2517 segment_number: 3,
2518 },
2519 OwnedSegment {
2520 id: "NAD".into(),
2521 elements: vec![vec!["MT".into()]],
2522 segment_number: 4,
2523 },
2524 OwnedSegment {
2525 id: "NAD".into(),
2526 elements: vec![vec!["Z04".into()]],
2527 segment_number: 20,
2528 },
2529 OwnedSegment {
2530 id: "NAD".into(),
2531 elements: vec![vec!["Z09".into()]],
2532 segment_number: 21,
2533 },
2534 ];
2535
2536 let report = validator.validate_with_navigator(
2537 &all_segments,
2538 &workflow,
2539 &external,
2540 ValidationLevel::Conditions,
2541 &nav,
2542 );
2543
2544 let code_errors: Vec<_> = report
2545 .by_category(ValidationCategory::Code)
2546 .filter(|i| i.severity == Severity::Error)
2547 .collect();
2548
2549 assert_eq!(
2552 code_errors.len(),
2553 1,
2554 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
2555 code_errors
2556 );
2557 assert!(code_errors[0].message.contains("MT"));
2558 assert!(code_errors[0].message.contains("MR"));
2560 assert!(code_errors[0].message.contains("MS"));
2561 assert!(
2562 !code_errors[0].message.contains("Z04"),
2563 "SG4/SG12 codes should not leak into SG2 error"
2564 );
2565 assert!(
2567 code_errors[0]
2568 .field_path
2569 .as_deref()
2570 .unwrap_or("")
2571 .contains("SG2"),
2572 "Error field_path should reference SG2, got: {:?}",
2573 code_errors[0].field_path
2574 );
2575 }
2576
2577 #[test]
2580 fn test_package_cardinality_within_bounds() {
2581 let evaluator = MockEvaluator::all_true(&[]);
2583 let validator = EdifactValidator::new(evaluator);
2584 let external = NoOpExternalProvider;
2585
2586 let segments = vec![OwnedSegment {
2587 id: "STS".into(),
2588 elements: vec![
2589 vec!["Z33".into()], vec![], vec!["E01".into()], ],
2593 segment_number: 5,
2594 }];
2595
2596 let workflow = AhbWorkflow {
2597 pruefidentifikator: "13017".to_string(),
2598 description: "Test".to_string(),
2599 communication_direction: None,
2600 ub_definitions: HashMap::new(),
2601 fields: vec![AhbFieldRule {
2602 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2603 name: "Statusanlaß, Code".to_string(),
2604 ahb_status: "X".to_string(),
2605 element_index: Some(2),
2606 component_index: Some(0),
2607 codes: vec![
2608 AhbCodeRule {
2609 value: "E01".into(),
2610 description: "Code 1".into(),
2611 ahb_status: "X [4P0..1]".into(),
2612 },
2613 AhbCodeRule {
2614 value: "E02".into(),
2615 description: "Code 2".into(),
2616 ahb_status: "X [4P0..1]".into(),
2617 },
2618 ],
2619 parent_group_ahb_status: Some("Muss".to_string()),
2620 }],
2621 };
2622
2623 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2624 let pkg_errors: Vec<_> = report
2625 .by_category(ValidationCategory::Ahb)
2626 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2627 .collect();
2628 assert!(
2629 pkg_errors.is_empty(),
2630 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
2631 pkg_errors
2632 );
2633 }
2634
2635 #[test]
2636 fn test_package_cardinality_zero_present_min_zero() {
2637 let evaluator = MockEvaluator::all_true(&[]);
2639 let validator = EdifactValidator::new(evaluator);
2640 let external = NoOpExternalProvider;
2641
2642 let segments = vec![OwnedSegment {
2643 id: "STS".into(),
2644 elements: vec![
2645 vec!["Z33".into()],
2646 vec![],
2647 vec!["X99".into()], ],
2649 segment_number: 5,
2650 }];
2651
2652 let workflow = AhbWorkflow {
2653 pruefidentifikator: "13017".to_string(),
2654 description: "Test".to_string(),
2655 communication_direction: None,
2656 ub_definitions: HashMap::new(),
2657 fields: vec![AhbFieldRule {
2658 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2659 name: "Statusanlaß, Code".to_string(),
2660 ahb_status: "X".to_string(),
2661 element_index: Some(2),
2662 component_index: Some(0),
2663 codes: vec![
2664 AhbCodeRule {
2665 value: "E01".into(),
2666 description: "Code 1".into(),
2667 ahb_status: "X [4P0..1]".into(),
2668 },
2669 AhbCodeRule {
2670 value: "E02".into(),
2671 description: "Code 2".into(),
2672 ahb_status: "X [4P0..1]".into(),
2673 },
2674 ],
2675 parent_group_ahb_status: Some("Muss".to_string()),
2676 }],
2677 };
2678
2679 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2680 let pkg_errors: Vec<_> = report
2681 .by_category(ValidationCategory::Ahb)
2682 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2683 .collect();
2684 assert!(
2685 pkg_errors.is_empty(),
2686 "0 codes, min=0 — no error expected, got: {:?}",
2687 pkg_errors
2688 );
2689 }
2690
2691 #[test]
2692 fn test_package_cardinality_too_many() {
2693 let evaluator = MockEvaluator::all_true(&[]);
2695 let validator = EdifactValidator::new(evaluator);
2696 let external = NoOpExternalProvider;
2697
2698 let segments = vec![
2700 OwnedSegment {
2701 id: "STS".into(),
2702 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
2703 segment_number: 5,
2704 },
2705 OwnedSegment {
2706 id: "STS".into(),
2707 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
2708 segment_number: 6,
2709 },
2710 ];
2711
2712 let workflow = AhbWorkflow {
2713 pruefidentifikator: "13017".to_string(),
2714 description: "Test".to_string(),
2715 communication_direction: None,
2716 ub_definitions: HashMap::new(),
2717 fields: vec![AhbFieldRule {
2718 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2719 name: "Statusanlaß, Code".to_string(),
2720 ahb_status: "X".to_string(),
2721 element_index: Some(2),
2722 component_index: Some(0),
2723 codes: vec![
2724 AhbCodeRule {
2725 value: "E01".into(),
2726 description: "Code 1".into(),
2727 ahb_status: "X [4P0..1]".into(),
2728 },
2729 AhbCodeRule {
2730 value: "E02".into(),
2731 description: "Code 2".into(),
2732 ahb_status: "X [4P0..1]".into(),
2733 },
2734 ],
2735 parent_group_ahb_status: Some("Muss".to_string()),
2736 }],
2737 };
2738
2739 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2740 let pkg_errors: Vec<_> = report
2741 .by_category(ValidationCategory::Ahb)
2742 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2743 .collect();
2744 assert_eq!(
2745 pkg_errors.len(),
2746 1,
2747 "2 codes present, max=1 — expected 1 error, got: {:?}",
2748 pkg_errors
2749 );
2750 assert!(pkg_errors[0].message.contains("[4P0..1]"));
2751 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
2752 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
2753 }
2754
2755 #[test]
2756 fn test_package_cardinality_too_few() {
2757 let evaluator = MockEvaluator::all_true(&[]);
2759 let validator = EdifactValidator::new(evaluator);
2760 let external = NoOpExternalProvider;
2761
2762 let segments = vec![OwnedSegment {
2763 id: "STS".into(),
2764 elements: vec![
2765 vec!["Z33".into()],
2766 vec![],
2767 vec!["X99".into()], ],
2769 segment_number: 5,
2770 }];
2771
2772 let workflow = AhbWorkflow {
2773 pruefidentifikator: "13017".to_string(),
2774 description: "Test".to_string(),
2775 communication_direction: None,
2776 ub_definitions: HashMap::new(),
2777 fields: vec![AhbFieldRule {
2778 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2779 name: "Statusanlaß, Code".to_string(),
2780 ahb_status: "X".to_string(),
2781 element_index: Some(2),
2782 component_index: Some(0),
2783 codes: vec![
2784 AhbCodeRule {
2785 value: "E01".into(),
2786 description: "Code 1".into(),
2787 ahb_status: "X [5P1..3]".into(),
2788 },
2789 AhbCodeRule {
2790 value: "E02".into(),
2791 description: "Code 2".into(),
2792 ahb_status: "X [5P1..3]".into(),
2793 },
2794 AhbCodeRule {
2795 value: "E03".into(),
2796 description: "Code 3".into(),
2797 ahb_status: "X [5P1..3]".into(),
2798 },
2799 ],
2800 parent_group_ahb_status: Some("Muss".to_string()),
2801 }],
2802 };
2803
2804 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2805 let pkg_errors: Vec<_> = report
2806 .by_category(ValidationCategory::Ahb)
2807 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2808 .collect();
2809 assert_eq!(
2810 pkg_errors.len(),
2811 1,
2812 "0 codes present, min=1 — expected 1 error, got: {:?}",
2813 pkg_errors
2814 );
2815 assert!(pkg_errors[0].message.contains("[5P1..3]"));
2816 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
2817 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
2818 }
2819
2820 #[test]
2821 fn test_package_cardinality_no_packages_in_workflow() {
2822 let evaluator = MockEvaluator::all_true(&[]);
2824 let validator = EdifactValidator::new(evaluator);
2825 let external = NoOpExternalProvider;
2826
2827 let segments = vec![OwnedSegment {
2828 id: "STS".into(),
2829 elements: vec![vec!["E01".into()]],
2830 segment_number: 5,
2831 }];
2832
2833 let workflow = AhbWorkflow {
2834 pruefidentifikator: "13017".to_string(),
2835 description: "Test".to_string(),
2836 communication_direction: None,
2837 ub_definitions: HashMap::new(),
2838 fields: vec![AhbFieldRule {
2839 segment_path: "STS/9015".to_string(),
2840 name: "Status Code".to_string(),
2841 ahb_status: "X".to_string(),
2842 codes: vec![AhbCodeRule {
2843 value: "E01".into(),
2844 description: "Code 1".into(),
2845 ahb_status: "X".into(),
2846 }],
2847 parent_group_ahb_status: Some("Muss".to_string()),
2848 ..Default::default()
2849 }],
2850 };
2851
2852 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2853 let pkg_errors: Vec<_> = report
2854 .by_category(ValidationCategory::Ahb)
2855 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2856 .collect();
2857 assert!(
2858 pkg_errors.is_empty(),
2859 "No packages in workflow — no errors expected"
2860 );
2861 }
2862
2863 #[test]
2864 fn test_package_cardinality_with_condition_and_package() {
2865 let evaluator = MockEvaluator::all_true(&[901]);
2867 let validator = EdifactValidator::new(evaluator);
2868 let external = NoOpExternalProvider;
2869
2870 let segments = vec![OwnedSegment {
2871 id: "STS".into(),
2872 elements: vec![vec![], vec![], vec!["E01".into()]],
2873 segment_number: 5,
2874 }];
2875
2876 let workflow = AhbWorkflow {
2877 pruefidentifikator: "13017".to_string(),
2878 description: "Test".to_string(),
2879 communication_direction: None,
2880 ub_definitions: HashMap::new(),
2881 fields: vec![AhbFieldRule {
2882 segment_path: "SG10/STS/C556/9013".to_string(),
2883 name: "Code".to_string(),
2884 ahb_status: "X".to_string(),
2885 element_index: Some(2),
2886 component_index: Some(0),
2887 codes: vec![
2888 AhbCodeRule {
2889 value: "E01".into(),
2890 description: "Code 1".into(),
2891 ahb_status: "X [901] [4P0..1]".into(),
2892 },
2893 AhbCodeRule {
2894 value: "E02".into(),
2895 description: "Code 2".into(),
2896 ahb_status: "X [901] [4P0..1]".into(),
2897 },
2898 ],
2899 parent_group_ahb_status: Some("Muss".to_string()),
2900 }],
2901 };
2902
2903 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2904 let pkg_errors: Vec<_> = report
2905 .by_category(ValidationCategory::Ahb)
2906 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2907 .collect();
2908 assert!(
2909 pkg_errors.is_empty(),
2910 "1 code within [4P0..1] bounds — no error, got: {:?}",
2911 pkg_errors
2912 );
2913 }
2914}