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');