From 6bd0045826a02a6bd9a5b0a2dd780ddc4cd8a288 Mon Sep 17 00:00:00 2001 From: Lizandro Date: Sun, 26 Jul 2026 21:09:58 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20validaci=C3=B3n=20de=20pago=20mejorada?= =?UTF-8?q?=20=E2=80=94=20IMAP=20progresivo,=20auto-reintento,=20solo=20Br?= =?UTF-8?q?e-B,=20recargar-y-comprar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PagoValidadorService: búsqueda IMAP progresiva (30min → 2h → todo el día) con una sola conexión - SolicitudRecarga::pendiente() amplía ventana de 30min a todo el día (startOfDay) - Auto-reintento silencioso: web via wire:poll cada 20s (9 intentos ≈3min), Telegram via ValidarPagoTelegramJob - Mensaje al usuario al recibir comprobante: 'Estamos validando tu pago...' en lugar de botón inmediato - Aviso en comprobante: indica que debe mostrarse nombre, valor, fecha y hora - Eliminado MercadoPago de todos los flujos de recarga y compra (solo Bre-B) - Flujo 'recargar y comprar': guarda pending_purchase y ejecuta la compra automáticamente tras confirmar recarga - Nuevo job ValidarPagoTelegramJob para reintentos asincrónicos en Telegram Co-Authored-By: Claude Sonnet 4.6 --- app/Http/Livewire/Chat/PublicChat.php | 313 ++++++++---------- app/Jobs/ValidarPagoTelegramJob.php | 148 +++++++++ app/Models/SolicitudRecarga.php | 2 +- app/Services/PagoValidadorService.php | 138 +++++--- app/Services/TelegramBotService.php | 225 ++++--------- .../views/livewire/chat/public-chat.blade.php | 1 + 6 files changed, 449 insertions(+), 378 deletions(-) create mode 100644 app/Jobs/ValidarPagoTelegramJob.php diff --git a/app/Http/Livewire/Chat/PublicChat.php b/app/Http/Livewire/Chat/PublicChat.php index 21e6f90..0014f6b 100755 --- a/app/Http/Livewire/Chat/PublicChat.php +++ b/app/Http/Livewire/Chat/PublicChat.php @@ -487,15 +487,12 @@ class PublicChat extends Component $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(), @@ -596,24 +593,12 @@ class PublicChat extends Component } elseif ($motivo === 'sin_coincidencia' || $motivo === 'sin_correos') { $this->flujo = 'pago.pendiente'; - $this->flujoData = array_merge($this->flujoData, ['datosPago' => $datosPago]); + $this->flujoData = array_merge($this->flujoData, ['datosPago' => $datosPago, 'intentos' => 0]); - 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->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; @@ -788,14 +773,27 @@ class PublicChat extends Component ]); } else { $falta = $precio - (float) $this->saldoUsuario; + + // Guardar compra pendiente para ejecutarla automáticamente tras recargar + $this->flujoData = array_merge($this->flujoData, [ + 'pending_purchase' => [ + 'type' => 'tarifa', + 'tarifa_id' => $tarifaId, + 'valor' => $precio, + 'servicio' => $tarifa->servicio->nombre, + 'dias' => $tarifa->dias, + 'pantallas' => $tarifa->pantallas, + ], + ]); + ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'buttons', - 'contenido' => "{$tarifa->servicio->nombre} - {$detalle}\nPrecio: $" . number_format($precio) . "\n\nSaldo actual: $" . number_format($this->saldoUsuario) . " — te faltan $" . number_format($falta) . "\n\nRecarga tu saldo para continuar:", + 'contenido' => "{$tarifa->servicio->nombre} - {$detalle}\nPrecio: $" . number_format($precio) . "\n\nSaldo actual: $" . number_format($this->saldoUsuario) . " — te faltan $" . number_format($falta) . "\n\nRecarga para comprar:", 'payload' => ['botones' => [ - ['label' => 'Recargar saldo', 'action' => 'recarga.iniciar', 'data' => []], - ['label' => 'Volver', 'action' => 'servicios.listar', 'data' => []], + ['label' => "💰 Recargar $" . number_format($falta) . " para comprar {$tarifa->servicio->nombre}", 'action' => 'recarga.iniciar', 'data' => []], + ['label' => 'Volver', 'action' => 'servicios.listar', 'data' => []], ]], 'leido' => true, ]); @@ -876,35 +874,6 @@ class PublicChat extends Component $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 @@ -988,14 +957,25 @@ class PublicChat extends Component ]); } else { $falta = $precio - (float) $this->saldoUsuario; + + // Guardar compra pendiente para ejecutarla automáticamente tras recargar + $this->flujoData = array_merge($this->flujoData, [ + 'pending_purchase' => [ + 'type' => 'promo', + 'promo_id' => $id, + 'valor' => $precio, + 'nombre' => $promo->nombre ?? 'Promoción', + ], + ]); + ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'buttons', - 'contenido' => ($promo->nombre ?? 'Promocion') . "\n$" . number_format($precio) . "\n\nSaldo actual: $" . number_format($this->saldoUsuario) . " — te faltan $" . number_format($falta) . "\n\nRecarga tu saldo para continuar:", + 'contenido' => ($promo->nombre ?? 'Promocion') . "\n$" . number_format($precio) . "\n\nSaldo actual: $" . number_format($this->saldoUsuario) . " — te faltan $" . number_format($falta) . "\n\nRecarga para comprar:", 'payload' => ['botones' => [ - ['label' => 'Recargar saldo', 'action' => 'recarga.iniciar', 'data' => []], - ['label' => 'Volver', 'action' => 'promo.listar', 'data' => []], + ['label' => "💰 Recargar $" . number_format($falta) . " para comprar " . ($promo->nombre ?? 'Promo'), 'action' => 'recarga.iniciar', 'data' => []], + ['label' => 'Volver', 'action' => 'promo.listar', 'data' => []], ]], 'leido' => true, ]); @@ -1076,42 +1056,6 @@ class PublicChat extends Component $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 @@ -1149,15 +1093,21 @@ class PublicChat extends Component 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' => 'MercadoPago', 'action' => 'recarga.mp', 'data' => ['monto' => $monto]], - ['label' => 'Bre-B', 'action' => 'recarga.transferencia', 'data' => ['monto' => $monto]], - ['label' => 'Volver', 'action' => 'recarga.iniciar', 'data' => []], + ['label' => '🔑 Bre-B', 'action' => 'recarga.transferencia', 'data' => ['monto' => $monto]], + ['label' => 'Volver', 'action' => 'recarga.iniciar', 'data' => []], ]], 'leido' => true, ]); @@ -1219,7 +1169,7 @@ class PublicChat extends Component $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."); + $this->guardarMensajeBot($this->convId, "El pago por Bre-B no esta disponible en este momento. Contacta a un asesor."); return; } @@ -1237,7 +1187,14 @@ class PublicChat extends Component '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."); + $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; @@ -1249,87 +1206,6 @@ class PublicChat extends Component $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 @@ -1457,6 +1333,73 @@ class PublicChat extends Component // 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'])) { @@ -1537,6 +1480,7 @@ class PublicChat extends Component $solicitud->confirmar(); + $pp = $this->flujoData['pending_purchase'] ?? null; $this->flujo = ''; $this->flujoData = []; @@ -1544,6 +1488,21 @@ class PublicChat extends Component $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'); } diff --git a/app/Jobs/ValidarPagoTelegramJob.php b/app/Jobs/ValidarPagoTelegramJob.php new file mode 100644 index 0000000..0f23c4d --- /dev/null +++ b/app/Jobs/ValidarPagoTelegramJob.php @@ -0,0 +1,148 @@ +chatId}"; + $state = Cache::get($cacheKey, []); + $usuarioId = $state['user_id'] ?? null; + $bot = new TelegramBotService(); + + if (! $usuarioId) { + return; + } + + $solicitud = SolicitudRecarga::pendiente($usuarioId); + if (! $solicitud) { + Log::info("[ValidarPagoTelegramJob] chatId={$this->chatId}: solicitud ya no está pendiente, abortando."); + return; + } + + $nombreUsuario = $state['data']['nombre_remitente'] ?? null; + + $resultado = app(PagoValidadorService::class)->validar( + $this->datosPago, $usuarioId, $nombreUsuario, 'telegram' + ); + + if ($resultado['estado'] === 'confirmado') { + $this->aplicarYNotificar($bot, $state, $cacheKey, $solicitud, $usuarioId); + return; + } + + if ($resultado['estado'] === 'ya_usado') { + $bot->sendWithKeyboard($this->chatId, + "⛔ *Comprobante ya utilizado.*\nEste pago ya fue registrado anteriormente.", + [[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]] + ); + return; + } + + // No encontrado — reintentar o agotar + if ($this->intento < self::MAX_INTENTOS) { + Log::info("[ValidarPagoTelegramJob] chatId={$this->chatId}: intento {$this->intento}/" . self::MAX_INTENTOS . " — reintentando en 20s"); + self::dispatch($this->chatId, $this->datosPago, $this->intento + 1) + ->delay(now()->addSeconds(20)); + } else { + Log::info("[ValidarPagoTelegramJob] chatId={$this->chatId}: intentos agotados, mostrando botón manual"); + $bot->sendWithKeyboard( + $this->chatId, + "⚠️ No pudimos confirmar tu pago automáticamente.\nSi ya realizaste la transferencia, presiona Reintentar.", + [ + [['text' => '🔄 Reintentar', 'callback_data' => 'pay_retry']], + [['text' => '🔙 Menú principal', 'callback_data' => 'menu']], + ] + ); + } + } + + private function aplicarYNotificar( + TelegramBotService $bot, + array $state, + string $cacheKey, + SolicitudRecarga $solicitud, + int $usuarioId + ): void { + $user = User::with('saldo')->find($usuarioId); + $saldo = $user->saldo ?? Saldo::create(['usuario_id' => $usuarioId, 'valor' => 0]); + $monto = $solicitud->monto; + $nuevoSaldo = $saldo->valor + $monto; + + $saldo->update(['valor' => $nuevoSaldo]); + + recarga::create([ + 'usuario_id' => $usuarioId, + 'saldo_id' => $saldo->id, + 'monto' => $monto, + 'valor_recarga' => $monto, + 'status' => 'Confirmado', + 'reference' => 'breb-bot-' . $solicitud->id, + ]); + + $solicitud->confirmar(); + + $pp = $state['data']['pending_purchase'] ?? null; + unset($state['data']['pago_pendiente'], $state['data']['solicitud_recarga_id'], $state['data']['pending_purchase']); + Cache::put($cacheKey, $state, now()->addSeconds(self::STATE_TTL)); + + $bot->send($this->chatId, + "✅ *¡Tu recarga de \$" . number_format($monto) . " fue aplicada exitosamente!*\n" + . "Tu nuevo saldo es *\$" . number_format($nuevoSaldo) . "*" + ); + + if ($pp && ($pp['type'] ?? '') === 'tarifa') { + $bot->sendWithKeyboard( + $this->chatId, + "🛒 Tienes una compra pendiente: *{$pp['servicio']}* — \$" . number_format($pp['valor']) . "\nPresiona para completarla:", + [[['text' => "Comprar {$pp['servicio']}", 'callback_data' => 'buy_start|' . $pp['tarifa_id']]]] + ); + } elseif ($pp && ($pp['type'] ?? '') === 'promo') { + $bot->sendWithKeyboard( + $this->chatId, + "🛒 Tienes una compra pendiente: *{$pp['nombre']}* — \$" . number_format($pp['valor']) . "\nPresiona para completarla:", + [[['text' => "Comprar {$pp['nombre']}", 'callback_data' => 'promo_saldo|' . $pp['promo_id']]]] + ); + } else { + $bot->sendWithKeyboard($this->chatId, "¿En qué más te puedo ayudar?", [ + [ + ['text' => '📺 Ver Servicios', 'callback_data' => 'svc_list'], + ['text' => '🎁 Promociones', 'callback_data' => 'promo_list'], + ], + [ + ['text' => '🔑 Mis Credenciales', 'callback_data' => 'creds'], + ['text' => '📋 Historial', 'callback_data' => 'history'], + ], + ]); + } + } +} diff --git a/app/Models/SolicitudRecarga.php b/app/Models/SolicitudRecarga.php index 41374b6..cce2fa2 100644 --- a/app/Models/SolicitudRecarga.php +++ b/app/Models/SolicitudRecarga.php @@ -33,7 +33,7 @@ class SolicitudRecarga extends Model { return static::where('user_id', $userId) ->where('estado', 'pendiente') - ->where('created_at', '>=', now()->subMinutes(30)) + ->where('created_at', '>=', now()->startOfDay()) ->latest('id') ->first(); } diff --git a/app/Services/PagoValidadorService.php b/app/Services/PagoValidadorService.php index d334aa5..0add9b5 100755 --- a/app/Services/PagoValidadorService.php +++ b/app/Services/PagoValidadorService.php @@ -9,6 +9,11 @@ use Illuminate\Support\Facades\Log; class PagoValidadorService { + /** + * Valida un pago con búsqueda IMAP progresiva: 30 min → 2 h → todo el día. + * Retorna en cuanto encuentra coincidencia; solo pasa a la siguiente ventana + * si la anterior no devuelve ningún correo o no hay coincidencia. + */ public function validar(array $datosPago, ?int $usuarioId = null, ?string $nombreUsuario = null, string $canal = 'telegram'): array { $host = WhatsappSystemConfig::get('correo_imap_host', ''); @@ -18,18 +23,52 @@ class PagoValidadorService } try { - $correos = $this->fetchCorreos(); + $correosDia = $this->fetchCorreosDia(); } catch (\Throwable $e) { Log::warning('[PagoValidador] Error IMAP: ' . $e->getMessage()); return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'error_imap', 'debug' => $e->getMessage()]; } - Log::info('[PagoValidador] Correos obtenidos: ' . count($correos)); + Log::info('[PagoValidador] Correos obtenidos del día: ' . count($correosDia)); - if (empty($correos)) { + if (empty($correosDia)) { return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'sin_correos']; } + // Ventanas progresivas: primero los más recientes, luego ampliar + $ventanas = [30, 120, 0]; // minutos; 0 = sin filtro (todo el día) + + foreach ($ventanas as $minutos) { + $subset = $minutos > 0 + ? array_values(array_filter($correosDia, fn ($c) => $this->dentroDeVentana($c, $minutos))) + : $correosDia; + + if (empty($subset)) { + Log::info("[PagoValidador] Ventana {$minutos}min: sin correos, ampliando..."); + continue; + } + + Log::info("[PagoValidador] Ventana {$minutos}min: revisando " . count($subset) . " correos"); + + $resultado = $this->buscarEnCorreos($subset, $datosPago, $usuarioId, $nombreUsuario, $canal); + + // Confirmado o ya_usado → retornar inmediatamente + if (in_array($resultado['estado'], ['confirmado', 'ya_usado'])) { + return $resultado; + } + + // sin_coincidencia → intentar ventana más amplia + } + + return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'sin_coincidencia']; + } + + // ───────────────────────────────────────────────────────── + // Lógica de matching (extraída del validar() original) + // ───────────────────────────────────────────────────────── + + private function buscarEnCorreos(array $correos, array $datosPago, ?int $usuarioId, ?string $nombreUsuario, string $canal): array + { $valorIA = (int) ($datosPago['valor'] ?? 0); $tolerancia = (int) ChatConfig::get('validacion_monto_tolerancia', '0'); $horaIA = $datosPago['hora'] ?? ''; @@ -38,14 +77,14 @@ class PagoValidadorService Log::info('[PagoValidador] Buscando: valor=' . $valorIA . ' fecha=' . $fechaIA . ' hora=' . $horaIA . ' llave=' . $llaveIA); - $hayYaUsado = false; // email que coincide en todo pero ya fue usado + $hayYaUsado = false; foreach ($correos as $i => $correo) { - $fuera = $correo['fuera_de_ventana'] ?? false; - $from = $correo['from'] ?? ''; - $subj = $correo['subject'] ?? ''; - $date = $correo['date'] ?? ''; - $body = $correo['body'] ?? ''; + $fuera = $correo['fuera_de_ventana'] ?? false; + $from = $correo['from'] ?? ''; + $subj = $correo['subject'] ?? ''; + $date = $correo['date'] ?? ''; + $body = $correo['body'] ?? ''; Log::info("[PagoValidador] Correo #{$i}: from={$from} | subj={$subj} | date={$date} | fuera_ventana=" . ($fuera ? 'SI' : 'NO') . " | body(200)=" . substr($body, 0, 200)); @@ -59,9 +98,8 @@ class PagoValidadorService Log::info("[PagoValidador] Correo #{$i} parseado: " . json_encode($datosCorreo)); // ── 1. Valor ──────────────────────────────────────────── - // Normalizar: "1,000.00" → 1000 (strip decimals first, then non-digits) $valorRaw = (string) ($datosCorreo['valor'] ?? '0'); - $valorRaw = preg_replace('/[.,]\d{1,2}$/', '', $valorRaw); // quitar decimales + $valorRaw = preg_replace('/[.,]\d{1,2}$/', '', $valorRaw); $valorCorreo = (int) preg_replace('/[^0-9]/', '', $valorRaw); if ($valorIA <= 0 || $valorCorreo <= 0) { @@ -74,7 +112,7 @@ class PagoValidadorService continue; } - // ── 2. Fecha (mismo día) — solo si ambos tienen fecha ── + // ── 2. Fecha (mismo día) ───────────────────────────────── if ($fechaIA && ($datosCorreo['fecha'] ?? '')) { if (! $this->mismoDia($fechaIA, $datosCorreo['fecha'])) { Log::info("[PagoValidador] Correo #{$i}: fecha no coincide (ia={$fechaIA} correo={$datosCorreo['fecha']})"); @@ -82,8 +120,7 @@ class PagoValidadorService } } - // ── 3. Hora exacta — el timestamp del comprobante y del correo deben coincidir - // El correo llega tarde al inbox pero su contenido registra la hora exacta de la transacción. + // ── 3. Hora exacta ─────────────────────────────────────── if ($horaIA && ($datosCorreo['hora'] ?? '')) { if (! $this->horaProxima($horaIA, $datosCorreo['hora'], 0)) { Log::info("[PagoValidador] Correo #{$i}: hora no coincide (ia={$horaIA} correo={$datosCorreo['hora']})"); @@ -91,7 +128,7 @@ class PagoValidadorService } } - // ── 4. Llave Bancolombia (si la foto la tiene) ────────── + // ── 4. Llave Bancolombia ───────────────────────────────── if ($llaveIA) { $llaveCorreo = strtolower(preg_replace('/\s+/', '', $datosCorreo['llave'] ?? '')); if ($llaveCorreo && $llaveIA !== $llaveCorreo) { @@ -100,23 +137,21 @@ class PagoValidadorService } } - // ── 5. Hash único (anti-doble-uso) — solo tras verificar fecha/hora/llave ── - // Si el email coincide en todos los campos pero ya fue usado → es el mismo comprobante + // ── 5. Hash anti-duplicado ─────────────────────────────── $emailHash = sha1($body . $date); try { if (PagoConfirmado::yaUsado($emailHash)) { - Log::info("[PagoValidador] Correo #{$i}: ya usado (hash={$emailHash}), buscando siguiente"); + Log::info("[PagoValidador] Correo #{$i}: ya usado (hash={$emailHash})"); $hayYaUsado = true; - continue; // seguir buscando: otro usuario puede tener un email fresco del mismo monto + continue; } } catch (\Throwable $e) { - // Falla cerrado: si no se puede verificar el anti-duplicado, no confirmar - Log::error('[PagoValidador] ERROR tabla pagos_confirmados (ejecutar migrate): ' . $e->getMessage()); + Log::error('[PagoValidador] ERROR tabla pagos_confirmados: ' . $e->getMessage()); return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'error_sistema']; } - // ── 6. Remitente (bloquea si el correo trae el campo y no coincide) ── + // ── 6. Remitente ───────────────────────────────────────── $remitenteCorreo = $datosCorreo['remitente'] ?? ''; if ($remitenteCorreo && $nombreUsuario) { if (! $this->remitenteCoincide($remitenteCorreo, $nombreUsuario)) { @@ -136,15 +171,11 @@ class PagoValidadorService 'referencia' => $datosPago['referencia'] ?? null, ]); } catch (\Throwable $e) { - // Falla cerrado: si no se puede registrar el hash, no confirmar - Log::error('[PagoValidador] ERROR al marcar hash (ejecutar migrate): ' . $e->getMessage()); + Log::error('[PagoValidador] ERROR al marcar hash: ' . $e->getMessage()); return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'error_sistema']; } - return [ - 'estado' => 'confirmado', - 'correo' => $datosCorreo, - ]; + return ['estado' => 'confirmado', 'correo' => $datosCorreo]; } if ($hayYaUsado) { @@ -152,11 +183,42 @@ class PagoValidadorService return ['estado' => 'ya_usado', 'correo' => null, 'motivo' => 'correo_ya_aplicado']; } - Log::info('[PagoValidador] Ningún correo coincidió con los criterios'); return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'sin_coincidencia', 'emails_revisados' => count($correos)]; } - // ── Helpers ────────────────────────────────────────────────────────── + // ───────────────────────────────────────────────────────── + // Helpers + // ───────────────────────────────────────────────────────── + + /** + * Obtiene todos los correos del día actual (desde medianoche) con un límite alto. + * Se hace una sola conexión IMAP; el filtrado progresivo se hace en memoria. + */ + private function fetchCorreosDia(): array + { + // Minutos desde medianoche + 60 de buffer para zona horaria + $minutesDesdeMedianoche = (int) ceil(now()->diffInMinutes(now()->copy()->startOfDay())) + 60; + + $service = new CorreoImapService( + host: WhatsappSystemConfig::get('correo_imap_host', ''), + port: (int) WhatsappSystemConfig::get('correo_imap_port', '993'), + useSsl: WhatsappSystemConfig::get('correo_imap_ssl', '1') === '1', + user: WhatsappSystemConfig::get('correo_imap_user', ''), + password: WhatsappSystemConfig::get('correo_imap_password', ''), + folder: WhatsappSystemConfig::get('correo_imap_folder', 'INBOX'), + ); + + return $service->fetchLatest(limit: 200, minutesBack: $minutesDesdeMedianoche); + } + + private function dentroDeVentana(array $correo, int $minutos): bool + { + $ts = ($correo['date'] ?? '') ? @strtotime($correo['date']) : false; + if ($ts === false) { + return true; // si no hay fecha, incluir por defecto + } + return $ts >= time() - ($minutos * 60); + } private function mismoDia(string $fechaA, string $fechaB): bool { @@ -205,20 +267,4 @@ class PagoValidadorService } return $coincidencias >= 2; } - - private function fetchCorreos(): array - { - $minutos = (int) WhatsappSystemConfig::get('correo_imap_minutos', '120'); - - $service = new CorreoImapService( - host: WhatsappSystemConfig::get('correo_imap_host', ''), - port: (int) WhatsappSystemConfig::get('correo_imap_port', '993'), - useSsl: WhatsappSystemConfig::get('correo_imap_ssl', '1') === '1', - user: WhatsappSystemConfig::get('correo_imap_user', ''), - password: WhatsappSystemConfig::get('correo_imap_password', ''), - folder: WhatsappSystemConfig::get('correo_imap_folder', 'INBOX'), - ); - - return $service->fetchLatest(limit: 30, minutesBack: $minutos); - } } diff --git a/app/Services/TelegramBotService.php b/app/Services/TelegramBotService.php index 724b25a..1da1f35 100755 --- a/app/Services/TelegramBotService.php +++ b/app/Services/TelegramBotService.php @@ -31,6 +31,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Str; +use App\Jobs\ValidarPagoTelegramJob; use App\Services\GeminiAgentService; class TelegramBotService @@ -136,15 +137,12 @@ class TelegramBotService 'svc_view' => $this->viewService($chatId, (int) ($parts[1] ?? 0)), 'buy_start' => $this->startPurchase($chatId, (int) ($parts[1] ?? 0)), 'buy_saldo' => $this->payWithSaldo($chatId), - 'buy_mp' => $this->payWithMP($chatId), 'promo_list' => $this->listPromos($chatId), 'promo_view' => $this->viewPromo($chatId, (int) ($parts[1] ?? 0)), 'promo_saldo' => $this->buyPromoSaldo($chatId, (int) ($parts[1] ?? 0)), - 'promo_mp' => $this->buyPromoMP($chatId, (int) ($parts[1] ?? 0)), 'rec_init' => $this->showRechargeAmounts($chatId), 'rec_custom' => $this->askCustomAmount($chatId), 'rec_amount' => $this->chooseRechargeMethod($chatId, (int) ($parts[1] ?? 0)), - 'rec_mp' => $this->rechargeMP($chatId, (int) ($parts[1] ?? 0)), 'rec_breb' => $this->askRemitenteNombre($chatId, (int) ($parts[1] ?? 0)), 'breb_nom' => $this->seleccionarNombreRemitente($chatId, (string) ($parts[1] ?? 'nuevo')), 'creds' => $this->showCredentials($chatId), @@ -232,15 +230,18 @@ class TelegramBotService ]); } elseif ($motivo === 'sin_coincidencia' || $motivo === 'sin_correos') { + // Guardar para reintento manual (por si el job agota sus intentos) $state['data']['pago_pendiente'] = $datosPago; $this->setState($chatId, $state); - $this->sendWithKeyboard($chatId, - "⏳ *Por ahora no encontramos tu pago de confirmación.*\n_Puede demorar entre 1 y 2 minutos en llegar. Espera un momento y presiona Reintentar._", - [ - [['text' => '🔄 Reintentar', 'callback_data' => 'pay_retry']], - [['text' => '🔙 Menú principal', 'callback_data' => 'menu']], - ] + + $this->send($chatId, + "⏳ *Estamos validando tu pago*, esto puede demorar unos minutos.\n" + . "Cuando terminemos te confirmaremos por este medio. Espera un momento." ); + + // Lanzar job que reintenta cada 20s hasta 9 veces (≈3 min) + ValidarPagoTelegramJob::dispatch($chatId, $datosPago, 1) + ->delay(now()->addSeconds(20)); } else { $motivoTexto = match ($motivo) { 'correo_deshabilitado' => '📧 Verificación por correo no configurada', @@ -304,7 +305,7 @@ class TelegramBotService } } - private function autoAplicarRecarga(string $chatId, array $state, SolicitudRecarga $solicitud, int $monto): void + public function autoAplicarRecarga(string $chatId, array $state, SolicitudRecarga $solicitud, int $monto): void { $usuarioId = $state['user_id']; $user = User::with('saldo')->find($usuarioId); @@ -323,14 +324,32 @@ class TelegramBotService $solicitud->confirmar(); - unset($state['data']['pago_pendiente'], $state['data']['solicitud_recarga_id']); + $pp = $state['data']['pending_purchase'] ?? null; + unset($state['data']['pago_pendiente'], $state['data']['solicitud_recarga_id'], $state['data']['pending_purchase']); $this->setState($chatId, $state); $this->send($chatId, "✅ *¡Tu recarga de \$" . number_format($monto) . " fue aplicada exitosamente!*\n" . "Tu nuevo saldo es *\$" . number_format($nuevoSaldo) . "*" ); - $this->showMainMenu($chatId); + + if ($pp && ($pp['type'] ?? '') === 'tarifa') { + // Configurar estado para la compra y ejecutarla directamente + $state = $this->getState($chatId); + $state['data'] = [ + 'flow' => 'compra', + 'tarifa_id' => $pp['tarifa_id'], + 'valor' => $pp['valor'], + 'servicio' => $pp['servicio'], + 'servicio_id' => $pp['servicio_id'], + ]; + $this->setState($chatId, $state); + $this->payWithSaldo($chatId); + } elseif ($pp && ($pp['type'] ?? '') === 'promo') { + $this->buyPromoSaldo($chatId, $pp['promo_id']); + } else { + $this->showMainMenu($chatId); + } } public function handleVoice(string $chatId, string $fileId, string $nombre = '', int $duracionSegundos = 0): void @@ -803,8 +822,19 @@ class TelegramBotService ); } else { $falta = $valor - $saldo; - $buttons[] = [['text' => '💰 Recargar saldo', 'callback_data' => 'rec_init']]; - $buttons[] = [['text' => '🔙 Ver planes', 'callback_data' => 'svc_view|' . $tarifa->servicio_id]]; + + // Guardar compra pendiente para ejecutarla automáticamente tras recargar + $state['data']['pending_purchase'] = [ + 'type' => 'tarifa', + 'tarifa_id' => $tarifaId, + 'valor' => $valor, + 'servicio' => $tarifa->servicio->nombre, + 'servicio_id' => $tarifa->servicio_id, + ]; + $this->setState($chatId, $state); + + $buttons[] = [['text' => "💰 Recargar \$" . number_format($falta) . " para comprar {$tarifa->servicio->nombre}", 'callback_data' => 'rec_init']]; + $buttons[] = [['text' => '🔙 Ver planes', 'callback_data' => 'svc_view|' . $tarifa->servicio_id]]; $this->sendWithKeyboard( $chatId, @@ -884,35 +914,6 @@ class TelegramBotService $this->showMainMenu($chatId); } - private function payWithMP(string $chatId): void - { - $state = $this->getState($chatId); - $data = $state['data'] ?? []; - - if (($data['flow'] ?? '') !== 'compra') { - $this->showMainMenu($chatId); - return; - } - - $valor = (int) ($data['valor'] ?? 0); - $servicio = $data['servicio'] ?? 'Plan streaming'; - $ref = 'CHAT-' . strtoupper(Str::random(12)); - $url = $this->createMPPreference($servicio, $valor, $ref); - - if (! $url) { - $this->send($chatId, "No se pudo generar el link de pago. Intenta de nuevo."); - return; - } - - $this->sendWithKeyboard( - $chatId, - "🏦 *Pago con MercadoPago*\n\n{$servicio} — \$" . number_format($valor) . "\nRef: `{$ref}`\n\n[👉 Pagar ahora]({$url})\n\nDespués de pagar, envía la foto del comprobante aquí.", - [[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]] - ); - - $state['data'] = []; - $this->setState($chatId, $state); - } // ─── Promos ─────────────────────────────────────────────── @@ -995,8 +996,19 @@ class TelegramBotService ); } else { $falta = $precio - $saldo; - $buttons[] = [['text' => '💰 Recargar saldo', 'callback_data' => 'rec_init']]; - $buttons[] = [['text' => '🔙 Promociones', 'callback_data' => 'promo_list']]; + + // Guardar compra pendiente para ejecutarla automáticamente tras recargar + $state = $this->getState($chatId); + $state['data']['pending_purchase'] = [ + 'type' => 'promo', + 'promo_id' => $promoId, + 'valor' => $precio, + 'nombre' => $promo->nombre ?? 'Promoción', + ]; + $this->setState($chatId, $state); + + $buttons[] = [['text' => "💰 Recargar \$" . number_format($falta) . " para comprar " . ($promo->nombre ?? 'Promo'), 'callback_data' => 'rec_init']]; + $buttons[] = [['text' => '🔙 Promociones', 'callback_data' => 'promo_list']]; $this->sendWithKeyboard( $chatId, @@ -1073,38 +1085,6 @@ class TelegramBotService $this->showMainMenu($chatId); } - private function buyPromoMP(string $chatId, int $promoId): void - { - $state = $this->getState($chatId); - $rolId = isset($state['user_id']) - ? (User::find($state['user_id'])?->rol_id ?? Role::where('nombre', 'cliente')->value('id')) - : Role::where('nombre', 'cliente')->value('id'); - - $promo = Promociones::with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])->find($promoId); - if (! $promo) { - return; - } - - $upric = isset($state['user_id']) - ? Usuario_promo::where('usuario_id', $state['user_id'])->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->createMPPreference($promo->nombre ?? 'Promocion streaming', (int) $precio, $ref); - - if (! $url) { - $this->send($chatId, "No se pudo generar el link de pago. Intenta de nuevo."); - return; - } - - $this->sendWithKeyboard( - $chatId, - "🏦 *Pago con MercadoPago*\n\n{$promo->nombre} — \$" . number_format($precio) . "\nRef: `{$ref}`\n\n[👉 Pagar ahora]({$url})\n\nDespués de pagar, envía la foto del comprobante aquí.", - [[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]] - ); - } // ─── Recharge ───────────────────────────────────────────── @@ -1160,13 +1140,16 @@ class TelegramBotService } $llave = ChatConfig::get('recarga_banco_llave', ''); - $buttons = [ - [['text' => '🏦 MercadoPago', 'callback_data' => 'rec_mp|' . $monto]], - ]; - if ($llave) { - $buttons[] = [['text' => '🔑 Bre-B', 'callback_data' => 'rec_breb|' . $monto]]; + if (! $llave) { + $this->sendWithKeyboard($chatId, "⚠️ La recarga no está disponible en este momento. Contacta a un asesor.", [ + [['text' => '🔙 Menú principal', 'callback_data' => 'menu']], + ]); + return; } - $buttons[] = [['text' => '🔙 Volver', 'callback_data' => 'rec_init']]; + $buttons = [ + [['text' => '🔑 Bre-B', 'callback_data' => 'rec_breb|' . $monto]], + [['text' => '🔙 Volver', 'callback_data' => 'rec_init']], + ]; $this->sendWithKeyboard( $chatId, @@ -1175,41 +1158,6 @@ class TelegramBotService ); } - private function rechargeMP(string $chatId, int $monto): void - { - $state = $this->getState($chatId); - - if ($monto <= 0) { - $this->send($chatId, "Monto inválido."); - return; - } - - $ref = 'REC-' . strtoupper(Str::random(12)); - $url = $this->createMPPreference('Recarga de saldo', $monto, $ref); - - if (! $url) { - $this->send($chatId, "No se pudo generar el link de recarga. Intenta de nuevo."); - return; - } - - if ($state['user_id'] ?? null) { - $user = User::with('saldo')->find($state['user_id']); - $saldo = $user->saldo ?? Saldo::create(['usuario_id' => $state['user_id'], 'valor' => 0]); - recarga::create([ - 'monto' => $monto, - 'reference' => $ref, - 'status' => 'pendiente_mp', - 'usuario_id' => $state['user_id'], - 'saldo_id' => $saldo->id, - ]); - } - - $this->sendWithKeyboard( - $chatId, - "🏦 *Recarga con MercadoPago*\n\n\$" . number_format($monto) . "\nRef: `{$ref}`\n\n[👉 Recargar ahora]({$url})\n\nDespués de pagar, envía la foto del comprobante aquí.", - [[['text' => '🔙 Menú principal', 'callback_data' => 'menu']]] - ); - } private function askRemitenteNombre(string $chatId, int $monto): void { @@ -1319,7 +1267,11 @@ class TelegramBotService if ($titular) { $text .= "Titular: *{$titular}*\n"; } - $text .= "\n⚠️ Después de transferir, envía la *foto del comprobante* aquí y el sistema lo validará automáticamente." + $text .= "\n📋 *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" + . "⚠️ Después de transferir, envía la *foto del comprobante* aquí y el sistema lo validará automáticamente." . "\n_Tienes hasta 15 minutos después de la transferencia para enviar el soporte._"; if ($userId = ($this->getState($chatId)['user_id'] ?? null)) { @@ -1679,39 +1631,4 @@ class TelegramBotService return mb_substr($local, 0, $visible) . str_repeat('*', max(0, $len - $visible)) . '@' . $domain; } - private function createMPPreference(string $titulo, int $valor, string $ref): ?string - { - $accessToken = config('services.mercadopago.token'); - if (! $accessToken) { - return null; - } - - try { - $response = 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']], - ], - ]); - - return $response->successful() ? $response->json('init_point') : null; - } catch (\Throwable $e) { - Log::warning('[TelegramMP] Error: ' . $e->getMessage()); - return null; - } - } } diff --git a/resources/views/livewire/chat/public-chat.blade.php b/resources/views/livewire/chat/public-chat.blade.php index b57e005..f39dc34 100755 --- a/resources/views/livewire/chat/public-chat.blade.php +++ b/resources/views/livewire/chat/public-chat.blade.php @@ -236,6 +236,7 @@
@forelse($mensajes as $msg)