From ddfdedc30289dc8eb9090263a11c361e3c7ce6a1 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Thu, 16 Jul 2026 12:13:47 +0200 Subject: [PATCH] fix(domain): correlate withdrawal by process instance to the subscribed execution (refs #12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verify-stack surfaced a Flowable 500: delivering messageEventReceived to the Beoordelen task's execution is wrong — a message boundary event's subscription lives on its own execution. Correlate instead by the registration's process instance id (recorded at submit): query the execution subscribed to RegistratieIngetrokken and deliver the message there. Withdrawal moves from IUserTaskClient to IWorkflowClient.WithdrawProcessAsync; the handler no longer needs a task lookup. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...adr-0014-withdrawal-cancels-the-process.md | 11 ++-- services/domain/Big.Application/Ports.cs | 19 +++--- .../Big.Application/WithdrawRegistration.cs | 25 +++----- .../FlowableWorkflowClient.cs | 37 ++++++++--- .../Big.Tests/BeoordeelRegistratieTests.cs | 2 +- services/domain/Big.Tests/Fakes.cs | 14 ++--- .../Big.Tests/FlowableWorkflowClientTests.cs | 62 ++++++++++++++----- services/domain/Big.Tests/WerkbakTests.cs | 6 +- .../Big.Tests/WithdrawRegistrationTests.cs | 47 +++++++------- .../acceptance/Support/InMemoryDomainPorts.cs | 18 +++--- 10 files changed, 144 insertions(+), 97 deletions(-) diff --git a/docs/architecture/adr-0014-withdrawal-cancels-the-process.md b/docs/architecture/adr-0014-withdrawal-cancels-the-process.md index 8bc2064..51cf3a6 100644 --- a/docs/architecture/adr-0014-withdrawal-cancels-the-process.md +++ b/docs/architecture/adr-0014-withdrawal-cancels-the-process.md @@ -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 diff --git a/services/domain/Big.Application/Ports.cs b/services/domain/Big.Application/Ports.cs index b75a805..4a26dd5 100644 --- a/services/domain/Big.Application/Ports.cs +++ b/services/domain/Big.Application/Ports.cs @@ -15,6 +15,14 @@ public interface IWorkflowClient /// aggregate. Returns the process instance id. /// Task StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default); + + /// + /// Cancel a running registratie process on withdrawal (S-11): correlate the + /// RegistratieIngetrokken 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. + /// + Task WithdrawProcessAsync(string processInstanceId, CancellationToken ct = default); } /// @@ -54,18 +62,11 @@ 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), 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); +/// complete it) and the registration it carries as a process variable. +public sealed record BeoordelingTask(string TaskId, 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 b218efc..92f632f 100644 --- a/services/domain/Big.Application/WithdrawRegistration.cs +++ b/services/domain/Big.Application/WithdrawRegistration.cs @@ -8,13 +8,13 @@ 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, 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 . +/// 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 completes its task best-effort. /// -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); } } diff --git a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs index 5aa8533..6a723c8 100644 --- a/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs +++ b/services/domain/Big.Infrastructure/FlowableWorkflowClient.cs @@ -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( + $"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 GetAsync(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(ct); + } + private async Task PostAsync(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? 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? Data); + + private sealed record ExecutionDto([property: JsonPropertyName("id")] string Id); + private sealed record AcquiredJob( [property: JsonPropertyName("id")] string Id, [property: JsonPropertyName("variables")] IReadOnlyList Variables) diff --git a/services/domain/Big.Tests/BeoordeelRegistratieTests.cs b/services/domain/Big.Tests/BeoordeelRegistratieTests.cs index d86cf8b..9ad48de 100644 --- a/services/domain/Big.Tests/BeoordeelRegistratieTests.cs +++ b/services/domain/Big.Tests/BeoordeelRegistratieTests.cs @@ -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() diff --git a/services/domain/Big.Tests/Fakes.cs b/services/domain/Big.Tests/Fakes.cs index c866314..539f554 100644 --- a/services/domain/Big.Tests/Fakes.cs +++ b/services/domain/Big.Tests/Fakes.cs @@ -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 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; + } } /// 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 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> GetOpenBeoordelingenAsync(CancellationToken ct = default) => Task.FromResult(open); @@ -63,12 +69,6 @@ internal sealed class FakeUserTaskClient(IReadOnlyList open) : Completed = (taskId, besluit); return Task.CompletedTask; } - - public Task WithdrawBeoordelingAsync(string executionId, CancellationToken ct = default) - { - WithdrawnExecutionId = executionId; - return Task.CompletedTask; - } } /// A fake ACL client that records the bsn it was asked to open a zaak for and returns a diff --git a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs index 6c8ddc0..aff3bf3 100644 --- a/services/domain/Big.Tests/FlowableWorkflowClientTests.cs +++ b/services/domain/Big.Tests/FlowableWorkflowClientTests.cs @@ -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(); + 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(() => 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] diff --git a/services/domain/Big.Tests/WerkbakTests.cs b/services/domain/Big.Tests/WerkbakTests.cs index 76449ac..0b3bdcf 100644 --- a/services/domain/Big.Tests/WerkbakTests.cs +++ b/services/domain/Big.Tests/WerkbakTests.cs @@ -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()); diff --git a/services/domain/Big.Tests/WithdrawRegistrationTests.cs b/services/domain/Big.Tests/WithdrawRegistrationTests.cs index eae7852..99de8b9 100644 --- a/services/domain/Big.Tests/WithdrawRegistrationTests.cs +++ b/services/domain/Big.Tests/WithdrawRegistrationTests.cs @@ -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(() => 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( () => 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)); diff --git a/tests/acceptance/Support/InMemoryDomainPorts.cs b/tests/acceptance/Support/InMemoryDomainPorts.cs index ac7e2e1..3e21752 100644 --- a/tests/acceptance/Support/InMemoryDomainPorts.cs +++ b/tests/acceptance/Support/InMemoryDomainPorts.cs @@ -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 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; + } } /// 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 _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> GetOpenBeoordelingenAsync(CancellationToken ct = default) => Task.FromResult>(_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; - } } /// An in-memory registration store for the domain acceptance scenario.