up
This commit is contained in:
@@ -160,30 +160,35 @@ func ParseBoldWebhookEvent(rawBody []byte) (*BoldWebhookEvent, error) {
|
|||||||
|
|
||||||
// ─── Consulta directa de estado de link ──────────────────────────────────────
|
// ─── Consulta directa de estado de link ──────────────────────────────────────
|
||||||
|
|
||||||
// BoldLinkStatus refleja el campo status del payload de la API de Bold.
|
// BoldLinkStatus refleja la respuesta de GET /online/link/v1/{payment_link}.
|
||||||
|
// La API de Bold devuelve los campos directamente en el root del JSON (no dentro de "payload").
|
||||||
type BoldLinkStatus struct {
|
type BoldLinkStatus struct {
|
||||||
Payload struct {
|
ID string `json:"id"`
|
||||||
Status string `json:"status"` // ACTIVE | PAID | EXPIRED | CANCELLED
|
Status string `json:"status"` // ACTIVE | PROCESSING | PAID | REJECTED | CANCELLED | EXPIRED
|
||||||
PaymentID string `json:"payment_id"` // presente cuando está pagado
|
TransactionID string `json:"transaction_id"` // ID de la transacción cuando está pagado
|
||||||
Amount struct {
|
Reference string `json:"reference"`
|
||||||
Total int64 `json:"total_amount"`
|
Total int64 `json:"total"`
|
||||||
Currency string `json:"currency"`
|
|
||||||
} `json:"amount"`
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
} `json:"payload"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckBoldLinkPaid consulta la API de Bold para saber si un link ya fue pagado.
|
// CheckBoldLinkStatus consulta la API de Bold y devuelve el status del link.
|
||||||
// Devuelve (pagado bool, paymentID string, error).
|
// Posibles valores: ACTIVE, PROCESSING, PAID, REJECTED, CANCELLED, EXPIRED.
|
||||||
func CheckBoldLinkPaid(cfg *models.BoldConfig, linkID string) (bool, string, error) {
|
func CheckBoldLinkStatus(cfg *models.BoldConfig, linkID string) (status string, transactionID string, err error) {
|
||||||
raw, err := GetBoldPaymentLinkStatus(cfg, linkID)
|
raw, err := GetBoldPaymentLinkStatus(cfg, linkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
var result BoldLinkStatus
|
var result BoldLinkStatus
|
||||||
if err := json.Unmarshal(raw, &result); err != nil {
|
if err := json.Unmarshal(raw, &result); err != nil {
|
||||||
return false, "", fmt.Errorf("bold: parse link status: %w", err)
|
return "", "", fmt.Errorf("bold: parse link status: %w", err)
|
||||||
}
|
}
|
||||||
paid := result.Payload.Status == "PAID" || result.Payload.Status == "APPROVED"
|
return result.Status, result.TransactionID, nil
|
||||||
return paid, result.Payload.PaymentID, nil
|
}
|
||||||
|
|
||||||
|
// CheckBoldLinkPaid es un wrapper de compatibilidad sobre CheckBoldLinkStatus.
|
||||||
|
func CheckBoldLinkPaid(cfg *models.BoldConfig, linkID string) (bool, string, error) {
|
||||||
|
st, txID, err := CheckBoldLinkStatus(cfg, linkID)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
return st == "PAID" || st == "APPROVED", txID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,22 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Estado: pendiente (sin ref o no encontrado aún) -->
|
<!-- Estado: rechazado / cancelado -->
|
||||||
|
<div x-show="estado === 'rechazado'" x-cloak>
|
||||||
|
<div class="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg class="w-9 h-9 text-red-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-red-700 mb-2">Pago no aprobado</h1>
|
||||||
|
<p class="text-sm text-gray-500 mb-4">Tu pago fue rechazado o cancelado por la pasarela de pago.</p>
|
||||||
|
<p class="text-xs text-gray-400 mb-6">Puedes intentarlo nuevamente o contactar a soporte si el problema persiste.</p>
|
||||||
|
<a href="/" class="inline-block px-6 py-2 bg-red-500 text-white rounded-lg text-sm font-medium hover:bg-red-600 transition">
|
||||||
|
Volver al inicio
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estado: pendiente -->
|
||||||
<div x-show="estado === 'pendiente'" x-cloak>
|
<div x-show="estado === 'pendiente'" x-cloak>
|
||||||
<div class="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div class="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
<svg class="w-9 h-9 text-yellow-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
<svg class="w-9 h-9 text-yellow-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
@@ -49,22 +64,26 @@
|
|||||||
<script>
|
<script>
|
||||||
function pagoApp() {
|
function pagoApp() {
|
||||||
return {
|
return {
|
||||||
estado: 'verificando',
|
// Estado inicial inyectado por el servidor (confirmado | rechazado | pendiente).
|
||||||
fechaPago: '',
|
// Si ya viene resuelto no se hace polling.
|
||||||
ref: '',
|
estado: '{{ .Estado }}' || 'verificando',
|
||||||
|
fechaPago: '{{ .FechaPago }}',
|
||||||
|
ref: '{{ .Ref }}',
|
||||||
intentos: 0,
|
intentos: 0,
|
||||||
maxIntentos: 10,
|
maxIntentos: 12,
|
||||||
intervalo: null,
|
intervalo: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
// Si el servidor ya resolvió el estado, no hay que hacer polling
|
||||||
this.ref = params.get('ref') || '';
|
if (this.estado === 'confirmado' || this.estado === 'rechazado') return;
|
||||||
|
|
||||||
if (!this.ref) {
|
if (!this.ref) {
|
||||||
this.estado = 'pendiente';
|
this.estado = 'pendiente';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Estado aún indeterminado: arrancar polling
|
||||||
|
this.estado = 'verificando';
|
||||||
this.verificar();
|
this.verificar();
|
||||||
this.intervalo = setInterval(() => this.verificar(), 3000);
|
this.intervalo = setInterval(() => this.verificar(), 3000);
|
||||||
},
|
},
|
||||||
@@ -75,12 +94,17 @@ function pagoApp() {
|
|||||||
const r = await fetch(`/api/pago-estado?ref=${encodeURIComponent(this.ref)}`);
|
const r = await fetch(`/api/pago-estado?ref=${encodeURIComponent(this.ref)}`);
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
|
|
||||||
if (data.confirmado) {
|
if (data.estado === 'confirmado' || data.confirmado) {
|
||||||
this.estado = 'confirmado';
|
this.estado = 'confirmado';
|
||||||
this.fechaPago = data.fecha_pago || '';
|
this.fechaPago = data.fecha_pago || '';
|
||||||
clearInterval(this.intervalo);
|
clearInterval(this.intervalo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (data.estado === 'rechazado') {
|
||||||
|
this.estado = 'rechazado';
|
||||||
|
clearInterval(this.intervalo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
if (this.intentos >= this.maxIntentos) {
|
if (this.intentos >= this.maxIntentos) {
|
||||||
|
|||||||
@@ -56,59 +56,56 @@ func PagoExitosoPage(c *fiber.Ctx) error {
|
|||||||
_ = models.SaveBoldCallbackLog(entry)
|
_ = models.SaveBoldCallbackLog(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Render("pago_exitoso", fiber.Map{"Ref": ref}, "layouts/landing")
|
// Verificar inmediatamente el estado del pago en el servidor
|
||||||
|
estado, fechaPago := verificarPago(ref)
|
||||||
|
|
||||||
|
return c.Render("pago_exitoso", fiber.Map{
|
||||||
|
"Ref": ref,
|
||||||
|
"Estado": estado,
|
||||||
|
"FechaPago": fechaPago,
|
||||||
|
}, "layouts/landing")
|
||||||
}
|
}
|
||||||
|
|
||||||
// PagoEstadoAPI devuelve el estado de pago de un contrato para polling desde el frontend.
|
// verificarPago centraliza la lógica de verificación de pago para una referencia dada.
|
||||||
// GET /api/pago-estado?ref=contrato-{id}
|
// Devuelve (confirmado, fechaPago). Lo usan tanto PagoExitosoPage como PagoEstadoAPI.
|
||||||
//
|
//
|
||||||
// Flujo de verificación (en orden):
|
// Flujo (en orden):
|
||||||
// 1. Lee pago_confirmado de la DB → si ya confirmado, retorna ok.
|
// 1. DB: si pago_confirmado = true → listo.
|
||||||
// 2. Si no confirmado, revisa dlocal_payment_log buscando ref con estado PAID.
|
// 2. dlocal_payment_log: si hay log con estado PAID/AUTHORIZED → marca y confirma.
|
||||||
// 3. Si aún no confirmado, consulta Bold API directamente usando enlace_pago_link_id.
|
// 3. Bold API: consulta directo usando enlace_pago_link_id si existe.
|
||||||
// 4. Si la pasarela confirma el pago, marca el contrato y retorna confirmado.
|
// 4. dLocal API: consulta directo por order_id.
|
||||||
func PagoEstadoAPI(c *fiber.Ctx) error {
|
func verificarPago(ref string) (confirmado bool, fechaPago string) {
|
||||||
ref := c.Query("ref", "")
|
|
||||||
if ref == "" {
|
|
||||||
return c.JSON(fiber.Map{"confirmado": false, "error": "ref requerido"})
|
|
||||||
}
|
|
||||||
|
|
||||||
var contratoID uint
|
var contratoID uint
|
||||||
if _, err := fmt.Sscanf(ref, "contrato-%d", &contratoID); err != nil || contratoID == 0 {
|
if _, err := fmt.Sscanf(ref, "contrato-%d", &contratoID); err != nil || contratoID == 0 {
|
||||||
return c.JSON(fiber.Map{"confirmado": false, "error": "ref inválido"})
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 1. Consultar DB ──────────────────────────────────────────────────────
|
// ─── 1. DB ────────────────────────────────────────────────────────────────
|
||||||
contrato, err := models.GetContratoParaVerificacion(contratoID)
|
contrato, err := models.GetContratoParaVerificacion(contratoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(fiber.Map{"confirmado": false, "error": "contrato no encontrado"})
|
return false, ""
|
||||||
}
|
}
|
||||||
if contrato.PagoConfirmado {
|
if contrato.PagoConfirmado {
|
||||||
resp := fiber.Map{"confirmado": true, "fuente": "db"}
|
fp := ""
|
||||||
if contrato.FechaPago != nil {
|
if contrato.FechaPago != nil {
|
||||||
resp["fecha_pago"] = contrato.FechaPago.Format("02/01/2006 15:04")
|
fp = contrato.FechaPago.Format("02/01/2006 15:04")
|
||||||
}
|
}
|
||||||
return c.JSON(resp)
|
return true, fp
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 2. Revisar log de dLocal (puede haber llegado antes que el webhook) ──
|
// ─── 2. dlocal_payment_log ────────────────────────────────────────────────
|
||||||
dlocalLogs, err := models.GetDlocalPaymentLogsByRef(ref)
|
dlocalLogs, err := models.GetDlocalPaymentLogsByRef(ref)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, l := range dlocalLogs {
|
for _, l := range dlocalLogs {
|
||||||
if l.Estado == "PAID" || l.Estado == "AUTHORIZED" {
|
if l.Estado == "PAID" || l.Estado == "AUTHORIZED" {
|
||||||
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dlocal_payment_log (id=%d)", contratoID, l.ID)
|
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dlocal_payment_log (id=%d)", contratoID, l.ID)
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
_ = models.MarcarContratoPagado(contratoID)
|
||||||
now := l.CreatedAt
|
return true, l.CreatedAt.Format("02/01/2006 15:04")
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"confirmado": true,
|
|
||||||
"fuente": "dlocal_log",
|
|
||||||
"fecha_pago": now.Format("02/01/2006 15:04"),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 3. Consultar Bold API directamente si hay link_id almacenado ─────────
|
// ─── 3. Bold API ─────────────────────────────────────────────────────────
|
||||||
if contrato.EnlacePagoLinkID != "" {
|
if contrato.EnlacePagoLinkID != "" {
|
||||||
boldCfg, boldErr := models.GetBoldConfig()
|
boldCfg, boldErr := models.GetBoldConfig()
|
||||||
if boldErr == nil {
|
if boldErr == nil {
|
||||||
@@ -118,7 +115,6 @@ func PagoEstadoAPI(c *fiber.Ctx) error {
|
|||||||
} else if paid {
|
} else if paid {
|
||||||
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via Bold API (payment_id=%s)", contratoID, paymentID)
|
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via Bold API (payment_id=%s)", contratoID, paymentID)
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
_ = models.MarcarContratoPagado(contratoID)
|
||||||
// Guardar en bold_webhook_log como registro de esta verificación activa
|
|
||||||
if paymentID != "" && !models.IsBoldNotificationDuplicate("api-check-"+paymentID) {
|
if paymentID != "" && !models.IsBoldNotificationDuplicate("api-check-"+paymentID) {
|
||||||
_ = models.SaveBoldWebhookLog(models.BoldWebhookLog{
|
_ = models.SaveBoldWebhookLog(models.BoldWebhookLog{
|
||||||
NotificationID: "api-check-" + paymentID,
|
NotificationID: "api-check-" + paymentID,
|
||||||
@@ -128,15 +124,12 @@ func PagoEstadoAPI(c *fiber.Ctx) error {
|
|||||||
Procesado: true,
|
Procesado: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return c.JSON(fiber.Map{
|
return true, ""
|
||||||
"confirmado": true,
|
|
||||||
"fuente": "bold_api",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 4. Consultar dLocal API directamente por order_id ────────────────────
|
// ─── 4. dLocal API ───────────────────────────────────────────────────────
|
||||||
dlocalCfg, dlocalErr := models.GetLastActiveDlocalApi()
|
dlocalCfg, dlocalErr := models.GetLastActiveDlocalApi()
|
||||||
if dlocalErr == nil {
|
if dlocalErr == nil {
|
||||||
paid, paymentID, dlocalApiErr := services.CheckDlocalPaymentByOrderID(*dlocalCfg, ref)
|
paid, paymentID, dlocalApiErr := services.CheckDlocalPaymentByOrderID(*dlocalCfg, ref)
|
||||||
@@ -145,7 +138,6 @@ func PagoEstadoAPI(c *fiber.Ctx) error {
|
|||||||
} else if paid {
|
} else if paid {
|
||||||
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dLocal API (payment_id=%s)", contratoID, paymentID)
|
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dLocal API (payment_id=%s)", contratoID, paymentID)
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
_ = models.MarcarContratoPagado(contratoID)
|
||||||
// Guardar en dlocal_payment_log como registro de esta verificación activa
|
|
||||||
notifID := "api-check-" + ref
|
notifID := "api-check-" + ref
|
||||||
if !models.IsDlocalNotificationDuplicate(notifID) {
|
if !models.IsDlocalNotificationDuplicate(notifID) {
|
||||||
_ = models.SaveDlocalPaymentLog(models.DlocalPaymentLog{
|
_ = models.SaveDlocalPaymentLog(models.DlocalPaymentLog{
|
||||||
@@ -158,13 +150,23 @@ func PagoEstadoAPI(c *fiber.Ctx) error {
|
|||||||
Procesado: true,
|
Procesado: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return c.JSON(fiber.Map{
|
return true, ""
|
||||||
"confirmado": true,
|
|
||||||
"fuente": "dlocal_api",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Sin confirmación aún ─────────────────────────────────────────────────
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// PagoEstadoAPI devuelve el estado de pago de un contrato para polling desde el frontend.
|
||||||
|
// GET /api/pago-estado?ref=contrato-{id}
|
||||||
|
func PagoEstadoAPI(c *fiber.Ctx) error {
|
||||||
|
ref := c.Query("ref", "")
|
||||||
|
if ref == "" {
|
||||||
|
return c.JSON(fiber.Map{"confirmado": false, "error": "ref requerido"})
|
||||||
|
}
|
||||||
|
confirmado, fechaPago := verificarPago(ref)
|
||||||
|
if confirmado {
|
||||||
|
return c.JSON(fiber.Map{"confirmado": true, "fecha_pago": fechaPago})
|
||||||
|
}
|
||||||
return c.JSON(fiber.Map{"confirmado": false})
|
return c.JSON(fiber.Map{"confirmado": false})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user