import { type NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api"; import { UpdatePOStatusSchema } from "@/lib/schemas"; import { audit } from "@/lib/audit"; import { clientIp } from "@/lib/request"; /** * Allowed manual status transitions. We specifically do NOT allow manually * jumping to "partial" or "received" from here — those move automatically * when a receipt is recorded via /receive. Cancelling is possible from any * pre-terminal state. */ const PO_TRANSITIONS: Record = { draft: ["sent", "cancelled"], sent: ["cancelled"], partial: ["cancelled"], received: [], cancelled: [], }; export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { const actor = await requireRole("admin"); const { id } = await ctx.params; const { status: next } = await parseJson(req, UpdatePOStatusSchema); const before = await prisma.purchaseOrder.findUnique({ where: { id } }); if (!before) throw new ApiError(404, "not_found", "Purchase order not found"); const allowed = PO_TRANSITIONS[before.status] ?? []; if (!allowed.includes(next)) { throw new ApiError( 409, "invalid_transition", `Cannot move PO from ${before.status} → ${next}`, ); } const data: { status: string; sentAt?: Date | null; receivedAt?: Date | null; } = { status: next }; if (next === "sent" && !before.sentAt) data.sentAt = new Date(); if (next === "cancelled") { // Keep sent/received timestamps for history; they're part of the audit. } const after = await prisma.purchaseOrder.update({ where: { id }, data }); await audit({ actorId: actor.id, action: "update_status", entity: "PurchaseOrder", entityId: id, before: { status: before.status }, after: { status: after.status }, ipAddress: clientIp(req), }); return ok({ purchaseOrder: after }); } catch (err) { return errorResponse(err); } }