feat: validación de pago mejorada — IMAP progresivo, auto-reintento, solo Bre-B, recargar-y-comprar
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
23eb448433
commit
6bd0045826
@@ -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');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\recarga;
|
||||
use App\Models\Saldo;
|
||||
use App\Models\SolicitudRecarga;
|
||||
use App\Models\User;
|
||||
use App\Services\PagoValidadorService;
|
||||
use App\Services\TelegramBotService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ValidarPagoTelegramJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 1;
|
||||
public int $timeout = 60;
|
||||
|
||||
private const MAX_INTENTOS = 9;
|
||||
private const STATE_TTL = 315360000;
|
||||
|
||||
public function __construct(
|
||||
private string $chatId,
|
||||
private array $datosPago,
|
||||
private int $intento = 1
|
||||
) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$cacheKey = "tgbot_state_{$this->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'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +236,7 @@
|
||||
<div class="flex-1 min-h-0 overflow-y-auto px-3 py-4 space-y-3 ios-scroll"
|
||||
id="pub-messages"
|
||||
wire:poll.3000ms="pollMensajes"
|
||||
wire:poll.20000ms="autoReintentarPago"
|
||||
style="overscroll-behavior-y:contain;-webkit-overflow-scrolling:touch;">
|
||||
|
||||
@forelse($mensajes as $msg)
|
||||
|
||||
Reference in New Issue
Block a user