All files / lib/whatsapp queue-handler.ts

100% Statements 22/22
100% Branches 13/13
100% Functions 1/1
100% Lines 22/22

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                          11x 1x 1x 2x   1x                           10x 10x 10x   10x 12x 12x   12x         1x 1x 11x           10x 8x   10x 6x       10x   2x 2x        
import { createDal } from "../../dal";
import { getDb } from "../../db";
import { CampaignService } from "../../services/whatsapp/campaign.service";
import { isWhatsAppSendable, requireWhatsAppClient } from "./client";
import type { WhatsAppQueueMessage } from "./types";
 
export async function handleWhatsAppQueue(
	batch: MessageBatch<WhatsAppQueueMessage>,
	env: CloudflareBindings,
) {
	// `isWhatsAppSendable`, not the raw token: local dry-run stands in for a
	// token, so a laptop exercises the real queue path instead of acking the
	// batch two steps short of it and proving nothing.
	if (!isWhatsAppSendable(env)) {
		console.error("[WA_QUEUE] WhatsApp not configured, skipping batch");
		for (const msg of batch.messages) {
			msg.ack();
		}
		return;
	}
 
	// The same factory every other send seam uses: the dry-run client locally,
	// the live one everywhere else. This was a hand-rolled
	// `createWhatsAppClient` with neither `dryRun` nor a safety guard, so a
	// campaign launched from dev or preview messaged its real recipient list.
	//
	// It is still only HALF of the guard, deliberately: whether a given message
	// may reach Meta depends on ITS recipient, which is per-message, so the
	// per-batch client cannot decide it. `CampaignService.processQueueMessage`
	// applies `applySafetyGuard` and substitutes a dry-run client for this one
	// when the rule says so — see the comment there for why that decision has
	// to sit next to the send rather than up here.
	const client = requireWhatsAppClient(env);
	const db = getDb(env.DB);
	const dal = createDal(db);
 
	for (const msg of batch.messages) {
		try {
			const data = msg.body;
 
			if (
				data.type === "campaign_send" &&
				data.campaignId &&
				data.recipientId
			) {
				const campaignService = new CampaignService(dal);
				await campaignService.processQueueMessage(data, client, env);
			} else if (data.type === "reply") {
				// NOTE: replies are sent to whoever wrote in, with no safety
				// guard applied to `data.payload.to`. Locally that is covered by
				// the dry-run client above; on dev/preview it is not, and the
				// fix belongs where the reply payload is built
				// (`routes/webhooks/whatsapp.routes.ts`), not here.
				const result = await client.sendRaw(data.payload);
				const wamid = result.messages[0]?.id;
 
				if (data.conversationId && wamid) {
					await dal.waMessages.updateStatus(wamid, "sent");
				}
			}
 
			msg.ack();
		} catch (error) {
			console.error("[WA_QUEUE] Processing error:", error);
			msg.retry();
		}
	}
}