Files
whatsapp/services/ConversationStateService.php
T
2026-01-13 01:48:33 -05:00

294 lines
8.3 KiB
PHP

<?php
/**
* Servicio de Gestión de Estados de Conversación
* Controla el flujo y contexto de las conversaciones
*/
class ConversationStateService
{
private $db;
// Estados posibles
const STATE_INITIAL = 'initial';
const STATE_MAIN_MENU = 'main_menu';
const STATE_SUBMENU_EXAMS = 'submenu_exams';
const STATE_REQUESTING_HOME_SERVICE = 'requesting_home_service';
const STATE_AWAITING_ORDER_PHOTO = 'awaiting_order_photo';
const STATE_AWAITING_PATIENT_DATA = 'awaiting_patient_data';
const STATE_AWAITING_ID_PHOTO = 'awaiting_id_photo';
const STATE_VALIDATING_DATA = 'validating_data';
const STATE_QUOTATION = 'quotation';
const STATE_AWAITING_CONFIRMATION = 'awaiting_confirmation';
const STATE_SCHEDULED = 'scheduled';
const STATE_REQUESTING_RESULTS_HELP = 'requesting_results_help';
const STATE_WITH_ADVISOR = 'with_advisor';
const STATE_COMPLETED = 'completed';
public function __construct()
{
$this->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);
}
}