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.directo.breb' => $this->pagarDirectoBreb($data), $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; $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\n¿Cómo quieres pagar?", 'payload' => ['botones' => [ ['label' => "💳 Pagar con Bre-B directo ($" . number_format($precio) . ")", 'action' => 'pago.directo.breb', 'data' => ['tipo' => 'tarifa', 'tarifa_id' => $tarifaId, 'valor' => $precio]], ['label' => "💰 Recargar saldo ($" . number_format($falta) . " faltantes)", '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]); $cuenta->update(['estado' => 'ocupado']); $saldo->update(['valor' => $nuevoSaldo]); }); $this->saldoUsuario = $nuevoSaldo; ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'credenciales', 'contenido' => '¡Muchas gracias por tu compra! 🎉 Disfruta tu servicio. Si tienes algún inconveniente no dudes en escribirnos, estamos aquí para ayudarte.', '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; $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\n¿Cómo quieres pagar?", 'payload' => ['botones' => [ ['label' => "💳 Pagar con Bre-B directo ($" . number_format($precio) . ")", 'action' => 'pago.directo.breb', 'data' => ['tipo' => 'promo', 'promo_id' => $id, 'valor' => $precio]], ['label' => "💰 Recargar saldo ($" . number_format($falta) . " faltantes)", '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; } $nuevoSaldo = $saldo->valor - $precio; DB::transaction(function () use ($promo, $user, $saldo, $cuenta, $precio, $utilidad, $promoId, $nuevoSaldo) { $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, ]); 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' => '¡Muchas gracias por tu compra! 🎉 Disfruta tu servicio. Si tienes algún inconveniente no dudes en escribirnos, estamos aquí para ayudarte.', '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'); } // ───────────────────────────────────────────────────────── // Pago directo con Bre-B sin necesidad de saldo previo // ───────────────────────────────────────────────────────── private function pagarDirectoBreb(array $data): void { $tipo = $data['tipo'] ?? 'tarifa'; $valor = (int) ($data['valor'] ?? 0); if ($valor <= 0) { $this->guardarMensajeBot($this->convId, "No se pudo iniciar el pago. Intenta de nuevo."); return; } if ($tipo === 'tarifa') { $tarifa = Tarifas::with('servicio')->find($data['tarifa_id'] ?? 0); if (! $tarifa) { $this->guardarMensajeBot($this->convId, "Plan no encontrado."); return; } $this->flujoData = array_merge($this->flujoData, [ 'pending_purchase' => [ 'type' => 'tarifa', 'tarifa_id' => $data['tarifa_id'], 'valor' => $valor, 'servicio' => $tarifa->servicio->nombre, 'dias' => $tarifa->dias, 'pantallas' => $tarifa->pantallas, ], ]); } elseif ($tipo === 'promo') { $promo = Promociones::find($data['promo_id'] ?? 0); if (! $promo) { $this->guardarMensajeBot($this->convId, "Promoción no encontrada."); return; } $this->flujoData = array_merge($this->flujoData, [ 'pending_purchase' => [ 'type' => 'promo', 'promo_id' => $data['promo_id'], 'valor' => $valor, 'nombre' => $promo->nombre ?? 'Promoción', ], ]); } $this->mostrarTransferencia($valor); } // ───────────────────────────────────────────────────────── // 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 = array_merge($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'); } }