This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api";
|
||||
import { ReceivePOSchema } from "@/lib/schemas";
|
||||
import { audit } from "@/lib/audit";
|
||||
import { clientIp } from "@/lib/request";
|
||||
|
||||
/**
|
||||
* Record receipt of PO line quantities. Body is an array of
|
||||
* { lineId, qty } — each qty is ADDED to the line's existing receivedQty.
|
||||
* Over-receipt (receivedQty > qty) is rejected so accidentally typing two
|
||||
* zeros doesn't silently corrupt inventory counts.
|
||||
*
|
||||
* Post-update, we auto-advance the PO status:
|
||||
* - every line fully received => "received" (+ receivedAt)
|
||||
* - any received > 0 but not all full => "partial"
|
||||
* - otherwise status left as-is (shouldn't happen given we require >=1 receipt).
|
||||
*/
|
||||
export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const actor = await requireRole("admin");
|
||||
const { id } = await ctx.params;
|
||||
const body = await parseJson(req, ReceivePOSchema);
|
||||
|
||||
const before = await prisma.purchaseOrder.findUnique({
|
||||
where: { id },
|
||||
include: { lines: true },
|
||||
});
|
||||
if (!before) throw new ApiError(404, "not_found", "Purchase order not found");
|
||||
if (before.status === "cancelled" || before.status === "received") {
|
||||
throw new ApiError(409, "po_terminal", `PO is ${before.status}; no more receipts accepted`);
|
||||
}
|
||||
|
||||
const byId = new Map(before.lines.map((l) => [l.id, l]));
|
||||
// Merge duplicate lineIds from the payload so the caller can post partials.
|
||||
const deltas = new Map<string, number>();
|
||||
for (const r of body.receipts) {
|
||||
deltas.set(r.lineId, (deltas.get(r.lineId) ?? 0) + r.qty);
|
||||
}
|
||||
for (const [lineId, qty] of deltas) {
|
||||
const line = byId.get(lineId);
|
||||
if (!line) throw new ApiError(400, "unknown_line", `Line ${lineId} not on this PO`);
|
||||
if (line.receivedQty + qty > line.qty) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
"over_receipt",
|
||||
`Line ${lineId} would receive ${line.receivedQty + qty} > ordered ${line.qty}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
for (const [lineId, qty] of deltas) {
|
||||
await tx.pOLine.update({
|
||||
where: { id: lineId },
|
||||
data: { receivedQty: { increment: qty } },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Re-read to compute the aggregate status.
|
||||
const after = await prisma.purchaseOrder.findUnique({
|
||||
where: { id },
|
||||
include: { lines: true },
|
||||
});
|
||||
if (!after) throw new ApiError(500, "internal", "PO vanished mid-transaction");
|
||||
|
||||
const allFull = after.lines.every((l) => l.receivedQty >= l.qty);
|
||||
const anyReceived = after.lines.some((l) => l.receivedQty > 0);
|
||||
let nextStatus = after.status;
|
||||
const extra: { receivedAt?: Date } = {};
|
||||
if (allFull) {
|
||||
nextStatus = "received";
|
||||
if (!after.receivedAt) extra.receivedAt = new Date();
|
||||
} else if (anyReceived) {
|
||||
nextStatus = "partial";
|
||||
}
|
||||
|
||||
const finalPo =
|
||||
nextStatus !== after.status || extra.receivedAt
|
||||
? await prisma.purchaseOrder.update({
|
||||
where: { id },
|
||||
data: { status: nextStatus, ...extra },
|
||||
include: { lines: true },
|
||||
})
|
||||
: after;
|
||||
|
||||
await audit({
|
||||
actorId: actor.id,
|
||||
action: "receive",
|
||||
entity: "PurchaseOrder",
|
||||
entityId: id,
|
||||
before: { status: before.status },
|
||||
after: { status: finalPo.status, receipts: Array.from(deltas.entries()) },
|
||||
ipAddress: clientIp(req),
|
||||
});
|
||||
|
||||
return ok({ purchaseOrder: finalPo });
|
||||
} catch (err) {
|
||||
return errorResponse(err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user