CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Failing after 54s
CI / frontend (pull_request) Successful in 2m38s
CI / storybook-a11y (pull_request) Failing after 3m28s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m9s
CI / e2e (pull_request) Successful in 2m55s
CI / api-client-drift (pull_request) Successful in 2m1s
New GET /beoordeling/{id} shows one aanvraag's status, linked documents, and a
canBesluiten decision flag, gated by the same CanBeoordelen capability as the
werkvoorraad list. Reads through IZaakSource.ListCases rather than a new seam
method (WP-66 needs one anyway for the real write); owner BSN is masked.
Fixes a real gap found while wiring this up: the behandelportal's login was still
WP-61's copied citizen/BSN DigiD flow, so nothing ever sent X-Medewerker and the
werkvoorraad screen (WP-64) always denied in a real browser. A dev-only
medewerkerInterceptor (mirrors the existing ?role= stand-in as ?rollen=) fixes that.
WP-65's own Risks note authorized splitting read from write across sessions given
its size; this is the read half. The decision-recording mutation is next (65b).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
139 lines
7.6 KiB
Markdown
139 lines
7.6 KiB
Markdown
# WP-65 — Behandelportal: zaak detail + beoordeling (decision) screen
|
||
|
||
Status: in progress (65a — detail read — done; 65b — decision write — not started)
|
||
Phase: 11 — Behandelportal
|
||
|
||
## Why
|
||
|
||
The core case-treatment write path — a medewerker opens one aanvraag's detail (including
|
||
its documents) and records a decision (goedkeuren/afwijzen/meer info opvragen), advancing
|
||
the status lifecycle WP-63 published. This is the first genuinely new _write_ capability
|
||
in the system beyond what the citizen SSP already does to itself.
|
||
|
||
## Read first
|
||
|
||
- `mutation-command` skill
|
||
- `form-machine` skill (the decision action is a state-changing form, same idiom as
|
||
everywhere else in this house)
|
||
- [WP-63](WP-63-aanvraag-status-lifecycle.md) (the status field being advanced)
|
||
|
||
## Decisions (pre-made, don't relitigate)
|
||
|
||
- The decision action is modeled as a `*.machine.ts` (Model/Msg/reduce) + a `submit-*`
|
||
command returning `Result`, per this house's one-idiom-for-forms rule — not a
|
||
hand-rolled mutable field.
|
||
- The mutation endpoint is a new BFF-lite write (per `mutation-command` recipe) that
|
||
transitions the status field from WP-63; it validates the transition is legal
|
||
server-side (e.g. can't approve an already-approved case) — the backend remains the
|
||
authority.
|
||
- Runs against `LocalZaakSource` for this slice; wiring the decision into real OpenZaak is
|
||
explicitly WP-66, not bundled here — keeps this slice's surface to app-level behavior
|
||
only.
|
||
|
||
## Files
|
||
|
||
New mutation endpoint + command in `BigRegister.Api`; `behandeling/ui` detail page +
|
||
`behandeling/application` decision machine in the behandelportal app.
|
||
|
||
## Steps
|
||
|
||
1. Backend: mutation endpoint advancing aanvraag status (goedkeuren/afwijzen/meer-info-
|
||
opvragen), validating the transition.
|
||
2. FE: zaak-detail page (documents + current status) + a decision form machine + submit
|
||
command.
|
||
3. Wire the werkvoorraad list (WP-64) to link into this detail page.
|
||
4. Storybook stories + a11y for the new detail/decision UI.
|
||
|
||
## Progress notes (65a — done)
|
||
|
||
This WP's own Risks note offered an explicit escape hatch ("split detail-view (read) from
|
||
decision-recording (write) into two sessions if it feels too big"). Taken: 65a below is done
|
||
and committed; 65b (the mutation + decision machine + form) is the remaining half — see its
|
||
own separate session notes once started. Don't relitigate the split; do relitigate nothing
|
||
else pre-made here.
|
||
|
||
**Blocking gap found and fixed, not in this WP's original scope:** the behandelportal's
|
||
login was still WP-61's copy-pasted citizen/BSN DigiD flow — nothing sent `X-Medewerker`, so
|
||
`StubIdentityProvider` always resolved a zorgverlener, `GET /me` never contained
|
||
`aanvraag:beoordelen`, and WP-64's `WerkvoorraadPage` always rendered its denial alert in a
|
||
real browser. WP-64's own Verification line ("manual: log in as a stub medewerker, see the
|
||
queue populated") could not have passed as written — CI stayed green because none of its
|
||
tests exercise the browser gate. Fixed with a dev-only `medewerkerInterceptor`
|
||
(`apps/behandelportal/src/app/auth/infrastructure/medewerker.interceptor.ts` +
|
||
`medewerker.ts`), mirroring `@shared/infrastructure/role.ts`'s `?role=` idiom but app-local
|
||
(`?rollen=`, sticky per tab, default `behandelaar`) — real employee-SSO login stays out of
|
||
scope (ADR-0002 §3: the two apps' login flows are expected to diverge). Documented in
|
||
`docs/reference/roles-and-access.md`.
|
||
|
||
**Backend (`GET /beoordeling/{id}`):** reads through `IZaakSource.ListCases` (no new seam
|
||
method — one would force an `OpenZaakZaakSource` get-by-id + mapper, which is WP-66's
|
||
surface), filters to the requested id, 404s a Concept or unknown id. Documents come from
|
||
`DocumentStore.ByIds` (new method, mirrors `ByLocalIds`) — `DocumentStore` directly, not
|
||
`IDocumentSource`, since that seam has no read method and its own header comment already
|
||
says `DocumentStore` stays the record of truth for preview/download/audit regardless of
|
||
config. The owner BSN is masked (`MaskTail`, same helper `/brief`'s BIG-nummer masking
|
||
uses) — WP-64's queue row leaks the full BSN via the same `ApplicationSummaryDto.Owner`
|
||
field; flagging as a follow-up, not fixed here or ssp's `/admin/cases` page moves too.
|
||
The gate that was `Werkvoorraad(ctx, action)` is now `Beoordelen(ctx, resource, action)` —
|
||
one gate for every behandelaar endpoint, `resource` feeding the audit row; the one existing
|
||
`/werkvoorraad` call site was updated to pass `"werkvoorraad"`.
|
||
|
||
**Decision-readiness (`BeoordelingDecisionsDto.canBesluiten`) ships now, not deferred to
|
||
65b:** `BeoordelingRules.CanDecide(AanvraagStatusTag)` only inspects the aanvraag's current
|
||
*computed* status tag (`Ingediend`/`InBehandeling`/`MeerInfoGevraagd` → decidable;
|
||
`Goedgekeurd`/`Afgewezen` → not) — no persisted "was a decision recorded" field exists yet,
|
||
so this pure rule needed nothing from 65b's eventual migration to be correct today. 65b adds
|
||
the mutation, the `Besluit` enum, and the transition-legality check that reuses this same
|
||
function.
|
||
|
||
**FE:** `BeoordelingStatus` (`domain/beoordeling.ts`) is the five-tag union (all of ssp's
|
||
`AanvraagStatus` minus `Concept`, which 404s) — wider than WP-64's `WerkvoorraadStatus` (two
|
||
tags). `TYPE_LABELS`/`statusLabel` moved from `werkvoorraad-item-view.ts` into a new
|
||
`domain/beoordeling-view.ts` (the file owning the wider union) and are re-exported from the
|
||
old location so no consumer or existing spec needed to change. Detail page composes
|
||
`<app-data-block>`/`<app-data-row>` (mirrors ssp's `aanvraag-detail.page.ts`) plus one new
|
||
organism, `beoordeling-documenten` (plain links to the existing, pre-existing-unauthenticated
|
||
`/uploads/{id}/content` endpoint — not `ApplicationLinkComponent`, whose `to` is a
|
||
`routerLink`, not an external href). The werkvoorraad list's rows now link to
|
||
`/aanvraag/:id`, gated by the same `aanvraag:beoordelen` capability. No `MaskedValueComponent`
|
||
reveal affordance — this screen never reveals the owner, so pulling in that atom's unused
|
||
reveal machinery would be speculative.
|
||
|
||
**Deferred to 65b, deliberately:** the mutation endpoint, the `Aanvraag.BesluitStatus`
|
||
column + EF migration, `Mappers.ToStatusDto`'s "a recorded decision wins" branch, the
|
||
`besluit.machine.ts` + `besluit-form` UI, and re-running the werkvoorraad/ssp-dashboard
|
||
end-to-end smoke this WP's acceptance criteria actually asks for (a decision advancing
|
||
status, illegal transitions rejected). None of WP-65's acceptance criteria are checked off
|
||
yet — 65a is infrastructure the decision screen needs, not a slice of the AC itself.
|
||
|
||
`npm run ci` green (lint, dep:check ×2, both apps' localized builds, both Storybook builds,
|
||
test, backend test — 197/197 including this WP's 9 new tests). Only the api-client-drift
|
||
step shows the expected pre-commit diff (this WP's own uncommitted endpoint).
|
||
|
||
## Acceptance criteria
|
||
|
||
- [ ] A medewerker can view one aanvraag's detail and record a decision that advances its
|
||
status.
|
||
- [ ] Illegal transitions are rejected server-side (tested).
|
||
- [ ] End-to-end smoke: werkvoorraad → detail → decision → status change reflected back
|
||
in the queue.
|
||
- [ ] `npm run ci` (behandelportal app) + `dotnet test` green.
|
||
|
||
## Verification
|
||
|
||
Manual/automated smoke test of the full werkvoorraad → beoordeling → besluit flow against
|
||
`LocalZaakSource`; `npm run ci`; `cd backend && dotnet test`.
|
||
|
||
## Out of scope
|
||
|
||
Writing the decision to real OpenZaak (WP-66).
|
||
|
||
## Risks
|
||
|
||
This is the largest FE slice in Phase 11 — if it feels too big at kickoff, split
|
||
detail-view (read) from decision-recording (write) into two sessions; the WP as scoped
|
||
already keeps them in one slice because a detail view with no decision action isn't
|
||
independently useful for a caseworker.
|
||
|
||
Depends on: WP-64.
|