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