feat(registratie): WP-35 — one Concept per case type (server-enforced)
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 5m17s
CI / backend (push) Successful in 1m29s
CI / e2e (push) Successful in 3m1s
CI / api-client-drift (push) Successful in 2m4s
CI / semgrep (push) Has been cancelled

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:
eho
2026-07-23 11:07:11 +02:00
co-authored by Claude Opus 4.8
parent 57f6f2f8d8
commit d1abd35b0d
9 changed files with 152 additions and 16 deletions
+21 -10
View File
@@ -63,17 +63,28 @@ export function createDraftSync(deps: DraftSyncDeps) {
const ensureId = async (): Promise<string> => {
await resumeGate;
if (id) return id;
ensuring ??= adapter.create(deps.type).then((newId) => {
id = newId;
// Stamp the id into the URL (no navigation) so a reload resumes this Concept.
void router!.navigate([], {
relativeTo: route!,
queryParams: { aanvraag: newId },
queryParamsHandling: 'merge',
replaceUrl: true,
ensuring ??= adapter
.create(deps.type)
// WP-35: one Concept per type is server-enforced. Within a tab the resumeGate
// already prevents a second create, but a cross-tab/stale race can still hit the
// server's guard (409) — recover by adopting the existing Concept instead of
// erroring. Only recover when one actually exists; otherwise surface the failure.
.catch(async (e) => {
const existing = await findConcept();
if (existing) return existing;
throw e;
})
.then((newId) => {
id = newId;
// Stamp the id into the URL (no navigation) so a reload resumes this Concept.
void router!.navigate([], {
relativeTo: route!,
queryParams: { aanvraag: newId },
queryParamsHandling: 'merge',
replaceUrl: true,
});
return newId;
});
return newId;
});
return ensuring;
};