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