use std::collections::HashMap;
use mako_engine::types::Pruefidentifikator;
use mako_engine::{
envelope::EventEnvelope,
error::WorkflowError,
ids::DeadlineId,
projection::Projection,
types::{MarktpartnerCode, MessageRef},
workflow::{CommandPayload, EventPayload, Workflow, WorkflowOutput},
};
pub const INVOIC_PIDS: &[u32] = &[31001, 31002, 31004, 31005, 31006, 31007, 31008];
pub const ABRECHNUNG_WINDOW_LABEL: &str = "invoic-settlement-deadline";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum AbrechnungEvent {
InvoicReceived {
invoice_ref: MessageRef,
sender: MarktpartnerCode,
recipient: MarktpartnerCode,
document_date: String,
pruefidentifikator: Pruefidentifikator,
},
ValidationPassed {
invoice_ref: MessageRef,
},
InvoiceSettled,
InvoiceDisputed {
reason: String,
},
Rejected {
reason: String,
},
DeadlineExpired {
deadline_id: DeadlineId,
label: Box<str>,
},
}
impl EventPayload for AbrechnungEvent {
fn event_type(&self) -> &'static str {
match self {
Self::InvoicReceived { .. } => "AbrechnungInvoicReceived",
Self::ValidationPassed { .. } => "AbrechnungValidationPassed",
Self::InvoiceSettled => "AbrechnungInvoiceSettled",
Self::InvoiceDisputed { .. } => "AbrechnungInvoiceDisputed",
Self::Rejected { .. } => "AbrechnungRejected",
Self::DeadlineExpired { .. } => "AbrechnungDeadlineExpired",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AbrechnungData {
pub pruefidentifikator: Pruefidentifikator,
pub sender: MarktpartnerCode,
pub recipient: MarktpartnerCode,
pub document_date: String,
pub invoice_ref: MessageRef,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "status", content = "data")]
pub enum AbrechnungState {
New,
InvoicReceived(AbrechnungData),
ValidationPassed(AbrechnungData),
Settled(AbrechnungData),
Disputed {
data: AbrechnungData,
reason: String,
},
Rejected {
reason: String,
},
}
impl Default for AbrechnungState {
fn default() -> Self {
Self::New
}
}
impl AbrechnungState {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::New => "New",
Self::InvoicReceived(_) => "InvoicReceived",
Self::ValidationPassed(_) => "ValidationPassed",
Self::Settled(_) => "Settled",
Self::Disputed { .. } => "Disputed",
Self::Rejected { .. } => "Rejected",
}
}
#[must_use]
pub fn abrechnung_data(&self) -> Option<&AbrechnungData> {
match self {
Self::InvoicReceived(d) | Self::ValidationPassed(d) | Self::Settled(d) => Some(d),
Self::Disputed { data, .. } => Some(data),
Self::New | Self::Rejected { .. } => None,
}
}
}
#[derive(Clone)]
pub enum AbrechnungCommand {
ReceiveInvoic {
pid: Pruefidentifikator,
sender: MarktpartnerCode,
recipient: MarktpartnerCode,
invoice_ref: MessageRef,
document_date: String,
validation_passed: bool,
validation_errors: Vec<String>,
},
SettleInvoice,
DisputeInvoice {
reason: String,
},
TimeoutExpired {
deadline_id: DeadlineId,
label: Box<str>,
},
}
impl CommandPayload for AbrechnungCommand {}
pub struct GpkeAbrechnungWorkflow;
impl Workflow for GpkeAbrechnungWorkflow {
type State = AbrechnungState;
type Event = AbrechnungEvent;
type Command = AbrechnungCommand;
fn on_deadline(
deadline: &mako_engine::deadline::Deadline,
state: &Self::State,
) -> Option<Self::Command> {
match (deadline.label(), state) {
(
ABRECHNUNG_WINDOW_LABEL,
AbrechnungState::InvoicReceived(_) | AbrechnungState::ValidationPassed(_),
) => Some(AbrechnungCommand::TimeoutExpired {
deadline_id: deadline.deadline_id(),
label: deadline.label().into(),
}),
_ => None,
}
}
fn apply(state: Self::State, event: &Self::Event) -> Self::State {
match event {
AbrechnungEvent::InvoicReceived {
invoice_ref,
sender,
recipient,
document_date,
pruefidentifikator,
} => AbrechnungState::InvoicReceived(AbrechnungData {
pruefidentifikator: *pruefidentifikator,
sender: sender.clone(),
recipient: recipient.clone(),
document_date: document_date.clone(),
invoice_ref: invoice_ref.clone(),
}),
AbrechnungEvent::ValidationPassed { .. } => match state {
AbrechnungState::InvoicReceived(data) => AbrechnungState::ValidationPassed(data),
other => other,
},
AbrechnungEvent::InvoiceSettled => match state {
AbrechnungState::ValidationPassed(data) => AbrechnungState::Settled(data),
other => other,
},
AbrechnungEvent::InvoiceDisputed { reason } => match state {
AbrechnungState::ValidationPassed(data) => AbrechnungState::Disputed {
data,
reason: reason.clone(),
},
other => other,
},
AbrechnungEvent::Rejected { reason } => AbrechnungState::Rejected {
reason: reason.clone(),
},
AbrechnungEvent::DeadlineExpired { label, .. } => match state {
AbrechnungState::Settled(_)
| AbrechnungState::Disputed { .. }
| AbrechnungState::Rejected { .. } => state,
_ => AbrechnungState::Rejected {
reason: format!("deadline expired: {label}"),
},
},
}
}
fn handle(
state: &Self::State,
command: Self::Command,
) -> Result<WorkflowOutput<Self::Event>, WorkflowError> {
match command {
AbrechnungCommand::ReceiveInvoic {
pid,
sender,
recipient,
invoice_ref,
document_date,
validation_passed,
validation_errors,
} => {
if !matches!(state, AbrechnungState::New) {
return Err(WorkflowError::invalid_state("New", state.label()));
}
if !INVOIC_PIDS.contains(&pid.as_u32()) {
return Err(WorkflowError::rejected(format!(
"expected a GPKE INVOIC PID (31001/31002/31004–31008), got {pid}",
)));
}
let mut events = vec![AbrechnungEvent::InvoicReceived {
invoice_ref: invoice_ref.clone(),
sender,
recipient,
document_date,
pruefidentifikator: pid,
}];
if validation_passed {
events.push(AbrechnungEvent::ValidationPassed { invoice_ref });
} else {
events.push(AbrechnungEvent::Rejected {
reason: validation_errors.join("; "),
});
}
Ok(events.into())
}
AbrechnungCommand::SettleInvoice => {
if !matches!(state, AbrechnungState::ValidationPassed(_)) {
return Err(WorkflowError::invalid_state(
"ValidationPassed",
state.label(),
));
}
Ok(vec![AbrechnungEvent::InvoiceSettled].into())
}
AbrechnungCommand::DisputeInvoice { reason } => {
if !matches!(state, AbrechnungState::ValidationPassed(_)) {
return Err(WorkflowError::invalid_state(
"ValidationPassed",
state.label(),
));
}
Ok(vec![AbrechnungEvent::InvoiceDisputed { reason }].into())
}
AbrechnungCommand::TimeoutExpired { deadline_id, label } => {
if matches!(
state,
AbrechnungState::Settled(_)
| AbrechnungState::Disputed { .. }
| AbrechnungState::Rejected { .. }
) {
return Ok(WorkflowOutput::events(vec![]));
}
Ok(vec![AbrechnungEvent::DeadlineExpired { deadline_id, label }].into())
}
}
}
}
#[derive(Debug)]
pub struct AbrechnungRecord {
pub status: &'static str,
pub pruefidentifikator: Option<Pruefidentifikator>,
pub event_count: usize,
}
impl Default for AbrechnungRecord {
fn default() -> Self {
Self {
status: "New",
pruefidentifikator: None,
event_count: 0,
}
}
}
#[derive(Debug, Default)]
pub struct AbrechnungProjection {
pub records: HashMap<String, AbrechnungRecord>,
pub last_seq: u64,
}
impl Projection for AbrechnungProjection {
fn name(&self) -> &'static str {
"AbrechnungProjection"
}
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::<AbrechnungEvent>() else {
return;
};
match event {
AbrechnungEvent::InvoicReceived {
pruefidentifikator, ..
} => {
record.status = "InvoicReceived";
record.pruefidentifikator = Some(pruefidentifikator);
}
AbrechnungEvent::ValidationPassed { .. } => {
record.status = "ValidationPassed";
}
AbrechnungEvent::InvoiceSettled => {
record.status = "Settled";
}
AbrechnungEvent::InvoiceDisputed { .. } => {
record.status = "Disputed";
}
AbrechnungEvent::Rejected { .. } => {
record.status = "Rejected";
}
AbrechnungEvent::DeadlineExpired { .. } => {
record.status = "Rejected";
}
}
}
fn last_sequence(&self) -> Option<u64> {
if self.last_seq == 0 {
None
} else {
Some(self.last_seq)
}
}
}