Files
jason 5847a175af
Build and Push Docker Image / build (push) Successful in 1m11s
stage 5-6
2026-04-21 13:14:27 -05:00

103 lines
3.6 KiB
TypeScript

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);
}
}