All checks were successful
## What & why
Second half of **S-12c** (behandel-portal backend), completing the decision path per **ADR-0013**:
- **Domain:** `BeoordeelRegistratie` now, after applying the decision (aggregate + ACL for approval), **completes the open Flowable `Beoordelen` task** for that registration (found by registrationId) with the besluit, so the workflow advances. No open task → the decision still stands (completes nothing); idempotent.
- **BFF:** `POST /behandel/registrations/{id}/decide` behind the medewerker/`behandelaar` policy, forwarding `goedkeuren`/`afwijzen` to the domain. Validates the besluit vocabulary (400 on unknown) without troubling the domain.
Behavior: decide is **401** without a token, **403** without the role, **400** for an unknown besluit, **204** (forwarded) for a behandelaar.
This completes the behandel backend. **S-12d** (the Angular behandel-portal + Playwright e2e) closes umbrella #13 and retires the temporary `/approve`.
## Definition of Done
- [x] Linked issue: #13 (umbrella, `refs`)
- [x] Tests first; red → green per layer
- [x] Unit + acceptance green (`make unit`): domain 79, bff 27, acceptance 9 (acl/event-subscriber unaffected)
- [x] Beoordeling acceptance scenario asserts task completion (goedkeuren + afwijzen)
- [x] openapi.json + api-client regenerated (drift guard passes)
- [x] Mutation ≥ break(90): **domain 100%, bff 100%**
- [ ] CI green (pending)
Part of #13.
Reviewed-on: #86
169 lines
7.2 KiB
C#
169 lines
7.2 KiB
C#
using Big.Application;
|
|
using Big.Domain;
|
|
|
|
namespace Big.Tests;
|
|
|
|
/// <summary>
|
|
/// 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. Either way the decision also completes the Flowable Beoordelen task (found by
|
|
/// registrationId) so the process advances. All idempotent — a repeated decision is a no-op.
|
|
/// </summary>
|
|
public class BeoordeelRegistratieTests
|
|
{
|
|
private static Registration WithZaak(string bsn = "123456782")
|
|
{
|
|
var registration = Registration.Submit(bsn);
|
|
registration.AttachZaak(FakeAclClient.DefaultZaakUrl);
|
|
return registration;
|
|
}
|
|
|
|
private static FakeUserTaskClient TaskFor(Registration registration) =>
|
|
new([new BeoordelingTask("task-1", registration.Id)]);
|
|
|
|
[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 tasks = TaskFor(registration);
|
|
var handler = new BeoordeelRegistratie(store, acl, tasks);
|
|
|
|
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);
|
|
// The behandelaar's decision advances the workflow: the Beoordelen task is completed.
|
|
Assert.Equal(("task-1", BeoordelingsBesluit.Goedkeuren), tasks.Completed);
|
|
}
|
|
|
|
[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 tasks = TaskFor(registration);
|
|
var handler = new BeoordeelRegistratie(store, acl, tasks);
|
|
|
|
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);
|
|
Assert.Equal(("task-1", BeoordelingsBesluit.Afwijzen), tasks.Completed);
|
|
}
|
|
|
|
[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, TaskFor(registration));
|
|
|
|
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, new FakeUserTaskClient([]));
|
|
|
|
await Assert.ThrowsAsync<ArgumentNullException>(() => 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, new FakeUserTaskClient([]));
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
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, TaskFor(registration));
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
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, TaskFor(registration));
|
|
|
|
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, TaskFor(registration));
|
|
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Deciding_completes_no_task_when_none_is_open_for_the_registration()
|
|
{
|
|
// The task may already be gone (redelivery / manual completion). The decision still applies
|
|
// and simply completes nothing rather than failing.
|
|
var store = new FakeRegistrationStore();
|
|
var acl = new FakeAclClient();
|
|
var registration = WithZaak();
|
|
store.Seed(registration);
|
|
var tasks = new FakeUserTaskClient([]); // no open task for this registration
|
|
var handler = new BeoordeelRegistratie(store, acl, tasks);
|
|
|
|
await handler.HandleAsync(new BeoordeelRegistratieCommand(registration.Id, BeoordelingsBesluit.Goedkeuren));
|
|
|
|
Assert.Equal(RegistrationStatus.Ingeschreven, (await store.GetAsync(registration.Id))!.Status);
|
|
Assert.Null(tasks.Completed);
|
|
}
|
|
}
|