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