feat(eoi): in-app pathway fills the same source PDF as Documenso
When the in-app pathway is used for EOI templates, we now load the same
source PDF that the Documenso template uploads and fill its AcroForm
fields with values from EoiContext via pdf-lib. Field names mirror the
Documenso template's formValues keys exactly (Name, Email, Address,
Yacht Name, Length, Width, Draft, Berth Number + Lease_10 / Purchase
checkboxes), so both pathways produce equivalent legal documents — only
the renderer differs.
The form is left interactive (not flattened) so a recipient can still
adjust values before signing. Non-EOI templates (welcome letters,
acknowledgments, etc.) keep using the existing HTML→pdfme path.
Adds:
- pdf-lib direct dep
- src/lib/pdf/fill-eoi-form.ts — load + fill helpers, EOI_TEMPLATE_PDF_PATH
env override
- assets/ + README documenting the expected source PDF
- next.config outputFileTracingIncludes so the asset is bundled in the
standalone build
Tests: 8 new (4 fill-form unit + 2 source-PDF route + 2 fallback);
645/645 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:38:02 +02:00
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 < Uint8Array > {
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 < PDFDocument [ 'getForm' ] > , 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 < PDFDocument [ 'getForm' ] > ,
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 < Uint8Array > {
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 ) ) ;
2026-05-02 03:11:14 +02:00
// 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 ? ? '' ) ;
feat(eoi): in-app pathway fills the same source PDF as Documenso
When the in-app pathway is used for EOI templates, we now load the same
source PDF that the Documenso template uploads and fill its AcroForm
fields with values from EoiContext via pdf-lib. Field names mirror the
Documenso template's formValues keys exactly (Name, Email, Address,
Yacht Name, Length, Width, Draft, Berth Number + Lease_10 / Purchase
checkboxes), so both pathways produce equivalent legal documents — only
the renderer differs.
The form is left interactive (not flattened) so a recipient can still
adjust values before signing. Non-EOI templates (welcome letters,
acknowledgments, etc.) keep using the existing HTML→pdfme path.
Adds:
- pdf-lib direct dep
- src/lib/pdf/fill-eoi-form.ts — load + fill helpers, EOI_TEMPLATE_PDF_PATH
env override
- assets/ + README documenting the expected source PDF
- next.config outputFileTracingIncludes so the asset is bundled in the
standalone build
Tests: 8 new (4 fill-form unit + 2 source-PDF route + 2 fallback);
645/645 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:38:02 +02:00
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 < Uint8Array > {
const bytes = await loadEoiTemplatePdf ( ) ;
return fillEoiFormFields ( bytes , context ) ;
}