/** * 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 `` (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 type { BrandingShell } from '@/lib/email/shell'; 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; } /** * 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; /** Per-port branding shell (logo, blur background, accent color, header/footer * HTML). Resolved once by the test-template route via getBrandingShell and * forwarded into every template so previews match the production look. * Null is acceptable - templates fall back to neutral defaults. */ branding: BrandingShell | null; } 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, }, { branding: s.branding }, ), }, { 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, }, { branding: s.branding }, ), }, { 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, }, { branding: s.branding }, ), }, { 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`, }, { branding: s.branding }, ), }, { 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`, }, { branding: s.branding }, ), }, { 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, }, { branding: s.branding }, ), }, { 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, }, { branding: s.branding }, ), }, { 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(), }, { branding: s.branding }, ), }, { 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.', }, { branding: s.branding }, ), }, { 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, }, { branding: s.branding }, ), }, { 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, }, { branding: s.branding }, ), }, { 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, }, { branding: s.branding }, ), }, { 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, }, { branding: s.branding }, ), }, ]; export function findTestTemplate(id: string): TestTemplateMeta | undefined { return TEST_TEMPLATES.find((t) => t.id === id); }