Files
atomic-design-poc/docs/project/backlog/WP-65-behandelportal-beoordeling.md
T
ehoandClaude Sonnet 5 4133b30e5d
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
feat(behandelportal): WP-65a beoordeling detail (read) + fix unreachable medewerker login
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>
2026-08-03 09:01:09 +02:00

139 lines
7.6 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.
# 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.