Files
sirpremiumv2/app/Http/Livewire/Chat/PublicChat.php
T
LizandroandClaude Sonnet 4.6 932a9b43c7 Saltar selección de monto si el cliente lo incluye en el mensaje de recarga
Tanto en Telegram como en el chat web, cuando el usuario escribe el monto
directamente (ej: "recargar 10.000"), el bot ahora va directo al paso de
método de pago sin volver a preguntar el monto.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 11:34:46 +00:00

1708 lines
72 KiB
PHP
Executable File

<?php
namespace App\Http\Livewire\Chat;
use App\Mail\ChatOtpMail;
use App\Models\Role;
use App\Models\Cuentas;
use App\Models\ChatContact;
use Illuminate\Support\Facades\DB;
use App\Models\ChatConversation;
use App\Models\ChatMessage;
use App\Models\Historiale;
use App\Models\Historial_cuenta;
use App\Models\Preferencial;
use App\Models\Promociones;
use App\Services\CuentasHelper;
use App\Models\TarifaPromo;
use App\Models\Usuario_promo;
use App\Models\recarga;
use App\Models\Saldo;
use App\Models\Servicio;
use App\Models\Tarifas;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Livewire\Component;
use Livewire\WithFileUploads;
class PublicChat extends Component
{
use WithFileUploads;
// ── Paso actual ───────────────────────────────────────────
public string $paso = 'email'; // email | verificacion | registro | chat
public string $email = '';
public string $nombreRegistro = '';
public string $cedulaRegistro = '';
public string $celularRegistro = '';
public string $codigoIngresado = '';
public ?string $emailMascarado = null;
public ?string $errorEnvioOtp = null;
public ?int $contactIdPending = null; // contact.id mientras espera verificacion
public string $input = '';
public array $mensajes = [];
public ?int $convId = null;
public string $estadoConv = 'bot';
// ── Usuario identificado ──────────────────────────────────
public ?int $userId = null;
public ?string $nombreUsuario = null;
public ?float $saldoUsuario = null;
// ── Estado del wizard ─────────────────────────────────────
public string $flujo = '';
public array $flujoData = [];
// ── Upload comprobante ────────────────────────────────────
public $fotoComprobante = null;
public bool $procesandoImagen = false;
public function mount(): void
{
$userId = session('chat_user_id');
if (! $userId) {
return;
}
$user = User::with('saldo')->find($userId);
if (! $user) {
session()->forget(['chat_user_id', 'chat_conv_id']);
return;
}
$this->userId = $user->id;
$this->nombreUsuario = $user->name;
$this->saldoUsuario = $user->saldo?->valor ?? 0;
$this->paso = 'chat';
$convId = session('chat_conv_id');
$conv = $convId ? ChatConversation::find($convId) : null;
if (! $conv) {
// Sesión perdida o expirada: buscar conversación activa existente o crear una nueva
session()->forget('chat_conv_id');
$contact = ChatContact::where('canal', 'web')->where('user_id', $user->id)->first();
if ($contact) {
$conv = $contact->activeConversation()
?? ChatConversation::create([
'contact_id' => $contact->id,
'canal' => 'web',
'estado' => 'bot',
'ultimo_mensaje_at' => now(),
]);
}
}
if ($conv) {
$this->convId = $conv->id;
$this->estadoConv = $conv->estado;
session()->put('chat_conv_id', $conv->id);
$this->cargarMensajes();
}
}
protected $rules = [
'email' => 'required|email|max:100',
'nombreRegistro' => 'required|max:80',
'cedulaRegistro' => 'nullable|max:20',
'celularRegistro' => 'nullable|max:20',
'codigoIngresado' => 'nullable|digits:6',
];
// ─────────────────────────────────────────────────────────
// Paso 1: buscar por correo
// ─────────────────────────────────────────────────────────
public function iniciar(): void
{
$this->validate(['email' => 'required|email|max:100']);
$user = User::where('email', $this->email)->first();
if (! $user) {
$this->paso = 'registro';
return;
}
$contact = ChatContact::firstOrCreate(
['canal' => 'web', 'canal_id' => $user->email],
['nombre' => $user->name, 'telefono' => $user->email, 'user_id' => $user->id]
);
if (! $contact->user_id) {
$contact->update(['user_id' => $user->id]);
}
$this->contactIdPending = $contact->id;
$this->emailMascarado = $this->enmascararEmail($user->email);
$this->enviarCodigoOtp($contact->id, $user);
$this->paso = 'verificacion';
}
// ─────────────────────────────────────────────────────────
// Paso 2a: crear cuenta nueva
// ─────────────────────────────────────────────────────────
public function crearCuenta(): void
{
$this->validate([
'email' => 'required|email|max:100',
'nombreRegistro' => 'required|max:80',
'cedulaRegistro' => 'nullable|max:20',
'celularRegistro' => 'nullable|max:20',
]);
$rolCliente = Role::where('nombre', 'cliente')->first();
$user = User::create([
'name' => $this->nombreRegistro,
'email' => $this->email,
'password' => Hash::make(Str::random(16)),
'cedula' => $this->cedulaRegistro ?: null,
'celular' => $this->celularRegistro ?: null,
'estado' => 'activo',
'rol_id' => $rolCliente?->id,
]);
$contact = ChatContact::create([
'canal' => 'web',
'canal_id' => $user->email,
'nombre' => $user->name,
'telefono' => $user->email,
'user_id' => $user->id,
]);
$this->contactIdPending = $contact->id;
$this->emailMascarado = $this->enmascararEmail($user->email);
$this->enviarCodigoOtp($contact->id, $user);
$this->paso = 'verificacion';
}
public function rechazarRegistro(): void
{
$this->email = '';
$this->nombreRegistro = '';
$this->cedulaRegistro = '';
$this->celularRegistro = '';
$this->paso = 'email';
}
public function confirmarCierreSesion(): void
{
if (! $this->convId) {
return;
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => '¿Seguro que deseas cerrar sesión? Tendrás que volver a ingresar tu correo y código la próxima vez.',
'payload' => ['botones' => [
['label' => '✅ Sí, cerrar sesión', 'action' => 'sesion.cerrar.confirmar', 'data' => []],
['label' => '❌ No, quedarme', 'action' => 'menu.principal', 'data' => []],
]],
'leido' => true,
]);
}
public function limpiarChat(): void
{
$this->flujo = '';
$this->flujoData = [];
$this->mensajes = [];
// Crear nueva conversación en lugar de dejar convId en null
if ($this->userId) {
$contact = ChatContact::where('canal', 'web')->where('user_id', $this->userId)->first();
if ($contact) {
$conv = ChatConversation::create([
'contact_id' => $contact->id,
'canal' => 'web',
'estado' => 'bot',
'ultimo_mensaje_at' => now(),
]);
$this->convId = $conv->id;
$this->estadoConv = 'bot';
session()->put('chat_conv_id', $conv->id);
$this->mostrarMenuPrincipal($conv->id);
$this->cargarMensajes();
}
}
}
public function cerrarSesion(): void
{
session()->forget(['chat_user_id', 'chat_conv_id']);
$this->email = '';
$this->nombreRegistro = '';
$this->cedulaRegistro = '';
$this->celularRegistro = '';
$this->codigoIngresado = '';
$this->emailMascarado = null;
$this->contactIdPending = null;
$this->userId = null;
$this->nombreUsuario = null;
$this->saldoUsuario = null;
$this->convId = null;
$this->estadoConv = 'bot';
$this->flujo = '';
$this->flujoData = [];
$this->mensajes = [];
$this->input = '';
$this->paso = 'email';
}
// ─────────────────────────────────────────────────────────
// Paso 2b: verificar código OTP
// ─────────────────────────────────────────────────────────
private function enviarCodigoOtp(int $contactId, User $user): bool
{
$codigo = (string) random_int(100000, 999999);
Cache::put("chat_otp_{$contactId}", $codigo, now()->addMinutes(10));
try {
Mail::to($user->email)->send(new ChatOtpMail($codigo, $user->name));
$this->errorEnvioOtp = null;
return true;
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::error('[ChatOTP] Error enviando correo a ' . $user->email . ': ' . $e->getMessage(), [
'exception' => $e,
'mailer' => config('mail.default'),
'host' => config('mail.mailers.smtp.host'),
]);
$this->errorEnvioOtp = 'No pudimos enviar el correo. Verifica tu email o intenta reenviar.';
return false;
}
}
public function verificarCodigo(): void
{
$this->validate(['codigoIngresado' => 'required|digits:6']);
if (! $this->contactIdPending) {
$this->addError('codigoIngresado', 'Sesion expirada. Recarga la pagina.');
return;
}
$codigoGuardado = Cache::get("chat_otp_{$this->contactIdPending}");
if (! $codigoGuardado) {
$this->addError('codigoIngresado', 'El codigo expiro. Solicita uno nuevo.');
return;
}
if ($this->codigoIngresado !== $codigoGuardado) {
$this->addError('codigoIngresado', 'Codigo incorrecto. Intentalo de nuevo.');
return;
}
Cache::forget("chat_otp_{$this->contactIdPending}");
$contact = ChatContact::find($this->contactIdPending);
if (! $contact) {
$this->addError('codigoIngresado', 'Sesion invalida. Recarga la pagina.');
return;
}
$this->contactIdPending = null;
$this->codigoIngresado = '';
$this->abrirChat($contact);
}
public function reenviarCodigo(): void
{
if (! $this->contactIdPending) {
return;
}
$contact = ChatContact::find($this->contactIdPending);
if (! $contact || ! $contact->user_id) {
return;
}
$user = User::find($contact->user_id);
if ($user) {
$ok = $this->enviarCodigoOtp($this->contactIdPending, $user);
if ($ok) {
session()->flash('otp_reenviado', 'Código reenviado a tu correo.');
}
}
}
// ─────────────────────────────────────────────────────────
// Abrir chat (después de identificación)
// ─────────────────────────────────────────────────────────
private function abrirChat(ChatContact $contact): void
{
if ($contact->user_id) {
$user = User::with('saldo')->find($contact->user_id);
$this->userId = $user->id;
$this->nombreUsuario = $user->name;
$this->saldoUsuario = $user->saldo?->valor ?? 0;
}
$conv = $contact->activeConversation();
$esNueva = ! $conv;
if ($esNueva) {
$conv = ChatConversation::create([
'contact_id' => $contact->id,
'canal' => 'web',
'estado' => 'bot',
'ultimo_mensaje_at' => now(),
]);
$saludo = $this->nombreUsuario
? "¡Hola, {$this->nombreUsuario}! Bienvenido."
: "¡Hola! Bienvenido.";
$this->guardarMensajeBot($conv->id, $saludo);
} else {
// Conversación existente: el usuario acaba de verificarse por OTP
$saludo = $this->nombreUsuario
? "Bienvenido de nuevo, {$this->nombreUsuario}!"
: "Bienvenido de nuevo!";
$this->guardarMensajeBot($conv->id, $saludo);
}
$this->convId = $conv->id;
$this->estadoConv = $conv->estado;
$this->paso = 'chat';
session()->put('chat_user_id', $this->userId);
session()->put('chat_conv_id', $conv->id);
// Siempre muestra el menú al autenticarse (mount no llama a este método)
$this->mostrarMenuPrincipal($conv->id);
$this->cargarMensajes();
}
private function enmascararEmail(string $email): string
{
[$local, $dominio] = explode('@', $email, 2);
$n = min(4, mb_strlen($local));
$visible = mb_substr($local, 0, $n);
return $visible . str_repeat('*', max(0, mb_strlen($local) - $n)) . '@' . $dominio;
}
// ─────────────────────────────────────────────────────────
// Envío de texto libre
// ─────────────────────────────────────────────────────────
public function enviar(): void
{
if (! $this->convId || trim($this->input) === '') {
return;
}
$texto = trim($this->input);
$this->input = '';
$this->guardarMensajeUsuario($this->convId, $texto);
$conv = ChatConversation::find($this->convId);
if (! $conv || $conv->estado === 'agente') {
$this->cargarMensajes();
return;
}
// Capturar nombre del titular bancario para validación de recarga
if ($this->flujo === 'pago.esperando_nombre') {
$nombre = trim($texto);
$palabras = array_filter(explode(' ', $nombre), fn($p) => strlen($p) >= 2);
if (count($palabras) < 2) {
$this->guardarMensajeBot($this->convId, "Escribe al menos nombre y apellido. Ejemplo: *Juan Garcia*");
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
return;
}
$monto = (int) ($this->flujoData['monto'] ?? 0);
$this->flujoData['nombre_remitente'] = $nombre;
$this->flujo = '';
$this->mostrarTransferenciaDatos($monto);
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
return;
}
// Capturar monto personalizado de recarga
if ($this->flujo === 'recarga.monto_custom') {
$monto = (int) preg_replace('/\D/', '', $texto);
if ($monto >= 1000) {
$this->flujo = '';
$this->flujoData = [];
$this->elegirMetodoRecarga($monto);
} else {
$this->guardarMensajeBot($this->convId, "El monto debe ser al menos $1.000. Escríbelo de nuevo:");
}
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
return;
}
$accion = $this->detectarIntencion($texto);
if ($accion) {
$this->clickBoton($accion['action'], $accion['data'] ?? []);
} else {
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "No entendí bien tu mensaje. Puedes escribir cosas como:\n\"ver planes\", \"hacer recarga\", \"mis credenciales\", \"ver promociones\" o usar los botones:",
'payload' => ['botones' => [
['label' => 'Ver planes', 'action' => 'servicios.listar', 'data' => []],
['label' => 'Recargar saldo', 'action' => 'recarga.iniciar', 'data' => []],
['label' => 'Mis credenciales','action' => 'credenciales.listar','data' => []],
['label' => 'Menú principal', 'action' => 'menu.principal', 'data' => []],
]],
'leido' => true,
]);
}
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
// ─────────────────────────────────────────────────────────
// Clic en botón
// ─────────────────────────────────────────────────────────
public function clickBoton(string $action, array $data = []): void
{
if (! $this->convId) {
return;
}
match (true) {
$action === 'menu.principal' => $this->mostrarMenuPrincipal($this->convId),
$action === 'servicios.listar' => $this->listarServicios(),
$action === 'servicios.ver' => $this->verServicio((int) ($data['id'] ?? 0)),
$action === 'compra.iniciar' => $this->iniciarCompra((int) ($data['tarifa_id'] ?? 0)),
$action === 'compra.pagar.saldo' => $this->pagarConSaldo(),
$action === 'promo.listar' => $this->listarPromociones(),
$action === 'promo.ver' => $this->verPromocion((int) ($data['id'] ?? 0)),
$action === 'promo.comprar.saldo' => $this->comprarPromoConSaldo((int) ($data['id'] ?? 0)),
$action === 'recarga.iniciar' => isset($data['monto']) && (int) $data['monto'] >= 1000
? $this->elegirMetodoRecarga((int) $data['monto'])
: $this->mostrarFormRecarga(),
$action === 'recarga.otro_valor' => $this->solicitarMontoPersonalizado(),
$action === 'recarga.elegir_metodo' => $this->elegirMetodoRecarga((int) ($data['monto'] ?? 0)),
$action === 'recarga.transferencia' => $this->mostrarTransferencia((int) ($data['monto'] ?? 0)),
$action === 'credenciales.listar' => $this->listarCredenciales(),
$action === 'historial.ver' => $this->verHistorial(),
$action === 'perfil.ver' => $this->verPerfil(),
$action === 'asesor.solicitar' => $this->solicitarAsesor(),
$action === 'pago.aplicar' => $this->aplicarRecargaValidada($data),
$action === 'pago.reintentar' => $this->reintentarValidacionPago(),
$action === 'pago.nombre_previo' => $this->usarNombrePrevio($data['nombre'] ?? ''),
$action === 'pago.nombre_nuevo' => $this->pedirNombreNuevo(),
$action === 'sesion.cerrar' => $this->confirmarCierreSesion(),
$action === 'sesion.cerrar.confirmar' => $this->cerrarSesion(),
default => $this->mostrarMenuPrincipal($this->convId),
};
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
// ─────────────────────────────────────────────────────────
// Upload comprobante de pago
// ─────────────────────────────────────────────────────────
public function updatedFotoComprobante(): void
{
if (! $this->fotoComprobante || ! $this->convId) {
return;
}
if (\App\Models\ChatConfig::get('validacion_foto_habilitada', '0') !== '1') {
$this->guardarMensajeBot($this->convId, "El analisis de comprobantes no esta habilitado. Contacta a un asesor.");
$this->fotoComprobante = null;
$this->cargarMensajes();
return;
}
$this->procesandoImagen = true;
try {
// Verificar solicitud activa ANTES de cualquier llamada de API
$solicitud = $this->userId ? \App\Models\SolicitudRecarga::pendiente($this->userId) : null;
if (! $solicitud) {
$this->procesandoImagen = false;
$this->fotoComprobante = null;
$this->guardarMensajeBot($this->convId, "⚠️ No tienes ninguna solicitud de recarga activa. Primero inicia el proceso de recarga desde el menú.");
$this->cargarMensajes();
return;
}
$path = $this->fotoComprobante->store('comprobantes', 'public');
$url = asset('storage/' . $path);
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'usuario',
'tipo_ui' => 'imagen',
'contenido' => '[Comprobante de pago]',
'payload' => ['url' => $url],
'leido' => false,
]);
$this->guardarMensajeBot($this->convId, "⏳ Analizando tu comprobante...");
$base64 = base64_encode(file_get_contents($this->fotoComprobante->getRealPath()));
$mimeType = $this->fotoComprobante->getMimeType();
$datosPago = app(\App\Services\GeminiVisionService::class)->extraerPago($base64, $mimeType);
if (! $datosPago) {
$this->guardarMensajeBot($this->convId, "No pude leer el comprobante. Intenta con una foto mas clara.");
$this->cargarMensajes();
return;
}
// Verificar que el monto coincida con la solicitud
$valorIA = (int) ($datosPago['valor'] ?? 0);
if ($valorIA !== $solicitud->monto) {
$this->guardarMensajeBot($this->convId, "El monto del comprobante ($" . number_format($valorIA) . ") no coincide con tu solicitud de recarga ($" . number_format($solicitud->monto) . ").");
$this->cargarMensajes();
return;
}
$nombreParaValidar = $this->flujoData['nombre_remitente'] ?? $this->nombreUsuario;
$resultado = app(\App\Services\PagoValidadorService::class)->validar(
$datosPago, $this->userId, $nombreParaValidar, 'web'
);
$motivo = $resultado['motivo'] ?? '';
if ($resultado['estado'] === 'confirmado') {
$this->autoAplicarRecargaWeb($solicitud, $solicitud->monto);
return;
} elseif ($resultado['estado'] === 'ya_usado') {
$this->flujo = '';
$this->flujoData = [];
$this->guardarMensajeBot($this->convId, "⛔ Comprobante ya utilizado. Este pago ya fue registrado anteriormente.");
} elseif ($motivo === 'sin_coincidencia' || $motivo === 'sin_correos') {
$this->flujo = 'pago.pendiente';
$this->flujoData = array_merge($this->flujoData, ['datosPago' => $datosPago, 'intentos' => 0]);
$this->guardarMensajeBot($this->convId,
"⏳ *Estamos validando tu pago*, esto puede demorar unos minutos.\n"
. "Cuando terminemos te confirmaremos por este medio. Espera un momento."
);
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
return;
} else {
$motivoTexto = match ($motivo) {
'correo_deshabilitado' => 'Verificacion por correo no configurada',
'error_imap' => 'Error al leer correos IMAP',
'error_sistema' => 'Error interno, intenta de nuevo en unos minutos',
default => 'No se pudo verificar automaticamente',
};
$this->guardarMensajeBot($this->convId, "⚠️ No confirmado: {$motivoTexto}");
}
} catch (\Throwable $e) {
$this->guardarMensajeBot($this->convId, "Error analizando el comprobante. Por favor intenta de nuevo.");
} finally {
$this->procesandoImagen = false;
$this->fotoComprobante = null;
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
}
// ─────────────────────────────────────────────────────────
// Menu principal
// ─────────────────────────────────────────────────────────
private function mostrarMenuPrincipal(int $convId): void
{
$this->flujo = '';
$this->flujoData = [];
ChatMessage::create([
'conversation_id' => $convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => 'En que te puedo ayudar?',
'payload' => ['botones' => [
['label' => 'Ver Servicios', 'action' => 'servicios.listar', 'data' => []],
['label' => 'Promociones', 'action' => 'promo.listar', 'data' => []],
['label' => 'Recargar Saldo', 'action' => 'recarga.iniciar', 'data' => []],
['label' => 'Mis Credenciales', 'action' => 'credenciales.listar', 'data' => []],
['label' => 'Historial', 'action' => 'historial.ver', 'data' => []],
['label' => 'Mi Perfil', 'action' => 'perfil.ver', 'data' => []],
['label' => 'Hablar con asesor', 'action' => 'asesor.solicitar', 'data' => []],
['label' => '🚪 Cerrar sesión', 'action' => 'sesion.cerrar', 'data' => []],
]],
'leido' => true,
]);
}
// ─────────────────────────────────────────────────────────
// Servicios
// ─────────────────────────────────────────────────────────
private function listarServicios(): void
{
$rolId = $this->userId ? User::find($this->userId)?->rol_id : Role::where('nombre', 'cliente')->value('id');
$servicios = Servicio::where('estado', 'activo')
->whereHas('tarifas', fn ($q) => $q->where('estado', 'activo')->where('rol_id', $rolId))
->orderBy('ubicacion')
->get();
if ($servicios->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No hay servicios disponibles en este momento.");
$this->volver('menu.principal');
return;
}
$cards = $servicios->map(fn ($s) => [
'imagen' => $s->img_inicio,
'titulo' => $s->nombre,
'detalle' => $s->por_tiempo ? 'Por tiempo' : 'Por pantallas',
'accion' => ['label' => 'Ver planes', 'action' => 'servicios.ver', 'data' => ['id' => $s->id]],
])->values()->all();
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'cards_slider',
'contenido' => 'Selecciona el servicio:',
'payload' => ['cards' => $cards],
'leido' => true,
]);
$this->volver('menu.principal');
}
private function verServicio(int $id): void
{
$rolId = $this->userId ? User::find($this->userId)?->rol_id : Role::where('nombre', 'cliente')->value('id');
$servicio = Servicio::with(['tarifas' => fn ($q) => $q->where('estado', 'activo')->where('rol_id', $rolId)])->find($id);
if (! $servicio) {
$this->guardarMensajeBot($this->convId, "Servicio no encontrado.");
return;
}
$tarifas = $servicio->tarifas;
if ($tarifas->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No hay planes para {$servicio->nombre} en este momento.");
$this->volver('servicios.listar');
return;
}
$botones = [];
foreach ($tarifas as $t) {
$precio = $this->efectivePrice($t->id, $this->userId, $t->valor);
$label = $servicio->por_tiempo
? "{$t->dias} dias - $" . number_format($precio)
: "{$t->pantallas} pantalla(s) / {$t->dias} dias - $" . number_format($precio);
$botones[] = ['label' => $label, 'action' => 'compra.iniciar', 'data' => ['tarifa_id' => $t->id]];
}
$botones[] = ['label' => 'Volver', 'action' => 'servicios.listar', 'data' => []];
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "Planes de {$servicio->nombre}:",
'payload' => ['botones' => $botones],
'leido' => true,
]);
}
// ─────────────────────────────────────────────────────────
// Compra de plan
// ─────────────────────────────────────────────────────────
private function iniciarCompra(int $tarifaId): void
{
$tarifa = Tarifas::with('servicio')->find($tarifaId);
if (! $tarifa) {
$this->guardarMensajeBot($this->convId, "Plan no encontrado.");
return;
}
if ($this->contarDisponibles($tarifa) < 1) {
$this->guardarMensajeBot($this->convId, "Sin cuentas disponibles para este plan. Intenta con otro.");
$this->volver('servicios.listar');
return;
}
$precio = $this->efectivePrice($tarifaId, $this->userId, $tarifa->valor);
$this->flujo = 'compra';
$this->flujoData = [
'tarifa_id' => $tarifaId,
'valor' => $precio,
'servicio' => $tarifa->servicio->nombre,
'dias' => $tarifa->dias,
'pantallas' => $tarifa->pantallas,
];
$detalle = $tarifa->servicio->por_tiempo
? "{$tarifa->dias} dias"
: "{$tarifa->pantallas} pantalla(s) / {$tarifa->dias} dias";
if ((float) $this->saldoUsuario >= $precio) {
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "{$tarifa->servicio->nombre} - {$detalle}\nPrecio: $" . number_format($precio) . "\n\nElige como pagar:",
'payload' => ['botones' => [
['label' => 'Pagar con saldo ($' . number_format($precio) . ')', 'action' => 'compra.pagar.saldo', 'data' => []],
['label' => 'Volver', 'action' => 'servicios.listar', 'data' => []],
]],
'leido' => true,
]);
} else {
$falta = $precio - (float) $this->saldoUsuario;
// Guardar compra pendiente para ejecutarla automáticamente tras recargar
$this->flujoData = array_merge($this->flujoData, [
'pending_purchase' => [
'type' => 'tarifa',
'tarifa_id' => $tarifaId,
'valor' => $precio,
'servicio' => $tarifa->servicio->nombre,
'dias' => $tarifa->dias,
'pantallas' => $tarifa->pantallas,
],
]);
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "{$tarifa->servicio->nombre} - {$detalle}\nPrecio: $" . number_format($precio) . "\n\nSaldo actual: $" . number_format($this->saldoUsuario) . " — te faltan $" . number_format($falta) . "\n\nRecarga para comprar:",
'payload' => ['botones' => [
['label' => "💰 Recargar $" . number_format($falta) . " para comprar {$tarifa->servicio->nombre}", 'action' => 'recarga.iniciar', 'data' => []],
['label' => 'Volver', 'action' => 'servicios.listar', 'data' => []],
]],
'leido' => true,
]);
}
}
private function pagarConSaldo(): void
{
if (! $this->userId || $this->flujo !== 'compra') {
$this->guardarMensajeBot($this->convId, "Debes estar identificado para pagar con saldo.");
return;
}
$tarifa = Tarifas::with('servicio')->find($this->flujoData['tarifa_id'] ?? 0);
if (! $tarifa) {
$this->guardarMensajeBot($this->convId, "Ocurrio un error. Intenta de nuevo.");
return;
}
$user = User::with('saldo')->find($this->userId);
$saldo = $user->saldo;
$precio = (float) ($this->flujoData['valor'] ?? $tarifa->valor);
if (! $saldo || $saldo->valor < $precio) {
$this->guardarMensajeBot(
$this->convId,
"Saldo insuficiente. Tienes $" . number_format($saldo?->valor ?? 0) .
" y el plan cuesta $" . number_format($precio) . "."
);
$this->volver('menu.principal');
return;
}
$cuenta = $this->buscarCuentaDisponible($tarifa);
if (! $cuenta) {
$this->guardarMensajeBot($this->convId, "No hay cuentas disponibles en este momento. Intenta más tarde o contacta a un asesor.");
return;
}
$nuevoSaldo = $saldo->valor - $precio;
DB::transaction(function () use ($tarifa, $user, $saldo, $cuenta, $precio, $nuevoSaldo) {
$historial = Historiale::create([
'fecha_inicio' => now(),
'fecha_final' => now()->addDays($tarifa->dias),
'valor' => $precio,
'utilidad' => $tarifa->utilidad ?? 0,
'tipo_pago' => 'saldo',
'estado' => 'entregado',
'vendedor_id' => $this->userId,
'tarifa_id' => $tarifa->id,
'cliente_id' => $this->userId,
'nombre_cliente' => $user->name,
]);
Historial_cuenta::create(['historial_id' => $historial->id, 'cuenta_id' => $cuenta->id]);
$saldo->update(['valor' => $nuevoSaldo]);
});
$this->saldoUsuario = $nuevoSaldo;
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'credenciales',
'contenido' => 'Compra exitosa! Tus credenciales:',
'payload' => [
'servicio' => $tarifa->servicio->nombre,
'email' => $cuenta->correo,
'password' => $cuenta->password,
'perfil' => $tarifa->pantallas ?? null,
'vence' => now()->addDays($tarifa->dias)->format('d/m/Y'),
],
'leido' => true,
]);
$this->flujo = '';
$this->flujoData = [];
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Promociones
// ─────────────────────────────────────────────────────────
private function listarPromociones(): void
{
$rolId = $this->userId
? User::find($this->userId)?->rol_id
: Role::where('nombre', 'cliente')->value('id');
$promos = Promociones::where('visible', 'true')
->where(fn ($q) => $q->whereNull('fecha_limite')->orWhere('fecha_limite', '>=', now()))
->with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])
->get();
if ($promos->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No hay promociones activas en este momento.");
$this->volver('menu.principal');
return;
}
$cards = $promos->map(function ($p) {
$tp = $p->tarifaPromo->first();
$upric = $this->userId
? Usuario_promo::where('usuario_id', $this->userId)->where('promocion_id', $p->id)->first()
: null;
$precio = $upric ? $upric->precio : ($tp ? $tp->precio : $p->precio);
return [
'imagen' => $p->img_publicidad,
'titulo' => $p->nombre ?? 'Promocion especial',
'descripcion' => $p->descripcion ?? null,
'precio' => $precio,
'detalle' => $p->fecha_limite ? 'Hasta ' . Carbon::parse($p->fecha_limite)->format('d/m/Y') : null,
'accion' => ['label' => 'Comprar', 'action' => 'promo.ver', 'data' => ['id' => $p->id]],
];
})->values()->all();
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'cards_slider',
'contenido' => 'Promociones disponibles:',
'payload' => ['cards' => $cards],
'leido' => true,
]);
$this->volver('menu.principal');
}
private function verPromocion(int $id): void
{
$rolId = $this->userId
? User::find($this->userId)?->rol_id
: Role::where('nombre', 'cliente')->value('id');
$promo = Promociones::with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])->find($id);
if (! $promo) {
$this->guardarMensajeBot($this->convId, "Promocion no encontrada.");
return;
}
$upric = $this->userId
? Usuario_promo::where('usuario_id', $this->userId)->where('promocion_id', $id)->first()
: null;
$tp = $promo->tarifaPromo->first();
$precio = $upric ? $upric->precio : ($tp ? $tp->precio : $promo->precio);
if ((float) $this->saldoUsuario >= $precio) {
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => ($promo->nombre ?? 'Promocion') . "\n$" . number_format($precio) . "\n\nElige como pagar:",
'payload' => ['botones' => [
['label' => 'Pagar con saldo ($' . number_format($precio) . ')', 'action' => 'promo.comprar.saldo', 'data' => ['id' => $id]],
['label' => 'Volver', 'action' => 'promo.listar', 'data' => []],
]],
'leido' => true,
]);
} else {
$falta = $precio - (float) $this->saldoUsuario;
// Guardar compra pendiente para ejecutarla automáticamente tras recargar
$this->flujoData = array_merge($this->flujoData, [
'pending_purchase' => [
'type' => 'promo',
'promo_id' => $id,
'valor' => $precio,
'nombre' => $promo->nombre ?? 'Promoción',
],
]);
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => ($promo->nombre ?? 'Promocion') . "\n$" . number_format($precio) . "\n\nSaldo actual: $" . number_format($this->saldoUsuario) . " — te faltan $" . number_format($falta) . "\n\nRecarga para comprar:",
'payload' => ['botones' => [
['label' => "💰 Recargar $" . number_format($falta) . " para comprar " . ($promo->nombre ?? 'Promo'), 'action' => 'recarga.iniciar', 'data' => []],
['label' => 'Volver', 'action' => 'promo.listar', 'data' => []],
]],
'leido' => true,
]);
}
}
private function comprarPromoConSaldo(int $promoId): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "No se pudo procesar la compra.");
return;
}
$user = User::with('saldo')->find($this->userId);
$rolId = $user->rol_id;
$promo = Promociones::with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])->find($promoId);
if (! $promo) {
$this->guardarMensajeBot($this->convId, "No se pudo procesar la compra.");
return;
}
$upric = Usuario_promo::where('usuario_id', $this->userId)->where('promocion_id', $promoId)->first();
$tp = $promo->tarifaPromo->first();
$precio = $upric ? $upric->precio : ($tp ? $tp->precio : $promo->precio);
$utilidad = $upric ? $upric->utilidad : ($tp ? $tp->utilidad : 0);
$saldo = $user->saldo;
if (! $saldo || $saldo->valor < $precio) {
$this->guardarMensajeBot($this->convId, "Saldo insuficiente ($" . number_format($saldo?->valor ?? 0) . "). La promo cuesta $" . number_format($precio) . ".");
return;
}
$cuenta = Cuentas::where('promocion_id', $promoId)->where('estado', 'activo')->first();
if (! $cuenta) {
$this->guardarMensajeBot($this->convId, "No hay cuentas disponibles para esta promocion.");
return;
}
$historial = Historiale::create([
'fecha_inicio' => now(),
'fecha_final' => now()->addDays(30),
'valor' => $precio,
'utilidad' => $utilidad,
'tipo_pago' => 'saldo',
'estado' => 'entregado',
'vendedor_id' => $this->userId,
'promocion_id' => $promoId,
'cliente_id' => $this->userId,
'nombre_cliente' => $user->name,
]);
$nuevoSaldo = $saldo->valor - $precio;
DB::transaction(function () use ($historial, $cuenta, $saldo, $nuevoSaldo) {
Historial_cuenta::create(['historial_id' => $historial->id, 'cuenta_id' => $cuenta->id]);
$cuenta->update(['estado' => 'ocupado']);
$saldo->update(['valor' => $nuevoSaldo]);
});
$this->saldoUsuario = $nuevoSaldo;
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'credenciales',
'contenido' => 'Promo activada! Tus credenciales:',
'payload' => [
'servicio' => $promo->nombre ?? 'Promocion',
'email' => $cuenta->correo,
'password' => $cuenta->password,
'vence' => now()->addDays(30)->format('d/m/Y'),
],
'leido' => true,
]);
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Recarga de saldo
// ─────────────────────────────────────────────────────────
private function mostrarFormRecarga(): void
{
$botones = [];
foreach ([20000, 50000, 100000, 200000, 500000] as $m) {
$botones[] = ['label' => '$' . number_format($m), 'action' => 'recarga.elegir_metodo', 'data' => ['monto' => $m]];
}
$botones[] = ['label' => 'Otro valor', 'action' => 'recarga.otro_valor', 'data' => []];
$botones[] = ['label' => 'Volver', 'action' => 'menu.principal', 'data' => []];
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "Selecciona el monto a recargar:",
'payload' => ['botones' => $botones],
'leido' => true,
]);
}
private function solicitarMontoPersonalizado(): void
{
$this->flujo = 'recarga.monto_custom';
$this->flujoData = [];
$this->guardarMensajeBot($this->convId, "Escribe el monto que deseas recargar (mínimo $1.000):");
}
private function elegirMetodoRecarga(int $monto): void
{
if ($monto <= 0) {
return;
}
$llave = \App\Models\ChatConfig::get('recarga_banco_llave', '');
if (! $llave) {
$this->guardarMensajeBot($this->convId, "⚠️ La recarga no está disponible en este momento. Contacta a un asesor.");
$this->volver('menu.principal');
return;
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => "Recarga de $" . number_format($monto) . "\n¿Como quieres pagar?",
'payload' => ['botones' => [
['label' => '🔑 Bre-B', 'action' => 'recarga.transferencia', 'data' => ['monto' => $monto]],
['label' => 'Volver', 'action' => 'recarga.iniciar', 'data' => []],
]],
'leido' => true,
]);
}
private function mostrarTransferencia(int $monto): void
{
if ($monto <= 0) {
return;
}
$this->flujoData = ['monto' => $monto];
$nombresPrevios = $this->userId ? \App\Models\SolicitudRecarga::nombresPrevios($this->userId) : [];
if (! empty($nombresPrevios)) {
$this->flujo = 'pago.esperando_nombre';
$botones = array_map(fn($n) => ['label' => $n, 'action' => 'pago.nombre_previo', 'data' => ['nombre' => $n]], $nombresPrevios);
$botones[] = ['label' => '✏️ Usar otro nombre', 'action' => 'pago.nombre_nuevo', 'data' => []];
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => '👤 ¿Desde qué cuenta vas a transferir? Elige un nombre anterior o usa uno nuevo:',
'payload' => ['botones' => $botones],
'leido' => true,
]);
} else {
$this->flujo = 'pago.esperando_nombre';
$this->guardarMensajeBot($this->convId, "👤 Escribe tu nombre completo tal como aparece en tu cuenta bancaria o billetera:");
}
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
private function usarNombrePrevio(string $nombre): void
{
if (strlen(trim($nombre)) < 3) {
return;
}
$this->flujoData['nombre_remitente'] = trim($nombre);
$this->flujo = '';
$this->mostrarTransferenciaDatos((int) ($this->flujoData['monto'] ?? 0));
}
private function pedirNombreNuevo(): void
{
$this->flujo = 'pago.esperando_nombre';
$this->guardarMensajeBot($this->convId, "👤 Escribe tu nombre completo tal como aparece en tu cuenta bancaria:");
}
private function mostrarTransferenciaDatos(int $monto): void
{
$banco = \App\Models\ChatConfig::get('recarga_banco_nombre', '');
$clave = \App\Models\ChatConfig::get('recarga_banco_llave', '');
$titular = \App\Models\ChatConfig::get('recarga_banco_titular', '');
if (! $clave) {
$this->guardarMensajeBot($this->convId, "El pago por Bre-B no esta disponible en este momento. Contacta a un asesor.");
return;
}
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'banco',
'contenido' => 'Datos para transferencia:',
'payload' => [
'banco' => $banco,
'clave' => $clave,
'titular' => $titular,
'monto' => $monto,
],
'leido' => true,
]);
$this->guardarMensajeBot($this->convId,
"📋 *Al enviar el comprobante, asegúrate de que se vea claramente:*\n"
. "✓ Tu nombre completo\n"
. "✓ El valor transferido (\$" . number_format($monto) . ")\n"
. "✓ La fecha y hora de la transacción\n\n"
. "Cuando hayas transferido, envia el comprobante usando el icono de camara (📷) y el sistema lo validara automaticamente.\n\n"
. "Tienes hasta el final del dia para enviar el soporte."
);
if ($this->userId) {
$nombre = $this->flujoData['nombre_remitente'] ?? null;
$solicitud = \App\Models\SolicitudRecarga::crear($this->userId, $monto, 'web', $nombre);
$this->flujo = 'pago.solicitud';
$this->flujoData = array_merge($this->flujoData, ['solicitud_id' => $solicitud->id, 'monto' => $monto]);
}
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Credenciales
// ─────────────────────────────────────────────────────────
private function listarCredenciales(): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "No encontre tu cuenta. Contacta a un asesor para ver tus credenciales.");
$this->volver('menu.principal');
return;
}
$historiales = Historiale::where('cliente_id', $this->userId)
->where('estado', 'entregado')
->where('fecha_final', '>=', now())
->with('tarifa.servicio')
->orderBy('fecha_final', 'desc')
->limit(10)
->get();
if ($historiales->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No tienes planes activos en este momento.");
$this->volver('menu.principal');
return;
}
$this->guardarMensajeBot($this->convId, "Tus planes activos:");
foreach ($historiales as $h) {
foreach ($h->cuentas as $c) {
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'credenciales',
'contenido' => '',
'payload' => [
'servicio' => $h->tarifa?->servicio?->nombre ?? 'Servicio',
'email' => $c->correo,
'password' => $c->password,
'perfil' => $h->tarifa?->pantallas ?? null,
'vence' => Carbon::parse($h->fecha_final)->format('d/m/Y'),
],
'leido' => true,
]);
}
}
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Historial
// ─────────────────────────────────────────────────────────
private function verHistorial(): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "Inicia sesion para ver tu historial.");
$this->volver('menu.principal');
return;
}
$historiales = Historiale::where('cliente_id', $this->userId)
->with('tarifa.servicio')
->orderBy('created_at', 'desc')
->limit(10)
->get();
if ($historiales->isEmpty()) {
$this->guardarMensajeBot($this->convId, "No tienes compras registradas.");
$this->volver('menu.principal');
return;
}
$lineas = ["Tus ultimas compras:\n"];
foreach ($historiales as $h) {
$activo = Carbon::parse($h->fecha_final)->gte(now()) ? '[activo]' : '[vencido]';
$nombre = $h->tarifa?->servicio?->nombre ?? ($h->promocion_id ? 'Promo' : 'Plan');
$fecha = Carbon::parse($h->created_at)->format('d/m/Y');
$lineas[] = "{$activo} {$nombre} - $" . number_format($h->valor) . " ({$fecha})";
}
$this->guardarMensajeBot($this->convId, implode("\n", $lineas));
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Perfil
// ─────────────────────────────────────────────────────────
private function verPerfil(): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "No encontre tu cuenta. Contacta a un asesor.");
$this->volver('menu.principal');
return;
}
$user = User::with('saldo')->find($this->userId);
$this->guardarMensajeBot(
$this->convId,
"Tu perfil\nNombre: {$user->name}\nEmail: {$user->email}\nSaldo: $" . number_format($user->saldo?->valor ?? 0)
);
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// Asesor
// ─────────────────────────────────────────────────────────
private function solicitarAsesor(): void
{
$conv = ChatConversation::find($this->convId);
if ($conv) {
$conv->update(['estado' => 'agente']);
$this->estadoConv = 'agente';
}
$this->guardarMensajeBot($this->convId, "Un asesor se comunicara contigo en breve. Por favor espera.");
}
// ─────────────────────────────────────────────────────────
// Aplicar recarga validada por IA
// ─────────────────────────────────────────────────────────
// Llamado por wire:poll cada 20s — solo actúa cuando hay un pago pendiente de validar
public function autoReintentarPago(): void
{
if ($this->flujo !== 'pago.pendiente' || empty($this->flujoData['datosPago'])) {
return;
}
$maxIntentos = 9;
$intentos = (int) ($this->flujoData['intentos'] ?? 0);
// Agotados — mostrar botón manual una sola vez
if ($intentos >= $maxIntentos) {
if (! ($this->flujoData['boton_manual_mostrado'] ?? false)) {
$this->flujoData['boton_manual_mostrado'] = true;
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'validacion',
'contenido' => '⚠️ No pudimos confirmar tu pago automáticamente. Si ya realizaste la transferencia, presiona Reintentar.',
'payload' => [
'estado' => 'pendiente',
'motivo' => 'No pudimos confirmar automáticamente.',
'botones' => [
['label' => '🔄 Reintentar', 'action' => 'pago.reintentar', 'data' => []],
['label' => '<- Menú', 'action' => 'menu.principal', 'data' => []],
],
],
'leido' => true,
]);
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
return;
}
$this->flujoData['intentos'] = $intentos + 1;
$solicitud = $this->userId ? \App\Models\SolicitudRecarga::pendiente($this->userId) : null;
if (! $solicitud) {
$this->flujo = '';
$this->flujoData = [];
$this->guardarMensajeBot($this->convId, '⚠️ Tu solicitud de recarga expiró. Inicia una nueva desde el menú.');
$this->cargarMensajes();
return;
}
$datosPago = $this->flujoData['datosPago'];
$nombreParaValidar = $this->flujoData['nombre_remitente'] ?? $this->nombreUsuario;
$resultado = app(\App\Services\PagoValidadorService::class)->validar($datosPago, $this->userId, $nombreParaValidar, 'web');
$motivo = $resultado['motivo'] ?? '';
if ($resultado['estado'] === 'confirmado') {
$this->autoAplicarRecargaWeb($solicitud, $solicitud->monto);
return;
}
if ($resultado['estado'] === 'ya_usado') {
$this->flujo = '';
$this->flujoData = [];
$this->guardarMensajeBot($this->convId, '⛔ Comprobante ya utilizado. Este pago ya fue registrado anteriormente.');
$this->cargarMensajes();
return;
}
// Sigue sin encontrarse — el poll volverá a llamar este método en 20s
}
private function reintentarValidacionPago(): void
{
if ($this->flujo !== 'pago.pendiente' || empty($this->flujoData['datosPago'])) {
$this->guardarMensajeBot($this->convId, 'No hay ningun comprobante pendiente. Envia la foto de tu comprobante.');
return;
}
$solicitud = $this->userId ? \App\Models\SolicitudRecarga::pendiente($this->userId) : null;
if (! $solicitud) {
$this->flujo = '';
$this->flujoData = [];
$this->guardarMensajeBot($this->convId, '⚠️ Tu solicitud de recarga expiro. Inicia una nueva recarga desde el menu.');
$this->cargarMensajes();
return;
}
$datosPago = $this->flujoData['datosPago'];
$nombreParaValidar = $this->flujoData['nombre_remitente'] ?? $this->nombreUsuario;
$resultado = app(\App\Services\PagoValidadorService::class)->validar($datosPago, $this->userId, $nombreParaValidar, 'web');
$motivo = $resultado['motivo'] ?? '';
if ($resultado['estado'] === 'confirmado') {
$this->autoAplicarRecargaWeb($solicitud, $solicitud->monto);
return;
} elseif ($resultado['estado'] === 'ya_usado') {
$this->flujo = '';
$this->flujoData = [];
$this->guardarMensajeBot($this->convId, '⛔ Comprobante ya utilizado. Este pago ya fue registrado anteriormente.');
} elseif ($motivo === 'sin_coincidencia' || $motivo === 'sin_correos') {
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'validacion',
'contenido' => 'Aun no encontramos tu pago.',
'payload' => [
'estado' => 'pendiente',
'motivo' => 'Aun no encontramos tu pago. Puede demorar entre 1 y 2 minutos en llegar.',
'ia_datos' => $datosPago,
'botones' => [
['label' => '🔄 Reintentar', 'action' => 'pago.reintentar', 'data' => []],
['label' => '<- Menu', 'action' => 'menu.principal', 'data' => []],
],
],
'leido' => true,
]);
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
return;
} else {
$this->flujo = '';
$this->flujoData = [];
$this->guardarMensajeBot($this->convId, '⚠️ No se pudo verificar el pago. Intenta de nuevo.');
}
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
private function autoAplicarRecargaWeb(\App\Models\SolicitudRecarga $solicitud, int $monto): void
{
$user = User::with('saldo')->find($this->userId);
$saldo = $user->saldo ?? Saldo::create(['usuario_id' => $this->userId, 'valor' => 0]);
$nuevoSaldo = $saldo->valor + $monto;
$saldo->update(['valor' => $nuevoSaldo]);
$this->saldoUsuario = $nuevoSaldo;
\App\Models\recarga::create([
'usuario_id' => $this->userId,
'saldo_id' => $saldo->id,
'monto' => $monto,
'valor_recarga' => $monto,
'status' => 'Confirmado',
'reference' => 'breb-chat-' . $solicitud->id,
]);
$solicitud->confirmar();
$pp = $this->flujoData['pending_purchase'] ?? null;
$this->flujo = '';
$this->flujoData = [];
$this->guardarMensajeBot(
$this->convId,
"✅ ¡Tu recarga de $" . number_format($monto) . " fue aplicada exitosamente!\nTu nuevo saldo es $" . number_format($nuevoSaldo) . "."
);
if ($pp && ($pp['type'] ?? '') === 'tarifa') {
$this->flujo = 'compra';
$this->flujoData = [
'tarifa_id' => $pp['tarifa_id'],
'valor' => $pp['valor'],
'servicio' => $pp['servicio'],
'dias' => $pp['dias'],
'pantallas' => $pp['pantallas'],
];
$this->pagarConSaldo();
} elseif ($pp && ($pp['type'] ?? '') === 'promo') {
$this->comprarPromoConSaldo($pp['promo_id']);
}
$this->cargarMensajes();
$this->dispatchBrowserEvent('scroll-chat');
}
// ─────────────────────────────────────────────────────────
private function aplicarRecargaValidada(array $data): void
{
if (! $this->userId) {
$this->guardarMensajeBot($this->convId, "No puedo aplicar la recarga: no estas identificado.");
return;
}
$monto = (int) ($data['monto'] ?? 0);
if ($monto <= 0) {
$this->guardarMensajeBot($this->convId, "Monto invalido.");
return;
}
$user = User::with('saldo')->find($this->userId);
$saldo = $user->saldo ?? Saldo::create(['usuario_id' => $this->userId, 'valor' => 0]);
$nuevoSaldo = $saldo->valor + $monto;
$saldo->update(['valor' => $nuevoSaldo]);
$this->saldoUsuario = $nuevoSaldo;
$this->guardarMensajeBot(
$this->convId,
"Recarga de $" . number_format($monto) . " aplicada. Tu nuevo saldo: $" . number_format($nuevoSaldo) . "."
);
$this->volver('menu.principal');
}
// ─────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────
// Detección de intención: keywords primero, Gemini como fallback
// ─────────────────────────────────────────────────────────
private function detectarIntencion(string $texto): ?array
{
return $this->detectarIntencionKeyword($texto)
?? $this->detectarIntencionIA($texto);
}
private function detectarIntencionKeyword(string $texto): ?array
{
$t = mb_strtolower(trim($texto));
$patrones = [
// Recarga de saldo
'recarga.iniciar' => '/recarg|recargar|cargar\s*(saldo|cuenta|dinero|plata)|abonar|depositar|agregar\s*(saldo|dinero|plata)|quiero\s*poner\s*saldo/',
// Ver servicios / planes
'servicios.listar' => '/servicio|plan(es)?|cat[aá]logo|qu[eé]\s*(hay|tienen|tienes)|ver\s*plan|netflix|disney|hbo|prime|spotify|crunchyroll|paramount|streaming|aplicacion|aplicaci[oó]n/',
// Promociones
'promo.listar' => '/promo(ci[oó]n)?|oferta|descuento|especial/',
// Credenciales / acceso
'credenciales.listar' => '/credencial|contrase[ñn]a|clave|password|acceso|usuario|mi\s*cuenta|mis\s*datos\s*de\s*acceso|c[oó]mo\s*(entro|accedo|me\s*conecto)/',
// Historial de compras
'historial.ver' => '/historial|compra(s)?|pedido(s)?|lo\s*que\s*(compr[eé]|he\s*comprado)|mis\s*compras/',
// Perfil
'perfil.ver' => '/perfil|mis\s*datos|mi\s*informaci[oó]n|mis\s*datos\s*personales/',
// Asesor humano
'asesor.solicitar' => '/asesor|agente|persona|humano|soporte|ayuda\s*(de\s*una\s*persona)?|hablar\s*con\s*(alguien|una\s*persona|un\s*asesor)/',
// Menú / saludo
'menu.principal' => '/^(hola|buenas?|buenos?|hey|men[uú]|inicio|principal|volver|regresar|empezar|comenzar|start)[\s!.]*$/',
];
foreach ($patrones as $action => $pattern) {
if (preg_match($pattern, $t)) {
$data = [];
if ($action === 'recarga.iniciar') {
if (preg_match('/\$?\s*(\d{1,3}(?:[.,]\d{3})+|\d{4,})\b/', $t, $m)) {
$monto = (int) preg_replace('/[.,]/', '', $m[1]);
if ($monto >= 1000) {
$data['monto'] = $monto;
}
}
}
return ['action' => $action, 'data' => $data];
}
}
return null;
}
private function detectarIntencionIA(string $texto): ?array
{
try {
return app(\App\Services\GeminiIntentService::class)->detectar($texto);
} catch (\Throwable) {
return null;
}
}
// ─────────────────────────────────────────────────────────
// Cargar mensajes
// ─────────────────────────────────────────────────────────
public function cargarMensajes(): void
{
if (! $this->convId) {
return;
}
$this->mensajes = ChatMessage::where('conversation_id', $this->convId)
->orderBy('id')
->get()
->map(fn ($m) => [
'tipo' => $m->tipo,
'tipo_ui' => $m->tipo_ui ?? 'text',
'contenido' => $m->contenido,
'payload' => $m->payload ?? [],
'created_at' => $m->created_at->format('H:i'),
])
->toArray();
$this->dispatchBrowserEvent('scroll-chat');
}
// Llamado por wire:poll — igual que cargarMensajes pero sin forzar scroll
public function pollMensajes(): void
{
if (! $this->convId) {
return;
}
$this->mensajes = ChatMessage::where('conversation_id', $this->convId)
->orderBy('id')
->get()
->map(fn ($m) => [
'tipo' => $m->tipo,
'tipo_ui' => $m->tipo_ui ?? 'text',
'contenido' => $m->contenido,
'payload' => $m->payload ?? [],
'created_at' => $m->created_at->format('H:i'),
])
->toArray();
// No dispatches scroll-chat — el MutationObserver del JS maneja el badge
}
// ─────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────
private function guardarMensajeBot(int $convId, string $contenido): void
{
ChatMessage::create([
'conversation_id' => $convId,
'tipo' => 'bot',
'tipo_ui' => 'text',
'contenido' => $contenido,
'leido' => true,
]);
}
private function guardarMensajeUsuario(int $convId, string $contenido): void
{
ChatMessage::create([
'conversation_id' => $convId,
'tipo' => 'usuario',
'tipo_ui' => 'text',
'contenido' => $contenido,
'leido' => false,
]);
}
private function contarDisponibles(Tarifas $tarifa): int
{
return CuentasHelper::contar($tarifa);
}
private function buscarCuentaDisponible(Tarifas $tarifa): ?Cuentas
{
return CuentasHelper::buscar($tarifa);
}
private function efectivePrice(int $tarifaId, ?int $userId, float $base): float
{
return CuentasHelper::precio($tarifaId, $userId, $base);
}
private function volver(string $action): void
{
ChatMessage::create([
'conversation_id' => $this->convId,
'tipo' => 'bot',
'tipo_ui' => 'buttons',
'contenido' => '',
'payload' => ['botones' => [
['label' => 'Menu principal', 'action' => $action, 'data' => []],
]],
'leido' => true,
]);
}
public function render()
{
return view('livewire.chat.public-chat');
}
}