Files
whatsapp/api/send_broadcast.php
T
2026-03-16 18:36:11 -05:00

343 lines
14 KiB
PHP

<?php
/**
* API - Envío masivo de mensajes
* Fecha: 13 de noviembre de 2025
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
// Asegurar que la tabla de historial existe
try {
Database::getInstance()->getConnection()->exec("
CREATE TABLE IF NOT EXISTS broadcast_history (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
sent_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
message_type VARCHAR(20) NOT NULL DEFAULT 'text',
selection_type VARCHAR(20) NOT NULL DEFAULT 'filter',
filter_used VARCHAR(50) NULL,
template_name VARCHAR(120) NULL,
message_preview TEXT NULL,
total_users INT UNSIGNED NOT NULL DEFAULT 0,
sent_count INT UNSIGNED NOT NULL DEFAULT 0,
error_count INT UNSIGNED NOT NULL DEFAULT 0,
sent_by INT UNSIGNED NULL,
sent_by_name VARCHAR(120) NULL,
INDEX idx_sent_at (sent_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
} catch (Exception $_e) { /* silencioso si ya existe */ }
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Método no permitido']);
exit;
}
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
http_response_code(400);
echo json_encode(['error' => 'Datos requeridos']);
exit;
}
$selectionType = $input['selection_type'] ?? 'filter';
$messageType = $input['message_type'] ?? 'text';
$db = Database::getInstance();
$whatsappService = new WhatsAppService();
// Obtener usuarios según tipo de selección
$users = [];
if ($selectionType === 'specific') {
// Usuarios específicos seleccionados
if (!isset($input['user_ids']) || !is_array($input['user_ids']) || empty($input['user_ids'])) {
http_response_code(400);
echo json_encode(['error' => 'Debes seleccionar al menos un usuario']);
exit;
}
$userIds = array_map('intval', $input['user_ids']);
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
$query = "SELECT id, phone_number, name FROM users WHERE id IN ($placeholders) ORDER BY id ASC";
$users = $db->fetchAll($query, $userIds);
} else {
// Por filtro
$filter = $input['filter'] ?? 'all';
$whereClause = "1=1";
switch ($filter) {
case 'active':
$whereClause .= " AND EXISTS (
SELECT 1 FROM conversations c
WHERE c.user_id = u.id
AND c.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
)";
break;
case 'recent':
$whereClause .= " AND u.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)";
break;
case 'all':
// Incluir todos los usuarios
break;
}
$query = "SELECT id, phone_number, name FROM users u WHERE " . $whereClause . " ORDER BY id ASC";
$users = $db->fetchAll($query);
}
if (empty($users)) {
http_response_code(404);
echo json_encode([
'error' => 'No se encontraron usuarios para enviar',
'selection_type' => $selectionType
]);
exit;
}
$sentCount = 0;
$errorCount = 0;
$errors = [];
$debugWamids = [];
foreach ($users as $user) {
try {
$response = null;
$messageContent = '';
// Enviar según tipo de mensaje
if ($messageType === 'template') {
// Enviar plantilla
if (!isset($input['template_id'])) {
throw new Exception('ID de plantilla requerido');
}
$templateId = intval($input['template_id']);
$template = $db->fetch("SELECT * FROM message_templates WHERE id = ?", [$templateId]);
if (!$template) {
throw new Exception('Plantilla no encontrada');
}
$status = strtoupper($template['status']);
if ($status !== 'APPROVED') {
throw new Exception('La plantilla no está aprobada. Status: ' . $template['status']);
}
// Preparar variables
$templateVariables = $input['template_variables'] ?? [];
error_log('📝 Variables recibidas: ' . json_encode($templateVariables));
error_log('🔍 Tipo: ' . gettype($templateVariables));
// Detectar si son variables numéricas o con nombres
$isNumericVars = false;
$isNamedVars = false;
if (is_array($templateVariables) && !empty($templateVariables)) {
$firstKey = array_key_first($templateVariables);
if (is_int($firstKey) && isset($templateVariables[0])) {
// Array indexado numéricamente [0 => "val1", 1 => "val2"]
$isNumericVars = true;
error_log('✅ Variables tipo: Array numérico indexado');
} elseif (is_string($firstKey) && !is_numeric($firstKey)) {
// Array asociativo con nombres ["nombre_tema" => "val1", "fecha" => "val2"]
$isNamedVars = true;
error_log('✅ Variables tipo: Objeto con nombres (asociativo)');
} elseif (is_numeric($firstKey)) {
// Array con claves numéricas [1 => "val1", 2 => "val2"]
$isNumericVars = true;
error_log('✅ Variables tipo: Array con claves numéricas');
}
}
// Procesar según el tipo
$processedVariables = [];
if ($isNamedVars) {
// Variables con nombres: pasar como objeto asociativo
$processedVariables = $templateVariables;
error_log('📦 Enviando como objeto con nombres: ' . json_encode($processedVariables));
} elseif ($isNumericVars) {
// Variables numéricas: ordenar y convertir a array indexado
ksort($templateVariables, SORT_NUMERIC);
foreach ($templateVariables as $value) {
$processedVariables[] = $value;
}
error_log('📊 Enviando como array ordenado: ' . json_encode($processedVariables));
} else {
error_log('⚠️ Sin variables o formato desconocido');
}
error_log('📤 Variables procesadas: ' . json_encode($processedVariables));
// Verificar variables si la plantilla usa formato numérico
$expectedVars = preg_match_all('/\{\{(\d+)\}\}/', $template['body_text'] ?? '', $matches);
if ($expectedVars > 0 && $isNumericVars && count($processedVariables) !== $expectedVars) {
throw new Exception("La plantilla requiere {$expectedVars} variable(s) numéricas, pero se enviaron " . count($processedVariables));
}
// Preparar header de imagen dinámica si se proporcionó
$headerParams = [];
if (!empty($input['header_image_url'])) {
$imageUrl = filter_var($input['header_image_url'], FILTER_VALIDATE_URL) ? $input['header_image_url'] : null;
if ($imageUrl) {
$headerParams = [['type' => 'image', 'image' => ['link' => $imageUrl]]];
error_log("Broadcast template header image URL: " . $imageUrl);
}
}
$response = $whatsappService->sendTemplateMessage(
$user['phone_number'],
$template['template_name'] ?? $template['name'],
$template['language_code'] ?? 'es',
$processedVariables,
$headerParams
);
// Log payload para diagnóstico
$dryPayload = $whatsappService->sendTemplateMessage(
$user['phone_number'],
$template['template_name'] ?? $template['name'],
$template['language_code'] ?? 'es',
$processedVariables,
$headerParams,
null, null, true // dryRun=true
);
error_log("=== BROADCAST TEMPLATE PAYLOAD (para " . $user['phone_number'] . ") ===");
error_log(json_encode($dryPayload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
error_log("=== FIN PAYLOAD ===");
// Construir contenido para BD (reemplazar variables en body_text)
$messageContent = $template['body_text'] ?? $template['name'];
if ($isNamedVars) {
// Reemplazar por nombres
foreach ($templateVariables as $key => $value) {
$messageContent = str_replace('{{' . $key . '}}', $value, $messageContent);
}
} else {
// Reemplazar por índice numérico
foreach ($processedVariables as $index => $value) {
$messageContent = str_replace('{{' . ($index + 1) . '}}', $value, $messageContent);
}
}
} else {
// Enviar texto simple
$message = trim($input['message'] ?? '');
if (empty($message)) {
throw new Exception('Mensaje vacío');
}
$response = $whatsappService->sendTextMessage($user['phone_number'], $message);
$messageContent = $message;
}
// La API de WhatsApp devuelve {"messaging_product":"whatsapp","messages":[{"id":"wamid.xxx"}]}
// No tiene clave "success" — verificar por la presencia de messages o contacts
$sentMessageId = $response['messages'][0]['id'] ?? null;
$isSuccess = $response && (isset($response['messages']) || isset($response['contacts']) || $sentMessageId);
if ($isSuccess) {
$sentCount++;
$debugWamids[] = [
'phone' => $user['phone_number'],
'wamid' => $sentMessageId,
'wa_status' => $response['messages'][0]['message_status'] ?? 'accepted',
'wa_contact' => $response['contacts'][0]['wa_id'] ?? null,
];
// Guardar mensaje en BD
$db->insert('conversations', [
'user_id' => $user['id'],
'message_id' => $sentMessageId,
'content' => $messageContent,
'direction' => 'outgoing',
'message_type' => $messageType === 'template' ? 'template' : 'text',
'status' => 'sent',
'created_at' => date('Y-m-d H:i:s')
]);
} else {
$errorCount++;
$errors[] = [
'phone' => $user['phone_number'],
'error' => 'Sin respuesta de WhatsApp: ' . json_encode($response)
];
}
// Pausa para no saturar la API (250ms entre mensajes)
usleep(250000);
} catch (Exception $e) {
$errorCount++;
$errors[] = [
'phone' => $user['phone_number'],
'name' => $user['name'] ?? 'Sin nombre',
'error' => $e->getMessage()
];
error_log("Error sending broadcast to {$user['phone_number']}: " . $e->getMessage());
}
}
// --- Guardar historial de broadcast ---
try {
$adminId = (int)($_SESSION['admin_user']['id'] ?? $_SESSION['user_id'] ?? 0);
$adminName = $_SESSION['admin_user']['name'] ?? $_SESSION['username'] ?? 'Sistema';
$tplName = null;
$preview = '';
if ($messageType === 'template' && isset($template)) {
$tplName = $template['template_name'] ?? $template['name'] ?? null;
$preview = mb_substr($template['body'] ?? $tplName ?? '', 0, 200);
} else {
$preview = mb_substr($input['message'] ?? '', 0, 200);
}
$db->insert('broadcast_history', [
'sent_at' => date('Y-m-d H:i:s'),
'message_type' => $messageType,
'selection_type' => $selectionType,
'filter_used' => ($selectionType === 'filter') ? ($input['filter'] ?? 'all') : null,
'template_name' => $tplName,
'message_preview' => $preview,
'total_users' => count($users),
'sent_count' => $sentCount,
'error_count' => $errorCount,
'sent_by' => $adminId ?: null,
'sent_by_name' => $adminName ?: null,
]);
} catch (Exception $eHist) {
error_log('broadcast_history insert failed: ' . $eHist->getMessage());
}
// --- Fin historial ---
echo json_encode([
'success' => true,
'sent_count' => $sentCount,
'error_count' => $errorCount,
'total_users' => count($users),
'selection_type' => $selectionType,
'message_type' => $messageType,
'errors' => $errorCount > 0 ? $errors : null,
'debug_wamids' => $debugWamids ?? []
]);
} catch (Exception $e) {
error_log("Error in send_broadcast.php: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Error interno del servidor: ' . $e->getMessage()]);
}
?>