import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' import { sendTextMessage, sendTemplateMessage } from '@/lib/whatsapp/meta-api' import { decrypt } from '@/lib/whatsapp/encryption' import { sanitizePhoneForMeta, isValidE164, phoneVariants, isRecipientNotAllowedError, } from '@/lib/whatsapp/phone-utils' 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 { conversation_id, message_type, content_text, media_url, template_name, template_params, } = body if (!conversation_id || !message_type) { return NextResponse.json( { error: 'conversation_id and message_type are required' }, { status: 400 } ) } if (message_type === 'text' && !content_text) { return NextResponse.json( { error: 'content_text is required for text messages' }, { status: 400 } ) } if (message_type === 'template' && !template_name) { return NextResponse.json( { error: 'template_name is required for template messages' }, { status: 400 } ) } // Fetch conversation and contact const { data: conversation, error: convError } = await supabase .from('conversations') .select('*, contact:contacts(*)') .eq('id', conversation_id) .eq('user_id', user.id) .single() if (convError || !conversation) { return NextResponse.json( { error: 'Conversation not found' }, { status: 404 } ) } const contact = conversation.contact if (!contact?.phone) { return NextResponse.json( { error: 'Contact phone number not found' }, { status: 400 } ) } // Sanitize and validate phone const sanitizedPhone = sanitizePhoneForMeta(contact.phone) if (!isValidE164(sanitizedPhone)) { return NextResponse.json( { error: 'Invalid phone number format' }, { 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) // Send via Meta API — retry with phone-number variants if Meta rejects // with "recipient not in allowed list" (common in sandbox / when a // number was registered with/without a trunk 0). If an alternate // format succeeds, we persist it back to the contact row so the // next send goes through on the first attempt. let waMessageId = '' let workingPhone = sanitizedPhone const attempt = async (phone: string): Promise => { if (message_type === 'template') { const result = await sendTemplateMessage( config.phone_number_id, accessToken, phone, template_name, undefined, // default language (en_US) template_params || [] ) return result.messageId } const result = await sendTextMessage( config.phone_number_id, accessToken, phone, content_text ) return result.messageId } try { const variants = phoneVariants(sanitizedPhone) let lastError: unknown = null for (const variant of variants) { try { waMessageId = await attempt(variant) workingPhone = variant lastError = null break } catch (err) { const message = err instanceof Error ? err.message : String(err) // Only retry when the failure is specifically that the // recipient isn't in Meta's allowed list. Any other error // (bad token, invalid template, etc.) bubbles up immediately. if (!isRecipientNotAllowedError(message)) { throw err } lastError = err console.warn(`[whatsapp/send] variant "${variant}" rejected by Meta, trying next…`) } } if (lastError) throw lastError } catch (err) { const message = err instanceof Error ? err.message : 'Unknown Meta API error' console.error('Meta API send failed for all variants:', message) return NextResponse.json( { error: `Meta API error: ${message}` }, { status: 502 } ) } // If a non-original variant succeeded, update the contact so future // sends go straight through. sanitizePhoneForMeta on workingPhone // will yield workingPhone itself, so re-storing preserves it. if (workingPhone !== sanitizedPhone) { console.log( `[whatsapp/send] Auto-corrected contact phone: ${sanitizedPhone} → ${workingPhone}` ) await supabase .from('contacts') .update({ phone: workingPhone }) .eq('id', contact.id) } // Insert message into DB — field names MUST match the messages schema // (see supabase/migrations/001_initial_schema.sql): // conversation_id, sender_type, content_type, content_text, // media_url, template_name, message_id, status, created_at const { data: messageRecord, error: msgError } = await supabase .from('messages') .insert({ conversation_id, sender_type: 'agent', content_type: message_type, content_text: content_text || null, media_url: media_url || null, template_name: template_name || null, message_id: waMessageId, status: 'sent', }) .select() .single() if (msgError) { console.error('Error inserting sent message:', msgError) return NextResponse.json( { error: `Message sent to Meta but failed to save to DB: ${msgError.message}` }, { status: 500 } ) } // Update conversation await supabase .from('conversations') .update({ last_message_text: content_text || `[${message_type}]`, last_message_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) .eq('id', conversation_id) return NextResponse.json({ success: true, message_id: messageRecord.id, whatsapp_message_id: waMessageId, }) } catch (error) { console.error('Error in WhatsApp send POST:', error) return NextResponse.json( { error: 'Failed to send message' }, { status: 500 } ) } }