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::tree::{AhbGroupNode, AhbNode, ValidatedTree};
15
16use super::codes::ErrorCodes;
17use super::issue::{Severity, ValidationCategory, ValidationIssue};
18use super::level::ValidationLevel;
19use super::report::ValidationReport;
20
21#[derive(Debug, Clone, Default)]
26pub struct AhbFieldRule {
27 pub segment_path: String,
29
30 pub name: String,
32
33 pub ahb_status: String,
35
36 pub codes: Vec<AhbCodeRule>,
38
39 pub parent_group_ahb_status: Option<String>,
44
45 pub element_index: Option<usize>,
48
49 pub component_index: Option<usize>,
52
53 pub mig_number: Option<String>,
56}
57
58#[derive(Debug, Clone, Default)]
60pub struct AhbCodeRule {
61 pub value: String,
63
64 pub description: String,
66
67 pub ahb_status: String,
69}
70
71#[derive(Debug, Clone)]
73pub struct AhbWorkflow {
74 pub pruefidentifikator: String,
76
77 pub description: String,
79
80 pub communication_direction: Option<String>,
82
83 pub fields: Vec<AhbFieldRule>,
85
86 pub ub_definitions: HashMap<String, ConditionExpr>,
92}
93
94pub struct EdifactValidator<E: ConditionEvaluator> {
127 evaluator: E,
128}
129
130impl<E: ConditionEvaluator> EdifactValidator<E> {
131 pub fn new(evaluator: E) -> Self {
133 Self { evaluator }
134 }
135
136 pub fn validate(
149 &self,
150 segments: &[OwnedSegment],
151 workflow: &AhbWorkflow,
152 external: &dyn ExternalConditionProvider,
153 level: ValidationLevel,
154 ) -> ValidationReport {
155 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
156 .with_format_version(self.evaluator.format_version())
157 .with_pruefidentifikator(&workflow.pruefidentifikator);
158
159 let ctx = EvaluationContext::new(&workflow.pruefidentifikator, external, segments);
160
161 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
162 self.validate_conditions(workflow, &ctx, &mut report);
163 }
164
165 report
166 }
167
168 pub fn validate_with_navigator(
174 &self,
175 segments: &[OwnedSegment],
176 workflow: &AhbWorkflow,
177 external: &dyn ExternalConditionProvider,
178 level: ValidationLevel,
179 navigator: &dyn GroupNavigator,
180 ) -> ValidationReport {
181 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
182 .with_format_version(self.evaluator.format_version())
183 .with_pruefidentifikator(&workflow.pruefidentifikator);
184
185 let ctx = EvaluationContext::with_navigator(
186 &workflow.pruefidentifikator,
187 external,
188 segments,
189 navigator,
190 );
191
192 if matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
193 self.validate_conditions(workflow, &ctx, &mut report);
194 }
195
196 report
197 }
198
199 pub fn validate_tree(
206 &self,
207 validated_tree: &ValidatedTree,
208 segments: &[OwnedSegment],
209 external: &dyn ExternalConditionProvider,
210 level: ValidationLevel,
211 navigator: Option<&dyn GroupNavigator>,
212 ) -> ValidationReport {
213 let mut report = ValidationReport::new(self.evaluator.message_type(), level)
214 .with_format_version(self.evaluator.format_version())
215 .with_pruefidentifikator(validated_tree.pruefidentifikator);
216
217 if !matches!(level, ValidationLevel::Conditions | ValidationLevel::Full) {
218 return report;
219 }
220
221 let ctx = match navigator {
222 Some(nav) => EvaluationContext::with_navigator(
223 validated_tree.pruefidentifikator,
224 external,
225 segments,
226 nav,
227 ),
228 None => EvaluationContext::new(
229 validated_tree.pruefidentifikator,
230 external,
231 segments,
232 ),
233 };
234
235 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
236
237 let mut all_nodes: Vec<&AhbNode> = Vec::new();
239 all_nodes.extend(validated_tree.root_fields.iter());
240 for group in &validated_tree.groups {
241 collect_nodes_depth_first(group, &mut all_nodes);
242 }
243
244 for node in &all_nodes {
246 let field = node.rule;
247
248 let node_ctx = ctx.with_resolved(node.value, node.segment_elements);
250
251 if should_skip_for_parent_group(field, &expr_eval, &ctx, validated_tree.ub_definitions)
253 {
254 continue;
255 }
256
257 let (condition_result, unknown_ids) = expr_eval.evaluate_status_detailed_with_ub(
260 &field.ahb_status,
261 &node_ctx,
262 validated_tree.ub_definitions,
263 );
264
265 match condition_result {
266 ConditionResult::True => {
267 if is_mandatory_status(&field.ahb_status) && node.value.is_none() {
271 let mut issue = ValidationIssue::new(
272 Severity::Error,
273 ValidationCategory::Ahb,
274 ErrorCodes::MISSING_REQUIRED_FIELD,
275 format!(
276 "Required field '{}' at {} is missing",
277 field.name, field.segment_path
278 ),
279 )
280 .with_field_path(&field.segment_path)
281 .with_rule(&field.ahb_status);
282 if let Some(first_code) = field.codes.first() {
283 issue.expected_value = Some(first_code.value.clone());
284 }
285 report.add_issue(issue);
286 }
287 }
288 ConditionResult::False => {
289 if is_mandatory_status(&field.ahb_status) && node.value.is_some() {
292 report.add_issue(
293 ValidationIssue::new(
294 Severity::Error,
295 ValidationCategory::Ahb,
296 ErrorCodes::CONDITIONAL_RULE_VIOLATION,
297 format!(
298 "Field '{}' at {} is present but does not satisfy condition: {}",
299 field.name, field.segment_path, field.ahb_status
300 ),
301 )
302 .with_field_path(&field.segment_path)
303 .with_rule(&field.ahb_status),
304 );
305 }
306 }
307 ConditionResult::Unknown => {
308 let mut external_ids = Vec::new();
310 let mut undetermined_ids = Vec::new();
311 let mut missing_ids = Vec::new();
312 for id in unknown_ids {
313 if self.evaluator.is_external(id) {
314 external_ids.push(id);
315 } else if self.evaluator.is_known(id) {
316 undetermined_ids.push(id);
317 } else {
318 missing_ids.push(id);
319 }
320 }
321
322 let mut parts = Vec::new();
323 if !external_ids.is_empty() {
324 let ids: Vec<String> =
325 external_ids.iter().map(|id| format!("[{id}]")).collect();
326 parts.push(format!(
327 "external conditions require provider: {}",
328 ids.join(", ")
329 ));
330 }
331 if !undetermined_ids.is_empty() {
332 let ids: Vec<String> = undetermined_ids
333 .iter()
334 .map(|id| format!("[{id}]"))
335 .collect();
336 parts.push(format!(
337 "conditions could not be determined from message data: {}",
338 ids.join(", ")
339 ));
340 }
341 if !missing_ids.is_empty() {
342 let ids: Vec<String> =
343 missing_ids.iter().map(|id| format!("[{id}]")).collect();
344 parts.push(format!("missing conditions: {}", ids.join(", ")));
345 }
346 let detail = if parts.is_empty() {
347 String::new()
348 } else {
349 format!(" ({})", parts.join("; "))
350 };
351 report.add_issue(
352 ValidationIssue::new(
353 Severity::Info,
354 ValidationCategory::Ahb,
355 ErrorCodes::CONDITION_UNKNOWN,
356 format!(
357 "Condition for field '{}' could not be fully evaluated{}",
358 field.name, detail
359 ),
360 )
361 .with_field_path(&field.segment_path)
362 .with_rule(&field.ahb_status),
363 );
364 }
365 }
366 }
367
368 for field in &validated_tree.unmatched_rules {
374 if should_skip_for_parent_group(field, &expr_eval, &ctx, validated_tree.ub_definitions)
375 {
376 continue;
377 }
378
379 let (condition_result, _) = expr_eval.evaluate_status_detailed_with_ub(
380 &field.ahb_status,
381 &ctx,
382 validated_tree.ub_definitions,
383 );
384
385 if matches!(condition_result, ConditionResult::True)
386 && is_mandatory_status(&field.ahb_status)
387 && !is_field_present(&ctx, field)
388 && !is_group_variant_absent(&ctx, field)
389 {
390 let mut issue = ValidationIssue::new(
391 Severity::Error,
392 ValidationCategory::Ahb,
393 ErrorCodes::MISSING_REQUIRED_FIELD,
394 format!(
395 "Required field '{}' at {} is missing",
396 field.name, field.segment_path
397 ),
398 )
399 .with_field_path(&field.segment_path)
400 .with_rule(&field.ahb_status);
401 if let Some(first_code) = field.codes.first() {
402 issue.expected_value = Some(first_code.value.clone());
403 }
404 report.add_issue(issue);
405 }
406 }
407
408 if matches!(level, ValidationLevel::Full) {
412 let mut all_fields: Vec<AhbFieldRule> = Vec::new();
413 for node in &all_nodes {
414 all_fields.push(node.rule.clone());
415 }
416 for rule in &validated_tree.unmatched_rules {
417 all_fields.push((*rule).clone());
418 }
419 let synthetic_workflow = AhbWorkflow {
420 pruefidentifikator: validated_tree.pruefidentifikator.to_string(),
421 description: String::new(),
422 communication_direction: None,
423 fields: all_fields,
424 ub_definitions: validated_tree.ub_definitions.clone(),
425 };
426 self.validate_codes_cross_field(&synthetic_workflow, &ctx, &mut report);
427 self.validate_package_cardinality(&synthetic_workflow, &ctx, &mut report);
428 }
429
430 report
431 }
432
433 fn validate_conditions(
435 &self,
436 workflow: &AhbWorkflow,
437 ctx: &EvaluationContext,
438 report: &mut ValidationReport,
439 ) {
440 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
441
442 for field in &workflow.fields {
443 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
445 continue;
446 }
447
448 let (condition_result, unknown_ids) = expr_eval.evaluate_status_detailed_with_ub(
451 &field.ahb_status,
452 ctx,
453 &workflow.ub_definitions,
454 );
455
456 match condition_result {
457 ConditionResult::True => {
458 if is_mandatory_status(&field.ahb_status)
460 && !is_field_present(ctx, field)
461 && !is_group_variant_absent(ctx, field)
462 {
463 let mut issue = ValidationIssue::new(
464 Severity::Error,
465 ValidationCategory::Ahb,
466 ErrorCodes::MISSING_REQUIRED_FIELD,
467 format!(
468 "Required field '{}' at {} is missing",
469 field.name, field.segment_path
470 ),
471 )
472 .with_field_path(&field.segment_path)
473 .with_rule(&field.ahb_status);
474 if let Some(first_code) = field.codes.first() {
478 issue.expected_value = Some(first_code.value.clone());
479 }
480 report.add_issue(issue);
481 }
482 }
483 ConditionResult::False => {
484 if is_mandatory_status(&field.ahb_status)
490 && is_field_present(ctx, field)
491 {
492 report.add_issue(
493 ValidationIssue::new(
494 Severity::Error,
495 ValidationCategory::Ahb,
496 ErrorCodes::CONDITIONAL_RULE_VIOLATION,
497 format!(
498 "Field '{}' at {} is present but does not satisfy condition: {}",
499 field.name, field.segment_path, field.ahb_status
500 ),
501 )
502 .with_field_path(&field.segment_path)
503 .with_rule(&field.ahb_status),
504 );
505 }
506 }
507 ConditionResult::Unknown => {
508 let mut external_ids = Vec::new();
513 let mut undetermined_ids = Vec::new();
514 let mut missing_ids = Vec::new();
515 for id in unknown_ids {
516 if self.evaluator.is_external(id) {
517 external_ids.push(id);
518 } else if self.evaluator.is_known(id) {
519 undetermined_ids.push(id);
520 } else {
521 missing_ids.push(id);
522 }
523 }
524
525 let mut parts = Vec::new();
526 if !external_ids.is_empty() {
527 let ids: Vec<String> =
528 external_ids.iter().map(|id| format!("[{id}]")).collect();
529 parts.push(format!(
530 "external conditions require provider: {}",
531 ids.join(", ")
532 ));
533 }
534 if !undetermined_ids.is_empty() {
535 let ids: Vec<String> = undetermined_ids
536 .iter()
537 .map(|id| format!("[{id}]"))
538 .collect();
539 parts.push(format!(
540 "conditions could not be determined from message data: {}",
541 ids.join(", ")
542 ));
543 }
544 if !missing_ids.is_empty() {
545 let ids: Vec<String> =
546 missing_ids.iter().map(|id| format!("[{id}]")).collect();
547 parts.push(format!("missing conditions: {}", ids.join(", ")));
548 }
549 let detail = if parts.is_empty() {
550 String::new()
551 } else {
552 format!(" ({})", parts.join("; "))
553 };
554 report.add_issue(
555 ValidationIssue::new(
556 Severity::Info,
557 ValidationCategory::Ahb,
558 ErrorCodes::CONDITION_UNKNOWN,
559 format!(
560 "Condition for field '{}' could not be fully evaluated{}",
561 field.name, detail
562 ),
563 )
564 .with_field_path(&field.segment_path)
565 .with_rule(&field.ahb_status),
566 );
567 }
568 }
569 }
570
571 self.validate_codes_cross_field(workflow, ctx, report);
576
577 self.validate_package_cardinality(workflow, ctx, report);
580 }
581
582 fn validate_package_cardinality(
589 &self,
590 workflow: &AhbWorkflow,
591 ctx: &EvaluationContext,
592 report: &mut ValidationReport,
593 ) {
594 struct PackageGroup {
597 min: u32,
598 max: u32,
599 code_values: Vec<String>,
600 element_index: usize,
601 component_index: usize,
602 }
603
604 let mut groups: HashMap<(String, u32), PackageGroup> = HashMap::new();
606
607 let expr_eval = ConditionExprEvaluator::new(&self.evaluator);
608
609 for field in &workflow.fields {
610 let el_idx = field.element_index.unwrap_or(0);
611 let comp_idx = field.component_index.unwrap_or(0);
612
613 if should_skip_for_parent_group(field, &expr_eval, ctx, &workflow.ub_definitions) {
615 continue;
616 }
617
618 for code in &field.codes {
619 if let Ok(Some(expr)) = ConditionParser::parse(&code.ahb_status) {
621 let mut packages = Vec::new();
623 collect_packages(&expr, &mut packages);
624
625 for (pkg_id, pkg_min, pkg_max) in packages {
626 let key = (field.segment_path.clone(), pkg_id);
627 let group = groups.entry(key).or_insert_with(|| PackageGroup {
628 min: pkg_min,
629 max: pkg_max,
630 code_values: Vec::new(),
631 element_index: el_idx,
632 component_index: comp_idx,
633 });
634 group.min = group.min.max(pkg_min);
637 group.max = group.max.min(pkg_max);
638 group.code_values.push(code.value.clone());
639 }
640 }
641 }
642 }
643
644 for ((seg_path, pkg_id), group) in &groups {
646 let segment_id = extract_segment_id(seg_path);
647 let segments = ctx.find_segments(&segment_id);
648
649 let mut present_count: usize = 0;
651 for seg in &segments {
652 if let Some(value) = seg
653 .elements
654 .get(group.element_index)
655 .and_then(|e| e.get(group.component_index))
656 .filter(|v| !v.is_empty())
657 {
658 if group.code_values.contains(value) {
659 present_count += 1;
660 }
661 }
662 }
663
664 let min = group.min as usize;
665 let max = group.max as usize;
666
667 if present_count < min || present_count > max {
668 let code_list = group.code_values.join(", ");
669 report.add_issue(
670 ValidationIssue::new(
671 Severity::Error,
672 ValidationCategory::Ahb,
673 ErrorCodes::PACKAGE_CARDINALITY_VIOLATION,
674 format!(
675 "Package [{}P{}..{}] at {}: {} code(s) present (allowed {}..{}). Codes in package: [{}]",
676 pkg_id, group.min, group.max, seg_path, present_count, group.min, group.max, code_list
677 ),
678 )
679 .with_field_path(seg_path)
680 .with_expected(format!("{}..{}", group.min, group.max))
681 .with_actual(present_count.to_string()),
682 );
683 }
684 }
685 }
686
687 fn validate_codes_cross_field(
699 &self,
700 workflow: &AhbWorkflow,
701 ctx: &EvaluationContext,
702 report: &mut ValidationReport,
703 ) {
704 if ctx.navigator.is_some() {
705 self.validate_codes_group_scoped(workflow, ctx, report);
706 } else {
707 self.validate_codes_tag_scoped(workflow, ctx, report);
708 }
709 }
710
711 fn validate_codes_group_scoped(
714 &self,
715 workflow: &AhbWorkflow,
716 ctx: &EvaluationContext,
717 report: &mut ValidationReport,
718 ) {
719 type CodeKey = (String, String, usize, usize);
722 let mut codes_by_group: HashMap<CodeKey, HashSet<&str>> = HashMap::new();
723
724 for field in &workflow.fields {
725 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
726 continue;
727 }
728 let tag = extract_segment_id(&field.segment_path);
729 let group_key = extract_group_path_key(&field.segment_path);
730 let el_idx = field.element_index.unwrap_or(0);
731 let comp_idx = field.component_index.unwrap_or(0);
732 let entry = codes_by_group
733 .entry((group_key, tag, el_idx, comp_idx))
734 .or_default();
735 for code in &field.codes {
736 if code.ahb_status == "X" || code.ahb_status.starts_with("Muss") {
737 entry.insert(&code.value);
738 }
739 }
740 }
741
742 let nav = ctx.navigator.unwrap();
743
744 for ((group_key, tag, el_idx, comp_idx), allowed_codes) in &codes_by_group {
745 if allowed_codes.is_empty() {
746 continue;
747 }
748
749 let group_path: Vec<&str> = if group_key.is_empty() {
750 Vec::new()
751 } else {
752 group_key.split('/').collect()
753 };
754
755 if group_path.is_empty() {
757 Self::check_segments_against_codes(
759 ctx.find_segments(tag),
760 allowed_codes,
761 tag,
762 *el_idx,
763 *comp_idx,
764 &format!("{tag}/qualifier"),
765 report,
766 );
767 } else {
768 let instance_count = nav.group_instance_count(&group_path);
769 for i in 0..instance_count {
770 let segments = nav.find_segments_in_group(tag, &group_path, i);
771 let refs: Vec<&OwnedSegment> = segments.iter().collect();
772 Self::check_segments_against_codes(
773 refs,
774 allowed_codes,
775 tag,
776 *el_idx,
777 *comp_idx,
778 &format!("{group_key}/{tag}/qualifier"),
779 report,
780 );
781 }
782 }
783 }
784 }
785
786 fn validate_codes_tag_scoped(
789 &self,
790 workflow: &AhbWorkflow,
791 ctx: &EvaluationContext,
792 report: &mut ValidationReport,
793 ) {
794 let mut codes_by_tag: HashMap<(String, usize, usize), HashSet<&str>> = HashMap::new();
796
797 for field in &workflow.fields {
798 if field.codes.is_empty() || !is_qualifier_field(&field.segment_path) {
799 continue;
800 }
801 let tag = extract_segment_id(&field.segment_path);
802 let el_idx = field.element_index.unwrap_or(0);
803 let comp_idx = field.component_index.unwrap_or(0);
804 let entry = codes_by_tag.entry((tag, el_idx, comp_idx)).or_default();
805 for code in &field.codes {
806 if code.ahb_status == "X" || code.ahb_status.starts_with("Muss") {
807 entry.insert(&code.value);
808 }
809 }
810 }
811
812 for ((tag, el_idx, comp_idx), allowed_codes) in &codes_by_tag {
813 if allowed_codes.is_empty() {
814 continue;
815 }
816 Self::check_segments_against_codes(
817 ctx.find_segments(tag),
818 allowed_codes,
819 tag,
820 *el_idx,
821 *comp_idx,
822 &format!("{tag}/qualifier"),
823 report,
824 );
825 }
826 }
827
828 fn check_segments_against_codes(
830 segments: Vec<&OwnedSegment>,
831 allowed_codes: &HashSet<&str>,
832 _tag: &str,
833 el_idx: usize,
834 comp_idx: usize,
835 field_path: &str,
836 report: &mut ValidationReport,
837 ) {
838 for segment in segments {
839 if let Some(code_value) = segment
840 .elements
841 .get(el_idx)
842 .and_then(|e| e.get(comp_idx))
843 .filter(|v| !v.is_empty())
844 {
845 if !allowed_codes.contains(code_value.as_str()) {
846 let mut sorted_codes: Vec<&str> = allowed_codes.iter().copied().collect();
847 sorted_codes.sort_unstable();
848 report.add_issue(
849 ValidationIssue::new(
850 Severity::Error,
851 ValidationCategory::Code,
852 ErrorCodes::CODE_NOT_ALLOWED_FOR_PID,
853 format!(
854 "Code '{}' is not allowed for this PID. Allowed: [{}]",
855 code_value,
856 sorted_codes.join(", ")
857 ),
858 )
859 .with_field_path(field_path)
860 .with_actual(code_value)
861 .with_expected(sorted_codes.join(", ")),
862 );
863 }
864 }
865 }
866 }
867}
868
869fn should_skip_for_parent_group<E: ConditionEvaluator>(
875 field: &AhbFieldRule,
876 expr_eval: &ConditionExprEvaluator<E>,
877 ctx: &EvaluationContext,
878 ub_definitions: &HashMap<String, ConditionExpr>,
879) -> bool {
880 if let Some(ref group_status) = field.parent_group_ahb_status {
881 if group_status.contains('[') {
882 let result = expr_eval.evaluate_status_with_ub(group_status, ctx, ub_definitions);
883 return matches!(result, ConditionResult::False | ConditionResult::Unknown);
884 }
885 }
886 false
887}
888
889fn is_field_present(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
898 let segment_id = extract_segment_id(&field.segment_path);
899
900 if !field.codes.is_empty() {
906 if let (Some(el_idx), Some(comp_idx)) = (field.element_index, field.component_index) {
907 let required_codes: Vec<&str> =
908 field.codes.iter().map(|c| c.value.as_str()).collect();
909 let matching = ctx.find_segments(&segment_id);
910 return matching.iter().any(|seg| {
911 seg.elements
912 .get(el_idx)
913 .and_then(|e| e.get(comp_idx))
914 .is_some_and(|v| required_codes.contains(&v.as_str()))
915 });
916 }
917 if is_qualifier_field(&field.segment_path) {
920 let required_codes: Vec<&str> =
921 field.codes.iter().map(|c| c.value.as_str()).collect();
922 let el_idx = field.element_index.unwrap_or(0);
923 let comp_idx = field.component_index.unwrap_or(0);
924 let matching = ctx.find_segments(&segment_id);
925 return matching.iter().any(|seg| {
926 seg.elements
927 .get(el_idx)
928 .and_then(|e| e.get(comp_idx))
929 .is_some_and(|v| required_codes.contains(&v.as_str()))
930 });
931 }
932 }
933
934 ctx.has_segment(&segment_id)
935}
936
937fn is_group_variant_absent(ctx: &EvaluationContext, field: &AhbFieldRule) -> bool {
952 let group_path: Vec<&str> = field
953 .segment_path
954 .split('/')
955 .take_while(|p| p.starts_with("SG"))
956 .collect();
957
958 if group_path.is_empty() {
959 return false;
960 }
961
962 let nav = match ctx.navigator {
963 Some(nav) => nav,
964 None => return false,
965 };
966
967 let instance_count = nav.group_instance_count(&group_path);
968
969 if instance_count == 0 {
974 let is_group_mandatory = field
975 .parent_group_ahb_status
976 .as_deref()
977 .is_some_and(is_mandatory_status);
978 if !is_group_mandatory {
979 return true;
980 }
981 return false;
983 }
984
985 if let Some(ref group_status) = field.parent_group_ahb_status {
989 if !is_mandatory_status(group_status) && !group_status.contains('[') {
990 if !field.codes.is_empty() && is_qualifier_field(&field.segment_path) {
993 let segment_id = extract_segment_id(&field.segment_path);
994 let required_codes: Vec<&str> =
995 field.codes.iter().map(|c| c.value.as_str()).collect();
996
997 let any_instance_has_qualifier = (0..instance_count).any(|i| {
998 nav.find_segments_in_group(&segment_id, &group_path, i)
999 .iter()
1000 .any(|seg| {
1001 seg.elements
1002 .first()
1003 .and_then(|e| e.first())
1004 .is_some_and(|v| required_codes.contains(&v.as_str()))
1005 })
1006 });
1007
1008 if !any_instance_has_qualifier {
1009 return true; }
1011 }
1012 }
1013 }
1014
1015 let segment_id = extract_segment_id(&field.segment_path);
1024 let segment_absent_from_all = (0..instance_count).all(|i| {
1025 nav.find_segments_in_group(&segment_id, &group_path, i)
1026 .is_empty()
1027 });
1028 if segment_absent_from_all {
1029 let group_has_other_segments =
1030 (0..instance_count).any(|i| nav.has_any_segment_in_group(&group_path, i));
1031 if group_has_other_segments {
1032 return true;
1033 }
1034 }
1035
1036 false
1037}
1038
1039fn collect_nodes_depth_first<'a, 'b>(group: &'b AhbGroupNode<'a>, out: &mut Vec<&'b AhbNode<'a>>) {
1041 out.extend(group.fields.iter());
1042 for child in &group.children {
1043 collect_nodes_depth_first(child, out);
1044 }
1045}
1046
1047fn collect_packages(expr: &ConditionExpr, out: &mut Vec<(u32, u32, u32)>) {
1049 match expr {
1050 ConditionExpr::Package { id, min, max } => {
1051 out.push((*id, *min, *max));
1052 }
1053 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
1054 for e in exprs {
1055 collect_packages(e, out);
1056 }
1057 }
1058 ConditionExpr::Xor(left, right) => {
1059 collect_packages(left, out);
1060 collect_packages(right, out);
1061 }
1062 ConditionExpr::Not(inner) => {
1063 collect_packages(inner, out);
1064 }
1065 ConditionExpr::Ref(_) => {}
1066 }
1067}
1068
1069fn is_mandatory_status(status: &str) -> bool {
1071 let trimmed = status.trim();
1072 trimmed.starts_with("Muss") || trimmed.starts_with('X')
1073}
1074
1075fn is_qualifier_field(path: &str) -> bool {
1084 let parts: Vec<&str> = path.split('/').filter(|p| !p.starts_with("SG")).collect();
1085 parts.len() == 2
1088}
1089
1090fn extract_group_path_key(path: &str) -> String {
1095 let sg_parts: Vec<&str> = path
1096 .split('/')
1097 .take_while(|p| p.starts_with("SG"))
1098 .collect();
1099 sg_parts.join("/")
1100}
1101
1102fn extract_segment_id(path: &str) -> String {
1104 for part in path.split('/') {
1105 if part.starts_with("SG") || part.starts_with("C_") || part.starts_with("D_") {
1107 continue;
1108 }
1109 if part.len() >= 3
1111 && part
1112 .chars()
1113 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
1114 {
1115 return part.to_string();
1116 }
1117 }
1118 path.split('/').next_back().unwrap_or(path).to_string()
1120}
1121
1122pub fn validate_unt_segment_count(segments: &[OwnedSegment]) -> Option<ValidationIssue> {
1134 let unh_count = segments.iter().filter(|s| s.id == "UNH").count();
1136 if unh_count > 1 {
1137 return Some(ValidationIssue::new(
1138 Severity::Error,
1139 ValidationCategory::Structure,
1140 ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH,
1141 format!(
1142 "UNT validation requires per-message segments, found {unh_count} UNH segments"
1143 ),
1144 ));
1145 }
1146
1147 let unt = segments.iter().rfind(|s| s.id == "UNT")?;
1149 let declared: usize = unt.get_element(0).parse().ok()?;
1150
1151 let actual = segments
1153 .iter()
1154 .filter(|s| s.id != "UNA" && s.id != "UNB" && s.id != "UNZ")
1155 .count();
1156
1157 if declared != actual {
1158 Some(
1159 ValidationIssue::new(
1160 Severity::Error,
1161 ValidationCategory::Structure,
1162 ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH,
1163 format!(
1164 "UNT segment count mismatch: declared {declared}, actual {actual}"
1165 ),
1166 )
1167 .with_field_path("UNT/0074")
1168 .with_expected(actual.to_string())
1169 .with_actual(declared.to_string()),
1170 )
1171 } else {
1172 None
1173 }
1174}
1175
1176#[cfg(test)]
1177mod tests {
1178 use super::*;
1179 use crate::eval::{ConditionResult as CR, NoOpExternalProvider};
1180 use std::collections::HashMap;
1181
1182 struct MockEvaluator {
1184 results: HashMap<u32, CR>,
1185 }
1186
1187 impl MockEvaluator {
1188 fn new(results: Vec<(u32, CR)>) -> Self {
1189 Self {
1190 results: results.into_iter().collect(),
1191 }
1192 }
1193
1194 fn all_true(ids: &[u32]) -> Self {
1195 Self::new(ids.iter().map(|&id| (id, CR::True)).collect())
1196 }
1197 }
1198
1199 impl ConditionEvaluator for MockEvaluator {
1200 fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> CR {
1201 self.results.get(&condition).copied().unwrap_or(CR::Unknown)
1202 }
1203 fn is_external(&self, _condition: u32) -> bool {
1204 false
1205 }
1206 fn message_type(&self) -> &str {
1207 "UTILMD"
1208 }
1209 fn format_version(&self) -> &str {
1210 "FV2510"
1211 }
1212 }
1213
1214 #[test]
1217 fn test_is_mandatory_status() {
1218 assert!(is_mandatory_status("Muss"));
1219 assert!(is_mandatory_status("Muss [182] ∧ [152]"));
1220 assert!(is_mandatory_status("X"));
1221 assert!(is_mandatory_status("X [567]"));
1222 assert!(!is_mandatory_status("Soll [1]"));
1223 assert!(!is_mandatory_status("Kann [1]"));
1224 assert!(!is_mandatory_status(""));
1225 }
1226
1227 #[test]
1228 fn test_extract_segment_id_simple() {
1229 assert_eq!(extract_segment_id("NAD"), "NAD");
1230 }
1231
1232 #[test]
1233 fn test_extract_segment_id_with_sg_prefix() {
1234 assert_eq!(extract_segment_id("SG2/NAD/C082/3039"), "NAD");
1235 }
1236
1237 #[test]
1238 fn test_extract_segment_id_nested_sg() {
1239 assert_eq!(extract_segment_id("SG4/SG8/SEQ/C286/6350"), "SEQ");
1240 }
1241
1242 #[test]
1245 fn test_validate_missing_mandatory_field() {
1246 let evaluator = MockEvaluator::all_true(&[182, 152]);
1247 let validator = EdifactValidator::new(evaluator);
1248 let external = NoOpExternalProvider;
1249
1250 let workflow = AhbWorkflow {
1251 pruefidentifikator: "11001".to_string(),
1252 description: "Test".to_string(),
1253 communication_direction: None,
1254 fields: vec![AhbFieldRule {
1255 segment_path: "SG2/NAD/C082/3039".to_string(),
1256 name: "MP-ID des MSB".to_string(),
1257 ahb_status: "Muss [182] ∧ [152]".to_string(),
1258 codes: vec![],
1259 parent_group_ahb_status: None,
1260 ..Default::default()
1261 }],
1262 ub_definitions: HashMap::new(),
1263 };
1264
1265 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1267
1268 assert!(!report.is_valid());
1270 let errors: Vec<_> = report.errors().collect();
1271 assert_eq!(errors.len(), 1);
1272 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1273 assert!(errors[0].message.contains("MP-ID des MSB"));
1274 }
1275
1276 #[test]
1277 fn test_validate_condition_false_no_error() {
1278 let evaluator = MockEvaluator::new(vec![(182, CR::True), (152, CR::False)]);
1280 let validator = EdifactValidator::new(evaluator);
1281 let external = NoOpExternalProvider;
1282
1283 let workflow = AhbWorkflow {
1284 pruefidentifikator: "11001".to_string(),
1285 description: "Test".to_string(),
1286 communication_direction: None,
1287 fields: vec![AhbFieldRule {
1288 segment_path: "NAD".to_string(),
1289 name: "Partnerrolle".to_string(),
1290 ahb_status: "Muss [182] ∧ [152]".to_string(),
1291 codes: vec![],
1292 parent_group_ahb_status: None,
1293 ..Default::default()
1294 }],
1295 ub_definitions: HashMap::new(),
1296 };
1297
1298 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1299
1300 assert!(report.is_valid());
1302 }
1303
1304 #[test]
1305 fn test_validate_condition_unknown_adds_info() {
1306 let evaluator = MockEvaluator::new(vec![(182, CR::True)]);
1308 let validator = EdifactValidator::new(evaluator);
1310 let external = NoOpExternalProvider;
1311
1312 let workflow = AhbWorkflow {
1313 pruefidentifikator: "11001".to_string(),
1314 description: "Test".to_string(),
1315 communication_direction: None,
1316 fields: vec![AhbFieldRule {
1317 segment_path: "NAD".to_string(),
1318 name: "Partnerrolle".to_string(),
1319 ahb_status: "Muss [182] ∧ [152]".to_string(),
1320 codes: vec![],
1321 parent_group_ahb_status: None,
1322 ..Default::default()
1323 }],
1324 ub_definitions: HashMap::new(),
1325 };
1326
1327 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1328
1329 assert!(report.is_valid());
1331 let infos: Vec<_> = report.infos().collect();
1332 assert_eq!(infos.len(), 1);
1333 assert_eq!(infos[0].code, ErrorCodes::CONDITION_UNKNOWN);
1334 }
1335
1336 #[test]
1337 fn test_validate_structure_level_skips_conditions() {
1338 let evaluator = MockEvaluator::all_true(&[182, 152]);
1339 let validator = EdifactValidator::new(evaluator);
1340 let external = NoOpExternalProvider;
1341
1342 let workflow = AhbWorkflow {
1343 pruefidentifikator: "11001".to_string(),
1344 description: "Test".to_string(),
1345 communication_direction: None,
1346 fields: vec![AhbFieldRule {
1347 segment_path: "NAD".to_string(),
1348 name: "Partnerrolle".to_string(),
1349 ahb_status: "Muss [182] ∧ [152]".to_string(),
1350 codes: vec![],
1351 parent_group_ahb_status: None,
1352 ..Default::default()
1353 }],
1354 ub_definitions: HashMap::new(),
1355 };
1356
1357 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Structure);
1359
1360 assert!(report.is_valid());
1362 assert_eq!(report.by_category(ValidationCategory::Ahb).count(), 0);
1363 }
1364
1365 #[test]
1366 fn test_validate_empty_workflow_no_condition_errors() {
1367 let evaluator = MockEvaluator::all_true(&[]);
1368 let validator = EdifactValidator::new(evaluator);
1369 let external = NoOpExternalProvider;
1370
1371 let empty_workflow = AhbWorkflow {
1372 pruefidentifikator: String::new(),
1373 description: String::new(),
1374 communication_direction: None,
1375 fields: vec![],
1376 ub_definitions: HashMap::new(),
1377 };
1378
1379 let report = validator.validate(&[], &empty_workflow, &external, ValidationLevel::Full);
1380
1381 assert!(report.is_valid());
1382 }
1383
1384 #[test]
1385 fn test_validate_bare_muss_always_required() {
1386 let evaluator = MockEvaluator::new(vec![]);
1387 let validator = EdifactValidator::new(evaluator);
1388 let external = NoOpExternalProvider;
1389
1390 let workflow = AhbWorkflow {
1391 pruefidentifikator: "55001".to_string(),
1392 description: "Test".to_string(),
1393 communication_direction: Some("NB an LF".to_string()),
1394 fields: vec![AhbFieldRule {
1395 segment_path: "SG2/NAD/3035".to_string(),
1396 name: "Partnerrolle".to_string(),
1397 ahb_status: "Muss".to_string(), codes: vec![],
1399 parent_group_ahb_status: None,
1400 ..Default::default()
1401 }],
1402 ub_definitions: HashMap::new(),
1403 };
1404
1405 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1406
1407 assert!(!report.is_valid());
1409 assert_eq!(report.error_count(), 1);
1410 }
1411
1412 #[test]
1413 fn test_validate_x_status_is_mandatory() {
1414 let evaluator = MockEvaluator::new(vec![]);
1415 let validator = EdifactValidator::new(evaluator);
1416 let external = NoOpExternalProvider;
1417
1418 let workflow = AhbWorkflow {
1419 pruefidentifikator: "55001".to_string(),
1420 description: "Test".to_string(),
1421 communication_direction: None,
1422 fields: vec![AhbFieldRule {
1423 segment_path: "DTM".to_string(),
1424 name: "Datum".to_string(),
1425 ahb_status: "X".to_string(),
1426 codes: vec![],
1427 parent_group_ahb_status: None,
1428 ..Default::default()
1429 }],
1430 ub_definitions: HashMap::new(),
1431 };
1432
1433 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1434
1435 assert!(!report.is_valid());
1436 let errors: Vec<_> = report.errors().collect();
1437 assert_eq!(errors[0].code, ErrorCodes::MISSING_REQUIRED_FIELD);
1438 }
1439
1440 #[test]
1441 fn test_validate_soll_not_mandatory() {
1442 let evaluator = MockEvaluator::new(vec![]);
1443 let validator = EdifactValidator::new(evaluator);
1444 let external = NoOpExternalProvider;
1445
1446 let workflow = AhbWorkflow {
1447 pruefidentifikator: "55001".to_string(),
1448 description: "Test".to_string(),
1449 communication_direction: None,
1450 fields: vec![AhbFieldRule {
1451 segment_path: "DTM".to_string(),
1452 name: "Datum".to_string(),
1453 ahb_status: "Soll".to_string(),
1454 codes: vec![],
1455 parent_group_ahb_status: None,
1456 ..Default::default()
1457 }],
1458 ub_definitions: HashMap::new(),
1459 };
1460
1461 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
1462
1463 assert!(report.is_valid());
1465 }
1466
1467 #[test]
1468 fn test_report_includes_metadata() {
1469 let evaluator = MockEvaluator::new(vec![]);
1470 let validator = EdifactValidator::new(evaluator);
1471 let external = NoOpExternalProvider;
1472
1473 let workflow = AhbWorkflow {
1474 pruefidentifikator: "55001".to_string(),
1475 description: String::new(),
1476 communication_direction: None,
1477 fields: vec![],
1478 ub_definitions: HashMap::new(),
1479 };
1480
1481 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Full);
1482
1483 assert_eq!(report.format_version.as_deref(), Some("FV2510"));
1484 assert_eq!(report.level, ValidationLevel::Full);
1485 assert_eq!(report.message_type, "UTILMD");
1486 assert_eq!(report.pruefidentifikator.as_deref(), Some("55001"));
1487 }
1488
1489 #[test]
1490 fn test_validate_with_navigator_returns_report() {
1491 let evaluator = MockEvaluator::all_true(&[]);
1492 let validator = EdifactValidator::new(evaluator);
1493 let external = NoOpExternalProvider;
1494 let nav = crate::eval::NoOpGroupNavigator;
1495
1496 let workflow = AhbWorkflow {
1497 pruefidentifikator: "55001".to_string(),
1498 description: "Test".to_string(),
1499 communication_direction: None,
1500 fields: vec![],
1501 ub_definitions: HashMap::new(),
1502 };
1503
1504 let report = validator.validate_with_navigator(
1505 &[],
1506 &workflow,
1507 &external,
1508 ValidationLevel::Full,
1509 &nav,
1510 );
1511 assert!(report.is_valid());
1512 }
1513
1514 #[test]
1515 fn test_code_validation_skips_composite_paths() {
1516 let evaluator = MockEvaluator::new(vec![]);
1520 let validator = EdifactValidator::new(evaluator);
1521 let external = NoOpExternalProvider;
1522
1523 let unh_segment = OwnedSegment {
1524 id: "UNH".to_string(),
1525 elements: vec![
1526 vec!["ALEXANDE951842".to_string()], vec![
1528 "UTILMD".to_string(),
1529 "D".to_string(),
1530 "11A".to_string(),
1531 "UN".to_string(),
1532 "S2.1".to_string(),
1533 ],
1534 ],
1535 segment_number: 1,
1536 };
1537
1538 let workflow = AhbWorkflow {
1539 pruefidentifikator: "55001".to_string(),
1540 description: "Test".to_string(),
1541 communication_direction: None,
1542 fields: vec![
1543 AhbFieldRule {
1544 segment_path: "UNH/S009/0065".to_string(),
1545 name: "Nachrichtentyp".to_string(),
1546 ahb_status: "X".to_string(),
1547 codes: vec![AhbCodeRule {
1548 value: "UTILMD".to_string(),
1549 description: "Stammdaten".to_string(),
1550 ahb_status: "X".to_string(),
1551 }],
1552 parent_group_ahb_status: None,
1553 ..Default::default()
1554 },
1555 AhbFieldRule {
1556 segment_path: "UNH/S009/0052".to_string(),
1557 name: "Version".to_string(),
1558 ahb_status: "X".to_string(),
1559 codes: vec![AhbCodeRule {
1560 value: "D".to_string(),
1561 description: "Draft".to_string(),
1562 ahb_status: "X".to_string(),
1563 }],
1564 parent_group_ahb_status: None,
1565 ..Default::default()
1566 },
1567 ],
1568 ub_definitions: HashMap::new(),
1569 };
1570
1571 let report = validator.validate(
1572 &[unh_segment],
1573 &workflow,
1574 &external,
1575 ValidationLevel::Conditions,
1576 );
1577
1578 let code_errors: Vec<_> = report
1580 .by_category(ValidationCategory::Code)
1581 .filter(|i| i.severity == Severity::Error)
1582 .collect();
1583 assert!(
1584 code_errors.is_empty(),
1585 "Expected no code errors for composite paths, got: {:?}",
1586 code_errors
1587 );
1588 }
1589
1590 #[test]
1591 fn test_cross_field_code_validation_valid_qualifiers() {
1592 let evaluator = MockEvaluator::new(vec![]);
1595 let validator = EdifactValidator::new(evaluator);
1596 let external = NoOpExternalProvider;
1597
1598 let nad_ms = OwnedSegment {
1599 id: "NAD".to_string(),
1600 elements: vec![vec!["MS".to_string()]],
1601 segment_number: 4,
1602 };
1603 let nad_mr = OwnedSegment {
1604 id: "NAD".to_string(),
1605 elements: vec![vec!["MR".to_string()]],
1606 segment_number: 5,
1607 };
1608
1609 let workflow = AhbWorkflow {
1610 pruefidentifikator: "55001".to_string(),
1611 description: "Test".to_string(),
1612 communication_direction: None,
1613 fields: vec![
1614 AhbFieldRule {
1615 segment_path: "SG2/NAD/3035".to_string(),
1616 name: "Absender".to_string(),
1617 ahb_status: "X".to_string(),
1618 codes: vec![AhbCodeRule {
1619 value: "MS".to_string(),
1620 description: "Absender".to_string(),
1621 ahb_status: "X".to_string(),
1622 }],
1623 parent_group_ahb_status: None,
1624 ..Default::default()
1625 },
1626 AhbFieldRule {
1627 segment_path: "SG2/NAD/3035".to_string(),
1628 name: "Empfaenger".to_string(),
1629 ahb_status: "X".to_string(),
1630 codes: vec![AhbCodeRule {
1631 value: "MR".to_string(),
1632 description: "Empfaenger".to_string(),
1633 ahb_status: "X".to_string(),
1634 }],
1635 parent_group_ahb_status: None,
1636 ..Default::default()
1637 },
1638 ],
1639 ub_definitions: HashMap::new(),
1640 };
1641
1642 let report = validator.validate(
1643 &[nad_ms, nad_mr],
1644 &workflow,
1645 &external,
1646 ValidationLevel::Conditions,
1647 );
1648
1649 let code_errors: Vec<_> = report
1650 .by_category(ValidationCategory::Code)
1651 .filter(|i| i.severity == Severity::Error)
1652 .collect();
1653 assert!(
1654 code_errors.is_empty(),
1655 "Expected no code errors for valid qualifiers, got: {:?}",
1656 code_errors
1657 );
1658 }
1659
1660 #[test]
1661 fn test_cross_field_code_validation_catches_invalid_qualifier() {
1662 let evaluator = MockEvaluator::new(vec![]);
1664 let validator = EdifactValidator::new(evaluator);
1665 let external = NoOpExternalProvider;
1666
1667 let nad_ms = OwnedSegment {
1668 id: "NAD".to_string(),
1669 elements: vec![vec!["MS".to_string()]],
1670 segment_number: 4,
1671 };
1672 let nad_mt = OwnedSegment {
1673 id: "NAD".to_string(),
1674 elements: vec![vec!["MT".to_string()]], segment_number: 5,
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/NAD/3035".to_string(),
1685 name: "Absender".to_string(),
1686 ahb_status: "X".to_string(),
1687 codes: vec![AhbCodeRule {
1688 value: "MS".to_string(),
1689 description: "Absender".to_string(),
1690 ahb_status: "X".to_string(),
1691 }],
1692 parent_group_ahb_status: None,
1693 ..Default::default()
1694 },
1695 AhbFieldRule {
1696 segment_path: "SG2/NAD/3035".to_string(),
1697 name: "Empfaenger".to_string(),
1698 ahb_status: "X".to_string(),
1699 codes: vec![AhbCodeRule {
1700 value: "MR".to_string(),
1701 description: "Empfaenger".to_string(),
1702 ahb_status: "X".to_string(),
1703 }],
1704 parent_group_ahb_status: None,
1705 ..Default::default()
1706 },
1707 ],
1708 ub_definitions: HashMap::new(),
1709 };
1710
1711 let report = validator.validate(
1712 &[nad_ms, nad_mt],
1713 &workflow,
1714 &external,
1715 ValidationLevel::Conditions,
1716 );
1717
1718 let code_errors: Vec<_> = report
1719 .by_category(ValidationCategory::Code)
1720 .filter(|i| i.severity == Severity::Error)
1721 .collect();
1722 assert_eq!(code_errors.len(), 1, "Expected one COD002 error for MT");
1723 assert!(code_errors[0].message.contains("MT"));
1724 assert!(code_errors[0].message.contains("MR"));
1725 assert!(code_errors[0].message.contains("MS"));
1726 }
1727
1728 #[test]
1729 fn test_cross_field_code_validation_unions_across_groups() {
1730 let evaluator = MockEvaluator::new(vec![]);
1734 let validator = EdifactValidator::new(evaluator);
1735 let external = NoOpExternalProvider;
1736
1737 let segments = vec![
1738 OwnedSegment {
1739 id: "NAD".to_string(),
1740 elements: vec![vec!["MS".to_string()]],
1741 segment_number: 3,
1742 },
1743 OwnedSegment {
1744 id: "NAD".to_string(),
1745 elements: vec![vec!["MR".to_string()]],
1746 segment_number: 4,
1747 },
1748 OwnedSegment {
1749 id: "NAD".to_string(),
1750 elements: vec![vec!["Z04".to_string()]],
1751 segment_number: 20,
1752 },
1753 OwnedSegment {
1754 id: "NAD".to_string(),
1755 elements: vec![vec!["Z09".to_string()]],
1756 segment_number: 21,
1757 },
1758 OwnedSegment {
1759 id: "NAD".to_string(),
1760 elements: vec![vec!["MT".to_string()]], segment_number: 22,
1762 },
1763 ];
1764
1765 let workflow = AhbWorkflow {
1766 pruefidentifikator: "55001".to_string(),
1767 description: "Test".to_string(),
1768 communication_direction: None,
1769 fields: vec![
1770 AhbFieldRule {
1771 segment_path: "SG2/NAD/3035".to_string(),
1772 name: "Absender".to_string(),
1773 ahb_status: "X".to_string(),
1774 codes: vec![AhbCodeRule {
1775 value: "MS".to_string(),
1776 description: "Absender".to_string(),
1777 ahb_status: "X".to_string(),
1778 }],
1779 parent_group_ahb_status: None,
1780 ..Default::default()
1781 },
1782 AhbFieldRule {
1783 segment_path: "SG2/NAD/3035".to_string(),
1784 name: "Empfaenger".to_string(),
1785 ahb_status: "X".to_string(),
1786 codes: vec![AhbCodeRule {
1787 value: "MR".to_string(),
1788 description: "Empfaenger".to_string(),
1789 ahb_status: "X".to_string(),
1790 }],
1791 parent_group_ahb_status: None,
1792 ..Default::default()
1793 },
1794 AhbFieldRule {
1795 segment_path: "SG4/SG12/NAD/3035".to_string(),
1796 name: "Anschlussnutzer".to_string(),
1797 ahb_status: "X".to_string(),
1798 codes: vec![AhbCodeRule {
1799 value: "Z04".to_string(),
1800 description: "Anschlussnutzer".to_string(),
1801 ahb_status: "X".to_string(),
1802 }],
1803 parent_group_ahb_status: None,
1804 ..Default::default()
1805 },
1806 AhbFieldRule {
1807 segment_path: "SG4/SG12/NAD/3035".to_string(),
1808 name: "Korrespondenzanschrift".to_string(),
1809 ahb_status: "X".to_string(),
1810 codes: vec![AhbCodeRule {
1811 value: "Z09".to_string(),
1812 description: "Korrespondenzanschrift".to_string(),
1813 ahb_status: "X".to_string(),
1814 }],
1815 parent_group_ahb_status: None,
1816 ..Default::default()
1817 },
1818 ],
1819 ub_definitions: HashMap::new(),
1820 };
1821
1822 let report =
1823 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
1824
1825 let code_errors: Vec<_> = report
1826 .by_category(ValidationCategory::Code)
1827 .filter(|i| i.severity == Severity::Error)
1828 .collect();
1829 assert_eq!(
1830 code_errors.len(),
1831 1,
1832 "Expected exactly one COD002 error for MT, got: {:?}",
1833 code_errors
1834 );
1835 assert!(code_errors[0].message.contains("MT"));
1836 }
1837
1838 #[test]
1839 fn test_is_qualifier_field_simple_paths() {
1840 assert!(is_qualifier_field("NAD/3035"));
1841 assert!(is_qualifier_field("SG2/NAD/3035"));
1842 assert!(is_qualifier_field("SG4/SG8/SEQ/6350"));
1843 assert!(is_qualifier_field("LOC/3227"));
1844 }
1845
1846 #[test]
1847 fn test_is_qualifier_field_composite_paths() {
1848 assert!(!is_qualifier_field("UNH/S009/0065"));
1849 assert!(!is_qualifier_field("NAD/C082/3039"));
1850 assert!(!is_qualifier_field("SG2/NAD/C082/3039"));
1851 }
1852
1853 #[test]
1854 fn test_is_qualifier_field_bare_segment() {
1855 assert!(!is_qualifier_field("NAD"));
1856 assert!(!is_qualifier_field("SG2/NAD"));
1857 }
1858
1859 #[test]
1860 fn test_missing_qualifier_instance_is_detected() {
1861 let evaluator = MockEvaluator::new(vec![]);
1864 let validator = EdifactValidator::new(evaluator);
1865 let external = NoOpExternalProvider;
1866
1867 let nad_ms = OwnedSegment {
1868 id: "NAD".to_string(),
1869 elements: vec![vec!["MS".to_string()]],
1870 segment_number: 3,
1871 };
1872
1873 let workflow = AhbWorkflow {
1874 pruefidentifikator: "55001".to_string(),
1875 description: "Test".to_string(),
1876 communication_direction: None,
1877 fields: vec![
1878 AhbFieldRule {
1879 segment_path: "SG2/NAD/3035".to_string(),
1880 name: "Absender".to_string(),
1881 ahb_status: "X".to_string(),
1882 codes: vec![AhbCodeRule {
1883 value: "MS".to_string(),
1884 description: "Absender".to_string(),
1885 ahb_status: "X".to_string(),
1886 }],
1887 parent_group_ahb_status: None,
1888 ..Default::default()
1889 },
1890 AhbFieldRule {
1891 segment_path: "SG2/NAD/3035".to_string(),
1892 name: "Empfaenger".to_string(),
1893 ahb_status: "Muss".to_string(),
1894 codes: vec![AhbCodeRule {
1895 value: "MR".to_string(),
1896 description: "Empfaenger".to_string(),
1897 ahb_status: "X".to_string(),
1898 }],
1899 parent_group_ahb_status: None,
1900 ..Default::default()
1901 },
1902 ],
1903 ub_definitions: HashMap::new(),
1904 };
1905
1906 let report =
1907 validator.validate(&[nad_ms], &workflow, &external, ValidationLevel::Conditions);
1908
1909 let ahb_errors: Vec<_> = report
1910 .by_category(ValidationCategory::Ahb)
1911 .filter(|i| i.severity == Severity::Error)
1912 .collect();
1913 assert_eq!(
1914 ahb_errors.len(),
1915 1,
1916 "Expected AHB001 for missing NAD+MR, got: {:?}",
1917 ahb_errors
1918 );
1919 assert!(ahb_errors[0].message.contains("Empfaenger"));
1920 }
1921
1922 #[test]
1923 fn test_present_qualifier_instance_no_error() {
1924 let evaluator = MockEvaluator::new(vec![]);
1926 let validator = EdifactValidator::new(evaluator);
1927 let external = NoOpExternalProvider;
1928
1929 let segments = vec![
1930 OwnedSegment {
1931 id: "NAD".to_string(),
1932 elements: vec![vec!["MS".to_string()]],
1933 segment_number: 3,
1934 },
1935 OwnedSegment {
1936 id: "NAD".to_string(),
1937 elements: vec![vec!["MR".to_string()]],
1938 segment_number: 4,
1939 },
1940 ];
1941
1942 let workflow = AhbWorkflow {
1943 pruefidentifikator: "55001".to_string(),
1944 description: "Test".to_string(),
1945 communication_direction: None,
1946 fields: vec![
1947 AhbFieldRule {
1948 segment_path: "SG2/NAD/3035".to_string(),
1949 name: "Absender".to_string(),
1950 ahb_status: "Muss".to_string(),
1951 codes: vec![AhbCodeRule {
1952 value: "MS".to_string(),
1953 description: "Absender".to_string(),
1954 ahb_status: "X".to_string(),
1955 }],
1956 parent_group_ahb_status: None,
1957 ..Default::default()
1958 },
1959 AhbFieldRule {
1960 segment_path: "SG2/NAD/3035".to_string(),
1961 name: "Empfaenger".to_string(),
1962 ahb_status: "Muss".to_string(),
1963 codes: vec![AhbCodeRule {
1964 value: "MR".to_string(),
1965 description: "Empfaenger".to_string(),
1966 ahb_status: "X".to_string(),
1967 }],
1968 parent_group_ahb_status: None,
1969 ..Default::default()
1970 },
1971 ],
1972 ub_definitions: HashMap::new(),
1973 };
1974
1975 let report =
1976 validator.validate(&segments, &workflow, &external, ValidationLevel::Conditions);
1977
1978 let ahb_errors: Vec<_> = report
1979 .by_category(ValidationCategory::Ahb)
1980 .filter(|i| i.severity == Severity::Error)
1981 .collect();
1982 assert!(
1983 ahb_errors.is_empty(),
1984 "Expected no AHB001 errors, got: {:?}",
1985 ahb_errors
1986 );
1987 }
1988
1989 #[test]
1990 fn test_extract_group_path_key() {
1991 assert_eq!(extract_group_path_key("SG2/NAD/3035"), "SG2");
1992 assert_eq!(extract_group_path_key("SG4/SG12/NAD/3035"), "SG4/SG12");
1993 assert_eq!(extract_group_path_key("NAD/3035"), "");
1994 assert_eq!(extract_group_path_key("SG4/SG8/SEQ/6350"), "SG4/SG8");
1995 }
1996
1997 #[test]
1998 fn test_absent_optional_group_no_missing_field_error() {
1999 use mig_types::navigator::GroupNavigator;
2002
2003 struct NavWithoutSG3;
2004 impl GroupNavigator for NavWithoutSG3 {
2005 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2006 vec![]
2007 }
2008 fn find_segments_with_qualifier_in_group(
2009 &self,
2010 _: &str,
2011 _: usize,
2012 _: &str,
2013 _: &[&str],
2014 _: usize,
2015 ) -> Vec<OwnedSegment> {
2016 vec![]
2017 }
2018 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2019 match group_path {
2020 ["SG2"] => 2, ["SG2", "SG3"] => 0, _ => 0,
2023 }
2024 }
2025 }
2026
2027 let evaluator = MockEvaluator::new(vec![]);
2028 let validator = EdifactValidator::new(evaluator);
2029 let external = NoOpExternalProvider;
2030 let nav = NavWithoutSG3;
2031
2032 let segments = vec![
2034 OwnedSegment {
2035 id: "NAD".into(),
2036 elements: vec![vec!["MS".into()]],
2037 segment_number: 3,
2038 },
2039 OwnedSegment {
2040 id: "NAD".into(),
2041 elements: vec![vec!["MR".into()]],
2042 segment_number: 4,
2043 },
2044 ];
2045
2046 let workflow = AhbWorkflow {
2047 pruefidentifikator: "55001".to_string(),
2048 description: "Test".to_string(),
2049 communication_direction: None,
2050 fields: vec![
2051 AhbFieldRule {
2052 segment_path: "SG2/SG3/CTA/3139".to_string(),
2053 name: "Funktion des Ansprechpartners, Code".to_string(),
2054 ahb_status: "Muss".to_string(),
2055 codes: vec![],
2056 parent_group_ahb_status: None,
2057 ..Default::default()
2058 },
2059 AhbFieldRule {
2060 segment_path: "SG2/SG3/CTA/C056/3412".to_string(),
2061 name: "Name vom Ansprechpartner".to_string(),
2062 ahb_status: "X".to_string(),
2063 codes: vec![],
2064 parent_group_ahb_status: None,
2065 ..Default::default()
2066 },
2067 ],
2068 ub_definitions: HashMap::new(),
2069 };
2070
2071 let report = validator.validate_with_navigator(
2072 &segments,
2073 &workflow,
2074 &external,
2075 ValidationLevel::Conditions,
2076 &nav,
2077 );
2078
2079 let ahb_errors: Vec<_> = report
2080 .by_category(ValidationCategory::Ahb)
2081 .filter(|i| i.severity == Severity::Error)
2082 .collect();
2083 assert!(
2084 ahb_errors.is_empty(),
2085 "Expected no AHB001 errors when SG3 is absent, got: {:?}",
2086 ahb_errors
2087 );
2088 }
2089
2090 #[test]
2091 fn test_present_group_still_checks_mandatory_fields() {
2092 use mig_types::navigator::GroupNavigator;
2094
2095 struct NavWithSG3;
2096 impl GroupNavigator for NavWithSG3 {
2097 fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
2098 vec![]
2099 }
2100 fn find_segments_with_qualifier_in_group(
2101 &self,
2102 _: &str,
2103 _: usize,
2104 _: &str,
2105 _: &[&str],
2106 _: usize,
2107 ) -> Vec<OwnedSegment> {
2108 vec![]
2109 }
2110 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2111 match group_path {
2112 ["SG2"] => 1,
2113 ["SG2", "SG3"] => 1, _ => 0,
2115 }
2116 }
2117 }
2118
2119 let evaluator = MockEvaluator::new(vec![]);
2120 let validator = EdifactValidator::new(evaluator);
2121 let external = NoOpExternalProvider;
2122 let nav = NavWithSG3;
2123
2124 let segments = vec![OwnedSegment {
2126 id: "NAD".into(),
2127 elements: vec![vec!["MS".into()]],
2128 segment_number: 3,
2129 }];
2130
2131 let workflow = AhbWorkflow {
2132 pruefidentifikator: "55001".to_string(),
2133 description: "Test".to_string(),
2134 communication_direction: None,
2135 fields: vec![AhbFieldRule {
2136 segment_path: "SG2/SG3/CTA/3139".to_string(),
2137 name: "Funktion des Ansprechpartners, Code".to_string(),
2138 ahb_status: "Muss".to_string(),
2139 codes: vec![],
2140 parent_group_ahb_status: None,
2141 ..Default::default()
2142 }],
2143 ub_definitions: HashMap::new(),
2144 };
2145
2146 let report = validator.validate_with_navigator(
2147 &segments,
2148 &workflow,
2149 &external,
2150 ValidationLevel::Conditions,
2151 &nav,
2152 );
2153
2154 let ahb_errors: Vec<_> = report
2155 .by_category(ValidationCategory::Ahb)
2156 .filter(|i| i.severity == Severity::Error)
2157 .collect();
2158 assert_eq!(
2159 ahb_errors.len(),
2160 1,
2161 "Expected AHB001 error when SG3 is present but CTA missing"
2162 );
2163 assert!(ahb_errors[0].message.contains("CTA"));
2164 }
2165
2166 #[test]
2167 fn test_missing_qualifier_with_navigator_is_detected() {
2168 use mig_types::navigator::GroupNavigator;
2171
2172 struct NavWithSG2;
2173 impl GroupNavigator for NavWithSG2 {
2174 fn find_segments_in_group(
2175 &self,
2176 segment_id: &str,
2177 group_path: &[&str],
2178 instance_index: usize,
2179 ) -> Vec<OwnedSegment> {
2180 if segment_id == "NAD" && group_path == ["SG2"] && instance_index == 0 {
2181 vec![OwnedSegment {
2182 id: "NAD".into(),
2183 elements: vec![vec!["MS".into()]],
2184 segment_number: 3,
2185 }]
2186 } else {
2187 vec![]
2188 }
2189 }
2190 fn find_segments_with_qualifier_in_group(
2191 &self,
2192 _: &str,
2193 _: usize,
2194 _: &str,
2195 _: &[&str],
2196 _: usize,
2197 ) -> Vec<OwnedSegment> {
2198 vec![]
2199 }
2200 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2201 match group_path {
2202 ["SG2"] => 1,
2203 _ => 0,
2204 }
2205 }
2206 }
2207
2208 let evaluator = MockEvaluator::new(vec![]);
2209 let validator = EdifactValidator::new(evaluator);
2210 let external = NoOpExternalProvider;
2211 let nav = NavWithSG2;
2212
2213 let segments = vec![OwnedSegment {
2214 id: "NAD".into(),
2215 elements: vec![vec!["MS".into()]],
2216 segment_number: 3,
2217 }];
2218
2219 let workflow = AhbWorkflow {
2220 pruefidentifikator: "55001".to_string(),
2221 description: "Test".to_string(),
2222 communication_direction: None,
2223 fields: vec![
2224 AhbFieldRule {
2225 segment_path: "SG2/NAD/3035".to_string(),
2226 name: "Absender".to_string(),
2227 ahb_status: "X".to_string(),
2228 codes: vec![AhbCodeRule {
2229 value: "MS".to_string(),
2230 description: "Absender".to_string(),
2231 ahb_status: "X".to_string(),
2232 }],
2233 parent_group_ahb_status: None,
2234 ..Default::default()
2235 },
2236 AhbFieldRule {
2237 segment_path: "SG2/NAD/3035".to_string(),
2238 name: "Empfaenger".to_string(),
2239 ahb_status: "Muss".to_string(),
2240 codes: vec![AhbCodeRule {
2241 value: "MR".to_string(),
2242 description: "Empfaenger".to_string(),
2243 ahb_status: "X".to_string(),
2244 }],
2245 parent_group_ahb_status: None,
2246 ..Default::default()
2247 },
2248 ],
2249 ub_definitions: HashMap::new(),
2250 };
2251
2252 let report = validator.validate_with_navigator(
2253 &segments,
2254 &workflow,
2255 &external,
2256 ValidationLevel::Conditions,
2257 &nav,
2258 );
2259
2260 let ahb_errors: Vec<_> = report
2261 .by_category(ValidationCategory::Ahb)
2262 .filter(|i| i.severity == Severity::Error)
2263 .collect();
2264 assert_eq!(
2265 ahb_errors.len(),
2266 1,
2267 "Expected AHB001 for missing NAD+MR even with navigator, got: {:?}",
2268 ahb_errors
2269 );
2270 assert!(ahb_errors[0].message.contains("Empfaenger"));
2271 }
2272
2273 #[test]
2274 fn test_optional_group_variant_absent_no_error() {
2275 use mig_types::navigator::GroupNavigator;
2280
2281 struct TestNav;
2282 impl GroupNavigator for TestNav {
2283 fn find_segments_in_group(
2284 &self,
2285 segment_id: &str,
2286 group_path: &[&str],
2287 instance_index: usize,
2288 ) -> Vec<OwnedSegment> {
2289 match (segment_id, group_path, instance_index) {
2290 ("LOC", ["SG4", "SG5"], 0) => vec![OwnedSegment {
2291 id: "LOC".into(),
2292 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2293 segment_number: 10,
2294 }],
2295 ("NAD", ["SG2"], 0) => vec![OwnedSegment {
2296 id: "NAD".into(),
2297 elements: vec![vec!["MS".into()]],
2298 segment_number: 3,
2299 }],
2300 _ => vec![],
2301 }
2302 }
2303 fn find_segments_with_qualifier_in_group(
2304 &self,
2305 _: &str,
2306 _: usize,
2307 _: &str,
2308 _: &[&str],
2309 _: usize,
2310 ) -> Vec<OwnedSegment> {
2311 vec![]
2312 }
2313 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2314 match group_path {
2315 ["SG2"] => 1,
2316 ["SG4"] => 1,
2317 ["SG4", "SG5"] => 1, _ => 0,
2319 }
2320 }
2321 }
2322
2323 let evaluator = MockEvaluator::new(vec![]);
2324 let validator = EdifactValidator::new(evaluator);
2325 let external = NoOpExternalProvider;
2326 let nav = TestNav;
2327
2328 let segments = vec![
2329 OwnedSegment {
2330 id: "NAD".into(),
2331 elements: vec![vec!["MS".into()]],
2332 segment_number: 3,
2333 },
2334 OwnedSegment {
2335 id: "LOC".into(),
2336 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2337 segment_number: 10,
2338 },
2339 ];
2340
2341 let workflow = AhbWorkflow {
2342 pruefidentifikator: "55001".to_string(),
2343 description: "Test".to_string(),
2344 communication_direction: None,
2345 fields: vec![
2346 AhbFieldRule {
2348 segment_path: "SG2/NAD/3035".to_string(),
2349 name: "Absender".to_string(),
2350 ahb_status: "X".to_string(),
2351 codes: vec![AhbCodeRule {
2352 value: "MS".to_string(),
2353 description: "Absender".to_string(),
2354 ahb_status: "X".to_string(),
2355 }],
2356 parent_group_ahb_status: Some("Muss".to_string()),
2357 ..Default::default()
2358 },
2359 AhbFieldRule {
2360 segment_path: "SG2/NAD/3035".to_string(),
2361 name: "Empfaenger".to_string(),
2362 ahb_status: "Muss".to_string(),
2363 codes: vec![AhbCodeRule {
2364 value: "MR".to_string(),
2365 description: "Empfaenger".to_string(),
2366 ahb_status: "X".to_string(),
2367 }],
2368 parent_group_ahb_status: Some("Muss".to_string()),
2369 ..Default::default()
2370 },
2371 AhbFieldRule {
2373 segment_path: "SG4/SG5/LOC/3227".to_string(),
2374 name: "Ortsangabe, Qualifier (Z16)".to_string(),
2375 ahb_status: "X".to_string(),
2376 codes: vec![AhbCodeRule {
2377 value: "Z16".to_string(),
2378 description: "Marktlokation".to_string(),
2379 ahb_status: "X".to_string(),
2380 }],
2381 parent_group_ahb_status: Some("Kann".to_string()),
2382 ..Default::default()
2383 },
2384 AhbFieldRule {
2385 segment_path: "SG4/SG5/LOC/3227".to_string(),
2386 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2387 ahb_status: "Muss".to_string(),
2388 codes: vec![AhbCodeRule {
2389 value: "Z17".to_string(),
2390 description: "Messlokation".to_string(),
2391 ahb_status: "X".to_string(),
2392 }],
2393 parent_group_ahb_status: Some("Kann".to_string()),
2394 ..Default::default()
2395 },
2396 ],
2397 ub_definitions: HashMap::new(),
2398 };
2399
2400 let report = validator.validate_with_navigator(
2401 &segments,
2402 &workflow,
2403 &external,
2404 ValidationLevel::Conditions,
2405 &nav,
2406 );
2407
2408 let ahb_errors: Vec<_> = report
2409 .by_category(ValidationCategory::Ahb)
2410 .filter(|i| i.severity == Severity::Error)
2411 .collect();
2412
2413 assert_eq!(
2416 ahb_errors.len(),
2417 1,
2418 "Expected only AHB001 for missing NAD+MR, got: {:?}",
2419 ahb_errors
2420 );
2421 assert!(
2422 ahb_errors[0].message.contains("Empfaenger"),
2423 "Error should be for missing NAD+MR (Empfaenger)"
2424 );
2425 }
2426
2427 #[test]
2428 fn test_conditional_group_variant_absent_no_error() {
2429 use mig_types::navigator::GroupNavigator;
2434
2435 struct TestNav;
2436 impl GroupNavigator for TestNav {
2437 fn find_segments_in_group(
2438 &self,
2439 segment_id: &str,
2440 group_path: &[&str],
2441 instance_index: usize,
2442 ) -> Vec<OwnedSegment> {
2443 if segment_id == "LOC" && group_path == ["SG4", "SG5"] && instance_index == 0 {
2444 vec![OwnedSegment {
2445 id: "LOC".into(),
2446 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2447 segment_number: 10,
2448 }]
2449 } else {
2450 vec![]
2451 }
2452 }
2453 fn find_segments_with_qualifier_in_group(
2454 &self,
2455 _: &str,
2456 _: usize,
2457 _: &str,
2458 _: &[&str],
2459 _: usize,
2460 ) -> Vec<OwnedSegment> {
2461 vec![]
2462 }
2463 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2464 match group_path {
2465 ["SG4"] => 1,
2466 ["SG4", "SG5"] => 1, _ => 0,
2468 }
2469 }
2470 }
2471
2472 let evaluator = MockEvaluator::new(vec![(165, CR::False), (2061, CR::True)]);
2475 let validator = EdifactValidator::new(evaluator);
2476 let external = NoOpExternalProvider;
2477 let nav = TestNav;
2478
2479 let segments = vec![OwnedSegment {
2480 id: "LOC".into(),
2481 elements: vec![vec!["Z16".into()], vec!["DE00012345".into()]],
2482 segment_number: 10,
2483 }];
2484
2485 let workflow = AhbWorkflow {
2486 pruefidentifikator: "55001".to_string(),
2487 description: "Test".to_string(),
2488 communication_direction: None,
2489 fields: vec![
2490 AhbFieldRule {
2492 segment_path: "SG4/SG5/LOC/3227".to_string(),
2493 name: "Ortsangabe, Qualifier (Z16)".to_string(),
2494 ahb_status: "X".to_string(),
2495 codes: vec![AhbCodeRule {
2496 value: "Z16".to_string(),
2497 description: "Marktlokation".to_string(),
2498 ahb_status: "X".to_string(),
2499 }],
2500 parent_group_ahb_status: Some("Muss [2061]".to_string()),
2501 ..Default::default()
2502 },
2503 AhbFieldRule {
2505 segment_path: "SG4/SG5/LOC/3227".to_string(),
2506 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2507 ahb_status: "X".to_string(),
2508 codes: vec![AhbCodeRule {
2509 value: "Z17".to_string(),
2510 description: "Messlokation".to_string(),
2511 ahb_status: "X".to_string(),
2512 }],
2513 parent_group_ahb_status: Some("Soll [165]".to_string()),
2514 ..Default::default()
2515 },
2516 ],
2517 ub_definitions: HashMap::new(),
2518 };
2519
2520 let report = validator.validate_with_navigator(
2521 &segments,
2522 &workflow,
2523 &external,
2524 ValidationLevel::Conditions,
2525 &nav,
2526 );
2527
2528 let ahb_errors: Vec<_> = report
2529 .by_category(ValidationCategory::Ahb)
2530 .filter(|i| i.severity == Severity::Error)
2531 .collect();
2532
2533 assert!(
2535 ahb_errors.is_empty(),
2536 "Expected no errors when conditional group variant [165]=False, got: {:?}",
2537 ahb_errors
2538 );
2539 }
2540
2541 #[test]
2542 fn test_conditional_group_variant_unknown_no_error() {
2543 let evaluator = MockEvaluator::new(vec![]);
2549 let validator = EdifactValidator::new(evaluator);
2550 let external = NoOpExternalProvider;
2551
2552 let workflow = AhbWorkflow {
2553 pruefidentifikator: "55001".to_string(),
2554 description: "Test".to_string(),
2555 communication_direction: None,
2556 fields: vec![AhbFieldRule {
2557 segment_path: "SG4/SG5/LOC/3227".to_string(),
2558 name: "Ortsangabe, Qualifier (Z17)".to_string(),
2559 ahb_status: "X".to_string(),
2560 codes: vec![AhbCodeRule {
2561 value: "Z17".to_string(),
2562 description: "Messlokation".to_string(),
2563 ahb_status: "X".to_string(),
2564 }],
2565 parent_group_ahb_status: Some("Soll [165]".to_string()),
2566 ..Default::default()
2567 }],
2568 ub_definitions: HashMap::new(),
2569 };
2570
2571 let report = validator.validate(&[], &workflow, &external, ValidationLevel::Conditions);
2572
2573 let ahb_errors: Vec<_> = report
2574 .by_category(ValidationCategory::Ahb)
2575 .filter(|i| i.severity == Severity::Error)
2576 .collect();
2577
2578 assert!(
2580 ahb_errors.is_empty(),
2581 "Expected no errors when parent group condition is Unknown, got: {:?}",
2582 ahb_errors
2583 );
2584 }
2585
2586 #[test]
2587 fn test_segment_absent_within_present_group_no_error() {
2588 use mig_types::navigator::GroupNavigator;
2592
2593 struct TestNav;
2594 impl GroupNavigator for TestNav {
2595 fn find_segments_in_group(
2596 &self,
2597 segment_id: &str,
2598 group_path: &[&str],
2599 instance_index: usize,
2600 ) -> Vec<OwnedSegment> {
2601 if segment_id == "QTY"
2603 && group_path == ["SG5", "SG6", "SG9", "SG10"]
2604 && instance_index == 0
2605 {
2606 vec![OwnedSegment {
2607 id: "QTY".into(),
2608 elements: vec![vec!["220".into(), "0".into()]],
2609 segment_number: 14,
2610 }]
2611 } else {
2612 vec![]
2613 }
2614 }
2615 fn find_segments_with_qualifier_in_group(
2616 &self,
2617 _: &str,
2618 _: usize,
2619 _: &str,
2620 _: &[&str],
2621 _: usize,
2622 ) -> Vec<OwnedSegment> {
2623 vec![]
2624 }
2625 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2626 match group_path {
2627 ["SG5"] => 1,
2628 ["SG5", "SG6"] => 1,
2629 ["SG5", "SG6", "SG9"] => 1,
2630 ["SG5", "SG6", "SG9", "SG10"] => 1,
2631 _ => 0,
2632 }
2633 }
2634 fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
2635 group_path == ["SG5", "SG6", "SG9", "SG10"] && instance_index == 0
2637 }
2638 }
2639
2640 let evaluator = MockEvaluator::all_true(&[]);
2641 let validator = EdifactValidator::new(evaluator);
2642 let external = NoOpExternalProvider;
2643 let nav = TestNav;
2644
2645 let segments = vec![OwnedSegment {
2646 id: "QTY".into(),
2647 elements: vec![vec!["220".into(), "0".into()]],
2648 segment_number: 14,
2649 }];
2650
2651 let workflow = AhbWorkflow {
2652 pruefidentifikator: "13017".to_string(),
2653 description: "Test".to_string(),
2654 communication_direction: None,
2655 fields: vec![
2656 AhbFieldRule {
2658 segment_path: "SG5/SG6/SG9/SG10/STS/C601/9015".to_string(),
2659 name: "Statuskategorie, Code".to_string(),
2660 ahb_status: "X".to_string(),
2661 codes: vec![],
2662 parent_group_ahb_status: Some("Muss".to_string()),
2663 ..Default::default()
2664 },
2665 AhbFieldRule {
2667 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2668 name: "Statusanlaß, Code".to_string(),
2669 ahb_status: "X [5]".to_string(),
2670 codes: vec![],
2671 parent_group_ahb_status: Some("Muss".to_string()),
2672 ..Default::default()
2673 },
2674 ],
2675 ub_definitions: HashMap::new(),
2676 };
2677
2678 let report = validator.validate_with_navigator(
2679 &segments,
2680 &workflow,
2681 &external,
2682 ValidationLevel::Conditions,
2683 &nav,
2684 );
2685
2686 let ahb_errors: Vec<_> = report
2687 .by_category(ValidationCategory::Ahb)
2688 .filter(|i| i.severity == Severity::Error)
2689 .collect();
2690
2691 assert!(
2692 ahb_errors.is_empty(),
2693 "Expected no AHB001 errors when STS segment is absent from SG10, got: {:?}",
2694 ahb_errors
2695 );
2696 }
2697
2698 #[test]
2699 fn test_group_scoped_code_validation_with_navigator() {
2700 use mig_types::navigator::GroupNavigator;
2704
2705 struct TestNav;
2706 impl GroupNavigator for TestNav {
2707 fn find_segments_in_group(
2708 &self,
2709 segment_id: &str,
2710 group_path: &[&str],
2711 _instance_index: usize,
2712 ) -> Vec<OwnedSegment> {
2713 if segment_id != "NAD" {
2714 return vec![];
2715 }
2716 match group_path {
2717 ["SG2"] => vec![
2718 OwnedSegment {
2719 id: "NAD".into(),
2720 elements: vec![vec!["MS".into()]],
2721 segment_number: 3,
2722 },
2723 OwnedSegment {
2724 id: "NAD".into(),
2725 elements: vec![vec!["MT".into()]], segment_number: 4,
2727 },
2728 ],
2729 ["SG4", "SG12"] => vec![
2730 OwnedSegment {
2731 id: "NAD".into(),
2732 elements: vec![vec!["Z04".into()]],
2733 segment_number: 20,
2734 },
2735 OwnedSegment {
2736 id: "NAD".into(),
2737 elements: vec![vec!["Z09".into()]],
2738 segment_number: 21,
2739 },
2740 ],
2741 _ => vec![],
2742 }
2743 }
2744 fn find_segments_with_qualifier_in_group(
2745 &self,
2746 _: &str,
2747 _: usize,
2748 _: &str,
2749 _: &[&str],
2750 _: usize,
2751 ) -> Vec<OwnedSegment> {
2752 vec![]
2753 }
2754 fn group_instance_count(&self, group_path: &[&str]) -> usize {
2755 match group_path {
2756 ["SG2"] | ["SG4", "SG12"] => 1,
2757 _ => 0,
2758 }
2759 }
2760 }
2761
2762 let evaluator = MockEvaluator::new(vec![]);
2763 let validator = EdifactValidator::new(evaluator);
2764 let external = NoOpExternalProvider;
2765 let nav = TestNav;
2766
2767 let workflow = AhbWorkflow {
2768 pruefidentifikator: "55001".to_string(),
2769 description: "Test".to_string(),
2770 communication_direction: None,
2771 fields: vec![
2772 AhbFieldRule {
2773 segment_path: "SG2/NAD/3035".to_string(),
2774 name: "Absender".to_string(),
2775 ahb_status: "X".to_string(),
2776 codes: vec![AhbCodeRule {
2777 value: "MS".to_string(),
2778 description: "Absender".to_string(),
2779 ahb_status: "X".to_string(),
2780 }],
2781 parent_group_ahb_status: None,
2782 ..Default::default()
2783 },
2784 AhbFieldRule {
2785 segment_path: "SG2/NAD/3035".to_string(),
2786 name: "Empfaenger".to_string(),
2787 ahb_status: "X".to_string(),
2788 codes: vec![AhbCodeRule {
2789 value: "MR".to_string(),
2790 description: "Empfaenger".to_string(),
2791 ahb_status: "X".to_string(),
2792 }],
2793 parent_group_ahb_status: None,
2794 ..Default::default()
2795 },
2796 AhbFieldRule {
2797 segment_path: "SG4/SG12/NAD/3035".to_string(),
2798 name: "Anschlussnutzer".to_string(),
2799 ahb_status: "X".to_string(),
2800 codes: vec![AhbCodeRule {
2801 value: "Z04".to_string(),
2802 description: "Anschlussnutzer".to_string(),
2803 ahb_status: "X".to_string(),
2804 }],
2805 parent_group_ahb_status: None,
2806 ..Default::default()
2807 },
2808 AhbFieldRule {
2809 segment_path: "SG4/SG12/NAD/3035".to_string(),
2810 name: "Korrespondenzanschrift".to_string(),
2811 ahb_status: "X".to_string(),
2812 codes: vec![AhbCodeRule {
2813 value: "Z09".to_string(),
2814 description: "Korrespondenzanschrift".to_string(),
2815 ahb_status: "X".to_string(),
2816 }],
2817 parent_group_ahb_status: None,
2818 ..Default::default()
2819 },
2820 ],
2821 ub_definitions: HashMap::new(),
2822 };
2823
2824 let all_segments = vec![
2826 OwnedSegment {
2827 id: "NAD".into(),
2828 elements: vec![vec!["MS".into()]],
2829 segment_number: 3,
2830 },
2831 OwnedSegment {
2832 id: "NAD".into(),
2833 elements: vec![vec!["MT".into()]],
2834 segment_number: 4,
2835 },
2836 OwnedSegment {
2837 id: "NAD".into(),
2838 elements: vec![vec!["Z04".into()]],
2839 segment_number: 20,
2840 },
2841 OwnedSegment {
2842 id: "NAD".into(),
2843 elements: vec![vec!["Z09".into()]],
2844 segment_number: 21,
2845 },
2846 ];
2847
2848 let report = validator.validate_with_navigator(
2849 &all_segments,
2850 &workflow,
2851 &external,
2852 ValidationLevel::Conditions,
2853 &nav,
2854 );
2855
2856 let code_errors: Vec<_> = report
2857 .by_category(ValidationCategory::Code)
2858 .filter(|i| i.severity == Severity::Error)
2859 .collect();
2860
2861 assert_eq!(
2864 code_errors.len(),
2865 1,
2866 "Expected exactly one COD002 error for MT in SG2, got: {:?}",
2867 code_errors
2868 );
2869 assert!(code_errors[0].message.contains("MT"));
2870 assert!(code_errors[0].message.contains("MR"));
2872 assert!(code_errors[0].message.contains("MS"));
2873 assert!(
2874 !code_errors[0].message.contains("Z04"),
2875 "SG4/SG12 codes should not leak into SG2 error"
2876 );
2877 assert!(
2879 code_errors[0]
2880 .field_path
2881 .as_deref()
2882 .unwrap_or("")
2883 .contains("SG2"),
2884 "Error field_path should reference SG2, got: {:?}",
2885 code_errors[0].field_path
2886 );
2887 }
2888
2889 #[test]
2892 fn test_package_cardinality_within_bounds() {
2893 let evaluator = MockEvaluator::all_true(&[]);
2895 let validator = EdifactValidator::new(evaluator);
2896 let external = NoOpExternalProvider;
2897
2898 let segments = vec![OwnedSegment {
2899 id: "STS".into(),
2900 elements: vec![
2901 vec!["Z33".into()], vec![], vec!["E01".into()], ],
2905 segment_number: 5,
2906 }];
2907
2908 let workflow = AhbWorkflow {
2909 pruefidentifikator: "13017".to_string(),
2910 description: "Test".to_string(),
2911 communication_direction: None,
2912 ub_definitions: HashMap::new(),
2913 fields: vec![AhbFieldRule {
2914 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2915 name: "Statusanlaß, Code".to_string(),
2916 ahb_status: "X".to_string(),
2917 element_index: Some(2),
2918 component_index: Some(0),
2919 codes: vec![
2920 AhbCodeRule {
2921 value: "E01".into(),
2922 description: "Code 1".into(),
2923 ahb_status: "X [4P0..1]".into(),
2924 },
2925 AhbCodeRule {
2926 value: "E02".into(),
2927 description: "Code 2".into(),
2928 ahb_status: "X [4P0..1]".into(),
2929 },
2930 ],
2931 parent_group_ahb_status: Some("Muss".to_string()),
2932 mig_number: None,
2933 }],
2934 };
2935
2936 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2937 let pkg_errors: Vec<_> = report
2938 .by_category(ValidationCategory::Ahb)
2939 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2940 .collect();
2941 assert!(
2942 pkg_errors.is_empty(),
2943 "1 code within [4P0..1] bounds — no error expected, got: {:?}",
2944 pkg_errors
2945 );
2946 }
2947
2948 #[test]
2949 fn test_package_cardinality_zero_present_min_zero() {
2950 let evaluator = MockEvaluator::all_true(&[]);
2952 let validator = EdifactValidator::new(evaluator);
2953 let external = NoOpExternalProvider;
2954
2955 let segments = vec![OwnedSegment {
2956 id: "STS".into(),
2957 elements: vec![
2958 vec!["Z33".into()],
2959 vec![],
2960 vec!["X99".into()], ],
2962 segment_number: 5,
2963 }];
2964
2965 let workflow = AhbWorkflow {
2966 pruefidentifikator: "13017".to_string(),
2967 description: "Test".to_string(),
2968 communication_direction: None,
2969 ub_definitions: HashMap::new(),
2970 fields: vec![AhbFieldRule {
2971 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
2972 name: "Statusanlaß, Code".to_string(),
2973 ahb_status: "X".to_string(),
2974 element_index: Some(2),
2975 component_index: Some(0),
2976 codes: vec![
2977 AhbCodeRule {
2978 value: "E01".into(),
2979 description: "Code 1".into(),
2980 ahb_status: "X [4P0..1]".into(),
2981 },
2982 AhbCodeRule {
2983 value: "E02".into(),
2984 description: "Code 2".into(),
2985 ahb_status: "X [4P0..1]".into(),
2986 },
2987 ],
2988 parent_group_ahb_status: Some("Muss".to_string()),
2989 mig_number: None,
2990 }],
2991 };
2992
2993 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
2994 let pkg_errors: Vec<_> = report
2995 .by_category(ValidationCategory::Ahb)
2996 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
2997 .collect();
2998 assert!(
2999 pkg_errors.is_empty(),
3000 "0 codes, min=0 — no error expected, got: {:?}",
3001 pkg_errors
3002 );
3003 }
3004
3005 #[test]
3006 fn test_package_cardinality_too_many() {
3007 let evaluator = MockEvaluator::all_true(&[]);
3009 let validator = EdifactValidator::new(evaluator);
3010 let external = NoOpExternalProvider;
3011
3012 let segments = vec![
3014 OwnedSegment {
3015 id: "STS".into(),
3016 elements: vec![vec!["Z33".into()], vec![], vec!["E01".into()]],
3017 segment_number: 5,
3018 },
3019 OwnedSegment {
3020 id: "STS".into(),
3021 elements: vec![vec!["Z33".into()], vec![], vec!["E02".into()]],
3022 segment_number: 6,
3023 },
3024 ];
3025
3026 let workflow = AhbWorkflow {
3027 pruefidentifikator: "13017".to_string(),
3028 description: "Test".to_string(),
3029 communication_direction: None,
3030 ub_definitions: HashMap::new(),
3031 fields: vec![AhbFieldRule {
3032 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3033 name: "Statusanlaß, Code".to_string(),
3034 ahb_status: "X".to_string(),
3035 element_index: Some(2),
3036 component_index: Some(0),
3037 codes: vec![
3038 AhbCodeRule {
3039 value: "E01".into(),
3040 description: "Code 1".into(),
3041 ahb_status: "X [4P0..1]".into(),
3042 },
3043 AhbCodeRule {
3044 value: "E02".into(),
3045 description: "Code 2".into(),
3046 ahb_status: "X [4P0..1]".into(),
3047 },
3048 ],
3049 parent_group_ahb_status: Some("Muss".to_string()),
3050 mig_number: None,
3051 }],
3052 };
3053
3054 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3055 let pkg_errors: Vec<_> = report
3056 .by_category(ValidationCategory::Ahb)
3057 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3058 .collect();
3059 assert_eq!(
3060 pkg_errors.len(),
3061 1,
3062 "2 codes present, max=1 — expected 1 error, got: {:?}",
3063 pkg_errors
3064 );
3065 assert!(pkg_errors[0].message.contains("[4P0..1]"));
3066 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("2"));
3067 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("0..1"));
3068 }
3069
3070 #[test]
3071 fn test_package_cardinality_too_few() {
3072 let evaluator = MockEvaluator::all_true(&[]);
3074 let validator = EdifactValidator::new(evaluator);
3075 let external = NoOpExternalProvider;
3076
3077 let segments = vec![OwnedSegment {
3078 id: "STS".into(),
3079 elements: vec![
3080 vec!["Z33".into()],
3081 vec![],
3082 vec!["X99".into()], ],
3084 segment_number: 5,
3085 }];
3086
3087 let workflow = AhbWorkflow {
3088 pruefidentifikator: "13017".to_string(),
3089 description: "Test".to_string(),
3090 communication_direction: None,
3091 ub_definitions: HashMap::new(),
3092 fields: vec![AhbFieldRule {
3093 segment_path: "SG5/SG6/SG9/SG10/STS/C556/9013".to_string(),
3094 name: "Statusanlaß, Code".to_string(),
3095 ahb_status: "X".to_string(),
3096 element_index: Some(2),
3097 component_index: Some(0),
3098 codes: vec![
3099 AhbCodeRule {
3100 value: "E01".into(),
3101 description: "Code 1".into(),
3102 ahb_status: "X [5P1..3]".into(),
3103 },
3104 AhbCodeRule {
3105 value: "E02".into(),
3106 description: "Code 2".into(),
3107 ahb_status: "X [5P1..3]".into(),
3108 },
3109 AhbCodeRule {
3110 value: "E03".into(),
3111 description: "Code 3".into(),
3112 ahb_status: "X [5P1..3]".into(),
3113 },
3114 ],
3115 parent_group_ahb_status: Some("Muss".to_string()),
3116 mig_number: None,
3117 }],
3118 };
3119
3120 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3121 let pkg_errors: Vec<_> = report
3122 .by_category(ValidationCategory::Ahb)
3123 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3124 .collect();
3125 assert_eq!(
3126 pkg_errors.len(),
3127 1,
3128 "0 codes present, min=1 — expected 1 error, got: {:?}",
3129 pkg_errors
3130 );
3131 assert!(pkg_errors[0].message.contains("[5P1..3]"));
3132 assert_eq!(pkg_errors[0].actual_value.as_deref(), Some("0"));
3133 assert_eq!(pkg_errors[0].expected_value.as_deref(), Some("1..3"));
3134 }
3135
3136 #[test]
3137 fn test_package_cardinality_no_packages_in_workflow() {
3138 let evaluator = MockEvaluator::all_true(&[]);
3140 let validator = EdifactValidator::new(evaluator);
3141 let external = NoOpExternalProvider;
3142
3143 let segments = vec![OwnedSegment {
3144 id: "STS".into(),
3145 elements: vec![vec!["E01".into()]],
3146 segment_number: 5,
3147 }];
3148
3149 let workflow = AhbWorkflow {
3150 pruefidentifikator: "13017".to_string(),
3151 description: "Test".to_string(),
3152 communication_direction: None,
3153 ub_definitions: HashMap::new(),
3154 fields: vec![AhbFieldRule {
3155 segment_path: "STS/9015".to_string(),
3156 name: "Status Code".to_string(),
3157 ahb_status: "X".to_string(),
3158 codes: vec![AhbCodeRule {
3159 value: "E01".into(),
3160 description: "Code 1".into(),
3161 ahb_status: "X".into(),
3162 }],
3163 parent_group_ahb_status: Some("Muss".to_string()),
3164 ..Default::default()
3165 }],
3166 };
3167
3168 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3169 let pkg_errors: Vec<_> = report
3170 .by_category(ValidationCategory::Ahb)
3171 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3172 .collect();
3173 assert!(
3174 pkg_errors.is_empty(),
3175 "No packages in workflow — no errors expected"
3176 );
3177 }
3178
3179 #[test]
3180 fn test_package_cardinality_with_condition_and_package() {
3181 let evaluator = MockEvaluator::all_true(&[901]);
3183 let validator = EdifactValidator::new(evaluator);
3184 let external = NoOpExternalProvider;
3185
3186 let segments = vec![OwnedSegment {
3187 id: "STS".into(),
3188 elements: vec![vec![], vec![], vec!["E01".into()]],
3189 segment_number: 5,
3190 }];
3191
3192 let workflow = AhbWorkflow {
3193 pruefidentifikator: "13017".to_string(),
3194 description: "Test".to_string(),
3195 communication_direction: None,
3196 ub_definitions: HashMap::new(),
3197 fields: vec![AhbFieldRule {
3198 segment_path: "SG10/STS/C556/9013".to_string(),
3199 name: "Code".to_string(),
3200 ahb_status: "X".to_string(),
3201 element_index: Some(2),
3202 component_index: Some(0),
3203 codes: vec![
3204 AhbCodeRule {
3205 value: "E01".into(),
3206 description: "Code 1".into(),
3207 ahb_status: "X [901] [4P0..1]".into(),
3208 },
3209 AhbCodeRule {
3210 value: "E02".into(),
3211 description: "Code 2".into(),
3212 ahb_status: "X [901] [4P0..1]".into(),
3213 },
3214 ],
3215 parent_group_ahb_status: Some("Muss".to_string()),
3216 mig_number: None,
3217 }],
3218 };
3219
3220 let report = validator.validate(&segments, &workflow, &external, ValidationLevel::Full);
3221 let pkg_errors: Vec<_> = report
3222 .by_category(ValidationCategory::Ahb)
3223 .filter(|i| i.code == ErrorCodes::PACKAGE_CARDINALITY_VIOLATION)
3224 .collect();
3225 assert!(
3226 pkg_errors.is_empty(),
3227 "1 code within [4P0..1] bounds — no error, got: {:?}",
3228 pkg_errors
3229 );
3230 }
3231
3232 fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
3233 OwnedSegment {
3234 id: id.to_string(),
3235 elements: elements
3236 .into_iter()
3237 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
3238 .collect(),
3239 segment_number: 0,
3240 }
3241 }
3242
3243 #[test]
3244 fn test_unt_count_correct() {
3245 let segments = vec![
3247 make_segment("UNH", vec![vec!["001"]]),
3248 make_segment("BGM", vec![vec!["E01"]]),
3249 make_segment("DTM", vec![vec!["137", "20250401"]]),
3250 make_segment("UNT", vec![vec!["4", "001"]]),
3251 ];
3252 assert!(
3253 validate_unt_segment_count(&segments).is_none(),
3254 "Correct count should produce no issue"
3255 );
3256 }
3257
3258 #[test]
3259 fn test_unt_count_mismatch() {
3260 let segments = vec![
3262 make_segment("UNH", vec![vec!["001"]]),
3263 make_segment("BGM", vec![vec!["E01"]]),
3264 make_segment("UNT", vec![vec!["5", "001"]]),
3265 ];
3266 let issue = validate_unt_segment_count(&segments)
3267 .expect("Mismatch should produce an issue");
3268 assert_eq!(issue.code, ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
3269 assert_eq!(issue.severity, Severity::Error);
3270 assert!(issue.message.contains("declared 5"));
3271 assert!(issue.message.contains("actual 3"));
3272 }
3273
3274 #[test]
3275 fn test_unt_count_excludes_envelope() {
3276 let segments = vec![
3278 make_segment("UNA", vec![]),
3279 make_segment("UNB", vec![vec!["UNOC", "3"]]),
3280 make_segment("UNH", vec![vec!["001"]]),
3281 make_segment("BGM", vec![vec!["E01"]]),
3282 make_segment("UNT", vec![vec!["3", "001"]]),
3283 make_segment("UNZ", vec![vec!["1"]]),
3284 ];
3285 assert!(
3286 validate_unt_segment_count(&segments).is_none(),
3287 "Envelope segments excluded — count should be 3 (UNH+BGM+UNT)"
3288 );
3289 }
3290
3291 #[test]
3292 fn test_unt_count_no_unt_returns_none() {
3293 let segments = vec![
3294 make_segment("UNH", vec![vec!["001"]]),
3295 make_segment("BGM", vec![vec!["E01"]]),
3296 ];
3297 assert!(
3298 validate_unt_segment_count(&segments).is_none(),
3299 "No UNT segment should return None (not our problem)"
3300 );
3301 }
3302
3303 #[test]
3304 fn test_unt_count_rejects_multi_message_input() {
3305 let segments = vec![
3307 make_segment("UNH", vec![vec!["001"]]),
3308 make_segment("BGM", vec![vec!["E01"]]),
3309 make_segment("UNT", vec![vec!["3", "001"]]),
3310 make_segment("UNH", vec![vec!["002"]]),
3311 make_segment("BGM", vec![vec!["E02"]]),
3312 make_segment("UNT", vec![vec!["3", "002"]]),
3313 ];
3314 let issue = validate_unt_segment_count(&segments)
3315 .expect("Multi-message input should produce an error");
3316 assert_eq!(issue.code, ErrorCodes::UNT_SEGMENT_COUNT_MISMATCH);
3317 assert!(issue.message.contains("2 UNH"), "Should mention UNH count: {}", issue.message);
3318 }
3319}