fix(intake): client inquiry emails mirror website copy + never show "Port Nimara CRM"
Client-facing confirmation emails now:
- use the PUBLIC port name ("Port Nimara" via ports.name), never the CRM
appName ("Port Nimara CRM") which is reserved for internal/staff surfaces
- mirror the website's wording verbatim ("Thank you for expressing
interest…", "Best regards,") and drop the CRM-style headings
- sign off per category: berth → "Port Nimara Sales Team", contact →
"Port Nimara Team", residential → "Port Nimara Residences Team"
- show + reply-to a public contact address, admin-configurable per category
(inquiry_contact_email → sales@ for berth/residence,
contact_form_contact_email → hello@ for contact form), never the noreply From
Internal alerts keep the CRM detail-line format + link (name fixed to
"Port Nimara"), EXCEPT the residential alert which drops all CRM mention
(it reaches an external recipient) and signs "- Port Nimara Residences".
sendEmail gains an optional per-message replyTo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2qc3xZTfif7N4Wq3QDa8X
This commit is contained in:
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