860 lines
58 KiB
HTML
860 lines
58 KiB
HTML
<div x-data="pasarelasApp()" class="bg-white rounded-lg shadow">
|
|
|
|
<!-- Overlay de carga -->
|
|
<div x-show="loading" class="fixed inset-0 bg-gray-800 bg-opacity-75 flex justify-center items-center z-50">
|
|
<img src="../img/loading.gif" alt="Cargando..." class="w-16 h-16" />
|
|
</div>
|
|
|
|
<!-- Toast de notificación -->
|
|
<div x-show="toast.show" x-transition
|
|
:class="toast.type === 'error' ? 'bg-red-600' : 'bg-green-600'"
|
|
class="fixed bottom-5 right-5 z-50 text-white text-sm px-5 py-3 rounded-xl shadow-lg flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
|
</svg>
|
|
<span x-text="toast.msg"></span>
|
|
</div>
|
|
|
|
<div class="container mx-auto p-6 w-full">
|
|
|
|
<!-- Header -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold">Pasarelas de Pago</h1>
|
|
<p class="text-xs text-slate-500 mt-0.5">Configura las credenciales de cada pasarela y selecciona el entorno activo.</p>
|
|
</div>
|
|
<!-- Indicador de modo activo por pasarela -->
|
|
<div class="flex gap-2 flex-wrap">
|
|
<span class="px-3 py-1 rounded-full text-xs font-semibold flex items-center gap-1"
|
|
:class="boldModo === 'production' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'">
|
|
<span class="w-2 h-2 rounded-full inline-block"
|
|
:class="boldModo === 'production' ? 'bg-green-500' : 'bg-yellow-500'"></span>
|
|
Bold — <span x-text="boldModo === 'production' ? 'Producción' : 'Test'"></span>
|
|
</span>
|
|
<span class="px-3 py-1 rounded-full text-xs font-semibold flex items-center gap-1"
|
|
:class="dlocalModo === 'prod' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'">
|
|
<span class="w-2 h-2 rounded-full inline-block"
|
|
:class="dlocalModo === 'prod' ? 'bg-green-500' : 'bg-yellow-500'"></span>
|
|
dLocal — <span x-text="dlocalModo === 'prod' ? 'Producción' : 'Desarrollo'"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="border-b border-gray-200 mb-6">
|
|
<nav class="-mb-px flex gap-6 text-sm">
|
|
<button @click="activeTab = 'bold'"
|
|
:class="activeTab === 'bold' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'"
|
|
class="pb-2 transition-colors flex items-center gap-2">
|
|
<!-- Bold icon -->
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M6 3h7a5 5 0 0 1 5 5 5 5 0 0 1-2.5 4.33A5.5 5.5 0 0 1 13 22H6V3zm3 3v4h4a2 2 0 0 0 0-4H9zm0 7v4h4.5a2.5 2.5 0 0 0 0-5H9z"/>
|
|
</svg>
|
|
Bold
|
|
</button>
|
|
<button @click="activeTab = 'dlocal'"
|
|
:class="activeTab === 'dlocal' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'"
|
|
class="pb-2 transition-colors flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
|
</svg>
|
|
dLocal
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
|
<!-- TAB: BOLD -->
|
|
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
|
<div x-show="activeTab === 'bold'" x-transition>
|
|
|
|
<!-- Sub-tabs Bold -->
|
|
<div class="border-b border-gray-100 mb-5">
|
|
<nav class="-mb-px flex gap-5 text-xs">
|
|
<button @click="boldTab = 'config'"
|
|
:class="boldTab==='config' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-400 hover:text-gray-600'"
|
|
class="pb-2 transition-colors">Configuración</button>
|
|
<button @click="boldTab = 'notificaciones'; loadBoldLogs()"
|
|
:class="boldTab==='notificaciones' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-400 hover:text-gray-600'"
|
|
class="pb-2 transition-colors flex items-center gap-1">
|
|
Notificaciones
|
|
<span x-show="boldLogsTotal > 0"
|
|
class="bg-[#8eb02f] text-white text-[10px] px-1.5 py-0.5 rounded-full"
|
|
x-text="boldLogsTotal"></span>
|
|
</button>
|
|
<button @click="boldTab = 'intentos'; loadBoldCallbacks()"
|
|
:class="boldTab==='intentos' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-400 hover:text-gray-600'"
|
|
class="pb-2 transition-colors flex items-center gap-1">
|
|
Intentos de pago
|
|
<span x-show="boldCallbacksTotal > 0"
|
|
class="bg-orange-400 text-white text-[10px] px-1.5 py-0.5 rounded-full"
|
|
x-text="boldCallbacksTotal"></span>
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- ── Sub-tab: Configuración ─────────────────────────────────── -->
|
|
<div x-show="boldTab === 'config'">
|
|
<div class="grid lg:grid-cols-2 gap-6">
|
|
|
|
<!-- Formulario de credenciales Bold -->
|
|
<div class="border border-gray-200 rounded-xl p-5">
|
|
<h2 class="text-base font-semibold mb-1 flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#8eb02f]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
|
</svg>
|
|
Credenciales Bold
|
|
</h2>
|
|
<p class="text-xs text-gray-500 mb-4">Panel Bold → Configuración → Integraciones → API</p>
|
|
|
|
<!-- Selector de entorno -->
|
|
<div class="mb-5">
|
|
<label class="block text-xs font-medium text-gray-600 mb-1.5">Entorno activo</label>
|
|
<div class="flex gap-3">
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" x-model="boldModo" value="test" class="accent-[#8eb02f]"/>
|
|
<span class="text-sm px-2 py-0.5 rounded bg-yellow-50 text-yellow-700 font-medium">Test / Sandbox</span>
|
|
</label>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" x-model="boldModo" value="production" class="accent-[#8eb02f]"/>
|
|
<span class="text-sm px-2 py-0.5 rounded bg-green-50 text-green-700 font-medium">Producción</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sección Test -->
|
|
<div class="mb-5 p-4 rounded-lg bg-yellow-50 border border-yellow-200">
|
|
<p class="text-xs font-semibold text-yellow-700 mb-3 flex items-center gap-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/>
|
|
</svg>
|
|
Entorno de prueba
|
|
</p>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">API Key (test)</label>
|
|
<input x-model="bold.api_key_test" type="text" placeholder="pk_test_..." class="ui-input text-xs font-mono" autocomplete="off"/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">
|
|
Secret Key (test)
|
|
<span class="text-gray-400 font-normal">(puede quedar vacía en sandbox)</span>
|
|
</label>
|
|
<input x-model="bold.secret_key_test" type="password" placeholder="Dejar vacío para test" class="ui-input text-xs font-mono" autocomplete="off"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sección Producción -->
|
|
<div class="mb-5 p-4 rounded-lg bg-green-50 border border-green-200">
|
|
<p class="text-xs font-semibold text-green-700 mb-3 flex items-center gap-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
|
</svg>
|
|
Producción
|
|
</p>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">API Key (producción)</label>
|
|
<input x-model="bold.api_key_prod" type="text" placeholder="pk_live_..." class="ui-input text-xs font-mono" autocomplete="off"/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Secret Key (producción)</label>
|
|
<input x-model="bold.secret_key_prod" type="password" placeholder="sk_live_..." class="ui-input text-xs font-mono" autocomplete="off"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Callback URL -->
|
|
<div class="mb-5">
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">
|
|
Callback URL
|
|
<span class="text-gray-400 font-normal">(Bold redirige aquí al usuario tras el pago)</span>
|
|
</label>
|
|
<input x-model="bold.callback_url" type="url" placeholder="https://tudominio.com/pago-exitoso" class="ui-input text-xs"/>
|
|
</div>
|
|
|
|
<!-- Nota -->
|
|
<div class="mb-5">
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Nota interna</label>
|
|
<input x-model="bold.nota" type="text" placeholder="Opcional" class="ui-input text-xs"/>
|
|
</div>
|
|
|
|
<button @click="saveBold()"
|
|
class="w-full py-2.5 rounded-xl text-sm font-semibold text-white transition-all"
|
|
style="background-color:#8eb02f"
|
|
onmouseover="this.style.backgroundColor='#6d8c24'"
|
|
onmouseout="this.style.backgroundColor='#8eb02f'">
|
|
Guardar configuración Bold
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Info / Guía Bold -->
|
|
<div class="space-y-4">
|
|
|
|
<!-- Estado actual -->
|
|
<div class="border border-gray-200 rounded-xl p-5">
|
|
<h3 class="text-sm font-semibold mb-3 text-gray-700">Estado actual</h3>
|
|
<div class="space-y-2 text-xs">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500">Entorno</span>
|
|
<span class="font-semibold"
|
|
:class="boldModo === 'production' ? 'text-green-600' : 'text-yellow-600'"
|
|
x-text="boldModo === 'production' ? 'Producción 🟢' : 'Test / Sandbox 🟡'"></span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500">API Key (activa)</span>
|
|
<span class="font-mono text-gray-700"
|
|
x-text="boldModo === 'production'
|
|
? (bold.api_key_prod ? bold.api_key_prod.slice(0,8)+'...' : '—')
|
|
: (bold.api_key_test ? bold.api_key_test.slice(0,8)+'...' : '—')"></span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500">Callback URL</span>
|
|
<span class="font-mono text-gray-700 truncate max-w-[180px]" x-text="bold.callback_url || '—'"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- URL del Webhook -->
|
|
<div class="border border-gray-200 rounded-xl p-5">
|
|
<h3 class="text-sm font-semibold mb-2 flex items-center gap-2 text-gray-700">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-[#8eb02f]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
|
|
</svg>
|
|
URL del Webhook
|
|
</h3>
|
|
<p class="text-xs text-gray-500 mb-3">Registra esta URL en el panel Bold → Configuración → Webhooks</p>
|
|
<div class="flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-lg px-3 py-2">
|
|
<span class="text-xs font-mono text-gray-700 flex-1 truncate">/webhooks/bold</span>
|
|
<button @click="copyWebhook()" title="Copiar URL"
|
|
class="text-[#8eb02f] hover:text-[#6d8c24] flex-shrink-0">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<p class="text-xs text-gray-400 mt-2">La firma se verifica con HMAC-SHA256. En modo test la secret key puede ser vacía.</p>
|
|
</div>
|
|
|
|
<!-- Eventos soportados -->
|
|
<div class="border border-gray-200 rounded-xl p-5">
|
|
<h3 class="text-sm font-semibold mb-3 text-gray-700">Eventos del webhook</h3>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between text-xs">
|
|
<span class="font-mono bg-green-50 text-green-700 px-2 py-0.5 rounded">SALE_APPROVED</span>
|
|
<span class="text-gray-500">Pago aprobado ✅</span>
|
|
</div>
|
|
<div class="flex items-center justify-between text-xs">
|
|
<span class="font-mono bg-red-50 text-red-600 px-2 py-0.5 rounded">SALE_REJECTED</span>
|
|
<span class="text-gray-500">Pago rechazado ❌</span>
|
|
</div>
|
|
<div class="flex items-center justify-between text-xs">
|
|
<span class="font-mono bg-orange-50 text-orange-600 px-2 py-0.5 rounded">SALE_REVERSED</span>
|
|
<span class="text-gray-500">Reverso / devolución 🔄</span>
|
|
</div>
|
|
<div class="flex items-center justify-between text-xs">
|
|
<span class="font-mono bg-purple-50 text-purple-600 px-2 py-0.5 rounded">CHARGEBACK</span>
|
|
<span class="text-gray-500">Contracargo iniciado ⚠️</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div><!-- /sub-tab config -->
|
|
|
|
<!-- ── Sub-tab: Notificaciones ────────────────────────────────── -->
|
|
<div x-show="boldTab === 'notificaciones'">
|
|
<!-- Filtros + estadísticas rápidas -->
|
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
<!-- Contadores por tipo -->
|
|
<template x-for="stat in boldLogsStats" :key="stat.tipo">
|
|
<button @click="boldLogsFilter = stat.tipo; boldLogsPage = 1; loadBoldLogs()"
|
|
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs font-medium transition-all"
|
|
:class="boldLogsFilter === stat.tipo
|
|
? 'border-[#8eb02f] bg-[#f2f9e6] text-[#6d8c24]'
|
|
: 'border-gray-200 bg-white text-gray-600 hover:border-gray-300'">
|
|
<span :class="stat.color" class="w-2 h-2 rounded-full inline-block"></span>
|
|
<span x-text="stat.label"></span>
|
|
<span class="font-bold" x-text="stat.count"></span>
|
|
</button>
|
|
</template>
|
|
<button @click="loadBoldLogs()" class="ml-auto text-xs text-[#8eb02f] hover:underline flex items-center gap-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
</svg>
|
|
Refrescar
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tabla notificaciones -->
|
|
<div class="border border-gray-200 rounded-xl overflow-hidden">
|
|
<div x-show="boldLogs.length === 0" class="py-12 text-center text-gray-400 text-sm">
|
|
Sin notificaciones recibidas aún.
|
|
</div>
|
|
<div x-show="boldLogs.length > 0">
|
|
<table class="w-full text-xs table-auto">
|
|
<thead>
|
|
<tr class="bg-gray-50 text-left border-b">
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Evento</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Payment ID</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Referencia</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Email pagador</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Monto</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Estado</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Fecha</th>
|
|
<th class="py-3 px-4"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="log in boldLogs" :key="log.ID">
|
|
<tr class="border-b last:border-0 hover:bg-gray-50 transition-colors">
|
|
<td class="py-3 px-4">
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase"
|
|
:class="{
|
|
'bg-green-50 text-green-700': log.tipo === 'SALE_APPROVED',
|
|
'bg-red-50 text-red-600': log.tipo === 'SALE_REJECTED',
|
|
'bg-orange-50 text-orange-600': log.tipo === 'SALE_REVERSED',
|
|
'bg-purple-50 text-purple-600': log.tipo === 'CHARGEBACK',
|
|
'bg-blue-50 text-blue-600': !['SALE_APPROVED','SALE_REJECTED','SALE_REVERSED','CHARGEBACK'].includes(log.tipo)
|
|
}"
|
|
x-text="log.tipo || '—'"></span>
|
|
</td>
|
|
<td class="py-3 px-4 font-mono text-gray-500" x-text="(log.payment_id||'—').slice(0,16) + ((log.payment_id||'').length > 16 ? '…' : '')"></td>
|
|
<td class="py-3 px-4 font-mono text-gray-600" x-text="log.referencia || '—'"></td>
|
|
<td class="py-3 px-4 text-gray-600" x-text="log.payer_email || '—'"></td>
|
|
<td class="py-3 px-4 font-semibold text-gray-800" x-text="log.monto ? '$' + Number(log.monto).toLocaleString('es-CO') : '—'"></td>
|
|
<td class="py-3 px-4">
|
|
<span class="px-2 py-0.5 rounded text-[10px] font-semibold"
|
|
:class="log.procesado ? 'bg-green-50 text-green-600' : 'bg-yellow-50 text-yellow-600'"
|
|
x-text="log.procesado ? 'Procesado' : 'Pendiente'"></span>
|
|
</td>
|
|
<td class="py-3 px-4 text-gray-400" x-text="log.CreatedAt ? new Date(log.CreatedAt).toLocaleString('es-CO',{dateStyle:'short',timeStyle:'short'}) : '—'"></td>
|
|
<td class="py-3 px-4">
|
|
<button @click="selectedLog = log; showLogModal = true" title="Ver detalle"
|
|
class="text-gray-400 hover:text-[#8eb02f] transition-colors">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
</svg>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
<!-- Paginación -->
|
|
<div class="flex items-center justify-between px-4 py-3 border-t bg-gray-50 text-xs text-gray-500">
|
|
<span>Mostrando <strong x-text="boldLogs.length"></strong> de <strong x-text="boldLogsTotal"></strong> registros</span>
|
|
<div class="flex gap-2">
|
|
<button :disabled="boldLogsPage <= 1" @click="boldLogsPage--; loadBoldLogs()"
|
|
class="px-3 py-1 rounded border border-gray-200 disabled:opacity-40 hover:bg-white">← Ant</button>
|
|
<span class="px-2 py-1">Pág <strong x-text="boldLogsPage"></strong></span>
|
|
<button :disabled="boldLogs.length < boldLogsLimit" @click="boldLogsPage++; loadBoldLogs()"
|
|
class="px-3 py-1 rounded border border-gray-200 disabled:opacity-40 hover:bg-white">Sig →</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div><!-- /sub-tab notificaciones -->
|
|
|
|
<!-- ── Sub-tab: Intentos de pago ──────────────────────────────── -->
|
|
<div x-show="boldTab === 'intentos'">
|
|
<div class="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-xs text-amber-700">
|
|
<strong>¿Qué es este registro?</strong> Cada vez que un usuario llega a la página de retorno
|
|
de Bold (callback URL) se registra un intento. Si el webhook posterior confirma el pago,
|
|
el estado cambia a <em>pagado</em>. Si el usuario abandonó antes de pagar queda en <em>pendiente</em>.
|
|
</div>
|
|
|
|
<!-- Filtros -->
|
|
<div class="flex flex-wrap items-center gap-2 mb-4">
|
|
<template x-for="s in [{v:'',l:'Todos'},{v:'pendiente',l:'Pendientes'},{v:'pagado',l:'Pagados'},{v:'fallido',l:'Fallidos'},{v:'revertido',l:'Revertidos'}]" :key="s.v">
|
|
<button @click="boldCallbacksFilter = s.v; boldCallbacksPage = 1; loadBoldCallbacks()"
|
|
class="px-3 py-1.5 rounded-lg border text-xs font-medium transition-all"
|
|
:class="boldCallbacksFilter === s.v
|
|
? 'border-[#8eb02f] bg-[#f2f9e6] text-[#6d8c24]'
|
|
: 'border-gray-200 bg-white text-gray-500 hover:border-gray-300'"
|
|
x-text="s.l">
|
|
</button>
|
|
</template>
|
|
<button @click="loadBoldCallbacks()" class="ml-auto text-xs text-[#8eb02f] hover:underline flex items-center gap-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
</svg>
|
|
Refrescar
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tabla intentos -->
|
|
<div class="border border-gray-200 rounded-xl overflow-hidden">
|
|
<div x-show="boldCallbacks.length === 0" class="py-12 text-center text-gray-400 text-sm">
|
|
Sin intentos registrados aún. Los intentos aparecen cuando un usuario llega al checkout de Bold.
|
|
</div>
|
|
<div x-show="boldCallbacks.length > 0">
|
|
<table class="w-full text-xs table-auto">
|
|
<thead>
|
|
<tr class="bg-gray-50 text-left border-b">
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Estado</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Referencia</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Payment Link</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Email</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">IP</th>
|
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Fecha intento</th>
|
|
<th class="py-3 px-4"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="cb in boldCallbacks" :key="cb.ID">
|
|
<tr class="border-b last:border-0 hover:bg-gray-50 transition-colors">
|
|
<td class="py-3 px-4">
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase"
|
|
:class="{
|
|
'bg-yellow-50 text-yellow-700': cb.estado === 'pendiente',
|
|
'bg-green-50 text-green-700': cb.estado === 'pagado',
|
|
'bg-red-50 text-red-600': cb.estado === 'fallido',
|
|
'bg-orange-50 text-orange-600':cb.estado === 'revertido'
|
|
}"
|
|
x-text="cb.estado || 'pendiente'"></span>
|
|
</td>
|
|
<td class="py-3 px-4 font-mono text-gray-600" x-text="cb.referencia || '—'"></td>
|
|
<td class="py-3 px-4 font-mono text-gray-500" x-text="(cb.payment_link || '—').slice(0,12) + ((cb.payment_link||'').length > 12 ? '…' : '')"></td>
|
|
<td class="py-3 px-4 text-gray-600" x-text="cb.payer_email || '—'"></td>
|
|
<td class="py-3 px-4 font-mono text-gray-400" x-text="cb.ip || '—'"></td>
|
|
<td class="py-3 px-4 text-gray-400" x-text="cb.CreatedAt ? new Date(cb.CreatedAt).toLocaleString('es-CO',{dateStyle:'short',timeStyle:'short'}) : '—'"></td>
|
|
<td class="py-3 px-4">
|
|
<button @click="selectedCallback = cb; showCallbackModal = true" title="Ver parámetros recibidos"
|
|
class="text-gray-400 hover:text-[#8eb02f] transition-colors">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
</svg>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
<!-- Paginación -->
|
|
<div class="flex items-center justify-between px-4 py-3 border-t bg-gray-50 text-xs text-gray-500">
|
|
<span>Mostrando <strong x-text="boldCallbacks.length"></strong> de <strong x-text="boldCallbacksTotal"></strong> intentos</span>
|
|
<div class="flex gap-2">
|
|
<button :disabled="boldCallbacksPage <= 1" @click="boldCallbacksPage--; loadBoldCallbacks()"
|
|
class="px-3 py-1 rounded border border-gray-200 disabled:opacity-40 hover:bg-white">← Ant</button>
|
|
<span class="px-2 py-1">Pág <strong x-text="boldCallbacksPage"></strong></span>
|
|
<button :disabled="boldCallbacks.length < boldCallbacksLimit" @click="boldCallbacksPage++; loadBoldCallbacks()"
|
|
class="px-3 py-1 rounded border border-gray-200 disabled:opacity-40 hover:bg-white">Sig →</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div><!-- /sub-tab intentos -->
|
|
|
|
</div><!-- /TAB BOLD -->
|
|
|
|
<!-- Modal detalle de notificación webhook -->
|
|
<div x-show="showLogModal" x-transition class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
|
<div @click.outside="showLogModal = false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg p-6 relative">
|
|
<button @click="showLogModal = false" class="absolute top-4 right-4 text-gray-400 hover:text-gray-700">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
<h3 class="text-sm font-semibold mb-4 flex items-center gap-2">
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold"
|
|
:class="{
|
|
'bg-green-50 text-green-700': selectedLog?.tipo === 'SALE_APPROVED',
|
|
'bg-red-50 text-red-600': selectedLog?.tipo === 'SALE_REJECTED',
|
|
'bg-orange-50 text-orange-600': selectedLog?.tipo === 'SALE_REVERSED',
|
|
'bg-purple-50 text-purple-600': selectedLog?.tipo === 'CHARGEBACK',
|
|
}"
|
|
x-text="selectedLog?.tipo || ''"></span>
|
|
Detalle de notificación
|
|
</h3>
|
|
<div class="space-y-2 text-xs mb-4">
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Notification ID</span><span class="font-mono text-gray-700 text-right" x-text="selectedLog?.notification_id || '—'"></span></div>
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Payment ID</span><span class="font-mono text-gray-700" x-text="selectedLog?.payment_id || '—'"></span></div>
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Referencia</span><span class="font-mono text-gray-700" x-text="selectedLog?.referencia || '—'"></span></div>
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Email pagador</span><span class="text-gray-700" x-text="selectedLog?.payer_email || '—'"></span></div>
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Monto</span><span class="font-semibold text-gray-800" x-text="selectedLog?.monto ? '$' + Number(selectedLog.monto).toLocaleString('es-CO') : '—'"></span></div>
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Procesado</span><span :class="selectedLog?.procesado ? 'text-green-600' : 'text-yellow-600'" x-text="selectedLog?.procesado ? 'Sí' : 'No'"></span></div>
|
|
<div class="flex justify-between py-1.5"><span class="text-gray-500">Fecha</span><span class="text-gray-600" x-text="selectedLog?.CreatedAt ? new Date(selectedLog.CreatedAt).toLocaleString('es-CO') : '—'"></span></div>
|
|
</div>
|
|
<!-- Body raw -->
|
|
<div x-show="selectedLog?.raw">
|
|
<p class="text-[10px] font-semibold text-gray-400 uppercase mb-1">Payload RAW</p>
|
|
<pre class="bg-gray-50 border rounded-lg p-3 text-[10px] font-mono text-gray-600 overflow-x-auto max-h-40"
|
|
x-text="tryPrettyJson(selectedLog?.raw)"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal detalle de intento de pago -->
|
|
<div x-show="showCallbackModal" x-transition class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
|
<div @click.outside="showCallbackModal = false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg p-6 relative">
|
|
<button @click="showCallbackModal = false" class="absolute top-4 right-4 text-gray-400 hover:text-gray-700">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
<h3 class="text-sm font-semibold mb-4">Detalle de intento de pago</h3>
|
|
<div class="space-y-2 text-xs mb-4">
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Estado</span>
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase"
|
|
:class="{
|
|
'bg-yellow-50 text-yellow-700': selectedCallback?.estado === 'pendiente',
|
|
'bg-green-50 text-green-700': selectedCallback?.estado === 'pagado',
|
|
'bg-red-50 text-red-600': selectedCallback?.estado === 'fallido',
|
|
'bg-orange-50 text-orange-600':selectedCallback?.estado === 'revertido'
|
|
}"
|
|
x-text="selectedCallback?.estado || '—'"></span></div>
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Referencia</span><span class="font-mono text-gray-700" x-text="selectedCallback?.referencia || '—'"></span></div>
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Payment Link</span><span class="font-mono text-gray-700" x-text="selectedCallback?.payment_link || '—'"></span></div>
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">Email</span><span class="text-gray-700" x-text="selectedCallback?.payer_email || '—'"></span></div>
|
|
<div class="flex justify-between py-1.5 border-b"><span class="text-gray-500">IP</span><span class="font-mono text-gray-600" x-text="selectedCallback?.ip || '—'"></span></div>
|
|
<div class="flex justify-between py-1.5"><span class="text-gray-500">Fecha</span><span class="text-gray-600" x-text="selectedCallback?.CreatedAt ? new Date(selectedCallback.CreatedAt).toLocaleString('es-CO') : '—'"></span></div>
|
|
</div>
|
|
<div x-show="selectedCallback?.params">
|
|
<p class="text-[10px] font-semibold text-gray-400 uppercase mb-1">Parámetros recibidos de Bold</p>
|
|
<pre class="bg-gray-50 border rounded-lg p-3 text-[10px] font-mono text-gray-600 overflow-x-auto max-h-40"
|
|
x-text="tryPrettyJson(selectedCallback?.params)"></pre>
|
|
</div>
|
|
<div x-show="selectedCallback?.user_agent" class="mt-3">
|
|
<p class="text-[10px] font-semibold text-gray-400 uppercase mb-1">User Agent</p>
|
|
<p class="text-[10px] font-mono text-gray-500 break-all" x-text="selectedCallback?.user_agent"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
|
<!-- TAB: dLOCAL -->
|
|
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
|
<div x-show="activeTab === 'dlocal'" x-transition>
|
|
|
|
<div class="grid lg:grid-cols-2 gap-6">
|
|
|
|
<!-- Formulario dLocal -->
|
|
<div class="border border-gray-200 rounded-xl p-5">
|
|
<h2 class="text-base font-semibold mb-1 flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#8eb02f]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
|
</svg>
|
|
Credenciales dLocal
|
|
</h2>
|
|
<p class="text-xs text-gray-500 mb-4">Panel dLocal → API Credentials</p>
|
|
|
|
<!-- Selector de entorno -->
|
|
<div class="mb-5">
|
|
<label class="block text-xs font-medium text-gray-600 mb-1.5">Entorno activo</label>
|
|
<div class="flex gap-3">
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" x-model="dlocalModo" value="dev" class="accent-[#8eb02f]"/>
|
|
<span class="text-sm px-2 py-0.5 rounded bg-yellow-50 text-yellow-700 font-medium">Desarrollo</span>
|
|
</label>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" x-model="dlocalModo" value="prod" class="accent-[#8eb02f]"/>
|
|
<span class="text-sm px-2 py-0.5 rounded bg-green-50 text-green-700 font-medium">Producción</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- URLs -->
|
|
<div class="grid grid-cols-2 gap-3 mb-4">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">URL Producción</label>
|
|
<input x-model="dlocal.url_prod" type="url" placeholder="https://api.dlocal.com" class="ui-input text-xs"/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">URL Desarrollo</label>
|
|
<input x-model="dlocal.url_dev" type="url" placeholder="https://sandbox.dlocal.com" class="ui-input text-xs"/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Producción -->
|
|
<div class="mb-4 p-4 rounded-lg bg-green-50 border border-green-200">
|
|
<p class="text-xs font-semibold text-green-700 mb-3">Producción</p>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Access Key ID</label>
|
|
<input x-model="dlocal.access_key_id" type="text" placeholder="xkey_..." class="ui-input text-xs font-mono" autocomplete="off"/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Access Key Secret</label>
|
|
<input x-model="dlocal.access_key_secret" type="password" placeholder="•••••••" class="ui-input text-xs font-mono" autocomplete="off"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desarrollo -->
|
|
<div class="mb-5 p-4 rounded-lg bg-yellow-50 border border-yellow-200">
|
|
<p class="text-xs font-semibold text-yellow-700 mb-3">Desarrollo / Sandbox</p>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Access Key ID (dev)</label>
|
|
<input x-model="dlocal.access_key_id_dev" type="text" placeholder="xkey_dev_..." class="ui-input text-xs font-mono" autocomplete="off"/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Access Key Secret (dev)</label>
|
|
<input x-model="dlocal.access_key_secret_dev" type="password" placeholder="•••••••" class="ui-input text-xs font-mono" autocomplete="off"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button @click="saveDlocal()"
|
|
class="w-full py-2.5 rounded-xl text-sm font-semibold text-white transition-all"
|
|
style="background-color:#8eb02f"
|
|
onmouseover="this.style.backgroundColor='#6d8c24'"
|
|
onmouseout="this.style.backgroundColor='#8eb02f'">
|
|
Guardar configuración dLocal
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Info dLocal -->
|
|
<div class="space-y-4">
|
|
<div class="border border-gray-200 rounded-xl p-5">
|
|
<h3 class="text-sm font-semibold mb-3 text-gray-700">Estado actual</h3>
|
|
<div class="space-y-2 text-xs">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500">Entorno</span>
|
|
<span class="font-semibold"
|
|
:class="dlocalModo === 'prod' ? 'text-green-600' : 'text-yellow-600'"
|
|
x-text="dlocalModo === 'prod' ? 'Producción 🟢' : 'Desarrollo 🟡'"></span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500">Access Key (activa)</span>
|
|
<span class="font-mono text-gray-700"
|
|
x-text="dlocalModo === 'prod'
|
|
? (dlocal.access_key_id ? dlocal.access_key_id.slice(0,8)+'...' : '—')
|
|
: (dlocal.access_key_id_dev ? dlocal.access_key_id_dev.slice(0,8)+'...' : '—')"></span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500">URL activa</span>
|
|
<span class="font-mono text-gray-700 truncate max-w-[180px]"
|
|
x-text="dlocalModo === 'prod' ? (dlocal.url_prod || '—') : (dlocal.url_dev || '—')"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="border border-gray-200 rounded-xl p-5">
|
|
<h3 class="text-sm font-semibold mb-2 text-gray-700">Endpoints disponibles</h3>
|
|
<div class="space-y-1.5 text-xs">
|
|
<div class="flex items-center gap-2">
|
|
<span class="bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-mono text-[10px]">POST</span>
|
|
<span class="text-gray-600">/v1/dlocal/subscription/crear-plan</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="bg-green-50 text-green-600 px-1.5 py-0.5 rounded font-mono text-[10px]">GET</span>
|
|
<span class="text-gray-600">/v1/dlocal/subscription/ver-plan/:id</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="bg-yellow-50 text-yellow-600 px-1.5 py-0.5 rounded font-mono text-[10px]">PATCH</span>
|
|
<span class="text-gray-600">/v1/dlocal/subscription/actualizar-plan/:id</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-mono text-[10px]">POST</span>
|
|
<span class="text-gray-600">/v1/dlocal/payment/crear-pago</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /container -->
|
|
</div><!-- /x-data -->
|
|
|
|
<script>
|
|
function pasarelasApp() {
|
|
return {
|
|
activeTab: 'bold',
|
|
loading: false,
|
|
toast: { show: false, msg: '', type: 'success' },
|
|
|
|
// ─── Bold ───────────────────────────────────────────────────────
|
|
boldModo: 'test',
|
|
bold: {
|
|
id: 0,
|
|
api_key_prod: '',
|
|
secret_key_prod: '',
|
|
api_key_test: '',
|
|
secret_key_test: '',
|
|
callback_url: '',
|
|
nota: '',
|
|
},
|
|
|
|
// Sub-tabs Bold
|
|
boldTab: 'config',
|
|
|
|
// Notificaciones webhook
|
|
boldLogs: [],
|
|
boldLogsTotal: 0,
|
|
boldLogsPage: 1,
|
|
boldLogsLimit: 25,
|
|
boldLogsFilter: '',
|
|
boldLogsStats: [
|
|
{ tipo: '', label: 'Todos', color: 'bg-gray-400', count: 0 },
|
|
{ tipo: 'SALE_APPROVED', label: 'Aprobados', color: 'bg-green-400', count: 0 },
|
|
{ tipo: 'SALE_REJECTED', label: 'Rechazados', color: 'bg-red-400', count: 0 },
|
|
{ tipo: 'SALE_REVERSED', label: 'Revertidos', color: 'bg-orange-400', count: 0 },
|
|
{ tipo: 'CHARGEBACK', label: 'Contracargos', color: 'bg-purple-400', count: 0 },
|
|
],
|
|
|
|
// Intentos de pago (callbacks)
|
|
boldCallbacks: [],
|
|
boldCallbacksTotal: 0,
|
|
boldCallbacksPage: 1,
|
|
boldCallbacksLimit: 25,
|
|
boldCallbacksFilter: '',
|
|
|
|
// Modales detalle
|
|
showLogModal: false,
|
|
selectedLog: null,
|
|
showCallbackModal: false,
|
|
selectedCallback: null,
|
|
|
|
// ─── dLocal ─────────────────────────────────────────────────────
|
|
dlocalModo: 'dev',
|
|
dlocal: {
|
|
id: 0,
|
|
access_key_id: '',
|
|
access_key_secret: '',
|
|
access_key_id_dev: '',
|
|
access_key_secret_dev: '',
|
|
url_prod: '',
|
|
url_dev: '',
|
|
},
|
|
|
|
// ─── Init ───────────────────────────────────────────────────────
|
|
init() {
|
|
this.loadBold();
|
|
this.loadDlocal();
|
|
// Carga perezosa: se carga cuando el usuario abre esos sub-tabs
|
|
this.loadBoldLogs();
|
|
},
|
|
|
|
// ─── Bold helpers ────────────────────────────────────────────────
|
|
async loadBold() {
|
|
try {
|
|
const r = await axios.get('/app/pasarelas/bold/config');
|
|
if (r.data.data) {
|
|
const d = r.data.data;
|
|
this.bold = {
|
|
id: d.ID || 0,
|
|
api_key_prod: d.api_key_prod || '',
|
|
secret_key_prod: d.secret_key_prod || '',
|
|
api_key_test: d.api_key_test || '',
|
|
secret_key_test: d.secret_key_test || '',
|
|
callback_url: d.callback_url || '',
|
|
nota: d.nota || '',
|
|
};
|
|
this.boldModo = d.modo || 'test';
|
|
}
|
|
} catch (_) {}
|
|
},
|
|
|
|
async saveBold() {
|
|
this.loading = true;
|
|
try {
|
|
const payload = { ...this.bold, modo: this.boldModo };
|
|
await axios.post('/app/pasarelas/bold/save', payload);
|
|
this.showToast('Configuración Bold guardada ✓');
|
|
this.loadBold();
|
|
} catch (e) {
|
|
this.showToast(e.response?.data?.error || 'Error guardando Bold', 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async loadBoldLogs() {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: this.boldLogsPage,
|
|
limit: this.boldLogsLimit,
|
|
});
|
|
if (this.boldLogsFilter) params.set('tipo', this.boldLogsFilter);
|
|
const r = await axios.get('/app/pasarelas/bold/logs?' + params.toString());
|
|
this.boldLogs = r.data.data || [];
|
|
this.boldLogsTotal = r.data.total || 0;
|
|
// Actualizar contadores rápidos (solo cuando carga sin filtro)
|
|
if (!this.boldLogsFilter) {
|
|
const all = r.data.total || 0;
|
|
this.boldLogsStats[0].count = all;
|
|
// Cargar contadores por tipo en paralelo
|
|
['SALE_APPROVED','SALE_REJECTED','SALE_REVERSED','CHARGEBACK'].forEach((tipo, i) => {
|
|
axios.get(`/app/pasarelas/bold/logs?page=1&limit=1&tipo=${tipo}`)
|
|
.then(res => { this.boldLogsStats[i+1].count = res.data.total || 0; })
|
|
.catch(() => {});
|
|
});
|
|
}
|
|
} catch (_) {}
|
|
},
|
|
|
|
async loadBoldCallbacks() {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: this.boldCallbacksPage,
|
|
limit: this.boldCallbacksLimit,
|
|
});
|
|
if (this.boldCallbacksFilter) params.set('estado', this.boldCallbacksFilter);
|
|
const r = await axios.get('/app/pasarelas/bold/callbacks?' + params.toString());
|
|
this.boldCallbacks = r.data.data || [];
|
|
this.boldCallbacksTotal = r.data.total || 0;
|
|
} catch (_) {}
|
|
},
|
|
|
|
copyWebhook() {
|
|
const base = window.location.origin;
|
|
const url = base + '/webhooks/bold';
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
this.showToast('URL copiada: ' + url);
|
|
});
|
|
},
|
|
|
|
tryPrettyJson(raw) {
|
|
if (!raw) return '';
|
|
try { return JSON.stringify(JSON.parse(raw), null, 2); } catch (_) { return raw; }
|
|
},
|
|
|
|
// ─── dLocal helpers ──────────────────────────────────────────────
|
|
async loadDlocal() {
|
|
try {
|
|
const r = await axios.get('/app/pasarelas/dlocal/config');
|
|
if (r.data.data) {
|
|
const d = r.data.data;
|
|
this.dlocal = {
|
|
id: d.ID || 0,
|
|
access_key_id: d.access_key_id || '',
|
|
access_key_secret: d.access_key_secret || '',
|
|
access_key_id_dev: d.access_key_id_dev || '',
|
|
access_key_secret_dev: d.access_key_secret_dev || '',
|
|
url_prod: d.url_prod || '',
|
|
url_dev: d.url_dev || '',
|
|
};
|
|
this.dlocalModo = d.modo || 'dev';
|
|
}
|
|
} catch (_) {}
|
|
},
|
|
|
|
async saveDlocal() {
|
|
this.loading = true;
|
|
try {
|
|
const payload = { ...this.dlocal, modo: this.dlocalModo };
|
|
await axios.post('/app/pasarelas/dlocal/save', payload);
|
|
this.showToast('Configuración dLocal guardada ✓');
|
|
this.loadDlocal();
|
|
} catch (e) {
|
|
this.showToast(e.response?.data?.error || 'Error guardando dLocal', 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// ─── Toast ───────────────────────────────────────────────────────
|
|
showToast(msg, type = 'success') {
|
|
this.toast = { show: true, msg, type };
|
|
setTimeout(() => { this.toast.show = false; }, 3500);
|
|
},
|
|
};
|
|
}
|
|
</script>
|