From 2663819eef1e2d83aae97cff644afe7f8a7ebfe8 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sun, 26 Apr 2026 21:24:24 +0200 Subject: [PATCH] fix(ST-511): retry on auto-code unique conflict in story and pbi create Co-Authored-By: Claude Opus 4.7 (1M context) --- actions/pbis.ts | 45 ++++++++++++++++++++++++---------------- actions/stories.ts | 51 ++++++++++++++++++++++++++++------------------ 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/actions/pbis.ts b/actions/pbis.ts index ec09e07..b0bce8c 100644 --- a/actions/pbis.ts +++ b/actions/pbis.ts @@ -8,7 +8,7 @@ import { prisma } from '@/lib/prisma' import { SessionData, sessionOptions } from '@/lib/session' import { getAccessibleProduct } from '@/lib/product-access' import { isValidCode, MAX_CODE_LENGTH, normalizeCode } from '@/lib/code' -import { generateNextPbiCode } from '@/lib/code-server' +import { createWithCodeRetry, generateNextPbiCode } from '@/lib/code-server' async function getSession() { return getIronSession(await cookies(), sessionOptions) @@ -49,14 +49,12 @@ export async function createPbiAction(_prevState: unknown, formData: FormData) { const product = await getAccessibleProduct(parsed.data.productId, session.userId) if (!product) return { error: 'Product niet gevonden' } - let code = normalizeCode(parsed.data.code) - if (code !== null && !isValidCode(code)) { + const manualCode = normalizeCode(parsed.data.code) + if (manualCode !== null && !isValidCode(manualCode)) { return { error: { code: ['Code mag alleen letters, cijfers, punten, koppeltekens of underscores bevatten'] } } } - if (code === null) { - code = await generateNextPbiCode(parsed.data.productId) - } else { - const dup = await prisma.pbi.findFirst({ where: { product_id: parsed.data.productId, code } }) + if (manualCode) { + const dup = await prisma.pbi.findFirst({ where: { product_id: parsed.data.productId, code: manualCode } }) if (dup) return { error: { code: ['Deze code is al in gebruik binnen dit product'] } } } @@ -66,16 +64,29 @@ export async function createPbiAction(_prevState: unknown, formData: FormData) { }) const sort_order = (last?.sort_order ?? 0) + 1.0 - const pbi = await prisma.pbi.create({ - data: { - product_id: parsed.data.productId, - code, - title: parsed.data.title, - description: parsed.data.description ?? null, - priority: parsed.data.priority, - sort_order, - }, - }) + const insert = (code: string | null) => + prisma.pbi.create({ + data: { + product_id: parsed.data.productId, + code, + title: parsed.data.title, + description: parsed.data.description ?? null, + priority: parsed.data.priority, + sort_order, + }, + }) + + let pbi + try { + pbi = manualCode + ? await insert(manualCode) + : await createWithCodeRetry( + () => generateNextPbiCode(parsed.data.productId), + (code) => insert(code), + ) + } catch { + return { error: { code: ['Kon geen unieke code genereren — probeer opnieuw'] } } + } revalidatePath(`/products/${parsed.data.productId}`) return { success: true, pbi } diff --git a/actions/stories.ts b/actions/stories.ts index 32f1751..cb18a3d 100644 --- a/actions/stories.ts +++ b/actions/stories.ts @@ -9,7 +9,7 @@ import { SessionData, sessionOptions } from '@/lib/session' import { getAccessibleProduct, productAccessFilter } from '@/lib/product-access' import { requireProductWriter } from '@/lib/auth' import { isValidCode, MAX_CODE_LENGTH, normalizeCode } from '@/lib/code' -import { generateNextStoryCode } from '@/lib/code-server' +import { createWithCodeRetry, generateNextStoryCode } from '@/lib/code-server' async function getSession() { return getIronSession(await cookies(), sessionOptions) @@ -68,14 +68,12 @@ export async function createStoryAction(_prevState: unknown, formData: FormData) }) if (!pbi) return { error: 'PBI niet gevonden' } - let code = normalizeCode(parsed.data.code) - if (code !== null && !isValidCode(code)) { + const manualCode = normalizeCode(parsed.data.code) + if (manualCode !== null && !isValidCode(manualCode)) { return { error: { code: ['Code mag alleen letters, cijfers, punten, koppeltekens of underscores bevatten'] } } } - if (code === null) { - code = await generateNextStoryCode(pbi.product_id) - } else { - const dup = await prisma.story.findFirst({ where: { product_id: pbi.product_id, code } }) + if (manualCode) { + const dup = await prisma.story.findFirst({ where: { product_id: pbi.product_id, code: manualCode } }) if (dup) return { error: { code: ['Deze code is al in gebruik binnen dit product'] } } } @@ -85,19 +83,32 @@ export async function createStoryAction(_prevState: unknown, formData: FormData) }) const sort_order = (last?.sort_order ?? 0) + 1.0 - const story = await prisma.story.create({ - data: { - pbi_id: parsed.data.pbiId, - product_id: pbi.product_id, - code, - title: parsed.data.title, - description: parsed.data.description ?? null, - acceptance_criteria: parsed.data.acceptance_criteria ?? null, - priority: parsed.data.priority, - sort_order, - status: 'OPEN', - }, - }) + const insert = (code: string | null) => + prisma.story.create({ + data: { + pbi_id: parsed.data.pbiId, + product_id: pbi.product_id, + code, + title: parsed.data.title, + description: parsed.data.description ?? null, + acceptance_criteria: parsed.data.acceptance_criteria ?? null, + priority: parsed.data.priority, + sort_order, + status: 'OPEN', + }, + }) + + let story + try { + story = manualCode + ? await insert(manualCode) + : await createWithCodeRetry( + () => generateNextStoryCode(pbi.product_id), + (code) => insert(code), + ) + } catch { + return { error: { code: ['Kon geen unieke code genereren — probeer opnieuw'] } } + } revalidatePath(`/products/${pbi.product_id}`) return { success: true, story }