use std::collections::HashMap;
use mako_engine::{
envelope::EventEnvelope,
error::WorkflowError,
ids::DeadlineId,
outbox::PendingOutbox,
projection::Projection,
types::{MaLo, MarktpartnerCode, MessageRef, Pruefidentifikator},
workflow::{CommandPayload, EventPayload, Workflow, WorkflowOutput},
};
pub const WORKFLOW_NAME: &str = "gpke-konfiguration";
pub const ORDERS_PIDS: &[u32] = &[17134, 17135];
pub const ORDRSP_PIDS: &[u32] = &[19001, 19002];
pub const KONFIGURATION_WINDOW_LABEL: &str = "konfiguration-deadline";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum KonfigurationEvent {
BeauftragungGesendet {
orders_pid: Pruefidentifikator,
msb_mp_id: MarktpartnerCode,
malo: MaLo,
new_supplier: MarktpartnerCode,
message_ref: MessageRef,
},
BestaetigungErhalten {
response_pid: Pruefidentifikator,
message_ref: MessageRef,
},
AblehungErhalten {
response_pid: Pruefidentifikator,
reason: String,
message_ref: MessageRef,
},
DeadlineExpired {
deadline_id: DeadlineId,
label: Box<str>,
},
}
impl EventPayload for KonfigurationEvent {
fn event_type(&self) -> &'static str {
match self {
Self::BeauftragungGesendet { .. } => "KonfigurationBeauftragungGesendet",
Self::BestaetigungErhalten { .. } => "KonfigurationBestaetigungErhalten",
Self::AblehungErhalten { .. } => "KonfigurationAblehungErhalten",
Self::DeadlineExpired { .. } => "KonfigurationDeadlineExpired",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BeauftragungData {
pub orders_pid: Pruefidentifikator,
pub msb_mp_id: MarktpartnerCode,
pub malo: MaLo,
pub new_supplier: MarktpartnerCode,
pub message_ref: MessageRef,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "status", content = "data")]
pub enum KonfigurationState {
New,
Beauftragt(BeauftragungData),
Bestaetigt {
data: BeauftragungData,
response_ref: MessageRef,
},
Abgelehnt {
data: BeauftragungData,
reason: String,
},
}
impl Default for KonfigurationState {
fn default() -> Self {
Self::New
}
}
impl KonfigurationState {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::New => "New",
Self::Beauftragt(_) => "Beauftragt",
Self::Bestaetigt { .. } => "Bestaetigt",
Self::Abgelehnt { .. } => "Abgelehnt",
}
}
}
#[derive(Clone)]
pub enum KonfigurationCommand {
NbSendsBeauftragung {
orders_pid: Pruefidentifikator,
msb_mp_id: MarktpartnerCode,
malo: MaLo,
new_supplier: MarktpartnerCode,
message_ref: MessageRef,
},
ReceiveOrdrsp {
pid: Pruefidentifikator,
accepted: bool,
reason: Option<String>,
message_ref: MessageRef,
},
TimeoutExpired {
deadline_id: DeadlineId,
label: Box<str>,
},
}
impl CommandPayload for KonfigurationCommand {}
pub struct GpkeKonfigurationWorkflow;
impl Workflow for GpkeKonfigurationWorkflow {
type State = KonfigurationState;
type Event = KonfigurationEvent;
type Command = KonfigurationCommand;
fn on_deadline(
deadline: &mako_engine::deadline::Deadline,
state: &Self::State,
) -> Option<Self::Command> {
match (deadline.label(), state) {
(KONFIGURATION_WINDOW_LABEL, KonfigurationState::Beauftragt(_)) => {
Some(KonfigurationCommand::TimeoutExpired {
deadline_id: deadline.deadline_id(),
label: deadline.label().into(),
})
}
_ => None,
}
}
fn apply(state: Self::State, event: &Self::Event) -> Self::State {
match event {
KonfigurationEvent::BeauftragungGesendet {
orders_pid,
msb_mp_id,
malo,
new_supplier,
message_ref,
} => KonfigurationState::Beauftragt(BeauftragungData {
orders_pid: *orders_pid,
msb_mp_id: msb_mp_id.clone(),
malo: malo.clone(),
new_supplier: new_supplier.clone(),
message_ref: message_ref.clone(),
}),
KonfigurationEvent::BestaetigungErhalten { message_ref, .. } => {
match state {
KonfigurationState::Beauftragt(data) => KonfigurationState::Bestaetigt {
response_ref: message_ref.clone(),
data,
},
other => other, }
}
KonfigurationEvent::AblehungErhalten { reason, .. } => match state {
KonfigurationState::Beauftragt(data) => KonfigurationState::Abgelehnt {
reason: reason.clone(),
data,
},
other => other,
},
KonfigurationEvent::DeadlineExpired { label, .. } => {
match state {
KonfigurationState::Bestaetigt { .. }
| KonfigurationState::Abgelehnt { .. } => state,
KonfigurationState::Beauftragt(data) => KonfigurationState::Abgelehnt {
data,
reason: format!("deadline expired: {label}"),
},
KonfigurationState::New => state, }
}
}
}
fn handle(
state: &Self::State,
command: Self::Command,
) -> Result<WorkflowOutput<Self::Event>, WorkflowError> {
match command {
KonfigurationCommand::NbSendsBeauftragung {
orders_pid,
msb_mp_id,
malo,
new_supplier,
message_ref,
} => {
if !matches!(state, KonfigurationState::New) {
return Err(WorkflowError::invalid_state("New", state.label()));
}
if !ORDERS_PIDS.contains(&orders_pid.as_u32()) {
return Err(WorkflowError::rejected(format!(
"expected ORDERS PID 17134 or 17135, got {orders_pid}",
)));
}
let event = KonfigurationEvent::BeauftragungGesendet {
orders_pid,
msb_mp_id: msb_mp_id.clone(),
malo: malo.clone(),
new_supplier: new_supplier.clone(),
message_ref: message_ref.clone(),
};
let outbox = vec![PendingOutbox::new(
"ORDERS",
msb_mp_id.as_str(),
serde_json::json!({
"type": "Beauftragung",
"pid": orders_pid.as_u32(),
"malo": malo.as_str(),
"new_supplier": new_supplier.as_str(),
"orders_ref": message_ref.as_str(),
}),
)];
Ok(WorkflowOutput::with_outbox(vec![event], outbox))
}
KonfigurationCommand::ReceiveOrdrsp {
pid,
accepted,
reason,
message_ref,
} => {
let _data = match state {
KonfigurationState::Beauftragt(d) => d,
_ => return Err(WorkflowError::invalid_state("Beauftragt", state.label())),
};
if !ORDRSP_PIDS.contains(&pid.as_u32()) {
return Err(WorkflowError::rejected(format!(
"expected ORDRSP PID 19001 or 19002, got {pid}",
)));
}
let event = if accepted {
KonfigurationEvent::BestaetigungErhalten {
response_pid: pid,
message_ref,
}
} else {
KonfigurationEvent::AblehungErhalten {
response_pid: pid,
reason: reason.unwrap_or_else(|| "no reason provided".to_owned()),
message_ref,
}
};
Ok(vec![event].into())
}
KonfigurationCommand::TimeoutExpired { deadline_id, label } => {
if matches!(
state,
KonfigurationState::Bestaetigt { .. } | KonfigurationState::Abgelehnt { .. }
) {
return Ok(WorkflowOutput::events(vec![]));
}
Ok(vec![KonfigurationEvent::DeadlineExpired { deadline_id, label }].into())
}
}
}
}
#[derive(Debug)]
pub struct KonfigurationRecord {
pub status: &'static str,
pub msb_mp_id: Option<MarktpartnerCode>,
pub malo: Option<MaLo>,
pub event_count: usize,
}
impl Default for KonfigurationRecord {
fn default() -> Self {
Self {
status: "New",
msb_mp_id: None,
malo: None,
event_count: 0,
}
}
}
#[derive(Debug, Default)]
pub struct KonfigurationProjection {
pub records: HashMap<String, KonfigurationRecord>,
pub last_seq: u64,
}
impl Projection for KonfigurationProjection {
fn name(&self) -> &'static str {
"KonfigurationProjection"
}
fn handle_event(&mut self, envelope: &EventEnvelope) {
self.last_seq = self.last_seq.max(envelope.sequence_number);
let record = self
.records
.entry(envelope.stream_id.as_str().to_owned())
.or_default();
record.event_count += 1;
let Ok(event) = envelope.decode::<KonfigurationEvent>() else {
return;
};
match event {
KonfigurationEvent::BeauftragungGesendet {
msb_mp_id, malo, ..
} => {
record.status = "Beauftragt";
record.msb_mp_id = Some(msb_mp_id);
record.malo = Some(malo);
}
KonfigurationEvent::BestaetigungErhalten { .. } => {
record.status = "Bestaetigt";
}
KonfigurationEvent::AblehungErhalten { .. } => {
record.status = "Abgelehnt";
}
KonfigurationEvent::DeadlineExpired { .. } => {
record.status = "Abgelehnt";
}
}
}
fn last_sequence(&self) -> Option<u64> {
if self.last_seq == 0 {
None
} else {
Some(self.last_seq)
}
}
}