All files / lib/communication whatsapp-otp.ts

100% Statements 15/15
87.5% Branches 7/8
100% Functions 1/1
100% Lines 15/15

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                                        9x   9x     1x     1x         8x 8x   9x                                 9x                 9x 9x   8x 2x     2x         6x       9x          
import { WhatsAppAdapter } from "./adapters/whatsapp.adapter";
import type { CommunicationQueueMessage, WhatsAppContent } from "./types";
import { normalizeToE164 } from "@interioring/utils/validation/phone";
 
/**
 * Send OTP via WhatsApp, routed through WhatsAppAdapter for safety guards.
 *
 * In non-prod environments, the adapter redirects non-allowlisted numbers
 * to the configured override number (see env-config.ts).
 * In local/dev + redirected, the adapter performs a dry run (no API call).
 *
 * Falls back to console logging if WHATSAPP_ACCESS_TOKEN is not configured.
 *
 * Returns delivery metadata so callers can log actualRecipient and provider.
 */
export async function sendWhatsAppOtp(
	env: CloudflareBindings,
	phoneNumber: string,
	code: string,
): Promise<{ actualRecipient: string; provider: string }> {
	const accessToken = env.WHATSAPP_ACCESS_TOKEN;
 
	if (!accessToken) {
		// DEV ONLY: Console fallback logs OTP code for local testing.
		// In production, WHATSAPP_ACCESS_TOKEN must be set as a secret.
		console.log(
			`[WhatsApp OTP] Phone: ${phoneNumber} | Code: ${code} | Provider: console (no WhatsApp credentials)`,
		);
		return { actualRecipient: phoneNumber, provider: "console" };
	}
 
	// WhatsApp Cloud API expects digits-only with country code (e.g., "919876543210").
	// Normalize first so any input format (E.164, formatted, raw) ends up consistent.
	const e164 = normalizeToE164(phoneNumber);
	const strippedPhone = e164 ? e164.slice(1) : phoneNumber.replace(/\D/g, "");
 
	const content: WhatsAppContent = {
		templateName: "interioring_auth_otp",
		languageCode: "en",
		components: [
			{
				type: "body",
				parameters: [{ type: "text", text: code }],
			},
			{
				type: "button",
				sub_type: "url",
				index: 0,
				parameters: [{ type: "text", text: code }],
			},
		],
	};
 
	const message: CommunicationQueueMessage = {
		logId: 0, // Not queued — no log entry yet
		channel: "whatsapp",
		recipient: strippedPhone,
		eventType: "otp_verification",
		content,
		transactional: true,
	};
 
	const adapter = new WhatsAppAdapter();
	const result = await adapter.send(message, env);
 
	if (result.status === "failed") {
		console.error(
			`[WhatsApp OTP] Failed: ${result.errorMessage}`,
		);
		throw new Error(
			`WhatsApp OTP delivery failed: ${result.errorMessage}`,
		);
	}
 
	console.log(
		`[WhatsApp OTP] Sent to ${result.actualRecipient} (provider: ${result.provider}${result.externalId ? `, message_id: ${result.externalId}` : ""})`,
	);
 
	return {
		actualRecipient: result.actualRecipient,
		provider: result.provider,
	};
}