use std::path::PathBuf;
use edifact_mapper::MapperError;
use edifact_mapper::{DataDir, EdifactParty, InterchangeEnvelope, InterchangeMessage, Mapper};
type Dynamic = mig_bo4e::model::Interchange<serde_json::Value, serde_json::Value>;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("crate is two levels under the workspace root")
.to_path_buf()
}
fn setup(pid: &str) -> Option<(Mapper, String)> {
let root = repo_root();
let fixture = root.join(format!("fixtures/generated/fv2604/utilmd/{pid}.edi"));
if !root.join("dist/edifact-data-FV2604.bin").exists() || !fixture.exists() {
eprintln!("skipping: regenerate the FV2604 bundle + fixture first");
return None;
}
let mapper = Mapper::from_data_dir(DataDir::path(root.join("dist"))).expect("load bundle");
let edifact = std::fs::read_to_string(&fixture).expect("read fixture");
Some((mapper, edifact))
}
fn render(mapper: &Mapper, ic: &Dynamic, pid: &str) -> Result<String, MapperError> {
let msg = &ic.nachrichten[0];
mapper.to_edifact_interchange(
&InterchangeEnvelope {
sender: EdifactParty::bdew("9900000000001"),
receiver: EdifactParty::bdew("9900000000002"),
interchange_ref: "1".to_string(),
},
&[InterchangeMessage {
message_ref: "1".to_string(),
msg_stammdaten: msg.stammdaten.clone(),
tx_stammdaten: msg.transaktionen.clone(),
fv: "FV2604".to_string(),
variant: "UTILMD_Strom".to_string(),
pid: pid.to_string(),
}],
)
}
fn segments(edifact: &str) -> Vec<&str> {
edifact.split('\'').map(str::trim).collect()
}
fn entities(tx: &serde_json::Value) -> &serde_json::Value {
tx.get("stammdaten").unwrap_or(tx)
}
fn entities_mut(tx: &mut serde_json::Value) -> &mut serde_json::Value {
if tx.get("stammdaten").is_some() {
&mut tx["stammdaten"]
} else {
tx
}
}
fn geschaeftspartner_count(tx: &serde_json::Value) -> usize {
match tx.get("geschaeftspartner") {
Some(serde_json::Value::Array(a)) => a.len(),
Some(_) => 1,
None => 0,
}
}
#[test]
fn zaehler_without_zaehlertyp_still_renders_its_cci_55042() {
let Some((mapper, edifact)) = setup("55042") else {
return;
};
let mut ic: Dynamic = mapper
.from_edifact(&edifact, "FV2604", "UTILMD_Strom", "55042")
.expect("parse fixture");
let tx = entities_mut(&mut ic.nachrichten[0].transaktionen[0]);
assert_eq!(tx["zaehler"]["zaehlertypMerkmal"], "zaehlertyp");
let geraete_nummer = tx["zaehler"]["geraeteNummer"]
.as_str()
.expect("zaehler.geraeteNummer")
.to_string();
assert!(!geraete_nummer.is_empty());
assert_eq!(geschaeftspartner_count(tx), 4);
tx["zaehler"]
.as_object_mut()
.unwrap()
.remove("zaehlertypMerkmal");
let rendered = render(&mapper, &ic, "55042").expect("render without zaehlertypMerkmal");
let segs = segments(&rendered);
let seq = segs.iter().position(|s| *s == "SEQ+Z03").expect("SEQ+Z03");
assert_eq!(
&segs[seq..seq + 3],
&[
"SEQ+Z03",
"CCI+++E13",
format!("CAV+Z30:::{geraete_nummer}").as_str()
],
"SG10 must open with its CCI:\n{rendered}"
);
let (reparsed, diagnostics): (Dynamic, _) = mapper
.from_edifact_with_diagnostics(&rendered, "FV2604", "UTILMD_Strom", "55042")
.expect("reparse");
assert!(diagnostics.is_empty(), "{diagnostics:?}");
let tx = entities(&reparsed.nachrichten[0].transaktionen[0]);
assert_eq!(tx["zaehler"]["geraeteNummer"], geraete_nummer);
assert_eq!(tx["zaehler"]["zaehlertypMerkmal"], "zaehlertyp");
assert_eq!(geschaeftspartner_count(tx), 4);
}
#[test]
fn group_without_entry_segment_fields_is_refused_55071() {
let Some((mapper, edifact)) = setup("55071") else {
return;
};
let mut ic: Dynamic = mapper
.from_edifact(&edifact, "FV2604", "UTILMD_Strom", "55071")
.expect("parse fixture");
render(&mapper, &ic, "55071").expect("the unmodified fixture renders");
let tx = entities_mut(&mut ic.nachrichten[0].transaktionen[0]);
let with_cav = tx["summenzeitreihenDaten"]
.as_array_mut()
.unwrap()
.iter_mut()
.find(|m| m["merkmal"].get("qualifier").is_some())
.expect("a SummenzeitreihenDaten Merkmal with CAV qualifier");
let merkmal = with_cav["merkmal"].as_object_mut().unwrap();
merkmal.remove("code");
merkmal.remove("klasse");
let err = render(&mapper, &ic, "55071").expect_err("CAV without CCI must be refused");
match &err {
MapperError::MissingGroupEntrySegment(details) => {
assert_eq!(details.pid, "55071");
assert_eq!(details.group_path, "SG4.SG8.SG10");
assert_eq!(details.entry_segment, "CCI");
assert_eq!(details.present_segments, ["CAV".to_string()]);
assert!(
details
.entities
.contains(&"SummenzeitreihenDaten".to_string()),
"{err}"
);
assert!(
details
.entry_fields
.contains(&"SummenzeitreihenDaten.merkmal.code".to_string()),
"{err}"
);
}
other => panic!("expected MissingGroupEntrySegment, got {other:?}"),
}
let message = err.to_string();
assert!(message.contains("'CCI'"), "{message}");
assert!(
message.contains("SummenzeitreihenDaten.merkmal.code"),
"{message}"
);
let msg = &ic.nachrichten[0];
mapper
.validate_bo4e(
&msg.stammdaten,
&msg.transaktionen,
"FV2604",
"UTILMD_Strom",
"55071",
None,
automapper_validation::ValidationLevel::Full,
)
.expect("validate_bo4e reports findings rather than failing");
}
#[test]
fn cav_without_cci_is_reported_as_orphaned_on_parse_55042() {
let Some((mapper, edifact)) = setup("55042") else {
return;
};
assert!(edifact.contains("CCI+++E13'"));
let broken = edifact.replace("CCI+++E13'", "");
let (ic, diagnostics): (Dynamic, _) = mapper
.from_edifact_with_diagnostics(&broken, "FV2604", "UTILMD_Strom", "55042")
.expect("a CCI-less SG10 must not fail the whole conversion");
let orphaned: Vec<_> = diagnostics
.iter()
.filter(|d| d.kind == mig_assembly::StructureDiagnosticKind::OrphanedGroupSegment)
.collect();
assert_eq!(orphaned.len(), 1, "{diagnostics:?}");
assert_eq!(orphaned[0].segment_id, "CAV");
assert!(
orphaned[0].message.contains("SG10") && orphaned[0].message.contains("'CCI'"),
"{}",
orphaned[0].message
);
let tx = entities(&ic.nachrichten[0].transaktionen[0]);
assert!(tx["zaehler"].get("geraeteNummer").is_none());
assert_eq!(
geschaeftspartner_count(tx),
4,
"NADs after the broken SG10 must survive"
);
}