import { promises as fs } from 'node:fs'; import path from 'node:path'; import { PDFDocument } from 'pdf-lib'; import type { EoiContext } from '@/lib/services/eoi-context'; /** * Source PDF for the in-app EOI pathway. Must contain AcroForm fields whose * names match the Documenso template's `formValues` keys exactly: * * Text: Name, Email, Address, Yacht Name, Length, Width, Draft, * Berth Number * Checkbox: Lease_10, Purchase * * See assets/eoi-template/README.md for full details and the field mapping * doc at docs/eoi-documenso-field-mapping.md for the canonical list. */ const DEFAULT_EOI_TEMPLATE_PATH = path.join(process.cwd(), 'assets', 'eoi-template.pdf'); function eoiTemplatePath(): string { return process.env.EOI_TEMPLATE_PDF_PATH ?? DEFAULT_EOI_TEMPLATE_PATH; } export async function loadEoiTemplatePdf(): Promise { const filePath = eoiTemplatePath(); try { return await fs.readFile(filePath); } catch (err) { throw new Error( `EOI source PDF not found at ${filePath}. Drop the same PDF used by the Documenso template (with AcroForm fields: Name, Email, Address, Yacht Name, Length, Width, Draft, Berth Number, Lease_10, Purchase) at this path, or override via EOI_TEMPLATE_PDF_PATH. Original error: ${(err as Error).message}`, ); } } function formatAddress(address: EoiContext['client']['address']): string { if (!address) return ''; return [address.street, address.city, address.country].filter(Boolean).join(', '); } function setText(form: ReturnType, name: string, value: string): void { try { form.getTextField(name).setText(value); } catch { // Field absent or wrong type — skip silently so a slightly different PDF // template still produces output. Missing field issues surface in QA, not // at runtime as a 500. } } function setCheckbox( form: ReturnType, name: string, checked: boolean, ): void { try { const cb = form.getCheckBox(name); if (checked) cb.check(); else cb.uncheck(); } catch { // See comment in setText. } } /** * Fills the AcroForm fields of the EOI source PDF with values drawn from * EoiContext. Field names mirror the Documenso template `formValues` keys so * a single source PDF can serve both pathways. * * The form is left interactive (not flattened) so a recipient can still tweak * fields if needed before signing. */ export async function fillEoiFormFields( pdfBytes: Uint8Array, context: EoiContext, ): Promise { const doc = await PDFDocument.load(pdfBytes); const form = doc.getForm(); setText(form, 'Name', context.client.fullName); setText(form, 'Email', context.client.primaryEmail ?? ''); setText(form, 'Address', formatAddress(context.client.address)); // Yacht + berth (EOI Section 3) are optional — leave the AcroForm fields // blank when the interest hasn't been linked to either. setText(form, 'Yacht Name', context.yacht?.name ?? ''); setText(form, 'Length', context.yacht?.lengthFt ?? ''); setText(form, 'Width', context.yacht?.widthFt ?? ''); setText(form, 'Draft', context.yacht?.draftFt ?? ''); setText(form, 'Berth Number', context.berth?.mooringNumber ?? ''); setCheckbox(form, 'Purchase', true); setCheckbox(form, 'Lease_10', false); return doc.save(); } /** * Convenience: loads the source PDF from disk and returns the filled bytes. */ export async function generateEoiPdfFromTemplate(context: EoiContext): Promise { const bytes = await loadEoiTemplatePdf(); return fillEoiFormFields(bytes, context); }