Adds an "Email to vendor" button on the PO detail (available once approved,
through CLOSED, and again after payment) that opens an Outlook draft addressed
to the vendor's primary contact with a time-limited PDF download link.
Since mailto: can't attach files, the PDF is rendered and stored, and the draft
carries a link (the approach chosen for this issue):
- PdfService/: new standalone Express + Playwright microservice (GstService/
EpfoService pattern) — POST /pdf { url } renders a page to a real PDF via
headless Chromium. SSRF-guarded (shared token + optional origin allowlist).
- export route: accepts a server-only `svc` token (PDF_SERVICE_TOKEN) so
PdfService can fetch /api/po/[id]/export?format=pdf without a user session;
`pdf=1` drops the print button + window.print() auto-trigger.
- lib/pdf-service.ts renderPoPdf(); prepareVendorEmail() server action renders →
uploads to R2 (po-pdf/…) → presigns a 7-day link → returns a mailto draft.
- po-detail: EmailVendorButton, shown when approved + vendor has a contact email.
- Gated by PDF_SERVICE_URL/PDF_SERVICE_TOKEN; friendly error if unconfigured.
- No DB model/migration. Tests: prepareVendorEmail (6, PdfService/storage mocked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
"use server";
|
|
|
|
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import { buildStorageKey, uploadBuffer, generateDownloadUrl } 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<Result> {
|
|
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.
|
|
let link: string;
|
|
try {
|
|
const pdf = await renderPoPdf(poId);
|
|
const slug = po.poNumber.replace(/\//g, "-");
|
|
const key = buildStorageKey("po-pdf", poId, `${slug}.pdf`);
|
|
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 };
|
|
}
|