use mako_engine::types::Pruefidentifikator;
use mako_engine::{
error::WorkflowError,
ids::DeadlineId,
outbox::PendingOutbox,
types::{MaLo, MarktpartnerCode, MessageRef},
workflow::{CommandPayload, EventPayload, Workflow, WorkflowOutput},
};
pub const WORKFLOW_NAME: &str = "gpke-lf-anmeldung";
pub const ANFRAGE_PIDS_LF: &[u32] = &[
55001, 55002, 55016, ];
pub const ANTWORT_PIDS_LF: &[u32] = &[
55003, 55004, 55005, 55006, 55017, 55018, ];
pub const NB_RESPONSE_WINDOW_LABEL: &str = "nb-response-window";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum LfAnmeldungEvent {
Initiated {
pruefidentifikator: Pruefidentifikator,
location_id: MaLo,
sender: MarktpartnerCode,
receiver: MarktpartnerCode,
process_date: String,
},
AntwortReceived {
response_pid: Pruefidentifikator,
accepted: bool,
reason: Option<String>,
response_ref: MessageRef,
},
Activated,
DeadlineExpired {
deadline_id: DeadlineId,
label: Box<str>,
},
}
impl EventPayload for LfAnmeldungEvent {
fn event_type(&self) -> &'static str {
match self {
Self::Initiated { .. } => "LfAnmeldungInitiated",
Self::AntwortReceived { .. } => "LfAnmeldungAntwortReceived",
Self::Activated => "LfAnmeldungActivated",
Self::DeadlineExpired { .. } => "LfAnmeldungDeadlineExpired",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LfAnmeldungData {
pub pruefidentifikator: Pruefidentifikator,
pub location_id: MaLo,
pub sender: MarktpartnerCode,
pub receiver: MarktpartnerCode,
pub process_date: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum LfAnmeldungState {
New,
Pending(LfAnmeldungData),
Active(LfAnmeldungData),
Rejected {
reason: String,
},
}
impl LfAnmeldungState {
fn label(&self) -> &'static str {
match self {
Self::New => "New",
Self::Pending(_) => "Pending",
Self::Active(_) => "Active",
Self::Rejected { .. } => "Rejected",
}
}
}
impl Default for LfAnmeldungState {
fn default() -> Self {
Self::New
}
}
#[derive(Clone)]
pub enum LfAnmeldungCommand {
InitiateAnmeldung {
pid: Pruefidentifikator,
sender: MarktpartnerCode,
receiver: MarktpartnerCode,
location_id: MaLo,
process_date: String,
},
HandleAntwort {
response_pid: Pruefidentifikator,
accepted: bool,
reason: Option<String>,
response_ref: MessageRef,
},
Activate,
TimeoutExpired {
deadline_id: DeadlineId,
label: Box<str>,
},
}
impl CommandPayload for LfAnmeldungCommand {}
pub struct GpkeLfAnmeldungWorkflow;
impl Workflow for GpkeLfAnmeldungWorkflow {
type State = LfAnmeldungState;
type Event = LfAnmeldungEvent;
type Command = LfAnmeldungCommand;
fn on_deadline(
deadline: &mako_engine::deadline::Deadline,
state: &Self::State,
) -> Option<Self::Command> {
match (deadline.label(), state) {
(NB_RESPONSE_WINDOW_LABEL, LfAnmeldungState::Pending(_)) => {
Some(LfAnmeldungCommand::TimeoutExpired {
deadline_id: deadline.deadline_id(),
label: deadline.label().into(),
})
}
_ => None,
}
}
fn apply(state: Self::State, event: &Self::Event) -> Self::State {
match event {
LfAnmeldungEvent::Initiated {
pruefidentifikator,
location_id,
sender,
receiver,
process_date,
} => LfAnmeldungState::Pending(LfAnmeldungData {
pruefidentifikator: *pruefidentifikator,
location_id: location_id.clone(),
sender: sender.clone(),
receiver: receiver.clone(),
process_date: process_date.clone(),
}),
LfAnmeldungEvent::AntwortReceived {
accepted, reason, ..
} => {
if *accepted {
match state {
LfAnmeldungState::Pending(data) => LfAnmeldungState::Active(data),
other => other,
}
} else {
LfAnmeldungState::Rejected {
reason: reason.clone().unwrap_or_else(|| "Ablehnung".to_owned()),
}
}
}
LfAnmeldungEvent::Activated => match state {
LfAnmeldungState::Active(data) => LfAnmeldungState::Active(data),
other => other,
},
LfAnmeldungEvent::DeadlineExpired { label, .. } => match state {
LfAnmeldungState::Active(_) | LfAnmeldungState::Rejected { .. } => state,
_ => LfAnmeldungState::Rejected {
reason: format!("deadline expired: {label}"),
},
},
}
}
fn handle(
state: &Self::State,
command: Self::Command,
) -> Result<WorkflowOutput<Self::Event>, WorkflowError> {
match command {
LfAnmeldungCommand::InitiateAnmeldung {
pid,
sender,
receiver,
location_id,
process_date,
} => {
if !matches!(state, LfAnmeldungState::New) {
return Err(WorkflowError::invalid_state("New", state.label()));
}
if !ANFRAGE_PIDS_LF.contains(&pid.as_u32()) {
return Err(WorkflowError::rejected(format!(
"expected an LF Anfrage PID (55001, 55002, 55016), got {pid}",
)));
}
let event = LfAnmeldungEvent::Initiated {
pruefidentifikator: pid,
location_id: location_id.clone(),
sender: sender.clone(),
receiver: receiver.clone(),
process_date: process_date.clone(),
};
let outbox = PendingOutbox::new(
"UTILMD",
receiver.as_str(),
serde_json::json!({
"direction": "outbound",
"pid": pid.as_u32(),
"sender": sender.as_str(),
"receiver": receiver.as_str(),
"malo": location_id.as_str(),
"process_date": process_date,
}),
);
Ok(WorkflowOutput::with_outbox(vec![event], vec![outbox]))
}
LfAnmeldungCommand::HandleAntwort {
response_pid,
accepted,
reason,
response_ref,
} => {
if !matches!(state, LfAnmeldungState::Pending(_)) {
return Err(WorkflowError::invalid_state("Pending", state.label()));
}
if !ANTWORT_PIDS_LF.contains(&response_pid.as_u32()) {
return Err(WorkflowError::rejected(format!(
"expected an LF Antwort PID (55003–55006, 55017, 55018), got {response_pid}",
)));
}
Ok(vec![LfAnmeldungEvent::AntwortReceived {
response_pid,
accepted,
reason,
response_ref,
}]
.into())
}
LfAnmeldungCommand::Activate => {
if !matches!(state, LfAnmeldungState::Active(_)) {
return Err(WorkflowError::invalid_state("Active", state.label()));
}
Ok(vec![LfAnmeldungEvent::Activated].into())
}
LfAnmeldungCommand::TimeoutExpired { deadline_id, label } => {
if matches!(
state,
LfAnmeldungState::Active(_) | LfAnmeldungState::Rejected { .. }
) {
return Ok(WorkflowOutput::events(vec![]));
}
Ok(vec![LfAnmeldungEvent::DeadlineExpired { deadline_id, label }].into())
}
}
}
}
#[cfg(test)]
mod tests {
use mako_engine::{
types::{MaLo, MarktpartnerCode, MessageRef, Pruefidentifikator},
workflow::Workflow,
};
use super::*;
fn make_initiate(pid: u32) -> LfAnmeldungCommand {
LfAnmeldungCommand::InitiateAnmeldung {
pid: Pruefidentifikator::new(pid).unwrap(),
sender: MarktpartnerCode::new("4012345000009"),
receiver: MarktpartnerCode::new("9900123456789"),
location_id: MaLo::new("10001234567"),
process_date: "2026-10-01".to_owned(),
}
}
#[test]
fn initiate_lieferbeginn_transitions_to_pending() {
let state = LfAnmeldungState::New;
let out = GpkeLfAnmeldungWorkflow::handle(&state, make_initiate(55001)).unwrap();
assert_eq!(out.events.len(), 1);
assert_eq!(out.outbox.len(), 1, "must enqueue UTILMD outbox entry");
let new_state = GpkeLfAnmeldungWorkflow::apply(state, &out.events[0]);
assert!(matches!(new_state, LfAnmeldungState::Pending(_)));
}
#[test]
fn initiate_lieferende_transitions_to_pending() {
let state = LfAnmeldungState::New;
let out = GpkeLfAnmeldungWorkflow::handle(&state, make_initiate(55002)).unwrap();
let new_state = GpkeLfAnmeldungWorkflow::apply(state, &out.events[0]);
assert!(matches!(new_state, LfAnmeldungState::Pending(_)));
}
#[test]
fn initiate_kuendigung_transitions_to_pending() {
let state = LfAnmeldungState::New;
let out = GpkeLfAnmeldungWorkflow::handle(&state, make_initiate(55016)).unwrap();
let new_state = GpkeLfAnmeldungWorkflow::apply(state, &out.events[0]);
assert!(matches!(new_state, LfAnmeldungState::Pending(_)));
}
#[test]
fn nb_acceptance_transitions_to_active() {
let initiated_event = LfAnmeldungEvent::Initiated {
pruefidentifikator: Pruefidentifikator::new(55001).unwrap(),
location_id: MaLo::new("10001234567"),
sender: MarktpartnerCode::new("4012345000009"),
receiver: MarktpartnerCode::new("9900123456789"),
process_date: "2026-10-01".to_owned(),
};
let state = GpkeLfAnmeldungWorkflow::apply(LfAnmeldungState::New, &initiated_event);
let cmd = LfAnmeldungCommand::HandleAntwort {
response_pid: Pruefidentifikator::new(55003).unwrap(),
accepted: true,
reason: None,
response_ref: MessageRef::new("NB-RESP-001"),
};
let out = GpkeLfAnmeldungWorkflow::handle(&state, cmd).unwrap();
assert_eq!(out.events.len(), 1);
let final_state = GpkeLfAnmeldungWorkflow::apply(state, &out.events[0]);
assert!(matches!(final_state, LfAnmeldungState::Active(_)));
}
#[test]
fn nb_rejection_transitions_to_rejected() {
let initiated_event = LfAnmeldungEvent::Initiated {
pruefidentifikator: Pruefidentifikator::new(55001).unwrap(),
location_id: MaLo::new("10001234567"),
sender: MarktpartnerCode::new("4012345000009"),
receiver: MarktpartnerCode::new("9900123456789"),
process_date: "2026-10-01".to_owned(),
};
let state = GpkeLfAnmeldungWorkflow::apply(LfAnmeldungState::New, &initiated_event);
let cmd = LfAnmeldungCommand::HandleAntwort {
response_pid: Pruefidentifikator::new(55004).unwrap(),
accepted: false,
reason: Some("MaLo nicht in Netzgebiet".to_owned()),
response_ref: MessageRef::new("NB-RESP-002"),
};
let out = GpkeLfAnmeldungWorkflow::handle(&state, cmd).unwrap();
let final_state = GpkeLfAnmeldungWorkflow::apply(state, &out.events[0]);
assert!(matches!(final_state, LfAnmeldungState::Rejected { .. }));
}
#[test]
fn invalid_pid_is_rejected() {
let state = LfAnmeldungState::New;
let err = GpkeLfAnmeldungWorkflow::handle(&state, make_initiate(55003));
assert!(err.is_err());
}
#[test]
fn timeout_on_pending_transitions_to_rejected() {
use mako_engine::ids::DeadlineId;
let initiated_event = LfAnmeldungEvent::Initiated {
pruefidentifikator: Pruefidentifikator::new(55001).unwrap(),
location_id: MaLo::new("10001234567"),
sender: MarktpartnerCode::new("4012345000009"),
receiver: MarktpartnerCode::new("9900123456789"),
process_date: "2026-10-01".to_owned(),
};
let state = GpkeLfAnmeldungWorkflow::apply(LfAnmeldungState::New, &initiated_event);
let cmd = LfAnmeldungCommand::TimeoutExpired {
deadline_id: DeadlineId::new(),
label: "nb-response-window".into(),
};
let out = GpkeLfAnmeldungWorkflow::handle(&state, cmd).unwrap();
let final_state = GpkeLfAnmeldungWorkflow::apply(state, &out.events[0]);
assert!(matches!(final_state, LfAnmeldungState::Rejected { .. }));
}
#[test]
fn timeout_on_active_is_noop() {
use mako_engine::ids::DeadlineId;
let initiated_event = LfAnmeldungEvent::Initiated {
pruefidentifikator: Pruefidentifikator::new(55001).unwrap(),
location_id: MaLo::new("10001234567"),
sender: MarktpartnerCode::new("4012345000009"),
receiver: MarktpartnerCode::new("9900123456789"),
process_date: "2026-10-01".to_owned(),
};
let state = GpkeLfAnmeldungWorkflow::apply(LfAnmeldungState::New, &initiated_event);
let accepted_event = LfAnmeldungEvent::AntwortReceived {
response_pid: Pruefidentifikator::new(55003).unwrap(),
accepted: true,
reason: None,
response_ref: MessageRef::new("REF-002"),
};
let state = GpkeLfAnmeldungWorkflow::apply(state, &accepted_event);
assert!(matches!(state, LfAnmeldungState::Active(_)));
let cmd = LfAnmeldungCommand::TimeoutExpired {
deadline_id: DeadlineId::new(),
label: "nb-response-window".into(),
};
let out = GpkeLfAnmeldungWorkflow::handle(&state, cmd).unwrap();
assert_eq!(out.events.len(), 0, "timeout is no-op on Active");
}
}