diff --git a/api/attend.php b/api/attend.php
index 59c5354..a9c452b 100644
--- a/api/attend.php
+++ b/api/attend.php
@@ -22,7 +22,12 @@ try {
$operator = $_SESSION['admin_user'] ?? null;
$operatorId = $operator['id'] ?? null;
-
+ // Guard: do not set attend if already in_service
+ $cur = $db->fetch('SELECT in_service FROM users WHERE id = ?', [$userId]);
+ if (!empty($cur['in_service'])) {
+ echo json_encode(['success' => false, 'error' => 'La conversación ya está en servicio']);
+ exit;
+ }
try {
require_once __DIR__ . '/../services/BotService.php';
$bot = new BotService();
diff --git a/api/finish_attend.php b/api/finish_attend.php
index 5412e5a..b2f215a 100644
--- a/api/finish_attend.php
+++ b/api/finish_attend.php
@@ -22,7 +22,12 @@ try {
$operator = $_SESSION['admin_user'] ?? null;
$operatorId = $operator['id'] ?? null;
-
+ // Guard: only finish if conversation is currently in service
+ $cur = $db->fetch('SELECT in_service FROM users WHERE id = ?', [$userId]);
+ if (empty($cur['in_service'])) {
+ echo json_encode(['success' => false, 'error' => 'La conversación no está en servicio']);
+ exit;
+ }
try {
require_once __DIR__ . '/../services/BotService.php';
$bot = new BotService();
diff --git a/conversations.php b/conversations.php
index fd712a7..0f30cf3 100644
--- a/conversations.php
+++ b/conversations.php
@@ -1764,6 +1764,38 @@
return text.substring(0, maxLength) + '...';
}
+ // Cache ligero del estado del usuario (in_service, advisor_requested, on_hold)
+ async getUserState(userId, force = false) {
+ if (!userId) return null;
+ this._userStateCache = this._userStateCache || {};
+ const now = Date.now();
+ const cached = this._userStateCache[userId];
+ const TTL = 15000; // 15 segundos
+ if (!force && cached && (now - cached.ts) < TTL) {
+ return cached.state;
+ }
+
+ try {
+ const resp = await this.apiCall(`get_conversation_detail.php?user_id=${userId}`);
+ if (resp && resp.success && resp.user) {
+ const s = {
+ in_service: !!resp.user.in_service,
+ advisor_requested: !!resp.user.advisor_requested,
+ on_hold: !!resp.user.on_hold
+ };
+ this._userStateCache[userId] = { state: s, ts: Date.now() };
+ return s;
+ }
+ } catch (e) {
+ console.warn('getUserState failed for', userId, e);
+ }
+
+ // fallback: keep previous cached or return null
+ if (cached) return cached.state;
+ this._userStateCache[userId] = { state: null, ts: Date.now() };
+ return null;
+ }
+
getConversationPhone(userId) {
const conv = this.conversations.find(c => c.user_id === userId);
if (conv) return conv.phone_number || conv.phone || conv.user_phone || null;
@@ -1911,28 +1943,57 @@
// refresh conv reference
const c = this.conversations.find(x => x.user_id === userId) || conv;
- if (c.in_service) {
- attendToggleBtn.innerHTML = ' Finalizar';
- attendToggleBtn.classList.remove('btn-outline-light');
- attendToggleBtn.classList.add('btn-light','text-dark');
- attendToggleBtn.style.display = 'inline-block';
- attendStatus.textContent = `Atendido por ${c.in_service_by_name || 'un asesor'}`;
- } else if (c.advisor_requested) {
- attendToggleBtn.innerHTML = ' Atender';
- attendToggleBtn.classList.remove('btn-light','text-dark');
- attendToggleBtn.classList.add('btn-outline-light');
- attendToggleBtn.style.display = 'inline-block';
- attendStatus.textContent = '';
- } else {
- attendToggleBtn.style.display = 'none';
- attendStatus.textContent = '';
- }
+ // request a cached state (fast) to be more preciso
+ this.getUserState(userId).then(serverState => {
+ const s = serverState || { in_service: c.in_service, advisor_requested: c.advisor_requested };
+
+ if (s.in_service) {
+ attendToggleBtn.innerHTML = ' Finalizar';
+ attendToggleBtn.classList.remove('btn-outline-light');
+ attendToggleBtn.classList.add('btn-light','text-dark');
+ attendToggleBtn.style.display = 'inline-block';
+ attendStatus.textContent = `Atendido por ${c.in_service_by_name || 'un asesor'}`;
+ } else if (s.advisor_requested) {
+ attendToggleBtn.innerHTML = ' Atender';
+ attendToggleBtn.classList.remove('btn-light','text-dark');
+ attendToggleBtn.classList.add('btn-outline-light');
+ attendToggleBtn.style.display = 'inline-block';
+ attendStatus.textContent = '';
+ } else {
+ attendToggleBtn.style.display = 'none';
+ attendStatus.textContent = '';
+ }
+ }).catch(e => {
+ // fallback to local conv fields
+ if (c.in_service) {
+ attendToggleBtn.innerHTML = ' Finalizar';
+ attendToggleBtn.classList.remove('btn-outline-light');
+ attendToggleBtn.classList.add('btn-light','text-dark');
+ attendToggleBtn.style.display = 'inline-block';
+ attendStatus.textContent = `Atendido por ${c.in_service_by_name || 'un asesor'}`;
+ } else if (c.advisor_requested) {
+ attendToggleBtn.innerHTML = ' Atender';
+ attendToggleBtn.classList.remove('btn-light','text-dark');
+ attendToggleBtn.classList.add('btn-outline-light');
+ attendToggleBtn.style.display = 'inline-block';
+ attendStatus.textContent = '';
+ } else {
+ attendToggleBtn.style.display = 'none';
+ attendStatus.textContent = '';
+ }
+ });
};
const toggleAttend = async () => {
try {
const c = this.conversations.find(x => x.user_id === userId) || conv;
- if (c.in_service) {
+
+ // Preflight: obtener estado canonico del servidor con cache corta
+ const serverState = await this.getUserState(userId, true);
+ const currentlyInService = serverState ? !!serverState.in_service : !!c.in_service;
+
+ if (currentlyInService) {
+ if (!confirm('¿Confirmas finalizar la atención?')) return;
const resp = await this.apiCall('finish_attend.php', { body: { user_id: userId } });
if (resp && resp.success) {
showAlert('Finalizada la atención', 'success');
@@ -1940,12 +2001,18 @@
c.advisor_requested = 0;
// hide hold indicator
if (holdIndicator) holdIndicator.style.display = 'none';
+
+ // actualizar cache local
+ this._userStateCache = this._userStateCache || {};
+ this._userStateCache[userId] = { state: { in_service: false, advisor_requested: false, on_hold: false }, ts: Date.now() };
+
await this.loadConversations();
await this.loadMessages(userId, true);
} else {
throw new Error(resp && resp.error ? resp.error : 'Error finalizando');
}
} else {
+ if (!confirm('¿Confirmas tomar la atención de esta conversación?')) return;
const resp = await this.apiCall('attend.php', { body: { user_id: userId } });
if (resp && resp.success) {
showAlert('Atención iniciada', 'success');
@@ -1958,6 +2025,11 @@
}
// hide bot toggle while in service
if (btn) btn.style.display = 'none';
+
+ // actualizar cache local
+ this._userStateCache = this._userStateCache || {};
+ this._userStateCache[userId] = { state: { in_service: true, advisor_requested: false, on_hold: false }, ts: Date.now() };
+
await this.loadConversations();
await this.loadMessages(userId, true);
} else {
diff --git a/logs/system.log b/logs/system.log
index 2d63eda..7158b31 100644
--- a/logs/system.log
+++ b/logs/system.log
@@ -67,3 +67,9 @@ Stack trace:
[2026-01-24 02:14:23] [INFO] Bot paused for 3 minutes for 573022548060
[2026-01-24 02:18:27] [INFO] Bot paused for 3 minutes for 573022548060
[2026-01-24 02:18:38] [INFO] Bot paused for 3 minutes for 573022548060
+[2026-01-25 22:24:15] [INFO] Bot paused for 3 minutes for 573022548060
+[2026-01-25 22:40:53] [INFO] Advisor solicited for 573022548060 until 2026-01-25 22:43:51
+[2026-01-25 22:45:57] [INFO] Bot paused for 3 minutes for 573194724531
+[2026-01-25 22:46:00] [INFO] Advisor solicited for 573194724531 until 2026-01-25 22:48:58
+[2026-01-25 22:48:41] [INFO] Bot paused for 3 minutes for 573194724531
+[2026-01-25 22:48:44] [INFO] Advisor solicited for 573194724531 until 2026-01-25 22:51:42
diff --git a/scripts/debug_user_state.php b/scripts/debug_user_state.php
new file mode 100644
index 0000000..da03ee8
--- /dev/null
+++ b/scripts/debug_user_state.php
@@ -0,0 +1,60 @@
+\n";
+ exit(1);
+}
+$phone = $argv[1];
+$db = Database::getInstance();
+
+$user = $db->fetch("SELECT * FROM users WHERE phone_number = :phone", ['phone' => $phone]);
+if (!$user) {
+ echo "User not found for phone: $phone\n";
+ exit(1);
+}
+
+echo "User: id={$user['id']} phone={$user['phone_number']} name={$user['name']}\n";
+echo "Flags: in_service={$user['in_service']} advisor_requested={$user['advisor_requested']} on_hold={$user['on_hold']} bot_paused_until={$user['bot_paused_until']} current_menu_id={$user['current_menu_id']} welcome_sent_at={$user['welcome_sent_at']}\n";
+
+// Last messages
+$messages = $db->fetchAll("SELECT id, direction, message_type, content, created_at FROM conversations WHERE user_id = :uid ORDER BY created_at DESC LIMIT 20", ['uid' => $user['id']]);
+
+echo "Last messages (most recent first):\n";
+foreach ($messages as $m) {
+ $content = $m['content'];
+ $short = mb_substr(strip_tags($content), 0, 120);
+ echo " - [{$m['created_at']}] {$m['direction']} {$m['message_type']} id={$m['id']} content=" . json_encode($short) . "\n";
+}
+
+// ConversationStateService data
+try {
+ if (class_exists('ConversationStateService')) {
+ $svc = new ConversationStateService();
+ $state = $svc->getState($phone);
+ echo "ConversationStateService state row: ";
+ if ($state) {
+ echo json_encode($state) . "\n";
+ echo "Current menu via service: " . json_encode($svc->getCurrentMenuId($phone)) . "\n";
+ } else {
+ echo "(no state row)\n";
+ }
+ } else {
+ echo "ConversationStateService not available\n";
+ }
+} catch (Exception $e) {
+ echo "Error querying ConversationStateService: " . $e->getMessage() . "\n";
+}
+
+// Recent operator activity
+$acts = $db->fetchAll("SELECT * FROM operator_activity WHERE user_id = :uid ORDER BY created_at DESC LIMIT 10", ['uid' => $user['id']]);
+if ($acts) {
+ echo "Recent operator activity:\n";
+ foreach ($acts as $a) {
+ echo " - [{$a['created_at']}] action={$a['action']} operator_id={$a['operator_id']} details=" . ($a['details'] ?? '') . "\n";
+ }
+} else {
+ echo "No operator activity found\n";
+}
+
+echo "Done\n";
diff --git a/scripts/set_menu_option_request_documents.php b/scripts/set_menu_option_request_documents.php
new file mode 100644
index 0000000..cc78dbc
--- /dev/null
+++ b/scripts/set_menu_option_request_documents.php
@@ -0,0 +1,19 @@
+\n";
+ exit(1);
+}
+$id = intval($argv[1]);
+
+$db = Database::getInstance();
+$opt = $db->fetch("SELECT * FROM menu_options WHERE id = :id", ['id' => $id]);
+if (!$opt) {
+ echo "Menu option not found: $id\n";
+ exit(1);
+}
+
+$db->update('menu_options', ['action_type' => 'request_documents'], 'id = :id', ['id' => $id]);
+
+echo "Updated menu_option id=$id to action_type=request_documents\n";
\ No newline at end of file
diff --git a/scripts/simulate_document_flow.php b/scripts/simulate_document_flow.php
new file mode 100644
index 0000000..0037393
--- /dev/null
+++ b/scripts/simulate_document_flow.php
@@ -0,0 +1,20 @@
+fetch('SELECT * FROM users WHERE phone_number = :phone', ['phone' => $phone]);
+if (!$user) { echo "No user found\n"; exit(1); }
+
+$bot = new BotService();
+
+echo "1) Simular selección del menú que pide documentos (opción 1)\n";
+$bot->processMessage($user, '1', 'text');
+
+echo "2) Simular envío de imagen (documento)\n";
+$bot->processMessage($user, 'media123', 'image');
+
+echo "3) Simular confirmación 'ENVIADA'\n";
+$bot->processMessage($user, 'ENVIADA', 'text');
+
+echo "Done\n";
diff --git a/scripts/simulate_process.php b/scripts/simulate_process.php
new file mode 100644
index 0000000..006e9a3
--- /dev/null
+++ b/scripts/simulate_process.php
@@ -0,0 +1,14 @@
+fetch('SELECT * FROM users WHERE phone_number = :phone', ['phone' => $phone]);
+if (!$user) { echo "No user found\n"; exit(1); }
+
+$bot = new BotService();
+
+// Simulate incoming message '1'
+$bot->processMessage($user, '1', 'text');
+
+echo "Simulated processMessage('1') for {$phone}\n";
diff --git a/scripts/simulate_process_message.php b/scripts/simulate_process_message.php
new file mode 100644
index 0000000..e55cb13
--- /dev/null
+++ b/scripts/simulate_process_message.php
@@ -0,0 +1,15 @@
+fetch('SELECT * FROM users WHERE phone_number = :phone', ['phone' => $phone]);
+if (!$user) { echo "No user found\n"; exit(1); }
+
+$bot = new BotService();
+
+// Simulate incoming message
+$bot->processMessage($user, $message, 'text');
+
+echo "Simulated processMessage('{$message}') for {$phone}\n";
diff --git a/services/BotService.php b/services/BotService.php
index 1e4505c..0874d64 100644
--- a/services/BotService.php
+++ b/services/BotService.php
@@ -48,7 +48,7 @@ class BotService {
// DEBUG: log basic context
try {
$userId = $user['id'] ?? 'unknown';
- error_log("[BotService] processMessage start - user_id={$userId} phone={$phoneNumber} type={$messageType} message='" . substr($messageText,0,200) . "' current_menu_id=" . ($user['current_menu_id'] ?? 'null') . " on_hold=" . ($user['on_hold'] ?? '0') . " advisor_requested=" . ($user['advisor_requested'] ?? '0') . " bot_paused_until=" . ($user['bot_paused_until'] ?? 'null'));
+ error_log("[BotService] processMessage start - user_id={$userId} phone={$phoneNumber} type={$messageType} message='" . substr($messageText,0,200) . "' current_menu_id=" . ($user['current_menu_id'] ?? 'null') . " on_hold=" . ($user['on_hold'] ?? '0') . " advisor_requested=" . ($user['advisor_requested'] ?? '0') . " bot_paused_until=" . ($user['bot_paused_until'] ?? 'null') . " bot_enabled=" . (isset($user['bot_enabled']) ? $user['bot_enabled'] : 'null'));
} catch (Throwable $t) {
// ignore logging errors
}
@@ -63,11 +63,12 @@ class BotService {
try {
// Support masculino/femenino, singular/plural: enviado, enviada, enviados, enviadas
if (preg_match('/\b(enviad[oa]s?)\b/iu', $messageText)) {
- // NO enviar respuesta al usuario (para conservar el indicador de atención en la lista).
- // En su lugar, marcar la conversación como que requiere atención y crear un mensaje 'system'
+ // Cuando el usuario confirma que envió la documentación, notificarle y transferir a un asesor.
try {
- $this->db->update('users', ['advisor_requested' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);
+ // Usar requestAdvisor para aplicar advisor_requested, pausar el bot y enviar notificación al usuario
+ $this->requestAdvisor($phoneNumber, 3);
+ // Registrar mensaje de sistema para trazabilidad (no altera flujo de notificación)
$sys = [
'system' => 'attention',
'type' => 'user_sent_documents',
@@ -84,10 +85,10 @@ class BotService {
]);
} catch (Exception $e) {
- error_log('[BotService] failed to mark advisor_requested on envio: ' . $e->getMessage());
+ error_log('[BotService] failed to request advisor on envio: ' . $e->getMessage());
}
- error_log("[BotService] processMessage - marked advisor_requested for user {$user['id']} (no outgoing message sent)");
+ error_log("[BotService] processMessage - requested advisor for user {$user['id']} (sent notification to user)");
return;
}
} catch (Throwable $t) {
@@ -129,11 +130,8 @@ class BotService {
// Permitir *navegación explícita* durante la espera: MENU, ATRÁS, números (opciones)
$normalized = mb_strtolower(trim($messageText));
$isNav = in_array($normalized, ['menu','menú','inicio','atras','atrás','volver','back'], true) || is_numeric($messageText);
- try {
- $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Su solicitud de asesor está pendiente. Un miembro del equipo le atenderá en breve. Si no hay respuesta, podrá usar *MENU* después de " . date('g:i a', $until) . ".");
- } catch (Exception $e) {
- // ignore
- }
+ // No enviar notificación automática al usuario cuando advisor_requested está activa.
+ // Mantener navegación explícita permitida (MENU/ATRÁS/números); no enviar mensajes proactivos.
if ($isNav) {
try { error_log("[BotService] processMessage - advisor_requested active but navigation input allowed for user {$user['id']} message='{$messageText}'"); } catch (Throwable $t) {}
@@ -285,10 +283,10 @@ class BotService {
}
}
- // Si el usuario ha desactivado el bot manualmente (toggle), no responder
- if (isset($user['bot_enabled']) && !$user['bot_enabled']) {
- return;
- }
+ // Si el usuario ha desactivado el bot manualmente (toggle), evitar respuestas automáticas
+ // pero permitir comandos explícitos (MENU, ATRÁS) y selección numérica dentro de menús.
+ $botDisabled = (isset($user['bot_enabled']) && !$user['bot_enabled']);
+
// Verificar si el asesor ya respondió después del último mensaje del usuario.
// En ese caso NO enviaremos respuestas automáticas, pero sí permitiremos
@@ -300,6 +298,9 @@ class BotService {
// El asesor ya respondió, marcar flag pero no retornar (permitir menús y comandos explícitos)
$advisorReplied = true;
error_log("[BotService] processMessage - advisorReplied=true for user {$user['id']}");
+ try { error_log("[BotService] processMessage - lastOutgoing={$lastOutgoing['created_at']} lastIncoming={$lastIncoming['created_at']}"); } catch (Throwable $t) {}
+ } else {
+ try { error_log("[BotService] processMessage - advisorReplied=false lastOutgoing=" . ($lastOutgoing['created_at'] ?? 'null') . " lastIncoming=" . ($lastIncoming['created_at'] ?? 'null')) ; } catch (Throwable $t) {}
}
// Resolver current_menu_id fresco (evitar usar $user desactualizado que viene del webhook)
@@ -355,7 +356,11 @@ class BotService {
// Estas respuestas solo se mostrarán en la UI del chat como "respuestas rápidas".
// Si un asesor ya respondió recientemente, no enviar respuestas automáticas.
if (empty($advisorReplied)) {
- $this->sendDefaultNoMatch($phoneNumber);
+ if (empty($botDisabled)) {
+ $this->sendDefaultNoMatch($phoneNumber);
+ } else {
+ try { error_log("[BotService] processMessage - bot disabled, skipping default no-match for user {$user['id']}"); } catch (Throwable $t) {}
+ }
} else {
// Opcional: informar que el asesor ya respondió y no podemos enviar automáticas
try {
@@ -565,6 +570,133 @@ class BotService {
// DEBUG
try { error_log("[BotService] processMenuSelection - RESOLVED menu_id={$currentMenuId} for phone={$phoneNumber} user_id={$user['id']} incoming='{$messageText}'"); } catch (Throwable $t) {}
+ // Prioritario: si estamos esperando documentos para esta conversación, manejarlo aquí
+ try {
+ $stateSvcTemp = new ConversationStateService();
+ $await = $stateSvcTemp->getStateData($phoneNumber, 'awaiting_documents');
+ } catch (Exception $e) {
+ $await = null;
+ }
+
+ if (!empty($await)) {
+ // Si llega multimedia (no text), marcar recibido y enviar ACK
+ if ($messageType !== 'text') {
+ try {
+ $stateSvcTemp->updateStateData($phoneNumber, ['awaiting_documents' => ['since' => $await['since'], 'received' => true]]);
+ } catch (Exception $e) {
+ // ignore
+ }
+ try {
+ $this->whatsappService->sendTextMessage($phoneNumber, "✅ Documento recibido. Cuando haya terminado, escriba *ENVIADA*.");
+ } catch (Exception $e) {
+ // ignore
+ }
+ return; // no procesar más
+ }
+
+ // Si es texto y dice enviada => transferir a asesor
+ if (preg_match('/\b(enviad[oa]s?)\b/iu', $messageText)) {
+ try {
+ $this->requestAdvisor($phoneNumber, 3);
+ // limpiar estado awaiting_documents
+ $stateSvcTemp->updateStateData($phoneNumber, ['awaiting_documents' => null]);
+
+ // registrar sistema para trazabilidad
+ $sys = [
+ 'system' => 'user_documents_sent',
+ 'type' => 'user_confirmed_documents',
+ 'text' => 'Usuario confirmó envío de documentos con la palabra ENVIADA.'
+ ];
+ $this->db->insert('conversations', [
+ 'user_id' => $user['id'],
+ 'direction' => 'incoming',
+ 'message_type' => 'text',
+ 'content' => json_encode($sys),
+ 'status' => 'received',
+ 'created_at' => date('Y-m-d H:i:s')
+ ]);
+ } catch (Exception $e) {
+ error_log('[BotService] failed to process ENVIADA: ' . $e->getMessage());
+ }
+ return;
+ }
+
+ // Permitir navegación explícita (MENU/ATRÁS/números) y en los demás casos evitar respuestas automáticas
+ $cmd = mb_strtolower(trim($messageText));
+ $backCommands = ['atras', 'atrás', 'volver', 'back'];
+ if (in_array($cmd, ['menu','menú','inicio'], true)) {
+ $this->showMainMenu($phoneNumber);
+ return;
+ }
+ if (in_array($cmd, $backCommands, true)) {
+ try {
+ $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
+ $currentMenuId = $cur ? $cur['current_menu_id'] : null;
+ } catch (Exception $e) {
+ $currentMenuId = null;
+ }
+
+ if (!empty($currentMenuId)) {
+ $this->goToParentMenu($phoneNumber, $currentMenuId);
+ } else {
+ $this->goToPreviousMenu($phoneNumber);
+ }
+ return;
+ }
+
+ if (is_numeric(trim($messageText))) {
+ try {
+ $stateSvc2 = new ConversationStateService();
+ $resolvedMenuId2 = $stateSvc2->getCurrentMenuId($phoneNumber);
+ } catch (Exception $e) {
+ $resolvedMenuId2 = null;
+ }
+
+ if (empty($resolvedMenuId2)) {
+ try {
+ $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
+ $resolvedMenuId2 = $cur ? $cur['current_menu_id'] : null;
+ } catch (Exception $e) {
+ $resolvedMenuId2 = null;
+ }
+ }
+
+ if (empty($resolvedMenuId2)) {
+ try {
+ $hist = (array)$stateSvc2->getStateData($phoneNumber, 'menu_history');
+ if (!empty($hist)) $resolvedMenuId2 = end($hist);
+ } catch (Exception $e) {
+ $resolvedMenuId2 = null;
+ }
+ }
+
+ if (!empty($resolvedMenuId2)) {
+ try {
+ $this->db->update('users', ['current_menu_id' => $resolvedMenuId2, 'current_step' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);
+ try {
+ $stateSvc2->setCurrentMenu($phoneNumber, (int)$resolvedMenuId2);
+ } catch (Exception $e) {
+ // ignore
+ }
+ } catch (Exception $e) {
+ error_log('[BotService] failed to set current_menu_id before processing numeric selection: ' . $e->getMessage());
+ }
+
+ $user['current_menu_id'] = $resolvedMenuId2;
+ $this->processMenuSelection($user, $messageText);
+ return;
+ }
+ }
+
+ // Si llegamos aquí y no es navegación, enviamos un aviso simple para que escriba ENVIADA
+ try {
+ $this->whatsappService->sendTextMessage($phoneNumber, "Si ya enviaste los documentos, escriba *ENVIADA* cuando hayas terminado. Si necesita volver al menú, escriba *MENU*. ");
+ } catch (Exception $e) {
+ // ignore
+ }
+ return;
+ }
+
// Soportar comando "atrás" o "volver" para navegar al menú padre o al menú previo
$cmd = mb_strtolower(trim($messageText));
$backCommands = ['atras', 'atrás', 'volver', 'back'];
@@ -662,9 +794,36 @@ class BotService {
// Llamar API externa (implementar según necesidad)
$this->processApiCall($phoneNumber, $option['action_value']);
break;
+
+ case 'request_documents':
+ // Solicitar al usuario que envíe documentación y activar estado awaiting_documents
+ $message = $option['action_value'] ?: "Por favor, envíe los documentos o fotos requeridas.";
+ $this->whatsappService->sendTextMessage($phoneNumber, $message);
+ try {
+ $stateSvc = new ConversationStateService();
+ $stateSvc->updateStateData($phoneNumber, ['awaiting_documents' => ['since' => date('c'), 'received' => false]]);
+ $this->whatsappService->sendTextMessage($phoneNumber, "Cuando hayas enviado los documentos, escribe *ENVIADA* para que un asesor lo revise.");
+ $sys = [
+ 'system' => 'awaiting_documents_started',
+ 'type' => 'awaiting_documents',
+ 'text' => 'Se solicitó documentación y se espera confirmación ENVIADA.'
+ ];
+ $this->db->insert('conversations', [
+ 'user_id' => $this->db->fetch('SELECT id FROM users WHERE phone_number = :phone', ['phone' => $phoneNumber])['id'] ?? null,
+ 'direction' => 'outgoing',
+ 'message_type' => 'text',
+ 'content' => json_encode($sys),
+ 'status' => 'sent',
+ 'created_at' => date('Y-m-d H:i:s')
+ ]);
+ } catch (Exception $e) {
+ error_log('[BotService] request_documents failed: ' . $e->getMessage());
+ }
+ $this->exitMenu($phoneNumber);
+ break;
case 'end':
- // Finalizar conversación
+ // Finalizar conversación (comportamiento por defecto)
$message = $option['action_value'] ?: "Gracias por usar nuestro servicio. ¡Hasta pronto!";
$this->whatsappService->sendTextMessage($phoneNumber, $message);
$this->exitMenu($phoneNumber);
@@ -791,7 +950,7 @@ class BotService {
try {
$pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
// Guardar bandera y tiempo de expiración (usamos bot_paused_until que ya existe)
- $this->db->update('users', ['on_hold' => 1, 'bot_paused_until' => $pausedUntil, 'advisor_requested' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);
+ $this->db->update('users', ['on_hold' => 1, 'bot_paused_until' => $pausedUntil, 'advisor_requested' => 1, 'bot_enabled' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
// Enviar mensaje (bot) marcando la solicitud; no incluimos metadata de operador para evitar limpiar la marca
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Se ha solicitado un asesor. Se le ha puesto en espera por {$minutes} minutos. Si no hay respuesta, podrá volver a usar *MENU*." );
if (function_exists('writeLog')) writeLog('INFO', "Advisor requested for $phoneNumber until $pausedUntil");
@@ -807,7 +966,8 @@ class BotService {
try {
$pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
// No ponemos on_hold para evitar bloqueo automático; usamos advisor_requested
- $this->db->update('users', ['advisor_requested' => 1, 'bot_paused_until' => $pausedUntil], 'phone_number = :phone', ['phone' => $phoneNumber]);
+ // Marcar solicitud de asesor y pausar bot temporalmente
+ $this->db->update('users', ['advisor_requested' => 1, 'bot_paused_until' => $pausedUntil, 'bot_enabled' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Le hemos transferido a un asesor; le responderemos en breve.");
if (function_exists('writeLog')) writeLog('INFO', "Advisor solicited for $phoneNumber until $pausedUntil");
@@ -819,7 +979,8 @@ class BotService {
public function releaseHold($phoneNumber) {
try {
// Clear hold/advisor flags and also ensure in_service is cleared for consistency
- $this->db->update('users', ['on_hold' => 0, 'bot_paused_until' => null, 'advisor_requested' => 0, 'in_service' => 0, 'in_service_by' => null, 'in_service_at' => null], 'phone_number = :phone', ['phone' => $phoneNumber]);
+ // Clear hold and reactivate bot
+ $this->db->update('users', ['on_hold' => 0, 'bot_paused_until' => null, 'advisor_requested' => 0, 'in_service' => 0, 'in_service_by' => null, 'in_service_at' => null, 'bot_enabled' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Su conversación ha sido retomada por el equipo. Puede usar *MENU* para continuar.");
if (function_exists('writeLog')) writeLog('INFO', "Advisor released hold for $phoneNumber");
} catch (Exception $e) {
@@ -833,7 +994,8 @@ class BotService {
public function attendConversation($phoneNumber, $operatorId = null) {
try {
$now = date('Y-m-d H:i:s');
- $update = ['in_service' => 1, 'in_service_at' => $now, 'advisor_requested' => 0];
+ // Marcar como en servicio y desactivar el bot temporalmente (solo mientras el operador atiende)
+ $update = ['in_service' => 1, 'in_service_at' => $now, 'advisor_requested' => 0, 'bot_enabled' => 0];
if ($operatorId) $update['in_service_by'] = $operatorId;
$this->db->update('users', $update, 'phone_number = :phone', ['phone' => $phoneNumber]);
@@ -876,7 +1038,8 @@ class BotService {
public function finishAttendConversation($phoneNumber, $operatorId = null) {
try {
// Limpiar estado de 'in_service'
- $this->db->update('users', ['in_service' => 0, 'in_service_by' => null, 'in_service_at' => null, 'advisor_requested' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
+ // Limpiar in_service y reactivar el bot
+ $this->db->update('users', ['in_service' => 0, 'in_service_by' => null, 'in_service_at' => null, 'advisor_requested' => 0, 'bot_enabled' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);
// Enviar plantilla de encuesta (configurable)
$template = getConfigFromDB('survey_template', 'encuesta_satisfaccin');