db = Database::getInstance(); } /** * Obtiene el estado actual de un usuario * @param string $phoneNumber * @return array|null */ public function getState(string $phoneNumber): ?array { $stmt = $this->db->prepare(" SELECT * FROM user_states WHERE phone_number = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$phoneNumber]); $state = $stmt->fetch(PDO::FETCH_ASSOC); if ($state && $state['state_data']) { $state['state_data'] = json_decode($state['state_data'], true); } return $state ?: null; } /** * Establece un nuevo estado para el usuario * @param string $phoneNumber * @param string $state * @param array $data Datos adicionales del estado * @return bool */ public function setState(string $phoneNumber, string $state, array $data = []): bool { $stmt = $this->db->prepare(" INSERT INTO user_states (phone_number, state, state_data, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE state = VALUES(state), state_data = VALUES(state_data), updated_at = NOW() "); $stateDataJson = !empty($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : null; return $stmt->execute([$phoneNumber, $state, $stateDataJson]); } /** * Actualiza los datos del estado actual sin cambiar el estado * @param string $phoneNumber * @param array $data * @return bool */ public function updateStateData(string $phoneNumber, array $data): bool { $currentState = $this->getState($phoneNumber); if (!$currentState) { return false; } $existingData = $currentState['state_data'] ?? []; $mergedData = array_merge($existingData, $data); $stmt = $this->db->prepare(" UPDATE user_states SET state_data = ?, updated_at = NOW() WHERE phone_number = ? "); return $stmt->execute([ json_encode($mergedData, JSON_UNESCAPED_UNICODE), $phoneNumber ]); } /** * Obtiene datos específicos del estado * @param string $phoneNumber * @param string $key * @return mixed */ public function getStateData(string $phoneNumber, string $key) { $state = $this->getState($phoneNumber); if (!$state || !isset($state['state_data'])) { return null; } return $state['state_data'][$key] ?? null; } /** * Limpia el estado del usuario * @param string $phoneNumber * @return bool */ public function clearState(string $phoneNumber): bool { $stmt = $this->db->prepare(" DELETE FROM user_states WHERE phone_number = ? "); return $stmt->execute([$phoneNumber]); } /** * Verifica si el usuario está en un flujo específico * @param string $phoneNumber * @param string $state * @return bool */ public function isInState(string $phoneNumber, string $state): bool { $currentState = $this->getState($phoneNumber); return $currentState && $currentState['state'] === $state; } /** * Obtiene el contador de intentos * @param string $phoneNumber * @return int */ public function getAttemptCount(string $phoneNumber): int { return (int)($this->getStateData($phoneNumber, 'attempt_count') ?? 0); } /** * Incrementa el contador de intentos * @param string $phoneNumber * @return int Nuevo número de intentos */ public function incrementAttempts(string $phoneNumber): int { $attempts = $this->getAttemptCount($phoneNumber) + 1; $this->updateStateData($phoneNumber, ['attempt_count' => $attempts]); return $attempts; } /** * Resetea el contador de intentos * @param string $phoneNumber * @return bool */ public function resetAttempts(string $phoneNumber): bool { return $this->updateStateData($phoneNumber, ['attempt_count' => 0]); } /** * Guarda datos temporales del agendamiento * @param string $phoneNumber * @param array $appointmentData * @return bool */ public function saveAppointmentData(string $phoneNumber, array $appointmentData): bool { return $this->updateStateData($phoneNumber, [ 'appointment' => $appointmentData, 'appointment_started_at' => date('Y-m-d H:i:s') ]); } /** * Obtiene datos del agendamiento en curso * @param string $phoneNumber * @return array|null */ public function getAppointmentData(string $phoneNumber): ?array { return $this->getStateData($phoneNumber, 'appointment'); } /** * Verifica si el estado ha expirado (más de 30 minutos de inactividad) * @param string $phoneNumber * @return bool */ public function isStateExpired(string $phoneNumber): bool { $state = $this->getState($phoneNumber); if (!$state) { return false; } $updatedAt = strtotime($state['updated_at']); $now = time(); $diffMinutes = ($now - $updatedAt) / 60; // Expirar después de 30 minutos return $diffMinutes > 30; } /** * Limpia estados expirados de todos los usuarios * @return int Número de estados eliminados */ public function cleanExpiredStates(): int { $stmt = $this->db->prepare(" DELETE FROM user_states WHERE updated_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE) AND state NOT IN (?, ?) "); $stmt->execute([ self::STATE_SCHEDULED, self::STATE_WITH_ADVISOR ]); return $stmt->rowCount(); } /** * Obtiene el menú actual del usuario * @param string $phoneNumber * @return int|null ID del menú */ public function getCurrentMenuId(string $phoneNumber): ?int { return $this->getStateData($phoneNumber, 'current_menu_id'); } /** * Establece el menú actual * @param string $phoneNumber * @param int $menuId * @return bool */ public function setCurrentMenu(string $phoneNumber, int $menuId): bool { return $this->updateStateData($phoneNumber, ['current_menu_id' => $menuId]); } /** * Marca la conversación para transferir a asesor * @param string $phoneNumber * @param string $reason * @return bool */ public function markForTransfer(string $phoneNumber, string $reason = ''): bool { return $this->setState($phoneNumber, self::STATE_WITH_ADVISOR, [ 'transfer_reason' => $reason, 'transferred_at' => date('Y-m-d H:i:s') ]); } /** * Verifica si el usuario está esperando asesor * @param string $phoneNumber * @return bool */ public function isWaitingForAdvisor(string $phoneNumber): bool { return $this->isInState($phoneNumber, self::STATE_WITH_ADVISOR); } }