import { DeliveryStatus } from "@prisma/client"; export type OutboundMessageRequest = { tenantId: string; channelId: string; channelProvider: string; phoneNumberId: string | null; to: string; content: string; messageId: string; }; export type OutboundMessageResult = { success: boolean; provider: string; deliveryStatus: DeliveryStatus; providerMessageId?: string | null; failureReason?: string; }; type MetaResponse = { messages?: Array<{ id?: string }>; error?: { message?: string; }; }; function normalizeProvider(provider: string) { return provider.trim().toLowerCase(); } function normalizeToPhone(phone: string) { return phone.replace(/\D/g, ""); } function randomMockMessageId() { if (typeof crypto !== "undefined" && "randomUUID" in crypto && typeof crypto.randomUUID === "function") { return `mock-${crypto.randomUUID()}`; } return `mock-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`; } function isMockAllowed() { const explicit = process.env.WHATSAPP_ALLOW_SIMULATED_SEND?.trim().toLowerCase(); if (process.env.NODE_ENV === "production") { return false; } if (explicit === undefined) { return true; } return explicit === "true" || explicit === "1" || explicit === "yes"; } async function sendViaMeta({ to, content, phoneNumberId }: { to: string; content: string; phoneNumberId: string; }) { const token = process.env.WHATSAPP_API_TOKEN?.trim(); const version = process.env.WHATSAPP_API_VERSION?.trim() || "v22.0"; if (!token) { return { success: false, failureReason: "WHATSAPP_API_TOKEN is not configured" }; } const normalizedTo = normalizeToPhone(to); const response = await fetch(`https://graph.facebook.com/${version}/${phoneNumberId}/messages`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ messaging_product: "whatsapp", to: normalizedTo, type: "text", text: { preview_url: false, body: content } }) }); const payloadText = await response.text(); let parsed: MetaResponse = {}; try { parsed = JSON.parse(payloadText) as MetaResponse; } catch { // ignore parse error } if (!response.ok) { return { success: false, failureReason: parsed.error?.message ? `Meta API error: ${parsed.error.message}` : `Meta API ${response.status}: ${payloadText.slice(0, 400)}` }; } const providerMessageId = parsed.messages?.[0]?.id?.trim(); if (!providerMessageId) { return { success: false, failureReason: "Meta API success response does not include message id" }; } return { success: true, providerMessageId }; } export async function sendOutboundTextMessage(input: OutboundMessageRequest): Promise { const normalized = normalizeProvider(input.channelProvider); const defaultResult: OutboundMessageResult = { success: false, provider: normalized || "unknown", deliveryStatus: DeliveryStatus.FAILED }; if (!input.phoneNumberId || !input.to || !input.content) { return { ...defaultResult, failureReason: "Missing recipient or message data" }; } if (!normalized.includes("meta")) { if (isMockAllowed()) { return { success: true, provider: "mock", deliveryStatus: DeliveryStatus.SENT, providerMessageId: randomMockMessageId() }; } return { ...defaultResult, failureReason: `Unsupported provider "${input.channelProvider}". Configure WhatsApp provider or enable simulation.` }; } const metaResult = await sendViaMeta({ to: input.to, content: input.content, phoneNumberId: input.phoneNumberId }); if (!metaResult.success) { if (!isMockAllowed()) { return { ...defaultResult, provider: "meta", failureReason: metaResult.failureReason }; } return { success: true, provider: "mock", deliveryStatus: DeliveryStatus.SENT, providerMessageId: randomMockMessageId(), failureReason: metaResult.failureReason }; } return { success: true, provider: "meta", deliveryStatus: DeliveryStatus.SENT, providerMessageId: metaResult.providerMessageId }; }