One-time prettier --write so the new format:check CI gate starts green. .prettierignore excludes generated (api-client.ts, documentation.json), vendored (public/cibg-huisstijl), and backend (dotnet format owns it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 KiB
PRD 0001 — "Mijn aanvragen": running wizards, application status & document preview
Status: Proposed · Date: 2026-07-01 · Context: SSP / Zorgverlener (see ADR-0002)
Cross-references: ADR-0001 (BFF-lite endpoints + decision DTOs) and ADR-0002 (user groups as actors; the
Concept → In behandeling → Goedgekeurd/Afgewezenaanvraag lifecycle). This PRD materializes that lifecycle as a backend-owned aggregate — still entirely within the Zorgverlener self-service context; the Behandelaar/backoffice app that advances manual cases stays a separate, unbuilt context.
1. Problem
A logged-in Zorgverlener cannot see what they have started or submitted, and cannot review what they uploaded. Concretely, today:
- Wizard drafts live in per-wizard
sessionStorage(registratie-v2,intake-v3); the herregistratie wizard has no persistence at all. Nothing enumerates "my in-progress applications." - There is no application list — only a single optimistic boolean,
pendingHerregistratie, insrc/app/registratie/application/big-profile.store.ts:53, surfaced as one dashboard alert. - Uploaded documents cannot be previewed or downloaded. The backend
DocumentStore(backend/src/BigRegister.Api/Data/DocumentStore.cs) deliberately stores metadata only, no bytes, and there is no GET-content endpoint. - A manual diploma is hard-rejected:
SubmissionRules.RejectRegistratie("handmatig")returns a 422 (backend/src/BigRegister.Api/Domain/Submissions/SubmissionRules.cs). The product wants such a submission to succeed and sit in a pending (manual-review) state instead.
2. Goals
- Show all running (concept) applications as dynamic blocks at the top of the dashboard, so the user immediately sees what they initiated, with Verder gaan (resume) and Annuleren (cancel → start fresh).
- On re-opening a concept wizard, let the user preview/download the documents they already uploaded.
- Show submitted-but-unprocessed applications in a pending state on the dashboard.
- Provide two registratie flows that both submit successfully (never disallow submission):
- Auto-approved (
diplomaHerkomst = 'duo') → resolves to Goedgekeurd. - Manual backoffice (
diplomaHerkomst = 'handmatig') → stays In behandeling (pending).
- Auto-approved (
- Make the backend the system of record for applications (concept + submitted), per the chosen architecture.
3. Non-goals / Out of scope (POC)
- The Behandeling/backoffice application that advances manual aanvragen (ADR-0002). Manual cases stay pending forever in this build.
- Real auth / multi-user — a single
DemoOwnerowns everything (matches the faked DigiD session). - Real blob storage, virus scanning, retention — document bytes are held in-memory and reset when the backend restarts.
- Withdrawing a submitted aanvraag ("intrekken") — cancel applies to concepts only.
- Document preview for the dev upload simulation (
?scenario=upload-slow|upload-fail) — that path returns a fakedemo-*documentId with no bytes; preview requires a real upload.
4. Personas
Single actor: the Zorgverlener (healthcare professional, DigiD/BSN, self-service). Per ADR-0002, "who may advance a manual application" is the Behandelaar, an actor in a separate backoffice context that is not part of this app.
5. Domain model — the Aanvraag aggregate (backend-owned)
The backend gains an Aanvraag (application) aggregate — the system of record the dashboard reads.
| Field | Type | Notes |
|---|---|---|
id |
string (uuid) | client-visible handle; used in the resume deep link |
type |
registratie | herregistratie | intake |
which wizard |
status |
discriminated union (below) | computed on read for auto-approval |
draft |
opaque JSON | the wizard's persisted machine snapshot (Concept only) |
stepIndex, stepCount |
int | for the "Stap X van Y" progress on the block |
documentIds |
string[] | documents linked to this aanvraag |
referentie |
string? | set on submit (e.g. BIG-2026-456789) |
owner |
string | DemoOwner |
autoApprovable |
bool | set at submit: diplomaHerkomst === 'duo' (registratie); other types auto |
createdAt / updatedAt / submittedAt? |
timestamps |
Status lifecycle
stateDiagram-v2
[*] --> Concept: create (first wizard step)
Concept --> Concept: draft sync (per step)
Concept --> [*]: Annuleren (cancel, delete)
Concept --> InBehandeling: submit (accepted)
Concept --> Afgewezen: submit invalid (e.g. 0 uren)
InBehandeling --> Goedgekeurd: auto (duo) after processing window Δ
InBehandeling --> InBehandeling: manual (handmatig) — awaits backoffice (not built)
Goedgekeurd --> [*]
Afgewezen --> [*]
FE-mirrored discriminated union (illegal states unrepresentable, same reflex as RemoteData):
type AanvraagStatus =
| { tag: 'Concept'; stepIndex: number; stepCount: number }
| { tag: 'InBehandeling'; referentie: string; manual: boolean } // manual=true → "wordt beoordeeld"
| { tag: 'Goedgekeurd'; referentie: string }
| { tag: 'Afgewezen'; referentie: string; reden: string };
Deterministic auto-approval (no background timer)
Auto-approval is computed on read: for an autoApprovable aanvraag, if
now > submittedAt + PROCESSING_WINDOW (≈8s) the status reports Goedgekeurd, else In behandeling.
Manual (autoApprovable === false) aanvragen never auto-advance. This yields a visible
pending→approved transition for the auto flow and a persistent pending for the manual flow, with no
timers or background jobs — purely a function of stored timestamps.
6. UX
Dashboard — new "Mijn aanvragen" section (top, above "Wat moet ik regelen")
Renders ApplicationsStore.applications() through <app-async> as a list of blocks, sorted:
Concept → In behandeling → resolved. Empty list → the section is hidden.
Per-status block (new aanvraag-block component; which actions/badge a block shows comes from a
pure blockActions(status)):
| Status | Badge | Body | Actions |
|---|---|---|---|
| Concept | grey "Concept" | wizard type + "Stap X van Y" | Verder gaan (resume), Annuleren (cancel) |
| In behandeling | amber "In behandeling" | referentie + ingediend-datum; manual → "wordt handmatig beoordeeld in de backoffice" | view/preview documents |
| Goedgekeurd | green "Goedgekeurd" | referentie | — |
| Afgewezen | red "Afgewezen" | referentie + reden | — |
- Verder gaan deep-links to the wizard with
?aanvraag=<id>; the wizard loads the draft from the backend and seeds its machine. - Annuleren confirms, then
DELETE /applications/{id}(removes the aanvraag and its unlinked documents); the block disappears and the user can start a new one from the existing action cards.
Document preview/download
document-chip (completed uploads) gains a "Voorbeeld / Download" affordance linking to
GET /api/v1/uploads/{documentId}/content — opens inline for application/pdf and images, downloads
otherwise. Available both inside the wizard (Concept) and from an In-behandeling block's document view.
7. Backend design (ASP.NET Core, in-memory — extends existing patterns)
ApplicationStore(new, mirrorsDocumentStore): in-memory dict + lock; create/get/list/ upsert-draft/delete;Submit()transition; status computed on read (auto-approval window).- Endpoints (
Program.cs,/api/v1):GET /applications→List<ApplicationSummaryDto>for the owner.GET /applications/{id}→ApplicationDetailDto(includesdraftto resume).POST /applications→ create Concept (returns id);PUT /applications/{id}→ draft sync (idempotent).DELETE /applications/{id}→ cancel Concept (cascades to unlinked documents).POST /applications/{id}/submit→ runsSubmissionRules, setsautoApprovable, transitions to In behandeling (or Afgewezen), links documents, returns{ referentie, status }.
SubmissionRuleschange:handmatigno longer 422s — it yieldsautoApprovable = false(manual/pending). Keep genuine validation rejects (0 uren → Afgewezen).DocumentStorechange: storebyte[] Content+string ContentType; addGET /uploads/{documentId}/contentreturning bytes (Content-Disposition inline for pdf/image, attachment otherwise). The multipartPOST /uploadsnow captures bytes + content-type.- Tests (
dotnet test): lifecycle (concept→submit→auto/manual/afgewezen), auto-approve-on-read window boundary, content endpoint returns bytes+type, cancel cascades to unlinked docs only.
8. Frontend design (Angular — TEA + atomic + BFF-lite)
- Contracts (
contracts/):ApplicationSummaryDto,ApplicationDetailDto,AanvraagStatusDto. applications.adapter.ts(infrastructure/, the only new network surface):httpResourcefor the list + commands (create/sync/delete/submit), with a hand-writtenparseApplication*boundary (DTO→domain), per ADR-0001. Upload adapter gains acontentUrl(documentId)helper.ApplicationsStore(application/,providedIn: 'root'):applicationsRemoteData list rendered via<app-async>; optimistic begin/confirm/rollback around cancel + submit;reload()after mutations — same pattern asBigProfileStore. Dashboard polls/reloads to reflect auto-approval.- Wizards: replace
sessionStoragepersistence with a debounced backend draft-sync command on each step change; open via?aanvraag=<id>(load detail → seed the machine). Apply the same to the herregistratie wizard (it gains persistence). The registratie machine already carriesdiplomaHerkomst, which flows into the submit payload to setautoApprovable. - Domain: FE
AanvraagStatusunion + pureblockActions(status)(badge + allowed actions); co-located*.spec.ts. - UI (atomic, mostly composition): new
aanvraag-block; new "Mijn aanvragen" dashboard section;document-chippreview/download affordance. Reuse existing atoms (card,button,alert,heading, status-icon/badge). All user-facing copy via$localize; shared/English components stay language-agnostic (props with localizable defaults). Stories for each block status + chip preview (a11y addon on).
9. Delivery phases
Each phase must leave every gate green (npm run lint, npm test, npm run build,
cd backend && dotnet test). Build the registratie vertical slice first; herregistratie/intake
blocks are the same pattern copied.
- A — Backend Aanvraag store + endpoints + lifecycle (+ auto-approve-on-read) + tests.
- B — Backend document bytes +
GET /uploads/{id}/content+ tests. - C — Contracts + adapters (applications parse boundary; upload content URL).
- D —
ApplicationsStore+ wizard backend draft-sync + resume-by-link (retire sessionStorage keys). - E — FE
AanvraagStatusunion +blockActions+ registratie two-flow wiring + herregistratie persistence. - F — Dashboard "Mijn aanvragen" blocks +
document-chippreview affordance + stories.
10. Testing / verification
Unit (co-located): blockActions, AanvraagStatus transitions, parseApplication* boundary; backend
ApplicationStore lifecycle + auto-approve window + content endpoint + cancel cascade. UI via Storybook
stories. End-to-end demo:
docker compose up(Swagger at:5000/swagger) ornpm start+ backend; log in.- Start each wizard partway → blocks appear at the top of the dashboard (Concept, "Stap X van Y").
- Reopen a Concept → previously uploaded documents preview/download (real upload, not
?scenario=upload-*). - Annuleren a Concept → block disappears; start a fresh one.
- Registratie with DUO diploma → submit → block shows In behandeling, then Goedgekeurd after the processing window (on dashboard reload).
- Registratie with handmatig diploma → submit (not blocked) → block stays In behandeling ("wordt handmatig beoordeeld").
11. Risks & notes
- Retiring sessionStorage changes existing wizard behavior; ensure resume-by-link and draft-sync
cover the previous "reload keeps progress" guarantee. Bump/remove the old
registratie-v2/intake-v3keys (no migration — ponytail). - Chatty sync: debounce the per-step
PUT /applications/{id}; keep it optimistic so typing stays snappy. - Dev simulation uploads have no bytes → hide the preview affordance when the documentId is a
demo-*sentinel (or when content 404s). - In-memory reset: backend restart clears all aanvragen/documents — acceptable for the POC; call it out so testers aren't surprised.
ponytail: the only genuinely new pieces are the backend Aanvraag store, document byte storage, and
the aanvraag-block component. Everything else reuses existing store/adapter/<app-async>/atom
patterns. Don't build the backoffice, real storage, or withdrawal — YAGNI until asked.