This commit is contained in:
Lizandro Guarnizo
2026-03-10 18:42:51 -05:00
parent b57fc16ab3
commit db469b2134
98 changed files with 16365 additions and 303 deletions
+205
View File
@@ -62,6 +62,21 @@ class BotService {
error_log('[BotService] maybeResetConversationAfterIdle threw: ' . $e->getMessage());
}
// ── Flujo de Términos y Condiciones ──────────────────────────────────
// 1. Si el usuario está esperando respuesta de términos, procesarla primero.
if (!empty($user['terms_pending'])) {
if ($this->processTermsResponse($user, $phoneNumber, $messageText)) {
return;
}
}
// 2. Si el usuario necesita aceptar (nuevo, expirado, o versión nueva con forzar_reenvio).
if ($this->needsTermsAcceptance($user)) {
$this->sendTermsMessage($user, $phoneNumber);
return;
}
// ─────────────────────────────────────────────────────────────────────
// Verificar si es un nuevo usuario (enviar mensaje de bienvenida)
if ($this->isNewUser($user['id'])) {
$this->sendWelcomeMessage($phoneNumber);
@@ -427,6 +442,196 @@ class BotService {
return false;
}
// =========================================================================
// TÉRMINOS Y CONDICIONES
// =========================================================================
/**
* Obtiene la versión activa de términos o null si no existe ninguna.
*/
private function getActiveTermsVersion(): ?array {
try {
return $this->db->fetch(
"SELECT * FROM terms_versions WHERE activa = 1 ORDER BY id DESC LIMIT 1"
) ?: null;
} catch (Exception $e) {
error_log('[BotService] getActiveTermsVersion error: ' . $e->getMessage());
return null;
}
}
/**
* Decide si el usuario debe aceptar (o re-aceptar) los términos.
* - No hay aceptación previa (terms_accepted_at IS NULL)
* - Han pasado más de 6 meses desde la última aceptación
* - La versión activa tiene forzar_reenvio=1 y el usuario aceptó una versión anterior
*/
private function needsTermsAcceptance(array $user): bool {
$terms = $this->getActiveTermsVersion();
if (!$terms) return false; // Sin versión activa → no hay términos que aceptar
// ¿El usuario ya aceptó esta versión exacta?
$acceptedVersionId = $user['terms_version_id'] ?? null;
$acceptedAt = $user['terms_accepted_at'] ?? null;
// Nunca aceptó
if (!$acceptedAt) return true;
// Forzar re-aceptación si la versión activa lo requiere y el usuario tiene una versión diferente
if (!empty($terms['forzar_reenvio']) && (int)$acceptedVersionId !== (int)$terms['id']) {
return true;
}
// Han pasado más de 6 meses (26 semanas) desde la última aceptación
$sixMonthsAgo = strtotime('-6 months');
if (strtotime($acceptedAt) < $sixMonthsAgo) return true;
return false;
}
/**
* Envía el mensaje de términos al usuario y registra el envío en BD.
*/
private function sendTermsMessage(array $user, string $phoneNumber): void {
$terms = $this->getActiveTermsVersion();
if (!$terms) return;
// Usar el mensaje configurado en terms_versions o el de config como fallback
$msg = !empty($terms['mensaje_aceptacion'])
? $terms['mensaje_aceptacion']
: getConfigFromDB('terms_message', '');
if (!$msg) return;
// Adjuntar URL del documento solo si es una URL pública (no localhost)
if (!empty($terms['documento_url'])) {
$docUrl = $terms['documento_url'];
$isLocal = (
strpos($docUrl, 'localhost') !== false ||
strpos($docUrl, '127.0.0.1') !== false ||
preg_match('/https?:\/\/\d+\.\d+\.\d+\.\d+:\d+/', $docUrl)
);
if (!$isLocal) {
$msg .= "\n\n📄 Documento: " . $docUrl;
}
}
$this->whatsappService->sendTextMessage($phoneNumber, $msg);
// Marcar usuario como "esperando respuesta de términos"
try {
$this->db->update(
'users',
['terms_pending' => 1],
'id = :id',
['id' => $user['id']]
);
} catch (Exception $e) {
error_log('[BotService] sendTermsMessage update terms_pending: ' . $e->getMessage());
}
// Registrar envío en terms_acceptance con estado pendiente
try {
$this->db->insert('terms_acceptance', [
'user_id' => $user['id'],
'terms_version_id' => $terms['id'],
'phone_number' => $phoneNumber,
'estado' => 'pendiente',
'fecha_envio' => date('Y-m-d H:i:s'),
]);
} catch (Exception $e) {
error_log('[BotService] sendTermsMessage insert terms_acceptance: ' . $e->getMessage());
}
error_log("[BotService] sendTermsMessage sent terms v{$terms['version']} to {$phoneNumber}");
}
/**
* Procesa la respuesta del usuario cuando está en estado terms_pending.
* Detecta "acepto" / "no acepto" / "rechazo".
* Devuelve true si el mensaje fue manejado (para detener el procesamiento normal).
*/
private function processTermsResponse(array $user, string $phoneNumber, string $messageText): bool {
$terms = $this->getActiveTermsVersion();
if (!$terms) {
// Sin versión activa: limpiar el flag y continuar
try {
$this->db->update('users', ['terms_pending' => 0], 'id = :id', ['id' => $user['id']]);
} catch (Exception $e) {}
return false;
}
$normalized = strtolower(trim(
iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $messageText) ?: $messageText
));
// Detectar aceptación: "acepto", "si acepto", "sí, acepto", "1", etc.
$accepted = (bool) preg_match('/\b(acepto|si acepto|si|sí|1|ok|aceptar|de acuerdo|confirmo)\b/', $normalized);
// Detectar rechazo explícito: "no acepto", "rechazo", "no", "0", etc.
$rejected = !$accepted && (bool) preg_match('/\b(no acepto|no|rechazo|rechazar|negar|0|rechaz[oa])\b/', $normalized);
if (!$accepted && !$rejected) {
// Respuesta no reconocida → volver a preguntar
$retry = "No entendí tu respuesta. Por favor responde *Acepto* o *No acepto*.";
$this->whatsappService->sendTextMessage($phoneNumber, $retry);
return true;
}
$estado = $accepted ? 'aceptado' : 'rechazado';
$now = date('Y-m-d H:i:s');
// Actualizar el último registro pendiente de esta versión
try {
$this->db->query(
"UPDATE terms_acceptance SET estado = :estado, fecha_respuesta = :now
WHERE user_id = :uid AND terms_version_id = :vid AND estado = 'pendiente'
ORDER BY id DESC LIMIT 1",
['estado' => $estado, 'now' => $now, 'uid' => $user['id'], 'vid' => $terms['id']]
);
} catch (Exception $e) {
error_log('[BotService] processTermsResponse update acceptance: ' . $e->getMessage());
}
if ($accepted) {
// Actualizar usuario: aceptó términos
try {
$this->db->update('users', [
'terms_pending' => 0,
'terms_accepted_at' => $now,
'terms_version_id' => $terms['id'],
// Limpiar forzar_reenvio para este usuario (lo manejamos por versión)
], 'id = :id', ['id' => $user['id']]);
} catch (Exception $e) {
error_log('[BotService] processTermsResponse update user accepted: ' . $e->getMessage());
}
// Continuar con el flujo normal (welcome si es nuevo)
if ($this->isNewUser($user['id'])) {
$this->sendWelcomeMessage($phoneNumber);
} else {
// Ya había interactuado antes → solo confirmar y mostrar menú
$this->showMainMenu($phoneNumber);
}
} else {
// Rechazó — enviar mensaje de rechazo y dejar terms_pending=1 para que pueda aceptar después
$rejMsg = !empty($terms['mensaje_rechazo'])
? $terms['mensaje_rechazo']
: getConfigFromDB('terms_rejected_message', 'Lamentablemente no podemos proceder sin tu aceptación. Cuando estés listo, responde *Acepto*.');
$this->whatsappService->sendTextMessage($phoneNumber, $rejMsg);
// Mantener terms_pending = 1 para que la próxima respuesta sea evaluada
}
error_log("[BotService] processTermsResponse user {$user['id']} {$estado} terms v{$terms['version']}");
return true;
}
// =========================================================================
// FIN TÉRMINOS Y CONDICIONES
// =========================================================================
/**
* Verificar si es un nuevo usuario
* Ahora: reenvía el welcome si no se envió o si el último welcome fue hace >= 6 horas