Previously every "Email to vendor" click re-rendered the PO via PdfService and re-uploaded to R2 under a timestamped key — wasteful, and it orphaned a new object each time. Now the PDF is stored at a deterministic per-PO key (buildPoPdfKey → po-pdf/<poId>/<slug>.pdf). On each send, statObject() checks for an existing copy: if it exists and is at least as new as the PO's updatedAt, it's reused (no re-render, no re-upload) and only a fresh presigned URL is minted — refreshing the 7-day download timer. It re-renders only when there's no copy yet or the PO changed since the cached one (so an edited PO never emails a stale PDF). - lib/storage.ts: buildPoPdfKey (deterministic) + statObject (HEAD/stat, no body transfer; null when absent). - email-actions.ts: reuse-or-render decision keyed on updatedAt; always re-presign. - Tests: +2 (reuse-on-second-send-only-refreshes-link, re-render-when-changed). email-vendor suite 8 green; tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.6 KiB
TypeScript
90 lines
3.6 KiB
TypeScript
"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<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. 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 };
|
|
}
|