276 lines
8.1 KiB
PHP
276 lines
8.1 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
|
|
{
|
|
return $this->db->delete('user_states', 'phone_number = :phone', ['phone' => $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
|
|
{
|
|
$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);
|
|
}
|
|
}
|