edifact-mapper 0.25.0

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! A group the PID schema merged from several variants is judged per variant.
//!
//! Requested by mako-twin for gerätewechsel / geräteübernahme: on the BO4E path
//! (`validate_pid_with_conditions`) the generated samples of ORDERS, ORDRSP,
//! QUOTES and REQOTE drew message-level errors. The schema holds SG2 NAD+MS,
//! NAD+MR, NAD+DP with the DP's LOC in one group (`sg2`), so every
//! `marktteilnehmer` element was asked for the DP's `ortQualifier`, and for the
//! MS's contact (SG3 CTA) — and ORDRSP's `sg1` holds RFF+ON and RFF+Z13, so
//! the free RFF+ON reference was checked against RFF+Z13's code list. The
//! variants are told apart by MIG number: each entry segment starts one, the
//! segments and child groups after it belong to it.

use std::path::PathBuf;

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

fn mapper(fv: &str) -> Option<Mapper> {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
    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"))
}

#[test]
fn a_merged_group_has_one_requirement_set_per_variant() {
    let Some(m) = mapper("FV2610") else {
        return;
    };
    let orders = m.pid_requirements("FV2610", "ORDERS", "17009").unwrap();
    let mt = orders
        .entities
        .iter()
        .find(|e| e.entity == "Marktteilnehmer")
        .unwrap();
    let variant = |code: &str| {
        mt.variants
            .iter()
            .find(|v| v.code == code)
            .unwrap_or_else(|| panic!("variant {code}: {:?}", mt.variants))
    };
    let has = |code: &str, field: &str| variant(code).fields.iter().any(|f| f.bo4e_name == field);
    assert!(has("DP", "ortQualifier") && has("DP", "meldepunktId"));
    assert!(!has("MS", "ortQualifier") && !has("MR", "ortQualifier"));

    // The SG14 contact (CTA 00017) sits under NAD+MS (00016).
    let reqote = m.pid_requirements("FV2610", "REQOTE", "35001").unwrap();
    let mt = reqote
        .entities
        .iter()
        .find(|e| e.entity == "Marktteilnehmer")
        .unwrap();
    let contact = mt
        .nested
        .iter()
        .find(|n| n.field == "ansprechpartner")
        .unwrap();
    let selected: Vec<&str> = contact
        .parent_selector
        .iter()
        .filter(|s| s.field == "marktrolle")
        .flat_map(|s| s.codes.iter().map(|c| c.code.as_str()))
        .collect();
    assert_eq!(selected, ["MS"], "{:?}", contact.parent_selector);

    // RFF+ON's reference is free text; RFF+Z13's is the PID.
    let ordrsp = m.pid_requirements("FV2610", "ORDRSP", "19015").unwrap();
    let referenz = ordrsp
        .entities
        .iter()
        .find(|e| e.entity == "Referenz")
        .unwrap();
    let codes_of = |code: &str| {
        referenz
            .variants
            .iter()
            .find(|v| v.code == code)
            .and_then(|v| v.fields.iter().find(|f| f.bo4e_name == "referenz"))
            .map(|f| f.valid_codes.len())
    };
    assert_eq!(codes_of("ON"), Some(0));
    assert_eq!(codes_of("Z13"), Some(1));
}

/// The generated samples of the ten gerätewechsel / geräteübernahme PIDs hold
/// to their rulebook on the BO4E path: no error, no invalid code.
#[test]
fn geraetewechsel_and_geraeteuebernahme_samples_pass_the_bo4e_gate() {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
    let pids = [
        ("ORDERS", "17009"),
        ("ORDRSP", "19015"),
        ("ORDRSP", "19016"),
        ("IFTSTA", "21010"),
        ("IFTSTA", "21036"),
        ("MSCONS", "13017"),
        ("REQOTE", "35001"),
        ("QUOTES", "15001"),
        ("ORDERS", "17001"),
        ("ORDRSP", "19001"),
        ("ORDRSP", "19002"),
    ];
    for fv in ["FV2604", "FV2610"] {
        let Some(m) = mapper(fv) else {
            continue;
        };
        for (variant, pid) in pids {
            let sample = root.join(format!(
                "fixtures/generated/{}/{}/{pid}.edi",
                fv.to_lowercase(),
                variant.to_lowercase()
            ));
            let edi = std::fs::read_to_string(&sample).expect("sample");
            let interchange = m
                .from_edifact::<Value, Value>(&edi, fv, variant, pid)
                .expect("parse");
            let nachricht = &interchange.nachrichten[0];
            let transactions: Vec<Option<&Value>> = if nachricht.transaktionen.is_empty() {
                vec![None]
            } else {
                nachricht.transaktionen.iter().map(Some).collect()
            };
            for tx in transactions {
                // The entity map: message and transaction stammdaten, the
                // transaction's process data and the message's own data.
                let mut entities = nachricht.stammdaten.clone();
                let map = entities.as_object_mut().expect("message stammdaten");
                map.insert(
                    "nachricht".into(),
                    Value::Object(nachricht.nachrichtendaten.nachricht.clone()),
                );
                if let Some(tx) = tx {
                    for (k, v) in tx["stammdaten"].as_object().into_iter().flatten() {
                        map.insert(k.clone(), v.clone());
                    }
                    if !tx["transaktionsdaten"].is_null() {
                        map.insert("prozessdaten".into(), tx["transaktionsdaten"].clone());
                    }
                }
                let hard: Vec<String> = m
                    .validate_pid_with_conditions(&entities, fv, variant, pid)
                    .expect("validate")
                    .iter()
                    .map(|e| e.to_string())
                    .filter(|e| e.starts_with("ERROR") || e.starts_with("INVALID"))
                    .collect();
                assert!(hard.is_empty(), "{fv} {variant} {pid}: {hard:#?}");
            }
        }
    }
}