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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user