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, STORNIERUNG_PID, STORNO_ABLEHNUNG_PID,
STORNO_BESTAETIGUNG_PID, STS_BEENDET, Zustellquittung,
};
pub use crate::esa::{
Abonnement, Angebot, Antwort, Bestellgegenstand, Lokationsebene, ProduktFehler, SmgwQuelle,
};
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,
gegenstand: Box<Bestellgegenstand>,
message_ref: MessageRef,
},
AngebotErhalten {
message_ref: MessageRef,
#[serde(with = "time::serde::rfc3339")]
bindungsfrist: OffsetDateTime,
#[serde(default, with = "time::serde::rfc3339::option")]
fruehester_start: Option<OffsetDateTime>,
#[serde(default)]
angebot: Box<Angebot>,
},
AnfrageAbgelehnt {
message_ref: MessageRef,
reason: String,
},
BestellungGesendet {
message_ref: MessageRef,
},
BestellungBestaetigt {
message_ref: MessageRef,
#[serde(default)]
antwort: Option<Antwort>,
#[serde(default)]
smgw_quelle: Option<SmgwQuelle>,
},
BestellungAbgelehnt {
message_ref: MessageRef,
#[serde(default)]
antwort: Option<Antwort>,
reason: String,
},
StornierungGesendet {
message_ref: MessageRef,
},
StornierungBestaetigt {
message_ref: MessageRef,
#[serde(default)]
antwort: Option<Antwort>,
},
StornierungAbgelehnt {
message_ref: MessageRef,
#[serde(default)]
antwort: Option<Antwort>,
reason: String,
},
AbbestellungGesendet {
message_ref: MessageRef,
#[serde(with = "time::serde::rfc3339")]
beendigung_zum: OffsetDateTime,
grund: String,
},
AbbestellungBestaetigt {
message_ref: MessageRef,
#[serde(default)]
antwort: Option<Antwort>,
},
AbbestellungAbgelehnt {
message_ref: MessageRef,
#[serde(default)]
antwort: Option<Antwort>,
reason: String,
},
LieferungBegonnen,
BeendetDurchMsb {
message_ref: MessageRef,
#[serde(with = "time::serde::rfc3339")]
beendigung_zum: OffsetDateTime,
reason: Option<String>,
},
FristVersaeumt {
label: String,
},
AntwortWidersprichtSich {
pid: u32,
antwort: Antwort,
},
}
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::AntwortWidersprichtSich { .. } => "EsaWertebestellungAntwortWidersprichtSich",
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,
pub gegenstand: Box<Bestellgegenstand>,
pub anfrage_ref: String,
#[serde(default)]
pub angebot_ref: Option<String>,
#[serde(default)]
pub bestellung_ref: Option<String>,
#[serde(default)]
pub stornierung_ref: Option<String>,
#[serde(default)]
pub abbestellung_ref: Option<String>,
#[serde(default)]
pub lieferung_begonnen: bool,
#[serde(default)]
#[serde(with = "time::serde::rfc3339::option")]
pub fruehester_start: Option<OffsetDateTime>,
#[serde(default)]
pub angebot: Box<Angebot>,
#[serde(default)]
pub smgw_quelle: Option<SmgwQuelle>,
#[serde(default)]
pub letzte_antwort: Option<Antwort>,
}
#[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>,
#[serde(with = "time::serde::rfc3339")]
bindungsfrist: OffsetDateTime,
},
BestellungGesendet(Box<EsaWertebestellungData>),
Beliefert(Box<EsaWertebestellungData>),
StornierungGesendet(Box<EsaWertebestellungData>),
AbbestellungGesendet(Box<EsaWertebestellungData>),
Storniert(Box<EsaWertebestellungData>),
Beendet(Box<EsaWertebestellungData>),
Abgelehnt {
reason: String,
},
}
impl mako_engine::workflow::OccupiesBusinessKey for EsaWertebestellungState {
fn occupies_business_key(&self) -> bool {
match self {
Self::AnfrageGesendet(_)
| Self::AngebotErhalten { .. }
| Self::BestellungGesendet(_)
| Self::Beliefert(_)
| Self::StornierungGesendet(_)
| Self::AbbestellungGesendet(_) => true,
Self::New | Self::Storniert(_) | Self::Beendet(_) | Self::Abgelehnt { .. } => false,
}
}
}
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::Beliefert(d)
| Self::StornierungGesendet(d)
| Self::AbbestellungGesendet(d)
| Self::Storniert(d)
| Self::Beendet(d) => Some(d),
Self::AngebotErhalten { data, .. } => Some(data),
Self::New | Self::Abgelehnt { .. } => None,
}
}
}
#[derive(Clone)]
pub enum EsaWertebestellungCommand {
SendWerteanfrage {
esa: MarktpartnerCode,
msb: MarktpartnerCode,
ebene: Lokationsebene,
lokations_id: String,
gegenstand: Box<Bestellgegenstand>,
message_ref: MessageRef,
},
ReceiveAngebot {
message_ref: MessageRef,
bindungsfrist: OffsetDateTime,
fruehester_start: Option<OffsetDateTime>,
angebot: Box<Angebot>,
},
ReceiveAnfrageAblehnung {
message_ref: MessageRef,
reason: Option<String>,
},
SendBestellung {
message_ref: MessageRef,
},
ReceiveBestaetigung {
message_ref: MessageRef,
antwort: Option<Antwort>,
smgw_quelle: Option<SmgwQuelle>,
},
ReceiveAblehnung {
message_ref: MessageRef,
antwort: Option<Antwort>,
},
SendStornierung {
message_ref: MessageRef,
},
ReceiveStornierungAntwort {
pid: Pruefidentifikator,
message_ref: MessageRef,
antwort: Option<Antwort>,
},
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 antwort_reason(antwort: Option<&Antwort>) -> String {
antwort.map_or_else(
|| {
"ORDRSP ohne SG2 AJT — der Antwortcode ist Muss (ORDRSP AHB 1.1b §4.15), \
die Ablehnung nennt damit keinen Grund"
.to_owned()
},
Antwort::beschreibung,
)
}
fn konflikt_event(
pid: Pruefidentifikator,
antwort: Option<&Antwort>,
pid_ist_zustimmung: bool,
) -> Vec<EsaWertebestellungEvent> {
match antwort {
Some(a) if a.widerspricht_pid(pid_ist_zustimmung) => {
vec![EsaWertebestellungEvent::AntwortWidersprichtSich {
pid: pid.as_u32(),
antwort: a.clone(),
}]
}
_ => Vec::new(),
}
}
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 on_deadline(
deadline: &mako_engine::deadline::Deadline,
state: &Self::State,
) -> Option<Self::Command> {
use mako_engine::workflow::OccupiesBusinessKey as _;
let owned = matches!(
deadline.label(),
ANGEBOT_WINDOW_LABEL | ANTWORT_WINDOW_LABEL | BINDUNGSFRIST_LABEL
);
(owned && state.occupies_business_key()).then(|| {
EsaWertebestellungCommand::TimeoutExpired {
deadline_id: deadline.deadline_id(),
label: deadline.label().into(),
}
})
}
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,
gegenstand,
message_ref,
} => S::AnfrageGesendet(Box::new(EsaWertebestellungData {
esa: esa.clone(),
msb: msb.clone(),
ebene: *ebene,
lokations_id: lokations_id.clone(),
gegenstand: gegenstand.clone(),
anfrage_ref: message_ref.as_str().to_owned(),
angebot_ref: None,
bestellung_ref: None,
stornierung_ref: None,
abbestellung_ref: None,
lieferung_begonnen: false,
fruehester_start: None,
angebot: Box::default(),
smgw_quelle: None,
letzte_antwort: None,
})),
E::AngebotErhalten {
message_ref,
bindungsfrist,
fruehester_start,
angebot,
} => match state {
S::AnfrageGesendet(mut data) => {
data.angebot_ref = Some(message_ref.as_str().to_owned());
data.fruehester_start = *fruehester_start;
data.angebot.clone_from(angebot);
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 {
antwort,
smgw_quelle,
..
} => match state {
S::BestellungGesendet(mut data) => {
data.letzte_antwort.clone_from(antwort);
data.smgw_quelle.clone_from(smgw_quelle);
S::Beliefert(data)
}
other => other,
},
E::BestellungAbgelehnt {
reason, antwort, ..
} => match state {
S::BestellungGesendet(_) => S::Abgelehnt {
reason: antwort
.as_ref()
.map_or_else(|| reason.clone(), Antwort::beschreibung),
},
other => other,
},
E::StornierungGesendet { message_ref } => match state {
S::Beliefert(mut data) => {
data.stornierung_ref = Some(message_ref.as_str().to_owned());
S::StornierungGesendet(data)
}
other => other,
},
E::StornierungBestaetigt { antwort, .. } => match state {
S::StornierungGesendet(mut data) => {
data.letzte_antwort.clone_from(antwort);
S::Storniert(data)
}
other => other,
},
E::StornierungAbgelehnt { antwort, .. } => match state {
S::StornierungGesendet(mut data) => {
data.letzte_antwort.clone_from(antwort);
S::Beliefert(data)
}
other => other,
},
E::AbbestellungGesendet { message_ref, .. } => match state {
S::Beliefert(mut data) => {
data.abbestellung_ref = Some(message_ref.as_str().to_owned());
S::AbbestellungGesendet(data)
}
other => other,
},
E::AbbestellungBestaetigt { antwort, .. } => match state {
S::AbbestellungGesendet(mut data) => {
data.letzte_antwort.clone_from(antwort);
S::Beendet(data)
}
other => other,
},
E::BeendetDurchMsb { .. } => match state {
S::Beliefert(data)
| S::AbbestellungGesendet(data)
| S::StornierungGesendet(data) => S::Beendet(data),
other => other,
},
E::AbbestellungAbgelehnt { antwort, .. } => match state {
S::AbbestellungGesendet(mut data) => {
data.letzte_antwort.clone_from(antwort);
S::Beliefert(data)
}
other => other,
},
E::LieferungBegonnen => match state {
S::Beliefert(mut data) => {
data.lieferung_begonnen = true;
S::Beliefert(data)
}
S::StornierungGesendet(mut data) => {
data.lieferung_begonnen = true;
S::StornierungGesendet(data)
}
S::AbbestellungGesendet(mut data) => {
data.lieferung_begonnen = true;
S::AbbestellungGesendet(data)
}
other => other,
},
E::FristVersaeumt { label } => match state {
S::AnfrageGesendet(_) => S::Abgelehnt {
reason: "Angebot nicht innerhalb der Frist erhalten".to_owned(),
},
S::AngebotErhalten { bindungsfrist, .. } if label == BINDUNGSFRIST_LABEL => {
S::Abgelehnt {
reason: format!(
"Bindungsfrist des Angebots am {bindungsfrist} abgelaufen, ohne dass \
bestellt wurde"
),
}
}
other => other,
},
E::AntwortWidersprichtSich { .. } => state,
}
}
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,
korrelation_ref: Option<&str>,
ausfuehrungsdatum: Option<OffsetDateTime>,
abonnement: Abonnement,
) -> PendingOutbox {
let traegt_location = pid == ANFRAGE_PID;
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": traegt_location.then(|| data.lokations_id.clone()),
"ebene": data.ebene,
"korrelation_ref": korrelation_ref,
"messprodukt": data.gegenstand.messprodukt,
"wunschtermin": data.gegenstand.wunschtermin.to_string(),
"zeitraum_bis": data.gegenstand.zeitraum_bis.map(|d| d.to_string()),
"abonnement": abonnement.imd_code(),
"ausfuehrungsdatum": ausfuehrungsdatum.map(|d| d.date().to_string()),
"smgw": data.gegenstand.smgw,
}),
)
}
use EsaWertebestellungCommand as C;
use EsaWertebestellungEvent as E;
use EsaWertebestellungState as S;
match command {
C::SendWerteanfrage {
esa,
msb,
ebene,
lokations_id,
gegenstand,
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()
)));
}
gegenstand
.validate(ebene)
.map_err(|e| WorkflowError::rejected(e.to_string()))?;
let data = EsaWertebestellungData {
esa: esa.clone(),
msb: msb.clone(),
ebene,
lokations_id: lokations_id.clone(),
gegenstand: gegenstand.clone(),
anfrage_ref: message_ref.as_str().to_owned(),
angebot_ref: None,
bestellung_ref: None,
stornierung_ref: None,
abbestellung_ref: None,
lieferung_begonnen: false,
fruehester_start: None,
angebot: Box::default(),
smgw_quelle: None,
letzte_antwort: None,
};
let wunschtermin = gegenstand.wunschtermin.midnight().assume_utc();
let outbox = esa_send(
"REQOTE",
ANFRAGE_PID,
&data,
&message_ref,
None,
Some(wunschtermin),
gegenstand.abonnement,
);
let due = mako_fristen::deadline_at_werktage(
OffsetDateTime::now_utc(),
super::wertebestellung::ANGEBOT_FRIST_WT,
mako_fristen::HolidayCalendar::BdewMaKo,
);
Ok(WorkflowOutput {
events: vec![E::AnfrageGesendet {
esa,
msb,
ebene,
lokations_id,
gegenstand,
message_ref,
}],
outbox: vec![outbox],
deadlines: vec![PendingDeadline::new(ANGEBOT_WINDOW_LABEL, due)],
})
}
C::ReceiveAngebot {
message_ref,
bindungsfrist,
fruehester_start,
angebot,
} => {
if !matches!(state, S::AnfrageGesendet(_)) {
return Err(WorkflowError::invalid_state(
"AnfrageGesendet",
state.label(),
));
}
if angebot.ist_leer() {
return Err(WorkflowError::rejected(
"QUOTES 15003 ohne bepreiste Position ist kein Angebot, sondern die \
Ablehnung der Anfrage (SG31 PRI und die PIA+5 …:SRW OBIS-Kennzahlen \
sind Muss, QUOTES AHB 1.1a §4.3) — nutze ReceiveAnfrageAblehnung",
));
}
Ok(WorkflowOutput {
events: vec![E::AngebotErhalten {
message_ref,
bindungsfrist,
fruehester_start,
angebot,
}],
outbox: Vec::new(),
deadlines: vec![PendingDeadline::new(BINDUNGSFRIST_LABEL, bindungsfrist)],
})
}
C::ReceiveAnfrageAblehnung {
message_ref,
reason,
} => {
if !matches!(state, S::AnfrageGesendet(_)) {
return Err(WorkflowError::invalid_state(
"AnfrageGesendet",
state.label(),
));
}
Ok(WorkflowOutput::events(vec![E::AnfrageAbgelehnt {
message_ref,
reason: reason.unwrap_or_else(|| {
"QUOTES 15003 ohne bepreiste Position — der MSB nennt keine Gründe"
.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 angebot_ref = data.angebot_ref.as_deref().ok_or_else(|| {
WorkflowError::rejected(
"Bestellung ohne Angebotsnummer — RFF+AAG ist Muss (ORDERS AHB 1.1b §4.15)",
)
})?;
let wunsch = data.gegenstand.wunschtermin.midnight().assume_utc();
let ausfuehrungsdatum = data.fruehester_start.map_or(wunsch, |f| wunsch.max(f));
let outbox = esa_send(
"ORDERS",
BESTELLUNG_PID,
data,
&message_ref,
Some(angebot_ref),
Some(ausfuehrungsdatum),
data.gegenstand.abonnement,
);
let due = mako_fristen::deadline_at_werktage(
OffsetDateTime::now_utc(),
ANTWORT_FRIST_WT,
mako_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,
antwort,
smgw_quelle,
} => {
let mut events = konflikt_event(BESTAETIGUNG_PID, antwort.as_ref(), true);
match state {
S::BestellungGesendet(_) => {
events.push(E::BestellungBestaetigt {
message_ref,
antwort,
smgw_quelle,
});
}
S::AbbestellungGesendet(_) => {
events.push(E::AbbestellungBestaetigt {
message_ref,
antwort,
});
}
_ => {
return Err(WorkflowError::invalid_state(
"BestellungGesendet|AbbestellungGesendet",
state.label(),
));
}
}
Ok(WorkflowOutput::events(events))
}
C::ReceiveAblehnung {
message_ref,
antwort,
} => {
let mut events = konflikt_event(ABLEHNUNG_PID, antwort.as_ref(), false);
let reason = antwort_reason(antwort.as_ref());
match state {
S::BestellungGesendet(_) => {
events.push(E::BestellungAbgelehnt {
message_ref,
antwort,
reason,
});
}
S::AbbestellungGesendet(_) => {
events.push(E::AbbestellungAbgelehnt {
message_ref,
antwort,
reason,
});
}
_ => {
return Err(WorkflowError::invalid_state(
"BestellungGesendet|AbbestellungGesendet",
state.label(),
));
}
}
Ok(WorkflowOutput::events(events))
}
C::SendStornierung { message_ref } => {
let S::Beliefert(data) = state else {
return Err(WorkflowError::invalid_state("Beliefert", state.label()));
};
if data.lieferung_begonnen {
return Err(WorkflowError::rejected(
"Stornierung ist nach Lieferbeginn nicht mehr möglich \
(UC 4.3 Vorbedingung) — nutze die Abbestellung (17008)",
));
}
let bestellung_ref = data.bestellung_ref.as_deref().ok_or_else(|| {
WorkflowError::rejected(
"Stornierung ohne Auftragsnummer — RFF+ON ist Muss (ORDCHG AHB 1.1 §3.2)",
)
})?;
let outbox = esa_send(
"ORDCHG",
STORNIERUNG_PID,
data,
&message_ref,
Some(bestellung_ref),
None,
data.gegenstand.abonnement,
);
let due = mako_fristen::deadline_at_werktage(
OffsetDateTime::now_utc(),
ANTWORT_FRIST_WT,
mako_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,
antwort,
} => {
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",
)?;
let bestaetigt = pid == STORNO_BESTAETIGUNG_PID;
let mut events = konflikt_event(pid, antwort.as_ref(), bestaetigt);
events.push(if bestaetigt {
E::StornierungBestaetigt {
message_ref,
antwort,
}
} else {
E::StornierungAbgelehnt {
reason: antwort_reason(antwort.as_ref()),
message_ref,
antwort,
}
});
Ok(WorkflowOutput::events(events))
}
C::SendAbbestellung {
message_ref,
beendigung_zum,
grund,
} => {
let S::Beliefert(data) = state else {
return Err(WorkflowError::invalid_state("Beliefert", state.label()));
};
if !data.gegenstand.abonnement.ist_abo() {
return Err(WorkflowError::rejected(
"einmalige Übermittlung (IMD++Z03) ist stornierbar, nicht abbestellbar — \
nutze die Stornierung (ORDCHG 39002); E_0254 Prüfschritt 1 lehnt eine \
Abbestellung mit A01 ab",
));
}
let bestellung_ref = data.bestellung_ref.as_deref().ok_or_else(|| {
WorkflowError::rejected(
"Abbestellung ohne Referenz auf die Bestellung — RFF+ACW ist Muss \
(ORDERS AHB 1.1b §4.15)",
)
})?;
let outbox = esa_send(
"ORDERS",
ABBESTELLUNG_PID,
data,
&message_ref,
Some(bestellung_ref),
Some(beendigung_zum),
Abonnement::EndeAbo,
);
let due = mako_fristen::deadline_at_werktage(
OffsetDateTime::now_utc(),
ANTWORT_FRIST_WT,
mako_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 => {
match state {
S::Beliefert(d) | S::StornierungGesendet(d) | S::AbbestellungGesendet(d)
if !d.lieferung_begonnen =>
{
Ok(WorkflowOutput::events(vec![E::LieferungBegonnen]))
}
_ => Ok(WorkflowOutput::events(Vec::new())),
}
}
C::ReceiveBeendigungDurchMsb {
message_ref,
beendigung_zum,
reason,
} => {
if !state.beliefert() && !matches!(state, S::StornierungGesendet(_) | S::Beendet(_))
{
return Err(WorkflowError::invalid_state(
"Beliefert|StornierungGesendet",
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(),
}]))
}
}
}
}