All files / lib/communication whatsapp-board-invite.ts

100% Statements 18/18
100% Branches 16/16
100% Functions 1/1
100% Lines 18/18

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 108 109 110 111 112 113 114 115 116 117 118 119 120                                                                                  6x 6x   6x         2x 1x     1x           1x     1x             4x   4x                             4x                 4x 4x   4x 2x     2x             2x       6x            
import { WhatsAppAdapter } from "./adapters/whatsapp.adapter";
import type {
	AdapterErrorCode,
	CommunicationQueueMessage,
	WhatsAppContent,
} from "./types";
 
export type BoardInviteResult =
	| { status: "sent"; actualRecipient: string; provider: string }
	| {
			status: "failed";
			errorCode: AdapterErrorCode;
			errorMessage: string;
	  };
 
/**
 * Send a mood-board share invite via WhatsApp.
 *
 * Mirrors whatsapp-otp.ts: routes through WhatsAppAdapter so non-prod safety
 * guards (override number + dry-run in local/dev) apply. Falls back to
 * console logging when WHATSAPP_ACCESS_TOKEN is unset so dev envs never
 * fail on this flow.
 *
 * The template is expected to take 3 body params:
 *   {{1}} inviter first name
 *   {{2}} board name
 *   {{3}} invite URL
 *
 * Returns a discriminated result so callers can translate adapter errorCode
 * into the right HTTP status + user-facing message instead of receiving an
 * opaque thrown Error.
 */
export async function sendWhatsAppBoardInvite(
	env: CloudflareBindings,
	params: {
		toPhone: string; // E.164, e.g. "+919876543210"
		inviterFirstName: string;
		boardName: string;
		url: string;
	},
): Promise<BoardInviteResult> {
	const accessToken = env.WHATSAPP_ACCESS_TOKEN;
	const templateName = env.WHATSAPP_INVITE_TEMPLATE ?? "board_invite_v1";
 
	if (!accessToken) {
		// In production, a missing token MUST surface as failure — otherwise the
		// route returns 200 and the UI lies "Sent on WhatsApp" while nothing
		// was delivered. Local/dev still console-fallback so devs without
		// credentials aren't blocked.
		if (env.ENVIRONMENT === "production") {
			console.error(
				"[WhatsApp BoardInvite] WHATSAPP_ACCESS_TOKEN missing in production",
			);
			return {
				status: "failed",
				errorCode: "config",
				errorMessage: "WhatsApp not configured",
			};
		}
		console.log(
			`[WhatsApp BoardInvite] Phone: ${params.toPhone} | Board: "${params.boardName}" | URL: ${params.url} | Provider: console (no WhatsApp credentials)`,
		);
		return {
			status: "sent",
			actualRecipient: params.toPhone,
			provider: "console",
		};
	}
 
	const strippedPhone = params.toPhone.replace("+", "");
 
	const content: WhatsAppContent = {
		templateName,
		languageCode: "en",
		components: [
			{
				type: "body",
				parameters: [
					{ type: "text", text: params.inviterFirstName },
					{ type: "text", text: params.boardName },
					{ type: "text", text: params.url },
				],
			},
		],
	};
 
	const message: CommunicationQueueMessage = {
		logId: 0, // Not queued — transactional direct send, no log entry yet
		channel: "whatsapp",
		recipient: strippedPhone,
		eventType: "team_invitation",
		content,
		transactional: true,
	};
 
	const adapter = new WhatsAppAdapter();
	const result = await adapter.send(message, env);
 
	if (result.status === "failed") {
		console.error(
			`[WhatsApp BoardInvite] Failed (${result.errorCode ?? "unknown"}): ${result.errorMessage}`,
		);
		return {
			status: "failed",
			errorCode: result.errorCode ?? "unknown",
			errorMessage: result.errorMessage ?? "Unknown delivery error",
		};
	}
 
	console.log(
		`[WhatsApp BoardInvite] Sent to ${result.actualRecipient} (provider: ${result.provider}${result.externalId ? `, message_id: ${result.externalId}` : ""})`,
	);
 
	return {
		status: "sent",
		actualRecipient: result.actualRecipient,
		provider: result.provider,
	};
}