feat(registratie): WP-35 — one Concept per case type (server-enforced)
Make "at most one unsubmitted Concept per type" a server invariant instead of a client-only convenience. ApplicationStore.Create → CreateConcept guards atomically under the write gate and POST /applications returns 409 when a duplicate would be created. The FE draft-sync recovers from the 409 by adopting the existing Concept (ensureId → findConcept) rather than erroring — one-per-type means the second attempt lands on the existing draft. Typed client regenerated (documents the 409). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -79,7 +79,7 @@ for its existing violations, so every WP ends green.
|
||||
| [WP-32](WP-32-stamdata-undo.md) | Undo/redo in the stamdata editor | 7 · refinements | done |
|
||||
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
|
||||
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
|
||||
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | todo |
|
||||
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
|
||||
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | todo |
|
||||
|
||||
Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# WP-35 — One Concept per case type (server-enforced)
|
||||
|
||||
Status: done
|
||||
Phase: 7 — refinements
|
||||
|
||||
## Why
|
||||
|
||||
The FE already keeps at most one Concept (draft aanvraag) per type — but only as a client-side
|
||||
convenience in `draft-sync.ts` (`resume()`/`findConcept()`/`resumeGate`). Per ADR-0001 the server
|
||||
is the authority for business rules; the FE guard is best-effort and a cross-tab / stale-list race
|
||||
can still POST a second Concept. This WP makes "at most one unsubmitted Concept per type" a
|
||||
**server-enforced invariant**, and makes the FE recover gracefully when the server refuses.
|
||||
|
||||
## Decisions (made while building — no spec existed; flagged for review)
|
||||
|
||||
- **Enforce at create, not submit.** The invariant is about the _existence_ of Concepts, so the
|
||||
guard lives in `POST /applications`. Enforcing at submit would only block submitting a duplicate,
|
||||
not its existence — that doesn't satisfy the title.
|
||||
- **Race-free in the store.** The check-and-insert happens atomically under the store's single
|
||||
write gate (`ApplicationStore.CreateConcept`), not as a separate list-then-create in the handler.
|
||||
- **409 Conflict** (ProblemDetails), matching the applications block's other guards
|
||||
(cancel-after-submit, submit-twice) — not 422. The generated client now handles 409 explicitly.
|
||||
- **FE recovery over error banner.** A create-409 means a Concept of this type already exists, so
|
||||
`ensureId` adopts it (`findConcept`) instead of surfacing an error — the whole point of
|
||||
one-per-type is that the second attempt lands you on the existing draft. Recovery fires only
|
||||
when one actually exists; otherwise the original failure is surfaced.
|
||||
- **Scope: only the persisted-lifecycle types** (`registratie | herregistratie | intake`). The
|
||||
stateless submits (`telefoonwijziging`, legacy `/registrations` etc.) never create a Concept.
|
||||
|
||||
## Files
|
||||
|
||||
- `backend/.../Data/ApplicationStore.cs` — `Create` → `CreateConcept` (nullable; atomic guard).
|
||||
- `backend/.../Program.cs` — `POST /applications` returns 409 when `CreateConcept` returns null.
|
||||
- `backend/tests/.../ApplicationTests.cs` — helper clears leftover Concepts (tests share one DB);
|
||||
+3 tests (dup conflicts, different type allowed, new allowed after submit).
|
||||
- `src/app/registratie/application/draft-sync.ts` (+spec) — `ensureId` adopts the existing Concept
|
||||
on a create-conflict.
|
||||
- Regenerated `api-client.ts` / `swagger.json` (create now documents its 409).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] A second unsubmitted Concept of the same type is refused server-side (409).
|
||||
- [x] A different type, and a new Concept after the previous is submitted, are allowed.
|
||||
- [x] FE recovers from the 409 by resuming the existing Concept (no error banner).
|
||||
- [x] `npm run ci` green (333 FE tests, backend 125, api-client drift clean after commit).
|
||||
Reference in New Issue
Block a user