Files
atomic-design-poc/docs/prd/0001-mijn-aanvragen-en-wizardstatus.md
Edwin van den Houdt cbb8ae548c
Some checks failed
CI / storybook-a11y (push) Successful in 4m12s
CI / backend (push) Successful in 1m6s
CI / api-client-drift (push) Successful in 1m37s
CI / frontend (push) Successful in 1m31s
CI / codeql (csharp) (push) Failing after 1m51s
CI / codeql (javascript-typescript) (push) Failing after 1m24s
docs(backlog): add WP-18..22 (productie-volwassenheid phase)
Gap analysis found the POC's designed-but-unbuilt strategic gaps: ABAC
authorization (ADR-0002/PRD-0002 phase P1), no e2e coverage, unproven
i18n second-locale seam, thin resilience seams (correlation-id,
idempotency, retry), and in-memory-only persistence. Each WP is grounded
in the current code (file paths + line numbers), not just the analysis.

Also corrects PRD-0001's stale 'Proposed' status header — the Mijn
aanvragen vertical is fully built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 20:07:05 +02:00

15 KiB

PRD 0001 — "Mijn aanvragen": running wizards, application status & document preview

Status: Implemented · 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/Afgewezen aanvraag 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, in src/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

  1. 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).
  2. On re-opening a concept wizard, let the user preview/download the documents they already uploaded.
  3. Show submitted-but-unprocessed applications in a pending state on the dashboard.
  4. 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).
  5. 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 DemoOwner owns 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 fake demo-* 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, mirrors DocumentStore): 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 /applicationsList<ApplicationSummaryDto> for the owner.
    • GET /applications/{id}ApplicationDetailDto (includes draft to 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 → runs SubmissionRules, sets autoApprovable, transitions to In behandeling (or Afgewezen), links documents, returns { referentie, status }.
  • SubmissionRules change: handmatig no longer 422s — it yields autoApprovable = false (manual/pending). Keep genuine validation rejects (0 uren → Afgewezen).
  • DocumentStore change: store byte[] Content + string ContentType; add GET /uploads/{documentId}/content returning bytes (Content-Disposition inline for pdf/image, attachment otherwise). The multipart POST /uploads now 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): httpResource for the list + commands (create/sync/delete/submit), with a hand-written parseApplication* boundary (DTO→domain), per ADR-0001. Upload adapter gains a contentUrl(documentId) helper.
  • ApplicationsStore (application/, providedIn: 'root'): applications RemoteData list rendered via <app-async>; optimistic begin/confirm/rollback around cancel + submit; reload() after mutations — same pattern as BigProfileStore. Dashboard polls/reloads to reflect auto-approval.
  • Wizards: replace sessionStorage persistence 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 carries diplomaHerkomst, which flows into the submit payload to set autoApprovable.
  • Domain: FE AanvraagStatus union + pure blockActions(status) (badge + allowed actions); co-located *.spec.ts.
  • UI (atomic, mostly composition): new aanvraag-block; new "Mijn aanvragen" dashboard section; document-chip preview/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 AanvraagStatus union + blockActions + registratie two-flow wiring + herregistratie persistence.
  • F — Dashboard "Mijn aanvragen" blocks + document-chip preview 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:

  1. docker compose up (Swagger at :5000/swagger) or npm start + backend; log in.
  2. Start each wizard partway → blocks appear at the top of the dashboard (Concept, "Stap X van Y").
  3. Reopen a Concept → previously uploaded documents preview/download (real upload, not ?scenario=upload-*).
  4. Annuleren a Concept → block disappears; start a fresh one.
  5. Registratie with DUO diploma → submit → block shows In behandeling, then Goedgekeurd after the processing window (on dashboard reload).
  6. 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-v3 keys (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.