From 23125ef4921317c17c6791a9c6f53e7f00031f84 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 14 Jul 2026 17:15:41 +0200 Subject: [PATCH 1/7] test(domain): a behandelaar can take a registration into behandeling (refs #13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First step of S-12a (beoordeling decision model): an INGEDIEND registration moves to IN_BEHANDELING when a behandelaar picks it up. Red — TakeIntoBehandeling() and the InBehandeling status do not exist yet. Co-Authored-By: Claude Opus 4.8 (1M context) --- services/domain/Big.Tests/RegistrationTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/domain/Big.Tests/RegistrationTests.cs b/services/domain/Big.Tests/RegistrationTests.cs index b069ecc..18ee1d9 100644 --- a/services/domain/Big.Tests/RegistrationTests.cs +++ b/services/domain/Big.Tests/RegistrationTests.cs @@ -121,4 +121,14 @@ public class RegistrationTests Assert.Contains("only an INGEDIEND", ex.Message); Assert.Equal(RegistrationStatus.Ingeschreven, registration.Status); } + + [Fact] + public void Taking_a_registration_into_behandeling_moves_it_to_in_behandeling() + { + var registration = Registration.Submit("123456782"); + + registration.TakeIntoBehandeling(); + + Assert.Equal(RegistrationStatus.InBehandeling, registration.Status); + } } -- 2.49.1 From 09000d8e6411e53a146bde24b3f0c6d4851b1f04 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 14 Jul 2026 17:18:18 +0200 Subject: [PATCH 2/7] feat(domain): take a registration into behandeling; generalise decisions (refs #13) Adds InBehandeling + Afgewezen statuses and TakeIntoBehandeling(). Generalises the behandelaar decision: Approve() (requires a zaak) and new Reject() both act on an INGEDIEND or IN_BEHANDELING registration, moving it to Ingeschreven/Afgewezen. Co-Authored-By: Claude Opus 4.8 (1M context) --- services/domain/Big.Domain/Registration.cs | 51 ++++++++++++++++--- .../domain/Big.Domain/RegistrationStatus.cs | 15 ++++-- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/services/domain/Big.Domain/Registration.cs b/services/domain/Big.Domain/Registration.cs index be1f1ea..3aa620d 100644 --- a/services/domain/Big.Domain/Registration.cs +++ b/services/domain/Big.Domain/Registration.cs @@ -66,10 +66,28 @@ public sealed class Registration } /// - /// Approve the registration — the behandelaar's decision to enter it in the register. Advances - /// . - /// Requires an opened zaak (the approval sets that zaak's status via the ACL), and only a - /// submitted registration can be approved: re-approving is rejected as an invalid transition. + /// A behandelaar picks the registration up for beoordeling. Advances + /// . + /// Re-taking one already is a no-op (the same or + /// another behandelaar re-opens it); a decided registration can no longer be taken into behandeling. + /// + public void TakeIntoBehandeling() + { + if (Status == RegistrationStatus.InBehandeling) + return; + + if (Status != RegistrationStatus.Ingediend) + throw new InvalidOperationException( + $"Registration {Id} is {Status}; only an INGEDIEND registration can be taken into behandeling."); + + Status = RegistrationStatus.InBehandeling; + } + + /// + /// Approve the registration — the behandelaar's decision to enter it in the register. Advances a + /// submitted or in-behandeling registration to . + /// Requires an opened zaak (the approval sets that zaak's status via the ACL); a registration that + /// has already been decided cannot be approved again. /// public void Approve() { @@ -77,10 +95,27 @@ public sealed class Registration throw new InvalidOperationException( $"Registration {Id} has no zaak yet; it cannot be approved before its zaak is opened."); - if (Status != RegistrationStatus.Ingediend) - throw new InvalidOperationException( - $"Registration {Id} is {Status}; only an INGEDIEND registration can be approved."); - + RequireOpenForDecision(nameof(Approve)); Status = RegistrationStatus.Ingeschreven; } + + /// + /// Reject the registration — the behandelaar's decision not to enter it in the register. Advances a + /// submitted or in-behandeling registration to . Unlike + /// approval this needs no zaak: a registration can be rejected before or after its zaak is opened. + /// A registration that has already been decided cannot be rejected again. + /// + public void Reject() + { + RequireOpenForDecision(nameof(Reject)); + Status = RegistrationStatus.Afgewezen; + } + + // A decision 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)) + throw new InvalidOperationException( + $"Registration {Id} is {Status}; only an INGEDIEND or IN_BEHANDELING registration can be decided ({decision})."); + } } diff --git a/services/domain/Big.Domain/RegistrationStatus.cs b/services/domain/Big.Domain/RegistrationStatus.cs index 60bb171..edae5a9 100644 --- a/services/domain/Big.Domain/RegistrationStatus.cs +++ b/services/domain/Big.Domain/RegistrationStatus.cs @@ -1,13 +1,20 @@ namespace Big.Domain; -/// The lifecycle states a moves through. The walking -/// skeleton knows and the terminal ; withdrawal, -/// beoordeling and herregistratie states arrive in their own slices (Iteration 2+). +/// 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+). public enum RegistrationStatus { /// Submitted by the zorgprofessional; the registratie process has been started. Ingediend, - /// Approved: entered in the register. Terminal in the walking skeleton (S-09b). + /// Picked up by a behandelaar for beoordeling (S-12). + InBehandeling, + + /// Approved: entered in the register. Terminal. Ingeschreven, + + /// Rejected by the behandelaar. Terminal. + Afgewezen, } -- 2.49.1 From 5c05879cdec46da28e811ed3932b9e0b92594f6a Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 14 Jul 2026 17:19:02 +0200 Subject: [PATCH 3/7] =?UTF-8?q?test(domain):=20lock=20beoordeling=20transi?= =?UTF-8?q?tions=20=E2=80=94=20reject,=20idempotent=20re-take,=20guards=20?= =?UTF-8?q?(refs=20#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../domain/Big.Tests/RegistrationTests.cs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/services/domain/Big.Tests/RegistrationTests.cs b/services/domain/Big.Tests/RegistrationTests.cs index 18ee1d9..2221201 100644 --- a/services/domain/Big.Tests/RegistrationTests.cs +++ b/services/domain/Big.Tests/RegistrationTests.cs @@ -131,4 +131,90 @@ public class RegistrationTests Assert.Equal(RegistrationStatus.InBehandeling, registration.Status); } + + [Fact] + public void Re_taking_an_in_behandeling_registration_is_idempotent() + { + var registration = Registration.Submit("123456782"); + registration.TakeIntoBehandeling(); + + registration.TakeIntoBehandeling(); + + Assert.Equal(RegistrationStatus.InBehandeling, registration.Status); + } + + [Fact] + public void Taking_a_decided_registration_into_behandeling_is_rejected() + { + var registration = Registration.Submit("123456782"); + registration.AttachZaak(new Uri("http://openzaak/zaken/api/v1/zaken/abc")); + registration.Approve(); + + var ex = Assert.Throws(() => registration.TakeIntoBehandeling()); + Assert.Contains("only an INGEDIEND", ex.Message); + Assert.Equal(RegistrationStatus.Ingeschreven, registration.Status); + } + + [Fact] + public void Approving_an_in_behandeling_registration_with_a_zaak_sets_it_ingeschreven() + { + var registration = Registration.Submit("123456782"); + registration.AttachZaak(new Uri("http://openzaak/zaken/api/v1/zaken/abc")); + registration.TakeIntoBehandeling(); + + registration.Approve(); + + Assert.Equal(RegistrationStatus.Ingeschreven, registration.Status); + } + + [Fact] + public void Rejecting_a_registration_sets_it_afgewezen() + { + var registration = Registration.Submit("123456782"); + + registration.Reject(); + + Assert.Equal(RegistrationStatus.Afgewezen, registration.Status); + } + + [Fact] + public void Rejecting_needs_no_zaak() + { + // A registration can be turned down before its zaak is ever opened, so Reject must not require one. + var registration = Registration.Submit("123456782"); + + registration.Reject(); + + Assert.Equal(RegistrationStatus.Afgewezen, registration.Status); + Assert.Null(registration.ZaakUrl); + } + + [Fact] + public void Rejecting_an_in_behandeling_registration_sets_it_afgewezen() + { + var registration = Registration.Submit("123456782"); + registration.TakeIntoBehandeling(); + + registration.Reject(); + + Assert.Equal(RegistrationStatus.Afgewezen, registration.Status); + } + + [Fact] + public void Deciding_an_already_decided_registration_is_rejected() + { + var registration = Registration.Submit("123456782"); + registration.Reject(); + + var approveEx = Assert.Throws(() => + { + registration.AttachZaak(new Uri("http://openzaak/zaken/api/v1/zaken/abc")); + registration.Approve(); + }); + Assert.Contains("IN_BEHANDELING", approveEx.Message); + + var rejectEx = Assert.Throws(() => registration.Reject()); + Assert.Contains("Afgewezen", rejectEx.Message); + Assert.Equal(RegistrationStatus.Afgewezen, registration.Status); + } } -- 2.49.1 From 63699c3f55120000d9c75c5569eb366cf81d002a Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 14 Jul 2026 17:20:22 +0200 Subject: [PATCH 4/7] =?UTF-8?q?test(domain):=20beoordeling=20use-case=20?= =?UTF-8?q?=E2=80=94=20goedkeuren=20via=20ACL,=20afwijzen,=20idempotency?= =?UTF-8?q?=20(refs=20#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Red — the BeoordeelRegistratie use-case and BeoordelingsBesluit do not exist yet. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Big.Tests/BeoordeelRegistratieTests.cs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 services/domain/Big.Tests/BeoordeelRegistratieTests.cs diff --git a/services/domain/Big.Tests/BeoordeelRegistratieTests.cs b/services/domain/Big.Tests/BeoordeelRegistratieTests.cs new file mode 100644 index 0000000..de51af2 --- /dev/null +++ b/services/domain/Big.Tests/BeoordeelRegistratieTests.cs @@ -0,0 +1,142 @@ +using Big.Application; +using Big.Domain; + +namespace Big.Tests; + +/// +/// The beoordeling use case (S-12): a behandelaar's decision on a registration. Goedkeuren sets the +/// zaak's final status via the ACL (§8.1) and marks the aggregate INGESCHREVEN; Afwijzen marks it +/// AFGEWEZEN in the domain (propagating a rejection to the zaak is a later sub-slice). Both are +/// idempotent so a repeated or redelivered decision is a no-op. +/// +public class BeoordeelRegistratieTests +{ + private static Registration WithZaak(string bsn = "123456782") + { + var registration = Registration.Submit(bsn); + registration.AttachZaak(FakeAclClient.DefaultZaakUrl); + return registration; + } + + [Fact] + public async Task Goedkeuren_sets_the_zaak_status_via_the_acl_and_marks_the_registration_ingeschreven() + { + var store = new FakeRegistrationStore(); + var acl = new FakeAclClient(); + var registration = WithZaak(); + store.Seed(registration); + var handler = new BeoordeelRegistratie(store, acl); + + await handler.HandleAsync(new BeoordeelRegistratieCommand(registration.Id, BeoordelingsBesluit.Goedkeuren)); + + var saved = await store.GetAsync(registration.Id); + Assert.Equal(RegistrationStatus.Ingeschreven, saved!.Status); + Assert.Equal(FakeAclClient.DefaultZaakUrl, acl.ApprovedZaakUrl); + Assert.Equal(1, acl.ApproveCallCount); + Assert.Equal(1, store.SaveCount); + } + + [Fact] + public async Task Afwijzen_marks_the_registration_afgewezen_without_calling_the_acl() + { + var store = new FakeRegistrationStore(); + var acl = new FakeAclClient(); + var registration = WithZaak(); + store.Seed(registration); + var handler = new BeoordeelRegistratie(store, acl); + + await handler.HandleAsync(new BeoordeelRegistratieCommand(registration.Id, BeoordelingsBesluit.Afwijzen)); + + var saved = await store.GetAsync(registration.Id); + Assert.Equal(RegistrationStatus.Afgewezen, saved!.Status); + Assert.Equal(0, acl.ApproveCallCount); + Assert.Equal(1, store.SaveCount); + } + + [Fact] + public async Task Goedkeuren_an_in_behandeling_registration_marks_it_ingeschreven() + { + var store = new FakeRegistrationStore(); + var acl = new FakeAclClient(); + var registration = WithZaak(); + registration.TakeIntoBehandeling(); + store.Seed(registration); + var handler = new BeoordeelRegistratie(store, acl); + + await handler.HandleAsync(new BeoordeelRegistratieCommand(registration.Id, BeoordelingsBesluit.Goedkeuren)); + + Assert.Equal(RegistrationStatus.Ingeschreven, (await store.GetAsync(registration.Id))!.Status); + } + + [Fact] + public async Task Rejects_a_null_command_without_touching_the_store_or_acl() + { + var store = new FakeRegistrationStore(); + var acl = new FakeAclClient(); + var handler = new BeoordeelRegistratie(store, acl); + + await Assert.ThrowsAsync(() => handler.HandleAsync(null!)); + Assert.Equal(0, acl.ApproveCallCount); + Assert.Equal(0, store.SaveCount); + } + + [Fact] + public async Task Deciding_an_unknown_registration_throws_and_does_not_call_the_acl() + { + var store = new FakeRegistrationStore(); + var acl = new FakeAclClient(); + var handler = new BeoordeelRegistratie(store, acl); + + var ex = await Assert.ThrowsAsync(() => + handler.HandleAsync(new BeoordeelRegistratieCommand(RegistrationId.New(), BeoordelingsBesluit.Goedkeuren))); + Assert.Contains("No registration", ex.Message); + Assert.Equal(0, acl.ApproveCallCount); + } + + [Fact] + public async Task Goedkeuren_before_the_zaak_is_opened_throws_without_calling_the_acl() + { + var store = new FakeRegistrationStore(); + var acl = new FakeAclClient(); + var registration = Registration.Submit("123456782"); // no zaak yet + store.Seed(registration); + var handler = new BeoordeelRegistratie(store, acl); + + var ex = await Assert.ThrowsAsync(() => + handler.HandleAsync(new BeoordeelRegistratieCommand(registration.Id, BeoordelingsBesluit.Goedkeuren))); + Assert.Contains("no zaak", ex.Message); + Assert.Equal(0, acl.ApproveCallCount); + } + + [Fact] + public async Task Re_goedkeuren_an_already_ingeschreven_registration_is_idempotent() + { + var store = new FakeRegistrationStore(); + var acl = new FakeAclClient(); + var registration = WithZaak(); + store.Seed(registration); + var handler = new BeoordeelRegistratie(store, acl); + + await handler.HandleAsync(new BeoordeelRegistratieCommand(registration.Id, BeoordelingsBesluit.Goedkeuren)); + await handler.HandleAsync(new BeoordeelRegistratieCommand(registration.Id, BeoordelingsBesluit.Goedkeuren)); + + Assert.Equal(1, acl.ApproveCallCount); + Assert.Equal(RegistrationStatus.Ingeschreven, (await store.GetAsync(registration.Id))!.Status); + } + + [Fact] + public async Task Re_afwijzen_an_already_afgewezen_registration_is_idempotent() + { + var store = new FakeRegistrationStore(); + var acl = new FakeAclClient(); + var registration = WithZaak(); + store.Seed(registration); + var handler = new BeoordeelRegistratie(store, acl); + + await handler.HandleAsync(new BeoordeelRegistratieCommand(registration.Id, BeoordelingsBesluit.Afwijzen)); + await handler.HandleAsync(new BeoordeelRegistratieCommand(registration.Id, BeoordelingsBesluit.Afwijzen)); + + Assert.Equal(1, store.SaveCount); + Assert.Equal(RegistrationStatus.Afgewezen, (await store.GetAsync(registration.Id))!.Status); + } +} -- 2.49.1 From d700659957644ad597d49104165d3c6c6ecda534 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 14 Jul 2026 17:20:51 +0200 Subject: [PATCH 5/7] feat(domain): implement the beoordeling use-case (goedkeuren/afwijzen) (refs #13) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Big.Application/BeoordeelRegistratie.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 services/domain/Big.Application/BeoordeelRegistratie.cs diff --git a/services/domain/Big.Application/BeoordeelRegistratie.cs b/services/domain/Big.Application/BeoordeelRegistratie.cs new file mode 100644 index 0000000..f0a384f --- /dev/null +++ b/services/domain/Big.Application/BeoordeelRegistratie.cs @@ -0,0 +1,57 @@ +using Big.Domain; + +namespace Big.Application; + +/// A behandelaar's beoordeling outcome, in domain language. +public enum BeoordelingsBesluit +{ + /// Approve — enter the registration in the register. + Goedkeuren, + + /// Reject — turn the registration down. + Afwijzen, +} + +/// A behandelaar's decision on a registration. +public sealed record BeoordeelRegistratieCommand(RegistrationId RegistrationId, BeoordelingsBesluit Besluit); + +/// +/// The beoordeling use case (S-12): apply a behandelaar's decision to a registration. +/// sets the zaak's final status via the ACL (§8.1) and +/// advances the aggregate to INGESCHREVEN; advances it to +/// AFGEWEZEN in the domain (propagating a rejection to the zaak, so the openbaar projection reflects +/// it, is a later sub-slice of S-12). Both decisions are idempotent — a repeated or redelivered +/// decision that matches the current terminal state is a no-op, so the ACL is not called twice. +/// +public sealed class BeoordeelRegistratie(IRegistrationStore store, IAclClient acl) +{ + public async Task HandleAsync(BeoordeelRegistratieCommand command, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(command); + + var registration = await store.GetAsync(command.RegistrationId, ct) + ?? throw new InvalidOperationException($"No registration {command.RegistrationId} to decide."); + + switch (command.Besluit) + { + case BeoordelingsBesluit.Goedkeuren: + // A repeated approval is a no-op: don't set the zaak status a second time. + if (registration.Status == RegistrationStatus.Ingeschreven) + return; + if (registration.ZaakUrl is null) + throw new InvalidOperationException( + $"Registration {command.RegistrationId} has no zaak yet; it cannot be approved."); + await acl.ApproveZaakAsync(registration.ZaakUrl, ct); + registration.Approve(); + break; + + case BeoordelingsBesluit.Afwijzen: + if (registration.Status == RegistrationStatus.Afgewezen) + return; + registration.Reject(); + break; + } + + await store.SaveAsync(registration, ct); + } +} -- 2.49.1 From ddebfd371a83a897a30bbd7e6e370023bafce961 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 14 Jul 2026 17:24:46 +0200 Subject: [PATCH 6/7] =?UTF-8?q?test(acceptance):=20beoordeling=20scenario?= =?UTF-8?q?=20=E2=80=94=20goedkeuren=20via=20ACL,=20afwijzen=20(refs=20#13?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BDD scenarios (Gherkin/Reqnroll) driving S-12's decide flow: a behandelaar takes an INGEDIEND registration into behandeling and decides it goedkeuren (→ INGESCHREVEN, zaak status via the ACL) or afwijzen (→ AFGEWEZEN, no ACL). Feature-scoped bindings avoid clashing with the identically-phrased status step in RegistratieIndienen. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Features/EenRegistratieBeoordelen.feature | 24 ++++++++ .../Steps/EenRegistratieBeoordelenSteps.cs | 58 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 tests/acceptance/Features/EenRegistratieBeoordelen.feature create mode 100644 tests/acceptance/Steps/EenRegistratieBeoordelenSteps.cs diff --git a/tests/acceptance/Features/EenRegistratieBeoordelen.feature b/tests/acceptance/Features/EenRegistratieBeoordelen.feature new file mode 100644 index 0000000..7c94fa6 --- /dev/null +++ b/tests/acceptance/Features/EenRegistratieBeoordelen.feature @@ -0,0 +1,24 @@ +# language: en +# Drives S-12 (#13). A behandelaar picks up a submitted registration for beoordeling and decides it: +# goedkeuren enters it in the register (INGESCHREVEN, the zaak's final status set via the ACL, §8.1), +# afwijzen turns it down (AFGEWEZEN). This scenario exercises the use cases against in-memory +# stand-ins for the ACL and the store; real Flowable user-task claim/complete arrives in a later +# sub-slice and is verified live. +Feature: Een registratie beoordelen + Als behandelaar wil ik een ingediende registratie beoordelen + zodat deze wordt ingeschreven of afgewezen. + + Scenario: Goedkeuren schrijft de registratie in via de ACL + Given a submitted registration with an opened zaak + When the behandelaar takes it into behandeling + Then the registration has status "INBEHANDELING" + When the behandelaar decides "goedkeuren" + Then the registration has status "INGESCHREVEN" + And the zaak's final status is set via the ACL + + Scenario: Afwijzen wijst de registratie af zonder de ACL + Given a submitted registration with an opened zaak + When the behandelaar takes it into behandeling + And the behandelaar decides "afwijzen" + Then the registration has status "AFGEWEZEN" + And the ACL is not asked to set the zaak status diff --git a/tests/acceptance/Steps/EenRegistratieBeoordelenSteps.cs b/tests/acceptance/Steps/EenRegistratieBeoordelenSteps.cs new file mode 100644 index 0000000..181951d --- /dev/null +++ b/tests/acceptance/Steps/EenRegistratieBeoordelenSteps.cs @@ -0,0 +1,58 @@ +using Acceptance.Support; +using Big.Application; +using Big.Domain; +using Reqnroll; +using Xunit; + +namespace Acceptance.Steps; + +/// Bindings for EenRegistratieBeoordelen.feature (S-12). Drives the TakeIntoBehandeling +/// transition and the BeoordeelRegistratie use case against in-memory ports; one instance per scenario. +/// Scoped to this feature so its "the registration has status" step does not clash with the identically +/// phrased (but differently-asserting) step in . +[Binding] +[Scope(Feature = "Een registratie beoordelen")] +public sealed class EenRegistratieBeoordelenSteps +{ + private readonly InMemoryAclClient _acl = new(); + private readonly InMemoryRegistrationStore _store = new(); + private RegistrationId _id; + + [Given("a submitted registration with an opened zaak")] + public async Task GivenASubmittedRegistrationWithAnOpenedZaak() + { + var registration = Registration.Submit("123456782"); + registration.AttachZaak(InMemoryAclClient.OpenedZaakUrl); + await _store.SaveAsync(registration); + _id = registration.Id; + } + + [When("the behandelaar takes it into behandeling")] + public async Task WhenTheBehandelaarTakesItIntoBehandeling() + { + var registration = await _store.GetAsync(_id); + registration!.TakeIntoBehandeling(); + await _store.SaveAsync(registration); + } + + [When("the behandelaar decides \"(.*)\"")] + public async Task WhenTheBehandelaarDecides(string besluit) + => await new BeoordeelRegistratie(_store, _acl).HandleAsync( + new BeoordeelRegistratieCommand(_id, Enum.Parse(besluit, ignoreCase: true))); + + [Then("the registration has status \"(.*)\"")] + public async Task ThenTheRegistrationHasStatus(string expected) + { + var registration = await _store.GetAsync(_id); + Assert.NotNull(registration); + Assert.Equal(expected, registration.Status.ToString().ToUpperInvariant()); + } + + [Then("the zaak's final status is set via the ACL")] + public void ThenTheZaakFinalStatusIsSetViaTheAcl() + => Assert.Equal(InMemoryAclClient.OpenedZaakUrl, _acl.ApprovedZaakUrl); + + [Then("the ACL is not asked to set the zaak status")] + public void ThenTheAclIsNotAskedToSetTheZaakStatus() + => Assert.Null(_acl.ApprovedZaakUrl); +} -- 2.49.1 From 7693a4a85a508f6228081ee8acc6a19624e42ac8 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 14 Jul 2026 17:24:46 +0200 Subject: [PATCH 7/7] feat(domain): expose POST /registrations/{id}/decide for the beoordeling (refs #13) The behandel-portal's decision reaches the domain here (via the BFF, later slices): goedkeuren/afwijzen, idempotent, superseding the temporary /approve endpoint. Co-Authored-By: Claude Opus 4.8 (1M context) --- services/domain/Big.Api/Program.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/services/domain/Big.Api/Program.cs b/services/domain/Big.Api/Program.cs index c180da8..eb26a17 100644 --- a/services/domain/Big.Api/Program.cs +++ b/services/domain/Big.Api/Program.cs @@ -24,6 +24,7 @@ builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -55,6 +56,22 @@ app.MapPost("/registrations/{id}/approve", async (string id, ApproveRegistration return Results.NoContent(); }); +// The behandelaar's beoordeling (S-12): decide a registration goedkeuren (→ INGESCHREVEN, sets the +// zaak's final status via the ACL) or afwijzen (→ AFGEWEZEN). Idempotent. This is the domain contract +// the behandel-portal's decision reaches through the BFF; it supersedes the temporary /approve above, +// which is retired once the portal lands. +app.MapPost("/registrations/{id}/decide", async (string id, DecideRequest body, BeoordeelRegistratie beoordeel, CancellationToken ct) => +{ + if (!Guid.TryParse(id, out var guid)) + return Results.NotFound(); + + if (!Enum.TryParse(body.Besluit, ignoreCase: true, out var besluit)) + return Results.BadRequest(new { error = $"Unknown besluit '{body.Besluit}'. Expected 'goedkeuren' or 'afwijzen'." }); + + await beoordeel.HandleAsync(new BeoordeelRegistratieCommand(new RegistrationId(guid), besluit), ct); + return Results.NoContent(); +}); + // Read a registration. Its zaak URL appears once the worker has opened the zaak (eventually). app.MapGet("/registrations/{id}", async (string id, IRegistrationStore store, CancellationToken ct) => { @@ -72,6 +89,8 @@ await app.RunAsync(); public sealed record SubmitRegistrationRequest(string Bsn); +public sealed record DecideRequest(string Besluit); + public sealed record RegistrationResponse(string RegistrationId, string Status, string? ZaakUrl); public partial class Program; -- 2.49.1