Compare commits
1 Commits
cutover/we
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| caaebd77fa |
@@ -141,9 +141,17 @@ const KNOWN_SETTINGS: Array<{
|
||||
},
|
||||
{
|
||||
key: 'inquiry_contact_email',
|
||||
label: 'Inquiry Contact Email',
|
||||
label: 'Berth & residence reply-to email',
|
||||
description:
|
||||
'Reply-to email shown in client confirmation emails when a new interest is registered',
|
||||
'Public "reach out to us at …" address shown to clients in berth + residence inquiry confirmation emails. Defaults to sales@portnimara.com when blank.',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
key: 'contact_form_contact_email',
|
||||
label: 'Contact-form reply-to email',
|
||||
description:
|
||||
'Public "reach out to us at …" address shown to clients in contact-form confirmation emails. Defaults to hello@portnimara.com when blank.',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
},
|
||||
|
||||
@@ -128,6 +128,10 @@ export async function sendEmail(
|
||||
// the safety net.
|
||||
cc?: string | string[],
|
||||
bcc?: string | string[],
|
||||
// Optional per-message Reply-To. Overrides the port's `email_reply_to`
|
||||
// setting (`cfg.replyTo`) when provided — used so client inquiry
|
||||
// confirmations reply to the public sales@/hello@ inbox, not the noreply From.
|
||||
replyTo?: string,
|
||||
): Promise<nodemailer.SentMessageInfo> {
|
||||
const cfg = portId ? await getPortEmailConfig(portId) : null;
|
||||
const transporter = cfg ? createTransporterFromConfig(cfg) : createTransporter();
|
||||
@@ -150,13 +154,14 @@ export async function sendEmail(
|
||||
`Port Nimara CRM <noreply@${env.SMTP_HOST}>`;
|
||||
|
||||
const resolvedAttachments = await resolveAttachments(attachments, portId);
|
||||
const effectiveReplyTo = replyTo ?? cfg?.replyTo ?? undefined;
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: fromHeader,
|
||||
to: effectiveTo,
|
||||
subject: effectiveSubject,
|
||||
html,
|
||||
...(cfg?.replyTo ? { replyTo: cfg.replyTo } : {}),
|
||||
...(effectiveReplyTo ? { replyTo: effectiveReplyTo } : {}),
|
||||
...(text ? { text } : {}),
|
||||
...(effectiveCc ? { cc: effectiveCc } : {}),
|
||||
...(effectiveBcc ? { bcc: effectiveBcc } : {}),
|
||||
|
||||
@@ -27,18 +27,13 @@ function ClientConfirmationBody({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '18px', fontWeight: 'bold', color: accent }}>
|
||||
Thank you for getting in touch
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>Dear {firstName},</Text>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
|
||||
Thank you for contacting {portName}. We have received your message and a member of our team
|
||||
will be in touch with you shortly.
|
||||
</Text>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px', lineHeight: '1.5' }}>
|
||||
Dear {firstName},
|
||||
</Text>
|
||||
<Text style={{ marginBottom: '20px', fontSize: '16px', lineHeight: '1.5' }}>
|
||||
Thank you for reaching out to {portName}. We have received your message and a member of our
|
||||
team will be in touch with you shortly.
|
||||
</Text>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px', lineHeight: '1.5' }}>
|
||||
If anything else comes to mind in the meantime, please write to us at{' '}
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
|
||||
If you have any questions in the meantime, please feel free to reach out to us at{' '}
|
||||
<Link
|
||||
href={safeUrl(`mailto:${contactEmail}`)}
|
||||
style={{ color: accent, textDecoration: 'underline' }}
|
||||
@@ -47,10 +42,10 @@ function ClientConfirmationBody({
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
<Text style={{ fontSize: '16px', marginTop: '30px' }}>
|
||||
With warm regards,
|
||||
<Text style={{ fontSize: '16px' }}>
|
||||
Best regards,
|
||||
<br />
|
||||
<strong>The {portName} Team</strong>
|
||||
The {portName} Team
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
@@ -60,10 +55,10 @@ export async function contactFormClientConfirmation(
|
||||
data: ContactFormClientConfirmationData,
|
||||
overrides?: RenderOpts,
|
||||
) {
|
||||
const portName = data.portName ?? 'our team';
|
||||
const portName = data.portName ?? 'Port Nimara';
|
||||
const subject = overrides?.subject?.trim()
|
||||
? overrides.subject
|
||||
: `Thank you for contacting ${portName}`;
|
||||
: `${portName} — Thank You for Contacting Us`;
|
||||
const accent = brandingPrimaryColor(overrides?.branding);
|
||||
const body = await render(
|
||||
<ClientConfirmationBody
|
||||
@@ -74,8 +69,19 @@ export async function contactFormClientConfirmation(
|
||||
/>,
|
||||
{ pretty: false },
|
||||
);
|
||||
const text = [
|
||||
`Dear ${data.firstName},`,
|
||||
'',
|
||||
`Thank you for contacting ${portName}. We have received your message and a member of our team will be in touch with you shortly.`,
|
||||
'',
|
||||
`If you have any questions in the meantime, please feel free to reach out to us at ${data.contactEmail}.`,
|
||||
'',
|
||||
'Best regards,',
|
||||
`The ${portName} Team`,
|
||||
].join('\n');
|
||||
return {
|
||||
subject,
|
||||
html: renderShell({ title: subject, body, branding: overrides?.branding }),
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ function ClientConfirmationBody({
|
||||
<>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>Dear {firstName},</Text>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
|
||||
Thank you for your interest in {berthText}. We've noted your enquiry, and a member of
|
||||
our team will be in touch shortly through your preferred channel with the details
|
||||
you've requested.
|
||||
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.
|
||||
</Text>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
|
||||
Should anything come to mind in the meantime, please don't hesitate to write to us at{' '}
|
||||
If you have any questions, please feel free to reach out to us at{' '}
|
||||
<Link
|
||||
href={safeUrl(`mailto:${contactEmail}`)}
|
||||
style={{ color: accent, textDecoration: 'underline' }}
|
||||
@@ -47,7 +47,7 @@ function ClientConfirmationBody({
|
||||
.
|
||||
</Text>
|
||||
<Text style={{ fontSize: '16px' }}>
|
||||
With warm regards,
|
||||
Best regards,
|
||||
<br />
|
||||
The {portName} Sales Team
|
||||
</Text>
|
||||
@@ -61,12 +61,10 @@ export async function inquiryClientConfirmation(
|
||||
) {
|
||||
const { firstName, mooringNumber, contactEmail } = data;
|
||||
const portName = data.portName ?? 'Port Nimara';
|
||||
const berthText = mooringNumber ? `Berth ${mooringNumber}` : `a ${portName} Berth`;
|
||||
const berthText = mooringNumber ? `Berth ${mooringNumber}` : 'a Berth';
|
||||
const subject = overrides?.subject?.trim()
|
||||
? overrides.subject
|
||||
: mooringNumber
|
||||
? `Thank you for your interest in Berth ${mooringNumber}`
|
||||
: `Thank you for your interest in ${portName}`;
|
||||
: `${portName} — Thank You for Your Interest`;
|
||||
const accent = brandingPrimaryColor(overrides?.branding);
|
||||
|
||||
const body = await render(
|
||||
@@ -83,11 +81,11 @@ export async function inquiryClientConfirmation(
|
||||
const text = [
|
||||
`Dear ${firstName},`,
|
||||
'',
|
||||
`Thank you for your interest in ${berthText}. We've noted your enquiry, and a member of our team will be in touch shortly through your preferred channel with the details you've requested.`,
|
||||
`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.`,
|
||||
'',
|
||||
`Should anything come to mind in the meantime, please don't hesitate to write to us at ${contactEmail}.`,
|
||||
`If you have any questions, please feel free to reach out to us at ${contactEmail}.`,
|
||||
'',
|
||||
'With warm regards,',
|
||||
'Best regards,',
|
||||
`The ${portName} Sales Team`,
|
||||
].join('\n');
|
||||
|
||||
|
||||
@@ -16,14 +16,13 @@ export interface ResidentialClientConfirmationData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable list of the residence types a lead selected, e.g.
|
||||
* Human-readable phrase for the residence types a lead selected, e.g.
|
||||
* "the Two Bedroom Marina Villa and the Four Bedroom Oceanfront Villa".
|
||||
* Falls back to a generic phrase when nothing was selected so the copy
|
||||
* always reads naturally.
|
||||
* Mirrors the website's phrasing, including its generic fallback.
|
||||
*/
|
||||
function residencePhrase(portName: string, types: string[] | undefined): string {
|
||||
const list = (types ?? []).filter(Boolean);
|
||||
if (list.length === 0) return `the residences at ${portName}`;
|
||||
if (list.length === 0) return `a ${portName} Residence`;
|
||||
if (list.length === 1) return `the ${list[0]}`;
|
||||
if (list.length === 2) return `the ${list[0]} and the ${list[1]}`;
|
||||
return `the ${list.slice(0, -1).join(', the ')}, and the ${list[list.length - 1]}`;
|
||||
@@ -44,19 +43,14 @@ function ClientConfirmationBody({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '18px', fontWeight: 'bold', color: accent }}>
|
||||
Welcome to {portName}
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>Dear {firstName},</Text>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
|
||||
Thank you for expressing interest in {residencePhraseText}. Our team has registered your
|
||||
interest, and we will reach out to you very shortly by your preferred method of contact with
|
||||
more information.
|
||||
</Text>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px', lineHeight: '1.5' }}>
|
||||
Dear {firstName},
|
||||
</Text>
|
||||
<Text style={{ marginBottom: '20px', fontSize: '16px', lineHeight: '1.5' }}>
|
||||
Thank you for your interest in {residencePhraseText}. Our residential sales team has
|
||||
received your enquiry, and a member of the team will be in touch shortly with the details
|
||||
you've requested.
|
||||
</Text>
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px', lineHeight: '1.5' }}>
|
||||
Should anything come to mind in the meantime, please don't hesitate to write to us at{' '}
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
|
||||
If you have any questions, please feel free to reach out to us at{' '}
|
||||
<Link
|
||||
href={safeUrl(`mailto:${contactEmail}`)}
|
||||
style={{ color: accent, textDecoration: 'underline' }}
|
||||
@@ -65,10 +59,10 @@ function ClientConfirmationBody({
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
<Text style={{ fontSize: '16px', marginTop: '30px' }}>
|
||||
With warm regards,
|
||||
<Text style={{ fontSize: '16px' }}>
|
||||
Best regards,
|
||||
<br />
|
||||
<strong>The {portName} Residential Team</strong>
|
||||
The {portName} Residences Team
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
@@ -78,10 +72,10 @@ export async function residentialClientConfirmation(
|
||||
data: ResidentialClientConfirmationData,
|
||||
overrides?: RenderOpts,
|
||||
) {
|
||||
const portName = data.portName ?? 'our team';
|
||||
const portName = data.portName ?? 'Port Nimara';
|
||||
const subject = overrides?.subject?.trim()
|
||||
? overrides.subject
|
||||
: `Thank you for your interest in ${portName} Residences`;
|
||||
: `${portName} — Thank You for Your Interest`;
|
||||
const accent = brandingPrimaryColor(overrides?.branding);
|
||||
const residencePhraseText = residencePhrase(portName, data.residenceTypes);
|
||||
const body = await render(
|
||||
@@ -97,12 +91,12 @@ export async function residentialClientConfirmation(
|
||||
const text = [
|
||||
`Dear ${data.firstName},`,
|
||||
'',
|
||||
`Thank you for your interest in ${residencePhraseText}. Our residential sales team has received your enquiry, and a member of the team will be in touch shortly with the details you've requested.`,
|
||||
`Thank you for expressing interest in ${residencePhraseText}. Our team has registered your interest, and we will reach out to you very shortly by your preferred method of contact with more information.`,
|
||||
'',
|
||||
`Should anything come to mind in the meantime, please don't hesitate to write to us at ${data.contactEmail}.`,
|
||||
`If you have any questions, please feel free to reach out to us at ${data.contactEmail}.`,
|
||||
'',
|
||||
'With warm regards,',
|
||||
`The ${portName} Residential Team`,
|
||||
'Best regards,',
|
||||
`The ${portName} Residences Team`,
|
||||
].join('\n');
|
||||
return {
|
||||
subject,
|
||||
@@ -120,6 +114,11 @@ export interface ResidentialSalesAlertData {
|
||||
preferredContactMethod?: 'email' | 'phone';
|
||||
notes?: string;
|
||||
preferences?: string;
|
||||
/**
|
||||
* Accepted for backwards-compat with the legacy `/api/public/residential-inquiries`
|
||||
* route, but intentionally NOT rendered: residential alerts go to external
|
||||
* recipients and must never mention the CRM.
|
||||
*/
|
||||
crmDeepLink?: string;
|
||||
portName?: string;
|
||||
}
|
||||
@@ -130,15 +129,7 @@ function formatPreferredContact(method: 'email' | 'phone' | undefined): string |
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function SalesAlertBody({
|
||||
portName,
|
||||
data,
|
||||
accent,
|
||||
}: {
|
||||
portName: string;
|
||||
data: ResidentialSalesAlertData;
|
||||
accent: string;
|
||||
}) {
|
||||
function SalesAlertBody({ portName, data }: { portName: string; data: ResidentialSalesAlertData }) {
|
||||
const detailStyle = { margin: '0 0 0', fontSize: '16px' } as const;
|
||||
const residenceTypes = (data.residenceTypes ?? []).filter(Boolean);
|
||||
const preferredContact = formatPreferredContact(data.preferredContactMethod);
|
||||
@@ -185,19 +176,7 @@ function SalesAlertBody({
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
{data.crmDeepLink ? (
|
||||
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
|
||||
Open the{' '}
|
||||
<Link
|
||||
href={safeUrl(data.crmDeepLink)}
|
||||
style={{ color: accent, textDecoration: 'underline' }}
|
||||
>
|
||||
{portName} CRM
|
||||
</Link>{' '}
|
||||
to follow up.
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={{ fontSize: '16px' }}>- {portName} CRM</Text>
|
||||
<Text style={{ fontSize: '16px' }}>- {portName} Residences</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -206,12 +185,11 @@ export async function residentialSalesAlert(
|
||||
data: ResidentialSalesAlertData,
|
||||
overrides?: RenderOpts,
|
||||
) {
|
||||
const portName = data.portName ?? 'our team';
|
||||
const portName = data.portName ?? 'Port Nimara';
|
||||
const subject = overrides?.subject?.trim()
|
||||
? overrides.subject
|
||||
: `New residential enquiry - ${data.fullName}`;
|
||||
const accent = brandingPrimaryColor(overrides?.branding);
|
||||
const body = await render(<SalesAlertBody portName={portName} data={data} accent={accent} />, {
|
||||
const body = await render(<SalesAlertBody portName={portName} data={data} />, {
|
||||
pretty: false,
|
||||
});
|
||||
|
||||
@@ -231,10 +209,7 @@ export async function residentialSalesAlert(
|
||||
...(data.preferences ? [`Preferences: ${data.preferences}`] : []),
|
||||
...(data.notes ? [`Comments: ${data.notes}`] : []),
|
||||
'',
|
||||
...(data.crmDeepLink
|
||||
? [`Open the ${portName} CRM (${data.crmDeepLink}) to follow up.`, '']
|
||||
: []),
|
||||
`- ${portName} CRM`,
|
||||
`- ${portName} Residences`,
|
||||
].join('\n');
|
||||
|
||||
return {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { and, eq, isNull, or } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { ports } from '@/lib/db/schema/ports';
|
||||
import { systemSettings } from '@/lib/db/schema/system';
|
||||
import { sendEmail } from '@/lib/email';
|
||||
import { getBrandingShell } from '@/lib/email/branding-resolver';
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
} from '@/lib/email/templates/residential-inquiry';
|
||||
import { contactFormSalesAlert } from '@/lib/email/templates/contact-form-alert';
|
||||
import { contactFormClientConfirmation } from '@/lib/email/templates/contact-form-client-confirmation';
|
||||
import { getPortBrandingConfig, getPortEmailConfig } from '@/lib/services/port-config';
|
||||
import { resolveNotificationRecipients } from '@/lib/services/notification-recipients';
|
||||
import { extractInquiryFields } from '@/lib/services/website-intake-fields';
|
||||
import { createNotification } from '@/lib/services/notifications.service';
|
||||
@@ -77,13 +77,36 @@ export async function sendWebsiteSubmissionEmails(
|
||||
const { portId, portSlug, kind, payload } = input;
|
||||
const fields = extractInquiryFields(payload);
|
||||
|
||||
const [branding, portBrand, emailCfg] = await Promise.all([
|
||||
const [branding, portRow] = await Promise.all([
|
||||
getBrandingShell(portId),
|
||||
getPortBrandingConfig(portId).catch(() => null),
|
||||
getPortEmailConfig(portId).catch(() => null),
|
||||
db.select({ name: ports.name }).from(ports).where(eq(ports.id, portId)).limit(1),
|
||||
]);
|
||||
const portName = portBrand?.appName ?? 'Port Nimara';
|
||||
const contactEmail = emailCfg?.fromAddress ?? 'sales@portnimara.com';
|
||||
// Client-facing copy uses the PUBLIC port name ("Port Nimara"), never the CRM
|
||||
// appName ("Port Nimara CRM") which is reserved for internal/staff surfaces.
|
||||
const portName = portRow[0]?.name ?? 'Port Nimara';
|
||||
|
||||
// Public reply-to shown to clients in confirmation emails ("reach out to us
|
||||
// at ..."). Admin-configurable per category via system_settings; contact-form
|
||||
// enquiries default to the hello@ general inbox, berth + residence to sales@.
|
||||
// Never the noreply From address.
|
||||
const contactEmailKey =
|
||||
kind === 'contact_form' ? 'contact_form_contact_email' : 'inquiry_contact_email';
|
||||
const contactEmailDefault =
|
||||
kind === 'contact_form' ? 'hello@portnimara.com' : 'sales@portnimara.com';
|
||||
const [contactRow] = await db
|
||||
.select({ value: systemSettings.value })
|
||||
.from(systemSettings)
|
||||
.where(
|
||||
and(
|
||||
eq(systemSettings.key, contactEmailKey),
|
||||
or(eq(systemSettings.portId, portId), isNull(systemSettings.portId)),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
const contactEmail =
|
||||
typeof contactRow?.value === 'string' && contactRow.value.trim()
|
||||
? contactRow.value.trim()
|
||||
: contactEmailDefault;
|
||||
// No interest/client row exists for a raw submission, so link to the
|
||||
// dashboard rather than a (nonexistent) entity detail page.
|
||||
const crmUrl = `${process.env.APP_URL ?? ''}/${portSlug}`;
|
||||
@@ -116,6 +139,10 @@ export async function sendWebsiteSubmissionEmails(
|
||||
undefined,
|
||||
confirmation.text,
|
||||
portId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
contactEmail,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,6 +199,10 @@ export async function sendWebsiteSubmissionEmails(
|
||||
undefined,
|
||||
confirmation.text,
|
||||
portId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
contactEmail,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,7 +217,6 @@ export async function sendWebsiteSubmissionEmails(
|
||||
preferredContactMethod: fields.preferredContact ?? undefined,
|
||||
placeOfResidence: fields.placeOfResidence ?? undefined,
|
||||
notes: fields.comments ?? undefined,
|
||||
crmDeepLink: crmUrl,
|
||||
portName,
|
||||
},
|
||||
{ branding },
|
||||
@@ -217,7 +247,18 @@ export async function sendWebsiteSubmissionEmails(
|
||||
fallback: confirmation.subject,
|
||||
tokens: { portName, recipientName: fields.firstName },
|
||||
});
|
||||
await sendEmail(fields.email, subject, confirmation.html, undefined, undefined, portId);
|
||||
await sendEmail(
|
||||
fields.email,
|
||||
subject,
|
||||
confirmation.html,
|
||||
undefined,
|
||||
confirmation.text,
|
||||
portId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
contactEmail,
|
||||
);
|
||||
}
|
||||
|
||||
// Contact-form alerts go to their own recipient list (the website routed
|
||||
|
||||
52
tests/unit/email/client-confirmations.test.ts
Normal file
52
tests/unit/email/client-confirmations.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { inquiryClientConfirmation } from '@/lib/email/templates/inquiry-client-confirmation';
|
||||
import { contactFormClientConfirmation } from '@/lib/email/templates/contact-form-client-confirmation';
|
||||
|
||||
// Note: assert prose that spans an interpolation on the plain-text part — the
|
||||
// React-Email HTML renderer inserts `<!-- -->` markers at value boundaries.
|
||||
|
||||
describe('inquiryClientConfirmation (berth)', () => {
|
||||
it('mirrors the website copy for a specific berth + signs off as Sales', async () => {
|
||||
const { subject, html, text } = await inquiryClientConfirmation({
|
||||
firstName: 'Jane',
|
||||
mooringNumber: 'D13',
|
||||
contactEmail: 'sales@portnimara.com',
|
||||
portName: 'Port Nimara',
|
||||
});
|
||||
expect(subject).toBe('Port Nimara — Thank You for Your Interest');
|
||||
expect(text).toContain('Thank you for expressing interest in Berth D13');
|
||||
expect(text).toContain('Our team has registered your interest');
|
||||
expect(text).toContain('reach out to us at sales@portnimara.com');
|
||||
expect(text).toContain('The Port Nimara Sales Team');
|
||||
expect(html).toContain('sales@portnimara.com');
|
||||
expect(html).not.toContain('Port Nimara CRM');
|
||||
});
|
||||
|
||||
it('uses "a Berth" when no mooring is given', async () => {
|
||||
const { text, html } = await inquiryClientConfirmation({
|
||||
firstName: 'Jane',
|
||||
mooringNumber: null,
|
||||
contactEmail: 'sales@portnimara.com',
|
||||
portName: 'Port Nimara',
|
||||
});
|
||||
expect(text).toContain('Thank you for expressing interest in a Berth');
|
||||
expect(html).not.toContain('Port Nimara CRM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('contactFormClientConfirmation', () => {
|
||||
it('mirrors the website copy + signs off as the Port Nimara Team', async () => {
|
||||
const { subject, html, text } = await contactFormClientConfirmation({
|
||||
firstName: 'Bob',
|
||||
contactEmail: 'hello@portnimara.com',
|
||||
portName: 'Port Nimara',
|
||||
});
|
||||
expect(subject).toBe('Port Nimara — Thank You for Contacting Us');
|
||||
expect(text).toContain('Thank you for contacting Port Nimara');
|
||||
expect(text).toContain('We have received your message');
|
||||
expect(text).toContain('reach out to us at hello@portnimara.com');
|
||||
expect(text).toContain('The Port Nimara Team');
|
||||
expect(html).not.toContain('Port Nimara CRM');
|
||||
});
|
||||
});
|
||||
@@ -6,30 +6,36 @@ import {
|
||||
} from '@/lib/email/templates/residential-inquiry';
|
||||
|
||||
describe('residentialClientConfirmation', () => {
|
||||
it('reflects the chosen residence types in the thank-you copy', async () => {
|
||||
const { html, text } = await residentialClientConfirmation({
|
||||
it('mirrors the website copy + reflects the chosen residence types', async () => {
|
||||
const { subject, html, text } = await residentialClientConfirmation({
|
||||
firstName: 'Mia',
|
||||
contactEmail: 'sales@portnimara.com',
|
||||
residenceTypes: ['Two Bedroom Marina Villa', 'Five Bedroom Oceanfront Villa'],
|
||||
portName: 'Port Nimara',
|
||||
});
|
||||
expect(html).toContain('the Two Bedroom Marina Villa and the Five Bedroom Oceanfront Villa');
|
||||
expect(text).toContain('the Two Bedroom Marina Villa and the Five Bedroom Oceanfront Villa');
|
||||
expect(html).toContain('Mia');
|
||||
expect(subject).toBe('Port Nimara — Thank You for Your Interest');
|
||||
expect(text).toContain(
|
||||
'Thank you for expressing interest in the Two Bedroom Marina Villa and the Five Bedroom Oceanfront Villa',
|
||||
);
|
||||
expect(text).toContain('Our team has registered your interest');
|
||||
expect(text).toContain('The Port Nimara Residences Team');
|
||||
// Never leak the CRM brand name to a client.
|
||||
expect(html).not.toContain('Port Nimara CRM');
|
||||
});
|
||||
|
||||
it('falls back to a generic phrase when no types are selected', async () => {
|
||||
it('falls back to the website generic phrase when no types are selected', async () => {
|
||||
const { html } = await residentialClientConfirmation({
|
||||
firstName: 'Sam',
|
||||
contactEmail: 'sales@portnimara.com',
|
||||
portName: 'Port Nimara',
|
||||
});
|
||||
expect(html).toContain('the residences at Port Nimara');
|
||||
expect(html).toContain('a Port Nimara Residence');
|
||||
expect(html).not.toContain('Port Nimara CRM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('residentialSalesAlert', () => {
|
||||
it('renders residence type(s) + preferred contact + comments in the detail-line format', async () => {
|
||||
it('renders residence type(s) + preferred contact + comments, with NO CRM mention', async () => {
|
||||
const { html, text } = await residentialSalesAlert({
|
||||
fullName: 'Mia Ng',
|
||||
email: 'mia@example.com',
|
||||
@@ -37,21 +43,22 @@ describe('residentialSalesAlert', () => {
|
||||
residenceTypes: ['Two Bedroom Marina Villa'],
|
||||
preferredContactMethod: 'phone',
|
||||
notes: 'Looking for a winter completion.',
|
||||
crmDeepLink: 'https://crm.portnimara.com/port-nimara',
|
||||
portName: 'Port Nimara',
|
||||
});
|
||||
// Uniform with the berth/contact alerts: friendly intro + bold detail lines + CRM link.
|
||||
expect(html).toContain('A new residential enquiry has come in');
|
||||
expect(html).toContain('Residence type(s):');
|
||||
expect(html).toContain('Two Bedroom Marina Villa');
|
||||
expect(html).toContain('Preferred contact:');
|
||||
expect(html).toContain('Phone call back');
|
||||
expect(html).toContain('Looking for a winter completion.');
|
||||
expect(html).toContain('to follow up');
|
||||
// Plain-text part mirrors the other alerts.
|
||||
expect(text).toContain('- Port Nimara Residences');
|
||||
// Residential internal alerts must not mention the CRM (recipient is external).
|
||||
expect(html).not.toContain('CRM');
|
||||
expect(html).not.toContain('to follow up');
|
||||
expect(text).toContain('Residence type(s): Two Bedroom Marina Villa');
|
||||
expect(text).toContain('Preferred contact: Phone call back');
|
||||
expect(text).toContain('Comments: Looking for a winter completion.');
|
||||
expect(text).not.toContain('CRM');
|
||||
});
|
||||
|
||||
it('omits optional rows cleanly when absent', async () => {
|
||||
|
||||
Reference in New Issue
Block a user