248 lines
8.4 KiB
TypeScript
248 lines
8.4 KiB
TypeScript
|
|
/**
|
|||
|
|
* Registry of every transactional template the system can emit, with a
|
|||
|
|
* pre-baked sample-prop fixture so an admin can fire a realistic
|
|||
|
|
* preview to a designated address without needing to trigger the real
|
|||
|
|
* upstream flow (a real signing send, a real portal invite, etc.).
|
|||
|
|
*
|
|||
|
|
* Consumed by `<TestTemplateCard>` (admin → Email page) and
|
|||
|
|
* `/api/v1/admin/email/test-template`. New templates land here once
|
|||
|
|
* they're plumbed; the UI dropdown reflects the registry at runtime so
|
|||
|
|
* adding an entry surfaces it without any UI change.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { activationEmail, resetEmail } from '@/lib/email/templates/portal-auth';
|
|||
|
|
import { crmInviteEmail } from '@/lib/email/templates/crm-invite';
|
|||
|
|
import { adminEmailChangeEmail } from '@/lib/email/templates/admin-email-change';
|
|||
|
|
import { notificationDigestEmail } from '@/lib/email/templates/notification-digest';
|
|||
|
|
import {
|
|||
|
|
signingInvitationEmail,
|
|||
|
|
signingCompletedEmail,
|
|||
|
|
signingReminderEmail,
|
|||
|
|
signingCancelledEmail,
|
|||
|
|
} from '@/lib/email/templates/document-signing';
|
|||
|
|
import { inquiryClientConfirmation } from '@/lib/email/templates/inquiry-client-confirmation';
|
|||
|
|
import { inquirySalesNotification } from '@/lib/email/templates/inquiry-sales-notification';
|
|||
|
|
import {
|
|||
|
|
residentialClientConfirmation,
|
|||
|
|
residentialSalesAlert,
|
|||
|
|
} from '@/lib/email/templates/residential-inquiry';
|
|||
|
|
|
|||
|
|
export type RenderedEmail = { subject: string; html: string; text?: string };
|
|||
|
|
|
|||
|
|
export interface TestTemplateMeta {
|
|||
|
|
/** Stable id - used as the dropdown value + the POST body key. */
|
|||
|
|
id: string;
|
|||
|
|
/** Human-facing dropdown label. */
|
|||
|
|
label: string;
|
|||
|
|
/** One-line description shown under the dropdown to clarify which
|
|||
|
|
* real flow fires this template in production. */
|
|||
|
|
description: string;
|
|||
|
|
/** Renders a fully-formed email with placeholder data baked in. */
|
|||
|
|
render: (sample: SampleContext) => Promise<RenderedEmail>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Shared sample fixture passed to every renderer so the previewed
|
|||
|
|
* subject/body line up with the admin's current port. Real flows
|
|||
|
|
* resolve these from DB lookups; the tester injects synthetic but
|
|||
|
|
* plausible values instead.
|
|||
|
|
*/
|
|||
|
|
export interface SampleContext {
|
|||
|
|
recipientName: string;
|
|||
|
|
recipientEmail: string;
|
|||
|
|
portName: string;
|
|||
|
|
portUrl: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const TEST_TEMPLATES: TestTemplateMeta[] = [
|
|||
|
|
{
|
|||
|
|
id: 'portal_activation',
|
|||
|
|
label: 'Portal · Activation invite',
|
|||
|
|
description: 'Fires when an admin invites a client to activate their portal account.',
|
|||
|
|
render: (s) =>
|
|||
|
|
activationEmail({
|
|||
|
|
recipientName: s.recipientName,
|
|||
|
|
portName: s.portName,
|
|||
|
|
link: `${s.portUrl}/portal/activate/sample-token`,
|
|||
|
|
ttlHours: 24,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'portal_reset',
|
|||
|
|
label: 'Portal · Password reset',
|
|||
|
|
description: 'Fires when a portal user requests a password reset link.',
|
|||
|
|
render: (s) =>
|
|||
|
|
resetEmail({
|
|||
|
|
recipientName: s.recipientName,
|
|||
|
|
portName: s.portName,
|
|||
|
|
link: `${s.portUrl}/portal/reset/sample-token`,
|
|||
|
|
ttlMinutes: 120,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'crm_invite',
|
|||
|
|
label: 'CRM · Teammate invitation',
|
|||
|
|
description: 'Fires when a super-admin invites a new teammate to the CRM.',
|
|||
|
|
render: (s) =>
|
|||
|
|
crmInviteEmail({
|
|||
|
|
recipientName: s.recipientName,
|
|||
|
|
portName: s.portName,
|
|||
|
|
isSuperAdmin: false,
|
|||
|
|
link: `${s.portUrl}/invite/sample-token`,
|
|||
|
|
ttlHours: 72,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'admin_email_change',
|
|||
|
|
label: 'CRM · Admin email change confirmation',
|
|||
|
|
description: 'Fires when an admin updates their CRM login email - confirmation step.',
|
|||
|
|
render: (s) =>
|
|||
|
|
adminEmailChangeEmail({
|
|||
|
|
recipientName: s.recipientName,
|
|||
|
|
portName: s.portName,
|
|||
|
|
newEmail: s.recipientEmail,
|
|||
|
|
changedByDisplayName: 'Sample Admin',
|
|||
|
|
loginUrl: `${s.portUrl}/login`,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'notification_digest',
|
|||
|
|
label: 'Reminders · Notification digest',
|
|||
|
|
description: 'Fires on the configured cadence (daily/weekly) with the rep’s open reminders.',
|
|||
|
|
render: (s) =>
|
|||
|
|
notificationDigestEmail({
|
|||
|
|
recipientName: s.recipientName,
|
|||
|
|
portName: s.portName,
|
|||
|
|
items: [
|
|||
|
|
{
|
|||
|
|
type: 'reminder',
|
|||
|
|
title: 'Follow up with Matthew Ciaccio on Berth A1',
|
|||
|
|
description: 'Reservation EOI sent 5 days ago - no response yet.',
|
|||
|
|
link: `${s.portUrl}/clients/sample-client-id`,
|
|||
|
|
createdAt: new Date(Date.now() - 86_400_000),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
type: 'alert',
|
|||
|
|
title: 'Berth B12 PDF parse failed',
|
|||
|
|
description: null,
|
|||
|
|
link: `${s.portUrl}/berths/sample-berth-id`,
|
|||
|
|
createdAt: new Date(Date.now() - 2 * 86_400_000),
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
totalUnread: 2,
|
|||
|
|
inboxLink: `${s.portUrl}/inbox`,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'signing_invitation',
|
|||
|
|
label: 'Documenso · Signing invitation',
|
|||
|
|
description: 'Fires when the rep dispatches the first signing-invite email for a doc.',
|
|||
|
|
render: (s) =>
|
|||
|
|
signingInvitationEmail({
|
|||
|
|
recipientName: s.recipientName,
|
|||
|
|
portName: s.portName,
|
|||
|
|
documentLabel: 'Sales Contract',
|
|||
|
|
signerRole: 'client',
|
|||
|
|
signingUrl: `${s.portUrl}/sign/sample-token`,
|
|||
|
|
senderName: 'Sample Sales Manager',
|
|||
|
|
customMessage: null,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'signing_reminder',
|
|||
|
|
label: 'Documenso · Signing reminder',
|
|||
|
|
description: 'Fires when a manual reminder is dispatched for an outstanding signer.',
|
|||
|
|
render: (s) =>
|
|||
|
|
signingReminderEmail({
|
|||
|
|
recipientName: s.recipientName,
|
|||
|
|
portName: s.portName,
|
|||
|
|
documentLabel: 'Sales Contract',
|
|||
|
|
signingUrl: `${s.portUrl}/sign/sample-token`,
|
|||
|
|
invitedAgo: '5 days ago',
|
|||
|
|
customMessage: null,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'signing_completed',
|
|||
|
|
label: 'Documenso · Fully signed notification',
|
|||
|
|
description: 'Fires when every required signer has signed and the document is complete.',
|
|||
|
|
render: (s) =>
|
|||
|
|
signingCompletedEmail({
|
|||
|
|
recipientName: s.recipientName,
|
|||
|
|
portName: s.portName,
|
|||
|
|
documentLabel: 'Sales Contract',
|
|||
|
|
clientName: s.recipientName,
|
|||
|
|
completedAt: new Date(),
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'signing_cancelled',
|
|||
|
|
label: 'Documenso · Signing cancelled',
|
|||
|
|
description: 'Fires when the rep cancels a document mid-signature with notify-recipients.',
|
|||
|
|
render: (s) =>
|
|||
|
|
signingCancelledEmail({
|
|||
|
|
recipientName: s.recipientName,
|
|||
|
|
portName: s.portName,
|
|||
|
|
documentLabel: 'Sales Contract',
|
|||
|
|
reason: 'Customer renegotiated terms; a fresh contract will follow.',
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'inquiry_client_confirmation',
|
|||
|
|
label: 'Public inquiry · Client confirmation',
|
|||
|
|
description: 'Fires when a public-site visitor submits the contact form (their copy).',
|
|||
|
|
render: (s) =>
|
|||
|
|
inquiryClientConfirmation({
|
|||
|
|
firstName: s.recipientName.split(' ')[0] ?? s.recipientName,
|
|||
|
|
mooringNumber: 'A1',
|
|||
|
|
contactEmail: 'sales@portnimara.com',
|
|||
|
|
portName: s.portName,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'inquiry_sales_notification',
|
|||
|
|
label: 'Public inquiry · Sales notification',
|
|||
|
|
description: 'Fires alongside the client confirmation - alerts the sales rep to a new lead.',
|
|||
|
|
render: (s) =>
|
|||
|
|
inquirySalesNotification({
|
|||
|
|
fullName: s.recipientName,
|
|||
|
|
email: s.recipientEmail,
|
|||
|
|
phone: '+1 555 0100',
|
|||
|
|
mooringNumber: 'A1',
|
|||
|
|
crmUrl: `${s.portUrl}/clients/sample-client-id`,
|
|||
|
|
portName: s.portName,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'residential_client_confirmation',
|
|||
|
|
label: 'Residential inquiry · Client confirmation',
|
|||
|
|
description: 'Fires when a residential-site visitor submits the contact form.',
|
|||
|
|
render: (s) =>
|
|||
|
|
residentialClientConfirmation({
|
|||
|
|
firstName: s.recipientName.split(' ')[0] ?? s.recipientName,
|
|||
|
|
contactEmail: 'sales@portnimara.com',
|
|||
|
|
portName: s.portName,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'residential_sales_alert',
|
|||
|
|
label: 'Residential inquiry · Sales alert',
|
|||
|
|
description: 'Fires alongside the residential client confirmation - alerts the sales team.',
|
|||
|
|
render: (s) =>
|
|||
|
|
residentialSalesAlert({
|
|||
|
|
fullName: s.recipientName,
|
|||
|
|
email: s.recipientEmail,
|
|||
|
|
phone: '+1 555 0100',
|
|||
|
|
placeOfResidence: 'Monaco',
|
|||
|
|
preferredContactMethod: 'email',
|
|||
|
|
notes: 'Looking for year-round mooring + marina apartment access.',
|
|||
|
|
crmDeepLink: `${s.portUrl}/residential/clients/sample-id`,
|
|||
|
|
portName: s.portName,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
export function findTestTemplate(id: string): TestTemplateMeta | undefined {
|
|||
|
|
return TEST_TEMPLATES.find((t) => t.id === id);
|
|||
|
|
}
|