diff --git a/src/lib/services/document-signing-emails.service.ts b/src/lib/services/document-signing-emails.service.ts index 4d69c16e..a78d9571 100644 --- a/src/lib/services/document-signing-emails.service.ts +++ b/src/lib/services/document-signing-emails.service.ts @@ -131,9 +131,14 @@ export interface SigningCompletedArgs { * Risk #5 - fixing this mapping prevents an `approver` invite from * landing on `/sign/error`. */ -const ROLE_TO_URL_SEGMENT: Record = { +const ROLE_TO_URL_SEGMENT: Record = { client: 'client', developer: 'developer', + // `document_signers.signer_role` persists Documenso's normalized role, so + // the order-2 EOI developer arrives here as 'signer' (not 'developer'). + // Without this alias the lookup returned `undefined` and the branded link + // became `…/sign/undefined/` (dead). Map it to the developer page. + signer: 'developer', approver: 'cc', witness: 'witness', other: 'cc', @@ -155,7 +160,9 @@ export function transformSigningUrl( // Trim trailing slashes off the host so we always produce a clean // single `/` between segments. const host = embeddedSigningHost.replace(/\/+$/, ''); - const urlRole = ROLE_TO_URL_SEGMENT[signerRole]; + // Fall back to the passive `cc` page for any unrecognised role rather than + // ever emitting `…/sign/undefined/`. + const urlRole = ROLE_TO_URL_SEGMENT[signerRole] ?? 'cc'; return `${host}/sign/${urlRole}/${token}`; } diff --git a/src/lib/services/documents.service.ts b/src/lib/services/documents.service.ts index 04bea46b..e143a4b0 100644 --- a/src/lib/services/documents.service.ts +++ b/src/lib/services/documents.service.ts @@ -46,6 +46,7 @@ import { sendSigningInvitation, sendSigningCompleted, sendSigningStatusNotification, + transformSigningUrl, type SignerRole, } from '@/lib/services/document-signing-emails.service'; import { @@ -1063,10 +1064,23 @@ export async function uploadSignedManually( export async function listDocumentSigners(documentId: string, portId: string) { await getDocumentById(documentId, portId); // verify access - return db.query.documentSigners.findMany({ + const rows = await db.query.documentSigners.findMany({ where: eq(documentSigners.documentId, documentId), orderBy: (ds, { asc }) => [asc(ds.signingOrder)], }); + + // Surface the BRANDED marketing-site embed URL (the same wrap the + // invitation email applies) rather than the bare Documenso link, so the + // EOI tab's "Copy link" shares the on-brand signing page. No-op when the + // port has no embeddedSigningHost configured (transformSigningUrl returns + // the raw URL unchanged). + const { embeddedSigningHost } = await getPortDocumensoConfig(portId); + return rows.map((r) => ({ + ...r, + signingUrl: r.signingUrl + ? transformSigningUrl(r.signingUrl, embeddedSigningHost, r.signerRole as SignerRole) + : r.signingUrl, + })); } // ─── List Events ────────────────────────────────────────────────────────────── diff --git a/tests/unit/services/document-signing-urls.test.ts b/tests/unit/services/document-signing-urls.test.ts index 181a0129..b91672a0 100644 --- a/tests/unit/services/document-signing-urls.test.ts +++ b/tests/unit/services/document-signing-urls.test.ts @@ -44,6 +44,21 @@ describe('transformSigningUrl', () => { ); }); + it("maps 'signer' (Documenso's persisted order-2 role) → /sign/developer/", () => { + // document_signers.signer_role stores Documenso's normalized role, so the + // EOI developer arrives as 'signer'. Regression: this used to fall through + // to `undefined` → dead `…/sign/undefined/` invitation links. + expect(transformSigningUrl(RAW, HOST, 'signer' as never)).toBe( + 'https://portnimara.com/sign/developer/vbT8hi3jKQmrFP_LN1WcS', + ); + }); + + it('falls back to /sign/cc/ for any unrecognised role (never undefined)', () => { + expect(transformSigningUrl(RAW, HOST, 'mystery-role' as never)).toBe( + 'https://portnimara.com/sign/cc/vbT8hi3jKQmrFP_LN1WcS', + ); + }); + it('maps witness → /sign/witness/', () => { expect(transformSigningUrl(RAW, HOST, 'witness')).toBe( 'https://portnimara.com/sign/witness/vbT8hi3jKQmrFP_LN1WcS',