automapper-validation 0.2.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! EBD cluster conditions across other message types.
//!
//! ORDRSP [17] / [18]: the resolved Prüfschritt code is in the
//! Zustimmung / Ablehnung cluster of the referenced EBD.
//! UTILTS [61]: any STS+E01 carries an Ablehnung-cluster code.
//! REMADV [14] / [15] / [16]: the resolved AJT DE4465 code is in the
//! corresponding Ablehnung sub-cluster.
//! REMADV [517] / [518]: any AJT in scope carries a Summen/Kopf-level
//! Ablehnung code (with [518]'s A80 carve-out).

use automapper_validation::eval::{
    ConditionEvaluator, ConditionResult, EvaluationContext, NoOpExternalProvider,
};
use automapper_validation::generated::fv2504::{
    OrdrspConditionEvaluatorFV2504, RemadvConditionEvaluatorFV2504, UtiltsConditionEvaluatorFV2504,
};
use mig_types::segment::OwnedSegment;

fn sts_e01_segment(code: &str, ebd: &str) -> OwnedSegment {
    OwnedSegment {
        id: "STS".to_string(),
        segment_number: 0,
        elements: vec![
            vec!["E01".to_string()],
            vec![String::new()],
            vec![code.to_string(), ebd.to_string()],
        ],
    }
}

fn ajt_segment(code: &str, ebd: &str) -> OwnedSegment {
    OwnedSegment {
        id: "AJT".to_string(),
        segment_number: 0,
        elements: vec![vec![code.to_string()], vec![ebd.to_string()]],
    }
}

// ---------- ORDRSP [17] / [18] ----------

#[test]
fn ordrsp_17_true_when_resolved_pruefschritt_is_zustimmung() {
    // STS+Z25++A36:E_0624 — A36 is Zustimmung in E_0624.
    let sts = sts_e01_segment("A36", "E_0624");
    let ext = NoOpExternalProvider;
    let segs = vec![sts.clone()];
    let ctx = EvaluationContext::new("01001", &ext, &segs)
        .with_resolved(Some("A36"), Some(&sts.elements));
    let eval = OrdrspConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(17, &ctx), ConditionResult::True);
}

#[test]
fn ordrsp_17_false_when_resolved_pruefschritt_is_ablehnung() {
    let sts = sts_e01_segment("A35", "E_0624");
    let ext = NoOpExternalProvider;
    let segs = vec![sts.clone()];
    let ctx = EvaluationContext::new("01001", &ext, &segs)
        .with_resolved(Some("A35"), Some(&sts.elements));
    let eval = OrdrspConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(17, &ctx), ConditionResult::False);
}

#[test]
fn ordrsp_18_true_when_resolved_pruefschritt_is_ablehnung() {
    let sts = sts_e01_segment("A35", "E_0624");
    let ext = NoOpExternalProvider;
    let segs = vec![sts.clone()];
    let ctx = EvaluationContext::new("01001", &ext, &segs)
        .with_resolved(Some("A35"), Some(&sts.elements));
    let eval = OrdrspConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(18, &ctx), ConditionResult::True);
}

#[test]
fn ordrsp_17_unknown_without_resolved_context() {
    let ext = NoOpExternalProvider;
    let segs: Vec<OwnedSegment> = vec![];
    let ctx = EvaluationContext::new("01001", &ext, &segs);
    let eval = OrdrspConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(17, &ctx), ConditionResult::Unknown);
}

#[test]
fn ordrsp_17_no_longer_marked_external() {
    // Before the rewrite [17]/[18] were EXTERNAL. Now they self-resolve
    // from the cluster lookup, so is_external must be false.
    let eval = OrdrspConditionEvaluatorFV2504::default();
    assert!(!eval.is_external(17));
    assert!(!eval.is_external(18));
}

// ---------- UTILTS [61] ----------

#[test]
fn utilts_61_true_when_any_sts_e01_is_ablehnung() {
    // Mixed — one Zustimmung, one Ablehnung → True (existential).
    let segs = vec![
        sts_e01_segment("A36", "E_0624"),
        sts_e01_segment("A35", "E_0624"),
    ];
    let ext = NoOpExternalProvider;
    let ctx = EvaluationContext::new("25001", &ext, &segs);
    let eval = UtiltsConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(61, &ctx), ConditionResult::True);
}

#[test]
fn utilts_61_false_when_all_sts_e01_are_zustimmung() {
    let segs = vec![sts_e01_segment("A36", "E_0624")];
    let ext = NoOpExternalProvider;
    let ctx = EvaluationContext::new("25001", &ext, &segs);
    let eval = UtiltsConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(61, &ctx), ConditionResult::False);
}

