"use server"; import { auth } from "@/auth"; import { db } from "@/lib/db"; import { canPerformAction, canCancel } from "@/lib/po-state-machine"; import { hasPermission } from "@/lib/permissions"; import { notify } from "@/lib/notifier"; import { revalidatePath } from "next/cache"; export async function provideVendorId({ poId, vendorId, }: { poId: string; vendorId: string; }): Promise<{ ok: true } | { error: string }> { const session = await auth(); if (!session?.user) return { error: "Unauthorized" }; if (!vendorId) return { error: "Please select a vendor with a verified ID." }; const po = await db.purchaseOrder.findUnique({ where: { id: poId }, include: { submitter: true }, }); if (!po) return { error: "PO not found" }; if (!canPerformAction(po.status, "provide_vendor_id", session.user.role)) { return { error: "You cannot provide a vendor ID for this PO in its current state." }; } const vendor = await db.vendor.findUnique({ where: { id: vendorId } }); if (!vendor?.vendorId) return { error: "The selected vendor does not have a verified ID." }; await db.purchaseOrder.update({ where: { id: poId }, data: { vendorId, status: "MGR_REVIEW", actions: { create: { actionType: "VENDOR_ID_PROVIDED", actorId: session.user.id }, }, }, }); const managers = await db.user.findMany({ where: { role: "MANAGER", isActive: true } }); await notify({ event: "VENDOR_ID_PROVIDED", po, recipients: managers }); revalidatePath(`/po/${poId}`); return { ok: true }; } export async function submitDraftPo( poId: string ): Promise<{ ok: true } | { error: string }> { const session = await auth(); if (!session?.user) return { error: "Unauthorized" }; const po = await db.purchaseOrder.findUnique({ where: { id: poId }, include: { submitter: true }, }); if (!po) return { error: "PO not found" }; if (po.status !== "DRAFT") return { error: "Only draft purchase orders can be submitted." }; if (po.submitterId !== session.user.id && session.user.role !== "SUPERUSER") { return { error: "You can only submit your own purchase orders." }; } await db.purchaseOrder.update({ where: { id: poId }, data: { status: "MGR_REVIEW", submittedAt: new Date(), actions: { create: { actionType: "SUBMITTED", actorId: session.user.id }, }, }, }); const managers = await db.user.findMany({ where: { role: "MANAGER", isActive: true } }); await notify({ event: "PO_SUBMITTED", po, recipients: managers }); revalidatePath(`/po/${poId}`); revalidatePath("/dashboard"); revalidatePath("/my-orders"); return { ok: true }; } export async function discardDraftPo( poId: string ): Promise<{ ok: true } | { error: string }> { const session = await auth(); if (!session?.user) return { error: "Unauthorized" }; const po = await db.purchaseOrder.findUnique({ where: { id: poId }, select: { id: true, status: true, submitterId: true }, }); if (!po) return { error: "PO not found" }; if (po.status !== "DRAFT") return { error: "Only DRAFT purchase orders can be discarded." }; const { role, id: userId } = session.user; const canDiscard = po.submitterId === userId || ["MANAGER", "SUPERUSER"].includes(role); if (!canDiscard) return { error: "You do not have permission to discard this PO." }; // POAction has no cascade — delete child records before the PO await db.$transaction([ db.pOAction.deleteMany({ where: { poId } }), db.notification.deleteMany({ where: { poId } }), db.purchaseOrder.delete({ where: { id: poId } }), ]); revalidatePath("/my-orders"); revalidatePath("/dashboard"); return { ok: true }; } // ── Cancel a PO ─────────────────────────────────────────────────────────────── // MANAGER / SUPERUSER only, from any state, with a mandatory reason. A cancelled // PO drops out of every spend tracker (those filter on POST_APPROVAL_STATUSES / // explicit whitelists, none of which include CANCELLED). export async function cancelPo({ poId, reason, }: { poId: string; reason: string; }): Promise<{ ok: true } | { error: string }> { const session = await auth(); if (!session?.user) return { error: "Unauthorized" }; if (!hasPermission(session.user.role, "cancel_po")) { return { error: "You do not have permission to cancel purchase orders." }; } const trimmed = (reason ?? "").trim(); if (!trimmed) return { error: "A cancellation reason is required." }; const po = await db.purchaseOrder.findUnique({ where: { id: poId }, include: { submitter: true }, }); if (!po) return { error: "PO not found" }; if (!canCancel(po.status, session.user.role)) { return { error: po.status === "CANCELLED" ? "This purchase order is already cancelled." : "You cannot cancel this purchase order.", }; } await db.purchaseOrder.update({ where: { id: poId }, data: { status: "CANCELLED", cancelledAt: new Date(), cancellationReason: trimmed, actions: { create: { actionType: "CANCELLED", actorId: session.user.id, note: trimmed } }, }, }); // Notify the submitter and Accounts (they track spend). const accounts = await db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } }); const recipients = [po.submitter, ...accounts].filter( (u, i, arr) => arr.findIndex((x) => x.id === u.id) === i ); await notify({ event: "PO_CANCELLED", po, recipients, note: trimmed }); revalidatePath(`/po/${poId}`); revalidatePath("/dashboard"); revalidatePath("/history"); revalidatePath("/my-orders"); revalidatePath("/payments"); return { ok: true }; } // ── Supersede a cancelled PO with an existing replacement PO ──────────────────── // Links a cancelled PO to the existing PO that replaces it (by PO number). No // vessel/account/vendor match is enforced. The reciprocal "supersedes" link is // surfaced on the replacement via the schema self-relation. export async function supersedePo({ poId, replacementPoNumber, }: { poId: string; replacementPoNumber: string; }): Promise<{ ok: true } | { error: string }> { const session = await auth(); if (!session?.user) return { error: "Unauthorized" }; if (!hasPermission(session.user.role, "cancel_po")) { return { error: "You do not have permission to link a superseding purchase order." }; } const num = (replacementPoNumber ?? "").trim(); if (!num) return { error: "Enter the PO number that supersedes this one." }; const po = await db.purchaseOrder.findUnique({ where: { id: poId }, select: { id: true, status: true }, }); if (!po) return { error: "PO not found" }; if (po.status !== "CANCELLED") { return { error: "Only a cancelled purchase order can be superseded." }; } const replacement = await db.purchaseOrder.findUnique({ where: { poNumber: num }, select: { id: true, poNumber: true }, }); if (!replacement) return { error: `No purchase order found with number "${num}".` }; if (replacement.id === po.id) return { error: "A purchase order cannot supersede itself." }; await db.purchaseOrder.update({ where: { id: poId }, data: { supersededById: replacement.id, actions: { create: { actionType: "SUPERSEDED", actorId: session.user.id, note: `Superseded by ${replacement.poNumber}`, }, }, }, }); revalidatePath(`/po/${poId}`); revalidatePath(`/po/${replacement.id}`); return { ok: true }; }