feat(intake): CRM-owned website inquiry emails + in-app notifications

Flag-gated (website_intake_email_enabled, default OFF) sending of registrant confirmation + staff alert for inquiries captured at /api/public/website-inquiries, reusing the branded berth + residential templates and adding contact-form client-confirmation + sales-alert templates. In-app (bell) notifications fire on every fresh capture, independent of the flag. Recipients resolve from the existing inquiry_/residential_notification_recipients settings; fires only on a fresh (non-deduped) insert so retries never re-send.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 17:22:08 +02:00
parent f699533224
commit 990b566eff
9 changed files with 675 additions and 1 deletions

View File

@@ -16,6 +16,11 @@ import {
} from '@/lib/errors';
import { logger } from '@/lib/logger';
import { checkRateLimit, rateLimiters } from '@/lib/rate-limit';
import {
isWebsiteIntakeEmailEnabled,
notifyWebsiteSubmissionInApp,
sendWebsiteSubmissionEmails,
} from '@/lib/services/website-intake-email.service';
/**
* POST /api/public/website-inquiries
@@ -169,6 +174,40 @@ export async function POST(req: NextRequest) {
},
'website inquiry captured',
);
// In-app (bell) notifications for reps - always on a fresh capture,
// independent of email ownership, so inquiries surface in the CRM inbox.
void notifyWebsiteSubmissionInApp({
portId: port.id,
portSlug: parsed.port_slug,
kind: parsed.kind,
submissionId: parsed.submission_id,
payload: parsed.payload,
}).catch((err) =>
logger.error(
{ err, submissionId: parsed.submission_id },
'Failed to create website-intake notifications',
),
);
// Flag-gated CRM-owned emails (registrant confirmation + staff alert).
// Fire only on this fresh-insert branch so a redelivery never re-sends.
// Inline fire-and-forget: a send failure must not 500 the capture POST.
if (await isWebsiteIntakeEmailEnabled(port.id)) {
void sendWebsiteSubmissionEmails({
portId: port.id,
portSlug: parsed.port_slug,
kind: parsed.kind,
submissionId: parsed.submission_id,
payload: parsed.payload,
}).catch((err) =>
logger.error(
{ err, submissionId: parsed.submission_id },
'Failed to send website-intake emails',
),
);
}
// L34 carve-out: deliberate bespoke `{ id, deduped }` shape (NOT the
// `{ data }` envelope). This is the public website's intake contract —
// the external marketing site reads `id`/`deduped` off the JSON root.