Multi-tenant branding admin (/admin/branding) was saving 5 settings
that no code read — every port's emails shipped Port Nimara's logo
and color regardless. Now wired end-to-end:
New shared infrastructure:
- src/lib/email/shell.ts — renderShell() + brandingPrimaryColor()
helpers; takes BrandingShell { logoUrl, primaryColor,
emailHeaderHtml, emailFooterHtml }, falls back to Port Nimara
defaults when null.
- src/lib/email/branding-resolver.ts — getBrandingShell(portId)
thin wrapper over getPortBrandingConfig() that returns null on
error / missing portId so senders never break on misconfig.
All 6 transactional templates refactored to use renderShell + the
shared accent color; portName now flows through every template
(crm-invite, portal activation/reset, both inquiries, both
residential templates, notification digest).
All 6 senders pass branding via getBrandingShell:
- portal-auth.service.ts (activation + reset)
- crm-invite.service.ts (resend path; create-invite has no portId
yet so falls through to defaults)
- email worker (inquiry confirmation + sales notification)
- residential-inquiries route (client confirmation + sales alert)
- notification-digest.service.ts (digest)
BrandedAuthShell takes an optional `branding` prop with logoUrl +
appName (parent page server-fetches via getPortBrandingConfig).
Defaults to Port Nimara if omitted, so single-tenant deployments
are unaffected.
1175/1175 vitest passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71 lines
2.5 KiB
TypeScript
71 lines
2.5 KiB
TypeScript
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 = `
|
|
<p style="margin-bottom:10px; font-size:16px;">Dear ${escapeHtml(firstName)},</p>
|
|
<p style="margin-bottom:10px; font-size:16px;">
|
|
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.
|
|
</p>
|
|
<p style="margin-bottom:10px; font-size:16px;">
|
|
If you have any questions, please feel free to reach out to us at
|
|
<a href="mailto:${escapeHtml(contactEmail)}" style="color:${accent}; text-decoration:underline;">${escapeHtml(contactEmail)}</a>.
|
|
</p>
|
|
<p style="font-size:16px;">
|
|
Best regards,<br />
|
|
The ${escapeHtml(portName)} Sales Team
|
|
</p>`;
|
|
|
|
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, '"')
|
|
.replace(/'/g, ''');
|
|
}
|