diff --git a/migrations/migrate.go b/migrations/migrate.go
index e647837..f141559 100755
--- a/migrations/migrate.go
+++ b/migrations/migrate.go
@@ -58,6 +58,7 @@ func Migrate() {
// Pasarelas de pago
&models.BoldConfig{},
&models.BoldWebhookLog{},
+ &models.BoldCallbackLog{},
&models.DlocalPaymentLog{},
); err != nil {
log.Fatalf("Error during main migration: %v", err)
diff --git a/pkg/models/bold_config.go b/pkg/models/bold_config.go
index 15c1502..f74db14 100644
--- a/pkg/models/bold_config.go
+++ b/pkg/models/bold_config.go
@@ -104,3 +104,74 @@ func GetBoldWebhookLogs(limit int) ([]BoldWebhookLog, error) {
}
return logs, nil
}
+
+// GetBoldWebhookLogsPaginated devuelve los logs con paginación y filtro por tipo.
+func GetBoldWebhookLogsPaginated(page, limit int, tipo string) ([]BoldWebhookLog, int64, error) {
+ var logs []BoldWebhookLog
+ var total int64
+ db := app.Http.Database.DB.Model(&BoldWebhookLog{})
+ if tipo != "" && tipo != "TODOS" {
+ db = db.Where("tipo = ?", tipo)
+ }
+ db.Count(&total)
+ offset := (page - 1) * limit
+ if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&logs).Error; err != nil {
+ return nil, 0, err
+ }
+ return logs, total, nil
+}
+
+// ─── Callback log (intentos de pago) ─────────────────────────────────────────
+
+// BoldCallbackLog registra cada visita a la URL de retorno de Bold.
+// Esto captura usuarios que iniciaron el proceso de pago (llegaron al checkout)
+// pero pueden haber abandonado, fallado o completado el pago.
+type BoldCallbackLog struct {
+ gorm.Model
+ // Referencia principal recibida de Bold (bold-order-id / payment_link / reference)
+ Referencia string `json:"referencia" gorm:"column:referencia;type:varchar(120);index"`
+ PaymentLink string `json:"payment_link" gorm:"column:payment_link;type:varchar(80)"`
+ // Todos los parámetros GET recibidos en JSON (para depuración)
+ Params string `json:"params" gorm:"column:params;type:text"`
+ // Estado: pendiente | pagado | fallido | revertido
+ Estado string `json:"estado" gorm:"column:estado;type:varchar(20);default:'pendiente'"`
+ // Datos del cliente si están disponibles
+ PayerEmail string `json:"payer_email" gorm:"column:payer_email;type:varchar(255)"`
+ // Datos de red (para análisis)
+ IP string `json:"ip" gorm:"column:ip;type:varchar(45)"`
+ UserAgent string `json:"user_agent" gorm:"column:user_agent;type:text"`
+}
+
+func (BoldCallbackLog) TableName() string { return "bold_callback_log" }
+
+// SaveBoldCallbackLog guarda un registro de intento de pago.
+func SaveBoldCallbackLog(entry BoldCallbackLog) error {
+ return app.Http.Database.DB.Create(&entry).Error
+}
+
+// UpdateBoldCallbackEstado actualiza el estado de los intentos que coincidan con la referencia.
+// Se llama desde el webhook cuando llega un evento SALE_APPROVED, SALE_REJECTED, etc.
+func UpdateBoldCallbackEstado(referencia, estado string) {
+ if referencia == "" {
+ return
+ }
+ app.Http.Database.DB.Model(&BoldCallbackLog{}).
+ Where("referencia = ? AND estado = 'pendiente'", referencia).
+ Update("estado", estado)
+}
+
+// GetBoldCallbackLogsPaginated devuelve los intentos de pago con paginación y filtro.
+func GetBoldCallbackLogsPaginated(page, limit int, estado string) ([]BoldCallbackLog, int64, error) {
+ var logs []BoldCallbackLog
+ var total int64
+ db := app.Http.Database.DB.Model(&BoldCallbackLog{})
+ if estado != "" && estado != "TODOS" {
+ db = db.Where("estado = ?", estado)
+ }
+ db.Count(&total)
+ offset := (page - 1) * limit
+ if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&logs).Error; err != nil {
+ return nil, 0, err
+ }
+ return logs, total, nil
+}
diff --git a/resources/views/pasarelas_pago.html b/resources/views/pasarelas_pago.html
index 99d2e28..de22a4b 100644
--- a/resources/views/pasarelas_pago.html
+++ b/resources/views/pasarelas_pago.html
@@ -68,6 +68,33 @@
+
+
+
+ Configuración
+
+ Notificaciones
+
+
+
+ Intentos de pago
+
+
+
+
+
+
+
@@ -199,7 +226,7 @@
Registra esta URL en el panel Bold → Configuración → Webhooks
- /webhooks/bold
+ /webhooks/bold
@@ -207,9 +234,7 @@
-
- La firma se verifica con HMAC-SHA256. En modo test la secret key puede ser vacía.
-
+
La firma se verifica con HMAC-SHA256. En modo test la secret key puede ser vacía.
@@ -234,48 +259,269 @@
+
+
+
-
-
-
-
Últimas notificaciones
- Actualizar
-
-
- Sin notificaciones recibidas aún.
-
-
-
-
-
- Tipo
- Payment ID
- Monto
- Estado
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Refrescar
+
+
+
+
+
+
+ Sin notificaciones recibidas aún.
+
+
+
+
+
+ Evento
+ Payment ID
+ Referencia
+ Email pagador
+ Monto
+ Estado
+ Fecha
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
Mostrando de registros
+
+ ← Ant
+ Pág
+ Sig →
+
+
+
+
+
+
+ ¿Qué es este registro? 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 pagado . Si el usuario abandonó antes de pagar queda en pendiente .
+
+
+
+
+
+
+
+
+
+
+
+
+ Refrescar
+
+
+
+
+
+
+ Sin intentos registrados aún. Los intentos aparecen cuando un usuario llega al checkout de Bold.
+
+
+
+
+
+ Estado
+ Referencia
+ Payment Link
+ Email
+ IP
+ Fecha intento
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mostrando de intentos
+
+ ← Ant
+ Pág
+ Sig →
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Detalle de notificación
+
+
+
Notification ID
+
Payment ID
+
Referencia
+
Email pagador
+
Monto
+
Procesado
+
Fecha
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Detalle de intento de pago
+
+
Estado
+
+
Referencia
+
Payment Link
+
Email
+
IP
+
Fecha
+
+
+
Parámetros recibidos de Bold
+
+
+
@@ -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';
}
diff --git a/resources/views/query_runner.html b/resources/views/query_runner.html
index 093bf2f..5164df9 100644
--- a/resources/views/query_runner.html
+++ b/resources/views/query_runner.html
@@ -44,20 +44,68 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Cargando campos…
+
Sin documentos en esta colección.
+
+
+
+
+
@@ -127,10 +175,90 @@
style="height:180px; tab-size:2;">
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generador de actualización
+ updateOne / updateMany
+
+
+
+
+
+ updateOne
+ updateMany
+
+
+ ↗ Generar en editor
+
+
+
+
@@ -233,12 +361,22 @@ document.addEventListener('alpine:init', () => {
testOk: true,
toast: { show: false, msg: '', type: 'ok' },
isMongo: false,
+ selectedCollection: '',
+ collectionFields: [],
+ loadingFields: false,
+ showUpdateBuilder: false,
+ updateBuilder: { collection: '', field: '', value: '', filterField: '_id', filterValue: '""', op: '$set', multi: 'one' },
mongoExamples: [
{ label: 'find()', cmd: 'db.coleccion.find({})' },
{ label: 'findOne()', cmd: 'db.coleccion.findOne({})' },
{ label: 'count()', cmd: 'db.coleccion.countDocuments({})' },
{ label: 'insertOne()', cmd: 'db.coleccion.insertOne({"campo": "valor"})' },
- { label: 'updateOne()', cmd: 'db.coleccion.updateOne({"_id": ""}, {"$set": {"campo": "valor"}})' },
+ { label: 'updateOne()', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$set": {"campo": "valor"}}\n)' },
+ { label: 'updateMany()', cmd: 'db.coleccion.updateMany(\n {},\n {"$set": {"campo": "valor"}}\n)' },
+ { label: '$unset', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$unset": {"campo": ""}}\n)' },
+ { label: '$inc', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$inc": {"numero": 1}}\n)' },
+ { label: '$push', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$push": {"array": "nuevo_elemento"}}\n)' },
+ { label: '$pull', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$pull": {"array": "elemento"}}\n)' },
{ label: 'deleteOne()', cmd: 'db.coleccion.deleteOne({"_id": ""})' },
{ label: 'aggregate()', cmd: 'db.coleccion.aggregate([{"$group": {"_id": "$campo", "total": {"$sum": 1}}}])' },
],
@@ -266,6 +404,8 @@ document.addEventListener('alpine:init', () => {
this.statusMsg = '';
this.history = [];
this.isMongo = false;
+ this.selectedCollection = '';
+ this.collectionFields = [];
if (!this.selectedConxId) return;
const conx = this.conexiones.find(c => c.ID == this.selectedConxId);
this.isMongo = conx?.tipo_db?.nombre?.toLowerCase().includes('mongo') ?? false;
@@ -280,6 +420,8 @@ document.addEventListener('alpine:init', () => {
async loadTables() {
this.tables = [];
+ this.selectedCollection = '';
+ this.collectionFields = [];
if (!this.selectedDb) return;
try {
const res = await axios.get('/app/query-runner/tables?conx_db_id=' + this.selectedConxId + '&db=' + encodeURIComponent(this.selectedDb));
@@ -384,6 +526,62 @@ document.addEventListener('alpine:init', () => {
this.runQuery();
},
+ async loadCollectionFields(name) {
+ this.selectedCollection = name;
+ this.collectionFields = [];
+ if (!this.selectedConxId || !name) return;
+ this.loadingFields = true;
+ try {
+ const res = await axios.post('/app/query-runner/run', {
+ conx_db_id: parseInt(this.selectedConxId),
+ database: this.selectedDb,
+ sql: `db.${name}.findOne({})`
+ });
+ const rows = res.data?.rows;
+ if (rows && rows.length > 0) {
+ this.collectionFields = Object.entries(rows[0]).map(([k, v]) => ({
+ name: k,
+ type: v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v,
+ preview: v === null ? 'null' : Array.isArray(v) ? `[${v.length} elem]` : String(v).substring(0, 30),
+ sample: v
+ }));
+ }
+ } catch (_) {}
+ this.loadingFields = false;
+ },
+
+ buildUpdateField(fieldName) {
+ const col = this.selectedCollection || 'coleccion';
+ const f = this.collectionFields.find(f => f.name === fieldName);
+ let sampleVal = '"nuevo_valor"';
+ if (f && f.sample !== null && f.sample !== undefined) {
+ if (typeof f.sample === 'number') sampleVal = String(f.sample);
+ else if (typeof f.sample === 'boolean') sampleVal = String(f.sample);
+ else if (Array.isArray(f.sample)) sampleVal = '[]';
+ else if (typeof f.sample === 'object') sampleVal = '{}';
+ else {
+ const safe = String(f.sample).replace(/\\/g, '\\\\').replace(/"/g, '\\"').substring(0, 50);
+ sampleVal = `"${safe}"`;
+ }
+ }
+ this.sqlText = `db.${col}.updateOne(\n { "_id": "" },\n { "$set": { "${fieldName}": ${sampleVal} } }\n)`;
+ document.getElementById('sql-editor')?.focus();
+ },
+
+ generateUpdate() {
+ const col = this.updateBuilder.collection || this.selectedCollection || 'coleccion';
+ const field = this.updateBuilder.field || 'campo';
+ const value = this.updateBuilder.value || '"nuevo_valor"';
+ const filterField = this.updateBuilder.filterField || '_id';
+ const filterValue = this.updateBuilder.filterValue || '""';
+ const op = this.updateBuilder.op || '$set';
+ const method = this.updateBuilder.multi === 'many' ? 'updateMany' : 'updateOne';
+ let updateDoc;
+ if (op === '$unset') updateDoc = `{ "${field}": "" }`;
+ else updateDoc = `{ "${field}": ${value} }`;
+ this.sqlText = `db.${col}.${method}(\n { "${filterField}": ${filterValue} },\n { "${op}": ${updateDoc} }\n)`;
+ },
+
clearEditor() { this.sqlText = ''; this.results = []; this.columns = []; this.statusMsg = ''; },
nullStr(v) { return v === null || v === undefined ? 'NULL' : String(v); },
diff --git a/rest/controllers/api/bold_controller.go b/rest/controllers/api/bold_controller.go
index b6cd63d..b11437d 100644
--- a/rest/controllers/api/bold_controller.go
+++ b/rest/controllers/api/bold_controller.go
@@ -83,9 +83,26 @@ func BoldWebhook(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
}
- // ─── 6. Solo procesar SALE_APPROVED ─────────────────────────────────────
+ // ─── 6. Actualizar estado en callback_log para TODOS los eventos ─────────
+ if referencia != "" {
+ estimado := "pendiente"
+ switch tipo {
+ case "SALE_APPROVED":
+ estimado = "pagado"
+ case "SALE_REJECTED":
+ estimado = "fallido"
+ case "SALE_REVERSED", "CHARGEBACK":
+ estimado = "revertido"
+ }
+ models.UpdateBoldCallbackEstado(referencia, estimado)
+ if paymentID != "" {
+ models.UpdateBoldCallbackEstado(paymentID, estimado)
+ }
+ }
+
+ // Solo continuar lógica de negocio para SALE_APPROVED ─────────────────────
if tipo != "SALE_APPROVED" {
- log.Printf("[BOLD] Webhook: tipo '%s' ignorado", tipo)
+ log.Printf("[BOLD] Webhook: tipo '%s' registrado", tipo)
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
}
diff --git a/rest/controllers/api/pago_controller.go b/rest/controllers/api/pago_controller.go
index e120ecc..f821689 100644
--- a/rest/controllers/api/pago_controller.go
+++ b/rest/controllers/api/pago_controller.go
@@ -10,9 +10,52 @@ import (
)
// PagoExitosoPage renderiza la página pública de confirmación de pago.
-// Bold redirige al cliente aquí tras el pago. La URL puede incluir ?ref=contrato-{id}.
+// Bold redirige al cliente aquí tras el pago. Registra el intento en bold_callback_log.
func PagoExitosoPage(c *fiber.Ctx) error {
- ref := c.Query("ref", "")
+ // Recolectar todos los parámetros posibles que Bold puede enviar
+ params := map[string]string{}
+ for _, k := range []string{"bold-order-id", "order_id", "payment_link", "reference", "id", "ref"} {
+ if v := c.Query(k, ""); v != "" {
+ params[k] = v
+ }
+ }
+ c.Request().URI().QueryArgs().VisitAll(func(k, v []byte) {
+ params[string(k)] = string(v)
+ })
+
+ // Resolver la referencia principal (orden de prioridad)
+ ref := ""
+ for _, k := range []string{"ref", "reference", "bold-order-id", "payment_link", "order_id", "id"} {
+ if v := params[k]; v != "" {
+ ref = v
+ break
+ }
+ }
+ paymentLink := params["payment_link"]
+
+ // Serializar todos los params para auditoría
+ paramsJSON := "{"
+ for k, v := range params {
+ paramsJSON += `"` + k + `":"` + v + `",`
+ }
+ if len(paramsJSON) > 1 {
+ paramsJSON = paramsJSON[:len(paramsJSON)-1]
+ }
+ paramsJSON += "}"
+
+ // Registrar el intento de pago en la tabla de callbacks
+ if ref != "" || paymentLink != "" {
+ entry := models.BoldCallbackLog{
+ Referencia: ref,
+ PaymentLink: paymentLink,
+ Params: paramsJSON,
+ Estado: "pendiente",
+ IP: c.IP(),
+ UserAgent: string(c.Request().Header.UserAgent()),
+ }
+ _ = models.SaveBoldCallbackLog(entry)
+ }
+
return c.Render("pago_exitoso", fiber.Map{"Ref": ref}, "layouts/landing")
}
diff --git a/rest/controllers/pasarelas_controller.go b/rest/controllers/pasarelas_controller.go
index 7ea5e12..ee17395 100644
--- a/rest/controllers/pasarelas_controller.go
+++ b/rest/controllers/pasarelas_controller.go
@@ -129,13 +129,46 @@ func GetDlocalConfigAPI(c *fiber.Ctx) error {
// ─── Logs Bold ────────────────────────────────────────────────────────────────
-// BoldWebhookLogs devuelve los últimos 50 registros del log de webhooks de Bold.
+// BoldWebhookLogs devuelve los logs de notificaciones con paginación y filtro por tipo.
+// Query params: page (default 1), limit (default 25), tipo (SALE_APPROVED|SALE_REJECTED|...|TODOS)
func BoldWebhookLogs(c *fiber.Ctx) error {
- logs, err := models.GetBoldWebhookLogs(50)
+ page := c.QueryInt("page", 1)
+ limit := c.QueryInt("limit", 25)
+ tipo := c.Query("tipo", "")
+ if page < 1 {
+ page = 1
+ }
+ logs, total, err := models.GetBoldWebhookLogsPaginated(page, limit, tipo)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
- return c.JSON(fiber.Map{"data": logs})
+ return c.JSON(fiber.Map{
+ "data": logs,
+ "total": total,
+ "page": page,
+ "limit": limit,
+ })
+}
+
+// BoldCallbackLogs devuelve los intentos de pago (visitas al callback) con paginación y filtro.
+// Query params: page (default 1), limit (default 25), estado (pendiente|pagado|fallido|revertido|TODOS)
+func BoldCallbackLogs(c *fiber.Ctx) error {
+ page := c.QueryInt("page", 1)
+ limit := c.QueryInt("limit", 25)
+ estado := c.Query("estado", "")
+ if page < 1 {
+ page = 1
+ }
+ logs, total, err := models.GetBoldCallbackLogsPaginated(page, limit, estado)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{
+ "data": logs,
+ "total": total,
+ "page": page,
+ "limit": limit,
+ })
}
// ─── Logs dLocal ──────────────────────────────────────────────────────────────
diff --git a/rest/routes/user.go b/rest/routes/user.go
index 474a518..02e37f6 100755
--- a/rest/routes/user.go
+++ b/rest/routes/user.go
@@ -135,6 +135,7 @@ func UserRoutes(app fiber.Router) {
protected.Get("/pasarelas/bold/config", controllers.GetBoldConfigAPI)
protected.Post("/pasarelas/bold/save", controllers.SaveBoldConfig)
protected.Get("/pasarelas/bold/logs", controllers.BoldWebhookLogs)
+ protected.Get("/pasarelas/bold/callbacks", controllers.BoldCallbackLogs)
// dLocal
protected.Get("/pasarelas/dlocal/config", controllers.GetDlocalConfigAPI)
protected.Post("/pasarelas/dlocal/save", controllers.SaveDlocalConfigWeb)