"use server"; import { auth } from "@/auth"; import { db } from "@/lib/db"; import { buildPoPdfKey, uploadBuffer, generateDownloadUrl, statObject } from "@/lib/storage"; import { renderPoPdf, isPdfServiceConfigured, PdfServiceError } from "@/lib/pdf-service"; type Result = { ok: true; mailto: string; to: string } | { error: string }; // PO must be approved (a valid document) before it can be emailed to a vendor; // available through every later state, incl. once payment is recorded (issue #14). const EMAILABLE = ["MGR_APPROVED", "SENT_FOR_PAYMENT", "PARTIALLY_PAID", "PAID_DELIVERED", "PARTIALLY_CLOSED", "CLOSED"]; const VIEW_ALL_ROLES = ["ACCOUNTS", "MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"]; const LINK_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days /** * Build an "email this PO to the vendor" Outlook draft: render the PO to a PDF, * store it (R2), and return a mailto: addressed to the vendor's primary contact * with a time-limited download link in the body. The user reviews & sends it. */ export async function prepareVendorEmail(poId: string): Promise { const session = await auth(); if (!session?.user) return { error: "Unauthorized" }; const po = await db.purchaseOrder.findUnique({ where: { id: poId }, include: { company: { select: { name: true } }, vendor: { include: { contacts: { where: { isPrimary: true }, take: 1 } } }, }, }); if (!po) return { error: "PO not found" }; const canView = VIEW_ALL_ROLES.includes(session.user.role) || po.submitterId === session.user.id; if (!canView) return { error: "You cannot access this purchase order." }; if (!EMAILABLE.includes(po.status)) { return { error: "The PO must be approved before it can be emailed to the vendor." }; } const to = po.vendor?.contacts?.[0]?.email?.trim(); if (!to) { return { error: "The vendor has no primary contact email. Add one on the vendor before emailing." }; } if (!isPdfServiceConfigured()) { return { error: "PDF emailing is not configured on this environment." }; } // Render → store → presigned link. The PDF is cached at a deterministic // per-PO key: if a copy already exists and is at least as new as the PO's last // change, reuse it and only mint a fresh presigned URL (refreshing the 7-day // timer). Re-render only when there's no copy yet or the PO changed since. let link: string; try { const slug = po.poNumber.replace(/\//g, "-"); const key = buildPoPdfKey(poId, `${slug}.pdf`); const cached = await statObject(key); const isFresh = cached !== null && cached.lastModified >= po.updatedAt; if (!isFresh) { const pdf = await renderPoPdf(poId); await uploadBuffer(key, pdf, "application/pdf"); } link = await generateDownloadUrl(key, LINK_TTL_SECONDS); } catch (e) { if (e instanceof PdfServiceError) return { error: `Could not generate the PO PDF: ${e.message}` }; return { error: "Could not generate the PO PDF." }; } const company = po.company?.name ?? "Pelagia Marine Services Pvt. Ltd."; const vendorName = po.vendor?.contacts?.[0]?.name || po.vendor?.name || "Sir/Madam"; const sender = session.user.name ?? ""; const subject = `Purchase Order ${po.poNumber}`; const body = [ `Dear ${vendorName},`, "", `Please find our Purchase Order ${po.poNumber} at the link below:`, link, "", "(The link is valid for 7 days.)", "", "Regards,", sender, company, ].join("\n"); const mailto = `mailto:${encodeURIComponent(to)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; return { ok: true, mailto, to }; }