edifact-mapper 0.17.0

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! Lists written by `parent_field` rules are validated per parent element.
//!
//! 55043's Tranche (SG8 SEQ+Z15) carries its SG9 as `mengen[]` and its SG10s
//! as `zuordnungen[]`. They had no requirements, so neither validator ever
//! demanded them or checked their codes (reported by mako-twin): a Tranche
//! without its QTY+11 (`Muss [384]`), a missing Marktpartner CCI+ZB3 and a
//! wrong CAV code all passed.

use std::path::PathBuf;

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

type Dynamic = mig_bo4e::model::Interchange<Value, Value>;

const FV: &str = "FV2604";
const VARIANT: &str = "UTILMD_Strom";
const PID: &str = "55043";

/// The generated 55043 fixture's transaction stammdaten.
fn load() -> Option<(Mapper, Value)> {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
    let fixture = root.join("fixtures/generated/fv2604/utilmd/55043.edi");
    if !root.join(format!("dist/edifact-data-{FV}.bin")).exists() || !fixture.exists() {
        eprintln!("skipping: regenerate the {FV} bundle + fixture first");
        return None;
    }
    let mapper =
        Mapper::from_data_dir(DataDir::path(root.join("dist")).eager(&[FV])).expect("bundle");
    let edifact = std::fs::read_to_string(&fixture).expect("fixture");
    let ic: Dynamic = mapper
        .from_edifact(&edifact, FV, VARIANT, PID)
        .expect("convert fixture");
    let raw = ic.nachrichten[0].transaktionen[0].clone();
    let mut tx = raw["stammdaten"].clone();
    mig_bo4e::model::restore_entity(
        &mut tx,
        mig_bo4e::model::TX_METADATA_ENTITY,
        raw.get("transaktionsdaten").unwrap_or(&Value::Null),
    );
    Some((mapper, tx))
}

/// The findings about the Tranche's lists, from both validators.
fn tranche_findings(mapper: &Mapper, tx: &Value) -> (Vec<String>, Vec<String>) {
    let about_lists = |errors: Vec<PidValidationError>| -> Vec<String> {
        errors
            .iter()
            .map(|e| e.to_string())
            .filter(|e| e.contains("TrancheDaten.mengen") || e.contains("TrancheDaten.zuordnungen"))
            .collect()
    };
    (
        about_lists(mapper.validate_pid(tx, FV, VARIANT, PID).unwrap()),
        about_lists(
            mapper
                .validate_pid_with_conditions(tx, FV, VARIANT, PID)
                .unwrap(),
        ),
    )
}

#[test]
fn a_complete_tranche_has_no_list_findings() {
    let Some((mapper, tx)) = load() else { return };
    assert!(
        tx["trancheDaten"]["mengen"].is_array(),
        "the fixture has a Tranche with mengen"
    );
    let (basic, conditional) = tranche_findings(&mapper, &tx);
    assert!(basic.is_empty(), "{basic:#?}");
    assert!(conditional.is_empty(), "{conditional:#?}");
}

#[test]
fn a_tranche_without_its_menge_is_reported_when_384_holds() {
    let Some((mapper, mut tx)) = load() else {
        return;
    };
    tx["trancheDaten"]
        .as_object_mut()
        .expect("trancheDaten")
        .remove("mengen");
    let (_, conditional) = tranche_findings(&mapper, &tx);
    assert!(
        conditional
            .iter()
            .any(|e| e.contains("TrancheDaten.mengen[]")),
        "{conditional:#?}"
    );
}

#[test]
fn a_missing_kind_of_zuordnung_is_reported() {
    let Some((mapper, mut tx)) = load() else {
        return;
    };
    let zuordnungen = tx["trancheDaten"]["zuordnungen"]
        .as_array_mut()
        .expect("zuordnungen");
    zuordnungen.retain(|z| z["merkmal"]["art"] != "zugeordneterMarktpartner");
    let (_, conditional) = tranche_findings(&mapper, &tx);
    assert!(
        conditional
            .iter()
            .any(|e| e.contains("TrancheDaten.zuordnungen[zugeordneterMarktpartner]")),
        "{conditional:#?}"
    );
}

#[test]
fn every_cav_code_is_checked() {
    let Some((mapper, mut tx)) = load() else {
        return;
    };
    let werte = tx["trancheDaten"]["zuordnungen"][0]["werte"]
        .as_array_mut()
        .expect("werte");
    // A second, wrong CAV behind the right one.
    werte.push(serde_json::json!({ "code": "Z99" }));
    let (basic, conditional) = tranche_findings(&mapper, &tx);
    for findings in [&basic, &conditional] {
        assert!(
            findings
                .iter()
                .any(|e| e.contains("zuordnungen[].werte[].code") && e.contains("Z99")),
            "{findings:#?}"
        );
    }
}