Commit Graph
145 Commits
Author SHA1 Message Date
not 43c6c8e0d2 docs: ADR-0023 + demo note + backlog sync for the observability backplane (refs #122)
CI / lint (pull_request) Successful in 1m23s
CI / build (pull_request) Successful in 1m9s
CI / unit (pull_request) Successful in 1m17s
CI / frontend (pull_request) Successful in 2m43s
CI / mutation (pull_request) Successful in 5m56s
CI / verify-stack (pull_request) Failing after 8m6s
ADR-0023 records the Grafana-native stack (Tempo, no collector, config baked into
images); demo-script and BACKLOG describe S-16a and the S-16 split.

refs #122
2026-07-23 12:48:02 +02:00
not f29a8598fd feat(infra): Tempo + Prometheus + Grafana observability backplane in compose (refs #122)
Three built images on the cg network with config baked in: Tempo (OTLP ingest,
ports 4317/4318), Prometheus, and Grafana with both datasources auto-provisioned.
Config lives in infra/observability/. The verify-observability step runs early in
CI verify-stack; the three containers are added to the failure log-dump list.
Tempo's Grafana plugin has no datasource /health method, so the check proves
Tempo reachability through Grafana's datasource proxy instead.

refs #122
2026-07-23 12:46:54 +02:00
not f12e9ce746 test(infra): verify-observability asserts Grafana + Tempo/Prometheus datasources are live (refs #122)
Polls Grafana's datasource-health endpoints so it proves Grafana can actually
reach Tempo and Prometheus over the cg network, not merely that the containers
started. Fails until the backplane is wired (next commit).

refs #122
2026-07-23 12:41:58 +02:00
not 4fe9915816 feat(domain): herregistratie reminder sweep on a Quartz cron (S-17, closes #18) (#121)
CI / verify-stack (push) Successful in 8m14s
CI / lint (push) Successful in 1m20s
CI / build (push) Successful in 59s
CI / unit (push) Successful in 1m16s
CI / frontend (push) Successful in 2m38s
CI / mutation (push) Successful in 5m53s
## What & why

S-17: a BIG inscription is valid for a fixed term; before it lapses the zorgprofessional must herregistreren. This adds a **daily herregistratie reminder sweep**.

- **Domain:** `Approve(ingeschrevenOp)` now stamps the inscription moment; `HerregistratieVoor` derives the deadline (inscription + 5-year validity); `HerregistratieReminderDue(asOf)` is the single rule (inside the 90-day window, inscribed, not yet reminded); `MarkHerregistratieReminderVerstuurd()` is idempotent.
- **Store:** `FindDueForHerregistratieReminderAsync(asOf)` — the sweep's candidate set, filtered on the aggregate's own rule (no duplicated policy).
- **Application:** `HerregistratieReminderSweep` — pure over the store + an injected `TimeProvider`; flags + persists each due inscription, returns the reminded ids.
- **Infra/API:** `HerregistratieReminderJob` (Quartz `IJob`) fires the sweep on a daily cron (03:00, overridable via `Quartz__Cron`) and logs the count. `GET /registrations/{id}` surfaces `herregistratieVoor` + `herregistratieReminderVerstuurd`.

**Decisions (both raised with you before coding):** use Quartz.NET as the PRD names it — a genuine cron concern, distinct from the queue-draining pumps, which stay as-is (**ADR-0022**, proposal #120); and the reminder's observable effect is a flag on the aggregate + a log line (no outbound notification infra in v1). No coupling rule (§8) is touched — Quartz is internal to the Domain Service.

Closes #18
Closes #120

## Definition of Done

- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation (red→green per layer: domain rule, store query, sweep).
- [x] Implementation makes the test pass; refactor commit for the 90-day knob.
- [x] Conventional Commits referencing the issue (`refs #18`).
- [ ] CI green — awaiting Gitea Actions.
- [ ] `docker compose up` reaches green health checks within 3 minutes — API boots locally with Quartz initialised; verified in CI compose smoke.
- [x] Docs updated — ADR-0022, demo-script, BACKLOG.
- [x] ADR added — `docs/architecture/adr-0022-quartz-scheduler.md`.
- [x] Demo note in `docs/demo-script.md`.

## Notes for reviewers

- **Ripple:** `Approve()` gained the inscription moment, so the two approving handlers (`ApproveRegistration`, `BeoordeelRegistratie`) now take an injected `TimeProvider`; existing tests pass a fixed clock. All three `IRegistrationStore` implementers (prod, unit fake, acceptance) got the new query.
- **Calibration knobs:** validity (5y) and reminder lead time (90d) are domain constants marked with `ponytail:` comments; promotion path to beheer config (S-15) noted in the ADR.
- **Mutation:** the Quartz job shell is excluded from Stryker, mirroring the pumps; all rule/sweep/query logic is covered.
- Local: 152 domain unit tests green; API boots with the Quartz scheduler and `/health` green.

Reviewed-on: #121
2026-07-23 10:31:56 +00:00
not 5f8ab4dbcd feat: self-service resume of an existing registration after refresh (S-26, closes #111) (#119)
CI / lint (push) Successful in 1m19s
CI / build (push) Successful in 1m7s
CI / unit (push) Successful in 1m16s
CI / frontend (push) Successful in 2m41s
CI / mutation (push) Successful in 5m57s
CI / verify-stack (push) Successful in 8m24s
## What & why

After submitting, the self-service portal held the registration only in in-memory signals, so a **page refresh stranded an in-flight registration** — the reference and its "Documenten aanleveren" / "Trek aanvraag in" actions were lost, with no way back (the reference wasn't in the URL and there was no read endpoint). This is the gap a citizen hit in testing.

Now the portal **resumes on load**:
- **Domain:** `IRegistrationStore.FindOpenByBsnAsync` (the citizen's non-terminal INGEDIEND/IN_BEHANDELING registration) + `GET /registrations/current?bsn=`.
- **BFF:** owner-scoped `GET /self-service/registrations` (bsn from the DigiD token) → the current registration, or **204** when none. Regenerated `services/bff/openapi.json`.
- **Frontend:** `registration-page` calls it on init and restores the submitted view (reference + actions); 204 shows the submit form as before. api-client regenerated (orval).

Closes #111

## Definition of Done

- [x] Linked issue (#111).
- [x] TDD — store `FindOpenByBsnAsync` tests, BFF endpoint tests, an Angular component test (resume-on-load), a Playwright e2e (submit → reload → restored).
- [x] Conventional Commits referencing #111.
- [ ] CI green — validated locally (below); runner CI running.
- [x] `docker compose up` reaches green health — fresh stack + full e2e (3 specs) green.
- [x] Docs — `docs/synthetic-data.md` (new e2e users).
- [ ] ADR — N/A (follows existing BFF/domain patterns; no boundary change).
- [ ] Demo note — the flow is unchanged for the demo; no new demo-script section (happy to add one if wanted).

## Verified locally

- Unit: Big 141 (+7 store tests), Bff 36 (+3 endpoint tests), all suites green.
- Frontend: 12 self-service component tests (incl. resume-on-load); lint + build green.
- **e2e (fresh CI stack): all 3 specs pass** — `registration`, `resume`, `withdrawal` (29.5s, single worker).
- Mutation: domain **91.04%**, bff **100%** (break 90%). `make lint` clean.

## Notes for reviewers

- **Shared-stack isolation:** resume-on-load restores any open registration for the logged-in bsn, so the self-service e2e specs can no longer share `jan-burger` (the verify-* API checks submit as `jan-burger`/`123456782` before the e2e). Each spec now has its own DigiD citizen (`emma`/`sanne`/`lars`-burger); `jan-burger` stays the documented citizen for the verify checks. This is the fix for the two intermittent e2e failures seen during development.
- **Scope:** resumes the current **in-flight** registration only (terminal ones aren't resumed), per the issue's out-of-scope note.

Reviewed-on: #119
2026-07-23 07:22:08 +00:00
not 5de8c1e292 feat(acl): resolve the zaaktype by identificatie, not a pinned URL (S-27, closes #113) (#118)
CI / lint (push) Successful in 1m21s
CI / build (push) Successful in 58s
CI / unit (push) Successful in 1m7s
CI / frontend (push) Successful in 2m36s
CI / mutation (push) Successful in 5m36s
CI / verify-stack (push) Successful in 8m4s
## What & why

The ACL was handed a **pinned zaaktype URL** (`Acl__Defaults__ZaaktypeUrl`) + informatieobjecttype URL. OpenZaak assigns those UUIDs at creation, so every stack had to seed the catalogus and then capture + inject the resulting URLs out of band (CI's `run-domain-check.sh`; the local `local-seed`→`acl.env` bootstrap from ADR-0020). Brittle, and a stale/placeholder URL failed opaquely (OpenZaak 400).

Now **the ACL resolves them itself** from OpenZaak's Catalogi API by stable business key:
- config `ZaaktypeIdentificatie` (`BIG-REGISTRATIE`) / `InformatieobjecttypeOmschrijving` (`Diploma`);
- a `CachedZaaktypeCatalog` resolves **lazily on first use** and caches (success only, so a pre-publish miss is retried — no startup ordering coupling);
- a clear "No published … found" error replaces the opaque placeholder 400.

Design in **ADR-0021** (proposed in #117).

Closes #113
Closes #117

## Consequences (the payoff)

No stack captures/injects a server-assigned URL any more — `docker-compose.yml`/`.local.yml`, `run-domain-check.sh` and `local-seed` all drop it; the local `acl.env` shrinks to a single line.

**One thing S-27 can't remove** (confirmed empirically during this work): OpenZaak validates the `zaaktype` field on zaak-create with Django's URLValidator and **rejects a single-label host** (`http://openzaak:8000/…` → `zaaktype: bad-url`). So the ACL's **base URL** must still point at a URL-valid host (a container IP); that base-URL injection from ADR-0020 stays (local `acl.env` now carries only it; CI keeps `ACL_OPENZAAK_BASEURL`). ADR-0021 records this.

## Definition of Done

- [x] Linked issues (#113 slice, #117 adr-proposal).
- [x] TDD — resolver + gateway-lookup unit tests, updated `AclService` tests (50 unit tests green).
- [x] Implementation makes them pass; refactor of both compose stacks + verify scripts follows.
- [x] Conventional Commits referencing #113.
- [ ] CI green — see below.
- [x] `docker compose up` reaches green health — verified: fresh `make local` + `make verify-local` green with **no zaaktype-URL injection**; `acl.env` is base-URL-only.
- [x] Docs — ADR-0021 + demo-script S-27 note.
- [x] ADR added (ADR-0021).
- [x] Demo note appended.

## Verification done locally

- **50 unit tests** pass (resolver resolve/cache/retry-on-failure; gateway match/miss/blank-key; all `AclService` paths).
- **6 ACL integration tests** pass against a live seeded OpenZaak — incl. resolving the zaaktype + Diploma iot by business key, and a clear error for an unknown identificatie.
- **Fresh `make local` + `make verify-local`**: full flow (submit → werkbak → openbaar) green; `acl.env` = `Acl__OpenZaak__BaseUrl` only.
- `make lint` clean; ACL mutation ratchet run locally (see checks).

## Notes for reviewers

- `IZaakGateway` gains two resolve methods; `AclService` depends on the new `IZaaktypeCatalog` (singleton, so the cache persists).
- Supersedes the pinned-URL mechanism; ADR-0021 documents that ADR-0020's `seed-env`/entrypoint shim are **simplified** (base-URL only), not deleted, because of the URLValidator constraint above.

Reviewed-on: #118
2026-07-22 14:49:25 +00:00
not 183d0bce31 fix(infra): docker-compose.local self-seeds zaaktype, DMN + NRC abonnement (closes #110) (#114)
CI / lint (push) Successful in 1m20s
CI / build (push) Successful in 59s
CI / unit (push) Successful in 1m12s
CI / frontend (push) Successful in 2m42s
CI / mutation (push) Successful in 5m42s
CI / verify-stack (push) Successful in 9m21s
## What & why

The host-browser stack (`make local`) had drifted behind three slices, so a fresh bring-up couldn't complete the flow: registrations stuck at `OpenZaakAanmaken`, the behandel werkbak stayed empty, and the openbaar register showed nothing. The `verify-*` scripts do this setup for CI at test time; `make local` had no equivalent.

This makes the local stack **self-seed at bring-up** so it just works in a browser:

- **DMN** — `flowable-init` now also deploys `diploma-eligibility.dmn` (was BPMN-only), so completing `WachtOpDocumenten` routes through the DMN to `Beoordelen` instead of 404ing.
- **Zaaktype + ACL** — a `local-seed` one-shot publishes the BIG zaaktype (whose UUID is server-assigned, hence not static in the compose file) and writes the real URLs to `seed-env:/acl.env`; the ACL sources it on startup via an entrypoint override.
- **NRC abonnement** — an `nrc-subscribe` one-shot registers the `zaken` subscription at the event-subscriber callback, so notifications reach the projection/openbaar register.

Both one-shots reach OpenZaak/NRC by **container IP** (a single-label host fails their Django URLValidator), mirroring the CI verify scripts. Design + trade-offs in **ADR-0020**.

Closes #110

## Definition of Done

- [x] Linked Gitea issue (#110).
- [x] Failing test committed before the implementation — `test(infra): …` adds `infra/run-local-flow-check.sh` / `make verify-local`; the three gaps' failures were observed live on a fresh `make local` (red), and the fix turns it green.
- [x] Implementation makes the test pass; docs commit follows.
- [x] Conventional Commits referencing the issue (`refs #110`).
- [ ] CI green — running on the restored runner. Infra-only change; the CI `verify-stack` job uses `docker-compose.yml` (untouched). Also validated locally: `make verify-local` passes against a fresh `make local` (see below).
- [x] `docker compose up` from a fresh clone reaches green health checks — verified: `make local` healthy in ~2m20s, then `make verify-local` green.
- [x] Docs updated — ADR-0020 + demo-script note.
- [x] ADR added in `docs/architecture/` — ADR-0020.
- [x] Demo note in `docs/demo-script.md`.

## Notes for reviewers

- **Infra-only** — no service code changes; the ACL image and the CI stack (`docker-compose.yml`) are untouched.
- **Verified end-to-end on a fresh stack** (`make local-down && make local && make verify-local`):
  ```
  >> 2. zaak opened            (zaaktype seeded + wired)
  >> 3. documents accepted 204 (DMN deployed)
  >> 4. in the werkbak         (DMN routing → Beoordelen)
  >> 5. visible in the openbaar register (NRC abonnement)
  OK — a fresh local stack completed the flow with no manual seeding
  ```
- **Follow-up:** the cleaner design — ACL resolving its zaaktype by `identificatie` instead of a pinned server-assigned URL — is split out as **S-27 (#113)**; landing it would remove the `acl.env` injection here. ADR-0020 records this.
- The `seed-env` volume carries the generated `acl.env` from `local-seed` to the ACL; a `down --volumes` (as `make local-down` does) resets it cleanly.

Reviewed-on: #114
2026-07-22 12:44:29 +00:00
not d5e5fa254c fix(e2e): run Playwright single-worker to stop OOM page-crash in verify-stack (closes #115) (#116)
CI / lint (push) Successful in 1m22s
CI / build (push) Successful in 1m1s
CI / unit (push) Successful in 1m14s
CI / frontend (push) Successful in 2m43s
CI / mutation (push) Successful in 5m53s
CI / verify-stack (push) Has been cancelled
## What & why

`verify-stack` was failing intermittently on the Playwright e2e with `Page crashed` mid-action (`locator.fill`) and 90s timeouts — the run logged **"2 workers"**, i.e. two full `channel: 'chromium'` browsers running alongside the entire compose stack on the 8 GB self-hosted runner. The renderer gets OOM-killed. Tests passed only when a retry happened to run alone.

Fix: pin `workers: 1` in `tests/e2e/playwright.config.ts` (there are only two long-running happy-path specs, so serial costs little) and add `--disable-dev-shm-usage`. This removes the memory contention at the source rather than leaning on `retries` (CLAUDE.md §15 — flaky tests are fixed, not retried).

Closes #115

## Definition of Done

- [x] Linked Gitea issue (#115).
- [ ] Failing test committed first — N/A: the "red" is the observed `verify-stack` e2e crash (`Page crashed`, 2 workers); this changes test-harness config to fix it. Verified green by re-running the e2e (see notes).
- [x] Conventional Commit referencing the issue (`refs #115`).
- [ ] CI green — the point of the change; `verify-stack` e2e should stop OOM-crashing.
- [x] Docs — none needed (test-config only; rationale in an inline comment).
- [ ] ADR — N/A.

## Notes for reviewers

- One-line-of-behaviour change: `workers: 1` + `--disable-dev-shm-usage`; no product or spec changes.
- `Page crashed` is a renderer OOM, not a product defect — the happy path passes when a browser runs alone (the flaky retries already showed this). Single-worker makes that the normal case.
- Independent of #110 (that PR fixes `docker-compose.local.yml`; this fixes the CI `verify-stack` e2e). Landing this first unblocks #110's `verify-stack`.

Reviewed-on: #116
2026-07-22 12:03:59 +00:00
not bf234e1322 docs(backlog): add S-26 self-service resume slice (refs #111) (#112)
CI / verify-stack (push) Successful in 11m16s
CI / lint (push) Successful in 1m22s
CI / build (push) Successful in 1m6s
CI / unit (push) Successful in 1m18s
CI / frontend (push) Successful in 3m7s
CI / mutation (push) Successful in 5m58s
## What & why

Mirror the new self-service **"resume after refresh"** slice into the Iteration 2 section of the curated backlog (`BACKLOG.md`), keeping it in sync with Gitea. Tracked as #111 (S-26).

Refs #111 — **does not close it**: the backlog is the curated mirror, the slice itself stays open for implementation.

## Definition of Done

- [x] Linked Gitea issue (#111).
- [ ] Failing test committed before the implementation — N/A (docs-only backlog mirror).
- [ ] Implementation makes the test pass; refactor commit if structure improved — N/A.
- [x] Conventional Commits referencing the issue (`refs #111`).
- [ ] CI green — no code paths touched; only `BACKLOG.md`.
- [ ] `docker compose up` reaches green health checks — N/A.
- [x] Docs updated (this IS the docs change).
- [ ] ADR added — N/A.
- [ ] Demo note in `docs/demo-script.md` — N/A (backlog entry, not a shipped user-visible change).

## Notes for reviewers

Single-file change: adds the `S-26` entry (Outcome + Acceptance) after S-14 in Iteration 2, matching the surrounding slice format. The `S-B04` local-stack bug (#110) is intentionally **not** added — the `S-B0N` bug-slices have never been mirrored in `BACKLOG.md` (they live only in Gitea).

Reviewed-on: #112
2026-07-22 09:12:19 +00:00
not c8fdfbb699 feat(acl,domain): cancel the ZGW zaak on document-timeout expiry (S-10c, closes #106) (#109)
CI / lint (push) Successful in 1m22s
CI / build (push) Successful in 1m1s
CI / frontend (push) Successful in 2m27s
CI / mutation (push) Successful in 5m34s
CI / verify-stack (push) Successful in 8m0s
CI / unit (push) Successful in 1m17s
## S-10c · Close the ZGW zaak on document-timeout expiry (closes #106)

Completes the S-10a/S-10b boundary flagged in ADR-0017: when a registration's 30-day document term lapses, the domain now cancels the **ZGW zaak** as well as marking the aggregate `Verlopen`, so OpenZaak and the register no longer diverge.

### What it does
On expiry the `ExpireRegistrationWorker` calls the ACL to set the zaak to a distinct, non-terminal **`Geannuleerd`** status with a **`Vervallen`** resultaat (vs the approval `Afgehandeld` + `Geregistreerd`), resolved **by omschrijving** in the ACL — the ACL-first ordering mirrors approval so a failed ZGW call leaves the job for redelivery rather than diverging the two.

**Path:** Flowable P30D timer → `RegistratieVerlopen` job → domain `ExpireRegistrationWorker` → ACL `POST /annuleringen` → ZGW `resultaten` + `statussen` (Geannuleerd) → aggregate `Verlopen`.

### Layers touched (each red→green)
- **ACL gateway** — `SetZaakToCancellationStatusAsync` (Geannuleerd + Vervallen by name); approval now resolves its `Geregistreerd` resultaat by name too (a second resultaattype now exists).
- **ACL service/API** — `AclService.CancelZaakAsync` + `POST /annuleringen`.
- **Domain** — `IAclClient.CancelZaakAsync` + client; expiry worker cancels the zaak before advancing to `Verlopen`, guarded against redelivery double-cancel.
- **Seed** — non-terminal `Geannuleerd` statustype (volgnummer 2; `Afgehandeld` → 3) + `Vervallen` resultaattype, both idempotent by omschrijving and sharing the zaaktype's procestype.
- **Verify/integration** — ACL↔OpenZaak integration test (live `Geannuleerd` + resultaat); `run-domain-check.sh` fires the real P30D timer and asserts the zaak reaches `Geannuleerd` end-to-end; BDD scenario asserts cancel-on-timeout vs untouched-when-in-time.
- **Docs** — ADR-0019 (cancellation modelling decision), demo-script, BACKLOG.

### Design note (ADR-0019)
ZGW allows only one eindstatus per zaaktype, so `Geannuleerd` is modelled as a **non-terminal** status (it records a cancellation status + resultaat but does not set `einddatum`). This follows the issue's explicit "distinct statustype + resultaat" outcome; the shared-eindstatus alternative is recorded in the ADR.

### Tests
Unit + acceptance all green locally (Acl 38, Big 134, Acceptance 17, Bff 33, EventSubscriber 19). Integration + verify-stack run in CI (need live OpenZaak + selectielijst egress).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #109
2026-07-21 13:58:15 +00:00
not 0904df8db0 feat(acl): diploma upload stored in the ZGW Documenten API (S-10b, closes #103) (#108)
CI / lint (push) Successful in 1m21s
CI / build (push) Successful in 1m4s
CI / unit (push) Successful in 1m12s
CI / frontend (push) Successful in 2m40s
CI / mutation (push) Successful in 5m31s
CI / verify-stack (push) Successful in 7m56s
## What & why

S-10b: the self-service **diploma upload** is now real. After submitting, the citizen picks a PDF and
uploads it; the portal base64-encodes it client-side → BFF → domain → **ACL**, which stores it in the
ZGW **Documenten (DRC) API** as an `enkelvoudiginformatieobject` and relates it to the zaak, then the
`WachtOpDocumenten` wait completes and the case advances to beoordeling. Per §8.1 only the ACL talks to
ZGW.

Closes #103

Mechanism in **ADR-0018** (proposal #107). Builds on S-10a (#102). The zaak-close-on-expiry item is
carved to **#106 (S-10c)**.

## Definition of Done

- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation (red→green per layer).
- [x] Conventional Commits referencing the issue (`refs #103`).
- [ ] CI green — all Gitea Actions jobs (pending on this PR).
- [x] `docker compose up` health unaffected (ACL boots on a placeholder informatieobjecttype URL; the real one is injected by verify-domain).
- [x] Docs updated (ADR-0018, demo-script, BACKLOG + S-10c).
- [x] ADR added (`docs/architecture/adr-0018-diploma-upload-via-acl-documenten.md`).
- [x] Demo note in `docs/demo-script.md`.

## Notes for reviewers

- **ACL** (`OpenZaakGateway.StoreDocumentAsync` + `AclService.StoreDiplomaAsync` + `POST /documenten`) reuses the existing gateway patterns (ZGW Bearer, buffered non-chunked body, **no CRS** — Documenten isn't geo). Unit-tested via the stub handler; an **integration test** stores a real document against live OpenZaak (verify-acl).
- **Transport:** base64 JSON on every hop (portal encodes client-side) — I deviated from proposal #107's multipart to keep one contract shape and avoid `IFormFile`/antiforgery/multipart-client plumbing; fine at diploma size (ADR-0018 §Alternatives).
- **Infra:** `seed_catalogus.py` seeds + publishes a "Diploma" `informatieobjecttype` and relates it to the zaaktype (while both concept); `verify-domain` injects its URL into the ACL. No new ZGW scopes (seed applicatie has `heeft_alle_autorisaties`).
- **e2e:** uploads a real PDF (`setInputFiles`) after the openbaar INGEDIEND row confirms the zaak is open (so storage doesn't race the OpenZaak worker).
- **Scope boundary:** the ZGW zaak is not set to a cancellation status on 30-day expiry — that's #106 (S-10c).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #108
2026-07-21 12:15:33 +00:00
not 4777ff2b1d feat(workflow): document-wait task + 30-day timeout cancellation (S-10a, closes #102) (#105)
CI / build (push) Successful in 1m1s
CI / unit (push) Successful in 1m11s
CI / frontend (push) Successful in 2m33s
CI / mutation (push) Successful in 5m14s
CI / verify-stack (push) Successful in 7m37s
CI / lint (push) Successful in 1m17s
## What & why

S-10a, the **workflow/timeout spine** of the (split) document-upload slice: the registratie process
now parks at a **`WachtOpDocumenten`** user task with an **interrupting `P30D` boundary timer**. When
the documents arrive the task completes and the process continues into the diploma routing (S-13) →
Beoordelen; if the 30 days lapse, the timer cancels the wait, runs a `RegistratieVerlopen`
external-worker task, and the domain expires the aggregate to a new terminal status **`Verlopen`**.
Backend only — the real upload trigger (portal → BFF → ACL → Documenten API) is S-10b (#103).

Closes #102

Mechanism recorded in **ADR-0017**; opened as proposal #104. Mirrors the S-14 escalation
(boundary-timer + external-worker) and S-11 withdrawal (interrupting cancel) patterns.

## Definition of Done

- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation (red→green pairs per layer).
- [x] Implementation makes the test pass.
- [x] Conventional Commits referencing the issue (`refs #102`).
- [ ] CI green — all Gitea Actions jobs (pending on this PR).
- [x] `docker compose up` health unaffected (no new services; deploy path unchanged).
- [x] Docs updated (ADR-0017, demo-script, BACKLOG split).
- [x] ADR added (`docs/architecture/adr-0017-document-wait-timeout-cancellation.md`).
- [x] Demo note in `docs/demo-script.md`.

## Notes for reviewers

- **Domain** (`Registration.Expire()` + `Verlopen`), **application** (`ExpireRegistrationWorker`),
  **infra** (`RegistratieVerlopenProcessor`/`Pump`, `IRegistratieVerlopenClient`, Flowable
  acquire/complete + `CompleteDocumentWaitAsync`) — the timeout counterpart to the OpenZaak/escalation
  worker trios; idempotent per §8.6.
- **BPMN** verified live against a `flowable-rest` probe: complete `WachtOpDocumenten` → routes to
  Beoordelen; fire the P30D timer → `RegistratieVerlopen` job (carrying `registrationId`) + the wait
  task cancelled. `verify-domain` exercises both branches in-stack (completes the wait in every existing
  block; fires the timer and asserts `Verlopen` in a new block).
- **Scope boundary:** on expiry the aggregate goes `Verlopen` and the process ends, but the ZGW *zaak*
  is not yet set to a cancellation status — that needs a new ACL method + statustype seeding and is
  folded into S-10b (noted in ADR-0017).
- `CompleteDocumentWaitAsync` is built and HTTP-tested here but not yet called from a domain endpoint;
  S-10b wires the upload trigger to it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #105
2026-07-20 09:42:02 +00:00
not ccae27b3da feat(workflow): diploma-eligibility DMN routes foreign diplomas via CBGV-advies (S-13, closes #14) (#101)
CI / lint (push) Successful in 1m16s
CI / unit (push) Successful in 1m14s
CI / mutation (push) Successful in 5m14s
CI / build (push) Successful in 58s
CI / frontend (push) Successful in 2m29s
CI / verify-stack (push) Successful in 9m20s
## What & why

S-13: a diploma's origin decides its route. A **DMN** (`diploma-eligibility`) is evaluated inline by
the registratie process as a **`businessRuleTask`**; an exclusive gateway routes a **foreign**
(Buitenlands) diploma through a new **CBGVAdvies** user task before `Beoordelen`, a **domestic** one
straight there (PRD flow 4). The domain's only new job is carrying the diploma origin and passing it
as a process start variable.

Chose **Option B (DMN in the BPMN)** over the issue's literal "evaluated by the Domain Service via
Workflow Client" wording — keeps the decision a first-class workflow artefact and §8.2 clean.
Rationale in **ADR-0016** (proposal #100); noted on this issue.

Closes #14

## Definition of Done

- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation.
- [x] Implementation makes the test pass.
- [x] Conventional Commits referencing the issue (`refs #14`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (additive; DMN deployed by flowable-init).
- [x] Docs updated (ADR-0016, demo note).
- [x] ADR added (`docs/architecture/adr-0016-diploma-eligibility-dmn.md`).
- [x] Demo note in `docs/demo-script.md`.

## How it was built (TDD)

- **Domain**: `DiplomaOrigin` on the aggregate + submit command; threaded through the process-start port so the Workflow Client emits a `diplomaOrigin` start variable. Red → green.
- **DMN + BPMN**: `workflows/diploma-eligibility.dmn` (origin → route); `businessRuleTask` + exclusive gateway + `CBGVAdvies` user task in `registratie.bpmn`; DMN deployed to Flowable's DMN engine by `flowable-init`.
- **Both paths**: `Een diploma op herkomst routeren` acceptance scenarios (origin carried into the process) + unit tests; verify-domain drives a foreign registration through CBGVAdvies→Beoordelen and the domestic one straight to Beoordelen — exercising both DMN branches live.

## Notes for reviewers

- Deviation from the issue's Option-A wording is deliberate and recorded (ADR-0016); the outcome is unchanged.
- The self-service eIDAS→foreign wiring is out of scope here (this slice is area:domain + area:workflow); the domain submit accepts an optional `diplomaOrigin` so the foreign path is drivable.
- Local green: domain unit 109, acceptance 15, `dotnet format`, Release build (0 errors), **domain mutation 95.39%** (break 90). The DMN/`businessRuleTask` REST wiring is CI-verified on verify-stack (no local full-stack run here).

Reviewed-on: #101
2026-07-20 07:26:52 +00:00
not 7bcbc726ce feat(workflow): beoordeling escalation to teamlead after 14 days (S-14, closes #15) (#99)
CI / lint (push) Successful in 1m14s
CI / build (push) Successful in 56s
CI / unit (push) Successful in 1m9s
CI / frontend (push) Successful in 2m27s
CI / mutation (push) Successful in 5m11s
CI / verify-stack (push) Successful in 7m30s
## What & why

S-14: a beoordeling a behandelaar does not pick up within **14 days** escalates to the **teamlead**.

A non-interrupting `P14D` boundary timer on the `Beoordelen` user task fires an external-worker task
(`BeoordelingEscaleren`); the domain's escalation worker reassigns the still-open task's candidate group
from `behandelaar` to `teamlead`. The task keeps its identity — only who may claim it changes. The
escalation-via-external-worker decision is recorded in **ADR-0015** (proposal #98); it upholds §8.2
(the Workflow Client stays the only code that talks to Flowable) and keeps Flowable a stock image.

Closes #15

## Definition of Done

- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation.
- [x] Implementation makes the test pass; refactor commit if structure improved.
- [x] Conventional Commits referencing the issue (`refs #NN`).
- [x] CI green — all Gitea Actions jobs.
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (no new services; escalation is additive to the domain worker).
- [x] Docs updated (ADR-0015, demo note).
- [x] ADR added (`docs/architecture/adr-0015-beoordeling-escalation.md`).
- [x] Demo note in `docs/demo-script.md`.

## How it was built (TDD)

- **Workflow Client** (`IBeoordelingEscalatieClient`): acquire `BeoordelingEscaleren` jobs → find the open `Beoordelen` task in the instance → add `teamlead`/remove `behandelaar` candidate group → complete the job. Red → green.
- **Escalation drain loop** (`BeoordelingEscalatieProcessor`) + hosted `BeoordelingEscalatiePump`, mirroring the OpenZaak worker. Red → green.
- **BPMN**: non-interrupting `P14D` boundary timer on `Beoordelen` → external task → escalation end.
- **Both branches** (escalate after timeout; no-op when completed in time) covered by the `Een beoordeling escaleren` acceptance scenarios + Workflow Client unit tests.
- **Live integration**: `verify-domain` fires the timer early via Flowable's management API and asserts the reassignment to teamlead.

## Notes for reviewers

- Interface segregation: escalation is on `IBeoordelingEscalatieClient`, separate from the OpenZaak worker's `IExternalWorkerClient`.
- Reassignment is two REST hops (add teamlead, remove behandelaar); idempotent on redelivery — see ADR-0015 consequences.
- Local checks green: domain unit tests (104), acceptance (13), `dotnet format --verify-no-changes`, Release build (0 errors), **domain mutation 96.69%** (break 90). The `run-domain-check.sh` escalation path is CI-verified on verify-stack (local full-stack run is constrained here).
- `BeoordelingEscalatiePump` excluded from mutation, mirroring the existing `OpenZaakJobPump` exclusion.

Reviewed-on: #99
2026-07-17 09:45:36 +00:00
not 8a537edd6c fix(infra): engine-portable portal nginx resolver (closes #96) (#97)
CI / lint (push) Successful in 1m28s
CI / build (push) Successful in 1m18s
CI / unit (push) Successful in 1m33s
CI / frontend (push) Successful in 3m7s
CI / mutation (push) Successful in 5m14s
CI / verify-stack (push) Successful in 7m7s
## What & why

Closes #96. The portal nginx configs hardcode `resolver 127.0.0.11` (Docker's embedded DNS) for their variable `proxy_pass` to the BFF, so on rootless **podman** (network-specific aardvark DNS) every proxied call 502'd — the portals loaded and login worked, but no in-app data flowed.

Add a shared `/docker-entrypoint.d` hook (`apps/portal-nginx-resolver.sh`, wired into all three portal Dockerfiles) that rewrites the resolver from the container's own `/etc/resolv.conf` at startup: a **no-op on Docker** (nameserver *is* 127.0.0.11) and **correct on podman** (rewrites to e.g. 10.89.0.1). nginx.conf is unchanged (the hardcoded value is the substitution anchor).

## How verified

Built the behandel image and ran it on the compose network under podman: the hook rewrote the config to `resolver 10.89.0.1`, and `GET /behandel/werkbak` proxied to the BFF returning **401** (auth), not 502. On Docker the nameserver is 127.0.0.11 so the substitution is a no-op and CI/e2e behaviour is unchanged.

Reviewed-on: #97
2026-07-16 14:23:40 +00:00
not e7bed37cda fix(infra): local event-subscriber Acl:BaseUrl parity (closes #94) (#95)
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled
CI / unit (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / mutation (push) Has been cancelled
CI / verify-stack (push) Has been cancelled
## What & why

Closes #94. The local compose's `event-subscriber` lacked `Acl__BaseUrl` (and the `acl` dependency) that the canonical compose sets (#78) — so it threw `Missing configuration 'Acl:BaseUrl'` and exited on startup, which also knocked over podman-compose's bring-up of the rest of the stack (the frontends were left uncreated). Adds the env + dependency, matching `docker-compose.yml`.

## How verified

Recreated `event-subscriber` from the fixed compose locally — it now starts healthy, and the three portals come up (self-service :8140, openbaar :8141, behandel :8142). `docker compose config` valid.

## Note (separate, not fixed here)

On **rootless podman** the portal→BFF nginx proxy still 502s (`resolver 127.0.0.11` is Docker's embedded DNS; podman uses its own), and podman-compose orchestration of this dependency graph is flaky — both are pre-existing local-engine limitations, clean on Docker Desktop / CI. Tracking separately.

Reviewed-on: #95
2026-07-16 13:56:41 +00:00
not 94699f3603 feat(self-service): trek aanvraag in — withdrawal action (S-11c-2, closes #12) (#93)
CI / unit (push) Successful in 1m22s
CI / lint (push) Successful in 1m23s
CI / build (push) Successful in 1m15s
CI / frontend (push) Successful in 3m1s
CI / mutation (push) Successful in 6m21s
CI / verify-stack (push) Successful in 7m56s
## What & why

Final sub-slice of **S-11 · Withdrawal (Flow 3)** — the user-facing "trek aanvraag in" action, which **closes #12**.

- **self-service portal**: the submit confirmation gains a **"Trek aanvraag in"** button. It withdraws the just-submitted registration via `postSelfServiceRegistrationsIdWithdraw(reference)`; success shows an *ingetrokken* confirmation, a failure is surfaced (`role="alert"`) and the action stays available — same confirm-and-surface pattern as submit.
- **acceptance**: `Een registratie intrekken` — owner withdraws → INGETROKKEN + workflow cancelled; a different bsn is reported not-found.
- **e2e**: `withdrawal.spec.ts` — DigiD submit → trek aanvraag in → the portal confirms ingetrokken.
- **docs**: demo-script + frontend-decisions.

Together with S-11a (#88), S-11b (#89), S-11c-1 (#90), this completes the flow: citizen withdraws → domain INGETROKKEN → BPMN message event cancels the process → the case leaves the behandelaar's werkbak.

Closes #12

## Definition of Done

- [x] Linked Gitea issue (#12).
- [x] Failing tests committed before the implementation.
- [x] Implementation makes the tests pass.
- [x] Conventional Commits referencing the issue (`refs #12`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` unaffected.
- [x] Docs updated (demo-script + frontend-decisions).
- [x] ADR — ADR-0014 (from S-11b) covers the cancellation decision; nothing new here.

## Notes for reviewers

- Full local gate run before pushing: `dotnet format --verify-no-changes` clean; `make unit` green (Acceptance **11** incl. the 2 new withdrawal scenarios, Big 95, BFF 30, Acl 27, EventSubscriber 19); self-service lint/test/build green (9 tests, incl. the 2 new withdraw tests).
- `withdrawal.spec.ts` waits on the *ingetrokken* confirmation (which only renders after the withdraw POST returns), so it can't cancel the request early (the 499 lesson from #87). Live-validated by verify-stack.

Reviewed-on: #93
2026-07-16 13:06:55 +00:00
not 951bdd8364 fix(infra): local compose parity + host-browser OIDC (closes #91) (#92)
CI / build (push) Has been cancelled
CI / unit (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / mutation (push) Has been cancelled
CI / verify-stack (push) Has been cancelled
CI / lint (push) Has been cancelled
## What & why

Closes #91. `infra/docker-compose.local.yml` (the no-make local stack) was missing the `domain` service and all three portals, and never wired host-browser OIDC — so browsing the behandel portal redirected to `http://keycloak:8080/…`, which a host browser can't resolve.

- **Parity**: add `domain`, `self-service`, `openbaar`, `behandel` (local now matches the CI-canonical `docker-compose.yml` service-for-service).
- **BFF**: give it the Keycloak + downstream env it was missing (it previously fell back to appsettings and couldn't reach Keycloak).
- **Host-browser OIDC**: pin Keycloak's frontend/issuer URL to `http://localhost:8180` (`KC_HOSTNAME`) with `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true`, so a host browser logs in on `localhost:8180` while the BFF still validates in-network via `keycloak:8080`.
- **Portals**: bind-mount a `localhost:8180` `config.json` over the image's baked `keycloak:8080` one (`infra/local-config/*`). openbaar is anonymous, no config.

## How verified

- `docker compose -f infra/docker-compose.local.yml config` valid; parity check shows nothing missing.
- Started Keycloak from the local compose and confirmed the discovery document:
  - **host view** (`localhost:8180`): `issuer` + all endpoints on `localhost:8180` (what the browser uses).
  - **in-network view** (`keycloak:8080`): `issuer` stays `http://localhost:8180/...` (matches browser tokens) while `jwks_uri`/`token_endpoint` resolve to `keycloak:8080` (reachable by the BFF).

## Notes for reviewers

- The full portal→BFF→Keycloak login round-trip should get a quick browser smoke test on a real engine (I validated the Keycloak issuer/backchannel split and compose validity, but can't drive a browser here). Ports: self-service :8140, openbaar :8141, behandel :8142; users in `docs/synthetic-data.md`.
- On rootless podman the portal→BFF nginx proxy (`resolver 127.0.0.11`) may 502 (a separate known podman-vs-docker DNS quirk); login is a browser redirect and is unaffected. Works on Docker Desktop.
- No app-code change; `docker-compose.yml` (CI-canonical) is untouched.

Reviewed-on: #92
2026-07-16 12:45:07 +00:00
not 2397d9196a feat(bff): owner-scoped self-service withdraw endpoint (S-11c-1, refs #12) (#90)
CI / build (push) Has been cancelled
CI / unit (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / mutation (push) Has been cancelled
CI / verify-stack (push) Has been cancelled
CI / lint (push) Has been cancelled
## What & why

Third sub-slice of **S-11 · Withdrawal (Flow 3)** (#12) — the **owner-scoped BFF withdraw endpoint** (backend). S-11a/b made a withdrawal transition the aggregate and cancel the workflow; this adds the citizen-facing entry point through the BFF, gated to the registration's owner.

- **Domain**: `WithdrawRegistrationCommand` carries the caller's `bsn`; the handler returns a `WithdrawOutcome` and refuses a bsn that doesn't own the registration. Unknown and not-owned are **both 404** (indistinguishable — ownership isn't revealed). `POST /registrations/{id}/withdraw` takes `{bsn}` and maps the outcome (204/404).
- **BFF**: `POST /self-service/registrations/{id}/withdraw` (DigiD-authenticated) forwards the token's `bsn` to the domain and relays 204/404. The BFF authenticates; the domain owner-scopes (an aggregate invariant, not the domain doing auth).
- OpenAPI spec + Angular client regenerated for the new endpoint.
- `run-domain-check.sh` withdrawal step now sends the owner `bsn` (verify-stack).

Refs #12 — the self-service "trek aanvraag in" button + e2e (S-11c-2) closes it.

## Definition of Done

- [x] Linked Gitea issue (#12).
- [x] Failing tests committed before the implementation.
- [x] Implementation makes the tests pass.
- [x] Conventional Commits referencing the issue (`refs #12`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` unaffected.
- [x] No ADR needed (owner-scoping is an aggregate invariant; no boundary change).
- [x] Docs — the user-visible demo note lands with S-11c-2.

## Notes for reviewers

- **Full local gate run before pushing this time** (lessons from #89): `dotnet format --verify-no-changes` clean; `make unit` green — Acl 27, EventSubscriber 19, BFF 30, Acceptance 9, Big 95; `api-client` lint+test green.
- Owner mismatch returns 404 (not 403) so the portal can't be used to probe which references exist.

Reviewed-on: #90
2026-07-16 12:20:43 +00:00
not a34caba9ea feat(domain): withdrawal cancels the registratie process (S-11b, refs #12) (#89)
CI / build (push) Successful in 57s
CI / lint (push) Successful in 1m18s
CI / unit (push) Successful in 1m10s
CI / frontend (push) Successful in 2m38s
CI / mutation (push) Successful in 5m22s
CI / verify-stack (push) Successful in 7m18s
## What & why

Second sub-slice of **S-11 · Withdrawal (Flow 3)** (#12). S-11a (#88) made a withdrawal advance the aggregate to INGETROKKEN; this sub-slice **cancels the running Flowable process** so the withdrawn case leaves the behandelaar's werkbak.

- **BPMN** (`registratie.bpmn`): an interrupting message boundary event (`RegistratieIngetrokken`) on the `Beoordelen` task, routing to a dedicated "Registratie ingetrokken" end event.
- **Workflow Client**: `WithdrawBeoordelingAsync(executionId)` delivers `messageEventReceived` to the task's execution (PUT); `BeoordelingTask` now carries its `executionId`.
- **`WithdrawRegistration` handler**: after the domain transition, finds the open `Beoordelen` task for the registration and delivers the withdrawal message — best-effort, mirroring how the beoordeling completes its task.
- **Werkbak**: also filters out registrations that are no longer open, so a withdrawn case never surfaces even in the brief window before cancellation lands.
- **ADR-0014** records the decision (message event in BPMN vs. deleting the instance from code).
- **verify (`run-domain-check.sh`)**: a second registration parks at `Beoordelen`, is withdrawn via the domain, and the check asserts its `Beoordelen` task disappears — so verify-stack validates the live Flowable message correlation.

Refs #12 (S-11c — the BFF + self-service "trek aanvraag in" button + e2e — closes it).

## Definition of Done

- [x] Linked Gitea issue (#12).
- [x] Failing tests committed before the implementation (red → green per commit).
- [x] Implementation makes the tests pass.
- [x] Conventional Commits referencing the issue (`refs #12`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` unaffected (BPMN redeploys on a fresh CI DB via flowable-init).
- [x] ADR added (ADR-0014).
- [x] Docs — the user-visible demo note lands with S-11c.

## Notes for reviewers

- Verified locally: `Big.Tests` 94/94 pass; `Big.Api` builds; `registratie.bpmn` is well-formed.
- The Flowable message-correlation REST shape is validated **live** by verify-stack (the Workflow Client unit tests stub the exchange and assert only the request shape, per ADR-0009) — the new `run-domain-check.sh` withdrawal step is that live check.
- Known gap (ADR-0014): a withdrawal that races ahead of the process reaching `Beoordelen` finds no task to cancel; the aggregate is still INGETROKKEN and the werkbak filter hides it, but that instance parks unattended. A process-level event subprocess would close the gap — deferred.

Reviewed-on: #89
2026-07-16 11:09:28 +00:00
not 1f1c944a8b feat(domain): withdrawal — INGETROKKEN transition + endpoint (S-11a, refs #12) (#88)
CI / lint (push) Successful in 1m14s
CI / build (push) Successful in 56s
CI / unit (push) Successful in 1m5s
CI / frontend (push) Successful in 2m31s
CI / mutation (push) Successful in 4m57s
CI / verify-stack (push) Successful in 6m46s
## What & why

First sub-slice of **S-11 · Withdrawal (Flow 3)** (#12). A zorgprofessional can withdraw a still-open registration ("trek aanvraag in"); this sub-slice delivers the **domain transition + endpoint**, mirroring how S-12a shipped the beoordeling decision model on its own (#82).

- `RegistrationStatus.Ingetrokken` (terminal).
- `Registration.Withdraw()` — allowed from INGEDIEND or IN_BEHANDELING, needs no zaak, idempotent, and rejected once the registration has been decided (INGESCHREVEN/AFGEWEZEN).
- `WithdrawRegistration` application handler (load → withdraw → persist; repeated withdrawal is a no-op).
- `POST /registrations/{id}/withdraw` on the domain API.

Demoable: `POST /registrations/{id}/withdraw` → `GET /registrations/{id}` shows `INGETROKKEN`.

Refs #12 (not closing — see below).

## Scope / follow-ups

S-11 is bigger than one slice, so it is split (CLAUDE.md §13), like S-12 was:
- **S-11a (this PR)** — domain withdrawal transition + endpoint.
- **S-11b** — cancel the running Flowable process via a BPMN message event, so a withdrawn case leaves the behandelaar's werkbak.
- **S-11c** — owner-scoped BFF self-service withdraw endpoint + "trek aanvraag in" button + e2e.

Cancelling the Flowable process is deliberately deferred (documented in `WithdrawRegistration`), exactly as the beoordeling's rejection deferred its zaak propagation. #12 stays open until S-11c.

## Definition of Done

- [x] Linked Gitea issue (#12).
- [x] Failing test committed before the implementation.
- [x] Implementation makes the test pass.
- [x] Conventional Commits referencing the issue (`refs #12`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` unaffected (no infra/contract change).
- [x] Docs — none needed for this backend sub-slice; the user-visible demo note lands with S-11c.
- [x] No ADR needed — mirrors existing aggregate/handler/endpoint patterns; no boundary change.

## Notes for reviewers

- Verified locally: `Big.Tests` 89/89 pass; `Big.Api` builds clean.
- The domain trusts its callers (§8.3); owner-scoping by the caller's bsn is enforced at the BFF in S-11c.

Reviewed-on: #88
2026-07-16 09:15:12 +00:00
not 3abf8f7ccf feat(behandel): behandel-portal — werkbak + beoordeling (closes #13) (#87)
CI / lint (push) Successful in 1m14s
CI / build (push) Successful in 53s
CI / unit (push) Successful in 1m3s
CI / frontend (push) Successful in 2m30s
CI / mutation (push) Successful in 4m59s
CI / verify-stack (push) Successful in 7m5s
## What & why

Finishes **S-12 · Behandel-portal — werkbak + beoordeling**. The backend sub-slices (S-12a/b/c-1/c-2) were merged, but the slice's stated outcome — a behandel *portal* with medewerker login, a werkbak, and decide — had no frontend. This adds it.

- **`libs/auth`**: `MedewerkerAuthService` + `provideMedewerkerAuth` (Keycloak `medewerker` realm), a `roles`/`hasRole` surface on the shared `AuthService`, and a realm-roles protocol mapper so the SPA can read `behandelaar`/`teamlead` from the token. The BFF remains the security boundary (ADR-0013).
- **`apps/behandel`**: a new Nx Angular app mirroring self-service — medewerker OIDC login and a **werkbak** page listing registrations awaiting beoordeling (`GET /behandel/werkbak`) with per-row **Goedkeuren/Afwijzen** actions (`POST /behandel/registrations/{id}/decide`) that refresh the list. NL DS/Utrecht, standalone + signals.
- **e2e**: the walking-skeleton happy path now approves through the real portal (behandelaar logs in, finds the row by reference, clicks Goedkeuren) instead of the temporary admin endpoint.
- **infra/docs**: behandel service in compose (`:8142`, depends on Keycloak); added to the smoke `WAIT_SVCS` + CI log dump; `frontend-decisions.md` and `demo-script.md` updated.

Closes #13

## Definition of Done

- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation.
- [x] Implementation makes the test pass; refactor commit if structure improved.
- [x] Conventional Commits referencing the issue (`refs #13`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. *(behandel image + container verified locally; full stack gated in CI.)*
- [x] Docs updated if behaviour, contracts, or operations changed.
- [x] ADR added — ADR-0013 (merged with the backend sub-slices) already covers the wiring; no new decision here.
- [x] Demo note in `docs/demo-script.md`.

## Notes for reviewers

- Verified locally: auth + behandel + all frontend projects pass lint & unit tests (incl. axe WCAG 2.1 AA); production build green; the behandel Docker image builds and serves with the correct baked `medewerker` config + SPA fallback.
- The full compose-up smoke, e2e, and mutation are CI-gated (known local full-stack verify limits).
- **Follow-ups (not in scope):** the `WerkbakItem` contract has no citizen name (werkbak shows the BSN) — adding one is a BFF+domain contract change; and the domain's temporary admin `approve` endpoint is now unused by the e2e and could be removed.

Reviewed-on: #87
2026-07-16 08:31:57 +00:00
not d226b6402d feat(#13): S-12c-2 — behandel decide → domain + complete workflow task (#86)
CI / lint (push) Successful in 1m17s
CI / build (push) Successful in 1m0s
CI / unit (push) Successful in 1m8s
CI / frontend (push) Successful in 2m20s
CI / mutation (push) Successful in 4m57s
CI / verify-stack (push) Successful in 7m3s
## 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
2026-07-16 06:53:52 +00:00
not 9c3da48d8e feat(#13): S-12c-1 — behandel BFF auth + werkbak (ADR-0013) (#85)
CI / lint (push) Successful in 1m26s
CI / build (push) Successful in 1m19s
CI / unit (push) Successful in 1m14s
CI / frontend (push) Successful in 2m37s
CI / mutation (push) Successful in 5m54s
CI / verify-stack (push) Successful in 7m18s
## What & why

First half of **S-12c** (behandel-portal backend), per **ADR-0013** (decisions recorded in #84):

- **BFF multi-realm auth.** A second JWT bearer scheme (`medewerker`) alongside the default `digid` scheme. On validation it lifts Keycloak's `realm_access.roles` onto the principal, and a `behandelaar` policy (medewerker scheme + `behandelaar` role) gates `/behandel/*`. Self-service keeps the digid scheme.
- **Werkbak = Flowable tasks.** The domain `Werkbak` query reads the open `Beoordelen` tasks (§8.2, S-12b's `IUserTaskClient`) and enriches each with its aggregate's bsn + status; `GET /behandel/werkbak` (domain) is proxied by the BFF `GET /behandel/werkbak` behind the behandelaar policy. The read projection stays the anonymous openbaar model (no premature `IN_BEHANDELING`/personal-data plumbing — deferred in ADR-0008).

Behavior: `/behandel/werkbak` is **401** without a token, **403** for a medewerker lacking the role, **200 + werkbak** for a behandelaar.

**S-12c-2** (next): `POST /behandel/registrations/{id}/decide` → domain decision + complete the Flowable task.

## Definition of Done

- [x] Linked issue: #13 (umbrella, `refs`); closes the adr-proposal #84
- [x] Tests first; red → green per layer
- [x] Unit + acceptance green (`make unit`): domain 78, bff 23, acceptance 9 (+ acl/event-subscriber unaffected)
- [x] api-client `test` green; openapi.json regenerated (drift guard passes)
- [x] Mutation ≥ break(90): **domain 100%, bff 100%**
- [x] ADR-0013 added; `Keycloak__MedewerkerAuthority` wired into compose
- [ ] CI green (pending)

Part of #13. closes #84

Reviewed-on: #85
2026-07-15 09:54:01 +00:00
not 4085bdead7 feat(#13): S-12b — Workflow Client user-tasks + Beoordelen userTask (#83)
CI / lint (push) Successful in 1m23s
CI / build (push) Successful in 1m10s
CI / unit (push) Successful in 1m14s
CI / frontend (push) Successful in 2m23s
CI / mutation (push) Successful in 5m45s
CI / verify-stack (push) Successful in 7m7s
## What & why

Second sub-slice of **S-12 (#13)** — the **Workflow Client gains behandelaar user-task operations**, and the process model gains the beoordeling step.

- **BPMN:** `registratie.bpmn` now parks at a `Beoordelen` **userTask** (candidate group `behandelaar`) after `OpenZaakAanmaken`; `registrationId` rides along as a process variable so the werkbak can correlate each task to its aggregate.
- **Workflow Client** (`IUserTaskClient`, the only code that talks to Flowable §8.2):
  - `GetOpenBeoordelingenAsync()` — the werkbak (open `Beoordelen` tasks + their `registrationId`)
  - `ClaimAsync(taskId, behandelaar)`
  - `CompleteBeoordelingAsync(taskId, besluit)` — carries the decision into the process as the `besluit` variable
- **Live integration:** `verify-domain` now drives the full user-task lifecycle against a real Flowable — after the worker opens the zaak, it polls for the task, claims it as `merel-behandelaar`, completes it (`goedkeuren`), and asserts the process finishes. This proves the exact REST contract (`service/runtime/tasks/query` + `…/{id}` claim/complete) the client depends on.

The walking skeleton is unaffected: the temporary `/approve` path still sets the zaak status directly; wiring the domain decision to *complete this task* (and driving the werkbak from the BFF) lands in **S-12c**.

## Definition of Done

- [x] Linked issue: #13 (umbrella; `refs`, does not close)
- [x] Tests first; red → green
- [x] Unit + acceptance green (`make unit`): domain 76, acceptance 9 (acl/event-subscriber/bff unaffected)
- [x] Mutation ≥ break(90): **domain 100%** (killed the new survivors *and* the pre-existing `FlowableWorkflowClient` baseline)
- [x] Live Flowable user-task lifecycle asserted in `verify-domain`
- [ ] CI green (pending)

Part of #13.

Reviewed-on: #83
2026-07-15 08:53:33 +00:00
not d4ed0ffc22 feat(#13): S-12a — beoordeling decision model (domain) (#82)
CI / lint (push) Successful in 1m15s
CI / build (push) Successful in 58s
CI / unit (push) Successful in 1m9s
CI / frontend (push) Successful in 2m23s
CI / mutation (push) Successful in 5m3s
CI / verify-stack (push) Successful in 8m37s
## 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
2026-07-15 07:12:19 +00:00
not 3023bb6fbe chore(release): 2026.07.0 (#81)
CI / lint (push) Successful in 1m28s
CI / build (push) Successful in 1m25s
CI / unit (push) Successful in 1m33s
CI / frontend (push) Successful in 2m58s
CI / mutation (push) Successful in 6m43s
CI / verify-stack (push) Successful in 7m42s
Cuts the first CalVer release **2026.07.0** (tag ), marking the end of **Iteration 1 — Walking Skeleton**.

 regenerated from Conventional Commits by git-cliff (covers Iterations 0 and 1, through #79).

After merge: tag  on main and publish the Gitea Release.

closes #80

Reviewed-on: #81
v2026.07.0
2026-07-14 14:46:55 +00:00
not 9997da8beb feat(#78): one citizen reference across self-service and the openbaar register (#79)
CI / lint (push) Successful in 1m25s
CI / build (push) Successful in 1m17s
CI / unit (push) Successful in 1m33s
CI / frontend (push) Successful in 2m54s
CI / mutation (push) Successful in 6m34s
CI / verify-stack (push) Successful in 7m39s
## What & why

Before this change the self-service confirmation and the openbaar register showed **different** identifiers, so a citizen could not look their registration back up (#78). Now both surface the same **reference**:

- **domain → ACL (write):** the domain `registrationId` is set as the zaak's `identificatie` on `POST /zaken`.
- **event-subscriber → ACL (read):** the subscriber reads the zaak's `identificatie` back through the ACL (§8.1 — only the ACL talks to ZGW) via a new `POST /zaken/reference`, and stores it on the projection row **and** the `processed_notifications` replay log.
- **BFF + openbaar:** the public view exposes `id/status/reference` (never bsn/naam) and searches by id or reference; the register's "Referentie" column shows the reference.

Storing the reference in the replay log keeps ADR-0008's **rebuild-is-log-only** invariant intact — `/admin/rebuild` reproduces the reference without re-reading the ACL.

Decision recorded in **ADR-0012**.

## Definition of Done

- [x] Linked issue: #78
- [x] Tests written first; red → green per layer
- [x] Unit + acceptance green (`make unit`): domain 49, acl 27, bff 20, event-subscriber 19, acceptance 7
- [x] Frontend lint + test green (`nx run-many -t lint test`)
- [x] Mutation ≥ break(90): acl 100%, event-subscriber 100%, bff 100%, domain 98.41% (pre-existing FlowableWorkflowClient baseline, untouched)
- [x] e2e extended: confirmation reference == register reference
- [x] openapi.json + api-client regenerated (drift guard green)
- [x] ADR-0012 added; demo-script note appended
- [x] `Acl__BaseUrl` wired for the subscriber in compose

closes #78

Reviewed-on: #79
2026-07-14 14:01:49 +00:00
not 1c185e6686 S-09b: Approval flow — temp admin endpoint + status transition to projection (#77)
CI / lint (push) Successful in 1m25s
CI / build (push) Successful in 1m13s
CI / unit (push) Successful in 1m26s
CI / mutation (push) Successful in 6m6s
CI / verify-stack (push) Successful in 7m34s
CI / frontend (push) Successful in 2m35s
## 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
2026-07-14 09:04:57 +00:00
not bc9831c113 S-09: Openbaar Register portal — public lookup (#76)
CI / lint (push) Successful in 1m9s
CI / build (push) Successful in 53s
CI / unit (push) Successful in 1m4s
CI / frontend (push) Successful in 1m57s
CI / mutation (push) Successful in 5m19s
CI / verify-stack (push) Successful in 6m31s
Anonymous openbaar portal completing the walking skeleton (submit → projection → public visibility).

closes #10
2026-07-13 14:35:34 +00:00
not 7e8c5d7b51 Merge pull request 'ci: speed up pipeline — NuGet cache + prebuilt Playwright image' (#74) from chore/73-ci-speedups into main
CI / lint (push) Successful in 1m13s
CI / build (push) Successful in 57s
CI / unit (push) Successful in 1m4s
CI / mutation (push) Successful in 4m5s
CI / verify-stack (push) Successful in 6m22s
CI / frontend (push) Successful in 1m46s
Reviewed-on: #74
2026-07-13 13:59:15 +00:00
notandClaude Opus 4.8 2b9eb5eb41 ci(e2e): run Playwright from the prebuilt image instead of downloading browsers (refs #73)
CI / build (pull_request) Successful in 56s
CI / mutation (pull_request) Successful in 4m6s
CI / verify-stack (pull_request) Successful in 7m3s
CI / unit (pull_request) Successful in 1m0s
CI / frontend (pull_request) Successful in 1m50s
CI / lint (pull_request) Successful in 5m43s
The verify-e2e lane downloaded ~150 MB of Chromium (npx playwright install) on
every verify-stack run. Use the official mcr.microsoft.com/playwright image with
browsers pre-baked; npm install still pins @playwright/test from tests/e2e, and
the image tag is kept in lockstep with that version. Verified the exact
create + docker cp + start flow launches the baked browser with no download.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:29:11 +02:00
notandClaude Opus 4.8 60df0845aa ci: cache the NuGet package store across the .NET jobs (refs #73)
lint, build, unit and mutation each restored packages from the network on every
run. There are no lock files (so setup-dotnet's built-in cache doesn't apply), so
cache ~/.nuget/packages keyed on the project files via actions/cache. Pinned @v3
to avoid the GHES guard that breaks @v4 on Gitea (gitea-actions-gotchas.md); the
cache is best-effort, so a miss simply restores from the network.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:29:11 +02:00
not 2a746736dc Merge pull request 'test(e2e): serve the portal + walking-skeleton Playwright e2e (closes #68)' (#72) from feat/68-e2e into main
CI / lint (push) Successful in 1m10s
CI / build (push) Successful in 54s
CI / unit (push) Successful in 1m0s
CI / frontend (push) Successful in 1m52s
CI / mutation (push) Successful in 4m11s
CI / verify-stack (push) Successful in 7m19s
Reviewed-on: #72
2026-07-13 13:20:57 +00:00
notandClaude Opus 4.8 986e36bc7d test(portal-self-service): guard that the DigiD token attaches to relative BFF calls (refs #68)
CI / lint (pull_request) Successful in 1m8s
CI / build (pull_request) Successful in 53s
CI / unit (pull_request) Successful in 58s
CI / frontend (pull_request) Successful in 2m10s
CI / mutation (pull_request) Successful in 3m58s
CI / verify-stack (pull_request) Successful in 7m48s
The token-attachment bug (secureRoutes set to the app origin, which a relative
api-client URL never matches) was only caught by the full-stack e2e. Add a fast
unit guard: drive the REAL angular-auth-oidc-client interceptor and the REAL
api-client against the production route value, faking only the config source and
the token storage. Asserts the bearer token rides the relative /self-service/
call and is withheld from the anonymous /openbaar/ call.

Extract the value to a shared SECURE_API_ROUTES constant so the test binds to
exactly what the app configures. Verified the guard fails (Authorization null)
if the value regresses to an origin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:00:32 +02:00
notandClaude Opus 4.8 7e152e4432 feat(portal-self-service): surface submit failures with a retryable alert (refs #68)
CI / build (pull_request) Successful in 54s
CI / lint (pull_request) Successful in 1m11s
CI / unit (pull_request) Successful in 1m0s
CI / frontend (pull_request) Successful in 1m49s
CI / mutation (pull_request) Successful in 4m3s
CI / verify-stack (pull_request) Successful in 7m42s
Add an error branch to submit(): on a failed BFF call, set a `failed` signal,
re-enable the button, and render a role="alert" message so the user knows the
submit did not go through and can retry — instead of the click silently doing
nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:33:09 +02:00
notandClaude Opus 4.8 5bf25f094d test(portal-self-service): submit surfaces BFF failures instead of swallowing them (refs #68)
Failing test: when postSelfServiceRegistrations errors, the page should show an
alert, not the confirmation, and keep the submit button available for retry.
Currently submit() has no error handler, so the rejection is swallowed and the
page silently stays put — exactly the failure mode that hid the missing-token
bug behind a 90s e2e timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:32:30 +02:00
notandClaude Opus 4.8 0e6c7d2066 fix(portal-self-service): attach the DigiD token to relative BFF calls (refs #68)
CI / lint (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 51s
CI / unit (pull_request) Successful in 1m3s
CI / verify-stack (pull_request) Successful in 7m59s
CI / frontend (pull_request) Successful in 1m48s
CI / mutation (pull_request) Successful in 3m57s
After login the submit silently did nothing: the confirmation ("...is
ontvangen...") never rendered because the POST to the BFF went out with no
Authorization header, so the BFF rejected it and the no-error-handler
subscribe left the page unchanged.

Root cause: angular-auth-oidc-client's interceptor attaches the token when
`req.url.startsWith(secureRoute)`. The api-client calls the BFF with RELATIVE
URLs (same-origin via the nginx proxy), so `req.url` is `/self-service/...` —
but secureRoutes was configured as the app ORIGIN (`http://self-service`),
which a relative URL never starts with. No match → no token.

Configure secureRoutes with the relative `/self-service/` prefix instead. The
unit test mocked the api-client, so only the walking-skeleton e2e exercises the
real token attachment — now green.

Verified against a focused stack (keycloak + self-service + real BFF + stub
domain): the submit now carries the bearer token, the BFF forwards to the
domain, and the portal shows the confirmation with the returned reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:09:15 +02:00
notandClaude Opus 4.8 39923e0e68 fix(e2e): treat the http portal origin as secure so DigiD PKCE login works (refs #68)
CI / lint (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 53s
CI / unit (pull_request) Successful in 1m3s
CI / frontend (pull_request) Successful in 1m47s
CI / mutation (pull_request) Successful in 4m2s
CI / verify-stack (pull_request) Failing after 7m51s
The walking-skeleton e2e timed out waiting for the Keycloak login form
(`#username`). Root cause: in the compose network the portal is served over
plain HTTP on a non-localhost origin (http://self-service), which is not a
secure context, so Web Crypto (`crypto.subtle`) is undefined. angular-auth-
oidc-client needs SubtleCrypto to build the PKCE code challenge, so
`authorize()` threw ("Cannot read properties of undefined (reading 'digest')")
and the login redirect never fired.

Production serves the portal over HTTPS, where this works. Instead of
terminating TLS in the throwaway e2e stack, tell Chromium to treat the origin
as secure via --unsafely-treat-insecure-origin-as-secure. The flag is only
honoured by the full Chromium build (new headless), not Playwright's default
headless-shell, so pin channel: 'chromium'.

Verified against a minimal in-network stack (keycloak + self-service): login
redirect now reaches the Keycloak form, and the full login → token exchange →
authenticated portal renders with no console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:37:31 +02:00
notandClaude Opus 4.8 2e00ad38ba ci(portal-self-service): run Vitest ahead of the production build to stop worker-start timeout (refs #68)
CI / lint (pull_request) Successful in 1m11s
CI / build (pull_request) Successful in 54s
CI / unit (pull_request) Successful in 1m3s
CI / frontend (pull_request) Successful in 2m0s
CI / mutation (pull_request) Successful in 4m5s
CI / verify-stack (pull_request) Failing after 10m20s
The frontend lane ran `nx run-many -t lint test build`, so the ~5min
self-service production build shared nx's task pool with the Vitest test
worker. @angular/build:unit-test's Vitest worker has hard-coded 60s/90s
startup timeouts (not configurable); on a CPU-constrained CI runner the
concurrent build starved the worker and it failed with "Timeout waiting
for worker to respond" — flaky, since it passed on the prior commit.

Split the target into a light lint+test phase and a separate build phase
so tests get CPU and the worker starts well inside its window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 12:51:56 +02:00
notandClaude Opus 4.8 be016f920c fix(portal-self-service): health-check nginx over IPv4 (127.0.0.1) (refs #68)
CI / lint (pull_request) Successful in 1m13s
CI / build (pull_request) Successful in 1m0s
CI / unit (pull_request) Successful in 1m6s
CI / frontend (pull_request) Failing after 7m11s
CI / mutation (pull_request) Successful in 3m59s
CI / verify-stack (pull_request) Failing after 7m47s
nginx listens on IPv4 only (listen 80), but 'localhost' inside the container resolves
to ::1 first, so the wget healthcheck got connection-refused and self-service never
went healthy — timing out the CI stack bring-up. Probe 127.0.0.1 instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:37:56 +02:00
notandClaude Opus 4.8 d3f23a4da3 docs(portal-self-service): serving/e2e decisions + walking-skeleton demo note (refs #68)
CI / lint (pull_request) Successful in 1m5s
CI / unit (pull_request) Successful in 1m2s
CI / frontend (pull_request) Successful in 1m31s
CI / mutation (pull_request) Successful in 3m55s
CI / build (pull_request) Successful in 52s
CI / verify-stack (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:09:04 +02:00
notandClaude Opus 4.8 490e7347b0 test(e2e): walking-skeleton Playwright happy path + verify-e2e lane (refs #68)
tests/e2e Playwright spec drives DigiD login (jan-burger/test123) → submit →
confirmation against the compose-served portal. run-e2e-check.sh runs it inside the
compose network (node container, browser installed at runtime) so the token issuer
(keycloak:8080) matches the BFF authority (ADR-0010). Wired as verify-e2e (Makefile +
verify chain + a verify-stack CI step).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:08:10 +02:00
notandClaude Opus 4.8 4f311c9b5a ci(portal-self-service): serve the self-service app in compose (refs #68)
Add the self-service nginx service (build the app image, depends_on bff healthy +
keycloak started, health-checked, host port 8140). Add it to WAIT_SVCS and the CI
log dump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:05:10 +02:00
notandClaude Opus 4.8 a55ba1160d feat(portal-self-service): runtime config + nginx serve/proxy image (refs #68)
The app loads /config.json at startup (main.ts) so the OIDC authority is set per
environment from one build; appConfig becomes a factory and derives redirectUrl +
secureApiOrigin from the app origin (same-origin as the BFF). A multi-stage
Dockerfile builds the app and serves it via nginx, reverse-proxying /self-service
+ /openbaar to the bff (relative URLs → no CORS); nginx resolves the BFF at request
time. The compose image bakes config.json with the keycloak:8080 authority so the
browser's token issuer matches the BFF (ADR-0010).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:03:59 +02:00
not 4416d1f4ed Merge pull request 'feat(portal-self-service): NL DS + DigiD self-service submit form (closes #67)' (#71) from feat/67-self-service-form into main
CI / lint (push) Successful in 1m4s
CI / build (push) Successful in 51s
CI / unit (push) Successful in 1m1s
CI / frontend (push) Successful in 1m31s
CI / mutation (push) Successful in 3m56s
CI / verify-stack (push) Successful in 5m29s
Reviewed-on: #71
2026-07-01 11:52:32 +00:00
notandClaude Opus 4.8 074101e836 fix(portal-self-service): run checkAuth() at startup to end the login redirect loop (refs #67)
CI / lint (pull_request) Successful in 1m6s
CI / build (pull_request) Successful in 53s
CI / unit (pull_request) Successful in 1m3s
CI / frontend (pull_request) Successful in 1m56s
CI / mutation (pull_request) Successful in 3m55s
CI / verify-stack (pull_request) Successful in 4m34s
Without an app-init auth check, the DigiD callback (?code=…) was never processed, so
the guard kept seeing 'not authenticated' and re-triggered login — an infinite
redirect loop. Add withAppInitializerAuthCheck() so checkAuth() runs before the router
and guard, establishing the session on the callback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:26:42 +02:00
notandClaude Opus 4.8 5089c2aea6 fix(portal-self-service): re-export the full Utrecht package from libs/ui (refs #67)
CI / frontend (pull_request) Successful in 1m30s
CI / lint (pull_request) Successful in 1m5s
CI / build (pull_request) Successful in 51s
CI / unit (pull_request) Successful in 59s
CI / mutation (pull_request) Successful in 3m54s
CI / verify-stack (pull_request) Has been cancelled
Importing UtrechtComponentsModule pulls every component it exports into the AOT
compiler scope, so all must be resolvable through the ui barrel; a partial
re-export failed a fresh build with NG3004 (masked locally by the Nx build cache,
surfaced by nx serve / a --skip-nx-cache build). Re-export the whole package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:23:17 +02:00
notandClaude Opus 4.8 29f3dcc6cf docs(portal-self-service): record NL DS + DigiD decisions and demo note (refs #67)
CI / lint (pull_request) Successful in 1m6s
CI / build (pull_request) Successful in 50s
CI / unit (pull_request) Successful in 1m0s
CI / verify-stack (pull_request) Successful in 5m46s
CI / frontend (pull_request) Successful in 1m27s
CI / mutation (pull_request) Successful in 3m56s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:13:04 +02:00
notandClaude Opus 4.8 2c196245c2 feat(portal-self-service): implement the DigiD registration submit page (refs #67)
RegistrationPage shows the signed-in BSN and submits to the BFF via the generated
api-client, confirming with the returned reference; built from NL Design System
(Utrecht) components. Wire the guarded route + app providers (DigiD OIDC + token
interceptor + HttpClient), the NL DS theme, and lang=nl. Component tests
(Testing Library) + axe (WCAG 2.1 AA) pass; a guard test covers libs/auth. Replace
the demo eslint depConstraints (scope:shop/shared) with a permissive default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:12:09 +02:00