From b1dfec09a03e7ab30b18333b0c587620da9187be Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 13 May 2026 14:08:52 +0200 Subject: [PATCH] feat(documenso-phase-7): Project Director RBAC binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin UI binding for the developer + approver user-id fields that Phase 1 schema'd but left unwired. Surfaces four new fields in the Documenso settings card so admins can: - Set per-port display labels for the developer/approver slots (documenso_developer_label / approver_label) — drives email subjects + signer-progress UI copy. Defaults to "Developer" / "Approver" when blank. - Link each slot to a CRM user (documenso_developer_user_id / approver_user_id) — UUID from /admin/users. Webhook side-effect: - handleRecipientSigned's cascade now fires an in-CRM notification for the next pending signer when their signerRole matches a configured developer_user_id / approver_user_id. The branded email is the primary channel; the notification is a defense-in- depth nudge for users who live in the CRM all day. - New notification type `document_signing_your_turn` with dedupeKey `document::your-turn:` so duplicate webhook deliveries don't re-notify. - Falls back silently when the binding isn't set or the signer isn't a developer/approver — preserves the existing flow. Out of scope (build plan flags as out-of-scope for v1): - Auto-fill name/email when a user is selected: needs a typeahead field type the SettingsFormCard doesn't have yet. Admin reads the user's UUID from /admin/users and pastes; minor friction for a one-time per-port config. - Webhook handler reading the linked user's email and matching against the inbound recipient: today the developer/approver email settings already drive the matching; the user-id is purely a notification target. Tests: 1340/1340 ✅; tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../[portSlug]/admin/documenso/page.tsx | 36 +++++++++++++++++++ src/lib/services/documents.service.ts | 34 ++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx b/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx index 7929134b..f50d0dca 100644 --- a/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx +++ b/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx @@ -57,6 +57,24 @@ const SIGNER_FIELDS: SettingFieldDef[] = [ placeholder: 'dm@portnimara.com', defaultValue: '', }, + { + key: 'documenso_developer_label', + label: 'Developer signer — display label', + description: + 'How the developer slot is referenced in email subjects + signer-progress UI copy. Defaults to "Developer" when blank.', + type: 'string', + placeholder: 'Developer', + defaultValue: '', + }, + { + key: 'documenso_developer_user_id', + label: 'Developer signer — linked CRM user (optional)', + description: + "Project Director RBAC binding. When set, the webhook handler fires an in-CRM notification for this user when it's their turn to sign — alongside the branded email. Leave blank if the developer slot doesn't map to a CRM user (e.g. external developer). Use the user's UUID from /admin/users.", + type: 'string', + placeholder: '00000000-0000-0000-0000-000000000000', + defaultValue: '', + }, { key: 'documenso_approver_name', label: 'Approver — name', @@ -74,6 +92,24 @@ const SIGNER_FIELDS: SettingFieldDef[] = [ placeholder: 'sales@portnimara.com', defaultValue: '', }, + { + key: 'documenso_approver_label', + label: 'Approver — display label', + description: + 'How the approver slot is referenced in email subjects + signer-progress UI copy. Defaults to "Approver" when blank.', + type: 'string', + placeholder: 'Approver', + defaultValue: '', + }, + { + key: 'documenso_approver_user_id', + label: 'Approver — linked CRM user (optional)', + description: + "Same as developer's linked user — when set, fires an in-CRM notification when it's the approver's turn. Use the user's UUID from /admin/users.", + type: 'string', + placeholder: '00000000-0000-0000-0000-000000000000', + defaultValue: '', + }, ]; const EOI_FIELDS: SettingFieldDef[] = [ diff --git a/src/lib/services/documents.service.ts b/src/lib/services/documents.service.ts index 9b02c6e5..c6b0fee8 100644 --- a/src/lib/services/documents.service.ts +++ b/src/lib/services/documents.service.ts @@ -1151,6 +1151,40 @@ async function sendCascadingInviteForNextSigner(doc: { .update(documentSigners) .set({ invitedAt: new Date() }) .where(eq(documentSigners.id, next.id)); + + // Phase 7 — Project Director RBAC binding: when the per-port settings + // map the developer / approver slot to a CRM user (developerUserId / + // approverUserId), fire an in-CRM notification so the user sees their + // pending signing turn alongside the branded email. The email is the + // primary channel; the notification is a defense-in-depth nudge for + // users who live in the CRM all day. Falls back silently when the + // settings aren't wired or the signer role doesn't match. + const linkedUserId = + next.signerRole === 'developer' + ? (docCfg.developerUserId ?? null) + : next.signerRole === 'approver' + ? (docCfg.approverUserId ?? null) + : null; + if (linkedUserId) { + void import('@/lib/services/notifications.service').then(({ createNotification }) => + createNotification({ + portId: doc.portId, + userId: linkedUserId, + type: 'document_signing_your_turn', + title: 'Your signature is needed', + description: `"${doc.title}" is waiting for you to sign.`, + link: `/documents/${doc.id}`, + entityType: 'document', + entityId: doc.id, + dedupeKey: `document:${doc.id}:your-turn:${next.id}`, + }).catch((err) => { + logger.warn( + { err, documentId: doc.id, signerId: next.id, linkedUserId }, + 'phase-7 in-CRM your-turn notification failed (email still sent)', + ); + }), + ); + } } // ─── Owner-wins resolution ────────────────────────────────────────────────────