chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
This commit is contained in:
247
src/lib/email/test-registry.ts
Normal file
247
src/lib/email/test-registry.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user