edifact-mapper 0.30.4

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! A code element whose status the AHB gives per code is judged by the code
//! sent, on the BO4E path as on the EDIFACT path.
//!
//! Reported by mako-twin against 0.30.0: UTILMD 55010's STS+7 Transaktionsgrund
//! lists E01 `X [192]` ("Wenn … ZW4/ZAP vorhanden") and E03 `X`. The PID
//! schema gives the element E01's status, so `validate_pid_with_conditions`
//! judged every Transaktionsgrund by `[192]` and refused an E03 beside ZW3.
//! The EDIFACT validator reads the code's own status (`governing_status`).

use std::path::PathBuf;

use edifact_mapper::{DataDir, Mapper};
use serde_json::Value;

fn root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}

fn mapper(fv: &str) -> Option<Mapper> {
    if !root().join(format!("dist/edifact-data-{fv}.bin")).exists() {
        eprintln!("skipping {fv}: build the bundle first");
        return None;
    }
    Some(Mapper::from_data_dir(DataDir::path(root().join("dist")).eager(&[fv])).expect("bundle"))
}

/// The BO4E path's errors for the one transaction of `edi`.
fn bo4e_errors(m: &Mapper, fv: &str, pid: &str, edi: &str) -> Vec<String> {
    let ic = m
        .from_edifact::<Value, Value>(edi, fv, "UTILMD_Strom", pid)
        .expect("parse");
    let n = &ic.nachrichten[0];
    let mut entities = n.stammdaten.clone();
    let map = entities.as_object_mut().expect("stammdaten");
    map.insert(
        "nachricht".into(),
        Value::Object(n.nachrichtendaten.nachricht.clone()),
    );
    let tx = &n.transaktionen[0];
    for (k, v) in tx["stammdaten"].as_object().into_iter().flatten() {
        map.insert(k.clone(), v.clone());
    }
    map.insert("prozessdaten".into(), tx["transaktionsdaten"].clone());
    m.validate_pid_with_conditions(&entities, fv, "UTILMD_Strom", pid)
        .expect("validate")
        .iter()
        .map(|e| e.to_string())
        .filter(|e| e.starts_with("ERROR") || e.starts_with("INVALID"))
        .collect()
}

#[test]
fn the_transaktionsgrund_is_judged_by_its_own_code() {
    for fv in ["FV2510", "FV2604", "FV2610"] {
        let Some(m) = mapper(fv) else {
            continue;
        };
        let sample = root().join(format!(
            "fixtures/generated/{}/utilmd/55010__ZW3.edi",
            fv.to_lowercase()
        ));
        let edi = std::fs::read_to_string(&sample).expect("sample");
        assert!(edi.contains("STS+7++E03+ZW3'"), "{fv}: {edi}");

        // E03 (`X`) beside ZW3: allowed.
        let found = bo4e_errors(&m, fv, "55010", &edi);
        assert!(found.is_empty(), "{fv} E03: {found:#?}");

        // E01 (`X [192]`, "Wenn … ZW4/ZAP vorhanden") beside ZW3: refused.
        let e01 = edi.replace("STS+7++E03+ZW3'", "STS+7++E01+ZW3'");
        let found = bo4e_errors(&m, fv, "55010", &e01);
        assert!(
            found
                .iter()
                .any(|e| e.contains("transaktionsgrund") && e.contains("[192]")),
            "{fv} E01: {found:#?}"
        );
    }
}