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 { if ok, _ := models.MarcarContratoPagado(contratoID); ok { go services.EnviarCorreoConfirmacionPago(contratoID, "bold") } // Enriquecer callback log: primero webhook log, luego Bold API directa go func(linkID string) { // 1. Si el webhook ya llegó, usar esos datos 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) return } } } // 2. Fallback: consultar Bold API directamente para obtener el monto if linkID != "" { if boldCfg, err := models.GetBoldConfig(); err == nil { if _, _, monto, err := services.CheckBoldLinkStatus(boldCfg, linkID); err == nil && monto > 0 { models.EnrichBoldCallbackLog(ref, "", monto) } } } }(paymentLink) } 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) if ok, _ := models.MarcarContratoPagado(contratoID); ok { 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) if ok, _ := models.MarcarContratoPagado(contratoID); ok { go services.EnviarCorreoConfirmacionPago(contratoID, "bold") } // Enriquecer callback log con monto y email del cliente go models.EnrichBoldCallbackLog(ref, contrato.Cliente.Email, monto) 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) if ok, _ := models.MarcarContratoPagado(contratoID); ok { 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}) }