diff --git a/services/domain/Big.Application/Ports.cs b/services/domain/Big.Application/Ports.cs
index 0859d7c..b75a805 100644
--- a/services/domain/Big.Application/Ports.cs
+++ b/services/domain/Big.Application/Ports.cs
@@ -54,11 +54,18 @@ public interface IUserTaskClient
/// Complete a beoordeling task, carrying the decision into the process as the
/// besluit variable so the workflow can continue on the chosen branch.
Task CompleteBeoordelingAsync(string taskId, BeoordelingsBesluit besluit, CancellationToken ct = default);
+
+ /// Deliver the withdrawal message to a Beoordelen task's execution, tripping the
+ /// process's interrupting message boundary event so the registratie process cancels (S-11,
+ /// ADR-0014). The registration itself is already INGETROKKEN in the domain; this ends its
+ /// workflow so the case leaves the werkbak.
+ Task WithdrawBeoordelingAsync(string executionId, CancellationToken ct = default);
}
/// A Beoordelen user task in the werkbak: the Flowable task id (needed to claim and
-/// complete it) and the registration it carries as a process variable.
-public sealed record BeoordelingTask(string TaskId, RegistrationId RegistrationId);
+/// complete it), the execution it runs in (needed to deliver the withdrawal message to its boundary
+/// event), and the registration it carries as a process variable.
+public sealed record BeoordelingTask(string TaskId, string ExecutionId, RegistrationId RegistrationId);
///
/// Persistence port for the aggregate. In-memory for the minimal slice
diff --git a/services/domain/Big.Application/WithdrawRegistration.cs b/services/domain/Big.Application/WithdrawRegistration.cs
index 282d35a..b218efc 100644
--- a/services/domain/Big.Application/WithdrawRegistration.cs
+++ b/services/domain/Big.Application/WithdrawRegistration.cs
@@ -7,13 +7,14 @@ public sealed record WithdrawRegistrationCommand(RegistrationId RegistrationId);
///
/// The withdrawal use case (S-11): a zorgprofessional pulls a still-open registration back. It
-/// advances the aggregate to INGETROKKEN and persists it. Idempotent — a repeated or redelivered
-/// withdrawal of an already-withdrawn registration is a no-op (the aggregate is not persisted again).
-/// Cancelling the running Flowable process (so the case leaves the behandelaar's werkbak) is a later
-/// sub-slice (S-11b, via a BPMN message event); this sub-slice owns the domain transition only —
-/// mirroring how the beoordeling's rejection deferred its zaak propagation.
+/// advances the aggregate to INGETROKKEN, persists it, then cancels the running registratie process
+/// by delivering the withdrawal message to its open Beoordelen task (ADR-0014), so the case
+/// leaves the behandelaar's werkbak. Idempotent — a repeated or redelivered withdrawal of an
+/// already-withdrawn registration is a no-op (not persisted or cancelled again). Cancelling is
+/// best-effort: if no Beoordelen task is open (the process has not parked there yet, or has
+/// already ended) the withdrawal still stands — mirroring .
///
-public sealed class WithdrawRegistration(IRegistrationStore store)
+public sealed class WithdrawRegistration(IRegistrationStore store, IUserTaskClient tasks)
{
public async Task HandleAsync(WithdrawRegistrationCommand command, CancellationToken ct = default)
{
@@ -22,11 +23,22 @@ public sealed class WithdrawRegistration(IRegistrationStore store)
var registration = await store.GetAsync(command.RegistrationId, ct)
?? throw new InvalidOperationException($"No registration {command.RegistrationId} to withdraw.");
- // A repeated withdrawal is a no-op: don't persist the already-withdrawn aggregate again.
+ // A repeated withdrawal is a no-op: don't persist or cancel the already-withdrawn one again.
if (registration.Status == RegistrationStatus.Ingetrokken)
return;
registration.Withdraw();
await store.SaveAsync(registration, ct);
+ await CancelWorkflowTaskAsync(command.RegistrationId, ct);
+ }
+
+ // Cancel the workflow: deliver the withdrawal message to the open Beoordelen task's execution so
+ // the process's boundary event ends it. If none is open the withdrawal still stands.
+ private async Task CancelWorkflowTaskAsync(RegistrationId registrationId, CancellationToken ct)
+ {
+ var open = await tasks.GetOpenBeoordelingenAsync(ct);
+ var task = open.FirstOrDefault(t => t.RegistrationId == registrationId);
+ if (task is not null)
+ await tasks.WithdrawBeoordelingAsync(task.ExecutionId, ct);
}
}
diff --git a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs
index f782cea..5aa8533 100644
--- a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs
+++ b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs
@@ -23,6 +23,7 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
private const string RegistrationIdVariable = "registrationId";
private const string ZaakUrlVariable = "zaakUrl";
private const string BesluitVariable = "besluit";
+ private const string IngetrokkenMessage = "RegistratieIngetrokken";
public async Task StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
{
@@ -65,7 +66,7 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
"service/query/tasks", request, ct);
var tasks = page?.Data ?? [];
- return [.. tasks.Select(t => new BeoordelingTask(t.Id, RegistrationId.Parse(t.RegistrationId())))];
+ return [.. tasks.Select(t => new BeoordelingTask(t.Id, t.ExecutionId, RegistrationId.Parse(t.RegistrationId())))];
}
public async Task ClaimAsync(string taskId, string behandelaar, CancellationToken ct = default)
@@ -85,6 +86,18 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
response.EnsureSuccessStatusCode();
}
+ public async Task WithdrawBeoordelingAsync(string executionId, CancellationToken ct = default)
+ {
+ // Deliver the withdrawal message to the Beoordelen task's execution, tripping the process's
+ // interrupting message boundary event so the registratie instance ends (ADR-0014). Flowable
+ // takes messageEventReceived as a PUT on the subscribed execution.
+ var request = new MessageEventRequest("messageEventReceived", IngetrokkenMessage);
+
+ using var response = await SendAsync(
+ $"service/runtime/executions/{executionId}", request, ct, HttpMethod.Put);
+ response.EnsureSuccessStatusCode();
+ }
+
private async Task PostAsync(string path, TRequest body, CancellationToken ct)
{
using var response = await SendAsync(path, body, ct);
@@ -92,9 +105,9 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
return await response.Content.ReadFromJsonAsync(ct);
}
- private Task SendAsync(string path, TRequest body, CancellationToken ct)
+ private Task SendAsync(string path, TRequest body, CancellationToken ct, HttpMethod? method = null)
{
- var message = new HttpRequestMessage(HttpMethod.Post, new Uri(options.BaseUrl, path))
+ var message = new HttpRequestMessage(method ?? HttpMethod.Post, new Uri(options.BaseUrl, path))
{
Content = JsonContent.Create(body),
};
@@ -132,11 +145,16 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
[property: JsonPropertyName("action")] string Action,
[property: JsonPropertyName("variables")] IReadOnlyList Variables);
+ private sealed record MessageEventRequest(
+ [property: JsonPropertyName("action")] string Action,
+ [property: JsonPropertyName("messageName")] string MessageName);
+
private sealed record TaskQueryResult(
[property: JsonPropertyName("data")] IReadOnlyList? Data);
private sealed record UserTaskDto(
[property: JsonPropertyName("id")] string Id,
+ [property: JsonPropertyName("executionId")] string ExecutionId,
// Flowable's task-query returns the (included) process variables under "variables", not
// "processVariables"; the request opts in via includeProcessVariables.
[property: JsonPropertyName("variables")] IReadOnlyList? Variables)
diff --git a/workflows/registratie.bpmn b/workflows/registratie.bpmn
index 51f7de9..99a7ec7 100644
--- a/workflows/registratie.bpmn
+++ b/workflows/registratie.bpmn
@@ -11,7 +11,13 @@
S-03 added the external task (Workflow Client / ACL). S-12 adds the behandelaar's beoordeling
as a user task: the process parks here until a behandelaar claims and completes it with a
`besluit` (goedkeuren/afwijzen). The registrationId set at start rides along as a process
- variable so the werkbak can correlate each task back to its aggregate. -->
+ variable so the werkbak can correlate each task back to its aggregate.
+ S-11 adds withdrawal: while parked at Beoordelen the citizen can trek de aanvraag in — an
+ interrupting message boundary event (RegistratieIngetrokken) cancels the task and ends the
+ process via a dedicated "ingetrokken" end (ADR-0014). The Workflow Client delivers the message
+ to the task's execution; the BPMN owns the cancellation path. -->
+
+
@@ -29,6 +35,16 @@
+
+
+
+
+
+
+
+
+
@@ -57,6 +73,16 @@
+
+
+
+
+
+
+
+
+
+