use mako_engine::{
error::WorkflowError,
outbox::PendingOutbox,
types::{BillingPeriod, MarktpartnerCode, MessageRef, Pruefidentifikator},
workflow::{CommandPayload, EventPayload, Workflow, WorkflowOutput},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ZpVorgang {
Aktivierung,
Deaktivierung,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ZpSerie {
NetzzeitreiheNachbarNb,
NetzzeitreiheBiko,
LieferantensummenzeitreiheNb,
LieferantensummenzeitreiheUenb,
Bilanzierungsgebietssummenzeitreihe,
BilanzkreissummenzeitreiheNb,
BilanzkreissummenzeitreiheUenb,
Deltazeitreihenuebertrag,
Abrechnungssummenzeitreihe,
TaeglicheBgSzr,
TaeglicheBkSzr,
Zuordnungsermaechtigung,
TaeglicheAauez,
LfAaszr,
MonatlicheAauezBkvLf,
MonatlicheAauezBkvAnfNb,
NetzgangzeitreiheNzr,
}
pub const TAEGLICHE_AAUEZ_ENDE: time::Date = crate::zeitreihen::KAPITEL_17_2_ENDE;
impl ZpSerie {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::NetzzeitreiheNachbarNb => "Netzzeitreihe (NB → benachbarter NB)",
Self::NetzzeitreiheBiko => "Netzzeitreihe (NB → BIKO)",
Self::LieferantensummenzeitreiheNb => "Lieferantensummenzeitreihe (NB → LF)",
Self::LieferantensummenzeitreiheUenb => "Lieferantensummenzeitreihe (ÜNB → LF)",
Self::Bilanzierungsgebietssummenzeitreihe => "Bilanzierungsgebietssummenzeitreihe",
Self::BilanzkreissummenzeitreiheNb => "Bilanzkreissummenzeitreihe (NB → BIKO)",
Self::BilanzkreissummenzeitreiheUenb => "Bilanzkreissummenzeitreihe (ÜNB → BIKO)",
Self::Deltazeitreihenuebertrag => "Deltazeitreihenübertrag",
Self::Abrechnungssummenzeitreihe => "Abrechnungssummenzeitreihe",
Self::TaeglicheBgSzr => "tägliche Bilanzierungsgebietssummenzeitreihe",
Self::TaeglicheBkSzr => "tägliche Bilanzkreissummenzeitreihe",
Self::Zuordnungsermaechtigung => "Zuordnungsermächtigung",
Self::TaeglicheAauez => "tägliche AAÜZ",
Self::LfAaszr => "LF-AASZR",
Self::MonatlicheAauezBkvLf => "monatliche AAÜZ (BKV des LF)",
Self::MonatlicheAauezBkvAnfNb => "monatliche AAÜZ (BKV des anfordernden NB)",
Self::NetzgangzeitreiheNzr => "Zuordnung ZP der NGZ zur NZR",
}
}
#[must_use]
pub fn endet_am(self) -> Option<time::Date> {
match self {
Self::TaeglicheAauez => Some(TAEGLICHE_AAUEZ_ENDE),
_ => None,
}
}
#[must_use]
pub fn gilt_am(self, date: time::Date) -> bool {
self.endet_am().is_none_or(|ende| date <= ende)
}
#[must_use]
pub fn from_wire(cav: &str, verantwortlicher: &str) -> Option<Self> {
use crate::zeitreihen::{Aggregationsebene as E, Familie as F, Kategorie as K, Rolle};
let (zeitreihe, ebene) = crate::zeitreihen::zeitreihe_aus_cav(cav)?;
let rolle = crate::zeitreihen::rolle_aus_cci(verantwortlicher)?;
Some(
match (zeitreihe.familie(), zeitreihe.kategorie(), ebene, rolle) {
(F::Nzr, _, _, Rolle::Nb) => {
Self::NetzzeitreiheBiko
}
(F::LfSzr, Some(K::A), _, _) => Self::LieferantensummenzeitreiheNb,
(F::LfSzr, Some(K::B), _, _) => Self::LieferantensummenzeitreiheUenb,
(F::BgSzr, Some(K::B), _, _) => Self::Bilanzierungsgebietssummenzeitreihe,
(F::BgSzr, Some(K::C), _, _) => Self::TaeglicheBgSzr,
(F::BkSzr, Some(K::A), _, _) => Self::BilanzkreissummenzeitreiheNb,
(F::BkSzr, Some(K::B), Some(E::Bilanzierungsgebiet), _) => {
Self::BilanzkreissummenzeitreiheUenb
}
(F::BkSzr, Some(K::C), _, _) => Self::TaeglicheBkSzr,
(F::Dzue, _, _, _) => Self::Deltazeitreihenuebertrag,
(F::Abrechnungssummenzeitreihe, _, _, _) => Self::Abrechnungssummenzeitreihe,
_ => return None,
},
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ZpFamilie {
pub serie: ZpSerie,
pub vorgang: ZpVorgang,
pub anfrage: u32,
pub antwort: Option<u32>,
pub antwort_ebd: Option<&'static str>,
pub weiterleitung: Option<u32>,
}
pub const ZP_FAMILIEN: &[ZpFamilie] = &[
ZpFamilie {
serie: ZpSerie::NetzzeitreiheNachbarNb,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: Some(55064),
antwort_ebd: Some("E_0020"),
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::NetzzeitreiheNachbarNb,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: Some(55064),
antwort_ebd: Some("E_0010"),
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::NetzzeitreiheBiko,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: Some(55064),
antwort_ebd: Some("E_0024"),
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::NetzzeitreiheBiko,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: Some(55064),
antwort_ebd: Some("E_0009"),
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::LieferantensummenzeitreiheNb,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::LieferantensummenzeitreiheNb,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::LieferantensummenzeitreiheUenb,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::LieferantensummenzeitreiheUenb,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::Bilanzierungsgebietssummenzeitreihe,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: Some(55064),
antwort_ebd: Some("E_0015"),
weiterleitung: Some(55062),
},
ZpFamilie {
serie: ZpSerie::Bilanzierungsgebietssummenzeitreihe,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: Some(55064),
antwort_ebd: Some("E_0035"),
weiterleitung: Some(55063),
},
ZpFamilie {
serie: ZpSerie::BilanzkreissummenzeitreiheNb,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: Some(55064),
antwort_ebd: Some("E_0034"),
weiterleitung: Some(55062),
},
ZpFamilie {
serie: ZpSerie::BilanzkreissummenzeitreiheNb,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: Some(55064),
antwort_ebd: Some("E_0018"),
weiterleitung: Some(55063),
},
ZpFamilie {
serie: ZpSerie::BilanzkreissummenzeitreiheUenb,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: Some(55064),
antwort_ebd: Some("E_0011"),
weiterleitung: Some(55062),
},
ZpFamilie {
serie: ZpSerie::BilanzkreissummenzeitreiheUenb,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: Some(55064),
antwort_ebd: Some("E_0012"),
weiterleitung: Some(55063),
},
ZpFamilie {
serie: ZpSerie::Deltazeitreihenuebertrag,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: Some(55064),
antwort_ebd: Some("E_0027"),
weiterleitung: Some(55062),
},
ZpFamilie {
serie: ZpSerie::Deltazeitreihenuebertrag,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: Some(55064),
antwort_ebd: Some("E_0028"),
weiterleitung: Some(55063),
},
ZpFamilie {
serie: ZpSerie::Abrechnungssummenzeitreihe,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::Abrechnungssummenzeitreihe,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::TaeglicheBgSzr,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::TaeglicheBgSzr,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::TaeglicheBkSzr,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55062,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::TaeglicheBkSzr,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55063,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::Zuordnungsermaechtigung,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55071,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::Zuordnungsermaechtigung,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55072,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::TaeglicheAauez,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55197,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::TaeglicheAauez,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55198,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::LfAaszr,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55199,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::LfAaszr,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55200,
antwort: None,
antwort_ebd: None,
weiterleitung: None,
},
ZpFamilie {
serie: ZpSerie::MonatlicheAauezBkvLf,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55203,
antwort: Some(55204),
antwort_ebd: Some("E_0071"),
weiterleitung: Some(55205),
},
ZpFamilie {
serie: ZpSerie::MonatlicheAauezBkvLf,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55206,
antwort: Some(55207),
antwort_ebd: Some("E_0072"),
weiterleitung: Some(55208),
},
ZpFamilie {
serie: ZpSerie::MonatlicheAauezBkvAnfNb,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55209,
antwort: Some(55210),
antwort_ebd: Some("E_0078"),
weiterleitung: Some(55211),
},
ZpFamilie {
serie: ZpSerie::MonatlicheAauezBkvAnfNb,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55212,
antwort: Some(55213),
antwort_ebd: Some("E_0079"),
weiterleitung: Some(55214),
},
ZpFamilie {
serie: ZpSerie::NetzgangzeitreiheNzr,
vorgang: ZpVorgang::Aktivierung,
anfrage: 55235,
antwort: Some(55237),
antwort_ebd: Some("E_0102"),
weiterleitung: Some(55235),
},
ZpFamilie {
serie: ZpSerie::NetzgangzeitreiheNzr,
vorgang: ZpVorgang::Deaktivierung,
anfrage: 55236,
antwort: Some(55237),
antwort_ebd: Some("E_0103"),
weiterleitung: Some(55236),
},
];
#[must_use]
pub fn familie_for(serie: ZpSerie, vorgang: ZpVorgang) -> Option<&'static ZpFamilie> {
ZP_FAMILIEN
.iter()
.find(|f| f.serie == serie && f.vorgang == vorgang)
}
#[must_use]
pub fn serien_fuer_pid(anfrage: u32) -> Vec<ZpSerie> {
ZP_FAMILIEN
.iter()
.filter(|f| f.anfrage == anfrage)
.map(|f| f.serie)
.collect()
}
#[must_use]
pub fn ist_antwort_pid(pid: u32) -> bool {
ZP_FAMILIEN.iter().any(|f| f.antwort == Some(pid))
}
#[must_use]
pub fn ist_weiterleitung_pid(pid: u32) -> bool {
ZP_FAMILIEN.iter().any(|f| f.weiterleitung == Some(pid))
}
#[must_use]
pub fn all_pids() -> Vec<u32> {
let mut v: Vec<u32> = ZP_FAMILIEN
.iter()
.flat_map(|f| [Some(f.anfrage), f.antwort, f.weiterleitung])
.flatten()
.collect();
v.sort_unstable();
v.dedup();
v
}
#[must_use]
pub fn antwort_ist_zustimmung(ebd: &str, code: &str) -> Option<bool> {
mako_pruefung::mabis::codes::lookup(ebd, code).and_then(|c| c.ist_zustimmung())
}
pub const WORKFLOW_NAME: &str = "mabis-zp-lifecycle";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ZpLifecycleData {
pub pruefidentifikator: Pruefidentifikator,
pub vorgang: ZpVorgang,
pub serie: ZpSerie,
pub mabis_zp_id: String,
pub sender: MarktpartnerCode,
pub receiver: MarktpartnerCode,
pub billing_period: BillingPeriod,
pub document_date: String,
pub message_ref: MessageRef,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum ZpLifecycleEvent {
AnfrageErhalten {
pruefidentifikator: Pruefidentifikator,
vorgang: ZpVorgang,
serie: ZpSerie,
mabis_zp_id: String,
sender: MarktpartnerCode,
receiver: MarktpartnerCode,
billing_period: BillingPeriod,
document_date: String,
message_ref: MessageRef,
},
AnfrageGesendet {
pruefidentifikator: Pruefidentifikator,
vorgang: ZpVorgang,
serie: ZpSerie,
mabis_zp_id: crate::MabisZaehlpunktId,
sender: MarktpartnerCode,
empfaenger: MarktpartnerCode,
billing_period: BillingPeriod,
document_date: String,
message_ref: MessageRef,
},
AntwortErhalten {
antwort_pid: Pruefidentifikator,
ebd: String,
bestaetigt: bool,
grund: Option<String>,
message_ref: MessageRef,
},
Erfasst {
message_ref: MessageRef,
},
AntwortGesendet {
antwort_pid: Pruefidentifikator,
ebd: String,
bestaetigt: bool,
grund: Option<String>,
},
WeiterleitungGesendet {
weiterleitung_pid: Pruefidentifikator,
empfaenger: MarktpartnerCode,
},
ValidationFailed {
reason: String,
},
}
impl EventPayload for ZpLifecycleEvent {
fn event_type(&self) -> &'static str {
match self {
Self::AnfrageErhalten { .. } => "MabisZpAnfrageErhalten",
Self::AnfrageGesendet { .. } => "MabisZpAnfrageGesendet",
Self::AntwortErhalten { .. } => "MabisZpAntwortErhalten",
Self::Erfasst { .. } => "MabisZpErfasst",
Self::AntwortGesendet { .. } => "MabisZpAntwortGesendet",
Self::WeiterleitungGesendet { .. } => "MabisZpWeiterleitungGesendet",
Self::ValidationFailed { .. } => "MabisZpValidationFailed",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(tag = "status", content = "data")]
pub enum ZpLifecycleState {
#[default]
New,
AnfrageErhalten(Box<ZpLifecycleData>),
AnfrageGesendet(Box<ZpLifecycleData>),
AntwortBestaetigt(Box<ZpLifecycleData>),
Erfasst(Box<ZpLifecycleData>),
Bestaetigt(Box<ZpLifecycleData>),
Abgelehnt {
grund: String,
},
Weitergeleitet(Box<ZpLifecycleData>),
ValidationFailed {
reason: String,
},
}
impl ZpLifecycleState {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::New => "New",
Self::AnfrageErhalten(_) => "AnfrageErhalten",
Self::AnfrageGesendet(_) => "AnfrageGesendet",
Self::AntwortBestaetigt(_) => "AntwortBestaetigt",
Self::Erfasst(_) => "Erfasst",
Self::Bestaetigt(_) => "Bestaetigt",
Self::Abgelehnt { .. } => "Abgelehnt",
Self::Weitergeleitet(_) => "Weitergeleitet",
Self::ValidationFailed { .. } => "ValidationFailed",
}
}
#[must_use]
pub fn data(&self) -> Option<&ZpLifecycleData> {
match self {
Self::AnfrageErhalten(d)
| Self::AnfrageGesendet(d)
| Self::AntwortBestaetigt(d)
| Self::Erfasst(d)
| Self::Bestaetigt(d)
| Self::Weitergeleitet(d) => Some(d),
Self::New | Self::Abgelehnt { .. } | Self::ValidationFailed { .. } => None,
}
}
}
#[derive(Clone)]
pub enum ZpLifecycleCommand {
ReceiveAnfrage {
pid: Pruefidentifikator,
serie: ZpSerie,
vorgang: ZpVorgang,
mabis_zp_id: String,
sender: MarktpartnerCode,
receiver: MarktpartnerCode,
billing_period: BillingPeriod,
document_date: String,
message_ref: MessageRef,
validation_passed: bool,
validation_errors: Vec<String>,
},
SendAnfrage {
serie: ZpSerie,
vorgang: ZpVorgang,
mabis_zp_id: crate::MabisZaehlpunktId,
sender: MarktpartnerCode,
empfaenger: MarktpartnerCode,
billing_period: BillingPeriod,
document_date: String,
message_ref: MessageRef,
},
ReceiveAntwort {
pid: Pruefidentifikator,
bestaetigt: bool,
grund: Option<String>,
ebd: String,
message_ref: MessageRef,
},
SendAntwort {
bestaetigt: bool,
grund: Option<String>,
},
SendWeiterleitung {
empfaenger: MarktpartnerCode,
},
}
impl CommandPayload for ZpLifecycleCommand {}
pub struct MabisZpLifecycleWorkflow;
impl Workflow for MabisZpLifecycleWorkflow {
type State = ZpLifecycleState;
type Event = ZpLifecycleEvent;
type Command = ZpLifecycleCommand;
fn apply(state: Self::State, event: &Self::Event) -> Self::State {
match event {
ZpLifecycleEvent::AnfrageErhalten {
pruefidentifikator,
vorgang,
serie,
mabis_zp_id,
sender,
receiver,
billing_period,
document_date,
message_ref,
} => ZpLifecycleState::AnfrageErhalten(Box::new(ZpLifecycleData {
pruefidentifikator: *pruefidentifikator,
vorgang: *vorgang,
serie: *serie,
mabis_zp_id: mabis_zp_id.clone(),
sender: sender.clone(),
receiver: receiver.clone(),
billing_period: billing_period.clone(),
document_date: document_date.clone(),
message_ref: message_ref.clone(),
})),
ZpLifecycleEvent::AnfrageGesendet {
pruefidentifikator,
vorgang,
serie,
mabis_zp_id,
sender,
empfaenger,
billing_period,
document_date,
message_ref,
} => ZpLifecycleState::AnfrageGesendet(Box::new(ZpLifecycleData {
pruefidentifikator: *pruefidentifikator,
vorgang: *vorgang,
serie: *serie,
mabis_zp_id: mabis_zp_id.as_str().to_owned(),
sender: sender.clone(),
receiver: empfaenger.clone(),
billing_period: billing_period.clone(),
document_date: document_date.clone(),
message_ref: message_ref.clone(),
})),
ZpLifecycleEvent::AntwortErhalten {
bestaetigt, grund, ..
} => match state {
ZpLifecycleState::AnfrageGesendet(d) => {
if *bestaetigt {
ZpLifecycleState::AntwortBestaetigt(d)
} else {
ZpLifecycleState::Abgelehnt {
grund: grund.clone().unwrap_or_default(),
}
}
}
other => other,
},
ZpLifecycleEvent::Erfasst { .. } => match state {
ZpLifecycleState::AnfrageErhalten(d) => ZpLifecycleState::Erfasst(d),
other => other,
},
ZpLifecycleEvent::AntwortGesendet {
bestaetigt, grund, ..
} => match state {
ZpLifecycleState::AnfrageErhalten(d) => {
if *bestaetigt {
ZpLifecycleState::Bestaetigt(d)
} else {
ZpLifecycleState::Abgelehnt {
grund: grund.clone().unwrap_or_default(),
}
}
}
other => other,
},
ZpLifecycleEvent::WeiterleitungGesendet { .. } => match state {
ZpLifecycleState::Bestaetigt(d) => ZpLifecycleState::Weitergeleitet(d),
other => other,
},
ZpLifecycleEvent::ValidationFailed { reason } => ZpLifecycleState::ValidationFailed {
reason: reason.clone(),
},
}
}
fn handle(
state: &Self::State,
command: Self::Command,
) -> Result<WorkflowOutput<Self::Event>, WorkflowError> {
match command {
ZpLifecycleCommand::ReceiveAnfrage {
pid,
serie,
vorgang,
mabis_zp_id,
sender,
receiver,
billing_period,
document_date,
message_ref,
validation_passed,
validation_errors,
} => {
if !matches!(state, ZpLifecycleState::New) {
return Ok(vec![].into());
}
let Some(familie) = familie_for(serie, vorgang) else {
return Err(WorkflowError::rejected(format!(
"{} kennt keinen Vorgang {vorgang:?}",
serie.label()
)));
};
if familie.anfrage != pid.as_u32() {
return Err(WorkflowError::rejected(format!(
"PID {pid} passt nicht zu {} / {vorgang:?} — erwartet {}",
serie.label(),
familie.anfrage
)));
}
if vorgang == ZpVorgang::Aktivierung
&& let Some(beginn) = abrechnungszeitraum_beginn(billing_period.as_str())
&& !serie.gilt_am(beginn)
{
return Err(WorkflowError::rejected(format!(
"{} endet am {} und kann für den Abrechnungszeitraum {} \
nicht mehr aktiviert werden",
serie.label(),
serie
.endet_am()
.expect("gilt_am was false, so there is an end date"),
billing_period.as_str()
)));
}
if !validation_passed {
return Ok(vec![ZpLifecycleEvent::ValidationFailed {
reason: validation_errors.join("; "),
}]
.into());
}
let erhalten = ZpLifecycleEvent::AnfrageErhalten {
pruefidentifikator: pid,
vorgang: familie.vorgang,
serie: familie.serie,
mabis_zp_id,
sender,
receiver,
billing_period,
document_date,
message_ref: message_ref.clone(),
};
if familie.antwort.is_none() {
return Ok(vec![erhalten, ZpLifecycleEvent::Erfasst { message_ref }].into());
}
Ok(vec![erhalten].into())
}
ZpLifecycleCommand::SendAnfrage {
serie,
vorgang,
mabis_zp_id,
sender,
empfaenger,
billing_period,
document_date,
message_ref,
} => {
if !matches!(state, ZpLifecycleState::New) {
return Ok(vec![].into());
}
let Some(familie) = familie_for(serie, vorgang) else {
return Err(WorkflowError::rejected(format!(
"{} kennt keinen Vorgang {vorgang:?}",
serie.label()
)));
};
if vorgang == ZpVorgang::Aktivierung
&& let Some(beginn) = abrechnungszeitraum_beginn(billing_period.as_str())
&& !serie.gilt_am(beginn)
{
return Err(WorkflowError::rejected(format!(
"{} endet am {} und kann für den Abrechnungszeitraum {} \
nicht mehr aktiviert werden",
serie.label(),
serie
.endet_am()
.expect("gilt_am was false, so there is an end date"),
billing_period.as_str()
)));
}
let pid = Pruefidentifikator::new(familie.anfrage).map_err(|e| {
WorkflowError::rejected(format!("invalid Anfrage PID {}: {e}", familie.anfrage))
})?;
let mut payload = serde_json::json!({
"pid": familie.anfrage,
"sender": sender.as_str(),
"receiver": empfaenger.as_str(),
"mabis_zaehlpunkt": mabis_zp_id.as_str(),
});
let datum_key = match vorgang {
ZpVorgang::Aktivierung => "bilanzierungsbeginn",
ZpVorgang::Deaktivierung => "bilanzierungsende",
};
payload[datum_key] = serde_json::Value::String(document_date.clone());
let outbox = PendingOutbox::new("UTILMD", empfaenger.as_str(), payload);
Ok(WorkflowOutput {
events: vec![ZpLifecycleEvent::AnfrageGesendet {
pruefidentifikator: pid,
vorgang: familie.vorgang,
serie: familie.serie,
mabis_zp_id,
sender,
empfaenger,
billing_period,
document_date,
message_ref,
}],
outbox: vec![outbox],
deadlines: vec![],
})
}
ZpLifecycleCommand::ReceiveAntwort {
pid,
bestaetigt,
grund,
ebd,
message_ref,
} => {
let data = match state {
ZpLifecycleState::AnfrageGesendet(data) => data,
ZpLifecycleState::AntwortBestaetigt(_) | ZpLifecycleState::Abgelehnt { .. } => {
return Ok(vec![].into());
}
other => {
return Err(WorkflowError::rejected(format!(
"ReceiveAntwort requires state AnfrageGesendet, got {}",
other.label()
)));
}
};
let familie = familie_for(data.serie, data.vorgang).ok_or_else(|| {
WorkflowError::rejected(format!(
"keine Familie für {} / {:?}",
data.serie.label(),
data.vorgang
))
})?;
let Some(antwort_pid_code) = familie.antwort else {
return Err(WorkflowError::rejected(format!(
"{} (Anfrage {}) definiert keine Antwort",
familie.serie.label(),
familie.anfrage
)));
};
if antwort_pid_code != pid.as_u32() {
return Err(WorkflowError::rejected(format!(
"Antwort-PID {pid} passt nicht zu {} / {:?} — erwartet {antwort_pid_code}",
familie.serie.label(),
familie.vorgang
)));
}
if !bestaetigt && grund.as_ref().is_none_or(|g| g.trim().is_empty()) {
return Err(WorkflowError::rejected(
"a rejecting Antwort requires a reason".to_owned(),
));
}
Ok(vec![ZpLifecycleEvent::AntwortErhalten {
antwort_pid: pid,
ebd,
bestaetigt,
grund,
message_ref,
}]
.into())
}
ZpLifecycleCommand::SendAntwort { bestaetigt, grund } => {
let ZpLifecycleState::AnfrageErhalten(data) = state else {
return Err(WorkflowError::rejected(format!(
"SendAntwort requires state AnfrageErhalten, got {}",
state.label()
)));
};
let familie = familie_for(data.serie, data.vorgang).ok_or_else(|| {
WorkflowError::rejected(format!(
"keine Familie für {} / {:?}",
data.serie.label(),
data.vorgang
))
})?;
let (Some(antwort_pid_code), Some(ebd)) = (familie.antwort, familie.antwort_ebd)
else {
return Err(WorkflowError::rejected(format!(
"{} (Anfrage {}) definiert keine Antwort",
familie.serie.label(),
familie.anfrage
)));
};
if !bestaetigt && grund.as_ref().is_none_or(|g| g.trim().is_empty()) {
return Err(WorkflowError::rejected(
"a rejecting Antwort requires a reason".to_owned(),
));
}
let antwort_pid = Pruefidentifikator::new(antwort_pid_code).map_err(|e| {
WorkflowError::rejected(format!("invalid Antwort PID {antwort_pid_code}: {e}"))
})?;
let mut payload = serde_json::json!({
"pid": antwort_pid_code,
"sender": data.receiver.as_str(),
"receiver": data.sender.as_str(),
"mabis_zaehlpunkt": data.mabis_zp_id,
"antwort_codeliste": ebd,
});
let datum_key = match data.vorgang {
ZpVorgang::Aktivierung => "bilanzierungsbeginn",
ZpVorgang::Deaktivierung => "bilanzierungsende",
};
payload[datum_key] = serde_json::Value::String(data.document_date.clone());
if let Some(ref text) = grund {
payload["bemerkung"] = serde_json::Value::String(text.clone());
}
let outbox = PendingOutbox::new("UTILMD", data.sender.as_str(), payload);
Ok(WorkflowOutput {
events: vec![ZpLifecycleEvent::AntwortGesendet {
antwort_pid,
ebd: ebd.to_owned(),
bestaetigt,
grund,
}],
outbox: vec![outbox],
deadlines: vec![],
})
}
ZpLifecycleCommand::SendWeiterleitung { empfaenger } => {
let ZpLifecycleState::Bestaetigt(data) = state else {
return Err(WorkflowError::rejected(format!(
"SendWeiterleitung requires state Bestaetigt, got {}",
state.label()
)));
};
let familie = familie_for(data.serie, data.vorgang).ok_or_else(|| {
WorkflowError::rejected(format!(
"keine Familie für {} / {:?}",
data.serie.label(),
data.vorgang
))
})?;
let Some(weiterleitung) = familie.weiterleitung else {
return Err(WorkflowError::rejected(format!(
"{} (Anfrage {}) definiert keine Weiterleitung",
familie.serie.label(),
familie.anfrage
)));
};
let weiterleitung_pid = Pruefidentifikator::new(weiterleitung).map_err(|e| {
WorkflowError::rejected(format!(
"invalid Weiterleitung PID {weiterleitung}: {e}"
))
})?;
let outbox = PendingOutbox::new(
"UTILMD",
empfaenger.as_str(),
serde_json::json!({
"pid": weiterleitung,
"sender": data.receiver.as_str(),
"receiver": empfaenger.as_str(),
"mabis_zaehlpunkt": data.mabis_zp_id,
"bilanzierungsbeginn": data.document_date,
}),
);
Ok(WorkflowOutput {
events: vec![ZpLifecycleEvent::WeiterleitungGesendet {
weiterleitung_pid,
empfaenger,
}],
outbox: vec![outbox],
deadlines: vec![],
})
}
}
}
}
fn abrechnungszeitraum_beginn(period: &str) -> Option<time::Date> {
let digits: String = period
.chars()
.filter(char::is_ascii_digit)
.take(6)
.collect();
if digits.len() != 6 {
return None;
}
let year: i32 = digits[..4].parse().ok()?;
let month = time::Month::try_from(digits[4..6].parse::<u8>().ok()?).ok()?;
time::Date::from_calendar_date(year, month, 1).ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn mp(s: &str) -> MarktpartnerCode {
MarktpartnerCode::new(s)
}
fn receive(serie: ZpSerie, vorgang: ZpVorgang) -> ZpLifecycleCommand {
let pid = familie_for(serie, vorgang).expect("in the table").anfrage;
receive_with_pid(serie, vorgang, pid)
}
fn receive_with_pid(serie: ZpSerie, vorgang: ZpVorgang, pid: u32) -> ZpLifecycleCommand {
ZpLifecycleCommand::ReceiveAnfrage {
pid: Pruefidentifikator::new(pid).expect("valid PID"),
serie,
vorgang,
mabis_zp_id: "DE0001112223334445556667778889990".to_owned(),
sender: mp("9900123456789"),
receiver: mp("9900987654321"),
billing_period: BillingPeriod::new("2026-07"),
document_date: "20260701".to_owned(),
message_ref: MessageRef::new("MSG-1"),
validation_passed: true,
validation_errors: vec![],
}
}
fn receive_for_period(serie: ZpSerie, vorgang: ZpVorgang, period: &str) -> ZpLifecycleCommand {
let mut cmd = receive(serie, vorgang);
if let ZpLifecycleCommand::ReceiveAnfrage {
ref mut billing_period,
..
} = cmd
{
*billing_period = BillingPeriod::new(period);
}
cmd
}
const ZP: &str = "DE0001112223334445556667778889990";
fn send(serie: ZpSerie, vorgang: ZpVorgang) -> ZpLifecycleCommand {
ZpLifecycleCommand::SendAnfrage {
serie,
vorgang,
mabis_zp_id: crate::MabisZaehlpunktId::new(ZP).expect("33 characters"),
sender: mp("9900987654321"),
empfaenger: mp("9900123456789"),
billing_period: BillingPeriod::new("2026-07"),
document_date: "20260701".to_owned(),
message_ref: MessageRef::new("MSG-OUT-1"),
}
}
fn gesendet(serie: ZpSerie, vorgang: ZpVorgang) -> ZpLifecycleState {
let out = MabisZpLifecycleWorkflow::handle(&ZpLifecycleState::New, send(serie, vorgang))
.expect("accepted");
fold(&out.events)
}
fn antwort(pid: u32, bestaetigt: bool, grund: Option<&str>) -> ZpLifecycleCommand {
ZpLifecycleCommand::ReceiveAntwort {
pid: Pruefidentifikator::new(pid).expect("valid PID"),
bestaetigt,
grund: grund.map(ToOwned::to_owned),
ebd: "E_0071".to_owned(),
message_ref: MessageRef::new("MSG-IN-1"),
}
}
fn fold(events: &[ZpLifecycleEvent]) -> ZpLifecycleState {
events.iter().fold(ZpLifecycleState::default(), |s, e| {
MabisZpLifecycleWorkflow::apply(s, e)
})
}
#[test]
fn every_series_has_exactly_one_row_per_vorgang() {
for f in ZP_FAMILIEN {
for vorgang in [ZpVorgang::Aktivierung, ZpVorgang::Deaktivierung] {
let rows = ZP_FAMILIEN
.iter()
.filter(|r| r.serie == f.serie && r.vorgang == vorgang)
.count();
assert_eq!(rows, 1, "{} / {vorgang:?}", f.serie.label());
}
}
}
#[test]
fn an_antwort_pid_always_comes_with_its_tree() {
for f in ZP_FAMILIEN {
assert_eq!(
f.antwort.is_some(),
f.antwort_ebd.is_some(),
"{} / {:?}",
f.serie.label(),
f.vorgang
);
}
}
#[test]
fn the_generic_codes_are_shared_by_eleven_series() {
let akt = serien_fuer_pid(55062);
let deakt = serien_fuer_pid(55063);
assert_eq!(akt.len(), 11, "55062 is shared: {akt:?}");
assert_eq!(deakt.len(), 11, "55063 is shared: {deakt:?}");
}
#[test]
fn the_shared_antwort_pid_reads_out_of_twelve_different_trees() {
let mut ebds: Vec<&str> = ZP_FAMILIEN
.iter()
.filter(|f| f.antwort == Some(55064))
.map(|f| f.antwort_ebd.expect("paired"))
.collect();
let total = ebds.len();
ebds.sort_unstable();
ebds.dedup();
assert_eq!(
total, 12,
"twelve (series, direction) pairs answer with 55064"
);
assert_eq!(ebds.len(), 12, "and no two of them share a tree: {ebds:?}");
}
#[test]
fn six_of_the_eleven_generic_series_answer_and_five_do_not() {
let generic = |with_antwort: bool| {
ZP_FAMILIEN
.iter()
.filter(|f| f.anfrage == 55062 && f.antwort.is_some() == with_antwort)
.count()
};
assert_eq!(
generic(true),
6,
"an implementation that never answers 55062 drops six obligations"
);
assert_eq!(
generic(false),
5,
"modelling 55062 → 55064 invents five obligations"
);
}
#[test]
fn the_generic_weiterleitung_re_uses_the_request_code() {
for f in ZP_FAMILIEN.iter().filter(|f| f.anfrage == 55062) {
if let Some(w) = f.weiterleitung {
assert_eq!(w, 55062, "{}", f.serie.label());
}
}
}
#[test]
fn all_pids_covers_anfragen_answers_and_weiterleitungen() {
let pids = all_pids();
for f in ZP_FAMILIEN {
assert!(pids.contains(&f.anfrage));
for p in [f.antwort, f.weiterleitung].into_iter().flatten() {
assert!(pids.contains(&p), "{p} missing from all_pids()");
}
}
let expected: Vec<u32> = vec![
55062, 55063, 55064, 55071, 55072, 55197, 55198, 55199, 55200, 55203, 55204, 55205,
55206, 55207, 55208, 55209, 55210, 55211, 55212, 55213, 55214,
55235, 55236, 55237,
];
assert_eq!(pids, expected);
}
#[test]
fn the_ngz_zuordnung_answers_one_pid_out_of_two_trees() {
let auf = familie_for(ZpSerie::NetzgangzeitreiheNzr, ZpVorgang::Aktivierung)
.expect("Zuordnung is a family");
let ab = familie_for(ZpSerie::NetzgangzeitreiheNzr, ZpVorgang::Deaktivierung)
.expect("Beendigung is a family");
assert_eq!((auf.anfrage, ab.anfrage), (55235, 55236));
assert_eq!(auf.antwort, ab.antwort, "55237 answers both");
assert_eq!(auf.antwort, Some(55237));
assert_eq!(auf.antwort_ebd, Some("E_0102"));
assert_eq!(ab.antwort_ebd, Some("E_0103"));
assert_ne!(auf.antwort_ebd, ab.antwort_ebd);
assert_eq!(auf.weiterleitung, Some(55235));
assert_eq!(ab.weiterleitung, Some(55236));
assert!(!serien_fuer_pid(55062).contains(&ZpSerie::NetzgangzeitreiheNzr));
assert_eq!(serien_fuer_pid(55235), vec![ZpSerie::NetzgangzeitreiheNzr]);
assert_eq!(ZpSerie::NetzgangzeitreiheNzr.endet_am(), None);
}
#[test]
fn the_two_monatliche_families_forward_to_different_recipients() {
let lf = familie_for(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung).unwrap();
let nb = familie_for(ZpSerie::MonatlicheAauezBkvAnfNb, ZpVorgang::Aktivierung).unwrap();
assert_eq!(lf.weiterleitung, Some(55205));
assert_eq!(nb.weiterleitung, Some(55211));
assert_eq!(lf.antwort_ebd, Some("E_0071"));
assert_eq!(nb.antwort_ebd, Some("E_0078"));
}
#[test]
fn only_the_taegliche_aauez_expires() {
for f in ZP_FAMILIEN {
let expected = f.serie == ZpSerie::TaeglicheAauez;
assert_eq!(
f.serie.endet_am().is_some(),
expected,
"{}",
f.serie.label()
);
}
let ende = TAEGLICHE_AAUEZ_ENDE;
assert!(ZpSerie::TaeglicheAauez.gilt_am(ende));
assert!(!ZpSerie::TaeglicheAauez.gilt_am(ende.next_day().unwrap()));
assert!(ZpSerie::LfAaszr.gilt_am(ende.next_day().unwrap()));
}
#[test]
fn a_family_without_an_antwort_is_terminal_on_arrival() {
let out = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
receive(ZpSerie::Zuordnungsermaechtigung, ZpVorgang::Aktivierung),
)
.expect("accepted");
let state = fold(&out.events);
assert_eq!(state.label(), "Erfasst");
assert!(out.outbox.is_empty(), "record-only family must not emit");
let err = MabisZpLifecycleWorkflow::handle(
&state,
ZpLifecycleCommand::SendAntwort {
bestaetigt: true,
grund: None,
},
)
.expect_err("must reject");
assert!(format!("{err}").contains("Antwort"), "got: {err}");
}
#[test]
fn the_same_pid_answers_or_does_not_depending_on_the_series() {
let owes = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
receive(
ZpSerie::Bilanzierungsgebietssummenzeitreihe,
ZpVorgang::Aktivierung,
),
)
.expect("accepted");
assert_eq!(fold(&owes.events).label(), "AnfrageErhalten");
let terminal = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
receive(ZpSerie::TaeglicheBkSzr, ZpVorgang::Aktivierung),
)
.expect("accepted");
assert_eq!(fold(&terminal.events).label(), "Erfasst");
}
#[test]
fn the_antwort_carries_the_tree_it_was_read_against() {
let out = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
receive(ZpSerie::Deltazeitreihenuebertrag, ZpVorgang::Deaktivierung),
)
.expect("accepted");
let state = fold(&out.events);
let antwort = MabisZpLifecycleWorkflow::handle(
&state,
ZpLifecycleCommand::SendAntwort {
bestaetigt: true,
grund: None,
},
)
.expect("answered");
assert_eq!(antwort.outbox[0].payload["pid"], 55064);
assert_eq!(antwort.outbox[0].payload["antwort_codeliste"], "E_0028");
}
#[test]
fn anfrage_antwort_weiterleitung_happy_path() {
let out = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
receive(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung),
)
.expect("accepted");
let state = fold(&out.events);
assert_eq!(state.label(), "AnfrageErhalten");
let antwort = MabisZpLifecycleWorkflow::handle(
&state,
ZpLifecycleCommand::SendAntwort {
bestaetigt: true,
grund: None,
},
)
.expect("pruefung");
assert_eq!(antwort.outbox.len(), 1);
assert_eq!(antwort.outbox[0].payload["pid"], 55204);
assert_eq!(antwort.outbox[0].payload["antwort_codeliste"], "E_0071");
assert_eq!(
antwort.outbox[0].recipient.as_ref(),
"9900123456789",
"the Antwort goes back to the requesting party"
);
let state = antwort
.events
.iter()
.fold(state, MabisZpLifecycleWorkflow::apply);
assert_eq!(state.label(), "Bestaetigt");
let out = MabisZpLifecycleWorkflow::handle(
&state,
ZpLifecycleCommand::SendWeiterleitung {
empfaenger: mp("9900555555555"),
},
)
.expect("weiterleitung");
assert_eq!(out.outbox[0].payload["pid"], 55205);
}
#[test]
fn sending_an_anfrage_emits_the_familys_pid_and_one_utilmd() {
let out = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
send(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung),
)
.expect("accepted");
assert_eq!(out.outbox.len(), 1);
assert_eq!(out.outbox[0].payload["pid"], 55203);
assert_eq!(out.outbox[0].payload["mabis_zaehlpunkt"], ZP);
assert_eq!(out.outbox[0].payload["bilanzierungsbeginn"], "20260701");
assert_eq!(out.outbox[0].recipient.as_ref(), "9900123456789");
let state = fold(&out.events);
assert_eq!(state.label(), "AnfrageGesendet");
assert_eq!(state.data().expect("carries data").mabis_zp_id, ZP);
}
#[test]
fn a_sent_deaktivierung_states_the_bilanzierungsende() {
let out = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
send(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Deaktivierung),
)
.expect("accepted");
assert_eq!(out.outbox[0].payload["pid"], 55206);
assert_eq!(out.outbox[0].payload["bilanzierungsende"], "20260701");
assert!(out.outbox[0].payload.get("bilanzierungsbeginn").is_none());
}
#[test]
fn a_repealed_series_cannot_be_sent_for_a_period_after_its_end() {
let mut cmd = send(ZpSerie::TaeglicheAauez, ZpVorgang::Aktivierung);
if let ZpLifecycleCommand::SendAnfrage {
ref mut billing_period,
..
} = cmd
{
*billing_period = BillingPeriod::new("202610");
}
let err = MabisZpLifecycleWorkflow::handle(&ZpLifecycleState::New, cmd)
.expect_err("the series is repealed with the end of 30.09.2026");
assert!(format!("{err}").contains("2026-09-30"), "got: {err}");
}
#[test]
fn a_confirming_antwort_closes_the_process_the_anfrage_opened() {
let state = gesendet(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung);
let out = MabisZpLifecycleWorkflow::handle(&state, antwort(55204, true, None))
.expect("the BIKO confirmed");
assert!(out.outbox.is_empty(), "an answer is not answered");
let state = out
.events
.iter()
.fold(state, MabisZpLifecycleWorkflow::apply);
assert_eq!(state.label(), "AntwortBestaetigt");
}
#[test]
fn a_requester_cannot_forward_what_it_asked_for() {
let state = gesendet(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung);
let out = MabisZpLifecycleWorkflow::handle(&state, antwort(55204, true, None))
.expect("confirmed");
let state = out
.events
.iter()
.fold(state, MabisZpLifecycleWorkflow::apply);
let err = MabisZpLifecycleWorkflow::handle(
&state,
ZpLifecycleCommand::SendWeiterleitung {
empfaenger: mp("9900555555555"),
},
)
.expect_err("only the answering party forwards");
assert!(format!("{err}").contains("Bestaetigt"), "got: {err}");
}
#[test]
fn a_rejecting_antwort_carries_its_begruendung_into_the_state() {
let state = gesendet(ZpSerie::MonatlicheAauezBkvAnfNb, ZpVorgang::Aktivierung);
let out = MabisZpLifecycleWorkflow::handle(
&state,
antwort(55210, false, Some("Bilanzierungsgebiet nicht gültig")),
)
.expect("rejections are applied");
let state = out
.events
.iter()
.fold(state, MabisZpLifecycleWorkflow::apply);
assert_eq!(state.label(), "Abgelehnt");
let ZpLifecycleState::Abgelehnt { grund } = state else {
panic!("expected Abgelehnt");
};
assert_eq!(grund, "Bilanzierungsgebiet nicht gültig");
}
#[test]
fn an_inbound_rejection_without_a_begruendung_is_refused() {
let state = gesendet(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung);
for grund in [None, Some(" ")] {
let err = MabisZpLifecycleWorkflow::handle(&state, antwort(55204, false, grund))
.expect_err("must refuse");
assert!(format!("{err}").contains("reason"), "got: {err}");
}
}
#[test]
fn a_record_only_family_takes_no_antwort() {
let state = gesendet(ZpSerie::LfAaszr, ZpVorgang::Aktivierung);
let err = MabisZpLifecycleWorkflow::handle(&state, antwort(55204, true, None))
.expect_err("55199 owes no answer");
let msg = format!("{err}");
assert!(msg.contains("definiert keine Antwort"), "got: {msg}");
assert!(msg.contains("55199"), "the error names the Anfrage: {msg}");
}
#[test]
fn an_antwort_pid_that_contradicts_the_family_is_refused() {
let state = gesendet(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung);
let err = MabisZpLifecycleWorkflow::handle(&state, antwort(55210, true, None))
.expect_err("55210 answers the anfNB family");
assert!(format!("{err}").contains("55204"), "got: {err}");
}
#[test]
fn an_antwort_without_a_question_is_refused_and_a_redelivered_one_is_a_no_op() {
let err =
MabisZpLifecycleWorkflow::handle(&ZpLifecycleState::New, antwort(55204, true, None))
.expect_err("an orphan answer");
assert!(format!("{err}").contains("AnfrageGesendet"), "got: {err}");
let state = gesendet(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung);
let out = MabisZpLifecycleWorkflow::handle(&state, antwort(55204, true, None))
.expect("confirmed");
let state = out
.events
.iter()
.fold(state, MabisZpLifecycleWorkflow::apply);
let again = MabisZpLifecycleWorkflow::handle(&state, antwort(55204, true, None))
.expect("idempotent");
assert!(again.events.is_empty());
}
#[test]
fn the_two_directions_do_not_share_a_positive_state() {
let answering = {
let out = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
receive(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung),
)
.expect("accepted");
let state = fold(&out.events);
let out = MabisZpLifecycleWorkflow::handle(
&state,
ZpLifecycleCommand::SendAntwort {
bestaetigt: true,
grund: None,
},
)
.expect("answered");
out.events
.iter()
.fold(state, MabisZpLifecycleWorkflow::apply)
};
assert_eq!(answering.label(), "Bestaetigt");
let requesting = {
let state = gesendet(ZpSerie::MonatlicheAauezBkvLf, ZpVorgang::Aktivierung);
let out = MabisZpLifecycleWorkflow::handle(&state, antwort(55204, true, None))
.expect("confirmed");
out.events
.iter()
.fold(state, MabisZpLifecycleWorkflow::apply)
};
assert_eq!(requesting.label(), "AntwortBestaetigt");
}
#[test]
fn the_generic_request_codes_are_also_weiterleitung_codes() {
for pid in [55062_u32, 55063] {
assert!(!serien_fuer_pid(pid).is_empty(), "{pid} is an Anfrage");
assert!(ist_weiterleitung_pid(pid), "{pid} is also a Weiterleitung");
assert!(!ist_antwort_pid(pid), "{pid} is never an Antwort");
}
for pid in [55064_u32, 55204, 55207, 55210, 55213, 55237] {
assert!(ist_antwort_pid(pid), "{pid} is an Antwort");
assert!(serien_fuer_pid(pid).is_empty(), "{pid} is no Anfrage");
assert!(!ist_weiterleitung_pid(pid), "{pid} is no Weiterleitung");
}
for pid in [55205_u32, 55208, 55211, 55214] {
assert!(ist_weiterleitung_pid(pid), "{pid} is a Weiterleitung");
assert!(!ist_antwort_pid(pid), "{pid} is no Antwort");
assert!(serien_fuer_pid(pid).is_empty(), "{pid} is no Anfrage");
}
}
#[test]
fn the_antwort_cluster_needs_both_the_tree_and_the_code() {
for ebd in ["E_0020", "E_0071", "E_0072", "E_0078", "E_0079"] {
let zustimmung = mako_pruefung::mabis::codes::zustimmung(ebd)
.unwrap_or_else(|| panic!("{ebd} publishes a Zustimmung"));
assert_eq!(
antwort_ist_zustimmung(ebd, zustimmung.code),
Some(true),
"{ebd} {}",
zustimmung.code
);
}
assert_eq!(antwort_ist_zustimmung("E_0020", "A12"), Some(true));
assert_eq!(antwort_ist_zustimmung("E_0071", "A12"), Some(false));
assert_eq!(antwort_ist_zustimmung("E_0020", "ZZZ"), None);
assert_eq!(
antwort_ist_zustimmung("E_0072", "A13"),
None,
"A13 is the Aktivierung trees' Zustimmung; E_0072 stops at A07"
);
assert_eq!(antwort_ist_zustimmung("E_9999", "A01"), None);
}
#[test]
fn a_rejecting_antwort_requires_a_reason() {
let out = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
receive(ZpSerie::NetzzeitreiheBiko, ZpVorgang::Aktivierung),
)
.expect("accepted");
let state = fold(&out.events);
let err = MabisZpLifecycleWorkflow::handle(
&state,
ZpLifecycleCommand::SendAntwort {
bestaetigt: false,
grund: None,
},
)
.expect_err("must reject");
assert!(format!("{err}").contains("reason"), "got: {err}");
}
#[test]
fn a_pid_that_contradicts_the_series_is_rejected() {
let err = MabisZpLifecycleWorkflow::handle(
&ZpLifecycleState::New,
receive_with_pid(ZpSerie::NetzzeitreiheBiko, ZpVorgang::Aktivierung, 55197),
)
.expect_err("must reject");
assert!(format!("{err}").contains("55062"), "got: {err}");
}
#[test]
fn validation_failure_is_terminal_and_emits_nothing() {
let cmd = match receive(ZpSerie::NetzzeitreiheBiko, ZpVorgang::Aktivierung) {
ZpLifecycleCommand::ReceiveAnfrage {
pid,
serie,
vorgang,
mabis_zp_id,
sender,
receiver,
billing_period,
document_date,
message_ref,
..
} => ZpLifecycleCommand::ReceiveAnfrage {
pid,
serie,
vorgang,
mabis_zp_id,
sender,
receiver,
billing_period,
document_date,
message_ref,
validation_passed: false,
validation_errors: vec!["SG6 LOC missing".to_owned()],
},
other => other,
};
let out = MabisZpLifecycleWorkflow::handle(&ZpLifecycleState::New, cmd).expect("accepted");
assert!(out.outbox.is_empty());
assert_eq!(fold(&out.events).label(), "ValidationFailed");
}
#[test]
fn a_redelivered_anfrage_is_a_no_op() {
let cmd = receive(ZpSerie::NetzzeitreiheBiko, ZpVorgang::Aktivierung);
let out = MabisZpLifecycleWorkflow::handle(&ZpLifecycleState::New, cmd.clone())
.expect("accepted");
let state = fold(&out.events);
let again = MabisZpLifecycleWorkflow::handle(&state, cmd).expect("idempotent");
assert!(again.events.is_empty());
assert!(again.outbox.is_empty());
}
#[test]
fn a_repealed_series_cannot_be_activated_after_its_end() {
let cmd = receive_for_period(ZpSerie::TaeglicheAauez, ZpVorgang::Aktivierung, "202610");
let out = MabisZpLifecycleWorkflow::handle(&ZpLifecycleState::New, cmd);
let err = out.expect_err("an activation past the repeal is refused");
assert!(
format!("{err}").contains("endet am 2026-09-30"),
"the refusal names the date: {err}"
);
}
#[test]
fn the_final_month_of_a_repealed_series_still_activates() {
let cmd = receive_for_period(ZpSerie::TaeglicheAauez, ZpVorgang::Aktivierung, "202609");
assert!(MabisZpLifecycleWorkflow::handle(&ZpLifecycleState::New, cmd).is_ok());
}
#[test]
fn a_deaktivierung_is_not_bound_by_the_end_date() {
let cmd = receive_for_period(ZpSerie::TaeglicheAauez, ZpVorgang::Deaktivierung, "202610");
assert!(MabisZpLifecycleWorkflow::handle(&ZpLifecycleState::New, cmd).is_ok());
}
#[test]
fn a_series_with_no_end_date_activates_in_any_period() {
let cmd = receive_for_period(ZpSerie::TaeglicheBkSzr, ZpVorgang::Aktivierung, "209912");
assert!(MabisZpLifecycleWorkflow::handle(&ZpLifecycleState::New, cmd).is_ok());
}
#[test]
fn an_unreadable_abrechnungszeitraum_does_not_refuse() {
let okt = time::Date::from_calendar_date(2026, time::Month::October, 1).unwrap();
for shape in ["202610", "2026-10", "20261001-20261031"] {
assert_eq!(
super::abrechnungszeitraum_beginn(shape),
Some(okt),
"{shape}"
);
}
for bad in ["2026", "", "202613"] {
assert_eq!(super::abrechnungszeitraum_beginn(bad), None, "{bad:?}");
}
}
}
#[cfg(test)]
mod wire_tests {
use super::*;
use crate::zeitreihen::{
Aggregationsebene, Familie, Kategorie, Rolle, Zeitreihe, cav_aus_zeitreihe, cci_aus_rolle,
};
#[test]
fn the_wire_codes_resolve_the_series_a_shared_pid_cannot() {
type Fall = (
Familie,
Option<Kategorie>,
Option<Aggregationsebene>,
Rolle,
ZpSerie,
);
let cases: &[Fall] = &[
(
Familie::BgSzr,
Some(Kategorie::B),
None,
Rolle::Uenb,
ZpSerie::Bilanzierungsgebietssummenzeitreihe,
),
(
Familie::BgSzr,
Some(Kategorie::C),
None,
Rolle::Uenb,
ZpSerie::TaeglicheBgSzr,
),
(
Familie::BkSzr,
Some(Kategorie::A),
None,
Rolle::Nb,
ZpSerie::BilanzkreissummenzeitreiheNb,
),
(
Familie::BkSzr,
Some(Kategorie::B),
Some(Aggregationsebene::Bilanzierungsgebiet),
Rolle::Uenb,
ZpSerie::BilanzkreissummenzeitreiheUenb,
),
(
Familie::BkSzr,
Some(Kategorie::C),
None,
Rolle::Uenb,
ZpSerie::TaeglicheBkSzr,
),
(
Familie::LfSzr,
Some(Kategorie::A),
None,
Rolle::Nb,
ZpSerie::LieferantensummenzeitreiheNb,
),
(
Familie::LfSzr,
Some(Kategorie::B),
Some(Aggregationsebene::Bilanzierungsgebiet),
Rolle::Uenb,
ZpSerie::LieferantensummenzeitreiheUenb,
),
(
Familie::Dzue,
None,
None,
Rolle::Uenb,
ZpSerie::Deltazeitreihenuebertrag,
),
(
Familie::Nzr,
None,
None,
Rolle::Nb,
ZpSerie::NetzzeitreiheBiko,
),
(
Familie::Abrechnungssummenzeitreihe,
None,
None,
Rolle::Biko,
ZpSerie::Abrechnungssummenzeitreihe,
),
];
for &(familie, kategorie, ebene, rolle, expected) in cases {
let z = Zeitreihe::new(familie, kategorie).expect("Tabelle-1 row");
let cav = cav_aus_zeitreihe(z, ebene).expect("has a CAV code");
let cci = cci_aus_rolle(rolle).expect("has a CCI code");
assert_eq!(
ZpSerie::from_wire(cav, cci),
Some(expected),
"CAV {cav} / CCI {cci}"
);
}
}
#[test]
fn every_resolved_series_has_a_family_row() {
for cav in [
"Z95", "Z96", "Z97", "Z99", "ZA0", "ZA1", "ZA3", "ZA4", "ZA5", "ZA6",
] {
for cci in ["ZA8", "ZA9", "ZB7"] {
if let Some(serie) = ZpSerie::from_wire(cav, cci) {
for vorgang in [ZpVorgang::Aktivierung, ZpVorgang::Deaktivierung] {
assert!(
familie_for(serie, vorgang).is_some(),
"{cav}/{cci} → {serie:?} / {vorgang:?} has no family row"
);
}
}
}
}
}
#[test]
fn an_unknown_code_resolves_to_nothing_rather_than_a_neighbour() {
assert_eq!(
ZpSerie::from_wire("ZG7", "ZA9"),
None,
"eMob is not MaBiS Tabelle 1"
);
assert_eq!(ZpSerie::from_wire("ZZZ", "ZA9"), None);
assert_eq!(ZpSerie::from_wire("Z95", "ZZZ"), None);
}
#[test]
fn the_series_with_their_own_pids_are_not_reachable_from_the_generic_codes() {
let unreachable = [
ZpSerie::Zuordnungsermaechtigung,
ZpSerie::TaeglicheAauez,
ZpSerie::LfAaszr,
ZpSerie::MonatlicheAauezBkvLf,
ZpSerie::MonatlicheAauezBkvAnfNb,
];
for cav in [
"Z95", "Z96", "Z97", "Z98", "Z99", "ZA0", "ZA1", "ZA2", "ZA3", "ZA4", "ZA5", "ZA6",
] {
for cci in ["ZA8", "ZA9", "ZB7"] {
if let Some(s) = ZpSerie::from_wire(cav, cci) {
assert!(!unreachable.contains(&s), "{cav}/{cci} → {s:?}");
}
}
}
}
}