Files
register-referentie/BACKLOG.md
T
not 4777ff2b1d
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
feat(workflow): document-wait task + 30-day timeout cancellation (S-10a, closes #102) (#105)
## 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

328 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BACKLOG.md — iteration plan (curated mirror of the active Gitea milestone)
> **Source of truth: Gitea Issues + Milestones.** This file is a human-readable mirror so contributors and Claude Code can see the iteration plan at a glance. When the two disagree, **Gitea wins** and this file is regenerated.
>
> Regenerate this file when a milestone is opened, closed, or significantly reshaped. A commit message of `docs(backlog): sync with milestone <name>` is appropriate.
Each slice has a unique ID (`S-NN`) matching the title of its Gitea issue. Work is pulled from the **active milestone**'s project board in Gitea, not from this file.
Each slice is **independently demoable** and meets the Definition of Done in `CLAUDE.md` §3. Pull only one slice at a time.
---
## Iteration 0 — Foundations *(milestone: `Iteration 0 — Foundations`)*
> **S-00 was split** (CLAUDE.md §13) into the sub-slices below. The original
> outcome — a fresh clone + `docker compose up` reaching a green BFF health
> endpoint, with CI green and the contributor scaffolding in place — is the sum
> of S-00-a…e. The Gitea milestones, labels, and slice issues already exist —
> they are managed directly with the `tea` CLI.
### S-00-a · Placeholder BFF + health endpoint
**Outcome:** A minimal .NET BFF exposing `GET /health` returning green; runnable with `dotnet run`. TDD anchor for the slice.
**Touches:** `services/bff/`, tests. **Out of scope:** Docker, CI, OIDC.
### S-00-b · Dockerfile + compose skeleton + compose-up smoke
**Outcome:** BFF containerized; `infra/docker-compose.yml` brings it up; health green within 3 minutes from a fresh clone.
**Touches:** `services/bff/Dockerfile`, `infra/docker-compose.yml`, smoke script. **Out of scope:** other services, CI.
### S-00-c · Gitea Actions CI (lint, build, unit, compose-up smoke)
**Outcome:** `.gitea/workflows/ci.yaml` green on PRs and `main`: lint, build, unit, compose-up smoke. Actions pinned by absolute URL (§8.7/§15).
**Touches:** `.gitea/workflows/ci.yaml`, `docs/runbooks/ci.md`. **Out of scope:** mutation, e2e, container push.
### S-00-d · Contributor workflow: issue/PR templates, git-cliff, CHANGELOG
**Outcome:** Issue templates (`slice`/`bug`/`adr-proposal`), PR template, `git-cliff` → empty `CHANGELOG.md`, `docs/gitea-workflow.md`.
**Touches:** `.gitea/ISSUE_TEMPLATE/`, `.gitea/PULL_REQUEST_TEMPLATE.md`, `cliff.toml`, `CHANGELOG.md`, `docs/gitea-workflow.md`. **Out of scope:** app code.
### S-00-e · Docs scaffold: MkDocs + ADR-0001 + README quickstart
**Outcome:** MkDocs builds the `docs/` site; `docs/architecture/adr-0001-loose-coupling.md` exists; README has a sub-10-minute quickstart.
**Touches:** `mkdocs.yml`, `docs/` nav, `docs/architecture/adr-0001-loose-coupling.md`, `README.md`. **Out of scope:** Gitea Pages publish.
---
## Iteration 1 — Walking skeleton *(milestone: `Iteration 1 — Walking Skeleton`)*
The skeleton proves the spine end-to-end: a registration, a workflow, a zaak in OpenZaak, an event back, a public projection. Minimum viable but real.
### S-01 · OpenZaak + Open Notificaties + Postgres come up in compose
**Outcome:** Local `docker compose up` brings up OpenZaak, Open Notificaties, their dependencies, and a seeded ZTC catalogus called `BIG`. A health check confirms all reachable.
**Acceptance:**
- `curl` to OpenZaak `/zaken/api/v1/` returns 401 (auth working).
- A test client with a generated JWT can list zaaktypen in the `BIG` catalogus.
- The seeded catalogus contains one lean `BIG-registratie` zaaktype with only the schema-mandatory fields plus `bsn` as an eigenschap.
**Touches:** `infra/openzaak/`, `infra/opennotificaties/`, `infra/seed/`, ADR for catalogus design.
**Out of scope:** any portal, BFF, Flowable, ACL code.
### S-02 · Keycloak with mock DigiD, eHerkenning, eIDAS, medewerker realms
**Outcome:** Keycloak runs locally with four realms pre-seeded. Each realm has 12 test users with known credentials documented in `docs/synthetic-data.md`.
**Acceptance:**
- Browser-based OIDC login flow works for each realm against a placeholder client.
- Mock DigiD realm returns a BSN claim; eHerkenning returns a KvK; eIDAS returns a foreign identifier; medewerker returns role claims.
**Touches:** `infra/keycloak/`, seed scripts.
**Out of scope:** real federation, MFA.
### S-03 · Flowable up with a minimal BPMN: "Registratie ontvangen"
**Outcome:** Flowable runs locally with Postgres. A single BPMN model (`registratie.bpmn`) deployed with one start event, one external task `OpenZaakAanmaken`, one end event.
**Acceptance:**
- BPMN model deployed via Flowable's REST API on container start.
- An HTTP call can start a process instance and observe it waiting on the external task.
**Touches:** `infra/flowable/`, `workflows/registratie.bpmn`.
**Out of scope:** DMN, boundary timers, second model.
### S-04 · ACL skeleton with one operation: open a zaak
**Outcome:** A .NET library + service that exposes one method: `OpenZaak(domainPayload) → zaakUrl`. It default-fills `bronorganisatie`, `verantwoordelijkeOrganisatie`, `startdatum`, `vertrouwelijkheidaanduiding`, and posts to OpenZaak. **Strict TDD throughout.**
**Acceptance:**
- BDD scenario: "Given a domain registration payload, when I call the ACL, then a zaak exists in OpenZaak with the default-filled fields."
- Mutation score baseline captured and enforced by the Gitea Actions pipeline.
- Integration test using Testcontainers against real OpenZaak passes.
**Touches:** `services/acl/`, tests, ADR for default-fill strategy.
**Out of scope:** all other ZGW operations, status transitions, documents.
### S-05 · BIG Domain Service skeleton with the Registration aggregate
**Outcome:** A .NET service exposing a single endpoint `POST /registrations`. The Registration aggregate has a state machine with at minimum `INGEDIEND`. The service orchestrates: start a Flowable process → external task callback executes the ACL `OpenZaak` → zaak URL stored on the aggregate.
**Acceptance:**
- BDD scenario: "Given a zorgprofessional submits a registration, when the domain service receives it, then a Flowable process is started and a zaak is opened in OpenZaak."
- Integration test exercises the full path (no real frontend yet).
- The Workflow Client is the only code that calls Flowable.
**Touches:** `services/domain/`, `services/acl/` (consumed), tests, ADR for external-task job-worker pattern.
**Out of scope:** any other use case, documents, decisions.
### S-06 · Event Subscriber + Read Projection (minimal)
**Outcome:** An NRC webhook consumer that, on `zaak.gecreeerd`, writes a row to a `register_projection` table with `id`, `bsn`, `naam_placeholder`, `status`. Idempotent. Rebuildable.
**Acceptance:**
- BDD scenario: "Given a zaak is created in OpenZaak, when the NRC event is delivered, then the projection contains a row with status INGEDIEND."
- Replaying the same event twice does not create duplicates.
- A `projection rebuild` admin command repopulates from OpenZaak.
**Touches:** `services/event-subscriber/`, `services/projection-api/`, tests.
**Out of scope:** decision events, multiple projections, public-safe field filtering (will tighten in S-09).
### S-07 · BFF with one endpoint per portal + OIDC validation
**Outcome:** A .NET BFF exposing four endpoint groups (one per portal). Validates tokens issued by Keycloak. Implements the minimum needed for the walking skeleton: `POST /self-service/registrations`, `GET /openbaar/register?q=...`.
**Acceptance:**
- BDD scenarios cover the two endpoints with valid and invalid tokens.
- OpenAPI spec generated and committed.
**Touches:** `services/bff/`, OpenAPI spec, tests.
**Out of scope:** behandelaar and beheer endpoints (later slices).
### S-08 · Self-Service portal (Angular, NL DS) — submit a registration
> **S-08 was split** (CLAUDE.md §13; issue #9 closed) into the sub-slices below — it bundled the
> Nx bootstrap, the generated client, the NL DS + DigiD form, and a full-stack Playwright e2e, well
> past 12 days. Each sub-slice is independently demoable and CI-green.
- **S-08a (#65)** · Nx monorepo + Angular tooling + CI Node lane. Placeholder `self-service` app; `nx lint/test/build` green in a new CI Node lane.
- **S-08b (#66)** · Generated api-client lib from `services/bff/openapi.json` (never hand-written, §10) + a mocked-BFF unit test.
- **S-08c (#67)** · Self-service submit form — NL Design System `libs/ui`, DigiD OIDC `libs/auth`, component tests (Angular Testing Library), axe WCAG 2.1 AA on the submit page.
- **S-08d (#68)** · Playwright happy-path e2e (login → submit → success) against the full stack + compose serving + CI e2e lane.
**Out of scope (whole of S-08):** document upload, status tracking page.
### S-09 · Openbaar Register portal — public lookup *(#10)*
**Outcome:** The openbaar Angular app shows a search box. Anonymous. Queries the BFF's `/openbaar/register` which reads only the projection's **public-safe** fields. Shows the public-visibility half of the walking skeleton.
_Split from the original S-09 — scoped to the portal only; the approval flow is **S-09b (#75)**._
**Acceptance:**
- E2E test: after a zorgprofessional registers via self-service (S-08), the openbaar register shows the entry (as `INGEDIEND`).
- Public-safe field whitelist enforced and tested (already in the BFF; add a portal component test + a11y check).
**Touches:** `apps/openbaar/`, compose serving, e2e, docs.
**Out of scope:** approval/status transition (S-09b), advanced search filters, sorting.
### S-09b · Approval flow — temp admin endpoint + status transition to projection *(#75)*
**Outcome:** A behandelaar approves a submitted registration via a temporary admin endpoint (no behandel-portal yet — S-12). The approval transitions the zaak status through the ACL → NRC → event-subscriber → projection, and the openbaar register then shows the entry as approved.
**Acceptance:**
- A new terminal/approved status (e.g. `INGESCHREVEN`) exists and is projected.
- Temporary admin approve endpoint transitions a registration via a real ZGW status set (behind the ACL, §8).
- E2E: register (S-08) → approve → openbaar shows the entry as approved.
**Touches:** `services/domain`, `services/acl`, `services/event-subscriber`, `services/projection-api`, e2e.
**Out of scope:** behandel-portal UI (S-12), assessment logic (S-13), escalation (S-15).
**End of walking skeleton** (S-09 + S-09b). Demo: submit → process → projection → public visibility. All CI gates green on Gitea Actions. Cut release `vYYYY.MM.0` and publish via Gitea Releases.
---
## Iteration 2 — Flow completeness and exception handling *(milestone: `Iteration 2 — Flow Completeness`)*
### S-10 · Document upload + boundary timer for document timeout (Flow 2)
Split (issue #11 closed) into two independently-demoable slices per §13 — the original spanned six net-new surfaces including a new ZGW boundary:
#### S-10a · Document-wait task + 30-day timeout cancellation + provision trigger — #102
**Outcome:** BPMN gains a `WachtOpDocumenten` user task with a 30-day (P30D) interrupting boundary timer. On timeout the case is cancelled — the timer runs to a dedicated cancel end-event and the domain aggregate moves to a new terminal status `Verlopen` via an external-worker (mirrors S-14 escalation / S-11 withdrawal). "Documents received" is wired end-to-end (domain endpoint + BFF + a "Documenten aanleveren" button on the self-service page) so the walking-skeleton e2e stays green — but the document is **not yet stored** in ZGW; that is S-10b.
**Acceptance:** BDD both branches (documents-in-time vs timeout-cancel); live timer-fire via the management-API "move" idiom; the registration e2e provides documents before the behandelaar step.
#### S-10b · Real diploma upload stored via the ACL Documenten API — #103
**Outcome:** the self-service "Documenten aanleveren" action becomes a real file upload; the document is stored in the ZGW Documenten (DRC) API and related to the zaak, with all document calls routed through the ACL (§8.1), and the zaak is set to a cancellation status on timeout expiry. Builds on the S-10a trigger/wait. Depends on #102.
**Acceptance:** ACL Documenten gateway integration test; Playwright e2e uploads a real document; the openbaar/zaak reflects the stored document.
### S-11 · Withdrawal (Flow 3)
**Outcome:** Self-service portal has a "trek aanvraag in" action. Domain service issues a withdraw command; BPMN message event correlates; case cancels with audit trail.
### S-12 · Behandel-portal — werkbak + beoordeling
**Outcome:** Behandel portal with login (medewerker realm), werkbak listing INGEDIEND/IN_BEHANDELING cases, claim and complete user tasks via Flowable, decision endpoint via Domain Service.
**Acceptance:** BDD scenarios for claim, complete, request additional document, decide.
### S-13 · DMN decision: diploma eligibility (Flow 4)
**Outcome:** A DMN decision table evaluated by the Domain Service via Workflow Client. Foreign diplomas route to an extra "CBGV-advies" user task in BPMN.
**Acceptance:** BDD scenarios for domestic and foreign diploma paths; DMN evaluated separately is unit-tested.
### S-14 · Beoordeling escalation (Flow 5)
**Outcome:** Boundary timer on beoordeling user task — 14 days. On timeout, reassigns to a teamlead role.
---
## Iteration 3 — Maintenance portal and observability *(milestone: `Iteration 3 — Beheer & Observability`)*
### S-15 · Beheer-portal — catalogus & default-fill rules
**Outcome:** Beheer portal lets an admin view ZTC catalogi (read-only first), and manage the ACL's default-fill configuration via a CRUD UI. MFA on the medewerker realm enforced.
### S-16 · OpenTelemetry traces + Grafana dashboard
**Outcome:** Traces span portal → BFF → Domain → ACL → OpenZaak and portal → BFF → Domain → Flowable. Grafana dashboards pre-built for golden signals.
### S-17 · Quartz.NET scheduler — herregistratie reminder sweep
**Outcome:** Nightly job that finds entries within 90 days of expiry and emits a domain event. (No outbound notification in v1 — logged.)
---
## Iteration 4 — Objecten and the authoritative register *(milestone: `Iteration 4 — Objecten`)*
### S-18 · Objecten + Objecttypen up in compose; Register objecttype defined
**Outcome:** Objecten and Objecttypen running. A `RegisterRecord` objecttype defined with the public-safe schema.
### S-19 · ACL extension: write register-record to Objecten on approval
**Outcome:** Approval path writes the canonical register record to Objecten, not OpenZaak eigenschappen. Projection now sourced from Objecten events.
**ADR required:** "Why Objecten holds the register, OpenZaak holds the process."
---
## Iteration 5 — Data governance module *(milestone: `Iteration 5 — Data Governance`)*
### S-20 · OpenMetadata module + seed bundle deployed alongside
**Outcome:** OpenMetadata stack runs as a separate compose file (`infra/governance/`). Seed bundle loaded: glossary, classification taxonomy, roles, default DQ tests.
### S-21 · Read-replica ingestion + API ingestion
**Outcome:** Postgres read replicas of domain, Flowable, projection. OpenMetadata ingestion connectors discover schemas. API connector ingests OpenZaak and Objecten via OpenAPI.
### S-22 · Lineage SDK (.NET) + lineage assertions across the personal-data path
**Outcome:** A thin .NET package wrapping OpenMetadata's lineage API, published to the **Gitea Packages** registry. ACL, Event Subscriber, and Domain Service call it as personal data flows. Each lineage edge carries purpose and legal basis.
**ADR required:** "Lineage as a property of code, not docs."
### S-23 · GDPR reporting cookbook
**Outcome:** `docs/gdpr-reporting.md` showing how to answer specific AVG questions using OpenMetadata (data subject request, processing register, lineage trace).
---
## Iteration 6 — Production aspirations (sketch only) *(milestone: `Iteration 6 — Production Posture`)*
### S-24 · Helm chart (sketch) + Kubernetes manifests for the platform
**Outcome:** A non-deployed-but-reviewable Helm chart and accompanying ADR on production posture. Documents HA, secrets, backup, observability, identity wiring.
### S-25 · Runbook completeness review
**Outcome:** All runbooks complete: startup, seed, common failures, upgrade upstream modules, restore from backup, rotate secrets, Gitea Actions gotchas.
---
## How to add a new slice
1. **Open a Gitea issue** using the `slice.md` template. Title format: `S-NN · One-line outcome`.
2. Assign it to the appropriate milestone and apply `type:slice` plus area labels.
3. Fill in: outcome (one sentence, user-visible if possible), acceptance (Gherkin or testable assertions), touches (services and folders), out of scope (explicit).
4. Add it to the milestone's project board in the `Todo` column.
5. Mirror it into this file under the relevant iteration heading.
6. Open a PR against `BACKLOG.md` for review **before** starting the work.
## How to split a slice
If a slice issue grows beyond ~12 days of focused work:
1. Identify the natural seam (often between backend and frontend, or between happy path and exception path).
2. **In Gitea,** close the original issue with a comment listing the replacement issues. Use `S-NN-a`, `S-NN-b`, etc., or just allocate fresh `S-NN` numbers.
3. Each split must still meet the Definition of Done independently.
4. Update `BACKLOG.md` in a `docs(backlog): split S-NN (refs #NN, closes #NN)` commit.
## Cross-references
- **System of record:** Gitea Issues (this repo's Issues tab).
- **Active board:** the Gitea project board on the current milestone.
- **Working agreements:** `CLAUDE.md`.
- **What we're building and why:** `PRD.md`.
- **Architecture diagrams + ADRs:** `docs/architecture/`.
- **Daily-changing operational notes:** Gitea Wiki.