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'); if ($convId) { $conv = ChatConversation::find($convId); if ($conv) { $this->convId = $conv->id; $this->estadoConv = $conv->estado; $this->cargarMensajes(); } else { session()->forget('chat_conv_id'); } } } 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->mensajes = []; $this->convId = null; $this->flujo = ''; $this->flujoData = []; $this->estadoConv = 'bot'; session()->forget('chat_conv_id'); // paso, userId, nombreUsuario, saldoUsuario se mantienen — sesión intacta } 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): void { $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)); } catch (\Throwable $e) { \Illuminate\Support\Facades\Log::warning('[ChatOTP] Error enviando correo: ' . $e->getMessage()); } } 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) { $this->enviarCodigoOtp($this->contactIdPending, $user); session()->flash('otp_reenviado', 'Codigo 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 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 === 'compra.pagar.mp' => $this->pagarConMP(), $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 === 'promo.comprar.mp' => $this->comprarPromoConMP((int) ($data['id'] ?? 0)), $action === 'recarga.iniciar' => $this->mostrarFormRecarga(), $action === 'recarga.otro_valor' => $this->solicitarMontoPersonalizado(), $action === 'recarga.elegir_metodo' => $this->elegirMetodoRecarga((int) ($data['monto'] ?? 0)), $action === 'recarga.mp' => $this->generarLinkRecargaMP((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 === '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; } $resultado = app(\App\Services\PagoValidadorService::class)->validar( $datosPago, $this->userId, $this->nombreUsuario, '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]); ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'validacion', 'contenido' => 'Por ahora no encontramos tu pago de confirmacion. Puede demorar entre 1 y 2 minutos en llegar. Espera un momento y presiona Reintentar.', 'payload' => [ 'estado' => 'pendiente', 'motivo' => 'Por ahora no encontramos tu pago de confirmacion. Puede demorar entre 1 y 2 minutos.', '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 { $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; 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 tu saldo para continuar:", 'payload' => ['botones' => [ ['label' => 'Recargar saldo', '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'); } private function pagarConMP(): void { if ($this->flujo !== 'compra') { return; } $valor = (int) ($this->flujoData['valor'] ?? 0); $servicio = $this->flujoData['servicio'] ?? 'Plan streaming'; $ref = 'CHAT-' . strtoupper(Str::random(12)); $url = $this->crearPreferenciaMP($servicio, $valor, $ref); if (! $url) { $this->guardarMensajeBot($this->convId, "No se pudo generar el link de pago. Intenta de nuevo o contacta a un asesor."); return; } ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'link', 'contenido' => "Link de pago por $" . number_format($valor) . " listo. Despues de pagar, envia el comprobante aqui.", 'payload' => ['url' => $url, 'label' => 'Pagar $' . number_format($valor) . ' con MercadoPago', 'nota' => 'Ref: ' . $ref], '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())) ->whereHas('tarifaPromo', fn ($q) => $q->where('rol_id', $rolId)) ->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; 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 tu saldo para continuar:", 'payload' => ['botones' => [ ['label' => 'Recargar saldo', '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'); } private function comprarPromoConMP(int $promoId): 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($promoId); if (! $promo) { return; } $upric = $this->userId ? Usuario_promo::where('usuario_id', $this->userId)->where('promocion_id', $promoId)->first() : null; $tp = $promo->tarifaPromo->first(); $precio = $upric ? $upric->precio : ($tp ? $tp->precio : $promo->precio); $ref = 'PROMO-' . strtoupper(Str::random(10)); $url = $this->crearPreferenciaMP($promo->nombre ?? 'Promocion streaming', (int) $precio, $ref); if (! $url) { $this->guardarMensajeBot($this->convId, "No se pudo generar el link de pago. Intenta de nuevo o contacta a un asesor."); return; } ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'link', 'contenido' => "Link de pago por $" . number_format($precio) . ". Envia el comprobante aqui al pagar.", 'payload' => ['url' => $url, 'label' => 'Pagar con MercadoPago', 'nota' => 'Ref: ' . $ref], '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; } ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'buttons', 'contenido' => "Recarga de $" . number_format($monto) . "\n¿Como quieres pagar?", 'payload' => ['botones' => [ ['label' => 'MercadoPago', 'action' => 'recarga.mp', 'data' => ['monto' => $monto]], ['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; } $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. Usa MercadoPago o 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, "Cuando hayas transferido, envia el comprobante usando el icono de camara (📷) y el sistema lo validara automaticamente.\n\nTienes hasta 15 minutos despues de la transferencia para enviar el soporte."); // Registrar solicitud pendiente if ($this->userId) { $solicitud = \App\Models\SolicitudRecarga::crear($this->userId, $monto, 'web'); $this->flujo = 'pago.solicitud'; $this->flujoData = ['solicitud_id' => $solicitud->id, 'monto' => $monto]; } $this->volver('menu.principal'); } private function generarLinkRecargaMP(int $monto): void { if ($monto <= 0) { $this->guardarMensajeBot($this->convId, "Monto invalido."); return; } $ref = 'REC-' . strtoupper(Str::random(12)); $url = $this->crearPreferenciaMP('Recarga de saldo', $monto, $ref); if (! $url) { $this->guardarMensajeBot($this->convId, "No se pudo generar el link de recarga. Intenta de nuevo o contacta a un asesor."); return; } if ($this->userId) { $user = User::with('saldo')->find($this->userId); $saldo = $user->saldo ?? Saldo::create(['usuario_id' => $this->userId, 'valor' => 0]); recarga::create([ 'monto' => $monto, 'reference' => $ref, 'status' => 'pendiente_mp', 'usuario_id' => $this->userId, 'saldo_id' => $saldo->id, ]); } ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'link', 'contenido' => "Tu link de recarga por $" . number_format($monto) . " esta listo. Despues de pagar, envia el comprobante aqui.", 'payload' => ['url' => $url, 'label' => 'Recargar $' . number_format($monto) . ' con MercadoPago', 'nota' => 'Ref: ' . $ref], 'leido' => true, ]); $this->volver('menu.principal'); } private function crearPreferenciaMP(string $titulo, int $valor, string $ref): ?string { $accessToken = config('services.mercadopago.token'); if (! $accessToken) { return null; } try { $response = \Illuminate\Support\Facades\Http::withToken($accessToken) ->timeout(10) ->post('https://api.mercadopago.com/checkout/preferences', [ 'items' => [[ 'title' => $titulo, 'quantity' => 1, 'unit_price' => $valor, 'currency_id' => 'COP', ]], 'external_reference' => $ref, 'back_urls' => [ 'success' => url('/chat'), 'failure' => url('/chat'), 'pending' => url('/chat'), ], 'auto_return' => 'approved', 'payment_methods' => [ 'excluded_payment_types' => [['id' => 'ticket']], ], ]); if ($response->successful()) { return $response->json('init_point'); } \Illuminate\Support\Facades\Log::warning('[ChatMP] Error creando preferencia: ' . $response->body()); return null; } catch (\Throwable $e) { \Illuminate\Support\Facades\Log::warning('[ChatMP] Excepcion: ' . $e->getMessage()); return null; } } // ───────────────────────────────────────────────────────── // 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 // ───────────────────────────────────────────────────────── 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']; $resultado = app(\App\Services\PagoValidadorService::class)->validar($datosPago, $this->userId, $this->nombreUsuario, '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; $solicitud->confirmar(); $this->flujo = ''; $this->flujoData = []; $this->guardarMensajeBot( $this->convId, "✅ ¡Tu recarga de $" . number_format($monto) . " fue aplicada exitosamente!\nTu nuevo saldo es $" . number_format($nuevoSaldo) . "." ); $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)) { return ['action' => $action, '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'); } }