mig-bo4e 0.1.58

Declarative TOML-based MIG-tree to BO4E mapping engine
Documentation
//! NAD+DP routing post-processor.
//!
//! NAD+DP segments carry a delivery-point reference plus address — they
//! aren't market participants. This module routes DP entries out of the
//! `marktteilnehmer[]` array into a standalone `Marktlokation` or
//! `Messlokation` entity (picked by `meldepunktId` format), and supports
//! the inverse transformation on the reverse path.
//!
//! Routing metadata (originalIdx, marktrolle, ortQualifier, …) is captured
//! in a [`DpRouting`] side-channel rather than stamped on the BO4E JSON, so
//! the public entity stays clean while roundtrip stays byte-identical.

use crate::engine::deep_merge_insert;

/// Side-channel metadata for NAD+DP entries routed into a lokation entity.
///
/// Keyed by destination entity name (`"marktlokation"` / `"messlokation"`).
/// Each `Vec` entry corresponds to one routed DP entry, in the same array
/// order they were emitted into the lokation key.
pub type DpRouting =
    std::collections::HashMap<String, Vec<serde_json::Map<String, serde_json::Value>>>;

/// Route NAD+DP entries out of `marktteilnehmer[]` into a Marktlokation or
/// Messlokation entity. Returns the routing metadata so the caller can
/// thread it into a side-channel and rebuild the segment on reverse.
///
/// - 11-digit numeric `meldepunktId` → Marktlokation.
/// - 33-char `DE`-prefixed `meldepunktId` → Messlokation.
/// - Anything else → Marktlokation (best-effort fallback).
///
/// Uses [`deep_merge_insert`] so a real lokation entity already produced by
/// SG5/LOC mapping isn't silently overwritten by the routed DP entity.
pub fn route_nad_dp_to_lokation(
    result: &mut serde_json::Map<String, serde_json::Value>,
) -> DpRouting {
    let mut dp_routing: DpRouting = std::collections::HashMap::new();

    let mts = match result.get_mut("marktteilnehmer") {
        Some(serde_json::Value::Array(arr)) => arr,
        _ => return dp_routing,
    };

    let mut dp_entries: Vec<(usize, serde_json::Value)> = Vec::new();
    let mut idx = 0;
    let mut original_idx = 0usize;
    while idx < mts.len() {
        let is_dp = mts[idx]
            .get("marktrolle")
            .and_then(|v| {
                v.get("code")
                    .and_then(|c| c.as_str())
                    .or_else(|| v.as_str())
            })
            .map(|s| s == "DP")
            .unwrap_or(false);
        if is_dp {
            dp_entries.push((original_idx, mts.remove(idx)));
        } else {
            idx += 1;
        }
        original_idx += 1;
    }

    if dp_entries.is_empty() {
        return dp_routing;
    }

    if mts.is_empty() {
        result.remove("marktteilnehmer");
    }

    let mut lokation_objs: Vec<(
        String,
        serde_json::Value,
        serde_json::Map<String, serde_json::Value>,
    )> = Vec::new();
    for (orig_idx, dp) in dp_entries {
        lokation_objs.push(build_lokation_from_dp(dp, orig_idx));
    }

    type Grouped = std::collections::BTreeMap<
        String,
        (
            Vec<serde_json::Value>,
            Vec<serde_json::Map<String, serde_json::Value>>,
        ),
    >;
    let mut grouped: Grouped = std::collections::BTreeMap::new();
    for (k, v, meta) in lokation_objs {
        let entry = grouped.entry(k).or_default();
        entry.0.push(v);
        entry.1.push(meta);
    }
    for (key, (mut values, meta)) in grouped {
        let v = if values.len() == 1 {
            values.remove(0)
        } else {
            serde_json::Value::Array(values)
        };
        deep_merge_insert(result, &key, v);
        dp_routing.insert(key, meta);
    }

    dp_routing
}

