All checks were successful
## What & why S-09b (#75, split from #10) — the **approval flow** that completes the walking skeleton. A behandelaar can now approve a submitted registration; the entry flips from `INGEDIEND` to `INGESCHREVEN` in the public register. Flow: `POST /registrations/{id}/approve` (domain) → ACL sets the zaak eindstatus (ZGW `/statussen`) → OpenZaak → NRC → event-subscriber → projection → openbaar. ## Changes (bottom-up, each red→green TDD) - **Domain** — `RegistrationStatus.Ingeschreven` + `Registration.Approve()` (guards: opened zaak, only from INGEDIEND); `ApproveRegistration` use case (idempotent) + temp `POST /registrations/{id}/approve` endpoint; `IAclClient.ApproveZaakAsync`. - **ACL** — resolves the zaaktype's **eindstatus** from the catalogus (`isEindstatus` / highest volgnummer) and POSTs a ZGW status; exposed as `POST /statussen`. Unit + real-OpenZaak integration test. - **Event-subscriber** — binds NRC `hoofdObject`, projects a `status`/`create` as `INGESCHREVEN` keyed on the zaak (updates the existing row), **without reading OpenZaak** (§8.1). Retains the ZGW `resource` in the log (new column + EF migration) so a rebuild reproduces the status. - **e2e** — extended: submit → public INGEDIEND → approve → public INGESCHREVEN. - **Docs** — ADR-0011 (the two non-obvious decisions + the walking-skeleton assumption) + demo note. ## Key decisions (see ADR-0011) - **ACL discovers the eindstatus** (chosen over injecting a statustype URL): no new config/seed plumbing, domain stays ZGW-ignorant. - **Any post-creation status-set ⇒ INGESCHREVEN**: in the walking skeleton the only status ever set after creation is the approval, and the subscriber may not read ZGW — documented to tighten when more transitions arrive (S-12+). ## Verification - All .NET unit suites green locally (domain 47, acl 11, event-subscriber 14, bff 16, acceptance 7); Release build + `dotnet format` clean. - No new compose config (the eindstatus-discovery approach avoided it). - The real-OpenZaak integration test (ACL status-set) and the full submit→approve→visible e2e run in CI `verify-stack` (live NRC→projection + selectielijst egress, not reproducible locally). closes #75 Reviewed-on: #77
60 lines
3.0 KiB
TypeScript
60 lines
3.0 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
|
|
// Walking-skeleton happy path (S-08d + S-09 + S-09b): a zorgprofessional logs in via mock DigiD and
|
|
// submits through the self-service portal → BFF → domain; the entry appears in the openbaar register
|
|
// as INGEDIEND; a behandelaar approves it via the temporary admin endpoint; the approval flows via the
|
|
// ACL → NRC → event-subscriber → projection, and the openbaar register then shows it as INGESCHREVEN.
|
|
test('DigiD login → submit → public INGEDIEND → approve → public INGESCHREVEN', async ({ page, request }) => {
|
|
// Visiting the guarded page redirects to the Keycloak (mock DigiD) login.
|
|
await page.goto('/');
|
|
|
|
// Keycloak's default login form (stable ids across themes).
|
|
await page.locator('#username').fill('jan-burger');
|
|
await page.locator('#password').fill('test123');
|
|
await page.locator('#kc-login').click();
|
|
|
|
// Back on the portal, authenticated.
|
|
await expect(page.getByRole('heading', { name: /Zelfservice/i })).toBeVisible();
|
|
|
|
await page.getByRole('button', { name: /indienen/i }).click();
|
|
|
|
// The BFF accepted it and the page shows the confirmation with the reference.
|
|
const confirmation = page.getByText(/ontvangen/i);
|
|
await expect(confirmation).toBeVisible();
|
|
const reference = (await confirmation.textContent())?.match(/Referentie:\s*([0-9a-fA-F-]+)/)?.[1];
|
|
expect(reference, 'the confirmation shows a registration reference').toBeTruthy();
|
|
|
|
// The openbaar register (anonymous, its own origin) shows the submitted entry once the projection
|
|
// catches up. The projection updates asynchronously (NRC → event-subscriber), and the register loads
|
|
// on open, so reload until a submitted (INGEDIEND) row appears.
|
|
await page.goto('http://openbaar/');
|
|
await expect(page.getByRole('heading', { name: /Openbaar BIG-register/i })).toBeVisible();
|
|
await expect
|
|
.poll(async () => {
|
|
await page.reload();
|
|
return page.getByRole('cell', { name: 'INGEDIEND' }).count();
|
|
}, { timeout: 30_000, intervals: [1_000, 2_000, 3_000, 5_000] })
|
|
.toBeGreaterThan(0);
|
|
|
|
// Approve via the temporary admin endpoint (reached directly on the compose network, as a
|
|
// behandelaar would until the behandel-portal exists — S-12). The zaak is opened off the request
|
|
// path by the worker, so wait for it before approving.
|
|
await expect
|
|
.poll(async () => {
|
|
const res = await request.get(`http://domain:8080/registrations/${reference}`);
|
|
return res.ok() ? (await res.json()).zaakUrl : null;
|
|
}, { timeout: 30_000, intervals: [1_000, 2_000, 3_000, 5_000] })
|
|
.toBeTruthy();
|
|
|
|
const approve = await request.post(`http://domain:8080/registrations/${reference}/approve`);
|
|
expect(approve.status()).toBe(204);
|
|
|
|
// The approval flows back to the projection; the openbaar register now shows it as INGESCHREVEN.
|
|
await expect
|
|
.poll(async () => {
|
|
await page.reload();
|
|
return page.getByRole('cell', { name: 'INGESCHREVEN' }).count();
|
|
}, { timeout: 30_000, intervals: [1_000, 2_000, 3_000, 5_000] })
|
|
.toBeGreaterThan(0);
|
|
});
|