pelagia-portal/App/lib/notifier.ts
2026-05-18 23:18:58 +05:30

224 lines
7.9 KiB
TypeScript

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<NotificationEvent, string> = {
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 `
<!DOCTYPE html>
<html>
<body style="font-family:Inter,sans-serif;max-width:560px;margin:0 auto;padding:32px 24px;color:#111827;">
<div style="border-bottom:2px solid #2563eb;padding-bottom:12px;margin-bottom:24px;">
<span style="font-size:20px;font-weight:700;color:#1d4ed8;">PPMS</span>
</div>
<p style="margin:0 0 16px;">Hi ${recipient.name},</p>
<p style="margin:0 0 24px;">${buildEmailBody(event, po, note)}</p>
<div style="background:#f3f4f6;border-radius:8px;padding:16px;font-size:13px;color:#374151;">
<strong>PO Number:</strong> ${po.poNumber}<br/>
<strong>Title:</strong> ${po.title}<br/>
<strong>Submitted by:</strong> ${po.submitter.name}
</div>
<p style="margin-top:32px;font-size:12px;color:#9ca3af;">
You received this message because you are a stakeholder on this purchase order.
</p>
</body>
</html>`;
}
function buildEmailBody(
event: NotificationEvent,
po: PurchaseOrder & { submitter: User },
note?: string
): string {
const noteHtml = note
? `<br/><br/><em style="color:#374151;">"${note}"</em>`
: "";
switch (event) {
case "PO_SUBMITTED":
return `<strong>${po.submitter.name}</strong> has submitted purchase order <strong>${po.poNumber}</strong> for your review.`;
case "PO_APPROVED":
case "PO_APPROVED_WITH_NOTE":
return `Your purchase order <strong>${po.poNumber}</strong> has been <span style="color:#16a34a;font-weight:600;">approved</span>.${noteHtml}`;
case "PO_REJECTED":
return `Your purchase order <strong>${po.poNumber}</strong> has been <span style="color:#dc2626;font-weight:600;">rejected</span>.${noteHtml}`;
case "EDITS_REQUESTED":
return `Edits have been requested on <strong>${po.poNumber}</strong>. Please update the order and resubmit.${noteHtml}`;
case "VENDOR_ID_REQUESTED":
return `A vendor ID is required before <strong>${po.poNumber}</strong> can be approved.`;
case "VENDOR_ID_PROVIDED":
return `The vendor ID has been provided for <strong>${po.poNumber}</strong>. It is ready for your review.`;
case "PAYMENT_PROCESSING":
return `Payment is being processed for <strong>${po.poNumber}</strong>.`;
case "PAYMENT_SENT":
return `Payment has been confirmed for <strong>${po.poNumber}</strong>. Please confirm delivery/receipt when the goods arrive.`;
case "RECEIPT_CONFIRMED":
return `Receipt has been confirmed for <strong>${po.poNumber}</strong>. The order is now closed.`;
default:
return `There has been an update on purchase order ${po.poNumber}.`;
}
}