/// Convert a single DP `Marktteilnehmer` entry into a lokation object.
/// Returns `(entity_key, json_value, dp_metadata)`.
fn build_lokation_from_dp(
    dp: serde_json::Value,
    original_idx: usize,
) -> (
    String,
    serde_json::Value,
    serde_json::Map<String, serde_json::Value>,
) {
    let dp_obj = match dp {
        serde_json::Value::Object(m) => m,
        other => return ("marktlokation".to_string(), other, serde_json::Map::new()),
    };

    let meldepunkt_id = dp_obj
        .get("meldepunktId")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();
    let target = classify_dp_id(&meldepunkt_id);

    let mut out = serde_json::Map::new();
    out.insert(
        "_type".to_string(),
        serde_json::Value::String(target.bo4e_type().to_string()),
    );
    out.insert(
        "boTyp".to_string(),
        serde_json::Value::String(target.bo4e_type().to_uppercase()),
    );
    if !meldepunkt_id.is_empty() {
        out.insert(
            target.id_field().to_string(),
            serde_json::Value::String(meldepunkt_id),
        );
    }

    let mut adresse = serde_json::Map::new();
    for (src, dst) in [
        ("strasse", "strasse"),
        ("strasse2", "strasse2"),
        ("hausnummer", "hausnummer"),
        ("strasse4", "strasse4"),
        ("postleitzahl", "postleitzahl"),
        ("ort", "ort"),
        ("land", "land"),
        ("zusatzinfo", "adresszusatz"),
    ] {
        if let Some(v) = dp_obj.get(src) {
            if !v.is_null() {
                adresse.insert(dst.to_string(), v.clone());
            }
        }
    }
    if !adresse.is_empty() {
        out.insert("adresse".to_string(), serde_json::Value::Object(adresse));
    }

    let mut dp_metadata = serde_json::Map::new();
    dp_metadata.insert(
        "originalIdx".to_string(),
        serde_json::Value::Number(serde_json::Number::from(original_idx)),
    );
    if let Some(v) = dp_obj.get("marktrolle") {
        dp_metadata.insert("marktrolle".to_string(), v.clone());
    }
    if let Some(v) = dp_obj.get("ortQualifier") {
        dp_metadata.insert("ortQualifier".to_string(), v.clone());
    }
    if let Some(v) = dp_obj.get("versionStruktur") {
        dp_metadata.insert("versionStruktur".to_string(), v.clone());
    }
    for (k, v) in dp_obj.iter() {
        if matches!(
            k.as_str(),
            "boTyp"
                | "marktrolle"
                | "ortQualifier"
                | "versionStruktur"
                | "meldepunktId"
                | "strasse"
                | "strasse2"
                | "hausnummer"
                | "strasse4"
                | "postleitzahl"
                | "ort"
                | "land"
                | "zusatzinfo"
        ) {
            continue;
        }
        dp_metadata.insert(k.clone(), v.clone());
    }

    (
        target.entity_key().to_string(),
        serde_json::Value::Object(out),
        dp_metadata,
    )
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DpTarget {
    Marktlokation,
    Messlokation,
}

impl DpTarget {
    fn entity_key(self) -> &'static str {
        match self {
            DpTarget::Marktlokation => "marktlokation",
            DpTarget::Messlokation => "messlokation",
        }
    }
    fn id_field(self) -> &'static str {
        match self {
            DpTarget::Marktlokation => "marktlokationsId",
            DpTarget::Messlokation => "messlokationsId",
        }
    }
    fn bo4e_type(self) -> &'static str {
        match self {
            DpTarget::Marktlokation => "Marktlokation",
            DpTarget::Messlokation => "Messlokation",
        }
    }
}

fn classify_dp_id(id: &str) -> DpTarget {
    if id.len() == 11 && id.chars().all(|c| c.is_ascii_digit()) {
        DpTarget::Marktlokation
    } else if id.len() == 33 && id.starts_with("DE") {
        DpTarget::Messlokation
    } else {
        DpTarget::Marktlokation
    }
}

/// Rebuild the marktteilnehmer DP entries from routed lokation entities,
/// using side-channel metadata captured during forward routing. Inverse of
/// [`route_nad_dp_to_lokation`].
pub fn unroute_lokation_to_nad_dp(
    entities: &mut serde_json::Map<String, serde_json::Value>,
    dp_routing: &DpRouting,
) {
    if dp_routing.is_empty() {
        return;
    }

    for (key, meta_list) in dp_routing {
        if meta_list.is_empty() {
            continue;
        }
        let lokation_value = match entities.remove(key) {
            Some(v) => v,
            None => continue,
        };
        let lokation_objs: Vec<serde_json::Value> = match lokation_value {
            serde_json::Value::Array(arr) => arr,
            other => vec![other],
        };
        if lokation_objs.len() != meta_list.len() {
            let restored = if lokation_objs.len() == 1 {
                lokation_objs.into_iter().next().unwrap()
            } else {
                serde_json::Value::Array(lokation_objs)
            };
            entities.insert(key.clone(), restored);
            continue;
        }

        let mut indexed: Vec<(usize, serde_json::Value)> = lokation_objs
            .into_iter()
            .zip(meta_list.iter())
            .map(|(lokation, meta)| {
                let idx = meta
                    .get("originalIdx")
                    .and_then(|n| n.as_u64())
                    .map(|n| n as usize)
                    .unwrap_or(usize::MAX);
                (idx, rebuild_dp_marktteilnehmer(lokation, meta))
            })
            .collect();
        indexed.sort_by_key(|(idx, _)| *idx);

        let mts = entities
            .entry("marktteilnehmer".to_string())
            .or_insert_with(|| serde_json::Value::Array(Vec::new()));
        if !mts.is_array() {
            let single = std::mem::replace(mts, serde_json::Value::Array(Vec::new()));
            if let serde_json::Value::Array(a) = mts {
                a.push(single);
            }
        }
        if let serde_json::Value::Array(arr) = mts {
            for (idx, dp) in indexed {
                let pos = idx.min(arr.len());
                arr.insert(pos, dp);
            }
        }
    }
}

fn rebuild_dp_marktteilnehmer(
    lokation: serde_json::Value,
    metadata: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
    let mut obj = match lokation {
        serde_json::Value::Object(m) => m,
        other => return other,
    };

    let mut out = serde_json::Map::new();
    out.insert(
        "boTyp".to_string(),
        serde_json::Value::String("MARKTTEILNEHMER".to_string()),
    );

    if let Some(v) = obj
        .remove("marktlokationsId")
        .or_else(|| obj.remove("messlokationsId"))
    {
        out.insert("meldepunktId".to_string(), v);
    }

    if let Some(serde_json::Value::Object(adresse)) = obj.remove("adresse") {
        for (k, v) in adresse {
            let target = match k.as_str() {
                "strasse" => "strasse",
                "strasse2" => "strasse2",
                "hausnummer" => "hausnummer",
                "strasse4" => "strasse4",
                "postleitzahl" => "postleitzahl",
                "ort" => "ort",
                "land" => "land",
                "adresszusatz" => "zusatzinfo",
                other => other,
            };
            out.insert(target.to_string(), v);
        }
    }

    for (k, v) in metadata.iter() {
        if k == "originalIdx" {
            continue;
        }
        out.entry(k.clone()).or_insert_with(|| v.clone());
    }

    serde_json::Value::Object(out)
}