feat(eoi): toggleable local-fill pathway — clean detail render + address wrapping
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:
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user