From d966a198fb9287903f28c9df3a4018ddec0dbf4e Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Thu, 16 Jul 2026 10:53:31 +0200 Subject: [PATCH] =?UTF-8?q?feat(domain):=20withdraw=20a=20registration=20?= =?UTF-8?q?=E2=86=92=20INGETROKKEN=20via=20POST=20/registrations/{id}/with?= =?UTF-8?q?draw=20(refs=20#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the Ingetrokken terminal status, Registration.Withdraw() (open-only, idempotent), the WithdrawRegistration handler, and the domain endpoint. Cancelling the running Flowable process (so the case leaves the werkbak) and the owner-scoped BFF/self-service action are follow-up sub-slices (S-11b/S-11c). Co-Authored-By: Claude Opus 4.8 (1M context) --- services/domain/Big.Api/Program.cs | 14 ++++++++ .../Big.Application/WithdrawRegistration.cs | 32 +++++++++++++++++++ services/domain/Big.Domain/Registration.cs | 18 ++++++++++- .../domain/Big.Domain/RegistrationStatus.cs | 6 +++- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 services/domain/Big.Application/WithdrawRegistration.cs diff --git a/services/domain/Big.Api/Program.cs b/services/domain/Big.Api/Program.cs index 176ef91..6b8dccb 100644 --- a/services/domain/Big.Api/Program.cs +++ b/services/domain/Big.Api/Program.cs @@ -26,6 +26,7 @@ builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -74,6 +75,19 @@ app.MapPost("/registrations/{id}/decide", async (string id, DecideRequest body, return Results.NoContent(); }); +// Withdraw a registration (S-11): the zorgprofessional pulls their own still-open submission back, +// advancing it to INGETROKKEN. Idempotent. The BFF reaches this behind a digid token, owner-scoped +// to the caller's bsn (S-11c); the domain trusts its callers (§8.3). Cancelling the running Flowable +// process is a later sub-slice (S-11b). +app.MapPost("/registrations/{id}/withdraw", async (string id, WithdrawRegistration withdraw, CancellationToken ct) => +{ + if (!Guid.TryParse(id, out var guid)) + return Results.NotFound(); + + await withdraw.HandleAsync(new WithdrawRegistrationCommand(new RegistrationId(guid)), ct); + return Results.NoContent(); +}); + // The behandelaar's werkbak (S-12): the registrations awaiting beoordeling, read from the open // Beoordelen user tasks (§8.2) and enriched with bsn + status. The BFF proxies this behind // medewerker-realm + behandelaar-role authorization; the domain trusts its callers (§8.3). diff --git a/services/domain/Big.Application/WithdrawRegistration.cs b/services/domain/Big.Application/WithdrawRegistration.cs new file mode 100644 index 0000000..282d35a --- /dev/null +++ b/services/domain/Big.Application/WithdrawRegistration.cs @@ -0,0 +1,32 @@ +using Big.Domain; + +namespace Big.Application; + +/// A zorgprofessional's request to withdraw their own registration ("trek aanvraag in"). +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. +/// +public sealed class WithdrawRegistration(IRegistrationStore store) +{ + public async Task HandleAsync(WithdrawRegistrationCommand command, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(command); + + 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. + if (registration.Status == RegistrationStatus.Ingetrokken) + return; + + registration.Withdraw(); + await store.SaveAsync(registration, ct); + } +} diff --git a/services/domain/Big.Domain/Registration.cs b/services/domain/Big.Domain/Registration.cs index 3aa620d..4b29eab 100644 --- a/services/domain/Big.Domain/Registration.cs +++ b/services/domain/Big.Domain/Registration.cs @@ -111,7 +111,23 @@ public sealed class Registration Status = RegistrationStatus.Afgewezen; } - // A decision is only valid while the registration is still open (INGEDIEND or IN_BEHANDELING). + /// + /// Withdraw the registration — the zorgprofessional pulls their own submission back (S-11). Allowed + /// while it is still open (INGEDIEND or IN_BEHANDELING) and needs no zaak; a registration that has + /// already been decided (INGESCHREVEN/AFGEWEZEN) can no longer be withdrawn. Re-withdrawing one + /// already is a no-op. + /// + public void Withdraw() + { + if (Status == RegistrationStatus.Ingetrokken) + return; + + RequireOpenForDecision(nameof(Withdraw)); + Status = RegistrationStatus.Ingetrokken; + } + + // A decision (or withdrawal) is only valid while the registration is still open (INGEDIEND or + // IN_BEHANDELING). private void RequireOpenForDecision(string decision) { if (Status is not (RegistrationStatus.Ingediend or RegistrationStatus.InBehandeling)) diff --git a/services/domain/Big.Domain/RegistrationStatus.cs b/services/domain/Big.Domain/RegistrationStatus.cs index edae5a9..fccfac7 100644 --- a/services/domain/Big.Domain/RegistrationStatus.cs +++ b/services/domain/Big.Domain/RegistrationStatus.cs @@ -3,7 +3,8 @@ namespace Big.Domain; /// The lifecycle states a moves through. Submission starts in /// ; a behandelaar takes it and decides it into one /// of the terminal states (approved) or (rejected). -/// Withdrawal and herregistratie states arrive in their own slices (S-11+). +/// A zorgprofessional can withdraw a still-open registration into (S-11). +/// The herregistratie state arrives in its own slice. public enum RegistrationStatus { /// Submitted by the zorgprofessional; the registratie process has been started. @@ -17,4 +18,7 @@ public enum RegistrationStatus /// Rejected by the behandelaar. Terminal. Afgewezen, + + /// Withdrawn by the zorgprofessional before a decision (S-11). Terminal. + Ingetrokken, }