feat(domain): withdrawal cancels the registratie process (S-11b, refs #12) #89
@@ -29,11 +29,12 @@ task; the Workflow Client correlates a `RegistratieIngetrokken` message to the t
|
||||
Client only delivers the message; it never reaches into Flowable to delete an instance. This keeps
|
||||
the workflow's control flow in the workflow (§8.2) and leaves an audit trail in Flowable history
|
||||
(the process ended via the ingetrokken path, not a raw delete).
|
||||
- **Correlated via the Beoordelen task's execution.** The `WithdrawRegistration` handler already
|
||||
knows the registration; it finds the open `Beoordelen` task for it (the same task-query the werkbak
|
||||
uses, §8.2) and asks the Workflow Client to deliver the withdrawal message to that task's execution
|
||||
(`messageEventReceived`). No separate correlation store is needed — the werkbak task set is the
|
||||
authoritative correlation, exactly as the beoordeling decision reuses it (ADR-0013).
|
||||
- **Correlated by the registration's own process instance.** The aggregate records its Flowable
|
||||
process instance id at submit, so the `WithdrawRegistration` handler correlates directly by that
|
||||
id — no task lookup. The Workflow Client asks Flowable for the execution **subscribed to** the
|
||||
`RegistratieIngetrokken` message in that instance and delivers `messageEventReceived` to it.
|
||||
Targeting the subscribed execution (not the user task's execution — a message boundary event's
|
||||
subscription lives on its own execution) is what makes the correlation land.
|
||||
- **Best-effort, mirroring the beoordeling.** If no open `Beoordelen` task is found (the process has
|
||||
not yet parked there — the `OpenZaakAanmaken` window — or has already ended), the withdrawal still
|
||||
stands: the aggregate is INGETROKKEN and the werkbak filters it out regardless (S-11b). We complete
|
||||
|
||||
@@ -15,6 +15,14 @@ public interface IWorkflowClient
|
||||
/// aggregate. Returns the process instance id.
|
||||
/// </summary>
|
||||
Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel a running <c>registratie</c> process on withdrawal (S-11): correlate the
|
||||
/// <c>RegistratieIngetrokken</c> message to the instance, tripping the interrupting message event
|
||||
/// that ends it (ADR-0014). Best-effort — if the instance is not waiting on that message (already
|
||||
/// ended, or not yet parked) it is a no-op; the aggregate is INGETROKKEN regardless.
|
||||
/// </summary>
|
||||
Task WithdrawProcessAsync(string processInstanceId, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,18 +62,11 @@ public interface IUserTaskClient
|
||||
/// <summary>Complete a beoordeling task, carrying the decision into the process as the
|
||||
/// <c>besluit</c> variable so the workflow can continue on the chosen branch.</summary>
|
||||
Task CompleteBeoordelingAsync(string taskId, BeoordelingsBesluit besluit, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Deliver the withdrawal message to a <c>Beoordelen</c> 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.</summary>
|
||||
Task WithdrawBeoordelingAsync(string executionId, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>A <c>Beoordelen</c> user task in the werkbak: the Flowable task id (needed to claim and
|
||||
/// 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.</summary>
|
||||
public sealed record BeoordelingTask(string TaskId, string ExecutionId, RegistrationId RegistrationId);
|
||||
/// complete it) and the registration it carries as a process variable.</summary>
|
||||
public sealed record BeoordelingTask(string TaskId, RegistrationId RegistrationId);
|
||||
|
||||
/// <summary>
|
||||
/// Persistence port for the <see cref="Registration"/> aggregate. In-memory for the minimal slice
|
||||
|
||||
@@ -8,13 +8,13 @@ public sealed record WithdrawRegistrationCommand(RegistrationId RegistrationId);
|
||||
/// <summary>
|
||||
/// The withdrawal use case (S-11): a zorgprofessional pulls a still-open registration back. It
|
||||
/// advances the aggregate to INGETROKKEN, persists it, then cancels the running registratie process
|
||||
/// by delivering the withdrawal message to its open <c>Beoordelen</c> 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 <c>Beoordelen</c> task is open (the process has not parked there yet, or has
|
||||
/// already ended) the withdrawal still stands — mirroring <see cref="BeoordeelRegistratie"/>.
|
||||
/// by correlating the withdrawal message to its instance (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 the
|
||||
/// registration never started a process the withdrawal still stands (the process cancel is skipped),
|
||||
/// mirroring how <see cref="BeoordeelRegistratie"/> completes its task best-effort.
|
||||
/// </summary>
|
||||
public sealed class WithdrawRegistration(IRegistrationStore store, IUserTaskClient tasks)
|
||||
public sealed class WithdrawRegistration(IRegistrationStore store, IWorkflowClient workflow)
|
||||
{
|
||||
public async Task HandleAsync(WithdrawRegistrationCommand command, CancellationToken ct = default)
|
||||
{
|
||||
@@ -29,16 +29,9 @@ public sealed class WithdrawRegistration(IRegistrationStore store, IUserTaskClie
|
||||
|
||||
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);
|
||||
// Cancel the running process (if one was started) so its Beoordelen task leaves the werkbak.
|
||||
if (registration.ProcessInstanceId is not null)
|
||||
await workflow.WithdrawProcessAsync(registration.ProcessInstanceId, ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,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, t.ExecutionId, RegistrationId.Parse(t.RegistrationId())))];
|
||||
return [.. tasks.Select(t => new BeoordelingTask(t.Id, RegistrationId.Parse(t.RegistrationId())))];
|
||||
}
|
||||
|
||||
public async Task ClaimAsync(string taskId, string behandelaar, CancellationToken ct = default)
|
||||
@@ -86,18 +86,35 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task WithdrawBeoordelingAsync(string executionId, CancellationToken ct = default)
|
||||
public async Task WithdrawProcessAsync(string processInstanceId, 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);
|
||||
// Correlate the withdrawal message to the instance: find the execution subscribed to it (the
|
||||
// interrupting message event's own execution — NOT the user task's), then deliver
|
||||
// messageEventReceived to that execution so the process ends (ADR-0014). If nothing is
|
||||
// subscribed (the process is not parked at Beoordelen) this is a best-effort no-op.
|
||||
var subscribed = await GetAsync<ExecutionQueryResult>(
|
||||
$"service/runtime/executions?messageEventSubscriptionName={IngetrokkenMessage}&processInstanceId={processInstanceId}",
|
||||
ct);
|
||||
|
||||
var execution = subscribed?.Data?.FirstOrDefault();
|
||||
if (execution is null)
|
||||
return;
|
||||
|
||||
var request = new MessageEventRequest("messageEventReceived", IngetrokkenMessage);
|
||||
using var response = await SendAsync(
|
||||
$"service/runtime/executions/{executionId}", request, ct, HttpMethod.Put);
|
||||
$"service/runtime/executions/{execution.Id}", request, ct, HttpMethod.Put);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
private async Task<TResponse?> GetAsync<TResponse>(string path, CancellationToken ct)
|
||||
{
|
||||
var message = new HttpRequestMessage(HttpMethod.Get, new Uri(options.BaseUrl, path));
|
||||
message.Headers.Authorization = new AuthenticationHeaderValue("Basic", BasicCredentials());
|
||||
using var response = await http.SendAsync(message, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<TResponse>(ct);
|
||||
}
|
||||
|
||||
private async Task<TResponse?> PostAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken ct)
|
||||
{
|
||||
using var response = await SendAsync(path, body, ct);
|
||||
@@ -154,7 +171,6 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
|
||||
|
||||
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<Variable>? Variables)
|
||||
@@ -172,6 +188,11 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
|
||||
|
||||
private sealed record ProcessInstance([property: JsonPropertyName("id")] string Id);
|
||||
|
||||
private sealed record ExecutionQueryResult(
|
||||
[property: JsonPropertyName("data")] IReadOnlyList<ExecutionDto>? Data);
|
||||
|
||||
private sealed record ExecutionDto([property: JsonPropertyName("id")] string Id);
|
||||
|
||||
private sealed record AcquiredJob(
|
||||
[property: JsonPropertyName("id")] string Id,
|
||||
[property: JsonPropertyName("variables")] IReadOnlyList<Variable> Variables)
|
||||
|
||||
@@ -19,7 +19,7 @@ public class BeoordeelRegistratieTests
|
||||
}
|
||||
|
||||
private static FakeUserTaskClient TaskFor(Registration registration) =>
|
||||
new([new BeoordelingTask("task-1", "exec-1", registration.Id)]);
|
||||
new([new BeoordelingTask("task-1", registration.Id)]);
|
||||
|
||||
[Fact]
|
||||
public async Task Goedkeuren_sets_the_zaak_status_via_the_acl_and_marks_the_registration_ingeschreven()
|
||||
|
||||
@@ -32,6 +32,7 @@ internal sealed class FakeWorkflowClient(string processInstanceId = "proc-1", Ac
|
||||
: IWorkflowClient
|
||||
{
|
||||
public RegistrationId? StartedFor { get; private set; }
|
||||
public string? WithdrawnProcessInstanceId { get; private set; }
|
||||
|
||||
public Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
|
||||
{
|
||||
@@ -39,6 +40,12 @@ internal sealed class FakeWorkflowClient(string processInstanceId = "proc-1", Ac
|
||||
StartedFor = registrationId;
|
||||
return Task.FromResult(processInstanceId);
|
||||
}
|
||||
|
||||
public Task WithdrawProcessAsync(string processInstanceId, CancellationToken ct = default)
|
||||
{
|
||||
WithdrawnProcessInstanceId = processInstanceId;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A fake user-task client for the werkbak/decision use cases: returns a scripted set of
|
||||
@@ -47,7 +54,6 @@ internal sealed class FakeUserTaskClient(IReadOnlyList<BeoordelingTask> open) :
|
||||
{
|
||||
public (string TaskId, string Behandelaar)? Claimed { get; private set; }
|
||||
public (string TaskId, BeoordelingsBesluit Besluit)? Completed { get; private set; }
|
||||
public string? WithdrawnExecutionId { get; private set; }
|
||||
|
||||
public Task<IReadOnlyList<BeoordelingTask>> GetOpenBeoordelingenAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult(open);
|
||||
@@ -63,12 +69,6 @@ internal sealed class FakeUserTaskClient(IReadOnlyList<BeoordelingTask> open) :
|
||||
Completed = (taskId, besluit);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task WithdrawBeoordelingAsync(string executionId, CancellationToken ct = default)
|
||||
{
|
||||
WithdrawnExecutionId = executionId;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A fake ACL client that records the bsn it was asked to open a zaak for and returns a
|
||||
|
||||
@@ -158,14 +158,13 @@ public class FlowableWorkflowClientTests
|
||||
var capture = new RequestCapture();
|
||||
var client = Client(capture.Responds(HttpStatusCode.OK,
|
||||
$$"""
|
||||
{"data":[{"id":"task-1","executionId":"exec-1","variables":[{"name":"registrationId","type":"string","value":"{{rid}}"}]}],"total":1}
|
||||
{"data":[{"id":"task-1","variables":[{"name":"registrationId","type":"string","value":"{{rid}}"}]}],"total":1}
|
||||
"""));
|
||||
|
||||
var tasks = await client.GetOpenBeoordelingenAsync();
|
||||
|
||||
var task = Assert.Single(tasks);
|
||||
Assert.Equal("task-1", task.TaskId);
|
||||
Assert.Equal("exec-1", task.ExecutionId);
|
||||
Assert.Equal(rid, task.RegistrationId);
|
||||
Assert.Equal(HttpMethod.Post, capture.Seen!.Method);
|
||||
Assert.Equal("http://flowable/flowable-rest/service/query/tasks",
|
||||
@@ -244,29 +243,58 @@ public class FlowableWorkflowClientTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Withdraw_beoordeling_delivers_the_message_to_the_task_execution()
|
||||
public async Task Withdraw_process_correlates_the_message_to_the_subscribed_execution()
|
||||
{
|
||||
var capture = new RequestCapture();
|
||||
var client = Client(capture.Responds(HttpStatusCode.OK));
|
||||
HttpRequestMessage? getReq = null;
|
||||
HttpRequestMessage? putReq = null;
|
||||
string? putBody = null;
|
||||
var client = Client(new StubHandler(async req =>
|
||||
{
|
||||
if (req.Method == HttpMethod.Get)
|
||||
{
|
||||
getReq = req;
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("""{"data":[{"id":"exec-9"}]}""", Encoding.UTF8, "application/json"),
|
||||
};
|
||||
}
|
||||
putReq = req;
|
||||
putBody = await req.Content!.ReadAsStringAsync();
|
||||
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||
}));
|
||||
|
||||
await client.WithdrawBeoordelingAsync("exec-1");
|
||||
await client.WithdrawProcessAsync("pi-1");
|
||||
|
||||
// A PUT messageEventReceived on the execution trips the Beoordelen boundary event (ADR-0014).
|
||||
Assert.Equal(HttpMethod.Put, capture.Seen!.Method);
|
||||
Assert.Equal("http://flowable/flowable-rest/service/runtime/executions/exec-1",
|
||||
capture.Seen.RequestUri!.ToString());
|
||||
Assert.Equal("rest-admin:test", DecodeBasic(capture.Seen));
|
||||
Assert.Contains("\"action\":\"messageEventReceived\"", capture.Body);
|
||||
Assert.Contains("\"messageName\":\"RegistratieIngetrokken\"", capture.Body);
|
||||
// 1. Find the execution subscribed to the withdrawal message for this instance.
|
||||
Assert.Equal(HttpMethod.Get, getReq!.Method);
|
||||
Assert.Contains("service/runtime/executions", getReq.RequestUri!.ToString());
|
||||
Assert.Contains("messageEventSubscriptionName=RegistratieIngetrokken", getReq.RequestUri!.Query);
|
||||
Assert.Contains("processInstanceId=pi-1", getReq.RequestUri!.Query);
|
||||
// 2. Deliver messageEventReceived to that execution (PUT), tripping the interrupting event.
|
||||
Assert.Equal(HttpMethod.Put, putReq!.Method);
|
||||
Assert.Equal("http://flowable/flowable-rest/service/runtime/executions/exec-9",
|
||||
putReq.RequestUri!.ToString());
|
||||
Assert.Contains("\"action\":\"messageEventReceived\"", putBody);
|
||||
Assert.Contains("\"messageName\":\"RegistratieIngetrokken\"", putBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Withdraw_beoordeling_throws_when_flowable_rejects_the_request()
|
||||
public async Task Withdraw_process_is_a_no_op_when_no_execution_is_subscribed()
|
||||
{
|
||||
var capture = new RequestCapture();
|
||||
var client = Client(capture.Responds(HttpStatusCode.InternalServerError));
|
||||
var methods = new List<HttpMethod>();
|
||||
var client = Client(new StubHandler(req =>
|
||||
{
|
||||
methods.Add(req.Method);
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("""{"data":[]}""", Encoding.UTF8, "application/json"),
|
||||
});
|
||||
}));
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(() => client.WithdrawBeoordelingAsync("exec-1"));
|
||||
await client.WithdrawProcessAsync("pi-1");
|
||||
|
||||
// No subscribed execution → no message delivered (best-effort), no throw.
|
||||
Assert.DoesNotContain(HttpMethod.Put, methods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -18,7 +18,7 @@ public class WerkbakTests
|
||||
registration.AttachZaak(FakeAclClient.DefaultZaakUrl);
|
||||
registration.TakeIntoBehandeling();
|
||||
store.Seed(registration);
|
||||
var tasks = new FakeUserTaskClient([new BeoordelingTask("task-1", "exec-1", registration.Id)]);
|
||||
var tasks = new FakeUserTaskClient([new BeoordelingTask("task-1", registration.Id)]);
|
||||
var werkbak = new Werkbak(tasks, store);
|
||||
|
||||
var items = await werkbak.GetAsync();
|
||||
@@ -41,7 +41,7 @@ public class WerkbakTests
|
||||
public async Task Skips_a_task_whose_registration_is_unknown()
|
||||
{
|
||||
// Defensive: the werkbak never invents an item for a task the domain has no aggregate for.
|
||||
var tasks = new FakeUserTaskClient([new BeoordelingTask("task-1", "exec-1", RegistrationId.New())]);
|
||||
var tasks = new FakeUserTaskClient([new BeoordelingTask("task-1", RegistrationId.New())]);
|
||||
var werkbak = new Werkbak(tasks, new FakeRegistrationStore());
|
||||
|
||||
Assert.Empty(await werkbak.GetAsync());
|
||||
@@ -56,7 +56,7 @@ public class WerkbakTests
|
||||
var registration = Registration.Submit("123456782");
|
||||
registration.Withdraw();
|
||||
store.Seed(registration);
|
||||
var tasks = new FakeUserTaskClient([new BeoordelingTask("task-1", "exec-1", registration.Id)]);
|
||||
var tasks = new FakeUserTaskClient([new BeoordelingTask("task-1", registration.Id)]);
|
||||
var werkbak = new Werkbak(tasks, store);
|
||||
|
||||
Assert.Empty(await werkbak.GetAsync());
|
||||
|
||||
@@ -5,15 +5,20 @@ namespace Big.Tests;
|
||||
|
||||
public class WithdrawRegistrationTests
|
||||
{
|
||||
private static FakeUserTaskClient NoTasks() => new([]);
|
||||
private static Registration Submitted(string processInstanceId = "proc-1")
|
||||
{
|
||||
var registration = Registration.Submit("123456782");
|
||||
registration.RecordProcessStarted(processInstanceId);
|
||||
return registration;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Withdrawing_marks_the_registration_ingetrokken_and_persists_it()
|
||||
{
|
||||
var store = new FakeRegistrationStore();
|
||||
var registration = Registration.Submit("123456782");
|
||||
var registration = Submitted();
|
||||
store.Seed(registration);
|
||||
var handler = new WithdrawRegistration(store, NoTasks());
|
||||
var handler = new WithdrawRegistration(store, new FakeWorkflowClient());
|
||||
|
||||
await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id));
|
||||
|
||||
@@ -23,43 +28,42 @@ public class WithdrawRegistrationTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Withdrawing_cancels_the_open_beoordelen_task_via_its_execution()
|
||||
public async Task Withdrawing_cancels_the_running_process()
|
||||
{
|
||||
var store = new FakeRegistrationStore();
|
||||
var registration = Registration.Submit("123456782");
|
||||
var registration = Submitted("proc-42");
|
||||
store.Seed(registration);
|
||||
var tasks = new FakeUserTaskClient([new BeoordelingTask("task-1", "exec-1", registration.Id)]);
|
||||
var handler = new WithdrawRegistration(store, tasks);
|
||||
var workflow = new FakeWorkflowClient();
|
||||
var handler = new WithdrawRegistration(store, workflow);
|
||||
|
||||
await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id));
|
||||
|
||||
// The withdrawal message is delivered to the Beoordelen task's execution, tripping the
|
||||
// boundary event that ends the process (ADR-0014).
|
||||
Assert.Equal("exec-1", tasks.WithdrawnExecutionId);
|
||||
// The process the registration recorded at submit is cancelled (ADR-0014).
|
||||
Assert.Equal("proc-42", workflow.WithdrawnProcessInstanceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Withdrawing_with_no_open_task_still_marks_ingetrokken()
|
||||
public async Task Withdrawing_before_a_process_was_started_still_marks_ingetrokken()
|
||||
{
|
||||
// If the process has not parked at Beoordelen yet (or already ended), the withdrawal stands:
|
||||
// the aggregate is INGETROKKEN and nothing is cancelled.
|
||||
// A registration with no recorded process (e.g. start never happened) can still be withdrawn;
|
||||
// there is simply no process to cancel.
|
||||
var store = new FakeRegistrationStore();
|
||||
var registration = Registration.Submit("123456782");
|
||||
var registration = Registration.Submit("123456782"); // no RecordProcessStarted
|
||||
store.Seed(registration);
|
||||
var tasks = NoTasks();
|
||||
var handler = new WithdrawRegistration(store, tasks);
|
||||
var workflow = new FakeWorkflowClient();
|
||||
var handler = new WithdrawRegistration(store, workflow);
|
||||
|
||||
await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id));
|
||||
|
||||
Assert.Equal(RegistrationStatus.Ingetrokken, (await store.GetAsync(registration.Id))!.Status);
|
||||
Assert.Null(tasks.WithdrawnExecutionId);
|
||||
Assert.Null(workflow.WithdrawnProcessInstanceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rejects_a_null_command_without_touching_the_store()
|
||||
{
|
||||
var store = new FakeRegistrationStore();
|
||||
var handler = new WithdrawRegistration(store, NoTasks());
|
||||
var handler = new WithdrawRegistration(store, new FakeWorkflowClient());
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => handler.HandleAsync(null!));
|
||||
Assert.Equal(0, store.SaveCount);
|
||||
@@ -69,7 +73,7 @@ public class WithdrawRegistrationTests
|
||||
public async Task Withdrawing_an_unknown_registration_throws()
|
||||
{
|
||||
var store = new FakeRegistrationStore();
|
||||
var handler = new WithdrawRegistration(store, NoTasks());
|
||||
var handler = new WithdrawRegistration(store, new FakeWorkflowClient());
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => handler.HandleAsync(new WithdrawRegistrationCommand(RegistrationId.New())));
|
||||
@@ -81,9 +85,10 @@ public class WithdrawRegistrationTests
|
||||
public async Task Re_withdrawing_an_already_ingetrokken_registration_is_idempotent()
|
||||
{
|
||||
var store = new FakeRegistrationStore();
|
||||
var registration = Registration.Submit("123456782");
|
||||
var registration = Submitted();
|
||||
store.Seed(registration);
|
||||
var handler = new WithdrawRegistration(store, NoTasks());
|
||||
var workflow = new FakeWorkflowClient();
|
||||
var handler = new WithdrawRegistration(store, workflow);
|
||||
|
||||
await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id));
|
||||
await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id));
|
||||
|
||||
@@ -11,12 +11,19 @@ public sealed class InMemoryWorkflowClient : IWorkflowClient
|
||||
public const string StartedProcessInstanceId = "proc-acc-1";
|
||||
|
||||
public RegistrationId? StartedFor { get; private set; }
|
||||
public string? WithdrawnProcessInstanceId { get; private set; }
|
||||
|
||||
public Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
|
||||
{
|
||||
StartedFor = registrationId;
|
||||
return Task.FromResult(StartedProcessInstanceId);
|
||||
}
|
||||
|
||||
public Task WithdrawProcessAsync(string processInstanceId, CancellationToken ct = default)
|
||||
{
|
||||
WithdrawnProcessInstanceId = processInstanceId;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>An in-memory ACL stand-in: records the bsn it opened a zaak for and returns a fixed URL,
|
||||
@@ -50,10 +57,8 @@ public sealed class InMemoryUserTaskClient : IUserTaskClient
|
||||
private readonly List<BeoordelingTask> _open = [];
|
||||
|
||||
public (string TaskId, BeoordelingsBesluit Besluit)? Completed { get; private set; }
|
||||
public string? WithdrawnExecutionId { get; private set; }
|
||||
|
||||
public void Open(RegistrationId registrationId) =>
|
||||
_open.Add(new BeoordelingTask($"task-{registrationId}", $"exec-{registrationId}", registrationId));
|
||||
public void Open(RegistrationId registrationId) => _open.Add(new BeoordelingTask($"task-{registrationId}", registrationId));
|
||||
|
||||
public Task<IReadOnlyList<BeoordelingTask>> GetOpenBeoordelingenAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<BeoordelingTask>>(_open);
|
||||
@@ -66,13 +71,6 @@ public sealed class InMemoryUserTaskClient : IUserTaskClient
|
||||
_open.RemoveAll(t => t.TaskId == taskId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task WithdrawBeoordelingAsync(string executionId, CancellationToken ct = default)
|
||||
{
|
||||
WithdrawnExecutionId = executionId;
|
||||
_open.RemoveAll(t => t.ExecutionId == executionId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>An in-memory registration store for the domain acceptance scenario.</summary>
|
||||
|
||||
Reference in New Issue
Block a user