Files
pn-new-crm/tests/unit/services/signing-status-notification.service.test.ts
Matt 1c91d76c52
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m56s
Build & Push Docker Images / build-and-push (push) Successful in 8m56s
feat(signing): internal "who signed" + completion email alerts to configurable recipients
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
2026-06-24 21:38:22 +02:00

83 lines
2.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
// Boundaries the sender depends on — mock the I/O edges, exercise the real
// wiring (recipient resolution → template → per-recipient send).
vi.mock('@/lib/email', () => ({ sendEmail: vi.fn().mockResolvedValue(undefined) }));
vi.mock('@/lib/email/branding-resolver', () => ({
getBrandingShell: vi.fn().mockResolvedValue(null),
}));
vi.mock('@/lib/services/notification-recipients', () => ({
resolveNotificationRecipients: vi.fn(),
}));
import { sendEmail } from '@/lib/email';
import { resolveNotificationRecipients } from '@/lib/services/notification-recipients';
import { sendSigningStatusNotification } from '@/lib/services/document-signing-emails.service';
const mockSendEmail = vi.mocked(sendEmail);
const mockResolve = vi.mocked(resolveNotificationRecipients);
const baseArgs = {
portId: 'port-1',
portName: 'Port Nimara',
event: 'signed' as const,
documentLabel: 'Expression of Interest',
clientName: 'Jane Doe',
crmUrl: 'https://crm.portnimara.com/port-nimara/documents/abc',
signerName: 'Jane Doe',
signerRole: 'client' as const,
signedCount: 1,
totalCount: 3,
};
describe('sendSigningStatusNotification', () => {
beforeEach(() => {
mockSendEmail.mockClear();
mockResolve.mockReset();
});
it('emails every configured recipient when a signer signs', async () => {
mockResolve.mockResolvedValue(['admin@portnimara.com', 'sales@portnimara.com']);
await sendSigningStatusNotification(baseArgs);
// Resolves from the signing list, falling back to the reply-to address.
expect(mockResolve).toHaveBeenCalledWith(
'port-1',
'signing_notification_recipients',
'email_reply_to',
);
expect(mockSendEmail).toHaveBeenCalledTimes(2);
const recipients = mockSendEmail.mock.calls.map((c) => c[0]);
expect(recipients).toContain('admin@portnimara.com');
expect(recipients).toContain('sales@portnimara.com');
// Subject reflects who signed.
const subject = mockSendEmail.mock.calls[0]?.[1] as string;
expect(subject).toContain('Jane Doe');
// portId threaded through so per-port From + redirect apply.
expect(mockSendEmail.mock.calls[0]?.[5]).toBe('port-1');
});
it('sends nothing when no recipients are configured or resolvable', async () => {
mockResolve.mockResolvedValue([]);
await sendSigningStatusNotification(baseArgs);
expect(mockSendEmail).not.toHaveBeenCalled();
});
it('uses the completion subject for the completed event', async () => {
mockResolve.mockResolvedValue(['sales@portnimara.com']);
await sendSigningStatusNotification({
...baseArgs,
event: 'completed',
signerName: null,
});
const subject = mockSendEmail.mock.calls[0]?.[1] as string;
expect(subject.toLowerCase()).toContain('fully signed');
expect(subject).toContain('Jane Doe');
});
});