All checks were successful
## What & why First sub-slice of **S-12 (#13)** — the **beoordeling decision model** in the Domain Service. Foundation for the behandel-portal: it gives the domain a proper decision lifecycle before any UI/Flowable/BFF work. - **Statuses:** add `InBehandeling` and `Afgewezen` to `RegistrationStatus`. - **Aggregate:** `TakeIntoBehandeling()` (`Ingediend → InBehandeling`, idempotent, guards terminal states); generalise the behandelaar decision — `Approve()` (requires a zaak) and new `Reject()` both act on an `Ingediend`/`InBehandeling` registration → `Ingeschreven`/`Afgewezen`. - **Use-case:** `BeoordeelRegistratie` (`goedkeuren` sets the zaak's final status via the ACL §8.1 → `Ingeschreven`; `afwijzen` → `Afgewezen`, domain-only for now). Idempotent. - **Endpoint:** `POST /registrations/{id}/decide` (`{ "besluit": "goedkeuren" | "afwijzen" }`), superseding the temporary `/approve` (retired when the portal lands, S-12d). - **BDD:** `EenRegistratieBeoordelen.feature` — goedkeuren + afwijzen scenarios (feature-scoped bindings). **Scoped out** to later S-12 sub-slices: Flowable user-task claim/complete + BPMN `userTask` (S-12b), BFF `/behandel/*` + medewerker authz (S-12c), the Angular behandel-portal + e2e (S-12d), and propagating a *rejection* to the zaak/projection via the ACL. ## Definition of Done - [x] Linked issue: #13 (umbrella; this PR `refs`, does not close) - [x] Tests first; red → green per behaviour - [x] Unit + acceptance green (`make unit`): domain 65, acceptance 9 - [x] Mutation ≥ break(90): domain 98.77%, no survivors in new code (the one unkilled mutant is the pre-existing `FlowableWorkflowClient` baseline) - [ ] CI green (pending) Part of #13. Reviewed-on: #82
143 lines
5.7 KiB
C#
143 lines
5.7 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 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.
|
|
/// </summary>
|
|
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<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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
}
|
|
}
|