mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
Meta WhatsApp returns error #131030 ("Recipient phone number not in allowed list") in two situations: 1. Sandbox: the recipient isn't registered as a test number. 2. The registered number format differs from what we sent, usually because of trunk-prefix 0 quirks (e.g. Lithuanian numbers stored as +370063949836 vs international +37063949836). Today that error bubbles up to the UI and the user has to manually edit the contact. This PR makes the send pipeline self-heal: on 131030 it retries with alternate phone-number formats, and if an alternate works it persists the corrected format back to the contact so future sends are a single API call. src/lib/whatsapp/phone-utils.ts - Added phoneVariants(sanitized) — yields original, then variants with a trunk 0 inserted/removed after each plausible country-code length (1-3 digits). Deduplicated. Original is always first so the fast path for correctly-formatted numbers is one attempt. - Added isRecipientNotAllowedError(message) helper — matches code 131030 or the English error text, so we only retry when the failure is actually about the recipient (other errors fail fast). src/app/api/whatsapp/send/route.ts - Reply send now iterates phoneVariants() and stops at the first that succeeds. If a non-original variant works, contacts.phone is updated to the working format. src/app/api/whatsapp/broadcast/route.ts - Same retry applied per recipient in the broadcast loop. - Also fixed a separate bug: broadcast was calling sendTemplateMessage(accessToken, phone_number_id, ...) — wrong order. Correct signature is (phoneNumberId, accessToken, to, templateName, language?, params?). This was silently rejecting every broadcast. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
154 lines
4.2 KiB
TypeScript
154 lines
4.2 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { createClient } from '@/lib/supabase/server'
|
|
import { sendTemplateMessage } from '@/lib/whatsapp/meta-api'
|
|
import { decrypt } from '@/lib/whatsapp/encryption'
|
|
import {
|
|
sanitizePhoneForMeta,
|
|
isValidE164,
|
|
phoneVariants,
|
|
isRecipientNotAllowedError,
|
|
} from '@/lib/whatsapp/phone-utils'
|
|
|
|
interface BroadcastResult {
|
|
phone: string
|
|
status: 'sent' | 'failed'
|
|
whatsapp_message_id?: string
|
|
error?: string
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const supabase = await createClient()
|
|
|
|
const {
|
|
data: { user },
|
|
error: authError,
|
|
} = await supabase.auth.getUser()
|
|
|
|
if (authError || !user) {
|
|
return NextResponse.json(
|
|
{ error: 'Unauthorized' },
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
const body = await request.json()
|
|
const { phone_numbers, template_name, template_language, template_params } = body
|
|
|
|
if (!phone_numbers || !Array.isArray(phone_numbers) || phone_numbers.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'phone_numbers array is required and must not be empty' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
if (!template_name) {
|
|
return NextResponse.json(
|
|
{ error: 'template_name is required' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// Fetch and decrypt WhatsApp config
|
|
const { data: config, error: configError } = await supabase
|
|
.from('whatsapp_config')
|
|
.select('*')
|
|
.eq('user_id', user.id)
|
|
.single()
|
|
|
|
if (configError || !config) {
|
|
return NextResponse.json(
|
|
{ error: 'WhatsApp not configured. Please set up your WhatsApp integration first.' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const accessToken = decrypt(config.access_token)
|
|
|
|
const results: BroadcastResult[] = []
|
|
let sentCount = 0
|
|
let failedCount = 0
|
|
|
|
for (const phone of phone_numbers) {
|
|
const sanitized = sanitizePhoneForMeta(phone)
|
|
|
|
if (!isValidE164(sanitized)) {
|
|
results.push({
|
|
phone,
|
|
status: 'failed',
|
|
error: 'Invalid phone number format',
|
|
})
|
|
failedCount++
|
|
continue
|
|
}
|
|
|
|
// Correct sendTemplateMessage signature is:
|
|
// (phoneNumberId, accessToken, to, templateName, language?, params?)
|
|
// Previously the args were passed in the wrong order (accessToken first,
|
|
// params before language), which made Meta reject every broadcast.
|
|
//
|
|
// Retry with phone variants on "not in allowed list" so numbers that
|
|
// differ only in a trunk-prefix 0 still reach recipients.
|
|
const variants = phoneVariants(sanitized)
|
|
let sentMessageId: string | null = null
|
|
let lastError: string | null = null
|
|
|
|
for (const variant of variants) {
|
|
try {
|
|
const result = await sendTemplateMessage(
|
|
config.phone_number_id,
|
|
accessToken,
|
|
variant,
|
|
template_name,
|
|
template_language || 'en_US',
|
|
template_params || []
|
|
)
|
|
sentMessageId = result.messageId
|
|
lastError = null
|
|
break
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : 'Unknown error'
|
|
if (!isRecipientNotAllowedError(errorMessage)) {
|
|
lastError = errorMessage
|
|
break // unrelated error — stop retrying
|
|
}
|
|
lastError = errorMessage
|
|
// retry with next variant
|
|
}
|
|
}
|
|
|
|
if (sentMessageId) {
|
|
results.push({
|
|
phone,
|
|
status: 'sent',
|
|
whatsapp_message_id: sentMessageId,
|
|
})
|
|
sentCount++
|
|
} else {
|
|
console.error(`Failed to send broadcast to ${phone}:`, lastError)
|
|
results.push({
|
|
phone,
|
|
status: 'failed',
|
|
error: lastError || 'Unknown error',
|
|
})
|
|
failedCount++
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
total: phone_numbers.length,
|
|
sent: sentCount,
|
|
failed: failedCount,
|
|
results,
|
|
})
|
|
} catch (error) {
|
|
console.error('Error in WhatsApp broadcast POST:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Failed to process broadcast' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|