Files
soft_usite/rest/controllers/api/pago_controller.go
T
2026-05-12 22:07:31 -05:00

242 lines
8.5 KiB
Go

package controllers
import (
"fmt"
"log"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
// PagoExitosoPage renderiza la página pública de confirmación de pago.
// Bold redirige al cliente aquí tras el pago. Registra el intento en bold_callback_log.
func PagoExitosoPage(c *fiber.Ctx) error {
// Recolectar todos los parámetros posibles que Bold puede enviar
params := map[string]string{}
// Parámetros clave que Bold envía en la callback_url
for _, k := range []string{"bold-order-id", "order_id", "payment_link", "reference", "id", "ref"} {
if v := c.Query(k, ""); v != "" {
params[k] = v
}
}
// bold-tx-status: Bold envía el resultado directamente en la URL
if v := c.Query("bold-tx-status", ""); v != "" {
params["bold-tx-status"] = 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 += "}"
// Si Bold ya envía el resultado en la URL (bold-tx-status), usarlo directamente
// antes de hacer cualquier llamada a la API externa.
boldTxStatus := params["bold-tx-status"]
if boldTxStatus == "rejected" || boldTxStatus == "cancelled" || boldTxStatus == "failed" {
_ = models.SaveBoldCallbackLog(models.BoldCallbackLog{
Referencia: ref, PaymentLink: paymentLink, Params: paramsJSON,
Estado: "fallido", IP: c.IP(), UserAgent: string(c.Request().Header.UserAgent()),
})
return c.Render("pago_exitoso", fiber.Map{
"Ref": ref, "Estado": "rechazado", "FechaPago": "",
}, "layouts/landing")
}
// Bold confirmó el pago directamente en la URL de retorno
if boldTxStatus == "approved" {
// IP real (detrás de nginx)
ip := c.Get("X-Real-IP")
if ip == "" {
ip = c.Get("X-Forwarded-For")
}
if ip == "" {
ip = c.IP()
}
_ = models.SaveBoldCallbackLog(models.BoldCallbackLog{
Referencia: ref, PaymentLink: paymentLink, Params: paramsJSON,
Estado: "pagado", IP: ip, UserAgent: string(c.Request().Header.UserAgent()),
})
// Marcar contrato si aún no está confirmado
var contratoID uint
if _, err := fmt.Sscanf(ref, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
_ = models.MarcarContratoPagado(contratoID)
go services.EnviarCorreoConfirmacionPago(contratoID, "bold")
// Intentar enriquecer con datos del webhook log si ya llegó
go func() {
if wLogs, err := models.GetBoldWebhookLogsByRef(ref); err == nil {
for _, wl := range wLogs {
if wl.PayerEmail != "" || wl.Monto > 0 {
models.EnrichBoldCallbackLog(ref, wl.PayerEmail, wl.Monto)
break
}
}
}
}()
}
estado, fechaPago := verificarPago(ref)
return c.Render("pago_exitoso", fiber.Map{
"Ref": ref, "Estado": estado, "FechaPago": fechaPago,
}, "layouts/landing")
}
// Registrar el intento de pago en la tabla de callbacks
if ref != "" || paymentLink != "" {
ip := c.Get("X-Real-IP")
if ip == "" {
ip = c.Get("X-Forwarded-For")
}
if ip == "" {
ip = c.IP()
}
entry := models.BoldCallbackLog{
Referencia: ref,
PaymentLink: paymentLink,
Params: paramsJSON,
Estado: "pendiente",
IP: ip,
UserAgent: string(c.Request().Header.UserAgent()),
}
_ = models.SaveBoldCallbackLog(entry)
}
// 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")
}
// verificarPago centraliza la lógica de verificación de pago para una referencia dada.
// Devuelve (confirmado, fechaPago). Lo usan tanto PagoExitosoPage como PagoEstadoAPI.
//
// Flujo (en orden):
// 1. DB: si pago_confirmado = true → listo.
// 2. dlocal_payment_log: si hay log con estado PAID/AUTHORIZED → marca y confirma.
// 3. Bold API: consulta directo usando enlace_pago_link_id si existe.
// 4. dLocal API: consulta directo por order_id.
func verificarPago(ref string) (confirmado bool, fechaPago string) {
var contratoID uint
if _, err := fmt.Sscanf(ref, "contrato-%d", &contratoID); err != nil || contratoID == 0 {
return false, ""
}
// ─── 1. DB ────────────────────────────────────────────────────────────────
contrato, err := models.GetContratoParaVerificacion(contratoID)
if err != nil {
return false, ""
}
if contrato.PagoConfirmado {
fp := ""
if contrato.FechaPago != nil {
fp = contrato.FechaPago.Format("02/01/2006 15:04")
}
return true, fp
}
// ─── 2. dlocal_payment_log ────────────────────────────────────────────────
dlocalLogs, err := models.GetDlocalPaymentLogsByRef(ref)
if err == nil {
for _, l := range dlocalLogs {
if l.Estado == "PAID" || l.Estado == "AUTHORIZED" {
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dlocal_payment_log (id=%d)", contratoID, l.ID)
_ = models.MarcarContratoPagado(contratoID)
go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal")
return true, l.CreatedAt.Format("02/01/2006 15:04")
}
}
}
// ─── 3. Bold API ─────────────────────────────────────────────────────────
if contrato.EnlacePagoLinkID != "" {
boldCfg, boldErr := models.GetBoldConfig()
if boldErr == nil {
paid, paymentID, monto, boldApiErr := services.CheckBoldLinkPaid(boldCfg, contrato.EnlacePagoLinkID)
if boldApiErr != nil {
log.Printf("[PAGO-ESTADO] Bold API error para link %s: %v", contrato.EnlacePagoLinkID, boldApiErr)
} else if paid {
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via Bold API (payment_id=%s)", contratoID, paymentID)
_ = models.MarcarContratoPagado(contratoID)
go services.EnviarCorreoConfirmacionPago(contratoID, "bold")
if paymentID != "" && !models.IsBoldNotificationDuplicate("api-check-"+paymentID) {
_ = models.SaveBoldWebhookLog(models.BoldWebhookLog{
NotificationID: "api-check-" + paymentID,
Tipo: "API_CHECK",
PaymentID: paymentID,
Referencia: ref,
PayerEmail: contrato.Cliente.Email,
Monto: monto,
Procesado: true,
})
}
return true, ""
}
}
}
// ─── 4. dLocal API ───────────────────────────────────────────────────────
dlocalCfg, dlocalErr := models.GetLastActiveDlocalApi()
if dlocalErr == nil {
paid, paymentID, monto, payerEmail, dlocalApiErr := services.CheckDlocalPaymentByOrderID(*dlocalCfg, ref)
if dlocalApiErr != nil {
log.Printf("[PAGO-ESTADO] dLocal API error para order_id %s: %v", ref, dlocalApiErr)
} else if paid {
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dLocal API (payment_id=%s)", contratoID, paymentID)
_ = models.MarcarContratoPagado(contratoID)
go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal")
notifID := "api-check-" + ref
if !models.IsDlocalNotificationDuplicate(notifID) {
_ = models.SaveDlocalPaymentLog(models.DlocalPaymentLog{
NotificationID: notifID,
Fuente: "api_check",
Tipo: "API_CHECK",
Estado: "PAID",
PaymentID: paymentID,
Referencia: ref,
PayerEmail: payerEmail,
Monto: monto,
Procesado: true,
})
}
return true, ""
}
}
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})
}