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