Files
mrp-qrcode/app/api/v1/purchase-orders/[id]/status/route.ts
T
jason 5847a175af
Build and Push Docker Image / build (push) Successful in 1m11s
stage 5-6
2026-04-21 13:14:27 -05:00

67 lines
2.1 KiB
TypeScript

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<string, string[]> = {
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);
}
}