fix(eoi): silence Documenso's own lifecycle emails on createDocument
The local-fill EOI pathway creates fresh Documenso envelopes via createDocument, which (unlike the template pathway that inherits template 8's all-false emailSettings) used Documenso's defaults — every email event defaults to true on both the v1 and v2.13 APIs. So Documenso fired its OWN unbranded "Waiting for others to complete signing." and "Signing Complete!" emails (signed PDF attached, reply-to sales@), bypassing EMAIL_REDIRECT_TO and duplicating the CRM's branded sends. Force emailSettings to all-false (DOCUMENSO_SILENT_EMAIL_SETTINGS) on every createDocument call (v1 JSON + v2 multipart). The CRM stays the sole sender of signing comms. Verified against the live v2.13 OpenAPI + template 8's stored meta. Also stop the EMAIL_REDIRECT_TO gate from appending "(was: <email>)" to the recipient NAME: a "Name" field auto-fills from it into the signed PDF, so the annotation overlapped the signature. Redirect the email only; the original is still logged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ describe('Documenso recipient redirect - EMAIL_REDIRECT_TO', () => {
|
||||
const originalRedirect = process.env.EMAIL_REDIRECT_TO;
|
||||
const originalDocumensoUrl = process.env.DOCUMENSO_API_URL;
|
||||
const originalDocumensoKey = process.env.DOCUMENSO_API_KEY;
|
||||
const originalDocumensoVersion = process.env.DOCUMENSO_API_VERSION;
|
||||
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
@@ -28,6 +29,10 @@ describe('Documenso recipient redirect - EMAIL_REDIRECT_TO', () => {
|
||||
process.env.EMAIL_REDIRECT_TO = REDIRECT_TARGET;
|
||||
process.env.DOCUMENSO_API_URL = 'https://documenso.example.test';
|
||||
process.env.DOCUMENSO_API_KEY = 'test-key';
|
||||
// Pin v1 — prod's API version + these assertions read the JSON request
|
||||
// body. Without this the local .env's DOCUMENSO_API_VERSION leaks in and
|
||||
// the v2 multipart/FormData path makes JSON.parse(body) throw.
|
||||
process.env.DOCUMENSO_API_VERSION = 'v1';
|
||||
|
||||
fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
@@ -49,6 +54,8 @@ describe('Documenso recipient redirect - EMAIL_REDIRECT_TO', () => {
|
||||
else process.env.DOCUMENSO_API_URL = originalDocumensoUrl;
|
||||
if (originalDocumensoKey === undefined) delete process.env.DOCUMENSO_API_KEY;
|
||||
else process.env.DOCUMENSO_API_KEY = originalDocumensoKey;
|
||||
if (originalDocumensoVersion === undefined) delete process.env.DOCUMENSO_API_VERSION;
|
||||
else process.env.DOCUMENSO_API_VERSION = originalDocumensoVersion;
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
@@ -63,10 +70,72 @@ describe('Documenso recipient redirect - EMAIL_REDIRECT_TO', () => {
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const callBody = JSON.parse(fetchMock.mock.calls[0]![1].body as string) as any;
|
||||
expect(callBody.recipients).toHaveLength(2);
|
||||
const namesByOrder = Object.fromEntries(
|
||||
callBody.recipients.map((r: any) => [r.signingOrder, r.name]),
|
||||
);
|
||||
for (const r of callBody.recipients) {
|
||||
expect(r.email).toBe(REDIRECT_TARGET);
|
||||
// Original email preserved in the name for traceability
|
||||
expect(r.name).toMatch(/\(was: .+@realclient\.com\)/);
|
||||
}
|
||||
// Name must stay CLEAN — it renders into the signed PDF's Name field, so
|
||||
// the "(was: …)" redirect annotation must NOT leak into it (it overlapped
|
||||
// the signature). Email-only redirect; original email lives in the logs.
|
||||
expect(namesByOrder[1]).toBe('Alice Smith');
|
||||
expect(namesByOrder[2]).toBe('Bob Smith');
|
||||
for (const r of callBody.recipients) {
|
||||
expect(r.name).not.toContain('(was:');
|
||||
}
|
||||
});
|
||||
|
||||
it('createDocument - suppresses Documenso own emails (emailSettings all false)', async () => {
|
||||
vi.resetModules();
|
||||
const mod = await import('@/lib/services/documenso-client');
|
||||
await mod.createDocument('Test Doc', 'pdf-base64', [
|
||||
{ name: 'Alice Smith', email: 'alice@realclient.com', role: 'SIGNER', signingOrder: 1 },
|
||||
]);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const callBody = JSON.parse(fetchMock.mock.calls[0]![1].body as string) as any;
|
||||
// The CRM is the SOLE sender of signing comms. Documenso must never fire
|
||||
// its own "Waiting for others" / "Signing Complete!" lifecycle emails, so
|
||||
// every per-document email event is disabled at creation time.
|
||||
expect(callBody.meta).toBeDefined();
|
||||
expect(callBody.meta.emailSettings).toBeDefined();
|
||||
const es = callBody.meta.emailSettings;
|
||||
for (const key of [
|
||||
'recipientSigningRequest',
|
||||
'recipientSigned',
|
||||
'recipientRemoved',
|
||||
'documentPending',
|
||||
'documentCompleted',
|
||||
'documentDeleted',
|
||||
'ownerDocumentCreated',
|
||||
'ownerDocumentCompleted',
|
||||
'ownerRecipientExpired',
|
||||
]) {
|
||||
expect(es[key]).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('createDocument (v2) - emailSettings all false in the multipart payload', async () => {
|
||||
process.env.DOCUMENSO_API_VERSION = 'v2';
|
||||
vi.resetModules();
|
||||
const mod = await import('@/lib/services/documenso-client');
|
||||
await mod.createDocument('Test Doc', 'pdf-base64', [
|
||||
{ name: 'Alice Smith', email: 'alice@realclient.com', role: 'SIGNER', signingOrder: 1 },
|
||||
]);
|
||||
|
||||
// v2 envelope/create is multipart/form-data; the JSON lives in `payload`.
|
||||
const form = fetchMock.mock.calls[0]![1].body as FormData;
|
||||
const payload = JSON.parse(form.get('payload') as string) as any;
|
||||
expect(payload.meta.emailSettings).toBeDefined();
|
||||
for (const key of [
|
||||
'recipientSigningRequest',
|
||||
'recipientSigned',
|
||||
'documentPending',
|
||||
'documentCompleted',
|
||||
'ownerDocumentCompleted',
|
||||
]) {
|
||||
expect(payload.meta.emailSettings[key]).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,7 +171,8 @@ describe('Documenso recipient redirect - EMAIL_REDIRECT_TO', () => {
|
||||
const callBody = JSON.parse(fetchMock.mock.calls[0]![1].body as string) as any;
|
||||
for (const r of callBody.recipients) {
|
||||
expect(r.email).toBe(REDIRECT_TARGET);
|
||||
expect(r.name).toMatch(/\(was: .+@realclient\.com\)/);
|
||||
// Name stays clean — no "(was: …)" annotation (renders into the PDF).
|
||||
expect(r.name).not.toContain('(was:');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user