This commit is contained in:
Lizandro Guarnizo
2026-05-01 22:55:51 -05:00
parent ddbabea23f
commit 560359173b
8 changed files with 755 additions and 80 deletions
+365 -54
View File
@@ -68,6 +68,33 @@
<!-- ═══════════════════════════════════════════════════════════════════ -->
<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 -->
@@ -199,7 +226,7 @@
</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" id="webhookUrl">/webhooks/bold</span>
<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">
@@ -207,9 +234,7 @@
</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>
<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 -->
@@ -234,48 +259,269 @@
</div>
</div>
</div>
</div>
</div>
</div><!-- /sub-tab config -->
<!-- Log de webhooks recientes (tabla) -->
<div class="border border-gray-200 rounded-xl p-5">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-gray-700">Últimas notificaciones</h3>
<button @click="loadBoldLogs()" class="text-xs text-[#8eb02f] hover:underline">Actualizar</button>
</div>
<div x-show="boldLogs.length === 0" class="text-xs text-gray-400 text-center py-4">
Sin notificaciones recibidas aún.
</div>
<div x-show="boldLogs.length > 0" class="overflow-x-auto">
<table class="w-full text-xs table-auto">
<thead>
<tr class="text-left border-b font-semibold text-gray-500">
<th class="pb-2 pr-3">Tipo</th>
<th class="pb-2 pr-3">Payment ID</th>
<th class="pb-2 pr-3">Monto</th>
<th class="pb-2">Estado</th>
<!-- ── 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>
</thead>
<tbody>
<template x-for="log in boldLogs" :key="log.ID">
<tr class="border-b last:border-0">
<td class="py-2 pr-3">
<span class="px-2 py-0.5 rounded font-semibold"
:class="log.tipo === 'SALE_APPROVED' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-600'"
x-text="log.tipo"></span>
</td>
<td class="py-2 pr-3 font-mono text-gray-600" x-text="log.payment_id || '—'"></td>
<td class="py-2 pr-3 text-gray-700" x-text="log.monto ? '$' + log.monto.toLocaleString('es-CO') : '—'"></td>
<td class="py-2">
<span class="px-2 py-0.5 rounded"
:class="log.procesado ? 'bg-green-50 text-green-600' : 'bg-yellow-50 text-yellow-600'"
x-text="log.procesado ? 'Procesado' : 'Pendiente'"></span>
</td>
</tr>
</template>
</tbody>
</table>
</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>
@@ -434,7 +680,36 @@ function pasarelasApp() {
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',
@@ -452,6 +727,7 @@ function pasarelasApp() {
init() {
this.loadBold();
this.loadDlocal();
// Carga perezosa: se carga cuando el usuario abre esos sub-tabs
this.loadBoldLogs();
},
@@ -462,13 +738,13 @@ function pasarelasApp() {
if (r.data.data) {
const d = r.data.data;
this.bold = {
id: d.ID || 0,
api_key_prod: d.api_key_prod || '',
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 || '',
api_key_test: d.api_key_test || '',
secret_key_test: d.secret_key_test || '',
callback_url: d.callback_url || '',
nota: d.nota || '',
callback_url: d.callback_url || '',
nota: d.nota || '',
};
this.boldModo = d.modo || 'test';
}
@@ -491,8 +767,38 @@ function pasarelasApp() {
async loadBoldLogs() {
try {
const r = await axios.get('/app/pasarelas/bold/logs');
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 (_) {}
},
@@ -504,6 +810,11 @@ function pasarelasApp() {
});
},
tryPrettyJson(raw) {
if (!raw) return '';
try { return JSON.stringify(JSON.parse(raw), null, 2); } catch (_) { return raw; }
},
// ─── dLocal helpers ──────────────────────────────────────────────
async loadDlocal() {
try {
@@ -511,13 +822,13 @@ function pasarelasApp() {
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 || '',
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';
}