use mako_engine::outbox::PendingOutbox;
use mako_engine::types::{MaLo, MarktpartnerCode};
pub use mako_engine::lf_vorgang::LfVorgangsdaten;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LfAntwort {
pub antwort_code: String,
pub ebd: Option<String>,
pub zustimmung: bool,
pub bemerkung: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bilanzkreis: Option<String>,
pub termin: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub malo_art: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub geplantes_produktpaket: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub malo_msb: Option<ZugeordneterMsb>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub messlokationen: Vec<AntwortMesslokation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AntwortForm {
pub document_code: &'static str,
pub ergaenzung: Option<&'static str>,
pub klassifiziert_malo: bool,
pub lokationsdaten: bool,
}
impl AntwortForm {
#[must_use]
pub const fn of(pid: u32) -> Option<Self> {
let (document_code, ergaenzung, klassifiziert_malo, lokationsdaten) = match pid {
55002 => ("E01", None, true, true),
55003 => ("E01", Some("ZW4"), false, false),
55005 | 55006 => ("E02", None, false, false),
55078 => ("E01", None, false, true),
55080 => ("E01", Some("ZW3"), false, false),
_ => return None,
};
Some(Self {
document_code,
ergaenzung,
klassifiziert_malo,
lokationsdaten,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ZugeordneterMsb {
pub mp_id: String,
pub rolle: String,
pub grundlage: String,
pub gmsb_mp_id: String,
}
impl ZugeordneterMsb {
#[must_use]
pub fn grundzustaendig(mp_id: impl Into<String>) -> Self {
let mp_id = mp_id.into();
Self {
rolle: "Z39".to_owned(),
grundlage: "Z19".to_owned(),
gmsb_mp_id: mp_id.clone(),
mp_id,
}
}
#[must_use]
pub fn wettbewerblich(mp_id: impl Into<String>, gmsb_mp_id: impl Into<String>) -> Self {
Self {
mp_id: mp_id.into(),
rolle: "Z40".to_owned(),
grundlage: "Z19".to_owned(),
gmsb_mp_id: gmsb_mp_id.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AntwortMesslokation {
pub melo_id: String,
pub msb: ZugeordneterMsb,
}
impl LfAntwort {
#[must_use]
pub fn zustimmung(antwort_code: impl Into<String>, ebd: impl Into<String>) -> Self {
Self {
antwort_code: antwort_code.into(),
ebd: Some(ebd.into()),
zustimmung: true,
bemerkung: None,
bilanzkreis: None,
termin: None,
malo_art: None,
geplantes_produktpaket: None,
malo_msb: None,
messlokationen: Vec::new(),
}
}
#[must_use]
pub fn ablehnung(antwort_code: impl Into<String>, ebd: impl Into<String>) -> Self {
Self {
antwort_code: antwort_code.into(),
ebd: Some(ebd.into()),
zustimmung: false,
bemerkung: None,
bilanzkreis: None,
termin: None,
malo_art: None,
geplantes_produktpaket: None,
malo_msb: None,
messlokationen: Vec::new(),
}
}
#[must_use]
pub fn with_klassifizierung(mut self, malo_art: impl Into<String>) -> Self {
self.malo_art = Some(malo_art.into());
self
}
#[must_use]
pub fn with_produktpaket(mut self, paket_id: impl Into<String>) -> Self {
self.geplantes_produktpaket = Some(paket_id.into());
self
}
#[must_use]
pub fn with_messlokation(mut self, melo_id: impl Into<String>, msb: ZugeordneterMsb) -> Self {
if self.malo_msb.is_none() {
self.malo_msb = Some(msb.clone());
}
self.messlokationen.push(AntwortMesslokation {
melo_id: melo_id.into(),
msb,
});
self
}
#[must_use]
pub fn with_bemerkung(mut self, text: impl Into<String>) -> Self {
self.bemerkung = Some(text.into());
self
}
#[must_use]
pub fn with_bilanzkreis(mut self, bk: impl Into<String>) -> Self {
self.bilanzkreis = Some(bk.into());
self
}
#[must_use]
pub fn with_termin(mut self, yyyymmdd: impl Into<String>) -> Self {
self.termin = Some(yyyymmdd.into());
self
}
}
#[must_use]
pub fn antwort_outbox(
response_pid: u32,
antwort: &LfAntwort,
location_id: &MaLo,
sender: &MarktpartnerCode,
receiver: &MarktpartnerCode,
process_date: &str,
referenz_vorgangsnummer: Option<&str>,
) -> PendingOutbox {
let mut payload = serde_json::json!({
"pid": response_pid,
"sender": receiver.as_str(),
"receiver": sender.as_str(),
"malo": location_id.as_str(),
"process_date": antwort.termin.as_deref().unwrap_or(process_date),
"antwort_code": antwort.antwort_code,
});
if let Some(ebd) = &antwort.ebd {
payload["antwort_codeliste"] = serde_json::Value::String(ebd.clone());
}
if let Some(bemerkung) = &antwort.bemerkung {
payload["bemerkung"] = serde_json::Value::String(bemerkung.clone());
}
if let Some(bilanzkreis) = &antwort.bilanzkreis {
payload["bilanzkreis"] = serde_json::Value::String(bilanzkreis.clone());
}
if let Some(referenz) = referenz_vorgangsnummer {
payload["referenz_vorgangsnummer"] = serde_json::Value::String(referenz.to_owned());
}
PendingOutbox::new("UTILMD", sender.as_str(), payload)
}
#[cfg(test)]
mod tests {
use super::*;
fn outbox_for(antwort: &LfAntwort) -> serde_json::Value {
antwort_outbox(
55_009,
antwort,
&MaLo::new("51238696012"),
&MarktpartnerCode::new("9900357000004"),
&MarktpartnerCode::new("9900000000001"),
"20260901",
Some("NNV1234"),
)
.payload
.clone()
}
#[test]
fn the_answer_back_references_the_requests_vorgangsnummer() {
let p = outbox_for(&LfAntwort::zustimmung("A10", "E_0609"));
assert_eq!(p["referenz_vorgangsnummer"], "NNV1234");
assert_ne!(
p.get("vorgangsnummer").and_then(serde_json::Value::as_str),
Some("NNV1234"),
"IDE+24 DE 7402 must stay globally unique (UTILMD MIG S2.2, Hinweis zu DE7402)"
);
}
#[test]
fn the_answer_travels_back_to_the_requester() {
let p = outbox_for(&LfAntwort::ablehnung("A35", "E_0624"));
assert_eq!(p["sender"], "9900000000001");
assert_eq!(p["receiver"], "9900357000004");
}
#[test]
fn the_antwortcode_and_its_ebd_are_both_carried() {
let p = outbox_for(&LfAntwort::ablehnung("A35", "E_0624"));
assert_eq!(p["antwort_code"], "A35");
assert_eq!(p["antwort_codeliste"], "E_0624");
}
#[test]
fn a_gas_answer_omits_the_ebd_reference() {
let antwort = LfAntwort {
antwort_code: "E15".to_owned(),
ebd: None,
zustimmung: true,
bemerkung: None,
bilanzkreis: None,
termin: None,
..LfAntwort::ablehnung("", "")
};
assert!(outbox_for(&antwort).get("antwort_codeliste").is_none());
}
#[test]
fn a_stated_termin_replaces_the_requested_date() {
let p = outbox_for(&LfAntwort::zustimmung("A34", "E_0624").with_termin("20260831"));
assert_eq!(p["process_date"], "20260831");
}
#[test]
fn an_answer_without_its_own_date_echoes_the_request() {
let p = outbox_for(&LfAntwort::zustimmung("A10", "E_0609"));
assert_eq!(p["process_date"], "20260901");
}
#[test]
fn a_bemerkung_is_carried() {
let p =
outbox_for(&LfAntwort::ablehnung("A99", "E_0609").with_bemerkung("Zählerstand fehlt"));
assert_eq!(p["bemerkung"], "Zählerstand fehlt");
}
}