Files
whatsapp/services/ConversationStateService.php
T
2026-01-24 02:15:54 -05:00

282 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
{
$state = $this->db->fetch(
"SELECT * FROM user_states WHERE phone_number = :phone ORDER BY id DESC LIMIT 1",
['phone' => $phoneNumber]
);
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
{
$sql = "INSERT INTO user_states (phone_number, state, state_data, created_at, updated_at)
VALUES (:phone, :state, :state_data, 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;
$this->db->query($sql, ['phone' => $phoneNumber, 'state' => $state, 'state_data' => $stateDataJson]);
return true;
}
/**
* 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);
$this->db->query(
"UPDATE user_states SET state_data = :sd, updated_at = NOW() WHERE phone_number = :phone",
['sd' => json_encode($mergedData, JSON_UNESCAPED_UNICODE), 'phone' => $phoneNumber]
);
return true;
}
/**
* 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
{
try {
$this->db->delete('user_states', 'phone_number = :phone', ['phone' => $phoneNumber]);
return true;
} catch (Exception $e) {
error_log('[ConversationStateService] clearState failed: ' . $e->getMessage());
return false;
}
}
/**
* 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
{
$sql = "DELETE FROM user_states
WHERE updated_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)
AND state NOT IN (?, ?)";
return $this->db->execute($sql, [
self::STATE_SCHEDULED,
self::STATE_WITH_ADVISOR
]);
}
/**
* 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
{
// Allow null to clear current_menu_id
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);
}
}