POST /beoordeling/{id}/besluit always 404'd against a real OpenZaak: {id} is the
FE-facing case id from IZaakSource.ListCases, which under OpenZaakZaakSource is the
ZGW zaak's own uuid, not ApplicationStore's primary key. Resolve the case through
ListCases first (same seam the GET sibling already uses), then to the local Aanvraag
via its Referentie — the one identifier stable across both sources.
Adds ApplicationStore.GetByReferentie and a regression test that reproduces the
divergence with a decorating IZaakSource test double instead of a live OpenZaak.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extends IZaakSource with RecordBesluit, mirroring WP-50's CreateZaak write
pattern: OpenZaakZaakSource POSTs a new Statussen entry (highest-volgnummer
statustype, since the harness catalogus has no per-outcome besluittype),
carrying the besluit + toelichting in statustoelichting; LocalZaakSource
no-ops. The beoordeling endpoint calls it after the local decision commits,
flagging a failure via RecordZgwDivergence the same way submit's
create-zaak/document writes do — closing WP-60's "second write pair" gap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New GET /beoordeling/{id} shows one aanvraag's status, linked documents, and a
canBesluiten decision flag, gated by the same CanBeoordelen capability as the
werkvoorraad list. Reads through IZaakSource.ListCases rather than a new seam
method (WP-66 needs one anyway for the real write); owner BSN is masked.
Fixes a real gap found while wiring this up: the behandelportal's login was still
WP-61's copied citizen/BSN DigiD flow, so nothing ever sent X-Medewerker and the
werkvoorraad screen (WP-64) always denied in a real browser. A dev-only
medewerkerInterceptor (mirrors the existing ?role= stand-in as ?rollen=) fixes that.
WP-65's own Risks note authorized splitting read from write across sessions given
its size; this is the read half. The decision-recording mutation is next (65b).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.
- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
environments/, the Storybook docs/*.mdx, and styles.scss into
libs/shared + libs/beheer (all confirmed identical between the two
repos before merging). auth stays deliberately duplicated per
ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
and Storybook into .storybook-ssp/.storybook-behandelportal - both
forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
WP-67 backlog entry documents the full decision trail.
npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.
The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Splits backend CallerIdentity into the two ADR-0002 §3 actor kinds
(ZorgverlenerCaller/MedewerkerCaller), a stub X-Medewerker/X-Rollen header
path mirroring WP-53's citizen stub, and Authz.CanBeoordelen as the first
medewerker capability — backend-only, no consumer until WP-64. Also fixes
the backlog README's stale WP-61 status (done, but table said todo).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Renamed scripts/create-ssp.mjs -> create-frontend.mjs (+ its WP-45 doc, npm
script, and every prose/command reference) since "ssp" reads as an acronym
where "create-frontend" says what it does.
Also fixes two real bugs found while running it for real during WP-61:
scripts/ci-local.sh was missing from RENAME_CONTENT_FILES (any --name'd
clone that keeps a backend would break `npm run ci`, still hardcoding
BigRegister.slnx), and plopfile.mjs's `gen:context` insertion into
.dependency-cruiser.js anchored on the `showcase: null,` line, which
create-ssp/create-frontend has already stripped by the time gen:context
runs in the same invocation — silently leaving a freshly scaffolded
context with no CONTEXT_ALLOWED fence entry at all. Re-anchored on the
`const CONTEXT_ALLOWED = {` line instead, which never moves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Local aanvraag/document writes and their paired ZGW writes aren't
transactional; a ZGW failure after the local write succeeds used to
diverge silently. ZgwHttpClient now retries transport-shaped failures
(not 500, which can follow a partial commit on the non-idempotent
statussen/rollen POSTs), and a ZGW failure that survives retry sets
Aanvraag.ZgwError plus a zgw:divergence audit row instead of failing
or diverging quietly. No outbox/reconcile job: three request-triggered
write paths don't justify a persisted queue that would also need to
carry citizen PII for the JWT audit claims.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OpenZaak doesn't serve the Notificaties API itself (it's a separate app,
open-notificaties) — standing one up for a real abonnement would triple
this harness for a benefit it doesn't need (exactly one subscriber, this
repo's own BFF). Instead, an opt-in compose overlay adds a celery worker
and points OpenZaak's NotificationsConfig straight at the BFF's webhook
via a zgw_consumers Service; bootstrap-notificaties.sh configures it
idempotently and verify-notificatie.sh proves a real write delivers to
the BFF's audit trail end-to-end.
Verified live: preflight proves the webhook's shared-secret gate both
ways (204/401), a zaak PATCH triggers real celery delivery, and rerunning
both scripts against an already-configured harness stays idempotent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Opt-in docker-compose (postgres+redis+OpenZaak, no celery/nginx) +
bootstrap-catalogus.sh seed a real OpenZaak instance; OpenZaakIntegrationTests
(Category=Integration, excluded from default dotnet test/CI) proves the ZGW
seam against it for the first time. That live run caught a real bug:
ZgwHttpClient never sent Content-Crs/Accept-Crs headers, so every write would
412 against a spec-compliant OpenZaak — fixed alongside the harness.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the hardcoded DocumentStore.DemoOwner and the static ZgwOptions
UserId/UserRepresentation with one per-request CallerIdentity, resolved by a
pluggable IIdentityProvider (StubIdentityProvider reads X-Role/X-Subject
today; a real OIDC/DigiD provider swaps in without touching any consumer).
- Domain/Authorization/{CallerIdentity,IIdentityProvider,StubIdentityProvider}.cs
+ a resolution middleware in Program.cs, right after correlation-id.
- Authz.ResolvePrincipal(ctx) keeps its signature (now reads ctx.Caller().Role),
so its ~15 call sites needed no changes.
- Every endpoint that passed DocumentStore.DemoOwner to a store now passes
ctx.Caller().Bsn.
- ZgwTokenProvider gains Mint(CallerIdentity) alongside the original Mint()
(kept for calls not tied to one citizen); ZgwHttpClient threads an optional
caller through to pick the right overload.
- IZaakSource gains ListMyCases(caller, now) — the citizen-scoped read
OpenZaakZaakSource backs with ZGW's rol__...__inpBsn filter. GET /applications
now routes through it instead of ApplicationStore directly, closing the last
"reads a static store" gap for a citizen-facing endpoint.
Backend 159/159 tests (+8, incl. an HTTP-level two-identity scoping proof),
npm run ci green, no api-client drift.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Endpoint/DTO/options landed already in c4dd846; this closes the loop with
NotificatieTests.cs (accept/reject/missing-header, asserting the AuthzAuditStore
row), missing appsettings.json keys (also backfills DrcBaseUrl/
InformatieobjecttypeUrls, stale since WP-51), and the webhook + abonnement
provisioning docs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the OpenZaak seam with IDocumentSource, sibling of IZaakSource
(WP-49/50): an upload always lands locally first (DocumentStore stays
the record of truth for preview/download/audit) and, when
Zgw:Enabled=true, is also registered as a DRC enkelvoudiginformatie-
object; once a zaak exists (IZaakSource.CreateZaak now also returns
its ZaakUrl), submit links each document to it via zaakinformatie-
object. FE upload/list DTOs are unchanged.
- ZgwOptions gains DrcBaseUrl + a category->informatieobjecttype URL
map (the document analogue of ZaaktypeUrls).
- LocalDocumentSource is the same DocumentStore.Add/Link calls the
endpoints used to make inline — zero behaviour change offline.
- OpenZaakDocumentSource POSTs the eio then the zaak link, persisting
the DRC url (DocumentStore.SetDrcUrl) so linking doesn't re-upload.
- Factored the GET/POST-with-bearer-JWT plumbing shared with
OpenZaakZaakSource into ZgwHttpClient; shared the stub handler
between the two source test classes as ZgwStubHandler.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the IZaakSource seam (WP-49, read-only) with CreateZaak: submitting
an aanvraag now also registers a Zaak + Status + Rol in OpenZaak when
Zgw:Enabled=true, routed through the existing /applications/{id}/submit
endpoint with the FE response DTO unchanged (ADR-0001/ADR-0005 — the
endpoint never branches on the config flag itself, DI already picked the
implementation).
- ZgwOptions gains a Type→zaaktype-URL map + the two RSINs a Zaak needs.
- LocalZaakSource.CreateZaak is a pure passthrough of what the endpoint
already computes locally (zero behaviour change for the offline default).
- OpenZaakZaakSource.CreateZaak POSTs the zaak (identificatie = the same
local reference, so both stay in sync), resolves + POSTs the initial
status and the initiator rol (BSN) via Catalogi lookups, and maps the
result back into the submit response.
- Marked ponytail shortcuts: first-statustype/roltype-Catalogi-returns
(no per-type config) and no compensating transaction on partial failure
— both fine for a first slice against a demo backend.
Verified: full `npm run ci` green, zero api-client drift, 144/144 backend
tests (142 existing + 2 new stub-handler tests asserting the POST bodies
+ type→zaaktype mapping per the acceptance criteria).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two highest-value OpenZaak roadmap gaps, each written self-contained (a "current
state" handoff section) so a fresh session can execute from the file + repo alone:
- WP-53: replace the stubbed owner/BSN with a real per-request CallerIdentity
(pluggable stub, not DigiD), threading it into Authz, the ZGW JWT user claims, and
a citizen-scoped read (rol__…__inpBsn). Production-blocking for a real deployment.
- WP-54: a separate docker-compose OpenZaak + scripted bootstrap + opt-in
Category=Integration test — makes 50/51/52 developable against a live instance
instead of only fixtures; kept out of the default gate.
Indexed in the backlog README (rows + phase-9 ordering note) and cited from
openzaak-integration.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- test-storybook:ci gets --maxWorkers=2 so the Jest runner stops spawning one
headless Chromium per core and OOM-ing the Gitea runner host (the root cause).
- storybook-a11y job gains a container resource ceiling (--cpus=2 --memory=4g) as
a belt-and-suspenders guardrail; noted it needs a docker-mode act_runner.
- openzaak-integration.md: add "Anti-corruption layer — two nested boundaries"
teaching section (BFF ACL vs upstreams + FE ACL vs BFF, and the principles).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stamdata: add beroepen, opleidingen (temporal), and specialismen tables to the
schema-driven catalog (zero UI code). opleidingen.beroep and specialismen.beroep
both reference beroepen.code — the first stamdata->stamdata references, enforced by
two new StamdataRef entries in the CI gate.
OpenZaak/ZGW (WP-49, slice 1 — read-only zaken): introduce IZaakSource as the cases
read seam. Default LocalZaakSource reads the local SQLite store (offline); an
OpenZaakZaakSource (Zgw/ client: HS256 per-call JWT, ZGW->existing-DTO mapper,
paginating HTTP source) is selected behind Zgw:Enabled (default false). The FE never
changes — same ApplicationSummaryDto, no api-client drift. Unit-tested with fixtures
+ a stub HttpMessageHandler; no live OpenZaak needed.
Docs: ADR-0005, reference/openzaak-integration.md, WP-49..52 roadmap, stamdata.md
update, README index rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Index roles-and-access, dependencies, and dependency-graph (were on disk
but missing from the docs README index).
- New reference/scaffolding.md: plop generators, gen:api (NSwag), gen:snippets,
and the skill recipes — the one consolidated "how generation works" page.
- Fix stale backlog range (WP-01…WP-28 -> WP-48) and de-link the dead WP-32 row
(undo folded into WP-31, no separate file).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three how-it-works/how-to-extend reference docs (docs/reference/), each with a
coupling section, indexed in docs/README.md. New document-feature skill so docs
ship in the same diff as the code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix the hardcoded LOCALE_ID: 'nl' — provide it from $localize.locale (the build-time
locale, 'nl'/'en', undefined→'nl' in dev) and register both nl+en locale data, so the en
bundle formats dates/numbers correctly. Make `docker compose up` serve the LOCALIZED build:
the web service now runs `ng build --localize` then serve-i18n.mjs, which gained a PORT env
+ an /api reverse-proxy (API_PROXY_TARGET → the api container) so both /nl/ and /en/ are
served with the language switcher working end-to-end. Drop the now-unused proxy.conf.docker.json
(serve-i18n proxies /api itself); update ARCHITECTURE. `npm start` stays the nl-only HMR loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt dependency-cruiser as the single declarative source for bounded-context +
atomic-layer boundaries, replacing the per-context no-restricted-imports blocks that
had to be hand-copied (and had left herregistratie uncovered). `.dependency-cruiser.js`
encodes context direction (everyone→shared, herregistratie→registratie, showcase→*),
domain-purity, contracts-import-nothing, ui↛infrastructure, ApiClient confinement, and
no-circular. `npm run dep:check` enforces (wired into ci-local.sh + the frontend CI job);
`npm run dep:graph` emits a committed mermaid context×layer graph. ESLint slimmed to
no-explicit-any + template a11y. Docs + new-context skill updated to the single source.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add docs/reference/roles-and-access.md — a short, scannable reference for the
three dev roles (drafter/approver/admin), that login is faked and role is a
separate dev-only stand-in, how to switch (dev switcher / ?role=), a per-role
capability table, and the one ABAC principle (server emits + enforces; UI
renders). Points to PRD-0002 + the code. Adds a pointer from CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`npm run format:check` (a CI gate) had drifted red across 44 files — pre-existing
files plus recently-added ones committed without formatting. Ran `prettier --write .`;
no logic changes. Also regenerates documentation.json (compodoc reflects the reformatted
component sources).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite all five mermaid blocks to portable syntax so they render (verified via
a mermaid-native render): split two `classDef`/`class` statements that shared a
line (the "why not just signals" graph); drop the `reduce() — PURE` participant
alias and the `;` inside a Note (§2c sequence); remove `<br/>` and parenthetical
transition labels from the two stateDiagram-v2 blocks (RemoteData, intake),
moving the aside into a note; normalize `<br/>`→`<br>` and drop `<b>` in the
atomic-hierarchy flowchart.
Extend §2g to document the now-closed gap: a CanDeactivate guard flushes the
pending debounce before in-app navigation, and beforeunload warns + best-effort
flushes on hard close (noting the HttpClient/keepalive limitation).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Amend ADR-0004 to record what shipped in 0e77faf: a table may live as typed
C# or as a typed JSON data-file (the compile-time value check traded for
editor ergonomics, caught by StamdataValidationTests instead); valid-time and
the generic StamdataCatalog/StamdataTable model; the low-code PR-emitting
editor is no longer "future". Add WP-29 backlog file (done) + README row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the geldigheidsvragen wording out of DiplomaRules into
Stamdata.PolicyQuestions (business-editable text, config-as-code); DiplomaRules
keeps only the rule of which questions apply. Extend StamdataValidationTests
(no blank id/wording, distinct ids in the manual set) and update ADR-0004.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the config-as-code strategy for business-tunable reference data:
typed checked-in config validated at compile time (never a production DB),
where UI text (), reference tables (Stamdata/), and letter content
each live, and why org-templates are the deliberate runtime-editable exception.
Index it in docs/README.md and add a CLAUDE.md pointer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New §2g explains field persistence (keystroke → model → 600ms snapshot
debounce; blur only marks touched, never saves) and §6a refreshes the stale
backend section with the real request lifecycle (NSwag client, httpClientFetch
seam, read/write traces) — both with relative links to the source files.
Add discovery pointers from the learning path (lesson 2.2 and the capstone).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move working docs (backlog, prd, roadmap) under docs/project/ and durable
docs (architecture ADRs, guides, audits) under docs/reference/; add a
docs/README.md index. Update every path reference in code comments, CLAUDE.md,
READMEs, and the new-ssp skill. Expand the Storybook Foundations curriculum
(Overview, BDD, i18n; rename Layers→Domain-Driven Design) and reorder the sidebar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One behavior per test across FE machine/store specs and backend endpoint
tests, so a failure names exactly what broke.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>