feat(eoi): toggleable local-fill pathway — clean detail render + address wrapping
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m48s
Build & Push Docker Images / build-and-push (push) Successful in 8m34s

EOI detail fields (address, name, yacht, berth) rendered oversized and
top-clipped because Documenso auto-sizes AcroForm text when *it* fills the
template (ignores the PDF's 12pt font; a taller box → bigger font → more clip,
and a 2-line address box renders huge). Proven: filling the same source PDF
locally at 12pt renders cleanly and wraps long addresses to a 2nd line.

Add a per-port `eoi_fill_method` setting (default `local`), toggleable in
admin → Documenso → Templates & signing pathway:
- local:     CRM fills + flattens the source PDF (pdf-lib, fixed 12pt +
             multiline address wrap), uploads the flattened PDF to Documenso,
             and places ONLY the 6 page-3 signature fields. Documenso never
             re-renders the body text → no clipping.
- documenso: legacy template AcroForm fill (auto-sizes/clips) — fallback only.

Both still flow through Documenso for signing, so branded invites, embedded
signing, webhooks, signer rows, and the EOI milestone are unchanged.

- computeEoiSignatureLayout(): 6 page-3 fields at template-8 coords (unit-tested)
- createDocument (v1): PUT bytes to Documenso's presigned uploadUrl (2.x v1-compat
  ignores the base64 field) so the uploaded document actually has content
- placeFields (v1): pass fieldMeta through so the Place-of-Signing TEXT field
  keeps its label/required

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 02:48:11 +02:00
parent af05bb18dc
commit 0ca9b2c3b5
6 changed files with 431 additions and 40 deletions

View File

@@ -391,8 +391,13 @@ export async function createDocument(
return getDocument(envelopeId, portId);
}
// v1: existing path. Meta keys are accepted at the top level.
return documensoFetch(
// v1: existing path. Meta keys are accepted at the top level. We still send
// `document` (base64) for older Documenso servers that store it inline, but
// Documenso 2.x's v1-compat endpoint instead returns a presigned `uploadUrl`
// and expects the PDF bytes to be PUT there (the base64 is ignored). So when
// the create response carries an `uploadUrl`, upload the bytes to it — without
// this the document is created with NO content (signers see a blank PDF).
const raw = (await documensoFetch(
'/api/v1/documents',
{
method: 'POST',
@@ -412,7 +417,39 @@ export async function createDocument(
}),
},
portId,
).then(normalizeDocument);
)) as Record<string, unknown>;
const uploadUrl = typeof raw.uploadUrl === 'string' ? raw.uploadUrl : null;
if (uploadUrl) {
const pdfBuffer = Buffer.from(pdfBase64, 'base64');
let putRes: Response;
try {
putRes = await fetchWithTimeout(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'application/pdf' },
body: pdfBuffer,
});
} catch (err) {
if (err instanceof FetchTimeoutError) {
throw new CodedError('DOCUMENSO_TIMEOUT', {
internalMessage: `v1 createDocument uploadUrl PUT timed out after ${err.timeoutMs}ms`,
});
}
throw err;
}
if (!putRes.ok) {
const errText = await putRes.text().catch(() => '');
logger.error(
{ status: putRes.status, err: errText, portId },
'Documenso v1 createDocument uploadUrl PUT failed - document has no content',
);
throw new CodedError('DOCUMENSO_UPSTREAM_ERROR', {
internalMessage: `v1 createDocument uploadUrl PUT → ${putRes.status}: ${errText}`,
});
}
}
return normalizeDocument(raw);
}
export async function generateDocumentFromTemplate(
@@ -1340,6 +1377,10 @@ export async function placeFields(
pageY: Math.round((f.pageY / 100) * dims.height),
pageWidth: Math.round((f.pageWidth / 100) * dims.width),
pageHeight: Math.round((f.pageHeight / 100) * dims.height),
// Pass fieldMeta through on v1 too (Documenso 2.x's v1-compat endpoint
// accepts it) so TEXT fields like "Place of Signing" keep their label /
// required / placeholder. Older v1 servers ignore unknown keys.
...(f.fieldMeta ? { fieldMeta: f.fieldMeta } : {}),
};
// Retry transient failures so one flaky 5xx mid-loop doesn't leave
// the document with a partial field set. 3 attempts at 250 / 500 /
@@ -1425,6 +1466,93 @@ export function computeDefaultSignatureLayout(
}));
}
/**
* EOI page-3 signature-block layout — the six fields template 8 carries, so
* the in-app pathway (local pdf-lib fill + flatten → upload as a Documenso
* document) produces a signed EOI that matches the legacy template output
* exactly. Coordinates are percent of page, captured verbatim from template 8.
*
* Client (signer 1) gets Signature + Name + Place-of-Signing (TEXT) + Date.
* Developer (signer 2) gets Name + Signature. The approver (signer 3) carries
* no fields. `fieldMeta` is passed through to Documenso (v1 + v2) so the
* Place-of-Signing field keeps its label / required / placeholder.
*/
export function computeEoiSignatureLayout(
clientRecipientId: number | string,
developerRecipientId: number | string,
): DocumensoFieldPlacement[] {
return [
{
recipientId: clientRecipientId,
type: 'SIGNATURE',
pageNumber: 3,
pageX: 39.64497370960451,
pageY: 64.81957098456644,
pageWidth: 21.21662173851308,
pageHeight: 4.303685358613111,
fieldMeta: { type: 'signature', fontSize: 18, overflow: 'auto' },
},
{
recipientId: clientRecipientId,
type: 'NAME',
pageNumber: 3,
pageX: 14.34911393977768,
pageY: 64.81957098456644,
pageWidth: 24.33234194973456,
pageHeight: 4.303685358613111,
fieldMeta: { type: 'name', fontSize: 12, textAlign: 'left' },
},
{
recipientId: clientRecipientId,
type: 'TEXT',
pageNumber: 3,
pageX: 14.49704042881816,
pageY: 57.4932908677896,
pageWidth: 24.4807121661721,
pageHeight: 4.40865329418904,
fieldMeta: {
type: 'text',
label: 'Place of Signing',
readOnly: false,
required: true,
textAlign: 'left',
placeholder: 'Anguilla, AI',
characterLimit: 0,
},
},
{
recipientId: clientRecipientId,
type: 'DATE',
pageNumber: 3,
pageX: 39.79290246256028,
pageY: 57.4932908677896,
pageWidth: 21.06824925816024,
pageHeight: 4.40865329418904,
fieldMeta: { type: 'date', fontSize: 10, overflow: 'auto', textAlign: 'left' },
},
{
recipientId: developerRecipientId,
type: 'NAME',
pageNumber: 3,
pageX: 14.34911393977768,
pageY: 72.56877244919716,
pageWidth: 24.33234194973456,
pageHeight: 3.988781551885322,
fieldMeta: { type: 'name', fontSize: 12, textAlign: 'left' },
},
{
recipientId: developerRecipientId,
type: 'SIGNATURE',
pageNumber: 3,
pageX: 39.64497370960451,
pageY: 72.56877244919716,
pageWidth: 21.21662173851308,
pageHeight: 3.988781551885322,
fieldMeta: { type: 'signature', fontSize: 18, overflow: 'auto' },
},
];
}
/**
* Void/cancel a Documenso document.
*