"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; }): 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: { include: { site: 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 ? { create: { storageKey: "", fileName: "no-file", 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, }, }, }, }); // Auto-update inventory for delivered quantities const siteId = (po as typeof po & { siteId?: string | null }).siteId ?? po.vessel?.site?.id ?? null; if (siteId) { for (const u of lineUpdates) { if (!u.productId || u.nowDelivered <= 0) continue; await db.itemInventory.upsert({ where: { productId_siteId: { productId: u.productId, siteId } }, update: { quantity: { increment: u.nowDelivered } }, create: { productId: u.productId, siteId, quantity: u.nowDelivered }, }); } revalidatePath(`/admin/sites/${siteId}`); } 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 }; }