# 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/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

```mermaid
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`):

```ts
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 /applications` → `List<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.
