fix(ST-511): retry on auto-code unique conflict in story and pbi create

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-26 21:24:24 +02:00
parent c62de4c0d0
commit 2663819eef
2 changed files with 59 additions and 37 deletions

View file

@ -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<SessionData>(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 }

View file

@ -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<SessionData>(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 }