diff --git a/src/app/api/public/residential-inquiries/route.ts b/src/app/api/public/residential-inquiries/route.ts index f6554c3a..7bb865f0 100644 --- a/src/app/api/public/residential-inquiries/route.ts +++ b/src/app/api/public/residential-inquiries/route.ts @@ -144,7 +144,7 @@ async function sendResidentialNotifications(args: { const branding = await getBrandingShell(portId); // Client confirmation - const confirmation = residentialClientConfirmation( + const confirmation = await residentialClientConfirmation( { firstName: data.firstName, contactEmail: 'sales@portnimara.com', @@ -186,7 +186,7 @@ async function sendResidentialNotifications(args: { return; } - const alert = residentialSalesAlert( + const alert = await residentialSalesAlert( { fullName: `${data.firstName} ${data.lastName}`.trim(), email: data.email, diff --git a/src/lib/email/templates/admin-email-change.ts b/src/lib/email/templates/admin-email-change.ts deleted file mode 100644 index c03f4f90..00000000 --- a/src/lib/email/templates/admin-email-change.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; - -interface AdminEmailChangeData { - recipientName?: string; - /** New address the user should sign in with from now on. */ - newEmail: string; - /** Display name of the admin who initiated the change — surfaced so the - * recipient knows who to follow up with. */ - changedByDisplayName?: string; - /** Optional URL for the sign-in page. */ - loginUrl?: string; - portName?: string; -} - -interface RenderOpts { - branding?: BrandingShell | null; -} - -export function adminEmailChangeEmail( - data: AdminEmailChangeData, - overrides?: RenderOpts, -): { subject: string; html: string; text: string } { - const portName = data.portName ?? 'Port Nimara'; - const subject = `An administrator updated your ${portName} sign-in email`; - const greeting = data.recipientName ? `Hello ${escapeHtml(data.recipientName)},` : 'Hello,'; - const accent = brandingPrimaryColor(overrides?.branding); - - const adminLine = data.changedByDisplayName - ? `${escapeHtml(data.changedByDisplayName)} (an administrator)` - : 'an administrator'; - - const body = ` -

- Your sign-in email was changed -

-

${greeting}

-

- ${adminLine} just updated the email address linked to your ${escapeHtml( - portName, - )} account. From now on, please sign in with the new address below: -

-

- ${escapeHtml(data.newEmail)} -

- ${ - data.loginUrl - ? `

- - Sign in - -

` - : '' - } -

- If you weren't expecting this change, contact your administrator immediately. - Your old address (the one this message was sent to) can no longer be used to - sign in. -

-

- Thanks,
- ${escapeHtml(portName)} -

`; - - const text = [ - `Your sign-in email was changed`, - '', - `${data.changedByDisplayName ?? 'An administrator'} updated the email linked to your ${portName} account.`, - `From now on, sign in with: ${data.newEmail}`, - '', - data.loginUrl ? `Sign in: ${data.loginUrl}` : '', - '', - `If you weren't expecting this change, contact your administrator immediately.`, - ] - .filter(Boolean) - .join('\n'); - - const html = renderShell({ - branding: overrides?.branding ?? null, - title: subject, - body, - }); - - return { subject, html, text }; -} - -function escapeHtml(s: string): string { - return s - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/src/lib/email/templates/admin-email-change.tsx b/src/lib/email/templates/admin-email-change.tsx new file mode 100644 index 00000000..dc02620c --- /dev/null +++ b/src/lib/email/templates/admin-email-change.tsx @@ -0,0 +1,109 @@ +import { Button, Hr, Text, render } from '@react-email/components'; +import * as React from 'react'; + +import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; + +interface AdminEmailChangeData { + recipientName?: string; + newEmail: string; + changedByDisplayName?: string; + loginUrl?: string; + portName?: string; +} + +interface RenderOpts { + branding?: BrandingShell | null; +} + +function AdminEmailChangeBody({ + portName, + newEmail, + recipientName, + changedByDisplayName, + loginUrl, + accent, +}: AdminEmailChangeData & { portName: string; accent: string }) { + const greeting = recipientName ? `Hello ${recipientName},` : 'Hello,'; + const adminLine = changedByDisplayName + ? `${changedByDisplayName} (an administrator)` + : 'an administrator'; + return ( + <> + + Your sign-in email was changed + + {greeting} + + {adminLine} just updated the email address linked to your {portName} account. From now on, + please sign in with the new address below: + + + {newEmail} + + {loginUrl ? ( +
+ +
+ ) : null} +
+ + If you weren't expecting this change, contact your administrator immediately. Your old + address (the one this message was sent to) can no longer be used to sign in. + + + Thanks, +
+ {portName} +
+ + ); +} + +export async function adminEmailChangeEmail( + data: AdminEmailChangeData, + overrides?: RenderOpts, +): Promise<{ subject: string; html: string; text: string }> { + const portName = data.portName ?? 'Port Nimara'; + const subject = `An administrator updated your ${portName} sign-in email`; + const accent = brandingPrimaryColor(overrides?.branding); + + const body = await render( + , + { + pretty: false, + }, + ); + + const text = [ + `Your sign-in email was changed`, + '', + `${data.changedByDisplayName ?? 'An administrator'} updated the email linked to your ${portName} account.`, + `From now on, sign in with: ${data.newEmail}`, + '', + data.loginUrl ? `Sign in: ${data.loginUrl}` : '', + '', + `If you weren't expecting this change, contact your administrator immediately.`, + ] + .filter(Boolean) + .join('\n'); + + return { + subject, + html: renderShell({ branding: overrides?.branding ?? null, title: subject, body }), + text, + }; +} diff --git a/src/lib/email/templates/crm-invite.ts b/src/lib/email/templates/crm-invite.ts deleted file mode 100644 index 3dc5337c..00000000 --- a/src/lib/email/templates/crm-invite.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; - -interface InviteData { - link: string; - ttlHours: number; - recipientName?: string; - isSuperAdmin: boolean; - /** Display name for the port — falls back to "Port Nimara" so the - * pre-multi-tenant default still reads correctly. */ - portName?: string; -} - -interface RenderOpts { - branding?: BrandingShell | null; -} - -export function crmInviteEmail( - data: InviteData, - overrides?: RenderOpts, -): { - subject: string; - html: string; - text: string; -} { - const portName = data.portName ?? 'Port Nimara'; - const subject = `You're invited to the ${portName} CRM`; - const greeting = data.recipientName ? `Dear ${escapeHtml(data.recipientName)},` : 'Welcome,'; - const role = data.isSuperAdmin ? 'super administrator' : 'administrator'; - const accent = brandingPrimaryColor(overrides?.branding); - - const body = ` -

- Welcome to the ${escapeHtml(portName)} CRM -

-

${greeting}

-

- You've been invited to the ${escapeHtml(portName)} CRM as a ${role}. Click the - button below to set your password and activate your account. The - link expires in ${data.ttlHours} hours. -

-

- - Set up your account - -

-

- If the button doesn't work, paste this link into your browser:
- ${data.link} -

-

- Thank you,
- ${escapeHtml(portName)} CRM -

`; - - const text = [ - `Welcome to the ${portName} CRM`, - '', - `You've been invited as a ${role}.`, - `Set up your account: ${data.link}`, - '', - `The link expires in ${data.ttlHours} hours.`, - '', - `Thank you,`, - `${portName} CRM`, - ].join('\n'); - - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - text, - }; -} - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/src/lib/email/templates/crm-invite.tsx b/src/lib/email/templates/crm-invite.tsx new file mode 100644 index 00000000..459bb69f --- /dev/null +++ b/src/lib/email/templates/crm-invite.tsx @@ -0,0 +1,121 @@ +import { Button, Hr, Link, Text, render } from '@react-email/components'; +import * as React from 'react'; + +import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; + +interface InviteData { + link: string; + ttlHours: number; + recipientName?: string; + isSuperAdmin: boolean; + /** Display name for the port — falls back to "Port Nimara" so the + * pre-multi-tenant default still reads correctly. */ + portName?: string; +} + +interface RenderOpts { + branding?: BrandingShell | null; +} + +function InviteBody({ + portName, + link, + ttlHours, + recipientName, + role, + accent, +}: { + portName: string; + link: string; + ttlHours: number; + recipientName?: string; + role: string; + accent: string; +}) { + const greeting = recipientName ? `Dear ${recipientName},` : 'Welcome,'; + return ( + <> + + Welcome to the {portName} CRM + + {greeting} + + You've been invited to the {portName} CRM as a {role}. Click the button below to set + your password and activate your account. The link expires in {ttlHours} hours. + +
+ +
+
+ + If the button doesn't work, paste this link into your browser: +
+ + {link} + +
+ + Thank you, +
+ {portName} CRM +
+ + ); +} + +export async function crmInviteEmail( + data: InviteData, + overrides?: RenderOpts, +): Promise<{ subject: string; html: string; text: string }> { + const portName = data.portName ?? 'Port Nimara'; + const subject = `You're invited to the ${portName} CRM`; + const role = data.isSuperAdmin ? 'super administrator' : 'administrator'; + const accent = brandingPrimaryColor(overrides?.branding); + + const body = await render( + , + { pretty: false }, + ); + + const text = [ + `Welcome to the ${portName} CRM`, + '', + `You've been invited as a ${role}.`, + `Set up your account: ${data.link}`, + '', + `The link expires in ${data.ttlHours} hours.`, + '', + `Thank you,`, + `${portName} CRM`, + ].join('\n'); + + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + text, + }; +} diff --git a/src/lib/email/templates/document-signing.ts b/src/lib/email/templates/document-signing.ts deleted file mode 100644 index 6479e50f..00000000 --- a/src/lib/email/templates/document-signing.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Branded transactional emails for the Documenso signing lifecycle. - * - * Three template families: - * - * 1. `signingInvitation` — sent to a single signer when their turn - * to sign comes up. Used both for the initial client invite (after - * EOI/contract/reservation generation) AND for the cascading - * "your turn" emails when an earlier signer completes (developer - * after client signs, approver after developer signs, etc). - * - * 2. `signingCompleted` — sent to ALL signers (with the finalized - * signed PDF as an attachment) when the document reaches a fully - * signed state. Mirrors the old system's - * `sendFinalizedDocumentToSignatories` flow. - * - * 3. `signingReminder` — sent when a rep nudges an unsigned recipient - * manually OR when the rate-limited reminder service fires. Same - * visual shape as `signingInvitation` with different copy. - * - * All three use the per-port `BrandingShell` (logo + primary color + - * header/footer HTML) so each tenant's outbound emails match its - * brand. The signing URL passed in is expected to already be - * embedded-format (e.g. `https://portnimara.com/sign//`) - * — the caller (interest service / webhook handler) does the - * transformation from the raw Documenso URL. - */ - -import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; - -interface RenderOpts { - subject?: string | null; - branding?: BrandingShell | null; -} - -interface InvitationData { - /** Display name for the recipient — used in the greeting. */ - recipientName: string; - /** Friendly document type label. e.g. "Expression of Interest", "Sales Contract", "Reservation Agreement". */ - documentLabel: string; - /** Optional. The signer's role: 'client' | 'developer' | 'approver' | 'witness' etc. Drives copy nuance. */ - signerRole?: string | null; - /** Embedded signing URL (already wrapped to the public branded host). */ - signingUrl: string; - /** Port name to brand the email. */ - portName: string; - /** Sales rep / sender name shown in the closing. Falls back to "{portName} team". */ - senderName?: string | null; - /** Optional plain-text message from the rep to include above the CTA. */ - customMessage?: string | null; -} - -export function signingInvitationEmail( - data: InvitationData, - overrides?: RenderOpts, -): { subject: string; html: string; text: string } { - const accent = brandingPrimaryColor(overrides?.branding); - const docLabelEsc = escapeHtml(data.documentLabel); - const subject = overrides?.subject - ? overrides.subject - .replace(/\{\{documentLabel\}\}/g, data.documentLabel) - .replace(/\{\{portName\}\}/g, data.portName) - .replace(/\{\{recipientName\}\}/g, data.recipientName) - : `${data.documentLabel} ready to sign — ${data.portName}`; - - const greeting = `Dear ${escapeHtml(data.recipientName)},`; - const closer = data.senderName - ? `${escapeHtml(data.senderName)}
${escapeHtml(data.portName)}` - : `The ${escapeHtml(data.portName)} team`; - - // Slightly different lead paragraph based on signer role so the - // developer / approver emails don't read as if they're the client. - const isClient = (data.signerRole ?? 'client') === 'client'; - const leadCopy = isClient - ? `Your ${docLabelEsc} for ${escapeHtml(data.portName)} is ready for signing. Click the button below to review and sign — it should only take a couple of minutes.` - : data.signerRole === 'approver' - ? `An ${docLabelEsc} is awaiting your approval. The earlier signers have completed their parts; please review and sign to finalise the document.` - : `An ${docLabelEsc} is awaiting your signature. The client has already signed; you're the next signer in the chain.`; - - const customMessageBlock = data.customMessage - ? `

${escapeHtml(data.customMessage).replace(/\n/g, '
')}

` - : ''; - - const body = ` -

- ${docLabelEsc} ready to sign -

-

${greeting}

-

${leadCopy}

- ${customMessageBlock} -

- - Review & sign - -

-

- If the button doesn't work, paste this link into your browser:
- ${data.signingUrl} -

-

- Signing happens directly inside our website — your data isn't sent to a third-party signing service. -

-

- Thank you,
- ${closer} -

`; - - const text = `${greeting}\n\n${stripTags(leadCopy)}\n\n${data.customMessage ? data.customMessage + '\n\n' : ''}Sign here: ${data.signingUrl}\n\nThank you,\n${data.senderName ?? `The ${data.portName} team`}`; - - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - text, - }; -} - -interface CompletedData { - recipientName: string; - documentLabel: string; - /** Identity of the linked client (the deal's primary subject). */ - clientName: string; - portName: string; - /** When the document reached fully-signed state. */ - completedAt: Date; -} - -export function signingCompletedEmail( - data: CompletedData, - overrides?: RenderOpts, -): { subject: string; html: string; text: string } { - const accent = brandingPrimaryColor(overrides?.branding); - const docLabelEsc = escapeHtml(data.documentLabel); - const subject = overrides?.subject - ? overrides.subject - .replace(/\{\{documentLabel\}\}/g, data.documentLabel) - .replace(/\{\{clientName\}\}/g, data.clientName) - .replace(/\{\{portName\}\}/g, data.portName) - : `${data.documentLabel} fully signed — ${data.clientName}`; - - const greeting = `Dear ${escapeHtml(data.recipientName)},`; - const completedDateStr = data.completedAt.toLocaleString('en-GB', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - - const body = ` -

- ${docLabelEsc} signed by all parties -

-

${greeting}

-

- The ${docLabelEsc} for ${escapeHtml(data.clientName)} has been signed by every party as of ${completedDateStr}. -

-

- The fully signed PDF is attached to this email for your records. A copy has also been stored in the ${escapeHtml(data.portName)} CRM. -

-

- Thank you,
- The ${escapeHtml(data.portName)} team -

`; - - const text = `${greeting}\n\nThe ${data.documentLabel} for ${data.clientName} has been signed by all parties on ${completedDateStr}. The signed PDF is attached for your records.\n\nThank you,\nThe ${data.portName} team`; - - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - text, - }; -} - -interface ReminderData { - recipientName: string; - documentLabel: string; - signingUrl: string; - portName: string; - /** Human-readable string of how long ago the original invitation was sent. */ - invitedAgo: string; - customMessage?: string | null; -} - -export function signingReminderEmail( - data: ReminderData, - overrides?: RenderOpts, -): { subject: string; html: string; text: string } { - const accent = brandingPrimaryColor(overrides?.branding); - const docLabelEsc = escapeHtml(data.documentLabel); - const subject = overrides?.subject - ? overrides.subject - .replace(/\{\{documentLabel\}\}/g, data.documentLabel) - .replace(/\{\{portName\}\}/g, data.portName) - : `Friendly reminder: ${data.documentLabel} still awaiting your signature — ${data.portName}`; - - const greeting = `Dear ${escapeHtml(data.recipientName)},`; - const customMessageBlock = data.customMessage - ? `

${escapeHtml(data.customMessage).replace(/\n/g, '
')}

` - : ''; - - const body = ` -

- Just a quick reminder -

-

${greeting}

-

- We sent you a ${docLabelEsc} ${escapeHtml(data.invitedAgo)} that's still awaiting your signature. If you've already signed, please disregard this message — it can take a few minutes for our system to catch up. -

- ${customMessageBlock} -

- - Sign now - -

-

- Direct link: ${data.signingUrl} -

-

- Thank you,
- The ${escapeHtml(data.portName)} team -

`; - - const text = `${greeting}\n\nWe sent you a ${data.documentLabel} ${data.invitedAgo} that's still awaiting your signature. ${data.customMessage ? '\n\n' + data.customMessage + '\n\n' : ''}\n\nSign here: ${data.signingUrl}\n\nThank you,\nThe ${data.portName} team`; - - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - text, - }; -} - -// ─── Helpers ──────────────────────────────────────────────────────────────── - -function escapeHtml(input: string): string { - return input - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -function stripTags(html: string): string { - return html.replace(/<[^>]+>/g, ''); -} diff --git a/src/lib/email/templates/document-signing.tsx b/src/lib/email/templates/document-signing.tsx new file mode 100644 index 00000000..4f978ea5 --- /dev/null +++ b/src/lib/email/templates/document-signing.tsx @@ -0,0 +1,327 @@ +/** + * Branded transactional emails for the Documenso signing lifecycle. + * + * Three template families: + * + * 1. signingInvitation — sent to a single signer when their turn to sign + * comes up. Used both for initial client invites AND cascading "your + * turn" emails when an earlier signer completes. + * + * 2. signingCompleted — sent to ALL signers (with the finalized signed + * PDF as an attachment) when the document reaches a fully signed state. + * + * 3. signingReminder — sent when a rep nudges manually OR when the + * rate-limited reminder service fires. + * + * All three use the per-port BrandingShell. The signing URL is expected + * to already be embedded-format (e.g. /sign//) — the caller + * does the transformation from the raw Documenso URL. + */ + +import { Button, Hr, Link, Text, render } from '@react-email/components'; +import * as React from 'react'; + +import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; + +interface RenderOpts { + subject?: string | null; + branding?: BrandingShell | null; +} + +// ─── 1. Invitation ─────────────────────────────────────────────────────────── + +interface InvitationData { + recipientName: string; + documentLabel: string; + signerRole?: string | null; + signingUrl: string; + portName: string; + senderName?: string | null; + customMessage?: string | null; +} + +function InvitationBody({ data, accent }: { data: InvitationData; accent: string }) { + const greeting = `Dear ${data.recipientName},`; + const isClient = (data.signerRole ?? 'client') === 'client'; + const leadCopy = isClient + ? `Your ${data.documentLabel} for ${data.portName} is ready for signing. Click the button below to review and sign — it should only take a couple of minutes.` + : data.signerRole === 'approver' + ? `An ${data.documentLabel} is awaiting your approval. The earlier signers have completed their parts; please review and sign to finalise the document.` + : `An ${data.documentLabel} is awaiting your signature. The client has already signed; you're the next signer in the chain.`; + + return ( + <> + + {data.documentLabel} ready to sign + + {greeting} + {leadCopy} + {data.customMessage ? ( + + {data.customMessage} + + ) : null} +
+ +
+
+ + If the button doesn't work, paste this link into your browser: +
+ + {data.signingUrl} + +
+ + Signing happens directly inside our website — your data isn't sent to a third-party + signing service. + + + Thank you, +
+ {data.senderName ? ( + <> + {data.senderName} +
+ {data.portName} + + ) : ( + The {data.portName} team + )} +
+ + ); +} + +export async function signingInvitationEmail( + data: InvitationData, + overrides?: RenderOpts, +): Promise<{ subject: string; html: string; text: string }> { + const accent = brandingPrimaryColor(overrides?.branding); + const subject = overrides?.subject + ? overrides.subject + .replace(/\{\{documentLabel\}\}/g, data.documentLabel) + .replace(/\{\{portName\}\}/g, data.portName) + .replace(/\{\{recipientName\}\}/g, data.recipientName) + : `${data.documentLabel} ready to sign — ${data.portName}`; + + const body = await render(, { pretty: false }); + + const isClient = (data.signerRole ?? 'client') === 'client'; + const leadText = isClient + ? `Your ${data.documentLabel} for ${data.portName} is ready for signing.` + : data.signerRole === 'approver' + ? `An ${data.documentLabel} is awaiting your approval.` + : `An ${data.documentLabel} is awaiting your signature.`; + const text = `Dear ${data.recipientName},\n\n${leadText}\n\n${data.customMessage ? data.customMessage + '\n\n' : ''}Sign here: ${data.signingUrl}\n\nThank you,\n${data.senderName ?? `The ${data.portName} team`}`; + + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + text, + }; +} + +// ─── 2. Completed ───────────────────────────────────────────────────────────── + +interface CompletedData { + recipientName: string; + documentLabel: string; + clientName: string; + portName: string; + completedAt: Date; +} + +function CompletedBody({ + data, + accent, + completedDateStr, +}: { + data: CompletedData; + accent: string; + completedDateStr: string; +}) { + const greeting = `Dear ${data.recipientName},`; + return ( + <> + + {data.documentLabel} signed by all parties + + {greeting} + + The {data.documentLabel} for {data.clientName} has been signed by every + party as of {completedDateStr}. + + + The fully signed PDF is attached to this email for your records. A copy has also been stored + in the {data.portName} CRM. + + + Thank you, +
+ The {data.portName} team +
+ + ); +} + +export async function signingCompletedEmail( + data: CompletedData, + overrides?: RenderOpts, +): Promise<{ subject: string; html: string; text: string }> { + const accent = brandingPrimaryColor(overrides?.branding); + const subject = overrides?.subject + ? overrides.subject + .replace(/\{\{documentLabel\}\}/g, data.documentLabel) + .replace(/\{\{clientName\}\}/g, data.clientName) + .replace(/\{\{portName\}\}/g, data.portName) + : `${data.documentLabel} fully signed — ${data.clientName}`; + const completedDateStr = data.completedAt.toLocaleString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + + const body = await render( + , + { pretty: false }, + ); + + const text = `Dear ${data.recipientName},\n\nThe ${data.documentLabel} for ${data.clientName} has been signed by all parties on ${completedDateStr}. The signed PDF is attached for your records.\n\nThank you,\nThe ${data.portName} team`; + + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + text, + }; +} + +// ─── 3. Reminder ────────────────────────────────────────────────────────────── + +interface ReminderData { + recipientName: string; + documentLabel: string; + signingUrl: string; + portName: string; + invitedAgo: string; + customMessage?: string | null; +} + +function ReminderBody({ data, accent }: { data: ReminderData; accent: string }) { + const greeting = `Dear ${data.recipientName},`; + return ( + <> + + Just a quick reminder + + {greeting} + + We sent you a {data.documentLabel} {data.invitedAgo} that's still awaiting your + signature. If you've already signed, please disregard this message — it can take a few + minutes for our system to catch up. + + {data.customMessage ? ( + + {data.customMessage} + + ) : null} +
+ +
+
+ + Direct link:{' '} + + {data.signingUrl} + + + + Thank you, +
+ The {data.portName} team +
+ + ); +} + +export async function signingReminderEmail( + data: ReminderData, + overrides?: RenderOpts, +): Promise<{ subject: string; html: string; text: string }> { + const accent = brandingPrimaryColor(overrides?.branding); + const subject = overrides?.subject + ? overrides.subject + .replace(/\{\{documentLabel\}\}/g, data.documentLabel) + .replace(/\{\{portName\}\}/g, data.portName) + : `Friendly reminder: ${data.documentLabel} still awaiting your signature — ${data.portName}`; + + const body = await render(, { pretty: false }); + + const text = `Dear ${data.recipientName},\n\nWe sent you a ${data.documentLabel} ${data.invitedAgo} that's still awaiting your signature. ${data.customMessage ? '\n\n' + data.customMessage + '\n\n' : ''}\n\nSign here: ${data.signingUrl}\n\nThank you,\nThe ${data.portName} team`; + + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + text, + }; +} diff --git a/src/lib/email/templates/inquiry-client-confirmation.ts b/src/lib/email/templates/inquiry-client-confirmation.ts deleted file mode 100644 index 18218808..00000000 --- a/src/lib/email/templates/inquiry-client-confirmation.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { brandingPrimaryColor, renderShell, type BrandingShell } from '@/lib/email/shell'; - -export interface InquiryClientConfirmationData { - firstName: string; - mooringNumber: string | null; - contactEmail: string; - /** Display name; falls back to "Port Nimara". */ - portName?: string; -} - -interface RenderOpts { - branding?: BrandingShell | null; -} - -export function inquiryClientConfirmation( - data: InquiryClientConfirmationData, - overrides?: RenderOpts, -) { - const { firstName, mooringNumber, contactEmail } = data; - const portName = data.portName ?? 'Port Nimara'; - - const berthText = mooringNumber ? `Berth ${mooringNumber}` : `a ${portName} Berth`; - const subject = mooringNumber - ? `Thank You for Your Interest in Berth ${mooringNumber}` - : `Thank You for Your Interest in a ${portName} Berth`; - - const accent = brandingPrimaryColor(overrides?.branding); - - const body = ` -

Dear ${escapeHtml(firstName)},

-

- Thank you for expressing interest in ${escapeHtml(berthText)}. - Our team has registered your interest, and we will reach out to you very shortly - by your preferred method of contact with more information. -

-

- If you have any questions, please feel free to reach out to us at - ${escapeHtml(contactEmail)}. -

-

- Best regards,
- The ${escapeHtml(portName)} Sales Team -

`; - - const text = [ - `Dear ${firstName},`, - '', - `Thank you for expressing interest in ${berthText}. Our team has registered your interest, and we will reach out to you very shortly by your preferred method of contact with more information.`, - '', - `If you have any questions, please feel free to reach out to us at ${contactEmail}.`, - '', - 'Best regards,', - `The ${portName} Sales Team`, - ].join('\n'); - - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - text, - }; -} - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/src/lib/email/templates/inquiry-client-confirmation.tsx b/src/lib/email/templates/inquiry-client-confirmation.tsx new file mode 100644 index 00000000..4de337c7 --- /dev/null +++ b/src/lib/email/templates/inquiry-client-confirmation.tsx @@ -0,0 +1,96 @@ +import { Link, Text, render } from '@react-email/components'; +import * as React from 'react'; + +import { brandingPrimaryColor, renderShell, type BrandingShell } from '@/lib/email/shell'; + +export interface InquiryClientConfirmationData { + firstName: string; + mooringNumber: string | null; + contactEmail: string; + portName?: string; +} + +interface RenderOpts { + branding?: BrandingShell | null; +} + +function ClientConfirmationBody({ + firstName, + berthText, + contactEmail, + portName, + accent, +}: { + firstName: string; + berthText: string; + contactEmail: string; + portName: string; + accent: string; +}) { + return ( + <> + Dear {firstName}, + + Thank you for expressing interest in {berthText}. Our team has registered your interest, and + we will reach out to you very shortly by your preferred method of contact with more + information. + + + If you have any questions, please feel free to reach out to us at{' '} + + {contactEmail} + + . + + + Best regards, +
+ The {portName} Sales Team +
+ + ); +} + +export async function inquiryClientConfirmation( + data: InquiryClientConfirmationData, + overrides?: RenderOpts, +) { + const { firstName, mooringNumber, contactEmail } = data; + const portName = data.portName ?? 'Port Nimara'; + const berthText = mooringNumber ? `Berth ${mooringNumber}` : `a ${portName} Berth`; + const subject = mooringNumber + ? `Thank You for Your Interest in Berth ${mooringNumber}` + : `Thank You for Your Interest in a ${portName} Berth`; + const accent = brandingPrimaryColor(overrides?.branding); + + const body = await render( + , + { pretty: false }, + ); + + const text = [ + `Dear ${firstName},`, + '', + `Thank you for expressing interest in ${berthText}. Our team has registered your interest, and we will reach out to you very shortly by your preferred method of contact with more information.`, + '', + `If you have any questions, please feel free to reach out to us at ${contactEmail}.`, + '', + 'Best regards,', + `The ${portName} Sales Team`, + ].join('\n'); + + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + text, + }; +} diff --git a/src/lib/email/templates/inquiry-sales-notification.ts b/src/lib/email/templates/inquiry-sales-notification.ts deleted file mode 100644 index 1a657a72..00000000 --- a/src/lib/email/templates/inquiry-sales-notification.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { brandingPrimaryColor, renderShell, type BrandingShell } from '@/lib/email/shell'; - -export interface InquirySalesNotificationData { - fullName: string; - email: string; - phone: string; - mooringNumber: string | null; - crmUrl: string; - /** Display name; falls back to "Port Nimara". */ - portName?: string; -} - -interface RenderOpts { - branding?: BrandingShell | null; -} - -export function inquirySalesNotification( - data: InquirySalesNotificationData, - overrides?: RenderOpts, -) { - const { fullName, email, phone, mooringNumber, crmUrl } = data; - const portName = data.portName ?? 'Port Nimara'; - const mooringDisplay = mooringNumber || 'None'; - const subject = `New Interest - ${portName}`; - const accent = brandingPrimaryColor(overrides?.branding); - - const body = ` -

Dear Administrator,

-

${escapeHtml(fullName)} has expressed their interest in ${escapeHtml(portName)}. Here are their details:

-

Name: ${escapeHtml(fullName)}

-

Email: ${escapeHtml(email)}

-

Telephone: ${escapeHtml(phone)}

-

Berths Selected: ${escapeHtml(mooringDisplay)}

-

Please visit the ${escapeHtml(portName)} CRM to view more information.

-

Thank you,
${escapeHtml(portName)} CRM

`; - - const text = [ - 'Dear Administrator,', - '', - `${fullName} has expressed their interest in ${portName}. Here are their details:`, - '', - `Name: ${fullName}`, - `Email: ${email}`, - `Telephone: ${phone}`, - `Berths Selected: ${mooringDisplay}`, - '', - `Please visit the ${portName} CRM (${crmUrl}) to view more information.`, - '', - 'Thank you', - `${portName} CRM`, - ].join('\n'); - - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - text, - }; -} - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/src/lib/email/templates/inquiry-sales-notification.tsx b/src/lib/email/templates/inquiry-sales-notification.tsx new file mode 100644 index 00000000..8d95c28b --- /dev/null +++ b/src/lib/email/templates/inquiry-sales-notification.tsx @@ -0,0 +1,115 @@ +import { Link, Text, render } from '@react-email/components'; +import * as React from 'react'; + +import { brandingPrimaryColor, renderShell, type BrandingShell } from '@/lib/email/shell'; + +export interface InquirySalesNotificationData { + fullName: string; + email: string; + phone: string; + mooringNumber: string | null; + crmUrl: string; + portName?: string; +} + +interface RenderOpts { + branding?: BrandingShell | null; +} + +function SalesNotificationBody({ + fullName, + email, + phone, + mooringDisplay, + crmUrl, + portName, + accent, +}: { + fullName: string; + email: string; + phone: string; + mooringDisplay: string; + crmUrl: string; + portName: string; + accent: string; +}) { + const detailStyle = { margin: '0 0 0', fontSize: '16px' } as const; + return ( + <> + Dear Administrator, + + {fullName} has expressed their interest in {portName}. Here are their + details: + + + Name: {fullName} + + + Email: {email} + + + Telephone: {phone} + + + Berths Selected: {mooringDisplay} + + + Please visit the{' '} + + {portName} CRM + {' '} + to view more information. + + + Thank you, +
+ {portName} CRM +
+ + ); +} + +export async function inquirySalesNotification( + data: InquirySalesNotificationData, + overrides?: RenderOpts, +) { + const portName = data.portName ?? 'Port Nimara'; + const mooringDisplay = data.mooringNumber || 'None'; + const subject = `New Interest - ${portName}`; + const accent = brandingPrimaryColor(overrides?.branding); + + const body = await render( + , + { pretty: false }, + ); + + const text = [ + 'Dear Administrator,', + '', + `${data.fullName} has expressed their interest in ${portName}. Here are their details:`, + '', + `Name: ${data.fullName}`, + `Email: ${data.email}`, + `Telephone: ${data.phone}`, + `Berths Selected: ${mooringDisplay}`, + '', + `Please visit the ${portName} CRM (${data.crmUrl}) to view more information.`, + '', + 'Thank you', + `${portName} CRM`, + ].join('\n'); + + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + text, + }; +} diff --git a/src/lib/email/templates/notification-digest.ts b/src/lib/email/templates/notification-digest.ts deleted file mode 100644 index f340f8c0..00000000 --- a/src/lib/email/templates/notification-digest.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Daily / hourly digest email of a CRM user's unread notifications. - * Used by the notification-digest scheduler (queued in `email` worker). - */ - -import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; - -interface DigestData { - portName: string; - recipientName: string; - /** Each notification we want to surface. Trimmed to ~20 by the - * caller — anything longer drops a link to the in-app inbox. */ - items: Array<{ - type: string; - title: string; - description: string | null; - link: string | null; - createdAt: Date; - }>; - totalUnread: number; - inboxLink: string; -} - -interface RenderOpts { - branding?: BrandingShell | null; -} - -const TYPE_LABELS: Record = { - reminder_due: 'Reminder due', - reminder_overdue: 'Reminder overdue', - new_registration: 'New inquiry', - eoi_signed: 'EOI signed', - eoi_completed: 'EOI completed', - email_received: 'New email', - duplicate_alert: 'Possible duplicate', - invoice_overdue: 'Invoice overdue', - system_alert: 'System alert', - follow_up_created: 'Follow-up', - tenure_expiring: 'Tenure expiring', - berth_released: 'Berth released', -}; - -export function notificationDigestEmail( - data: DigestData, - overrides?: RenderOpts, -): { - subject: string; - html: string; - text: string; -} { - const subject = `${data.portName} CRM digest — ${data.totalUnread} unread`; - const accent = brandingPrimaryColor(overrides?.branding); - - const itemsHtml = data.items - .map((item) => { - const label = TYPE_LABELS[item.type] ?? item.type.replace(/_/g, ' '); - const titleHtml = item.link - ? `${escapeHtml(item.title)}` - : `${escapeHtml(item.title)}`; - const desc = item.description - ? `
${escapeHtml(item.description)}
` - : ''; - return ` -
${label}
-
${titleHtml}
- ${desc} - `; - }) - .join(''); - - const tail = - data.totalUnread > data.items.length - ? `

…and ${data.totalUnread - data.items.length} more. - Open the inbox to see everything.

` - : ''; - - const greeting = data.recipientName ? `Hi ${escapeHtml(data.recipientName)},` : 'Hi,'; - - const body = ` -

- Your ${escapeHtml(data.portName)} CRM digest -

-

${greeting}

-

- You have ${data.totalUnread} unread notification${data.totalUnread === 1 ? '' : 's'} since the last digest. -

- - ${itemsHtml} -
- ${tail} -

- Thank you,
- ${escapeHtml(data.portName)} CRM -

`; - - const text = [ - `${data.portName} CRM digest`, - '', - `You have ${data.totalUnread} unread notifications.`, - '', - ...data.items.map((i) => `• [${i.type.replace(/_/g, ' ')}] ${i.title}`), - '', - `Inbox: ${data.inboxLink}`, - ].join('\n'); - - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - text, - }; -} - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/src/lib/email/templates/notification-digest.tsx b/src/lib/email/templates/notification-digest.tsx new file mode 100644 index 00000000..e226d12a --- /dev/null +++ b/src/lib/email/templates/notification-digest.tsx @@ -0,0 +1,140 @@ +import { Link, Text, render } from '@react-email/components'; +import * as React from 'react'; + +import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; + +interface DigestData { + portName: string; + recipientName: string; + items: Array<{ + type: string; + title: string; + description: string | null; + link: string | null; + createdAt: Date; + }>; + totalUnread: number; + inboxLink: string; +} + +interface RenderOpts { + branding?: BrandingShell | null; +} + +const TYPE_LABELS: Record = { + reminder_due: 'Reminder due', + reminder_overdue: 'Reminder overdue', + new_registration: 'New inquiry', + eoi_signed: 'EOI signed', + eoi_completed: 'EOI completed', + email_received: 'New email', + duplicate_alert: 'Possible duplicate', + invoice_overdue: 'Invoice overdue', + system_alert: 'System alert', + follow_up_created: 'Follow-up', + tenure_expiring: 'Tenure expiring', + berth_released: 'Berth released', +}; + +function DigestBody({ + portName, + recipientName, + items, + totalUnread, + inboxLink, + accent, +}: DigestData & { accent: string }) { + const greeting = recipientName ? `Hi ${recipientName},` : 'Hi,'; + return ( + <> + + Your {portName} CRM digest + + {greeting} + + You have {totalUnread} unread notification + {totalUnread === 1 ? '' : 's'} since the last digest. + + + + {items.map((item, i) => { + const label = TYPE_LABELS[item.type] ?? item.type.replace(/_/g, ' '); + return ( + + + + ); + })} + +
+
+ {label} +
+
+ {item.link ? ( + + {item.title} + + ) : ( + {item.title} + )} +
+ {item.description ? ( +
+ {item.description} +
+ ) : null} +
+ {totalUnread > items.length ? ( + + …and {totalUnread - items.length} more.{' '} + + Open the inbox + {' '} + to see everything. + + ) : null} + + Thank you, +
+ {portName} CRM +
+ + ); +} + +export async function notificationDigestEmail( + data: DigestData, + overrides?: RenderOpts, +): Promise<{ subject: string; html: string; text: string }> { + const subject = `${data.portName} CRM digest — ${data.totalUnread} unread`; + const accent = brandingPrimaryColor(overrides?.branding); + + const body = await render(, { pretty: false }); + + const text = [ + `${data.portName} CRM digest`, + '', + `You have ${data.totalUnread} unread notifications.`, + '', + ...data.items.map((i) => `• [${i.type.replace(/_/g, ' ')}] ${i.title}`), + '', + `Inbox: ${data.inboxLink}`, + ].join('\n'); + + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + text, + }; +} diff --git a/src/lib/email/templates/residential-inquiry.ts b/src/lib/email/templates/residential-inquiry.ts deleted file mode 100644 index be62f180..00000000 --- a/src/lib/email/templates/residential-inquiry.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; - -interface RenderOpts { - branding?: BrandingShell | null; -} - -export interface ResidentialClientConfirmationData { - firstName: string; - contactEmail: string; - /** Display name; falls back to "Port Nimara". */ - portName?: string; -} - -export function residentialClientConfirmation( - data: ResidentialClientConfirmationData, - overrides?: RenderOpts, -) { - const portName = data.portName ?? 'Port Nimara'; - const subject = `Thank You for Your Interest - ${portName} Residences`; - const accent = brandingPrimaryColor(overrides?.branding); - const body = ` -

- Welcome to ${escapeHtml(portName)} -

-

- Dear ${escapeHtml(data.firstName)}, -

-

- Thank you for expressing interest in ${escapeHtml(portName)} residences. Our residential - sales team has received your inquiry and will reach out to you shortly with - more information. -

-

- If you have any questions in the meantime, please reach us at - ${escapeHtml(data.contactEmail)}. -

-

- Best regards,
- The ${escapeHtml(portName)} Residential Team -

`; - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - }; -} - -export interface ResidentialSalesAlertData { - fullName: string; - email: string; - phone: string; - placeOfResidence?: string; - preferredContactMethod?: 'email' | 'phone'; - notes?: string; - preferences?: string; - crmDeepLink?: string; - portName?: string; -} - -export function residentialSalesAlert(data: ResidentialSalesAlertData, overrides?: RenderOpts) { - const portName = data.portName ?? 'Port Nimara'; - const subject = `New Residential Inquiry - ${data.fullName}`; - const accent = brandingPrimaryColor(overrides?.branding); - const body = ` -

- New residential inquiry -

- - - - - ${data.placeOfResidence ? `` : ''} - ${data.preferredContactMethod ? `` : ''} - ${data.preferences ? `` : ''} - ${data.notes ? `` : ''} -
Name${escapeHtml(data.fullName)}
Email${escapeHtml(data.email)}
Phone${escapeHtml(data.phone)}
Residence${escapeHtml(data.placeOfResidence)}
Prefers${escapeHtml(data.preferredContactMethod)}
Preferences${escapeHtml(data.preferences)}
Notes${escapeHtml(data.notes)}
- ${data.crmDeepLink ? `

Open in CRM

` : ''} -

- ${escapeHtml(portName)} CRM

`; - return { - subject, - html: renderShell({ title: subject, body, branding: overrides?.branding }), - }; -} - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/src/lib/email/templates/residential-inquiry.tsx b/src/lib/email/templates/residential-inquiry.tsx new file mode 100644 index 00000000..a3393244 --- /dev/null +++ b/src/lib/email/templates/residential-inquiry.tsx @@ -0,0 +1,190 @@ +import { Button, Link, Text, render } from '@react-email/components'; +import * as React from 'react'; + +import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell'; + +interface RenderOpts { + branding?: BrandingShell | null; +} + +export interface ResidentialClientConfirmationData { + firstName: string; + contactEmail: string; + portName?: string; +} + +function ClientConfirmationBody({ + portName, + firstName, + contactEmail, + accent, +}: { + portName: string; + firstName: string; + contactEmail: string; + accent: string; +}) { + return ( + <> + + Welcome to {portName} + + + Dear {firstName}, + + + Thank you for expressing interest in {portName} residences. Our residential sales team has + received your inquiry and will reach out to you shortly with more information. + + + If you have any questions in the meantime, please reach us at{' '} + + {contactEmail} + + . + + + Best regards, +
+ The {portName} Residential Team +
+ + ); +} + +export async function residentialClientConfirmation( + data: ResidentialClientConfirmationData, + overrides?: RenderOpts, +) { + const portName = data.portName ?? 'Port Nimara'; + const subject = `Thank You for Your Interest - ${portName} Residences`; + const accent = brandingPrimaryColor(overrides?.branding); + const body = await render( + , + { pretty: false }, + ); + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + }; +} + +export interface ResidentialSalesAlertData { + fullName: string; + email: string; + phone: string; + placeOfResidence?: string; + preferredContactMethod?: 'email' | 'phone'; + notes?: string; + preferences?: string; + crmDeepLink?: string; + portName?: string; +} + +function SalesAlertBody({ + portName, + data, + accent, +}: { + portName: string; + data: ResidentialSalesAlertData; + accent: string; +}) { + const labelCell = { color: '#666', width: '140px' } as const; + return ( + <> + + New residential inquiry + + + + + + + + + + + + + + + + {data.placeOfResidence ? ( + + + + + ) : null} + {data.preferredContactMethod ? ( + + + + + ) : null} + {data.preferences ? ( + + + + + ) : null} + {data.notes ? ( + + + + + ) : null} + +
Name{data.fullName}
Email{data.email}
Phone{data.phone}
Residence{data.placeOfResidence}
Prefers{data.preferredContactMethod}
Preferences{data.preferences}
Notes{data.notes}
+ {data.crmDeepLink ? ( +
+ +
+ ) : null} + - {portName} CRM + + ); +} + +export async function residentialSalesAlert( + data: ResidentialSalesAlertData, + overrides?: RenderOpts, +) { + const portName = data.portName ?? 'Port Nimara'; + const subject = `New Residential Inquiry - ${data.fullName}`; + const accent = brandingPrimaryColor(overrides?.branding); + const body = await render(, { + pretty: false, + }); + return { + subject, + html: renderShell({ title: subject, body, branding: overrides?.branding }), + }; +} diff --git a/src/lib/queue/workers/email.ts b/src/lib/queue/workers/email.ts index ad4a6f90..baf0c657 100644 --- a/src/lib/queue/workers/email.ts +++ b/src/lib/queue/workers/email.ts @@ -32,7 +32,7 @@ export const emailWorker = new Worker( const { resolveSubject } = await import('@/lib/email/resolve-subject'); const { getBrandingShell } = await import('@/lib/email/branding-resolver'); const branding = await getBrandingShell(portId); - const email = inquiryClientConfirmation( + const email = await inquiryClientConfirmation( { firstName, mooringNumber, contactEmail, portName }, { branding }, ); @@ -67,7 +67,7 @@ export const emailWorker = new Worker( const { resolveSubject } = await import('@/lib/email/resolve-subject'); const { getBrandingShell } = await import('@/lib/email/branding-resolver'); const branding = await getBrandingShell(portId); - const notification = inquirySalesNotification( + const notification = await inquirySalesNotification( { fullName, email, diff --git a/src/lib/services/crm-invite.service.ts b/src/lib/services/crm-invite.service.ts index e7d55f4d..818c3bd0 100644 --- a/src/lib/services/crm-invite.service.ts +++ b/src/lib/services/crm-invite.service.ts @@ -69,7 +69,7 @@ export async function createCrmInvite(args: { }); const link = `${env.APP_URL}/set-password?token=${raw}`; - const result = crmInviteEmail({ + const result = await crmInviteEmail({ link, ttlHours: INVITE_TTL_HOURS, recipientName: args.name, @@ -232,7 +232,7 @@ export async function resendCrmInvite( const link = `${env.APP_URL}/set-password?token=${raw}`; const branding = await getBrandingShell(meta.portId); - const result = crmInviteEmail( + const result = await crmInviteEmail( { link, ttlHours: INVITE_TTL_HOURS, diff --git a/src/lib/services/document-signing-emails.service.ts b/src/lib/services/document-signing-emails.service.ts index 5a2e5fc6..0519f867 100644 --- a/src/lib/services/document-signing-emails.service.ts +++ b/src/lib/services/document-signing-emails.service.ts @@ -151,7 +151,7 @@ export async function sendSigningInvitation(args: SigningInvitationArgs): Promis args.signerRole, ); - const { subject, html, text } = signingInvitationEmail( + const { subject, html, text } = await signingInvitationEmail( { recipientName: args.recipient.name, documentLabel: args.documentLabel, @@ -194,7 +194,7 @@ export async function sendSigningReminder(args: SigningReminderArgs): Promise sendLimit(async () => { - const { subject, html, text } = signingCompletedEmail( + const { subject, html, text } = await signingCompletedEmail( { recipientName: recipient.name, documentLabel: args.documentLabel, diff --git a/src/lib/services/notification-digest.service.ts b/src/lib/services/notification-digest.service.ts index 2a25fd5c..5f20f94e 100644 --- a/src/lib/services/notification-digest.service.ts +++ b/src/lib/services/notification-digest.service.ts @@ -138,7 +138,7 @@ export async function runNotificationDigest(now: Date = new Date()): Promise null), ]); - const { subject, html, text } = adminEmailChangeEmail( + const { subject, html, text } = await adminEmailChangeEmail( { recipientName: args.displayName, newEmail: args.newEmail,