use std::collections::HashMap;
use mako_engine::types::Pruefidentifikator;
use mako_engine::{
envelope::EventEnvelope,
error::WorkflowError,
ids::DeadlineId,
outbox::PendingOutbox,
projection::Projection,
types::{MarktpartnerCode, MessageRef},
workflow::{CommandPayload, EventPayload, Workflow, WorkflowOutput},
};
use rubo4e::current::Rechnung;
pub const INVOIC_PIDS: &[u32] = &[31001, 31002, 31005, 31006];
pub const REMADV_PIDS: &[u32] = &[33001, 33002, 33003, 33004];
pub const COMDIS_ABLEHNUNG_REMADV_PID: u32 = 29001;
pub const ABRECHNUNG_WINDOW_LABEL: &str = "invoic-settlement-deadline";
pub const WORKFLOW_NAME: &str = "gpke-abrechnung";
#[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,
#[serde(default, skip_serializing_if = "Option::is_none")]
rechnung: Option<Box<Rechnung>>,
},
ValidationPassed {
invoice_ref: MessageRef,
},
InvoiceSettled,
InvoiceDisputed {
reason: String,
},
Rejected {
reason: String,
},
DeadlineExpired {
deadline_id: DeadlineId,
label: Box<str>,
},
InvoicSent {
pruefidentifikator: Pruefidentifikator,
sender: MarktpartnerCode,
recipient: MarktpartnerCode,
document_date: String,
invoice_ref: MessageRef,
},
RemadvReceived {
pid: Pruefidentifikator,
remadv_ref: MessageRef,
sender: MarktpartnerCode,
is_confirmed: bool,
},
ComdisAbLehnungReceived {
comdis_ref: MessageRef,
},
}
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",
Self::InvoicSent { .. } => "AbrechnungInvoicSent",
Self::RemadvReceived { .. } => "AbrechnungRemadvReceived",
Self::ComdisAbLehnungReceived { .. } => "AbrechnungComdisAbLehnungReceived",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AbrechnungData {
pub pruefidentifikator: Pruefidentifikator,
pub sender: MarktpartnerCode,
pub recipient: MarktpartnerCode,
pub document_date: String,
pub invoice_ref: MessageRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rechnung: Option<Box<Rechnung>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "status", content = "data")]
#[derive(Default)]
pub enum AbrechnungState {
#[default]
New,
InvoicReceived(AbrechnungData),
ValidationPassed(AbrechnungData),
Settled(AbrechnungData),
Disputed {
data: AbrechnungData,
reason: String,
},
Rejected {
reason: String,
},
InvoicSent(AbrechnungData),
PaymentConfirmed(AbrechnungData),
PaymentDisputed {
data: AbrechnungData,
remadv_pid: Pruefidentifikator,
},
ComdisRejected(AbrechnungData),
}
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",
Self::InvoicSent(_) => "InvoicSent",
Self::PaymentConfirmed(_) => "PaymentConfirmed",
Self::PaymentDisputed { .. } => "PaymentDisputed",
Self::ComdisRejected(_) => "ComdisRejected",
}
}
#[must_use]
pub fn abrechnung_data(&self) -> Option<&AbrechnungData> {
match self {
Self::InvoicReceived(d)
| Self::ValidationPassed(d)
| Self::Settled(d)
| Self::InvoicSent(d)
| Self::PaymentConfirmed(d)
| Self::ComdisRejected(d) => Some(d),
Self::Disputed { data, .. } | Self::PaymentDisputed { 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>,
rechnung: Option<Box<Rechnung>>,
},
SettleInvoice,
DisputeInvoice {
reason: String,
},
TimeoutExpired {
deadline_id: DeadlineId,
label: Box<str>,
},
SendInvoic {
pid: Pruefidentifikator,
sender: MarktpartnerCode,
recipient: MarktpartnerCode,
document_date: String,
invoice_ref: MessageRef,
},
ReceiveRemadv {
pid: Pruefidentifikator,
remadv_ref: MessageRef,
sender: MarktpartnerCode,
},
ReceiveComdis {
comdis_ref: MessageRef,
},
}
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,
rechnung,
} => AbrechnungState::InvoicReceived(AbrechnungData {
pruefidentifikator: *pruefidentifikator,
sender: sender.clone(),
recipient: recipient.clone(),
document_date: document_date.clone(),
invoice_ref: invoice_ref.clone(),
rechnung: rechnung.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 { .. }
| AbrechnungState::PaymentConfirmed(_)
| AbrechnungState::PaymentDisputed { .. }
| AbrechnungState::ComdisRejected(_) => state,
_ => AbrechnungState::Rejected {
reason: format!("deadline expired: {label}"),
},
},
AbrechnungEvent::InvoicSent {
pruefidentifikator,
sender,
recipient,
document_date,
invoice_ref,
} => AbrechnungState::InvoicSent(AbrechnungData {
pruefidentifikator: *pruefidentifikator,
sender: sender.clone(),
recipient: recipient.clone(),
document_date: document_date.clone(),
invoice_ref: invoice_ref.clone(),
rechnung: None,
}),
AbrechnungEvent::RemadvReceived {
pid, is_confirmed, ..
} => match state {
AbrechnungState::InvoicSent(data) => {
if *is_confirmed {
AbrechnungState::PaymentConfirmed(data)
} else {
AbrechnungState::PaymentDisputed {
remadv_pid: *pid,
data,
}
}
}
other => other,
},
AbrechnungEvent::ComdisAbLehnungReceived { .. } => match state {
AbrechnungState::ValidationPassed(data)
| AbrechnungState::InvoicSent(data)
| AbrechnungState::Settled(data) => AbrechnungState::ComdisRejected(data),
other => other,
},
}
}
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,
rechnung,
} => {
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/31005/31006), got {pid}",
)));
}
let mut events = vec![AbrechnungEvent::InvoicReceived {
invoice_ref: invoice_ref.clone(),
sender: sender.clone(),
recipient: recipient.clone(),
document_date: document_date.clone(),
pruefidentifikator: pid,
rechnung: rechnung.clone(),
}];
let mut outbox: Vec<PendingOutbox> = Vec::new();
if validation_passed {
events.push(AbrechnungEvent::ValidationPassed {
invoice_ref: invoice_ref.clone(),
});
outbox.push(
PendingOutbox::new(
"ProcessInitiated",
recipient.as_str(),
serde_json::json!({
"pid": pid.as_u32(),
"invoice_ref": invoice_ref.as_str(),
"sender_mp_id": sender.as_str(),
"rechnung": serde_json::to_value(rechnung.as_deref())
.unwrap_or(serde_json::Value::Null),
}),
)
.caused_by(1),
);
} else {
events.push(AbrechnungEvent::Rejected {
reason: validation_errors.join("; "),
});
}
Ok(WorkflowOutput::with_outbox(events, outbox))
}
AbrechnungCommand::SettleInvoice => {
if !matches!(state, AbrechnungState::ValidationPassed(_)) {
return Err(WorkflowError::invalid_state(
"ValidationPassed",
state.label(),
));
}
let pid = state
.abrechnung_data()
.map(|d| d.pruefidentifikator.as_u32())
.unwrap_or(0);
let invoice_ref = state
.abrechnung_data()
.map(|d| d.invoice_ref.to_string())
.unwrap_or_default();
let outbox = vec![PendingOutbox::new(
"ProcessCompleted",
"",
serde_json::json!({
"pid": pid,
"invoice_ref": invoice_ref,
"outcome": "settled",
}),
)];
Ok(WorkflowOutput::with_outbox(
vec![AbrechnungEvent::InvoiceSettled],
outbox,
))
}
AbrechnungCommand::DisputeInvoice { reason } => {
if !matches!(state, AbrechnungState::ValidationPassed(_)) {
return Err(WorkflowError::invalid_state(
"ValidationPassed",
state.label(),
));
}
let pid = state
.abrechnung_data()
.map(|d| d.pruefidentifikator.as_u32())
.unwrap_or(0);
let invoice_ref = state
.abrechnung_data()
.map(|d| d.invoice_ref.to_string())
.unwrap_or_default();
let outbox = vec![PendingOutbox::new(
"ProcessCompleted",
"",
serde_json::json!({
"pid": pid,
"invoice_ref": invoice_ref,
"outcome": "disputed",
"reason": &reason,
}),
)];
Ok(WorkflowOutput::with_outbox(
vec![AbrechnungEvent::InvoiceDisputed { reason }],
outbox,
))
}
AbrechnungCommand::TimeoutExpired { deadline_id, label } => {
if matches!(
state,
AbrechnungState::Settled(_)
| AbrechnungState::Disputed { .. }
| AbrechnungState::Rejected { .. }
| AbrechnungState::PaymentConfirmed(_)
| AbrechnungState::PaymentDisputed { .. }
| AbrechnungState::ComdisRejected(_)
) {
return Ok(WorkflowOutput::events(vec![]));
}
Ok(vec![AbrechnungEvent::DeadlineExpired { deadline_id, label }].into())
}
AbrechnungCommand::SendInvoic {
pid,
sender,
recipient,
document_date,
invoice_ref,
} => {
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/31005/31006), got {pid}",
)));
}
Ok(vec![AbrechnungEvent::InvoicSent {
pruefidentifikator: pid,
sender,
recipient,
document_date,
invoice_ref,
}]
.into())
}
AbrechnungCommand::ReceiveRemadv {
pid,
remadv_ref,
sender,
} => {
if !matches!(state, AbrechnungState::InvoicSent(_)) {
return Err(WorkflowError::invalid_state("InvoicSent", state.label()));
}
if !REMADV_PIDS.contains(&pid.as_u32()) {
return Err(WorkflowError::rejected(format!(
"expected a GPKE REMADV PID (33001–33004), got {pid}",
)));
}
let is_confirmed = pid.as_u32() == 33001;
Ok(vec![AbrechnungEvent::RemadvReceived {
pid,
remadv_ref,
sender,
is_confirmed,
}]
.into())
}
AbrechnungCommand::ReceiveComdis { comdis_ref } => {
if matches!(
state,
AbrechnungState::New
| AbrechnungState::InvoicReceived(_)
| AbrechnungState::Rejected { .. }
| AbrechnungState::ComdisRejected(_)
) {
return Err(WorkflowError::invalid_state(
"ValidationPassed|Settled",
state.label(),
));
}
Ok(vec![AbrechnungEvent::ComdisAbLehnungReceived { comdis_ref }].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";
}
AbrechnungEvent::InvoicSent {
pruefidentifikator, ..
} => {
record.status = "InvoicSent";
record.pruefidentifikator = Some(pruefidentifikator);
}
AbrechnungEvent::RemadvReceived { is_confirmed, .. } => {
record.status = if is_confirmed {
"PaymentConfirmed"
} else {
"PaymentDisputed"
};
}
AbrechnungEvent::ComdisAbLehnungReceived { .. } => {
record.status = "ComdisRejected";
}
}
}
fn last_sequence(&self) -> Option<u64> {
if self.last_seq == 0 {
None
} else {
Some(self.last_seq)
}
}
}