From b1961f0116cd006ea835d93c323fda14c0144c13 Mon Sep 17 00:00:00 2001 From: lizandrogd <77708265+lizandrogd@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:22:29 -0500 Subject: [PATCH] up --- api/debug_webhook_init.php | 16 ++ api/delete_conversation.php | 30 +++ api/delete_template.php | 25 +++ api/get_conversations.php | 32 +-- api/get_templates.php | 29 +-- api/get_user_messages.php | 9 +- api/mark_conversation_read.php | 22 ++ api/send_message.php | 39 +--- api/update_user.php | 49 +++-- api/webhook.php | 28 ++- assets/js/app.js | 10 +- conversations.php | 260 ++++++++++++++++++++--- logs/system.log | 12 ++ scripts/add_conversation_fields.php | 36 ++++ scripts/add_message_is_read.php | 15 ++ scripts/add_reaction_to_message_type.php | 26 +++ scripts/add_user_avatar_column.php | 15 ++ scripts/db_debug.php | 10 + scripts/db_test.php | 13 ++ scripts/debug_init_service.php | 24 +++ scripts/describe_conversations.php | 7 + scripts/describe_users.php | 7 + scripts/run_webhook_get.php | 24 +++ scripts/run_webhook_out.txt | 0 scripts/test_reaction_and_reply.php | 121 +++++++++++ scripts/test_require.php | 12 ++ scripts/wa_out.txt | Bin 0 -> 1670 bytes scripts/wa_service_test.php | 16 ++ scripts/webhook_test_cli.php | 13 ++ services/BotService.php | 10 +- services/WhatsAppService.php | 11 + tests/webhook_tests.php | 121 ++++++++++- tests/webhook_tests_out.txt | Bin 0 -> 264 bytes tests/whatsapp_service_tests.php | 86 ++++++++ tests/whatsapp_service_tests_out.txt | Bin 0 -> 318 bytes 35 files changed, 1006 insertions(+), 122 deletions(-) create mode 100644 api/debug_webhook_init.php create mode 100644 api/delete_conversation.php create mode 100644 api/delete_template.php create mode 100644 api/mark_conversation_read.php create mode 100644 scripts/add_conversation_fields.php create mode 100644 scripts/add_message_is_read.php create mode 100644 scripts/add_reaction_to_message_type.php create mode 100644 scripts/add_user_avatar_column.php create mode 100644 scripts/db_debug.php create mode 100644 scripts/db_test.php create mode 100644 scripts/debug_init_service.php create mode 100644 scripts/describe_conversations.php create mode 100644 scripts/describe_users.php create mode 100644 scripts/run_webhook_get.php create mode 100644 scripts/run_webhook_out.txt create mode 100644 scripts/test_reaction_and_reply.php create mode 100644 scripts/test_require.php create mode 100644 scripts/wa_out.txt create mode 100644 scripts/wa_service_test.php create mode 100644 scripts/webhook_test_cli.php create mode 100644 tests/webhook_tests_out.txt create mode 100644 tests/whatsapp_service_tests.php create mode 100644 tests/whatsapp_service_tests_out.txt diff --git a/api/debug_webhook_init.php b/api/debug_webhook_init.php new file mode 100644 index 0000000..acc2d72 --- /dev/null +++ b/api/debug_webhook_init.php @@ -0,0 +1,16 @@ + true, 'message' => 'Webhook class instantiated successfully']); +} catch (Throwable $t) { + http_response_code(500); + error_log('[debug_webhook_init] Throwable: ' . $t->getMessage()); + error_log($t->getTraceAsString()); + echo json_encode(['ok' => false, 'error' => $t->getMessage(), 'trace' => $t->getTraceAsString()]); +} diff --git a/api/delete_conversation.php b/api/delete_conversation.php new file mode 100644 index 0000000..9b06884 --- /dev/null +++ b/api/delete_conversation.php @@ -0,0 +1,30 @@ + false, 'error' => 'conversation_id or user_id required']); + exit; +} + +try { + $db = Database::getInstance(); + if ($conversationId) { + $db->execute('DELETE FROM conversations WHERE id = ?', [$conversationId]); + } + if ($userId) { + $db->execute('DELETE FROM conversations WHERE user_id = ?', [$userId]); + } + echo json_encode(['success' => true]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/delete_template.php b/api/delete_template.php new file mode 100644 index 0000000..3e6b421 --- /dev/null +++ b/api/delete_template.php @@ -0,0 +1,25 @@ + false, 'error' => 'id is required']); + exit; +} + +try { + $db = Database::getInstance(); + $db->execute("DELETE FROM message_templates WHERE id = ?", [$id]); + echo json_encode(['success' => true]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/get_conversations.php b/api/get_conversations.php index 6350c1b..48688df 100644 --- a/api/get_conversations.php +++ b/api/get_conversations.php @@ -14,21 +14,29 @@ header('Access-Control-Allow-Headers: Content-Type'); try { $db = Database::getInstance(); - // Obtener conversaciones de la tabla conversations + // Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar $conversations = $db->fetchAll( "SELECT - c.id, - c.user_id, - COALESCE(c.content, '') as content, - c.direction, - c.message_type, - c.status, - c.created_at, + u.id AS user_id, + COALESCE(u.name, u.phone_number) AS name, u.phone_number, - COALESCE(u.name, u.phone_number) as name - FROM conversations c - JOIN users u ON c.user_id = u.id - ORDER BY c.created_at DESC + u.avatar_url, + lm.message_id AS last_message_id, + lm.content AS last_message, + lm.direction AS direction, + lm.message_type AS message_type, + lm.created_at AS last_time, + IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) AS unread_count + FROM users u + LEFT JOIN conversations c ON c.user_id = u.id + LEFT JOIN ( + SELECT t1.* FROM conversations t1 + JOIN ( + SELECT user_id, MAX(created_at) AS last_time FROM conversations GROUP BY user_id + ) t2 ON t1.user_id = t2.user_id AND t1.created_at = t2.last_time + ) lm ON lm.user_id = u.id + GROUP BY u.id + ORDER BY lm.created_at DESC LIMIT 500" ); diff --git a/api/get_templates.php b/api/get_templates.php index a8b88c9..62e0e11 100644 --- a/api/get_templates.php +++ b/api/get_templates.php @@ -24,18 +24,23 @@ header('Access-Control-Allow-Headers: Content-Type'); try { $db = Database::getInstance(); - $templates = $db->fetchAll( - "SELECT - id, - name, - template_name, - language_code, - category, - status, - created_at - FROM message_templates - ORDER BY name ASC" - ); + $approvedOnly = isset($_GET['approved_only']) && $_GET['approved_only'] == '1'; + $limit = isset($_GET['limit']) ? intval($_GET['limit']) : null; + + $sql = "SELECT id, name, template_name, language_code, category, status, body_text, created_at FROM message_templates"; + $params = []; + + if ($approvedOnly) { + $sql .= " WHERE status = 'approved'"; + } + + $sql .= " ORDER BY name ASC"; + + if ($limit && $limit > 0) { + $sql .= " LIMIT " . $limit; + } + + $templates = $db->fetchAll($sql, $params); echo json_encode([ 'success' => true, diff --git a/api/get_user_messages.php b/api/get_user_messages.php index 83f41b2..6ed0327 100644 --- a/api/get_user_messages.php +++ b/api/get_user_messages.php @@ -37,7 +37,8 @@ try { direction, message_type, status, - created_at + created_at, + COALESCE(c.is_read, 0) as is_read FROM conversations c LEFT JOIN users u ON c.user_id = u.id WHERE user_id = :user_id @@ -46,10 +47,14 @@ try { id, user_id, message_text as content, + NULL as message_id, + NULL as media_url, + NULL as user_phone, direction, message_type, status, - created_at + created_at, + 1 as is_read FROM messages WHERE user_id = :user_id ORDER BY created_at ASC", diff --git a/api/mark_conversation_read.php b/api/mark_conversation_read.php new file mode 100644 index 0000000..c46145f --- /dev/null +++ b/api/mark_conversation_read.php @@ -0,0 +1,22 @@ + false, 'error' => 'user_id is required']); + exit; + } + + $db = Database::getInstance(); + $db->execute("UPDATE conversations SET is_read = 1 WHERE user_id = ? AND direction = 'incoming' AND (is_read IS NULL OR is_read = 0)", [$userId]); + + echo json_encode(['success' => true]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/send_message.php b/api/send_message.php index c66b1c7..4f446cd 100644 --- a/api/send_message.php +++ b/api/send_message.php @@ -371,13 +371,8 @@ try { $messageContent = "Plantilla: {$templateName}"; } - // Guardar en la tabla conversations - $stmt = $db->query( - "INSERT INTO conversations (user_id, content, direction, message_type, status, message_id, created_at) VALUES (?, ?, 'outgoing', ?, 'sent', ?, NOW())", - [$userId, $messageContent, $messageType, $response['messages'][0]['id'] ?? null] - ); - - error_log("Mensaje guardado en BD - User ID: $userId, Content: $messageContent"); + // Guardado en BD manejado por WhatsAppService->saveOutgoingMessage() + error_log("Outgoing message saving delegated to WhatsAppService - User ID: $userId, Content: $messageContent"); } else { error_log("Error: No se pudo obtener o crear user_id para recipient: $recipient"); } @@ -476,35 +471,7 @@ try { $response = $whatsappService->sendTextMessage($user['phone_number'], $message); if ($response && isset($response['messages']) && !empty($response['messages'])) { - // Guardar mensaje en la base de datos - $messageData = [ - 'user_id' => $userId, - 'content' => $message, - 'direction' => 'outgoing', - 'message_type' => 'text', - 'status' => 'sent', - 'message_id' => $response['messages'][0]['id'] ?? null, - 'created_at' => date('Y-m-d H:i:s') - ]; - - // Intentar insertar en conversations primero - try { - $stmt = $db->query( - "INSERT INTO conversations (user_id, content, direction, message_type, status, message_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", - array_values($messageData) - ); - } catch (Exception $e) { - // Si falla, intentar en messages - try { - $stmt = $db->query( - "INSERT INTO messages (user_id, message_text, direction, message_type, status, message_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", - [$userId, $message, 'outgoing', 'text', 'sent', $response['messages'][0]['id'] ?? null, date('Y-m-d H:i:s')] - ); - } catch (Exception $e2) { - error_log("Error guardando mensaje: " . $e2->getMessage()); - } - } - + // Saving handled by WhatsAppService->saveOutgoingMessage(), skip manual insert to avoid duplicates echo json_encode([ 'success' => true, 'message' => 'Mensaje enviado correctamente', diff --git a/api/update_user.php b/api/update_user.php index f01ebb7..0e0effa 100644 --- a/api/update_user.php +++ b/api/update_user.php @@ -42,12 +42,13 @@ try { throw new Exception('Datos inválidos'); } - // Validar datos requeridos - if (!isset($data['user_id']) || empty($data['user_id'])) { - throw new Exception('ID de usuario requerido'); + // Validar datos requeridos: user_id o phone + if ((!isset($data['user_id']) || empty($data['user_id'])) && (!isset($data['phone']) || empty($data['phone']))) { + throw new Exception('ID de usuario o teléfono requerido'); } - $user_id = intval($data['user_id']); + $user_id = isset($data['user_id']) ? intval($data['user_id']) : null; + $phone = isset($data['phone']) ? trim($data['phone']) : null; $name = isset($data['name']) && !empty(trim($data['name'])) ? trim($data['name']) : null; $status = isset($data['status']) ? $data['status'] : 'active'; @@ -60,20 +61,34 @@ try { // Crear conexión a la base de datos usando el patrón Singleton $db = Database::getInstance(); - // Verificar si el usuario existe - $checkUser = $db->fetch("SELECT id FROM users WHERE id = :user_id", ['user_id' => $user_id]); - - if (!$checkUser) { - throw new Exception('Usuario no encontrado'); - } + // Si tenemos user_id - actualizar + if ($user_id) { + // Verificar si el usuario existe + $checkUser = $db->fetch("SELECT id FROM users WHERE id = :user_id", ['user_id' => $user_id]); + + if (!$checkUser) { + throw new Exception('Usuario no encontrado'); + } - // Actualizar usuario usando el método de la clase Database - $updateData = [ - 'name' => $name, - 'status' => $status - ]; - - $result = $db->update('users', $updateData, 'id = :user_id', ['user_id' => $user_id]); + // Actualizar usuario usando el método de la clase Database + $updateData = [ + 'name' => $name, + 'status' => $status + ]; + + $result = $db->update('users', $updateData, 'id = :user_id', ['user_id' => $user_id]); + + } else { + // Buscar por teléfono y crear/actualizar + $existing = $db->fetch('SELECT id FROM users WHERE phone_number = :p LIMIT 1', ['p' => $phone]); + if ($existing) { + $result = $db->update('users', ['name' => $name, 'status' => $status], 'id = :id', ['id' => $existing['id']]); + $user_id = $existing['id']; + } else { + $user_id = $db->insert('users', ['phone_number' => $phone, 'name' => $name, 'status' => $status, 'created_at' => date('Y-m-d H:i:s')]); + $result = $user_id ? true : false; + } + } if ($result === false) { throw new Exception('No se pudo actualizar el usuario'); diff --git a/api/webhook.php b/api/webhook.php index 2108c8c..494a962 100644 --- a/api/webhook.php +++ b/api/webhook.php @@ -100,7 +100,7 @@ class WhatsAppWebhook { if (!isset($value['messages'])) { return; } - + foreach ($value['messages'] as $message) { $phoneNumber = $message['from']; $messageId = $message['id']; @@ -134,6 +134,13 @@ class WhatsAppWebhook { $emoji = $message['reaction']['emoji'] ?? ''; $reactionTo = $message['reaction']['message_id'] ?? ''; $messageText = json_encode(['emoji' => $emoji, 'message_id' => $reactionTo]); + + // Guardar campos específicos para reacciones + $extraFields = [ + 'reaction_to_message_id' => $reactionTo ? intval($reactionTo) : null, + 'reaction_emoji' => $emoji + ]; + } elseif (isset($message['text'])) { $messageText = $message['text']['body']; $messageType = 'text'; @@ -155,15 +162,28 @@ class WhatsAppWebhook { } // Guardar mensaje en base de datos - $this->saveMessage([ + $saveData = [ 'user_id' => $user['id'], 'message_id' => $messageId, 'direction' => 'incoming', 'message_type' => $messageType, 'content' => $messageText, 'media_url' => $mediaUrl, - 'status' => 'received' - ]); + 'status' => 'received', + 'is_read' => 0 + ]; + + // Si el mensaje incluye contexto (es respuesta a otro mensaje) + if (isset($message['context']) && isset($message['context']['id'])) { + $saveData['reply_to_message_id'] = $message['context']['id']; + } + + // Añadir campos extra (si se detectaron) + if (isset($extraFields) && is_array($extraFields)) { + $saveData = array_merge($saveData, $extraFields); + } + + $this->saveMessage($saveData); // Procesar con bot (protección contra excepciones externas) try { diff --git a/assets/js/app.js b/assets/js/app.js index 40265e9..208f17c 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1245,9 +1245,13 @@ class WhatsAppBotManager { try { this.showLoading('Eliminando plantilla...'); - // Por ahora usaremos update_template_status para marcar como eliminada - // En el futuro se puede crear delete_template.php - this.showError('Función de eliminar aún no implementada'); + const result = await this.apiCall('delete_template.php', 'POST', { id: templateId }); + if (result && result.success) { + this.showSuccess('Plantilla eliminada'); + this.loadTemplates(); + } else { + this.showError(result.error || 'Error eliminando plantilla'); + } } catch (error) { this.showError('Error eliminando plantilla: ' + error.message); } finally { diff --git a/conversations.php b/conversations.php index 2c6f25c..126b0de 100644 --- a/conversations.php +++ b/conversations.php @@ -398,6 +398,31 @@ transform: translateX(-100%); } } + + .reply-preview { + background: #f4f6f8; + border-left: 3px solid #d0d7de; + padding: 6px 8px; + margin-bottom: 6px; + font-size: 12px; + color: #555; + border-radius: 4px; + } + + .reaction-badge { + display: inline-block; + background: rgba(0,0,0,0.06); + padding: 2px 6px; + border-radius: 12px; + font-size: 12px; + margin-top: 8px; + } + + .conversation-avatar img, .conversation-avatar-img { max-width: 44px; max-height:44px; border-radius:50%; } + .conversation-avatar-wrapper { width:48px; display:flex; align-items:center; justify-content:center } + .conversation-item.unread { background: rgba(255, 248, 220, 0.9); } + .unread-badge { margin-left: 8px; font-size: 12px; } + @@ -446,9 +471,12 @@
-
Usuario
+
Usuario
+1234567890
+
+ +
@@ -477,6 +505,9 @@ +
+ +
@@ -773,6 +847,51 @@ } } + async loadQuickReplies() { + const container = document.getElementById('quick-replies'); + container.innerHTML = ''; + try { + const resp = await fetch('api/get_templates.php?approved_only=1&limit=6'); + const json = await resp.json(); + if (json && json.success && Array.isArray(json.data)) { + json.data.forEach(t => { + const btn = document.createElement('button'); + btn.className = 'btn btn-outline-primary btn-sm'; + btn.style.marginRight = '6px'; + btn.textContent = t.name; + btn.addEventListener('click', () => this.sendTemplateQuick(t.template_name, t.language_code || 'es')); + container.appendChild(btn); + }); + } + } catch (e) { + console.error('Error loading quick replies', e); + } + } + + async sendTemplateQuick(templateName, language) { + if (!this.currentUserId) return; + const conv = this.conversations.find(c => c.user_id === this.currentUserId); + const phone = conv ? conv.user_phone : document.getElementById('chat-phone').textContent; + + try { + const resp = await fetch('api/send_message.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ recipient: phone, type: 'template', template_name: templateName, language }) + }); + const result = await resp.json(); + if (result.success) { + await this.loadMessages(this.currentUserId, false); + await this.loadConversations(); + } else { + alert('Error al enviar plantilla: ' + (result.error || JSON.stringify(result))); + } + } catch (e) { + console.error('Error sending template quick', e); + alert('Error al enviar plantilla'); + } + } + scrollToBottom() { const container = document.getElementById('chat-messages'); container.scrollTop = container.scrollHeight; @@ -812,6 +931,8 @@ await this.loadMessages(this.currentUserId, false); // Actualizar lista de conversaciones this.loadConversations(); + // Actualizar respuestas rápidas + await this.loadQuickReplies(); } else { alert('Error al enviar mensaje: ' + (result.error || 'Error desconocido')); } @@ -825,6 +946,79 @@ } } + async loadQuickReplies() { + try { + const resp = await fetch('api/get_templates.php?approved_only=1&limit=10'); + const json = await resp.json(); + const container = document.getElementById('quick-replies'); + if (!container) return; + container.innerHTML = ''; + if (json && json.success && Array.isArray(json.data)) { + const templates = json.data.slice(0,5); + templates.forEach(t => { + const text = (t.body_text || t.name || t.template_name || '').replace(/\n/g,' '); + const btn = document.createElement('button'); + btn.className = 'btn btn-sm btn-outline-secondary'; + btn.dataset.qr = text; + btn.textContent = text.length > 30 ? text.substring(0,30) + '...' : text; + container.appendChild(btn); + }); + } + } catch (err) { + console.error('loadQuickReplies error', err); + } + } + + async promptEditUser() { + const currentName = document.getElementById('chat-name').textContent || ''; + const newName = prompt('Nombre del usuario:', currentName); + if (!newName) return; + + try { + const resp = await fetch('api/update_user.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: this.currentUserId, name: newName }) + }); + const json = await resp.json(); + if (json && json.success) { + document.getElementById('chat-name').textContent = newName; + this.showSuccess('Usuario actualizado'); + this.loadConversations(); + } else { + this.showError('No se pudo actualizar usuario'); + } + } catch (err) { + console.error(err); + this.showError('Error actualizando usuario'); + } + } + + async deleteCurrentConversation() { + if (!confirm('¿Eliminar esta conversación? Se borrará todo el historial.')) return; + try { + const resp = await fetch('api/delete_conversation.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: this.currentUserId }) + }); + const json = await resp.json(); + if (json && json.success) { + this.showSuccess('Conversación eliminada'); + // reset UI + document.getElementById('chat-area').style.display = 'none'; + document.getElementById('no-conversation').style.display = 'block'; + this.currentUserId = null; + this.loadConversations(); + } else { + this.showError('No se pudo eliminar la conversación'); + } + } catch (err) { + console.error(err); + this.showError('Error eliminando la conversación'); + } + } + searchConversations(query) { const items = document.querySelectorAll('.conversation-item'); items.forEach(item => { diff --git a/logs/system.log b/logs/system.log index 6798d57..8d0b8aa 100644 --- a/logs/system.log +++ b/logs/system.log @@ -6,3 +6,15 @@ Stack trace: #0 C:\laragon\www\whatsapp\scripts\send_template_test.php(18): WhatsAppService->__construct() #1 {main} thrown in C:\laragon\www\whatsapp\services\WhatsAppService.php on line 18 +[20-Jan-2026 23:53:41 America/Bogota] [webhook] __construct start +[20-Jan-2026 23:53:42 America/Bogota] [webhook] __construct end +[20-Jan-2026 23:53:42 America/Bogota] [webhook] handleRequest start, method=GET +[20-Jan-2026 23:53:47 America/Bogota] [webhook] __construct start +[20-Jan-2026 23:53:47 America/Bogota] [webhook] __construct end +[20-Jan-2026 23:53:47 America/Bogota] [webhook] handleRequest start, method=GET +[20-Jan-2026 23:53:55 America/Bogota] [webhook] __construct start +[20-Jan-2026 23:53:55 America/Bogota] [webhook] __construct end +[20-Jan-2026 23:53:55 America/Bogota] [webhook] handleRequest start, method=GET +[20-Jan-2026 23:54:02 America/Bogota] [webhook] __construct start +[20-Jan-2026 23:54:03 America/Bogota] [webhook] __construct end +[20-Jan-2026 23:54:03 America/Bogota] [webhook] handleRequest start, method=GET diff --git a/scripts/add_conversation_fields.php b/scripts/add_conversation_fields.php new file mode 100644 index 0000000..d796017 --- /dev/null +++ b/scripts/add_conversation_fields.php @@ -0,0 +1,36 @@ +fetchAll("SHOW COLUMNS FROM conversations"); + $colNames = array_column($columns, 'Field'); + + if (!in_array('reply_to_message_id', $colNames)) { + $db->query("ALTER TABLE conversations ADD COLUMN reply_to_message_id INT NULL AFTER message_id"); + echo "Added column reply_to_message_id\n"; + } else { + echo "reply_to_message_id exists\n"; + } + + if (!in_array('reaction_to_message_id', $colNames)) { + $db->query("ALTER TABLE conversations ADD COLUMN reaction_to_message_id INT NULL AFTER reply_to_message_id"); + echo "Added column reaction_to_message_id\n"; + } else { + echo "reaction_to_message_id exists\n"; + } + + if (!in_array('reaction_emoji', $colNames)) { + $db->query("ALTER TABLE conversations ADD COLUMN reaction_emoji VARCHAR(32) NULL AFTER reaction_to_message_id"); + echo "Added column reaction_emoji\n"; + } else { + echo "reaction_emoji exists\n"; + } + +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} diff --git a/scripts/add_message_is_read.php b/scripts/add_message_is_read.php new file mode 100644 index 0000000..f8f3a85 --- /dev/null +++ b/scripts/add_message_is_read.php @@ -0,0 +1,15 @@ +fetch("SHOW COLUMNS FROM conversations LIKE 'is_read'"); +if ($col) { + echo "Column is_read already exists\n"; + exit(0); +} +try { + $db->query("ALTER TABLE conversations ADD COLUMN is_read TINYINT(1) NULL DEFAULT 0 AFTER status"); + echo "Added column is_read\n"; +} catch (Exception $e) { + echo "Failed to add is_read: " . $e->getMessage() . "\n"; + exit(1); +} diff --git a/scripts/add_reaction_to_message_type.php b/scripts/add_reaction_to_message_type.php new file mode 100644 index 0000000..d323193 --- /dev/null +++ b/scripts/add_reaction_to_message_type.php @@ -0,0 +1,26 @@ +fetch("SHOW COLUMNS FROM conversations LIKE 'message_type'"); +if (!$col) { + echo "Column message_type not found\n"; + exit(1); +} + +$type = $col['Type']; +if (strpos($type, "'reaction'") !== false) { + echo "reaction already present in message_type enum\n"; + exit(0); +} + +// Construir nuevo enum +$newEnum = "ENUM('text','image','audio','video','document','template','reaction') NOT NULL DEFAULT 'text'"; +try { + $db->query("ALTER TABLE conversations MODIFY COLUMN message_type $newEnum"); + echo "Added reaction to message_type enum\n"; +} catch (Exception $e) { + echo "Failed to alter table: " . $e->getMessage() . "\n"; + exit(1); +} diff --git a/scripts/add_user_avatar_column.php b/scripts/add_user_avatar_column.php new file mode 100644 index 0000000..5fbf5f1 --- /dev/null +++ b/scripts/add_user_avatar_column.php @@ -0,0 +1,15 @@ +fetch("SHOW COLUMNS FROM users LIKE 'avatar_url'"); +if ($col) { + echo "Column avatar_url already exists\n"; + exit(0); +} +try { + $db->query("ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER name"); + echo "Added column avatar_url\n"; +} catch (Exception $e) { + echo "Failed to add avatar_url: " . $e->getMessage() . "\n"; + exit(1); +} diff --git a/scripts/db_debug.php b/scripts/db_debug.php new file mode 100644 index 0000000..aa4171b --- /dev/null +++ b/scripts/db_debug.php @@ -0,0 +1,10 @@ +fetch('SELECT * FROM conversations WHERE message_type = :mt ORDER BY id DESC LIMIT 5', ['mt' => 'reaction']); +var_export($r); +echo "\nAll recent rows:\n"; +$rows = $db->fetchAll('SELECT id,message_id,message_type,direction,content,reaction_to_message_id,reaction_emoji,reply_to_message_id FROM conversations ORDER BY id DESC LIMIT 10'); +foreach ($rows as $row) { + echo json_encode($row) . "\n"; +} diff --git a/scripts/db_test.php b/scripts/db_test.php new file mode 100644 index 0000000..19610d8 --- /dev/null +++ b/scripts/db_test.php @@ -0,0 +1,13 @@ +getMessage() . "\n"; + echo $t->getTraceAsString() . "\n"; +} diff --git a/scripts/debug_init_service.php b/scripts/debug_init_service.php new file mode 100644 index 0000000..7f04c89 --- /dev/null +++ b/scripts/debug_init_service.php @@ -0,0 +1,24 @@ +getMessage() . "\n"; + echo $t->getTraceAsString() . "\n"; +} + +try { + $b = new BotService(); + echo "BotService OK\n"; +} catch (Throwable $t) { + echo "BotService ERR: " . $t->getMessage() . "\n"; + echo $t->getTraceAsString() . "\n"; +} diff --git a/scripts/describe_conversations.php b/scripts/describe_conversations.php new file mode 100644 index 0000000..976a1a5 --- /dev/null +++ b/scripts/describe_conversations.php @@ -0,0 +1,7 @@ +fetchAll('DESCRIBE conversations'); +foreach ($cols as $c) { + echo $c['Field'] . ' => ' . $c['Type'] . PHP_EOL; +} diff --git a/scripts/describe_users.php b/scripts/describe_users.php new file mode 100644 index 0000000..9299ade --- /dev/null +++ b/scripts/describe_users.php @@ -0,0 +1,7 @@ +fetchAll('DESCRIBE users'); +foreach ($cols as $c) { + echo $c['Field'] . ' => ' . $c['Type'] . PHP_EOL; +} diff --git a/scripts/run_webhook_get.php b/scripts/run_webhook_get.php new file mode 100644 index 0000000..1b4ddf7 --- /dev/null +++ b/scripts/run_webhook_get.php @@ -0,0 +1,24 @@ +handleRequest(); + $out = ob_get_clean(); + echo "Output:\n" . $out . "\n"; +} catch (Throwable $t) { + echo "Throwable: " . $t->getMessage() . "\n"; + echo $t->getTraceAsString() . "\n"; +} diff --git a/scripts/run_webhook_out.txt b/scripts/run_webhook_out.txt new file mode 100644 index 0000000..e69de29 diff --git a/scripts/test_reaction_and_reply.php b/scripts/test_reaction_and_reply.php new file mode 100644 index 0000000..8a3e38e --- /dev/null +++ b/scripts/test_reaction_and_reply.php @@ -0,0 +1,121 @@ +insert('users', ['phone_number' => $testPhone, 'name' => 'Reactor', 'status' => 'active']); +$origMsgId = '1001'; +$db->insert('conversations', [ + 'user_id' => $userId, + 'message_id' => $origMsgId, + 'direction' => 'incoming', + 'message_type' => 'text', + 'content' => 'Original msg', + 'status' => 'received', + 'created_at' => date('Y-m-d H:i:s') +]); + +$payload = [ + 'object' => 'whatsapp_business_account', + 'entry' => [ + [ + 'changes' => [ + [ + 'field' => 'messages', + 'value' => [ + 'messaging_product' => 'whatsapp', + 'metadata' => [ + 'display_phone_number' => '16505551111', + 'phone_number_id' => '123' + ], + 'contacts' => [ + [ 'profile' => ['name' => 'reactor'], 'wa_id' => $testPhone ] + ], + 'messages' => [ + [ + 'from' => $testPhone, + 'id' => '2001', + 'timestamp' => time(), + 'type' => 'reaction', + 'reaction' => ['message_id' => $origMsgId, 'emoji' => '👍'] + ] + ] + ] + ] + ] + ] + ] +]; + +$r = $webhook->processPayload($payload); +$conv = $db->fetch('SELECT * FROM conversations WHERE message_id = :mid', ['mid' => '2001']); +if ($conv) { + echo "Reaction saved: reaction_to_message_id={$conv['reaction_to_message_id']}, reaction_emoji={$conv['reaction_emoji']}\n"; +} else { + echo "Reaction not saved\n"; +} + +// Cleanup reaction test +$db->execute('DELETE FROM conversations WHERE message_id = :mid', ['mid' => '2001']); +$db->execute('DELETE FROM conversations WHERE message_id = :mid', ['mid' => $origMsgId]); +$db->execute('DELETE FROM users WHERE id = :id', ['id' => $userId]); + +// Test reply/context +$testPhone2 = '573007777111'; +$userId2 = $db->insert('users', ['phone_number' => $testPhone2, 'name' => 'Replier', 'status' => 'active']); +$origMsgId2 = '1002'; +$db->insert('conversations', [ + 'user_id' => $userId2, + 'message_id' => $origMsgId2, + 'direction' => 'incoming', + 'message_type' => 'text', + 'content' => 'Original for reply', + 'status' => 'received', + 'created_at' => date('Y-m-d H:i:s') +]); + +$payload2 = [ + 'object' => 'whatsapp_business_account', + 'entry' => [ + [ + 'changes' => [ + [ + 'field' => 'messages', + 'value' => [ + 'messages' => [ + [ + 'from' => $testPhone2, + 'id' => '2002', + 'timestamp' => time(), + 'type' => 'text', + 'text' => ['body' => 'Reply message'], + 'context' => ['id' => $origMsgId2] + ] + ] + ] + ] + ] + ] + ] +]; + +$r2 = $webhook->processPayload($payload2); +$conv2 = $db->fetch('SELECT * FROM conversations WHERE message_id = :mid', ['mid' => '2002']); +if ($conv2) { + echo "Reply saved: reply_to_message_id={$conv2['reply_to_message_id']}\n"; +} else { + echo "Reply not saved\n"; +} + +// Cleanup reply test +$db->execute('DELETE FROM conversations WHERE message_id = :mid', ['mid' => '2002']); +$db->execute('DELETE FROM conversations WHERE message_id = :mid', ['mid' => $origMsgId2]); +$db->execute('DELETE FROM users WHERE id = :id', ['id' => $userId2]); + +echo "Done\n"; diff --git a/scripts/test_require.php b/scripts/test_require.php new file mode 100644 index 0000000..05ffaf0 --- /dev/null +++ b/scripts/test_require.php @@ -0,0 +1,12 @@ +getMessage() . "\n"; + echo $t->getTraceAsString() . "\n"; +} diff --git a/scripts/wa_out.txt b/scripts/wa_out.txt new file mode 100644 index 0000000000000000000000000000000000000000..609a2fd4696698179ca6be5f7ece4b9ee846924a GIT binary patch literal 1670 zcmeH|!A`%G_@&nDIdNT%ozZ$^8KO)W&l#JdYEKT(P3R3BYmaKS zCx5Rw^i{<~kEvLm75T&GK25#Ch#E(#Q*(PJzyGJjo^9q#PPS*Vh8>$dlhcYbF{_ uO`UTew7C@my{#MF>%vFo?S3({3HfHeKi~I7(6uf-OIueuBSSSoE#n7AT`+(E literal 0 HcmV?d00001 diff --git a/scripts/wa_service_test.php b/scripts/wa_service_test.php new file mode 100644 index 0000000..7f043b6 --- /dev/null +++ b/scripts/wa_service_test.php @@ -0,0 +1,16 @@ +getMessage() . "\n"; + echo $t->getTraceAsString() . "\n"; +} +echo "END wa_service_test\n"; diff --git a/scripts/webhook_test_cli.php b/scripts/webhook_test_cli.php new file mode 100644 index 0000000..bb4556d --- /dev/null +++ b/scripts/webhook_test_cli.php @@ -0,0 +1,13 @@ +getMessage() . "\n"; + echo $t->getTraceAsString() . "\n"; +} diff --git a/services/BotService.php b/services/BotService.php index a836e93..8f00e3a 100644 --- a/services/BotService.php +++ b/services/BotService.php @@ -35,7 +35,13 @@ class BotService { if ($this->processSpecialCommands($phoneNumber, $messageText)) { return; } - + // Si el asesor ya respondió después del último mensaje del usuario, frenar al bot + $lastIncoming = $this->db->fetch("SELECT created_at FROM conversations WHERE user_id = :uid AND direction = 'incoming' ORDER BY created_at DESC LIMIT 1", ['uid' => $user['id']]); + $lastOutgoing = $this->db->fetch("SELECT created_at FROM conversations WHERE user_id = :uid AND direction = 'outgoing' ORDER BY created_at DESC LIMIT 1", ['uid' => $user['id']]); + if ($lastOutgoing && $lastIncoming && strtotime($lastOutgoing['created_at']) >= strtotime($lastIncoming['created_at'])) { + // El asesor ya respondió, no enviar respuestas automáticas + return; + } // Verificar si el usuario está en un menú if ($user['current_menu_id']) { $this->processMenuSelection($user, $messageText); @@ -85,7 +91,7 @@ class BotService { $this->exitMenu($phoneNumber); return true; - case 'help': + case 'asesor': case 'ayuda': $this->showHelp($phoneNumber); return true; diff --git a/services/WhatsAppService.php b/services/WhatsAppService.php index a5bf3e9..3ba4bb1 100644 --- a/services/WhatsAppService.php +++ b/services/WhatsAppService.php @@ -514,6 +514,17 @@ class WhatsAppService 'created_at' => date('Y-m-d H:i:s') ]; + // Si es reply (context) + if (isset($data['context']['message_id'])) { + $messageData['reply_to_message_id'] = is_numeric($data['context']['message_id']) ? intval($data['context']['message_id']) : null; + } + + // Si es reaction + if ($data['type'] === 'reaction' && isset($data['reaction'])) { + $messageData['reaction_to_message_id'] = is_numeric($data['reaction']['message_id'] ?? null) ? intval($data['reaction']['message_id']) : null; + $messageData['reaction_emoji'] = $data['reaction']['emoji'] ?? null; + } + $this->db->insert('conversations', $messageData); } } catch (Exception $e) { diff --git a/tests/webhook_tests.php b/tests/webhook_tests.php index c4ac2f1..a6c86ed 100644 --- a/tests/webhook_tests.php +++ b/tests/webhook_tests.php @@ -306,6 +306,117 @@ class WebhookTestSuite { return "Conversación guardada correctamente"; }); + + $this->webhookTest("Procesamiento de reacción entrante", function() { + $testPhone = '573007777000'; + // Crear usuario y mensaje referenciado + $userId = $this->db->insert('users', ['phone_number' => $testPhone, 'name' => 'Reactor', 'status' => 'active']); + $origMsgId = '1001'; + $this->db->insert('conversations', [ + 'user_id' => $userId, + 'message_id' => $origMsgId, + 'direction' => 'incoming', + 'message_type' => 'text', + 'content' => 'Original msg', + 'status' => 'received', + 'created_at' => date('Y-m-d H:i:s') + ]); + + $payload = [ + 'object' => 'whatsapp_business_account', + 'entry' => [ + [ + 'changes' => [ + [ + 'field' => 'messages', + 'value' => [ + 'messages' => [ + [ + 'from' => $testPhone, + 'id' => '2001', + 'timestamp' => time(), + 'type' => 'reaction', + 'reaction' => ['message_id' => $origMsgId, 'emoji' => '👍'] + ] + ] + ] + ] + ] + ] + ] + ]; + + $webhook = new WhatsAppWebhook(); + $result = $webhook->processPayload($payload); + + $conv = $this->db->fetch("SELECT * FROM conversations WHERE message_id = :mid", ['mid' => '2001']); + + // Limpiar + $this->db->execute("DELETE FROM conversations WHERE message_id = :mid", ['mid' => '2001']); + $this->db->execute("DELETE FROM conversations WHERE message_id = :mid", ['mid' => $origMsgId]); + $this->db->execute("DELETE FROM users WHERE id = :id", ['id' => $userId]); + + if (!$conv) throw new Exception("Reacción no se guardó"); + if (intval($conv['reaction_to_message_id']) !== intval($origMsgId)) throw new Exception("reaction_to_message_id no guardado correctamente"); + if ($conv['reaction_emoji'] !== '👍') throw new Exception("reaction_emoji incorrecto"); + + return "Reacción procesada y guardada correctamente"; + }); + + $this->webhookTest("Procesamiento de reply/context entrante", function() { + $testPhone = '573007777111'; + $userId = $this->db->insert('users', ['phone_number' => $testPhone, 'name' => 'Replier', 'status' => 'active']); + $origMsgId = '1002'; + $this->db->insert('conversations', [ + 'user_id' => $userId, + 'message_id' => $origMsgId, + 'direction' => 'incoming', + 'message_type' => 'text', + 'content' => 'Original for reply', + 'status' => 'received', + 'created_at' => date('Y-m-d H:i:s') + ]); + + $payload = [ + 'object' => 'whatsapp_business_account', + 'entry' => [ + [ + 'changes' => [ + [ + 'field' => 'messages', + 'value' => [ + 'messages' => [ + [ + 'from' => $testPhone, + 'id' => '2002', + 'timestamp' => time(), + 'type' => 'text', + 'text' => ['body' => 'Reply message'], + 'context' => ['id' => $origMsgId] + ] + ] + ] + ] + ] + ] + ] + ]; + + $webhook = new WhatsAppWebhook(); + $result = $webhook->processPayload($payload); + + $conv = $this->db->fetch("SELECT * FROM conversations WHERE message_id = :mid", ['mid' => '2002']); + + // Limpiar + $this->db->execute("DELETE FROM conversations WHERE message_id = :mid", ['mid' => '2002']); + $this->db->execute("DELETE FROM conversations WHERE message_id = :mid", ['mid' => $origMsgId]); + $this->db->execute("DELETE FROM users WHERE id = :id", ['id' => $userId]); + + if (!$conv) throw new Exception("Reply no se guardó"); + if ($conv['reply_to_message_id'] != $origMsgId) throw new Exception("reply_to_message_id no guardado correctamente"); + + return "Reply/context procesado y guardado correctamente"; + }); } private function testWebhookSecurity() { @@ -524,12 +635,20 @@ class WebhookTestSuite { } // No ejecutar si es incluido por el runner principal -if (basename($_SERVER['PHP_SELF']) === 'webhook_tests.php') { +if (basename($_SERVER['PHP_SELF']) === 'webhook_tests.php' || php_sapi_name() === 'cli') { $configPath = __DIR__ . '/../config/config.php'; if (file_exists($configPath)) { require_once $configPath; } + + // Asegurar que la clase WhatsAppWebhook esté disponible cuando se ejecutan tests desde CLI + $webhookPath = __DIR__ . '/../api/webhook.php'; + if (file_exists($webhookPath)) { + require_once $webhookPath; + } + $suite = new WebhookTestSuite(); + echo "RUNNING WEBHOOK TESTS\n"; $suite->runAllWebhookTests(); } ?> \ No newline at end of file diff --git a/tests/webhook_tests_out.txt b/tests/webhook_tests_out.txt new file mode 100644 index 0000000000000000000000000000000000000000..58528b7b3f51c72a5648daf53c27ce99e0f84384 GIT binary patch literal 264 zcmZ{f%?bfw6owy`${kqDX4Yc1vQR>bGJcj5t(iymIFDL;N_$@^6V)C_?c=y(KsA#@hbR&X}p3*>!getMethod('saveOutgoingMessage'); +$method->setAccessible(true); + +// Test reaction save +$phone = '573001234999'; +$db->execute('DELETE FROM users WHERE phone_number = :p', ['p' => $phone]); +$userId = $db->insert('users', ['phone_number' => $phone, 'name' => 'Outgoing Reactor', 'status' => 'active']); + +$data = [ + 'to' => $phone, + 'type' => 'reaction', + 'reaction' => ['message_id' => '5001', 'emoji' => '❤️'] +]; +$response = ['messages' => [['id' => 'out_msg_1']]]; + +$foundUser = $db->fetch('SELECT * FROM users WHERE phone_number = :p', ['p' => $phone]); +echo "User found for outgoing reaction? " . ($foundUser ? 'yes' : 'no') . "\n"; + +$method->invokeArgs($service, [$data, $response]); +$conv = $db->fetch('SELECT * FROM conversations WHERE message_id = :mid', ['mid' => 'out_msg_1']); +if ($conv) { + echo "Outgoing reaction saved: id={$conv['id']} reaction_to={$conv['reaction_to_message_id']} emoji={$conv['reaction_emoji']}\n"; +} else { + echo "Outgoing reaction NOT saved\n"; + $recent = $db->fetchAll('SELECT id,message_id,message_type,direction,content,created_at FROM conversations WHERE user_id = :uid ORDER BY id DESC LIMIT 5', ['uid' => $userId]); + echo "Recent rows for user:\n"; + foreach ($recent as $r) echo json_encode($r) . "\n"; + + echo "Trying direct DB insert to see if constraints block it...\n"; + $msg = [ + 'user_id' => $userId, + 'message_id' => 'direct_out_msg_1', + 'direction' => 'outgoing', + 'message_type' => 'reaction', + 'content' => '❤️', + 'status' => 'sent', + 'created_at' => date('Y-m-d H:i:s') + ]; + try { + $res = $db->insert('conversations', $msg); + echo "Direct insert result: " . ($res ? 'ok id=' . $res : 'failed') . "\n"; + $r2 = $db->fetch('SELECT * FROM conversations WHERE message_id = :mid', ['mid' => 'direct_out_msg_1']); + echo "Fetched direct row: " . json_encode($r2) . "\n"; + $db->execute('DELETE FROM conversations WHERE message_id = :mid', ['mid' => 'direct_out_msg_1']); + } catch (Exception $e) { + echo "Direct insert threw: " . $e->getMessage() . "\n"; + } +} +// cleanup +$db->execute('DELETE FROM conversations WHERE message_id = :mid', ['mid' => 'out_msg_1']); +$db->execute('DELETE FROM users WHERE id = :id', ['id' => $userId]); + +// Test text reply save +$phone2 = '573001234998'; +$db->execute('DELETE FROM users WHERE phone_number = :p', ['p' => $phone2]); +$userId2 = $db->insert('users', ['phone_number' => $phone2, 'name' => 'Outgoing Replier', 'status' => 'active']); + +$data2 = [ + 'to' => $phone2, + 'type' => 'text', + 'context' => ['message_id' => '5002'], + 'text' => ['body' => 'Gracias'] +]; +$response2 = ['messages' => [['id' => 'out_msg_2']]]; +$method->invokeArgs($service, [$data2, $response2]); +$conv2 = $db->fetch('SELECT * FROM conversations WHERE message_id = :mid', ['mid' => 'out_msg_2']); +if ($conv2) { + echo "Outgoing reply saved: id={$conv2['id']} reply_to={$conv2['reply_to_message_id']}\n"; +} else { + echo "Outgoing reply NOT saved\n"; +} +// cleanup +$db->execute('DELETE FROM conversations WHERE message_id = :mid', ['mid' => 'out_msg_2']); +$db->execute('DELETE FROM users WHERE id = :id', ['id' => $userId2]); + +echo "Done tests\n"; diff --git a/tests/whatsapp_service_tests_out.txt b/tests/whatsapp_service_tests_out.txt new file mode 100644 index 0000000000000000000000000000000000000000..b0a1ade167685cbe9a671dc28161c83e5ae58112 GIT binary patch literal 318 zcmaivJqp4=6okK8@D4db45%Ok8+$8j31$r|nnI_D9OXSWi4?OnpR37Qp=sO5VfKuW3hu+LzfQIzy3(hjJ-B;M%uNS zDG4!y57+0)sigO%$MobdY(!StOSy36#)%{6AL(1x`r-H7VaR>?-V+mlcjB8G`J8X+ GLfa>1C^}jI literal 0 HcmV?d00001