All files / lib/rrm signup-notify.ts

100% Statements 15/15
100% Branches 12/12
100% Functions 2/2
100% Lines 13/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107                                                                                                        87x 69x                         14x 14x   13x       13x 13x 13x 14x     14x                 14x                         14x   1x      
/**
 * Sign-up notification email (review §3.1, "nobody is told").
 *
 * A landing-page submission that becomes — or updates — a prospect otherwise
 * sits unseen until an operator happens to open `/admin/rrm/signups`; a 9pm
 * sign-up waits until morning. This sends one email per real sign-up so it
 * doesn't.
 *
 * Reuses the same email-provider path as `routes/pro/feedback.routes.ts`
 * (`createEmailService` → whichever provider the factory selects) rather
 * than a second notification channel.
 *
 * The caller (`routes/internal/rrm-submissions.routes.ts`) fires this inside
 * `c.executionCtx.waitUntil(...)` so the visitor's redirect is never delayed.
 * Errors here are therefore logged and swallowed, never thrown — a failed
 * notification must never surface as a failed submission, and nothing is
 * awaiting this promise's rejection anyway.
 *
 * NEVER logs the phone number. `phoneE164` is the only phone-shaped value
 * this module touches, and it goes into the email body only.
 */
import type { RRM_CTAS, RRM_LOCALES } from "../../db/schema/enums";
import { escapeHtml } from "../html-escape";
import { logger } from "../logger";
 
type RrmCta = (typeof RRM_CTAS)[number];
type RrmLocale = (typeof RRM_LOCALES)[number];
 
export type NotifySignupInput = {
	cta: RrmCta;
	/** The agent's own name, as typed. */
	name: string;
	firmName?: string;
	/** E.164 with the leading '+'. Recomputed by the caller — never guessed here. */
	phoneE164: string;
	lang: RrmLocale;
	/** `have_contact` only: the referred contact, as typed. */
	contactName?: string;
	contactPhone?: string;
	contactProject?: string;
	/**
	 * "Already referred by X on Y — first submission wins.", present only
	 * when this referral duplicates an earlier one (review §2.2). Unlike the
	 * `rrm_prospect_events` payload, this email is not an append-only audit
	 * record, so naming the earlier submitter here is fine.
	 */
	duplicateNote?: string;
	/** The `?src=` slug this submission carried, if any (review §1.3). */
	sourceDetail?: string;
};
 
function row(label: string, value: string | undefined): string {
	if (!value) return "";
	return `<p style="margin:0 0 8px;"><strong>${escapeHtml(label)}:</strong> ${escapeHtml(value)}</p>`;
}
 
/**
 * Emails `env.RRM_NOTIFY_EMAIL` (falling back to `FEEDBACK_NOTIFY_EMAIL`) one
 * message per sign-up. Skips silently when neither is configured — a public
 * form's notification path must never become a reason it fails.
 */
export async function notifySignup(
	env: CloudflareBindings,
	_ctx: ExecutionContext,
	input: NotifySignupInput,
): Promise<void> {
	const to = env.RRM_NOTIFY_EMAIL ?? env.FEEDBACK_NOTIFY_EMAIL;
	if (!to) return;
 
	try {
		// Loaded lazily: ../email pulls @react-email/render (prettier, html-to-text,
		// entities, react). Evaluating that at module scope costs startup CPU on every
		// cold isolate, including the ones that never send an email.
		const { createEmailService } = await import("../email");
		const emailService = createEmailService(env);
		const kind = input.cta === "have_contact" ? "Referral" : "Interested";
		const subject = `New partner sign-up: ${input.name} (${kind})`;
 
		const referralRows =
			input.cta === "have_contact"
				? row("Contact name", input.contactName) +
					row("Contact number", input.contactPhone) +
					row("Contact project", input.contactProject) +
					(input.duplicateNote
						? `<p style="margin:0 0 8px;">${escapeHtml(input.duplicateNote)}</p>`
						: "")
				: "";
 
		const html = `
			<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:480px;margin:0 auto;padding:24px;">
				${row("Name", input.name)}
				${row("Firm", input.firmName)}
				${row("WhatsApp", input.phoneE164)}
				${row("Language", input.lang)}
				${row("Submitted", kind)}
				${referralRows}
				${row("Source", input.sourceDetail)}
				<p style="margin:16px 0 0;">Open: /admin/rrm/signups</p>
			</div>
		`;
 
		await emailService.sendRawEmail(to, subject, html);
	} catch (err) {
		logger.error("[RRM signup-notify] email failed:", err);
	}
}