import { Resend } from "resend"; import { db } from "@/lib/db"; import type { PurchaseOrder, User } from "@prisma/client"; const isDev = process.env.NODE_ENV === "development"; const resend = isDev ? null : new Resend(process.env.RESEND_API_KEY); const FROM = `${process.env.EMAIL_FROM_NAME ?? "PPMS"} <${process.env.EMAIL_FROM ?? "noreply@ppms.pelagiamarine.com"}>`; export type NotificationEvent = | "PO_SUBMITTED" | "PO_APPROVED" | "PO_APPROVED_WITH_NOTE" | "PO_REJECTED" | "EDITS_REQUESTED" | "VENDOR_ID_REQUESTED" | "VENDOR_ID_PROVIDED" | "PAYMENT_PROCESSING" | "PAYMENT_SENT" | "RECEIPT_CONFIRMED"; interface NotifyParams { event: NotificationEvent; po: PurchaseOrder & { submitter: User }; recipients: User[]; note?: string; } export async function notify({ event, po, recipients, note }: NotifyParams) { const subject = buildSubject(event, po.poNumber); if (!subject) return; await Promise.allSettled( recipients.map(async (recipient) => { const body = buildInAppBody(event, po, recipient); const link = buildInAppLink(event, po, recipient); let status = "sent"; if (isDev) { console.log( `\nšŸ“§ [DEV EMAIL] To: ${recipient.email}\n Subject: ${subject}\n Body: ${buildEmailBody(event, po, note)}\n` ); } else { try { const { error } = await resend!.emails.send({ from: FROM, to: recipient.email, subject, html: buildHtml(event, po, recipient, note), }); if (error) status = "failed"; } catch { status = "failed"; } } await db.notification.create({ data: { subject, body, link, status, poId: po.id, userId: recipient.id }, }); }) ); } // ── In-app message ──────────────────────────────────────────────────────────── function buildInAppBody( event: NotificationEvent, po: PurchaseOrder & { submitter: User }, recipient: User ): string { const pn = po.poNumber; const submitter = po.submitter.name; switch (event) { case "PO_SUBMITTED": return recipient.id === po.submitterId ? `${pn} submitted for review` : `${submitter} submitted ${pn} for your review`; case "PO_APPROVED": case "PO_APPROVED_WITH_NOTE": return recipient.id === po.submitterId ? `${pn} approved` : `${pn} approved — ready for payment`; case "PO_REJECTED": return `${pn} rejected`; case "EDITS_REQUESTED": return `Edits requested on ${pn}`; case "VENDOR_ID_REQUESTED": return `Vendor ID needed before ${pn} can be approved`; case "VENDOR_ID_PROVIDED": return `Vendor ID provided for ${pn} — ready to review`; case "PAYMENT_PROCESSING": return `Payment processing for ${pn}`; case "PAYMENT_SENT": return `Payment confirmed for ${pn} — please confirm receipt`; case "RECEIPT_CONFIRMED": return `Receipt confirmed — ${pn} closed`; default: return `Update on ${pn}`; } } function buildInAppLink( event: NotificationEvent, po: PurchaseOrder & { submitter: User }, recipient: User ): string { const isManager = recipient.role === "MANAGER" || recipient.role === "SUPERUSER"; const isAccounts = recipient.role === "ACCOUNTS"; const isSubmitter = recipient.id === po.submitterId; switch (event) { // Manager needs to act on the approval queue case "PO_SUBMITTED": case "VENDOR_ID_PROVIDED": return isManager ? `/approvals/${po.id}` : `/po/${po.id}`; // Accounts needs to process payment; everyone else sees the PO case "PO_APPROVED": case "PO_APPROVED_WITH_NOTE": return isAccounts ? `/payments` : `/po/${po.id}`; // Submitter needs to open the edit form case "EDITS_REQUESTED": return isSubmitter ? `/po/${po.id}/edit` : `/po/${po.id}`; // Submitter needs to confirm receipt case "PAYMENT_SENT": return isSubmitter ? `/po/${po.id}/receipt` : `/po/${po.id}`; default: return `/po/${po.id}`; } } // ── Email subject ───────────────────────────────────────────────────────────── function buildSubject(event: NotificationEvent, poNumber: string): string | null { const base = `PO ${poNumber}`; const map: Record = { PO_SUBMITTED: `${base} submitted for review`, PO_APPROVED: `${base} has been approved`, PO_APPROVED_WITH_NOTE: `${base} has been approved`, PO_REJECTED: `${base} has been rejected`, EDITS_REQUESTED: `Edits requested on ${base}`, VENDOR_ID_REQUESTED: `Vendor ID needed for ${base}`, VENDOR_ID_PROVIDED: `Vendor ID provided for ${base}`, PAYMENT_PROCESSING: `Payment initiated for ${base}`, PAYMENT_SENT: `Payment confirmed for ${base}`, RECEIPT_CONFIRMED: `Receipt confirmed — ${base} closed`, }; return map[event] ?? null; } // ── Email HTML ──────────────────────────────────────────────────────────────── function buildHtml( event: NotificationEvent, po: PurchaseOrder & { submitter: User }, recipient: User, note?: string ): string { return `
PPMS

Hi ${recipient.name},

${buildEmailBody(event, po, note)}

PO Number: ${po.poNumber}
Title: ${po.title}
Submitted by: ${po.submitter.name}

You received this message because you are a stakeholder on this purchase order.

`; } function buildEmailBody( event: NotificationEvent, po: PurchaseOrder & { submitter: User }, note?: string ): string { const noteHtml = note ? `

"${note}"` : ""; switch (event) { case "PO_SUBMITTED": return `${po.submitter.name} has submitted purchase order ${po.poNumber} for your review.`; case "PO_APPROVED": case "PO_APPROVED_WITH_NOTE": return `Your purchase order ${po.poNumber} has been approved.${noteHtml}`; case "PO_REJECTED": return `Your purchase order ${po.poNumber} has been rejected.${noteHtml}`; case "EDITS_REQUESTED": return `Edits have been requested on ${po.poNumber}. Please update the order and resubmit.${noteHtml}`; case "VENDOR_ID_REQUESTED": return `A vendor ID is required before ${po.poNumber} can be approved.`; case "VENDOR_ID_PROVIDED": return `The vendor ID has been provided for ${po.poNumber}. It is ready for your review.`; case "PAYMENT_PROCESSING": return `Payment is being processed for ${po.poNumber}.`; case "PAYMENT_SENT": return `Payment has been confirmed for ${po.poNumber}. Please confirm delivery/receipt when the goods arrive.`; case "RECEIPT_CONFIRMED": return `Receipt has been confirmed for ${po.poNumber}. The order is now closed.`; default: return `There has been an update on purchase order ${po.poNumber}.`; } }