feat(signing): internal "who signed" + completion email alerts to configurable recipients
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m56s
Build & Push Docker Images / build-and-push (push) Successful in 8m56s

Adds a per-port `signing_notification_recipients` setting (users / roles /
explicit emails via the existing RecipientPicker) and fires a branded
internal email on (a) each party signing and (b) full completion —
replicating the legacy "Document Signed" / "EOI Complete Update Status"
Activepieces flows that staff relied on.

- New branded template `signing-status-notification.tsx` (per-signer
  progress + completion variants, deep-links into the CRM).
- New `sendSigningStatusNotification` resolver in document-signing-emails:
  resolves recipients, falls back to the port reply-to (sales@) when the
  list is empty so alerts are never silently dropped, per-recipient send.
- Wired into `handleRecipientSigned` (first signed transition) and
  `handleDocumentCompleted` (idempotent, fires once) — reached by both the
  Documenso webhook and the 5-min poll. Fully guarded so a notification
  failure never undoes a signing side effect. Respects EMAIL_REDIRECT_TO.
- Admin UI: `Document signing alerts` recipients card in settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012iJPYbh5X53iBh9h7ffQoy
This commit is contained in:
2026-06-24 21:38:22 +02:00
parent 352b2420b7
commit 1c91d76c52
6 changed files with 447 additions and 0 deletions

View File

@@ -45,6 +45,7 @@ import {
import {
sendSigningInvitation,
sendSigningCompleted,
sendSigningStatusNotification,
type SignerRole,
} from '@/lib/services/document-signing-emails.service';
import {
@@ -1248,6 +1249,14 @@ export async function handleRecipientSigned(eventData: {
'cascading "your turn" invite failed after recipient signed',
);
});
// Internal "who signed" alert to the port's signing-notification
// recipients (admin + sales@). Fire-and-forget + fully guarded inside
// the helper so it can't undo the signing that just succeeded.
void notifySigningStatus(doc, 'signed', {
name: signer.signerName,
role: signer.signerRole,
});
}
}
@@ -1345,6 +1354,70 @@ async function sendCascadingInviteForNextSigner(doc: {
}
}
/**
* Fire the internal "signing progress" alert to the port's configured
* signing-notification recipients (admin + sales@, etc). Self-contained
* and fully guarded — a notification failure must never undo a signing /
* completion side effect, so all errors are swallowed + logged. Resolves
* the deal client name + a deep CRM link, and (for `signed`) the running
* signed/total progress.
*/
async function notifySigningStatus(
doc: Parameters<typeof resolveDocumentOwner>[1] & {
id: string;
portId: string;
documentType: string;
title: string;
},
event: 'signed' | 'completed',
signer?: { name: string; role: string | null } | null,
): Promise<void> {
try {
const port = await db.query.ports.findFirst({
where: eq(ports.id, doc.portId),
columns: { name: true, slug: true },
});
let clientName = doc.title;
const owner = await resolveDocumentOwner(doc.portId, doc);
if (owner?.entityType === 'client') {
const client = await db.query.clients.findFirst({
where: eq(clients.id, owner.entityId),
columns: { fullName: true },
});
if (client?.fullName) clientName = client.fullName;
}
let signedCount: number | undefined;
let totalCount: number | undefined;
if (event === 'signed') {
const all = await db
.select({ status: documentSigners.status })
.from(documentSigners)
.where(eq(documentSigners.documentId, doc.id));
totalCount = all.length;
signedCount = all.filter((s) => s.status === 'signed').length;
}
const crmUrl = `${env.APP_URL ?? ''}/${port?.slug ?? ''}/documents/${doc.id}`;
await sendSigningStatusNotification({
portId: doc.portId,
portName: port?.name ?? 'Port Nimara',
event,
documentLabel: DOC_TYPE_LABEL[doc.documentType] ?? 'Expression of Interest',
clientName,
crmUrl,
signerName: signer?.name ?? null,
signerRole: (signer?.role as SignerRole) ?? null,
signedCount,
totalCount,
});
} catch (err) {
logger.error({ err, documentId: doc.id, event }, 'signing status notification failed');
}
}
/**
* Manually (re)send the finalized signed PDF of a completed document to the
* deal's client. Mirrors the automatic completion fan-out (sendSigningCompleted)
@@ -1924,6 +1997,11 @@ export async function handleDocumentCompleted(eventData: { documentId: string; p
}),
);
}
// Internal "fully signed" alert to the port's signing-notification
// recipients (admin + sales@). handleDocumentCompleted is idempotent
// (early-returns on re-delivery), so this fires exactly once per doc.
void notifySigningStatus(doc, 'completed');
}
export async function handleDocumentExpired(eventData: { documentId: string; portId?: string }) {