use automapper_validation::eval::NoOpExternalProvider;
use automapper_validation::fv2504::UtilmdStromConditionEvaluatorFV2504;
use automapper_validation::validator::validate::{AhbFieldRule, AhbWorkflow};
use automapper_validation::{
build_validated_tree, ConditionParser, EdifactValidator, ValidationLevel,
};
use mig_assembly::assembler::{
AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
};
use mig_types::segment::OwnedSegment;
use std::collections::{BTreeMap, HashMap};
fn make_tree(dtm92_value: &str, dtm137_value: &str) -> AssembledTree {
AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![
AssembledSegment {
tag: "DTM".to_string(),
elements: vec![vec![
"92".to_string(),
dtm92_value.to_string(),
"303".to_string(),
]],
mig_number: Some("0082".to_string()),
segment_number: None,
},
AssembledSegment {
tag: "DTM".to_string(),
elements: vec![vec![
"137".to_string(),
dtm137_value.to_string(),
"303".to_string(),
]],
mig_number: Some("0081".to_string()),
segment_number: None,
},
],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
}
}
fn make_workflow() -> AhbWorkflow {
let ub1_expr = ConditionParser::parse_raw("([931] ∧ [932] [490]) ⊻ ([931] ∧ [933] [491])")
.expect("UB1 should parse")
.expect("UB1 should not be empty");
let mut ub_definitions = HashMap::new();
ub_definitions.insert("UB1".to_string(), ub1_expr);
AhbWorkflow {
pruefidentifikator: "55001".to_string(),
description: "Test workflow".to_string(),
communication_direction: None,
fields: vec![AhbFieldRule {
segment_path: "SG4/DTM/C507/2380".to_string(),
name: "Eingangsdatum".to_string(),
ahb_status: "X [UB1]".to_string(),
codes: vec![],
parent_group_ahb_status: None,
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(1),
mig_number: Some("0082".to_string()),
}],
ub_definitions,
}
}
fn make_flat_segments(dtm92_value: &str, dtm137_value: &str) -> Vec<OwnedSegment> {
vec![
OwnedSegment {
id: "DTM".to_string(),
elements: vec![vec![
"92".to_string(),
dtm92_value.to_string(),
"303".to_string(),
]],
segment_number: 1,
},
OwnedSegment {
id: "DTM".to_string(),
elements: vec![vec![
"137".to_string(),
dtm137_value.to_string(),
"303".to_string(),
]],
segment_number: 2,
},
]
}
#[test]
fn ub1_passes_with_tree_based_validation() {
let tree = make_tree("202604302200+00", "202503311329+00");
let workflow = make_workflow();
let flat_segments = make_flat_segments("202604302200+00", "202503311329+00");
let validated_tree = build_validated_tree(&workflow, &tree);
let evaluator = UtilmdStromConditionEvaluatorFV2504::default();
let validator = EdifactValidator::new(evaluator);
let external = NoOpExternalProvider;
let report = validator.validate_tree(
&validated_tree,
&flat_segments,
&external,
ValidationLevel::Conditions,
None,
);
let ahb003_errors: Vec<_> = report
.errors()
.filter(|issue| issue.code == "AHB003")
.collect();
assert!(
ahb003_errors.is_empty(),
"UB1 should pass for DTM+92 with 202604302200+00, but got AHB003 errors: {ahb003_errors:?}"
);
}
#[test]
fn missing_group_variant_reports_error() {
use automapper_validation::validator::validate::AhbCodeRule;
let workflow = AhbWorkflow {
pruefidentifikator: "55001".to_string(),
description: "Test".to_string(),
communication_direction: None,
fields: vec![
AhbFieldRule {
segment_path: "SG2/NAD/3035".to_string(),
name: "MP-ID Absender Qualifier".to_string(),
ahb_status: "X".to_string(),
codes: vec![AhbCodeRule {
value: "MS".to_string(),
description: "Absender".to_string(),
ahb_status: "X".to_string(),
}],
parent_group_ahb_status: Some("Muss".to_string()),
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(0),
mig_number: Some("0010".to_string()),
},
AhbFieldRule {
segment_path: "SG2/NAD/3035".to_string(),
name: "MP-ID Empfänger Qualifier".to_string(),
ahb_status: "X".to_string(),
codes: vec![AhbCodeRule {
value: "MR".to_string(),
description: "Empfänger".to_string(),
ahb_status: "X".to_string(),
}],
parent_group_ahb_status: Some("Muss".to_string()),
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(0),
mig_number: Some("0011".to_string()),
},
],
ub_definitions: HashMap::new(),
};
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG2".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "NAD".to_string(),
elements: vec![vec!["MR".to_string()]],
mig_number: Some("0011".to_string()),
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: Some("0011".to_string()),
variant_mig_numbers: vec!["0011".to_string()],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
};
let flat_segments = vec![OwnedSegment {
id: "NAD".to_string(),
elements: vec![vec!["MR".to_string(), "9900269000000".to_string()]],
segment_number: 1,
}];
let validated_tree = build_validated_tree(&workflow, &tree);
assert_eq!(
validated_tree.unmatched_rules.len(),
1,
"NAD+MS rule should be unmatched"
);
let evaluator = UtilmdStromConditionEvaluatorFV2504::default();
let validator = EdifactValidator::new(evaluator);
let external = NoOpExternalProvider;
let report = validator.validate_tree(
&validated_tree,
&flat_segments,
&external,
ValidationLevel::Conditions,
None,
);
let missing_errors: Vec<_> = report
.errors()
.filter(|issue| issue.code == "AHB001")
.collect();
assert!(
!missing_errors.is_empty(),
"Should report missing NAD+MS field, but got no AHB001 errors. All issues: {:?}",
report.issues
);
let error = &missing_errors[0];
assert_eq!(error.field_path.as_deref(), Some("SG2/NAD/3035"));
assert!(
error.message.contains("MP-ID Absender"),
"Error should mention Absender, got: {}",
error.message
);
}
#[test]
fn missing_segment_within_instance_reports_error() {
use automapper_validation::validator::validate::AhbCodeRule;
let workflow = AhbWorkflow {
pruefidentifikator: "55001".to_string(),
description: "Test".to_string(),
communication_direction: None,
fields: vec![
AhbFieldRule {
segment_path: "SG4/DTM/C507/2005".to_string(),
name: "Eingangsdatum Qualifier".to_string(),
ahb_status: "X".to_string(),
codes: vec![AhbCodeRule {
value: "92".to_string(),
description: "Eingangsdatum".to_string(),
ahb_status: "X".to_string(),
}],
parent_group_ahb_status: None,
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(0),
mig_number: Some("00023".to_string()),
},
AhbFieldRule {
segment_path: "SG4/DTM/C507/2380".to_string(),
name: "Eingangsdatum Wert".to_string(),
ahb_status: "X".to_string(),
codes: vec![],
parent_group_ahb_status: None,
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(1),
mig_number: Some("00023".to_string()),
},
AhbFieldRule {
segment_path: "SG4/DTM/C507/2005".to_string(),
name: "Enddatum Qualifier".to_string(),
ahb_status: "X".to_string(),
codes: vec![AhbCodeRule {
value: "93".to_string(),
description: "Enddatum".to_string(),
ahb_status: "X".to_string(),
}],
parent_group_ahb_status: None,
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(0),
mig_number: Some("00024".to_string()),
},
AhbFieldRule {
segment_path: "SG4/DTM/C507/2380".to_string(),
name: "Enddatum Wert".to_string(),
ahb_status: "X".to_string(),
codes: vec![],
parent_group_ahb_status: None,
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(1),
mig_number: Some("00024".to_string()),
},
],
ub_definitions: HashMap::new(),
};
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "DTM".to_string(),
elements: vec![vec![
"93".to_string(),
"202512312300+00".to_string(),
"303".to_string(),
]],
mig_number: Some("00024".to_string()),
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec!["00023".to_string(), "00024".to_string()],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
};
let flat_segments = vec![OwnedSegment {
id: "DTM".to_string(),
elements: vec![vec![
"93".to_string(),
"202512312300+00".to_string(),
"303".to_string(),
]],
segment_number: 1,
}];
let validated_tree = build_validated_tree(&workflow, &tree);
let evaluator = UtilmdStromConditionEvaluatorFV2504::default();
let validator = EdifactValidator::new(evaluator);
let external = NoOpExternalProvider;
let report = validator.validate_tree(
&validated_tree,
&flat_segments,
&external,
ValidationLevel::Conditions,
None,
);
let missing_errors: Vec<_> = report
.errors()
.filter(|issue| issue.code == "AHB001")
.collect();
assert!(
!missing_errors.is_empty(),
"Should report missing DTM+92 fields, but got no AHB001 errors"
);
let dtm92_errors: Vec<_> = missing_errors
.iter()
.filter(|e| e.message.contains("Eingangsdatum"))
.collect();
assert!(
dtm92_errors.len() >= 2,
"Should report at least 2 DTM+92 missing errors (qualifier + value), got {}",
dtm92_errors.len()
);
let dtm93_errors: Vec<_> = missing_errors
.iter()
.filter(|e| e.message.contains("Enddatum"))
.collect();
assert!(
dtm93_errors.is_empty(),
"DTM+93 is present — should NOT be reported as missing. Got: {:?}",
dtm93_errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
}
#[test]
fn present_segments_with_mig_number_not_reported_missing() {
use automapper_validation::validator::validate::AhbCodeRule;
let workflow = AhbWorkflow {
pruefidentifikator: "55001".to_string(),
description: "Test".to_string(),
communication_direction: None,
fields: vec![
AhbFieldRule {
segment_path: "SG4/DTM/C507/2005".to_string(),
name: "Eingangsdatum Qualifier".to_string(),
ahb_status: "X".to_string(),
codes: vec![AhbCodeRule {
value: "92".to_string(),
description: "".to_string(),
ahb_status: "X".to_string(),
}],
parent_group_ahb_status: None,
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(0),
mig_number: Some("00023".to_string()),
},
AhbFieldRule {
segment_path: "SG4/DTM/C507/2005".to_string(),
name: "Enddatum Qualifier".to_string(),
ahb_status: "X".to_string(),
codes: vec![AhbCodeRule {
value: "93".to_string(),
description: "".to_string(),
ahb_status: "X".to_string(),
}],
parent_group_ahb_status: None,
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(0),
mig_number: Some("00024".to_string()),
},
AhbFieldRule {
segment_path: "SG4/STS/C601/9015".to_string(),
name: "Statuskategorie".to_string(),
ahb_status: "X".to_string(),
codes: vec![],
parent_group_ahb_status: None,
segment_ahb_status: None,
element_index: Some(0),
component_index: Some(0),
mig_number: Some("00035".to_string()),
},
],
ub_definitions: HashMap::new(),
};
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![
AssembledSegment {
tag: "DTM".to_string(),
elements: vec![vec![
"92".to_string(),
"202505312200+00".to_string(),
"303".to_string(),
]],
mig_number: Some("00023".to_string()),
segment_number: None,
},
AssembledSegment {
tag: "DTM".to_string(),
elements: vec![vec![
"93".to_string(),
"202512312300+00".to_string(),
"303".to_string(),
]],
mig_number: Some("00024".to_string()),
segment_number: None,
},
AssembledSegment {
tag: "STS".to_string(),
elements: vec![vec!["7".to_string()], vec![], vec!["E01".to_string()]],
mig_number: Some("00035".to_string()),
segment_number: None,
},
],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![
"00023".to_string(),
"00024".to_string(),
"00035".to_string(),
],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
};
let flat_segments = vec![];
let validated_tree = build_validated_tree(&workflow, &tree);
let evaluator = UtilmdStromConditionEvaluatorFV2504::default();
let validator = EdifactValidator::new(evaluator);
let external = NoOpExternalProvider;
let report = validator.validate_tree(
&validated_tree,
&flat_segments,
&external,
ValidationLevel::Conditions,
None,
);
let missing_errors: Vec<_> = report
.errors()
.filter(|issue| issue.code == "AHB001")
.collect();
assert!(
missing_errors.is_empty(),
"All segments are present — should have no AHB001 errors. Got:\n{}",
missing_errors
.iter()
.map(|e| format!(
" {} at {}",
e.message,
e.field_path.as_deref().unwrap_or("?")
))
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn ub1_fails_when_dtm92_has_wrong_time() {
let tree = make_tree("202604301329+00", "202503311329+00");
let workflow = make_workflow();
let flat_segments = make_flat_segments("202604301329+00", "202503311329+00");
let validated_tree = build_validated_tree(&workflow, &tree);
let evaluator = UtilmdStromConditionEvaluatorFV2504::default();
let validator = EdifactValidator::new(evaluator);
let external = NoOpExternalProvider;
let report = validator.validate_tree(
&validated_tree,
&flat_segments,
&external,
ValidationLevel::Conditions,
None,
);
let ahb003_errors: Vec<_> = report
.errors()
.filter(|issue| issue.code == "AHB003")
.collect();
assert!(
!ahb003_errors.is_empty(),
"UB1 should fail for DTM+92 with 202604301329+00 (HHMM=1329 is neither 2200 nor 2300)"
);
let error = &ahb003_errors[0];
assert_eq!(
error.field_path.as_deref(),
Some("SG4/DTM/C507/2380"),
"Error should reference DTM+92 field path"
);
assert!(
error.rule.as_deref().unwrap_or("").contains("UB1"),
"Error rule should mention UB1"
);
}
#[test]
fn ahb003_issue_carries_segment_position_from_assembled_segment() {
let mut tree = make_tree("202604301329+00", "202503311329+00");
tree.groups[0].repetitions[0].segments[0].segment_number = Some(7);
let workflow = make_workflow();
let flat_segments = vec![
OwnedSegment {
id: "DTM".to_string(),
elements: vec![vec![
"92".to_string(),
"202604301329+00".to_string(),
"303".to_string(),
]],
segment_number: 7,
},
OwnedSegment {
id: "DTM".to_string(),
elements: vec![vec![
"137".to_string(),
"202503311329+00".to_string(),
"303".to_string(),
]],
segment_number: 3,
},
];
let validated_tree = build_validated_tree(&workflow, &tree);
let evaluator = UtilmdStromConditionEvaluatorFV2504::default();
let validator = EdifactValidator::new(evaluator);
let external = NoOpExternalProvider;
let report = validator.validate_tree(
&validated_tree,
&flat_segments,
&external,
ValidationLevel::Conditions,
None,
);
let ahb003 = report
.errors()
.find(|issue| issue.code == "AHB003")
.expect("AHB003 should fire for DTM+92 with HHMM=1329");
let pos = ahb003
.segment_position
.expect("AHB003 issue must carry segment_position");
assert_eq!(
pos.segment_number, 7,
"segment_position must reflect the matched DTM+92's counter"
);
}