From d74270b0bd6d29dcb5a180ad58cfb8905475dfb0 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 24 Jul 2026 15:45:18 +0200 Subject: [PATCH] docs+test(e2e): ADR-0026, S-15b demo note, default-fill e2e (refs #131) --- .../adr-0026-mutable-default-fill-store.md | 61 +++++++++++++++++++ docs/demo-script.md | 22 +++++++ tests/e2e/default-fill.spec.ts | 26 ++++++++ 3 files changed, 109 insertions(+) create mode 100644 docs/architecture/adr-0026-mutable-default-fill-store.md create mode 100644 tests/e2e/default-fill.spec.ts diff --git a/docs/architecture/adr-0026-mutable-default-fill-store.md b/docs/architecture/adr-0026-mutable-default-fill-store.md new file mode 100644 index 0000000..f263298 --- /dev/null +++ b/docs/architecture/adr-0026-mutable-default-fill-store.md @@ -0,0 +1,61 @@ +# ADR-0026: Runtime-mutable ACL default-fill (in-memory store, seeded from config) + +- **Status:** Accepted +- **Date:** 2026-07-24 +- **Deciders:** Respellion engineering +- **Slice:** S-15b (#131), second of the S-15 (#16) split + +## Context + +ADR-0003 made the ACL *default-fill* the ZGW-mandatory fields it stamps on every +zaak, supplied as static configuration (`Acl:Defaults`, read once at startup as an +immutable singleton). S-15b lets a beheerder **edit** those values from the portal +and have the next zaak reflect them — so the defaults must become mutable at runtime. + +Two questions: **what** is editable, and **where** the mutable state lives. + +## Decision + +**Make the three ZGW default-fill fields a runtime-mutable, in-memory store +(`IDefaultFillStore`), seeded from `Acl:Defaults` at startup. The ACL reads it per +zaak; the beheer `PUT /default-fill` replaces it.** + +### Only the three ZGW fill fields are editable + +`Acl:Defaults` also carries the S-27 catalog-resolution keys (`ZaaktypeIdentificatie`, +`InformatieobjecttypeOmschrijving`). Those feed the resolved-URL cache +(`CachedZaaktypeCatalog`, ADR-0021); editing them at runtime would leave a stale cache +and is catalogus *wiring*, not "default fill". So they **stay static config** and are +out of scope for the CRUD. The editable set is exactly `Bronorganisatie`, +`VerantwoordelijkeOrganisatie`, `Vertrouwelijkheidaanduiding` (`DefaultFillSettings`). + +### In-memory, not persisted + +The store is a thread-safe in-memory singleton. **An edit is lost on restart**, when it +reverts to the configured env. That is acceptable for this reference app: the slice +demonstrates the *pattern* (beheer edits config that the ACL honours), not durable +config management. The ACL stays stateless — no DB, no EF, no migration, no extra +compose service. + +- ponytail ceiling: no persistence, no audit trail, no optimistic concurrency. +- Upgrade path: back `IDefaultFillStore` with a DB (or an Objecten record) if durable, + audited, multi-instance config is needed — the port stays the same. + +## Consequences + +**Positive** + +- Demoable end to end (edit in portal → next zaak reflects it) with minimal moving parts. +- The read path is per-zaak, so no restart and no cache concerns for the ZGW fields. + +**Negative / costs** + +- Edits don't survive a restart and aren't shared across replicas (single-instance + assumption). Documented ceiling above. +- Two sources of default config now (static keys on `AclDefaults`, mutable fields in the + store) — a deliberate split by editability. + +## Coupling rules touched (CLAUDE.md §8) + +None new. The BFF→ACL edge already exists (ADR-0025); this adds a read/write pair on it. +The ACL remains the owner of the ZGW-facing config. diff --git a/docs/demo-script.md b/docs/demo-script.md index 58b8770..2fb41c8 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -5,6 +5,28 @@ copy-pasteable walkthrough against a local `make up` stack. --- +## S-15b — Beheer-portal: default-fill configuration editor (#131, ADR-0026) + +**Outcome:** a beheerder edits the ACL's ZGW **default-fill** values (bronorganisatie, +verantwoordelijke organisatie, vertrouwelijkheidaanduiding) from the beheer portal, and the next zaak +is stamped with the new values — no restart. Path: portal → BFF `GET/PUT /beheer/default-fill` +(beheerder role) → ACL `GET/PUT /default-fill` → a runtime-mutable in-memory store the ACL reads per +zaak (ADR-0026). The S-27 catalog-resolution keys stay static config (editing them would desync the +zaaktype cache). Store is in-memory: an edit reverts to the configured env on restart. + +```bash +make up +# 1. Log in as bram-beheerder / test123 → "Default-fill" tab → change a value → Opslaan. +open http://localhost:8143/default-fill +# +# 2. Automated: the ACL uses the current default-fill per zaak (unit) and the endpoints are behind the +# beheerder role (BFF unit): +# Acl.Tests → AclServiceTests.Opening_a_zaak_reflects_a_default_fill_update +# Bff.Tests → BeheerDefaultFillEndpointTests +``` + +--- + ## S-15a — Beheer-portal: read-only catalogus viewer (#130, ADR-0025) **Outcome:** a new **beheer** portal (medewerker realm, like behandel) shows the ZTC catalogus — diff --git a/tests/e2e/default-fill.spec.ts b/tests/e2e/default-fill.spec.ts new file mode 100644 index 0000000..8ff8d03 --- /dev/null +++ b/tests/e2e/default-fill.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from '@playwright/test'; + +// S-15b: a beheerder edits the ACL default-fill in the beheer portal and gets a saved confirmation. +// Runs against the shared verify stack; it edits + saves (the ACL store is in-memory, ADR-0026) and +// asserts the confirmation, without depending on another test's state. +test('a beheerder edits and saves the default-fill', async ({ page }) => { + await page.goto('http://beheer/'); + + // Keycloak medewerker-realm login (same realm as behandel). + await page.locator('#username').fill('bram-beheerder'); + await page.locator('#password').fill('test123'); + await page.locator('#kc-login').click(); + + await expect(page.getByRole('heading', { name: /Catalogus/i })).toBeVisible(); + + // Navigate to the default-fill editor and change a value. + await page.getByRole('link', { name: /Default-fill/i }).click(); + await expect(page.getByRole('heading', { name: /Default-fill/i })).toBeVisible(); + + const bron = page.getByLabel('Bronorganisatie'); + await expect(bron).toBeVisible(); + await bron.fill('517439943'); + await page.getByRole('button', { name: /Opslaan/i }).click(); + + await expect(page.getByText(/standaardwaarden zijn opgeslagen/i)).toBeVisible(); +});