diff --git a/app/Http/Livewire/Chat/PublicChat.php b/app/Http/Livewire/Chat/PublicChat.php index a10e980..1283a6f 100644 --- a/app/Http/Livewire/Chat/PublicChat.php +++ b/app/Http/Livewire/Chat/PublicChat.php @@ -2,28 +2,68 @@ namespace App\Http\Livewire\Chat; +use App\Mail\ChatOtpMail; +use App\Models\Cuentas; use App\Models\ChatContact; use App\Models\ChatConversation; use App\Models\ChatMessage; -use App\Services\ChatBotEngine; +use App\Models\Historiale; +use App\Models\Historial_cuenta; +use App\Models\Promociones; +use App\Models\recarga; +use App\Models\Saldo; +use App\Models\Servicio; +use App\Models\Tarifas; +use App\Models\User; +use Carbon\Carbon; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Str; use Livewire\Component; +use Livewire\WithFileUploads; class PublicChat extends Component { - public string $paso = 'telefono'; // 'telefono' | 'chat' - public string $telefono = ''; - public string $nombre = ''; - public string $input = ''; - public array $mensajes = []; - public ?int $convId = null; - public string $estadoConv = 'bot'; + use WithFileUploads; - public function iniciar() + // ── Paso actual ─────────────────────────────────────────── + public string $paso = 'telefono'; // telefono | verificacion | chat + public string $telefono = ''; + public string $nombre = ''; + public string $codigoIngresado = ''; + public ?string $emailMascarado = null; + public ?int $contactIdPending = null; // contact.id mientras espera verificacion + public string $input = ''; + public array $mensajes = []; + public ?int $convId = null; + public string $estadoConv = 'bot'; + + // ── Usuario identificado ────────────────────────────────── + public ?int $userId = null; + public ?string $nombreUsuario = null; + public ?float $saldoUsuario = null; + + // ── Estado del wizard ───────────────────────────────────── + public string $flujo = ''; + public array $flujoData = []; + + // ── Upload comprobante ──────────────────────────────────── + public $fotoComprobante = null; + public bool $procesandoImagen = false; + + protected $rules = [ + 'telefono' => 'required|min:7|max:20', + 'nombre' => 'nullable|max:80', + 'codigoIngresado' => 'nullable|digits:6', + ]; + + // ───────────────────────────────────────────────────────── + // Paso 1: identificar usuario + // ───────────────────────────────────────────────────────── + + public function iniciar(): void { - $this->validate([ - 'telefono' => 'required|min:7|max:20', - 'nombre' => 'nullable|max:80', - ]); + $this->validate(['telefono' => 'required|min:7|max:20', 'nombre' => 'nullable|max:80']); $contact = ChatContact::firstOrCreate( ['canal' => 'web', 'canal_id' => $this->telefono], @@ -34,26 +74,147 @@ class PublicChat extends Component $contact->update(['nombre' => $this->nombre]); } + // Buscar usuario en el sistema por cédula o email + $user = null; + if (! $contact->user_id) { + $user = User::where('cedula', $this->telefono) + ->orWhere('email', $this->telefono) + ->first(); + if ($user) { + $contact->update(['user_id' => $user->id]); + } + } else { + $user = User::find($contact->user_id); + } + + // Si se encontró el usuario: enviar OTP al correo registrado + if ($user && $user->email) { + $this->contactIdPending = $contact->id; + $this->emailMascarado = $this->enmascararEmail($user->email); + $this->enviarCodigoOtp($contact->id, $user); + $this->paso = 'verificacion'; + return; + } + + // Sin cuenta registrada: acceso como invitado (sin saldo ni compras) + $this->abrirChat($contact); + } + + 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()); + } + } + + // ───────────────────────────────────────────────────────── + // Paso 2: verificar código OTP + // ───────────────────────────────────────────────────────── + + 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(); if (! $conv) { - // Primera vez — el engine inicia el flujo con "hola" - $engine = new ChatBotEngine(); - $engine->handle('web', $this->telefono, 'hola', $contact->nombre); - $contact->refresh(); - $conv = $contact->activeConversation(); + $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); } - if ($conv) { - $this->convId = $conv->id; - $this->estadoConv = $conv->estado; - $this->cargarMensajes(); - } - - $this->paso = 'chat'; + $this->convId = $conv->id; + $this->estadoConv = $conv->estado; + $this->paso = 'chat'; + $this->cargarMensajes(); + $this->mostrarMenuPrincipal($conv->id); } - public function enviar() + private function enmascararEmail(string $email): string + { + [$local, $dominio] = explode('@', $email, 2); + $visible = mb_substr($local, 0, min(3, mb_strlen($local))); + return $visible . str_repeat('*', max(0, mb_strlen($local) - 3)) . '@' . $dominio; + } + + // ───────────────────────────────────────────────────────── + // Envío de texto libre + // ───────────────────────────────────────────────────────── + + public function enviar(): void { if (! $this->convId || trim($this->input) === '') { return; @@ -62,19 +223,813 @@ class PublicChat extends Component $texto = trim($this->input); $this->input = ''; - $engine = new ChatBotEngine(); - $engine->handle('web', $this->telefono, $texto, $this->nombre ?: $this->telefono); + $this->guardarMensajeUsuario($this->convId, $texto); $conv = ChatConversation::find($this->convId); - if ($conv) { - $this->estadoConv = $conv->fresh()->estado; + if (! $conv || $conv->estado === 'agente') { + $this->cargarMensajes(); + return; + } + + $accionIA = $this->detectarIntencionIA($texto); + + if ($accionIA) { + $this->clickBoton($accionIA['action'], $accionIA['data'] ?? []); + } else { + $this->guardarMensajeBot($this->convId, "No entendi tu mensaje. Usa los botones o escribe lo que deseas:"); + $this->mostrarMenuPrincipal($this->convId); } $this->cargarMensajes(); - // scroll ya se dispara desde cargarMensajes() + $this->dispatchBrowserEvent('scroll-chat'); } - public function cargarMensajes() + // ───────────────────────────────────────────────────────── + // 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.mp' => $this->generarLinkRecargaMP((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), + 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 { + $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, + ]); + + $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; + } + + $resultado = app(\App\Services\PagoValidadorService::class)->validar($datosPago); + + $botones = []; + if ($resultado['estado'] === 'confirmado') { + $botones[] = [ + 'label' => 'Aplicar recarga de $' . number_format($datosPago['valor'] ?? 0), + 'action' => 'pago.aplicar', + 'data' => ['monto' => $datosPago['valor'], 'referencia' => $datosPago['referencia'] ?? null], + ]; + } + $botones[] = ['label' => 'Hablar con asesor', 'action' => 'asesor.solicitar', 'data' => []]; + $botones[] = ['label' => '<- Menu', 'action' => 'menu.principal', 'data' => []]; + + ChatMessage::create([ + 'conversation_id' => $this->convId, + 'tipo' => 'bot', + 'tipo_ui' => 'validacion', + 'contenido' => 'Resultado del analisis:', + 'payload' => [ + 'estado' => $resultado['estado'], + 'ia_datos' => $datosPago, + 'botones' => $botones, + ], + 'leido' => true, + ]); + + } 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' => []], + ]], + 'leido' => true, + ]); + } + + // ───────────────────────────────────────────────────────── + // Servicios + // ───────────────────────────────────────────────────────── + + private function listarServicios(): void + { + $servicios = Servicio::where('estado', 'activo')->orderBy('ubicacion')->get(); + + if ($servicios->isEmpty()) { + $this->guardarMensajeBot($this->convId, "No hay servicios disponibles en este momento."); + $this->volver('menu.principal'); + return; + } + + $this->guardarMensajeBot($this->convId, "Selecciona el servicio:"); + + foreach ($servicios as $s) { + ChatMessage::create([ + 'conversation_id' => $this->convId, + 'tipo' => 'bot', + 'tipo_ui' => 'card', + 'contenido' => '', + 'payload' => [ + '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]], + ], + 'leido' => true, + ]); + } + + $this->volver('menu.principal'); + } + + private function verServicio(int $id): void + { + $servicio = Servicio::with('tarifas')->find($id); + if (! $servicio) { + $this->guardarMensajeBot($this->convId, "Servicio no encontrado."); + return; + } + + $tarifas = $servicio->tarifas->where('estado', 'activo'); + + 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) { + $label = $servicio->por_tiempo + ? "{$t->dias} dias - $" . number_format($t->valor) + : "{$t->pantallas} pantalla(s) / {$t->dias} dias - $" . number_format($t->valor); + $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; + } + + $disponibles = Cuentas::where('tarifa_id', $tarifaId)->where('estado', 'activo')->count(); + if ($disponibles < 1) { + $this->guardarMensajeBot($this->convId, "Sin cuentas disponibles para este plan. Intenta con otro."); + $this->volver('servicios.listar'); + return; + } + + $this->flujo = 'compra'; + $this->flujoData = [ + 'tarifa_id' => $tarifaId, + 'valor' => $tarifa->valor, + '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"; + $saldoInsuficiente = ! $this->userId || ($this->saldoUsuario < $tarifa->valor); + + ChatMessage::create([ + 'conversation_id' => $this->convId, + 'tipo' => 'bot', + 'tipo_ui' => 'buttons', + 'contenido' => "{$tarifa->servicio->nombre} - {$detalle}\nPrecio: $" . number_format($tarifa->valor) . "\n\nElige como pagar:", + 'payload' => ['botones' => [ + ['label' => 'Saldo ($' . number_format($tarifa->valor) . ')', + 'action' => 'compra.pagar.saldo', + 'data' => [], + 'disabled' => $saldoInsuficiente], + ['label' => 'MercadoPago', 'action' => 'compra.pagar.mp', '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; + + if (! $saldo || $saldo->valor < $tarifa->valor) { + $this->guardarMensajeBot( + $this->convId, + "Saldo insuficiente. Tienes $" . number_format($saldo?->valor ?? 0) . + " y el plan cuesta $" . number_format($tarifa->valor) . "." + ); + $this->volver('menu.principal'); + return; + } + + $cuenta = Cuentas::where('tarifa_id', $tarifa->id)->where('estado', 'activo')->first(); + if (! $cuenta) { + $this->guardarMensajeBot($this->convId, "No hay cuentas disponibles en este momento."); + return; + } + + $historial = Historiale::create([ + 'fecha_inicio' => now(), + 'fecha_final' => now()->addDays($tarifa->dias), + 'valor' => $tarifa->valor, + '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' => $saldo->valor - $tarifa->valor]); + $this->saldoUsuario = $saldo->valor - $tarifa->valor; + + 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 + { + $promos = Promociones::where('visible', true) + ->where(fn($q) => $q->whereNull('fecha_limite')->orWhere('fecha_limite', '>=', now())) + ->get(); + + if ($promos->isEmpty()) { + $this->guardarMensajeBot($this->convId, "No hay promociones activas en este momento."); + $this->volver('menu.principal'); + return; + } + + $this->guardarMensajeBot($this->convId, "Promociones disponibles:"); + + foreach ($promos as $p) { + ChatMessage::create([ + 'conversation_id' => $this->convId, + 'tipo' => 'bot', + 'tipo_ui' => 'card', + 'contenido' => '', + 'payload' => [ + 'imagen' => $p->img_publicidad, + 'titulo' => $p->nombre ?? 'Promocion especial', + 'descripcion' => $p->descripcion ?? null, + 'precio' => $p->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]], + ], + 'leido' => true, + ]); + } + + $this->volver('menu.principal'); + } + + private function verPromocion(int $id): void + { + $promo = Promociones::find($id); + if (! $promo) { + $this->guardarMensajeBot($this->convId, "Promocion no encontrada."); + return; + } + + $saldoInsuficiente = ! $this->userId || ($this->saldoUsuario < $promo->precio); + + ChatMessage::create([ + 'conversation_id' => $this->convId, + 'tipo' => 'bot', + 'tipo_ui' => 'buttons', + 'contenido' => ($promo->nombre ?? 'Promocion') . "\n$" . number_format($promo->precio) . "\n\nElige como pagar:", + 'payload' => ['botones' => [ + ['label' => 'Saldo ($' . number_format($promo->precio) . ')', + 'action' => 'promo.comprar.saldo', + 'data' => ['id' => $id], + 'disabled' => $saldoInsuficiente], + ['label' => 'MercadoPago', 'action' => 'promo.comprar.mp', 'data' => ['id' => $id]], + ['label' => 'Volver', 'action' => 'promo.listar', 'data' => []], + ]], + 'leido' => true, + ]); + } + + private function comprarPromoConSaldo(int $promoId): void + { + $promo = Promociones::find($promoId); + if (! $promo || ! $this->userId) { + $this->guardarMensajeBot($this->convId, "No se pudo procesar la compra."); + return; + } + + $user = User::with('saldo')->find($this->userId); + $saldo = $user->saldo; + + if (! $saldo || $saldo->valor < $promo->precio) { + $this->guardarMensajeBot($this->convId, "Saldo insuficiente ($" . number_format($saldo?->valor ?? 0) . "). La promo cuesta $" . number_format($promo->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' => $promo->precio, + 'utilidad' => $promo->utilidad ?? 0, + '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' => $saldo->valor - $promo->precio]); + $this->saldoUsuario = $saldo->valor - $promo->precio; + + 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 + { + $promo = Promociones::find($promoId); + if (! $promo) { + return; + } + + $ref = 'PROMO-' . strtoupper(Str::random(10)); + $url = $this->crearPreferenciaMP($promo->nombre ?? 'Promocion streaming', (int) $promo->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($promo->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.mp', 'data' => ['monto' => $m]]; + } + $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 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 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'); + } + + // ───────────────────────────────────────────────────────── + // IA: detectar intencion + // ───────────────────────────────────────────────────────── + + 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; @@ -85,7 +1040,9 @@ class PublicChat extends Component ->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(); @@ -93,6 +1050,46 @@ class PublicChat extends Component $this->dispatchBrowserEvent('scroll-chat'); } + // ───────────────────────────────────────────────────────── + // 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 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'); diff --git a/app/Http/Livewire/Chat/ShowConfiguracionChat.php b/app/Http/Livewire/Chat/ShowConfiguracionChat.php index 8b747e2..a6455af 100644 --- a/app/Http/Livewire/Chat/ShowConfiguracionChat.php +++ b/app/Http/Livewire/Chat/ShowConfiguracionChat.php @@ -10,30 +10,61 @@ class ShowConfiguracionChat extends Component { use LivewireAlert; + // ── Chat general ────────────────────────────────────────── public string $telegram_token = ''; public string $mensaje_bienvenida = ''; public string $mensaje_transferencia = ''; public string $webhookResult = ''; + // ── Gemini IA ───────────────────────────────────────────── + public string $gemini_api_key = ''; + public string $gemini_habilitado = '0'; + public string $gemini_prompt_extra = ''; + + // ── Validación de pago por foto + correo ────────────────── + public string $validacion_foto_habilitada = '0'; + public string $validacion_accion_auto = 'solo_notificar'; // recargar_saldo | solo_notificar + public string $validacion_monto_tolerancia = '0'; + public function mount(): void { - $this->telegram_token = ChatConfig::get('telegram_token', ''); - $this->mensaje_bienvenida = ChatConfig::get('mensaje_bienvenida', 'Hola 👋 Bienvenido. Escribe tu consulta.'); - $this->mensaje_transferencia = ChatConfig::get('mensaje_transferencia', 'Un agente se comunicará contigo en breve. Por favor espera.'); + $keys = [ + 'telegram_token', 'mensaje_bienvenida', 'mensaje_transferencia', + 'gemini_api_key', 'gemini_habilitado', 'gemini_prompt_extra', + 'validacion_foto_habilitada', 'validacion_accion_auto', 'validacion_monto_tolerancia', + ]; + + $defaults = [ + 'mensaje_bienvenida' => 'Hola! Bienvenido. Escribe tu consulta.', + 'mensaje_transferencia' => 'Un agente se comunicara contigo en breve. Por favor espera.', + 'validacion_accion_auto' => 'solo_notificar', + 'validacion_monto_tolerancia' => '0', + ]; + + foreach ($keys as $key) { + $this->{$key} = ChatConfig::get($key, $defaults[$key] ?? ''); + } } public function guardar(): void { $this->validate([ - 'mensaje_bienvenida' => 'required|string|max:500', - 'mensaje_transferencia' => 'required|string|max:500', + 'mensaje_bienvenida' => 'required|string|max:500', + 'mensaje_transferencia' => 'required|string|max:500', + 'validacion_monto_tolerancia' => 'nullable|integer|min:0', ]); - ChatConfig::set('telegram_token', $this->telegram_token); - ChatConfig::set('mensaje_bienvenida', $this->mensaje_bienvenida); - ChatConfig::set('mensaje_transferencia', $this->mensaje_transferencia); + $keys = [ + 'telegram_token', 'mensaje_bienvenida', 'mensaje_transferencia', + 'gemini_api_key', 'gemini_habilitado', 'gemini_prompt_extra', + 'validacion_foto_habilitada', 'validacion_accion_auto', 'validacion_monto_tolerancia', + ]; - $this->alert('success', 'Configuración guardada correctamente.'); + foreach ($keys as $key) { + ChatConfig::set($key, $this->{$key}); + } + + $this->alert('success', 'Configuracion guardada correctamente.'); } public function registrarWebhook(): void @@ -41,11 +72,10 @@ class ShowConfiguracionChat extends Component $token = trim($this->telegram_token); if (! $token) { - $this->webhookResult = '⚠️ El token de Telegram está vacío.'; + $this->webhookResult = 'El token de Telegram esta vacio.'; return; } - // Guardar el token antes de registrar ChatConfig::set('telegram_token', $token); $webhookUrl = url('/chat/webhook/telegram'); @@ -63,14 +93,14 @@ class ShowConfiguracionChat extends Component $result = @file_get_contents($apiUrl, false, $context); if ($result === false) { - $this->webhookResult = '❌ Error de conexión con la API de Telegram.'; + $this->webhookResult = 'Error de conexion con la API de Telegram.'; return; } $data = json_decode($result, true); $this->webhookResult = ($data['ok'] ?? false) - ? '✅ Webhook registrado correctamente: ' . ($data['description'] ?? 'OK') - : '❌ Error: ' . ($data['description'] ?? 'respuesta desconocida'); + ? 'Webhook registrado: ' . ($data['description'] ?? 'OK') + : 'Error: ' . ($data['description'] ?? 'respuesta desconocida'); } public function render() diff --git a/app/Mail/ChatOtpMail.php b/app/Mail/ChatOtpMail.php new file mode 100644 index 0000000..18d9609 --- /dev/null +++ b/app/Mail/ChatOtpMail.php @@ -0,0 +1,61 @@ +subject('Tu codigo de verificacion - ' . config('app.name')) + ->html($this->renderHtml()); + } + + private function renderHtml(): string + { + $appName = config('app.name', 'SirPremium'); + $codigo = $this->codigo; + $nombre = htmlspecialchars($this->nombre); + + return << + +
+ +| + + |
+ Enviamos un código de 6 dígitos a
+ {{ $emailMascarado }}
+
{{ session('otp_reenviado') }}
+ @endif + + + +Soporte en línea
++ {{ $nombreUsuario ?: 'Soporte en línea' }} +
@if ($estadoConv === 'agente') Atendido por un agente @elseif ($estadoConv === 'cerrada') Conversación cerrada - @else Bot activo - @endif + @else Bot activo @endif
Saldo
+${{ number_format($saldoUsuario) }}
+{{ $msg['contenido'] }}
- {{ $msg['created_at'] }} -{{ $msg['contenido'] }}
+ {{ $msg['created_at'] }} +{{ $msg['contenido'] }}
- {{ $msg['created_at'] }} -{{ $msg['contenido'] }}
+ {{ $msg['created_at'] }} +{{ $msg['contenido'] }}
+ {{ $msg['created_at'] }} +{{ $payload['titulo'] ?? '' }}
+ @if (!empty($payload['descripcion'])) +{{ $payload['descripcion'] }}
+ @endif + @if (!empty($payload['precio'])) +${{ number_format($payload['precio']) }}
+ @endif + @if (!empty($payload['detalle'])) +{{ $payload['detalle'] }}
+ @endif + @if (!empty($payload['accion'])) + + @endif +{{ $msg['contenido'] }}
+ + + {{ $payload['label'] ?? 'Ir a pagar' }} + + @if (!empty($payload['nota'])) +{{ $payload['nota'] }}
+ @endif ++ @if ($esConfirmado) ✅ Pago confirmado + @elseif ($esIncorrecto) ⚠️ Monto no coincide + @else 🔍 No encontrado en correos + @endif +
+Datos del comprobante
+ @foreach (['banco' => 'Banco', 'valor' => 'Valor', 'remitente' => 'De', 'referencia' => 'Referencia', 'fecha' => 'Fecha'] as $key => $label) + @if (!empty($payload['ia_datos'][$key])) +{{ $payload['servicio'] ?? 'Credenciales' }}
+Usuario / Email
+{{ $payload['email'] }}
+Contraseña
+{{ $payload['password'] }}
+Perfil
+{{ $payload['perfil'] }}
+Vence: {{ $payload['vence'] }}
+ @endif +{{ $msg['contenido'] }}
+ {{ $msg['created_at'] }} +Obtén tu clave en aistudio.google.com
+ +La IA lee el comprobante y cruza los datos con los correos del IMAP configurado en la seccion WhatsApp.
+Diferencia maxima permitida entre comprobante y correo. Recomendado: 0.
+