up
This commit is contained in:
@@ -13,11 +13,11 @@ type BoldConfig struct {
|
|||||||
ApiKeyProd string `json:"api_key_prod" gorm:"column:api_key_prod;type:text"`
|
ApiKeyProd string `json:"api_key_prod" gorm:"column:api_key_prod;type:text"`
|
||||||
SecretKeyProd string `json:"secret_key_prod" gorm:"column:secret_key_prod;type:text"`
|
SecretKeyProd string `json:"secret_key_prod" gorm:"column:secret_key_prod;type:text"`
|
||||||
// Claves de prueba / test
|
// Claves de prueba / test
|
||||||
ApiKeyTest string `json:"api_key_test" gorm:"column:api_key_test;type:text"`
|
ApiKeyTest string `json:"api_key_test" gorm:"column:api_key_test;type:text"`
|
||||||
// En modo test la secret key es cadena vacía según la doc oficial
|
// En modo test la secret key es cadena vacía según la doc oficial
|
||||||
SecretKeyTest string `json:"secret_key_test" gorm:"column:secret_key_test;type:text"`
|
SecretKeyTest string `json:"secret_key_test" gorm:"column:secret_key_test;type:text"`
|
||||||
// Modo activo: "test" | "production"
|
// Modo activo: "test" | "production"
|
||||||
Modo string `json:"modo" gorm:"column:modo;default:'test'"`
|
Modo string `json:"modo" gorm:"column:modo;default:'test'"`
|
||||||
// URL a la que Bold redirige al usuario tras el pago
|
// URL a la que Bold redirige al usuario tras el pago
|
||||||
CallbackUrl string `json:"callback_url" gorm:"column:callback_url;type:text"`
|
CallbackUrl string `json:"callback_url" gorm:"column:callback_url;type:text"`
|
||||||
// Nota interna
|
// Nota interna
|
||||||
@@ -186,6 +186,16 @@ func UpdateBoldCallbackEstado(referencia, estado string) {
|
|||||||
Update("estado", estado)
|
Update("estado", estado)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBoldCallbackLogByID carga un intento de pago por su PK.
|
||||||
|
func GetBoldCallbackLogByID(id uint, out *BoldCallbackLog) error {
|
||||||
|
return app.Http.Database.DB.First(out, id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBoldCallbackEstadoByID actualiza el estado de un intento de pago específico por su PK.
|
||||||
|
func UpdateBoldCallbackEstadoByID(id uint, estado string) error {
|
||||||
|
return app.Http.Database.DB.Model(&BoldCallbackLog{}).Where("id = ?", id).Update("estado", estado).Error
|
||||||
|
}
|
||||||
|
|
||||||
// GetBoldCallbackLogsPaginated devuelve los intentos de pago con paginación y filtro.
|
// GetBoldCallbackLogsPaginated devuelve los intentos de pago con paginación y filtro.
|
||||||
func GetBoldCallbackLogsPaginated(page, limit int, estado string) ([]BoldCallbackLog, int64, error) {
|
func GetBoldCallbackLogsPaginated(page, limit int, estado string) ([]BoldCallbackLog, int64, error) {
|
||||||
var logs []BoldCallbackLog
|
var logs []BoldCallbackLog
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ type Contrato struct {
|
|||||||
// Enlace de pago Bold: único por ciclo de pago.
|
// Enlace de pago Bold: único por ciclo de pago.
|
||||||
// Se reutiliza en múltiples notificaciones del mismo ciclo.
|
// Se reutiliza en múltiples notificaciones del mismo ciclo.
|
||||||
// Se anula (vacía) cuando SALE_APPROVED llega y se registra el pago.
|
// Se anula (vacía) cuando SALE_APPROVED llega y se registra el pago.
|
||||||
EnlacePago string `json:"enlace_pago" gorm:"column:enlace_pago;type:text"`
|
EnlacePago string `json:"enlace_pago" gorm:"column:enlace_pago;type:text"`
|
||||||
EnlacePagoLinkID string `json:"enlace_pago_link_id" gorm:"column:enlace_pago_link_id;type:varchar(64)"`
|
EnlacePagoLinkID string `json:"enlace_pago_link_id" gorm:"column:enlace_pago_link_id;type:varchar(64)"`
|
||||||
// Confirmación de pago
|
// Confirmación de pago
|
||||||
PagoConfirmado bool `json:"pago_confirmado" gorm:"column:pago_confirmado;default:false"`
|
PagoConfirmado bool `json:"pago_confirmado" gorm:"column:pago_confirmado;default:false"`
|
||||||
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
||||||
@@ -191,9 +191,9 @@ func LimpiarEnlacePago(contratoID uint) error {
|
|||||||
func MarcarContratoPagado(contratoID uint) error {
|
func MarcarContratoPagado(contratoID uint) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return app.Http.Database.DB.Model(&Contrato{}).Where("id = ?", contratoID).Updates(map[string]interface{}{
|
return app.Http.Database.DB.Model(&Contrato{}).Where("id = ?", contratoID).Updates(map[string]interface{}{
|
||||||
"pago_confirmado": true,
|
"pago_confirmado": true,
|
||||||
"fecha_pago": now,
|
"fecha_pago": now,
|
||||||
"enlace_pago": "",
|
"enlace_pago": "",
|
||||||
"enlace_pago_link_id": "",
|
"enlace_pago_link_id": "",
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ type DlocalPaymentLog struct {
|
|||||||
gorm.Model
|
gorm.Model
|
||||||
// notification_id único; para entradas manuales se genera con prefijo "manual-"
|
// notification_id único; para entradas manuales se genera con prefijo "manual-"
|
||||||
NotificationID string `json:"notification_id" gorm:"column:notification_id;uniqueIndex;type:varchar(128);not null"`
|
NotificationID string `json:"notification_id" gorm:"column:notification_id;uniqueIndex;type:varchar(128);not null"`
|
||||||
Fuente string `json:"fuente" gorm:"column:fuente;type:varchar(20)"` // "webhook" | "manual"
|
Fuente string `json:"fuente" gorm:"column:fuente;type:varchar(20)"` // "webhook" | "manual"
|
||||||
Tipo string `json:"tipo" gorm:"column:tipo;type:varchar(50)"` // PAYMENT, SUBSCRIPTION_CHARGE, …
|
Tipo string `json:"tipo" gorm:"column:tipo;type:varchar(50)"` // PAYMENT, SUBSCRIPTION_CHARGE, …
|
||||||
Estado string `json:"estado" gorm:"column:estado;type:varchar(30)"` // PAID, PENDING, REJECTED, CANCELLED, EXPIRED
|
Estado string `json:"estado" gorm:"column:estado;type:varchar(30)"` // PAID, PENDING, REJECTED, CANCELLED, EXPIRED
|
||||||
PaymentID string `json:"payment_id" gorm:"column:payment_id;type:varchar(64)"`
|
PaymentID string `json:"payment_id" gorm:"column:payment_id;type:varchar(64)"`
|
||||||
OrderID string `json:"order_id" gorm:"column:order_id;type:varchar(120)"`
|
OrderID string `json:"order_id" gorm:"column:order_id;type:varchar(120)"`
|
||||||
Referencia string `json:"referencia" gorm:"column:referencia;type:varchar(120)"`
|
Referencia string `json:"referencia" gorm:"column:referencia;type:varchar(120)"`
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type NotificacionRegla struct {
|
|||||||
PlantillaID uint `json:"plantilla_id" gorm:"column:plantilla_id"`
|
PlantillaID uint `json:"plantilla_id" gorm:"column:plantilla_id"`
|
||||||
Plantilla PlantillaCorreo `json:"plantilla" gorm:"foreignKey:PlantillaID"`
|
Plantilla PlantillaCorreo `json:"plantilla" gorm:"foreignKey:PlantillaID"`
|
||||||
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||||
AplicaA string `json:"aplica_a" gorm:"column:aplica_a;default:'todos'"` // todos | renovable | unico
|
AplicaA string `json:"aplica_a" gorm:"column:aplica_a;default:'todos'"` // todos | renovable | unico
|
||||||
PasarelaEnlace string `json:"pasarela_enlace" gorm:"column:pasarela_enlace;default:'bold'"` // bold | dlocal | ninguna
|
PasarelaEnlace string `json:"pasarela_enlace" gorm:"column:pasarela_enlace;default:'bold'"` // bold | dlocal | ninguna
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import (
|
|||||||
// La vinculación es: Contrato → Servicios (m2m) → servicio_id ↔ SaasProducto.ServicioID → SaasApiConfig.SaasID
|
// La vinculación es: Contrato → Servicios (m2m) → servicio_id ↔ SaasProducto.ServicioID → SaasApiConfig.SaasID
|
||||||
type SaasApiConfig struct {
|
type SaasApiConfig struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
SaasID uint `json:"saas_id" gorm:"column:saas_id;not null;index"`
|
SaasID uint `json:"saas_id" gorm:"column:saas_id;not null;index"`
|
||||||
SaasProducto SaasProducto `json:"saas_producto" gorm:"foreignKey:SaasID"`
|
SaasProducto SaasProducto `json:"saas_producto" gorm:"foreignKey:SaasID"`
|
||||||
Nombre string `json:"nombre" gorm:"column:nombre;not null"` // etiqueta amigable
|
Nombre string `json:"nombre" gorm:"column:nombre;not null"` // etiqueta amigable
|
||||||
// Pasarela que dispara este callback: dlocal | bold | ambas (default)
|
// Pasarela que dispara este callback: dlocal | bold | ambas (default)
|
||||||
Pasarela string `json:"pasarela" gorm:"column:pasarela;default:'ambas'"`
|
Pasarela string `json:"pasarela" gorm:"column:pasarela;default:'ambas'"`
|
||||||
EndpointURL string `json:"endpoint_url" gorm:"column:endpoint_url;type:text;not null"`
|
EndpointURL string `json:"endpoint_url" gorm:"column:endpoint_url;type:text;not null"`
|
||||||
Metodo string `json:"metodo" gorm:"column:metodo;default:'POST'"` // POST|PUT|GET
|
Metodo string `json:"metodo" gorm:"column:metodo;default:'POST'"` // POST|PUT|GET
|
||||||
ApiKeyHeader string `json:"api_key_header" gorm:"column:api_key_header"` // ej: "X-API-Key"
|
ApiKeyHeader string `json:"api_key_header" gorm:"column:api_key_header"` // ej: "X-API-Key"
|
||||||
ApiKeyValue string `json:"api_key_value" gorm:"column:api_key_value;type:text"` // valor secreto
|
ApiKeyValue string `json:"api_key_value" gorm:"column:api_key_value;type:text"` // valor secreto
|
||||||
// PayloadTemplate es un JSON con marcadores que se reemplazarán antes de enviar.
|
// PayloadTemplate es un JSON con marcadores que se reemplazarán antes de enviar.
|
||||||
// Variables disponibles: {{.ContratoID}} {{.Referencia}} {{.Email}} {{.Monto}} {{.Moneda}} {{.SaasID}} {{.SaasSlug}} {{.Fuente}}
|
// Variables disponibles: {{.ContratoID}} {{.Referencia}} {{.Email}} {{.Monto}} {{.Moneda}} {{.SaasID}} {{.SaasSlug}} {{.Fuente}}
|
||||||
PayloadTemplate string `json:"payload_template" gorm:"column:payload_template;type:text"`
|
PayloadTemplate string `json:"payload_template" gorm:"column:payload_template;type:text"`
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ func ParseBoldWebhookEvent(rawBody []byte) (*BoldWebhookEvent, error) {
|
|||||||
// La API de Bold devuelve los campos directamente en el root del JSON (no dentro de "payload").
|
// La API de Bold devuelve los campos directamente en el root del JSON (no dentro de "payload").
|
||||||
type BoldLinkStatus struct {
|
type BoldLinkStatus struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"` // ACTIVE | PROCESSING | PAID | REJECTED | CANCELLED | EXPIRED
|
Status string `json:"status"` // ACTIVE | PROCESSING | PAID | REJECTED | CANCELLED | EXPIRED
|
||||||
TransactionID string `json:"transaction_id"` // ID de la transacción cuando está pagado
|
TransactionID string `json:"transaction_id"` // ID de la transacción cuando está pagado
|
||||||
Reference string `json:"reference"`
|
Reference string `json:"reference"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
|
|||||||
@@ -434,6 +434,7 @@
|
|||||||
<td class="py-3 px-4 font-mono text-gray-400" x-text="cb.ip || '—'"></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 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">
|
<td class="py-3 px-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<button @click="selectedCallback = cb; showCallbackModal = true" title="Ver parámetros recibidos"
|
<button @click="selectedCallback = cb; showCallbackModal = true" title="Ver parámetros recibidos"
|
||||||
class="text-gray-400 hover:text-[#8eb02f] transition-colors">
|
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">
|
<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">
|
||||||
@@ -441,6 +442,17 @@
|
|||||||
<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"/>
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Validar: solo en intentos pendientes -->
|
||||||
|
<button x-show="cb.estado === 'pendiente'"
|
||||||
|
@click="validarCallback(cb)"
|
||||||
|
:disabled="validandoCallbackID === cb.ID"
|
||||||
|
title="Verificar si el pago realmente se realizó"
|
||||||
|
class="flex items-center gap-1 text-[10px] px-2 py-0.5 rounded border border-indigo-300 text-indigo-600 hover:bg-indigo-50 disabled:opacity-40 transition-colors">
|
||||||
|
<svg x-show="validandoCallbackID !== cb.ID" class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"/></svg>
|
||||||
|
<svg x-show="validandoCallbackID === cb.ID" class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
||||||
|
Validar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -1030,6 +1042,7 @@ function pasarelasApp() {
|
|||||||
showCallbackModal: false,
|
showCallbackModal: false,
|
||||||
selectedCallback: null,
|
selectedCallback: null,
|
||||||
validandoLogID: 0,
|
validandoLogID: 0,
|
||||||
|
validandoCallbackID: 0,
|
||||||
|
|
||||||
// ─── dLocal ─────────────────────────────────────────────────────
|
// ─── dLocal ─────────────────────────────────────────────────────
|
||||||
dlocalModo: 'dev',
|
dlocalModo: 'dev',
|
||||||
@@ -1155,6 +1168,24 @@ function pasarelasApp() {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async validarCallback(cb) {
|
||||||
|
this.validandoCallbackID = cb.ID;
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(`/app/pasarelas/bold/callbacks/${cb.ID}/validar`);
|
||||||
|
if (data.ok && data.data) {
|
||||||
|
const idx = this.boldCallbacks.findIndex(c => c.ID === cb.ID);
|
||||||
|
if (idx !== -1) this.boldCallbacks[idx] = data.data;
|
||||||
|
}
|
||||||
|
const msg = data.mensaje || (data.confirmado ? 'Pago confirmado ✓' : 'Pago aún no confirmado');
|
||||||
|
this.showToast(msg, data.confirmado ? 'success' : 'error');
|
||||||
|
if (data.confirmado) this.loadBoldCallbacks();
|
||||||
|
} catch (e) {
|
||||||
|
this.showToast(e.response?.data?.error || 'Error al validar', 'error');
|
||||||
|
} finally {
|
||||||
|
this.validandoCallbackID = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
copyWebhook() {
|
copyWebhook() {
|
||||||
const base = window.location.origin;
|
const base = window.location.origin;
|
||||||
const url = base + '/webhooks/bold';
|
const url = base + '/webhooks/bold';
|
||||||
|
|||||||
@@ -348,8 +348,8 @@ func DlocalRegistrarPago(c *fiber.Ctx) error {
|
|||||||
PayerEmail string `json:"payer_email"`
|
PayerEmail string `json:"payer_email"`
|
||||||
Monto float64 `json:"monto"`
|
Monto float64 `json:"monto"`
|
||||||
Moneda string `json:"moneda"`
|
Moneda string `json:"moneda"`
|
||||||
Estado string `json:"estado"` // PAID, PENDING, REJECTED, …
|
Estado string `json:"estado"` // PAID, PENDING, REJECTED, …
|
||||||
Tipo string `json:"tipo"` // PAYMENT, SUBSCRIPTION_CHARGE, manual, …
|
Tipo string `json:"tipo"` // PAYMENT, SUBSCRIPTION_CHARGE, manual, …
|
||||||
Nota string `json:"nota"`
|
Nota string `json:"nota"`
|
||||||
}
|
}
|
||||||
var b body
|
var b body
|
||||||
|
|||||||
@@ -244,34 +244,34 @@ func GetHistorialContrato(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// Construir timeline unificado
|
// Construir timeline unificado
|
||||||
type Evento struct {
|
type Evento struct {
|
||||||
Tipo string `json:"tipo"` // creacion | renovacion | notificacion | pago
|
Tipo string `json:"tipo"` // creacion | renovacion | notificacion | pago
|
||||||
Icono string `json:"icono"`
|
Icono string `json:"icono"`
|
||||||
Titulo string `json:"titulo"`
|
Titulo string `json:"titulo"`
|
||||||
Detalle string `json:"detalle"`
|
Detalle string `json:"detalle"`
|
||||||
Estado string `json:"estado"` // ok | error | info
|
Estado string `json:"estado"` // ok | error | info
|
||||||
FechaISO string `json:"fecha"`
|
FechaISO string `json:"fecha"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeline []Evento
|
var timeline []Evento
|
||||||
|
|
||||||
// Evento: creación del contrato
|
// Evento: creación del contrato
|
||||||
timeline = append(timeline, Evento{
|
timeline = append(timeline, Evento{
|
||||||
Tipo: "creacion",
|
Tipo: "creacion",
|
||||||
Icono: "document",
|
Icono: "document",
|
||||||
Titulo: "Contrato creado",
|
Titulo: "Contrato creado",
|
||||||
Detalle: "Inicio: " + contrato.FechaInicio.Format("02/01/2006") + " · Vence: " + contrato.FechaVencimiento.Format("02/01/2006"),
|
Detalle: "Inicio: " + contrato.FechaInicio.Format("02/01/2006") + " · Vence: " + contrato.FechaVencimiento.Format("02/01/2006"),
|
||||||
Estado: "info",
|
Estado: "info",
|
||||||
FechaISO: contrato.CreatedAt.Format(time.RFC3339),
|
FechaISO: contrato.CreatedAt.Format(time.RFC3339),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Evento: renovaciones (updated_at con estado renovado — heurístico por estado)
|
// Evento: renovaciones (updated_at con estado renovado — heurístico por estado)
|
||||||
if contrato.Estado == "renovado" {
|
if contrato.Estado == "renovado" {
|
||||||
timeline = append(timeline, Evento{
|
timeline = append(timeline, Evento{
|
||||||
Tipo: "renovacion",
|
Tipo: "renovacion",
|
||||||
Icono: "refresh",
|
Icono: "refresh",
|
||||||
Titulo: "Contrato renovado",
|
Titulo: "Contrato renovado",
|
||||||
Detalle: "Nuevo vencimiento: " + contrato.FechaVencimiento.Format("02/01/2006"),
|
Detalle: "Nuevo vencimiento: " + contrato.FechaVencimiento.Format("02/01/2006"),
|
||||||
Estado: "ok",
|
Estado: "ok",
|
||||||
FechaISO: contrato.UpdatedAt.Format(time.RFC3339),
|
FechaISO: contrato.UpdatedAt.Format(time.RFC3339),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -293,11 +293,11 @@ func GetHistorialContrato(c *fiber.Ctx) error {
|
|||||||
reglaLabel = " (" + n.Regla.Nombre + ")"
|
reglaLabel = " (" + n.Regla.Nombre + ")"
|
||||||
}
|
}
|
||||||
timeline = append(timeline, Evento{
|
timeline = append(timeline, Evento{
|
||||||
Tipo: "notificacion",
|
Tipo: "notificacion",
|
||||||
Icono: "mail",
|
Icono: "mail",
|
||||||
Titulo: "Notificación enviada" + reglaLabel,
|
Titulo: "Notificación enviada" + reglaLabel,
|
||||||
Detalle: detalle,
|
Detalle: detalle,
|
||||||
Estado: estado,
|
Estado: estado,
|
||||||
FechaISO: n.CreatedAt.Format(time.RFC3339),
|
FechaISO: n.CreatedAt.Format(time.RFC3339),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -316,11 +316,11 @@ func GetHistorialContrato(c *fiber.Ctx) error {
|
|||||||
detalle += " · " + p.Fuente
|
detalle += " · " + p.Fuente
|
||||||
}
|
}
|
||||||
timeline = append(timeline, Evento{
|
timeline = append(timeline, Evento{
|
||||||
Tipo: "pago",
|
Tipo: "pago",
|
||||||
Icono: "currency",
|
Icono: "currency",
|
||||||
Titulo: "Pago recibido",
|
Titulo: "Pago recibido",
|
||||||
Detalle: detalle,
|
Detalle: detalle,
|
||||||
Estado: estado,
|
Estado: estado,
|
||||||
FechaISO: p.CreatedAt.Format(time.RFC3339),
|
FechaISO: p.CreatedAt.Format(time.RFC3339),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,6 +315,104 @@ func ValidarDlocalLog(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Validación de intento de pago (callback) ───────────────────────────────
|
||||||
|
|
||||||
|
// ValidarBoldCallback verifica si el pago de un intento pendiente realmente se realizó.
|
||||||
|
// Recorre las mismas fuentes que verificarPago() y, si confirma, marca el contrato + actualiza el callback.
|
||||||
|
// POST /app/pasarelas/bold/callbacks/:id/validar
|
||||||
|
func ValidarBoldCallback(c *fiber.Ctx) error {
|
||||||
|
cbID, err := c.ParamsInt("id")
|
||||||
|
if err != nil || cbID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var cb models.BoldCallbackLog
|
||||||
|
if err := models.GetBoldCallbackLogByID(uint(cbID), &cb); err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Intento no encontrado"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var contratoID uint
|
||||||
|
if _, err := fmt.Sscanf(cb.Referencia, "contrato-%d", &contratoID); err != nil || contratoID == 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Referencia no tiene formato contrato-{id}"})
|
||||||
|
}
|
||||||
|
|
||||||
|
contrato, err := models.GetContratoParaVerificacion(contratoID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Contrato no encontrado"})
|
||||||
|
}
|
||||||
|
|
||||||
|
pagoConfirmado := contrato.PagoConfirmado
|
||||||
|
fuente := ""
|
||||||
|
|
||||||
|
// ─── 1. Ya confirmado en DB ───────────────────────────────────────────────
|
||||||
|
if pagoConfirmado {
|
||||||
|
fuente = "db"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 2. dlocal_payment_log ───────────────────────────────────────────────
|
||||||
|
if !pagoConfirmado {
|
||||||
|
if dlLogs, dlErr := models.GetDlocalPaymentLogsByRef(cb.Referencia); dlErr == nil {
|
||||||
|
for _, l := range dlLogs {
|
||||||
|
if l.Estado == "PAID" || l.Estado == "AUTHORIZED" {
|
||||||
|
pagoConfirmado = true
|
||||||
|
fuente = "dlocal_log"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 3. Bold API ─────────────────────────────────────────────────────────
|
||||||
|
if !pagoConfirmado && contrato.EnlacePagoLinkID != "" {
|
||||||
|
if boldCfg, boldErr := models.GetBoldConfig(); boldErr == nil {
|
||||||
|
if paid, _, _, apiErr := services.CheckBoldLinkPaid(boldCfg, contrato.EnlacePagoLinkID); apiErr == nil && paid {
|
||||||
|
pagoConfirmado = true
|
||||||
|
fuente = "bold_api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 4. dLocal API ───────────────────────────────────────────────────────
|
||||||
|
if !pagoConfirmado {
|
||||||
|
if dlocalCfg, dlErr := models.GetLastActiveDlocalApi(); dlErr == nil {
|
||||||
|
if paid, _, _, _, apiErr := services.CheckDlocalPaymentByOrderID(*dlocalCfg, cb.Referencia); apiErr == nil && paid {
|
||||||
|
pagoConfirmado = true
|
||||||
|
fuente = "dlocal_api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar contrato si se confirmó ahora
|
||||||
|
if pagoConfirmado && !contrato.PagoConfirmado {
|
||||||
|
_ = models.MarcarContratoPagado(contratoID)
|
||||||
|
go services.EnviarCorreoConfirmacionPago(contratoID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar estado del callback
|
||||||
|
nuevoEstado := cb.Estado
|
||||||
|
if pagoConfirmado && cb.Estado == "pendiente" {
|
||||||
|
nuevoEstado = "pagado"
|
||||||
|
_ = models.UpdateBoldCallbackEstadoByID(uint(cbID), "pagado")
|
||||||
|
cb.Estado = "pagado"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := "Pago aún no confirmado"
|
||||||
|
if pagoConfirmado && !contrato.PagoConfirmado {
|
||||||
|
msg = fmt.Sprintf("Pago confirmado via %s — contrato marcado como pagado", fuente)
|
||||||
|
} else if pagoConfirmado {
|
||||||
|
msg = fmt.Sprintf("Pago ya confirmado (%s)", fuente)
|
||||||
|
}
|
||||||
|
_ = nuevoEstado
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"ok": true,
|
||||||
|
"confirmado": pagoConfirmado,
|
||||||
|
"fuente": fuente,
|
||||||
|
"mensaje": msg,
|
||||||
|
"data": cb,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Validación de log API_CHECK ─────────────────────────────────────────────
|
// ─── Validación de log API_CHECK ─────────────────────────────────────────────
|
||||||
|
|
||||||
// ValidarBoldLog verifica el estado de pago real de un log API_CHECK consultando
|
// ValidarBoldLog verifica el estado de pago real de un log API_CHECK consultando
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ func UserRoutes(app fiber.Router) {
|
|||||||
protected.Get("/pasarelas/bold/logs", controllers.BoldWebhookLogs)
|
protected.Get("/pasarelas/bold/logs", controllers.BoldWebhookLogs)
|
||||||
protected.Post("/pasarelas/bold/logs/:id/validar", controllers.ValidarBoldLog)
|
protected.Post("/pasarelas/bold/logs/:id/validar", controllers.ValidarBoldLog)
|
||||||
protected.Get("/pasarelas/bold/callbacks", controllers.BoldCallbackLogs)
|
protected.Get("/pasarelas/bold/callbacks", controllers.BoldCallbackLogs)
|
||||||
|
protected.Post("/pasarelas/bold/callbacks/:id/validar", controllers.ValidarBoldCallback)
|
||||||
// dLocal
|
// dLocal
|
||||||
protected.Get("/pasarelas/dlocal/config", controllers.GetDlocalConfigAPI)
|
protected.Get("/pasarelas/dlocal/config", controllers.GetDlocalConfigAPI)
|
||||||
protected.Post("/pasarelas/dlocal/save", controllers.SaveDlocalConfigWeb)
|
protected.Post("/pasarelas/dlocal/save", controllers.SaveDlocalConfigWeb)
|
||||||
|
|||||||
Reference in New Issue
Block a user