'use server' import { revalidatePath } from 'next/cache' import { cookies } from 'next/headers' import { getIronSession } from 'iron-session' import { z } from 'zod' import { prisma } from '@/lib/prisma' import { SessionData, sessionOptions } from '@/lib/session' import { enforceUserRateLimit } from '@/lib/rate-limit' import { ACTIVE_JOB_STATUSES } from '@/lib/job-status' async function getSession() { return getIronSession(await cookies(), sessionOptions) } type ActionResult = | { success: true; data?: T } | { error: string; code?: number } const createSchema = z.object({ ideaId: z.string().cuid(), question: z.string().min(1).max(2000), }) export async function createUserQuestionAction( ideaId: string, question: string, ): Promise> { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd', code: 401 } if (session.isDemo) return { error: 'Demo-gebruikers kunnen geen vragen stellen', code: 403 } const limited = enforceUserRateLimit('create-user-question', session.userId) if (limited) return limited const parsed = createSchema.safeParse({ ideaId, question }) if (!parsed.success) return { error: 'Ongeldige invoer', code: 422 } const idea = await prisma.idea.findFirst({ where: { id: parsed.data.ideaId, user_id: session.userId }, select: { id: true, plan_md: true, product_id: true }, }) if (!idea) return { error: 'Idee niet gevonden', code: 404 } if (!idea.plan_md) return { error: 'Er is nog geen plan voor dit idee', code: 422 } if (!idea.product_id) return { error: 'Koppel eerst een product aan dit idee', code: 422 } // Idempotency: weiger als er al een actieve PLAN_CHAT job loopt voor dit idee. const existing = await prisma.claudeJob.findFirst({ where: { idea_id: parsed.data.ideaId, kind: 'PLAN_CHAT', status: { in: ACTIVE_JOB_STATUSES }, }, select: { id: true }, }) if (existing) return { error: 'Er loopt al een actieve PLAN_CHAT voor dit idee', code: 409 } const [uq, job] = await prisma.$transaction([ prisma.userQuestion.create({ data: { idea_id: parsed.data.ideaId, user_id: session.userId, question: parsed.data.question, }, }), prisma.claudeJob.create({ data: { user_id: session.userId, product_id: idea.product_id, idea_id: parsed.data.ideaId, kind: 'PLAN_CHAT', status: 'QUEUED', }, }), ]) await prisma.ideaLog.create({ data: { idea_id: parsed.data.ideaId, type: 'JOB_EVENT', content: 'PLAN_CHAT queued', metadata: { job_id: job.id, kind: 'PLAN_CHAT', user_question_id: uq.id }, }, }) await prisma.$executeRaw` SELECT pg_notify('scrum4me_changes', ${JSON.stringify({ type: 'claude_job_enqueued', job_id: job.id, idea_id: parsed.data.ideaId, user_id: session.userId, product_id: idea.product_id, kind: 'PLAN_CHAT', status: 'queued', })}::text) ` revalidatePath('/ideas/' + parsed.data.ideaId, 'page') return { success: true, data: { questionId: uq.id, jobId: job.id } } }