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