#[test]
fn utilts_61_false_without_sts_e01() {
    // Existential check — premise absent → False, not Unknown.
    let segs: Vec<OwnedSegment> = vec![];
    let ext = NoOpExternalProvider;
    let ctx = EvaluationContext::new("25001", &ext, &segs);
    let eval = UtiltsConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(61, &ctx), ConditionResult::False);
}

#[test]
fn utilts_61_no_longer_marked_external() {
    let eval = UtiltsConditionEvaluatorFV2504::default();
    assert!(!eval.is_external(61));
}

// ---------- REMADV [14] / [15] / [16] ----------

// Real examples from mako_prozesse (via ebd_cluster_map.json):
// - E_0407 A01 → Ablehnung auf Kopfebene
// - E_0407 A80 → Ablehnung auf Kopfebene (carved out by [518])
// - E_0407 A24 → Ablehnung auf Positionsebene
// - E_0407 A66 → Ablehnung auf Summenebene

#[test]
fn remadv_14_true_when_resolved_ajt_is_ablehnung_kopfebene() {
    let ajt = ajt_segment("A01", "E_0407");
    let ext = NoOpExternalProvider;
    let segs = vec![ajt.clone()];
    let ctx = EvaluationContext::new("29001", &ext, &segs)
        .with_resolved(Some("A01"), Some(&ajt.elements));
    let eval = RemadvConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(14, &ctx), ConditionResult::True);
}

#[test]
fn remadv_14_false_when_resolved_ajt_is_positionsebene() {
    let ajt = ajt_segment("A24", "E_0407");
    let ext = NoOpExternalProvider;
    let segs = vec![ajt.clone()];
    let ctx = EvaluationContext::new("29001", &ext, &segs)
        .with_resolved(Some("A24"), Some(&ajt.elements));
    let eval = RemadvConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(14, &ctx), ConditionResult::False);
}

#[test]
fn remadv_15_true_when_resolved_ajt_is_positionsebene() {
    let ajt = ajt_segment("A24", "E_0407");
    let ext = NoOpExternalProvider;
    let segs = vec![ajt.clone()];
    let ctx = EvaluationContext::new("29001", &ext, &segs)
        .with_resolved(Some("A24"), Some(&ajt.elements));
    let eval = RemadvConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(15, &ctx), ConditionResult::True);
}

#[test]
fn remadv_16_true_when_resolved_ajt_is_summenebene() {
    let ajt = ajt_segment("A66", "E_0407");
    let ext = NoOpExternalProvider;
    let segs = vec![ajt.clone()];
    let ctx = EvaluationContext::new("29001", &ext, &segs)
        .with_resolved(Some("A66"), Some(&ajt.elements));
    let eval = RemadvConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(16, &ctx), ConditionResult::True);
}

#[test]
fn remadv_14_unknown_without_resolved_context() {
    let ext = NoOpExternalProvider;
    let segs: Vec<OwnedSegment> = vec![];
    let ctx = EvaluationContext::new("29001", &ext, &segs);
    let eval = RemadvConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(14, &ctx), ConditionResult::Unknown);
}

// ---------- REMADV [517] / [518] ----------

#[test]
fn remadv_517_true_when_ajt_has_summenebene_code() {
    let segs = vec![ajt_segment("A66", "E_0407")];
    let ext = NoOpExternalProvider;
    let ctx = EvaluationContext::new("29001", &ext, &segs);
    let eval = RemadvConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(517, &ctx), ConditionResult::True);
}

#[test]
fn remadv_517_false_when_ajt_has_only_kopf_or_position_codes() {
    let segs = vec![ajt_segment("A01", "E_0407"), ajt_segment("A24", "E_0407")];
    let ext = NoOpExternalProvider;
    let ctx = EvaluationContext::new("29001", &ext, &segs);
    let eval = RemadvConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(517, &ctx), ConditionResult::False);
}

#[test]
fn remadv_518_true_when_ajt_has_kopfebene_code_other_than_a80() {
    let segs = vec![ajt_segment("A01", "E_0407")];
    let ext = NoOpExternalProvider;
    let ctx = EvaluationContext::new("29001", &ext, &segs);
    let eval = RemadvConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(518, &ctx), ConditionResult::True);
}

#[test]
fn remadv_518_false_when_only_a80_kopfebene_present() {
    // A80 is excluded from [518] by construction. The remaining code
    // A56 is Positionsebene, not Kopfebene — also shouldn't trigger.
    let segs = vec![ajt_segment("A80", "E_0407"), ajt_segment("A24", "E_0407")];
    let ext = NoOpExternalProvider;
    let ctx = EvaluationContext::new("29001", &ext, &segs);
    let eval = RemadvConditionEvaluatorFV2504::default();
    assert_eq!(eval.evaluate(518, &ctx), ConditionResult::False);
}