import { type NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; import { ok, errorResponse, requireRole, ApiError } from "@/lib/api"; import { audit } from "@/lib/audit"; import { clientIp } from "@/lib/request"; /** * Claim an operation for the current operator. We enforce the "single claim" * invariant at the DB level: updateMany's where clause includes * { claimedByUserId: null, status: "pending" }, so a second operator racing * us will match 0 rows and we reject with 409. No transaction needed. * * On success we also open a TimeLog row so "hours on machine" telemetry * later lines up with actual floor time. */ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { const actor = await requireRole("operator"); const { id } = await ctx.params; const existing = await prisma.operation.findUnique({ where: { id }, select: { id: true, status: true, claimedByUserId: true, name: true }, }); if (!existing) throw new ApiError(404, "not_found", "Operation not found"); if (existing.status === "completed") { throw new ApiError(409, "op_completed", "This step is already completed"); } if (existing.claimedByUserId && existing.claimedByUserId !== actor.id) { throw new ApiError(409, "op_claimed", "Another operator is already working on this step"); } if (existing.claimedByUserId === actor.id) { // Idempotent: scanning again while already holding the claim is a no-op. const op = await prisma.operation.findUnique({ where: { id } }); return ok({ operation: op, alreadyClaimed: true }); } const now = new Date(); const updateResult = await prisma.operation.updateMany({ where: { id, claimedByUserId: null, status: "pending" }, data: { status: "in_progress", claimedByUserId: actor.id, claimedAt: now, }, }); if (updateResult.count === 0) { // Lost a race to another operator between the check above and the update. throw new ApiError(409, "op_claimed", "Another operator just claimed this step"); } await prisma.timeLog.create({ data: { operationId: id, operatorId: actor.id, startedAt: now }, }); const op = await prisma.operation.findUnique({ where: { id } }); await audit({ actorId: actor.id, action: "claim_op", entity: "Operation", entityId: id, after: { status: "in_progress", claimedByUserId: actor.id }, ipAddress: clientIp(req), }); return ok({ operation: op }); } catch (err) { return errorResponse(err); } }