use mako_engine::{
error::WorkflowError,
ids::DeadlineId,
outbox::PendingOutbox,
types::{MarktpartnerCode, MessageRef, Pruefidentifikator},
workflow::{CommandPayload, EventPayload, PendingDeadline, Workflow, WorkflowOutput},
};
use time::OffsetDateTime;
pub use super::wertebestellung::{
ABBESTELLUNG_PID, ABLEHNUNG_PID, ANFRAGE_PID, ANGEBOT_PID, ANTWORT_FRIST_WT,
BEENDIGUNG_MSB_PID, BESTAETIGUNG_PID, BESTELLUNG_PID, Lokationsebene, STORNIERUNG_PID,
STORNO_ABLEHNUNG_PID, STORNO_BESTAETIGUNG_PID, STS_BEENDET, Zustellquittung,
};
pub const WORKFLOW_NAME: &str = "esa-wertebestellung";
pub const ANGEBOT_WINDOW_LABEL: &str = "esa-wertebestellung-angebot";
pub const BINDUNGSFRIST_LABEL: &str = "esa-wertebestellung-bindungsfrist";
pub const ANTWORT_WINDOW_LABEL: &str = "esa-wertebestellung-antwort";
pub use super::wertebestellung::ESA_INBOUND_PIDS;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum EsaWertebestellungEvent {
AnfrageGesendet {
esa: MarktpartnerCode,
msb: MarktpartnerCode,
ebene: Lokationsebene,
lokations_id: String,
message_ref: MessageRef,
},
AngebotErhalten {
message_ref: MessageRef,
bindungsfrist: OffsetDateTime,
},
AnfrageAbgelehnt {
reason: String,
},
BestellungGesendet {
message_ref: MessageRef,
},
BestellungBestaetigt {
message_ref: MessageRef,
},
BestellungAbgelehnt {
reason: String,
},
StornierungGesendet {
message_ref: MessageRef,
},
StornierungBestaetigt {
message_ref: MessageRef,
},
StornierungAbgelehnt {
reason: String,
},
AbbestellungGesendet {
message_ref: MessageRef,
beendigung_zum: OffsetDateTime,
grund: String,
},
AbbestellungBestaetigt {
message_ref: MessageRef,
},
AbbestellungAbgelehnt {
reason: String,
},
LieferungBegonnen,
BeendetDurchMsb {
message_ref: MessageRef,
beendigung_zum: OffsetDateTime,
reason: Option<String>,
},
FristVersaeumt {
label: String,
},
}
impl EventPayload for EsaWertebestellungEvent {
fn event_type(&self) -> &'static str {
match self {
Self::AnfrageGesendet { .. } => "EsaWertebestellungAnfrageGesendet",
Self::AngebotErhalten { .. } => "EsaWertebestellungAngebotErhalten",
Self::BestellungGesendet { .. } => "EsaWertebestellungBestellungGesendet",
Self::BestellungBestaetigt { .. } => "EsaWertebestellungBestellungBestaetigt",
Self::AnfrageAbgelehnt { .. } => "EsaWertebestellungAnfrageAbgelehnt",
Self::BestellungAbgelehnt { .. } => "EsaWertebestellungBestellungAbgelehnt",
Self::StornierungGesendet { .. } => "EsaWertebestellungStornierungGesendet",
Self::StornierungBestaetigt { .. } => "EsaWertebestellungStornierungBestaetigt",
Self::StornierungAbgelehnt { .. } => "EsaWertebestellungStornierungAbgelehnt",
Self::AbbestellungGesendet { .. } => "EsaWertebestellungAbbestellungGesendet",
Self::AbbestellungBestaetigt { .. } => "EsaWertebestellungAbbestellungBestaetigt",
Self::AbbestellungAbgelehnt { .. } => "EsaWertebestellungAbbestellungAbgelehnt",
Self::LieferungBegonnen => "EsaWertebestellungLieferungBegonnen",
Self::BeendetDurchMsb { .. } => "EsaWertebestellungBeendetDurchMsb",
Self::FristVersaeumt { .. } => "EsaWertebestellungFristVersaeumt",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EsaWertebestellungData {
pub esa: MarktpartnerCode,
pub msb: MarktpartnerCode,
pub ebene: Lokationsebene,
pub lokations_id: String,
#[serde(default)]
pub bestellung_ref: Option<String>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(tag = "status", content = "data")]
pub enum EsaWertebestellungState {
#[default]
New,
AnfrageGesendet(Box<EsaWertebestellungData>),
AngebotErhalten {
data: Box<EsaWertebestellungData>,
bindungsfrist: OffsetDateTime,
},
BestellungGesendet(Box<EsaWertebestellungData>),
Beliefert {
data: Box<EsaWertebestellungData>,
lieferung_begonnen: bool,
},
StornierungGesendet(Box<EsaWertebestellungData>),
AbbestellungGesendet(Box<EsaWertebestellungData>),
Storniert(Box<EsaWertebestellungData>),
Beendet(Box<EsaWertebestellungData>),
Abgelehnt {
reason: String,
},
}
impl EsaWertebestellungState {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::New => "New",
Self::AnfrageGesendet(_) => "AnfrageGesendet",
Self::AngebotErhalten { .. } => "AngebotErhalten",
Self::BestellungGesendet(_) => "BestellungGesendet",
Self::Beliefert { .. } => "Beliefert",
Self::StornierungGesendet(_) => "StornierungGesendet",
Self::AbbestellungGesendet(_) => "AbbestellungGesendet",
Self::Storniert(_) => "Storniert",
Self::Beendet(_) => "Beendet",
Self::Abgelehnt { .. } => "Abgelehnt",
}
}
#[must_use]
pub const fn beliefert(&self) -> bool {
matches!(self, Self::Beliefert { .. } | Self::AbbestellungGesendet(_))
}
#[must_use]
pub const fn data(&self) -> Option<&EsaWertebestellungData> {
match self {
Self::AnfrageGesendet(d)
| Self::BestellungGesendet(d)
| Self::StornierungGesendet(d)
| Self::AbbestellungGesendet(d)
| Self::Storniert(d)
| Self::Beendet(d) => Some(d),
Self::AngebotErhalten { data, .. } | Self::Beliefert { data, .. } => Some(data),
Self::New | Self::Abgelehnt { .. } => None,
}
}
}
#[derive(Clone)]
pub enum EsaWertebestellungCommand {
SendWerteanfrage {
esa: MarktpartnerCode,
msb: MarktpartnerCode,
ebene: Lokationsebene,
lokations_id: String,
message_ref: MessageRef,
},
ReceiveAngebot {
message_ref: MessageRef,
bindungsfrist: OffsetDateTime,
},
ReceiveAnfrageAblehnung {
reason: Option<String>,
},
SendBestellung {
message_ref: MessageRef,
},
ReceiveBestaetigung {
message_ref: MessageRef,
},
ReceiveAblehnung {
message_ref: MessageRef,
reason: Option<String>,
},
SendStornierung {
message_ref: MessageRef,
},
ReceiveStornierungAntwort {
pid: Pruefidentifikator,
message_ref: MessageRef,
reason: Option<String>,
},
SendAbbestellung {
message_ref: MessageRef,
beendigung_zum: OffsetDateTime,
grund: String,
},
ReceiveBeendigungDurchMsb {
message_ref: MessageRef,
beendigung_zum: OffsetDateTime,
reason: Option<String>,
},
MarkLieferungBegonnen,
TimeoutExpired {
deadline_id: DeadlineId,
label: Box<str>,
},
}
impl CommandPayload for EsaWertebestellungCommand {}
pub struct EsaWertebestellungWorkflow;
fn require_pid(
pid: Pruefidentifikator,
allowed: &[Pruefidentifikator],
what: &str,
) -> Result<(), WorkflowError> {
if allowed.contains(&pid) {
Ok(())
} else {
let allowed: Vec<u32> = allowed.iter().map(|a| a.as_u32()).collect();
Err(WorkflowError::rejected(format!(
"{what} erwartet PID {allowed:?}, erhielt {pid}"
)))
}
}
impl Workflow for EsaWertebestellungWorkflow {
type State = EsaWertebestellungState;
type Event = EsaWertebestellungEvent;
type Command = EsaWertebestellungCommand;
fn apply(state: Self::State, event: &Self::Event) -> Self::State {
use EsaWertebestellungEvent as E;
use EsaWertebestellungState as S;
match event {
E::AnfrageGesendet {
esa,
msb,
ebene,
lokations_id,
..
} => S::AnfrageGesendet(Box::new(EsaWertebestellungData {
esa: esa.clone(),
msb: msb.clone(),
ebene: *ebene,
lokations_id: lokations_id.clone(),
bestellung_ref: None,
})),
E::AngebotErhalten { bindungsfrist, .. } => match state {
S::AnfrageGesendet(data) => S::AngebotErhalten {
data,
bindungsfrist: *bindungsfrist,
},
other => other,
},
E::AnfrageAbgelehnt { reason } => match state {
S::AnfrageGesendet(_) => S::Abgelehnt {
reason: reason.clone(),
},
other => other,
},
E::BestellungGesendet { message_ref } => match state {
S::AngebotErhalten { mut data, .. } => {
data.bestellung_ref = Some(message_ref.as_str().to_owned());
S::BestellungGesendet(data)
}
other => other,
},
E::BestellungBestaetigt { .. } => match state {
S::BestellungGesendet(data) => S::Beliefert {
data,
lieferung_begonnen: false,
},
other => other,
},
E::BestellungAbgelehnt { reason } => S::Abgelehnt {
reason: reason.clone(),
},
E::StornierungGesendet { .. } => match state {
S::Beliefert { data, .. } => S::StornierungGesendet(data),
other => other,
},
E::StornierungBestaetigt { .. } => match state {
S::StornierungGesendet(data) => S::Storniert(data),
other => other,
},
E::StornierungAbgelehnt { .. } => match state {
S::StornierungGesendet(data) => S::Beliefert {
data,
lieferung_begonnen: false,
},
other => other,
},
E::AbbestellungGesendet { .. } => match state {
S::Beliefert { data, .. } => S::AbbestellungGesendet(data),
other => other,
},
E::AbbestellungBestaetigt { .. } => match state {
S::AbbestellungGesendet(data) => S::Beendet(data),
other => other,
},
E::BeendetDurchMsb { .. } => match state {
S::Beliefert { data, .. } | S::AbbestellungGesendet(data) => S::Beendet(data),
other => other,
},
E::AbbestellungAbgelehnt { .. } => match state {
S::AbbestellungGesendet(data) => S::Beliefert {
data,
lieferung_begonnen: true,
},
other => other,
},
E::LieferungBegonnen => match state {
S::Beliefert { data, .. } => S::Beliefert {
data,
lieferung_begonnen: true,
},
other => other,
},
E::FristVersaeumt { .. } => match state {
S::AnfrageGesendet(_) => S::Abgelehnt {
reason: "Angebot nicht innerhalb der Frist erhalten".to_owned(),
},
other => other,
},
}
}
fn handle(
state: &Self::State,
command: Self::Command,
) -> Result<WorkflowOutput<Self::Event>, WorkflowError> {
fn esa_send(
message_type: &'static str,
pid: Pruefidentifikator,
data: &EsaWertebestellungData,
message_ref: &MessageRef,
order_reference: Option<&str>,
) -> PendingOutbox {
PendingOutbox::new(
message_type,
data.msb.as_str(),
serde_json::json!({
"pid": pid,
"sender": data.esa.as_str(),
"receiver": data.msb.as_str(),
"message_ref": message_ref.as_str(),
"location": data.lokations_id,
"order_reference": order_reference,
}),
)
}
use EsaWertebestellungCommand as C;
use EsaWertebestellungEvent as E;
use EsaWertebestellungState as S;
match command {
C::SendWerteanfrage {
esa,
msb,
ebene,
lokations_id,
message_ref,
} => {
if !matches!(state, S::New) {
return Err(WorkflowError::invalid_state("New", state.label()));
}
if lokations_id.trim().is_empty() {
return Err(WorkflowError::rejected(format!(
"Werteanfrage auf Ebene {} ohne Lokations-ID",
ebene.as_str()
)));
}
let data = EsaWertebestellungData {
esa: esa.clone(),
msb: msb.clone(),
ebene,
lokations_id: lokations_id.clone(),
bestellung_ref: None,
};
let outbox = esa_send("REQOTE", ANFRAGE_PID, &data, &message_ref, None);
let due = mako_engine::fristen::deadline_at_werktage(
OffsetDateTime::now_utc(),
super::wertebestellung::ANGEBOT_FRIST_WT,
mako_engine::fristen::HolidayCalendar::BdewMaKo,
);
Ok(WorkflowOutput {
events: vec![E::AnfrageGesendet {
esa,
msb,
ebene,
lokations_id,
message_ref,
}],
outbox: vec![outbox],
deadlines: vec![PendingDeadline::new(ANGEBOT_WINDOW_LABEL, due)],
})
}
C::ReceiveAngebot {
message_ref,
bindungsfrist,
} => {
if !matches!(state, S::AnfrageGesendet(_)) {
return Err(WorkflowError::invalid_state(
"AnfrageGesendet",
state.label(),
));
}
Ok(WorkflowOutput {
events: vec![E::AngebotErhalten {
message_ref,
bindungsfrist,
}],
outbox: Vec::new(),
deadlines: vec![PendingDeadline::new(BINDUNGSFRIST_LABEL, bindungsfrist)],
})
}
C::ReceiveAnfrageAblehnung { reason } => {
if !matches!(state, S::AnfrageGesendet(_)) {
return Err(WorkflowError::invalid_state(
"AnfrageGesendet",
state.label(),
));
}
Ok(WorkflowOutput::events(vec![E::AnfrageAbgelehnt {
reason: reason.unwrap_or_else(|| "Anfrage vom MSB abgelehnt".to_owned()),
}]))
}
C::SendBestellung { message_ref } => {
let S::AngebotErhalten {
data,
bindungsfrist,
} = state
else {
return Err(WorkflowError::invalid_state(
"AngebotErhalten",
state.label(),
));
};
if OffsetDateTime::now_utc() > *bindungsfrist {
return Err(WorkflowError::rejected(format!(
"Bindungsfrist des Angebots endete am {bindungsfrist}"
)));
}
let outbox = esa_send("ORDERS", BESTELLUNG_PID, data, &message_ref, None);
let due = mako_engine::fristen::deadline_at_werktage(
OffsetDateTime::now_utc(),
ANTWORT_FRIST_WT,
mako_engine::fristen::HolidayCalendar::BdewMaKo,
);
Ok(WorkflowOutput {
events: vec![E::BestellungGesendet { message_ref }],
outbox: vec![outbox],
deadlines: vec![PendingDeadline::new(ANTWORT_WINDOW_LABEL, due)],
})
}
C::ReceiveBestaetigung { message_ref } => match state {
S::BestellungGesendet(_) => {
Ok(WorkflowOutput::events(vec![E::BestellungBestaetigt {
message_ref,
}]))
}
S::AbbestellungGesendet(_) => {
Ok(WorkflowOutput::events(vec![E::AbbestellungBestaetigt {
message_ref,
}]))
}
_ => Err(WorkflowError::invalid_state(
"BestellungGesendet|AbbestellungGesendet",
state.label(),
)),
},
C::ReceiveAblehnung {
message_ref: _,
reason,
} => match state {
S::BestellungGesendet(_) => {
Ok(WorkflowOutput::events(vec![E::BestellungAbgelehnt {
reason: reason.unwrap_or_else(|| "ohne Begründung".to_owned()),
}]))
}
S::AbbestellungGesendet(_) => {
Ok(WorkflowOutput::events(vec![E::AbbestellungAbgelehnt {
reason: reason.unwrap_or_else(|| "ohne Begründung".to_owned()),
}]))
}
_ => Err(WorkflowError::invalid_state(
"BestellungGesendet|AbbestellungGesendet",
state.label(),
)),
},
C::SendStornierung { message_ref } => {
let S::Beliefert {
data,
lieferung_begonnen,
} = state
else {
return Err(WorkflowError::invalid_state("Beliefert", state.label()));
};
if *lieferung_begonnen {
return Err(WorkflowError::rejected(
"Stornierung ist nach Lieferbeginn nicht mehr möglich \
(UC 4.3 Vorbedingung) — nutze die Abbestellung (17008)",
));
}
let outbox = esa_send(
"ORDCHG",
STORNIERUNG_PID,
data,
&message_ref,
data.bestellung_ref.as_deref(),
);
let due = mako_engine::fristen::deadline_at_werktage(
OffsetDateTime::now_utc(),
ANTWORT_FRIST_WT,
mako_engine::fristen::HolidayCalendar::BdewMaKo,
);
Ok(WorkflowOutput {
events: vec![E::StornierungGesendet { message_ref }],
outbox: vec![outbox],
deadlines: vec![PendingDeadline::new(ANTWORT_WINDOW_LABEL, due)],
})
}
C::ReceiveStornierungAntwort {
pid,
message_ref,
reason,
} => {
if !matches!(state, S::StornierungGesendet(_)) {
return Err(WorkflowError::invalid_state(
"StornierungGesendet",
state.label(),
));
}
require_pid(
pid,
&[STORNO_BESTAETIGUNG_PID, STORNO_ABLEHNUNG_PID],
"Antwort auf Stornierung",
)?;
if pid == STORNO_BESTAETIGUNG_PID {
Ok(WorkflowOutput::events(vec![E::StornierungBestaetigt {
message_ref,
}]))
} else {
Ok(WorkflowOutput::events(vec![E::StornierungAbgelehnt {
reason: reason.unwrap_or_else(|| "ohne Begründung".to_owned()),
}]))
}
}
C::SendAbbestellung {
message_ref,
beendigung_zum,
grund,
} => {
let S::Beliefert { data, .. } = state else {
return Err(WorkflowError::invalid_state("Beliefert", state.label()));
};
let outbox = esa_send("ORDERS", ABBESTELLUNG_PID, data, &message_ref, None);
let due = mako_engine::fristen::deadline_at_werktage(
OffsetDateTime::now_utc(),
ANTWORT_FRIST_WT,
mako_engine::fristen::HolidayCalendar::BdewMaKo,
);
Ok(WorkflowOutput {
events: vec![E::AbbestellungGesendet {
message_ref,
beendigung_zum,
grund,
}],
outbox: vec![outbox],
deadlines: vec![PendingDeadline::new(ANTWORT_WINDOW_LABEL, due)],
})
}
C::MarkLieferungBegonnen => {
if !matches!(state, S::Beliefert { .. }) {
return Err(WorkflowError::invalid_state("Beliefert", state.label()));
}
Ok(WorkflowOutput::events(vec![E::LieferungBegonnen]))
}
C::ReceiveBeendigungDurchMsb {
message_ref,
beendigung_zum,
reason,
} => {
if !state.beliefert() && !matches!(state, S::Beendet(_)) {
return Err(WorkflowError::invalid_state("Beliefert", state.label()));
}
Ok(WorkflowOutput::events(vec![E::BeendetDurchMsb {
message_ref,
beendigung_zum,
reason,
}]))
}
C::TimeoutExpired { label, .. } => {
Ok(WorkflowOutput::events(vec![E::FristVersaeumt {
label: label.to_string(),
}]))
}
}
}
}