pelagia-portal/App/app/(portal)/po/[id]/receipt/actions.ts
Claude (auto-fix) 9adc93e54a fix(receipt): upsert Receipt record on repeat confirmations with notes
Partial-receipt flows call confirmReceipt multiple times. The nested
`create` on the Receipt relation threw a unique-constraint error on the
second call when both confirmations supplied notes, preventing any
delivery from completing and blocking attachment uploads.

Changed to `upsert` so subsequent confirmations update the existing
Receipt row's notes instead of failing.

Adds integration tests covering full receipt, partial receipt, the
upsert scenario (two confirmations each with notes), and permission guards.

Fixes #9
2026-06-19 04:01:26 +05:30

158 lines
5.1 KiB
TypeScript

"use server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { canPerformAction } from "@/lib/po-state-machine";
import { notify } from "@/lib/notifier";
import { revalidatePath } from "next/cache";
export async function confirmReceipt({
poId,
notes,
deliveries,
}: {
poId: string;
notes?: string;
/**
* Per-line delivery quantities for this receipt event.
* Key = line item id, value = quantity delivered now (not cumulative).
* If omitted or empty, all items are treated as fully delivered.
*/
deliveries?: Record<string, number>;
}): Promise<{ ok: true; partial: boolean } | { error: string }> {
const session = await auth();
if (!session?.user) return { error: "Unauthorized" };
const po = await db.purchaseOrder.findUnique({
where: { id: poId },
include: {
submitter: true,
lineItems: true,
vessel: true,
},
});
if (!po) return { error: "PO not found" };
const isAllowedStatus =
po.status === "PAID_DELIVERED" ||
po.status === "PARTIALLY_CLOSED" ||
po.status === "PARTIALLY_PAID";
if (!isAllowedStatus) {
return { error: "You cannot confirm receipt on this PO in its current state." };
}
if (
!canPerformAction(po.status, "confirm_receipt", session.user.role) &&
!canPerformAction(po.status, "confirm_partial_receipt", session.user.role)
) {
return { error: "You do not have permission to confirm receipt on this PO." };
}
if (po.submitterId !== session.user.id && session.user.role !== "SUPERUSER") {
return { error: "You can only confirm receipt on your own purchase orders." };
}
// Reject negative delivery values — only remaining items may be delivered
if (deliveries) {
for (const [id, qty] of Object.entries(deliveries)) {
if (qty < 0) return { error: `Invalid delivery quantity for item ${id}: must be ≥ 0.` };
}
}
// Compute the updated deliveredQuantity for each line item
const lineUpdates = po.lineItems.map((li) => {
const prevDelivered = Number(li.deliveredQuantity ?? 0);
const nowDelivered = deliveries ? (deliveries[li.id] ?? 0) : Number(li.quantity);
const totalDelivered = prevDelivered + nowDelivered;
const ordered = Number(li.quantity);
return {
id: li.id,
productId: li.productId,
quantity: ordered,
deliveredQuantity: Math.min(totalDelivered, ordered),
nowDelivered: Math.min(nowDelivered, ordered - prevDelivered),
};
});
// Determine if all items are now fully delivered
const allDelivered = lineUpdates.every((u) => u.deliveredQuantity >= u.quantity);
// Re-fetch paidAmount for accurate check
const updatedPo = await db.purchaseOrder.findUnique({
where: { id: poId },
select: { paidAmount: true, totalAmount: true },
});
const fullyPaid =
Number(updatedPo?.paidAmount ?? 0) >= Number(updatedPo?.totalAmount ?? 0);
const newStatus: "CLOSED" | "PARTIALLY_CLOSED" | "PARTIALLY_PAID" =
allDelivered && fullyPaid
? "CLOSED"
: !allDelivered && fullyPaid
? "PARTIALLY_CLOSED"
: "PARTIALLY_PAID";
const isPartial = newStatus !== "CLOSED";
// Persist delivery quantities
await Promise.all(
lineUpdates.map((u) =>
db.pOLineItem.update({
where: { id: u.id },
data: { deliveredQuantity: u.deliveredQuantity },
})
)
);
// Update PO status and log action
await db.purchaseOrder.update({
where: { id: poId },
data: {
status: newStatus,
closedAt: newStatus === "CLOSED" ? new Date() : undefined,
receipt: notes
? {
upsert: {
create: { storageKey: "", fileName: "no-file", notes },
update: { notes },
},
}
: undefined,
actions: {
create: {
actionType: isPartial ? "PARTIAL_RECEIPT_CONFIRMED" : "RECEIPT_CONFIRMED",
actorId: session.user.id,
note: notes ?? null,
metadata: isPartial
? {
deliveries: lineUpdates.map((u) => ({
lineItemId: u.id,
deliveredNow: u.nowDelivered,
totalDelivered: u.deliveredQuantity,
ordered: u.quantity,
})),
}
: undefined,
},
},
},
});
// Closing a PO auto-verifies its vendor (proof of a real, completed transaction).
if (newStatus === "CLOSED" && po.vendorId) {
await db.vendor.update({ where: { id: po.vendorId }, data: { isVerified: true } });
revalidatePath("/admin/vendors");
revalidatePath("/inventory/vendors");
}
const managers = await db.user.findMany({ where: { role: "MANAGER", isActive: true } });
if (newStatus === "CLOSED") {
const accounts = await db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } });
await notify({ event: "RECEIPT_CONFIRMED", po, recipients: [...managers, ...accounts] });
} else {
await notify({ event: "PARTIAL_RECEIPT_CONFIRMED", po, recipients: managers });
}
revalidatePath(`/po/${poId}`);
revalidatePath("/dashboard");
revalidatePath("/my-orders");
return { ok: true, partial: isPartial };
}