feat: integracion Bold y pagina unificada de pasarelas de pago
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
// ─── Webhook Bold ─────────────────────────────────────────────────────────────
|
||||
|
||||
// BoldWebhook recibe las notificaciones de Bold y las procesa de forma idempotente.
|
||||
// La respuesta HTTP 200 se envía ANTES de cualquier lógica de negocio (requisito Bold).
|
||||
func BoldWebhook(c *fiber.Ctx) error {
|
||||
rawBody := c.Body()
|
||||
|
||||
// ─── 1. Responder 200 de inmediato ────────────────────────────────────────
|
||||
// Bold reintenta si no recibe 200 en < 2 segundos.
|
||||
// Fiber no permite flush parcial, así que respondemos aquí y procesamos después.
|
||||
c.Set("Content-Type", "application/json")
|
||||
|
||||
// Obtener configuración activa
|
||||
cfg, err := models.GetBoldConfig()
|
||||
if err != nil {
|
||||
log.Println("[BOLD] Webhook: sin configuración activa")
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── 2. Verificar firma HMAC ─────────────────────────────────────────────
|
||||
signature := c.Get("x-bold-signature")
|
||||
if signature != "" {
|
||||
if !services.VerifyBoldSignature(rawBody, signature, cfg) {
|
||||
log.Println("[BOLD] Webhook: firma inválida — descartado")
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 3. Parsear evento ───────────────────────────────────────────────────
|
||||
event, err := services.ParseBoldWebhookEvent(rawBody)
|
||||
if err != nil {
|
||||
log.Printf("[BOLD] Webhook: error parseando body: %v", err)
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
notificationID := event.ID
|
||||
tipo := event.Type
|
||||
|
||||
// ID vacío no se puede procesar con idempotencia
|
||||
if notificationID == "" {
|
||||
log.Println("[BOLD] Webhook: notificationID vacío")
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── 4. Idempotencia: descartar duplicados ───────────────────────────────
|
||||
if models.IsBoldNotificationDuplicate(notificationID) {
|
||||
log.Printf("[BOLD] Webhook: notificación duplicada %s", notificationID)
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
paymentID := event.Data.PaymentID
|
||||
if paymentID == "" {
|
||||
paymentID = event.Subject
|
||||
}
|
||||
referencia := event.Data.Metadata.Reference
|
||||
payerEmail := event.Data.PayerEmail
|
||||
monto := event.Data.Amount.Total
|
||||
|
||||
// ─── 5. Guardar log del webhook ──────────────────────────────────────────
|
||||
logEntry := models.BoldWebhookLog{
|
||||
NotificationID: notificationID,
|
||||
Tipo: tipo,
|
||||
PaymentID: paymentID,
|
||||
Referencia: referencia,
|
||||
PayerEmail: payerEmail,
|
||||
Monto: monto,
|
||||
Procesado: false,
|
||||
Raw: string(rawBody),
|
||||
}
|
||||
if err := models.SaveBoldWebhookLog(logEntry); err != nil {
|
||||
log.Printf("[BOLD] Webhook: error guardando log: %v", err)
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── 6. Solo procesar SALE_APPROVED ─────────────────────────────────────
|
||||
if tipo != "SALE_APPROVED" {
|
||||
log.Printf("[BOLD] Webhook: tipo '%s' ignorado", tipo)
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
log.Printf("[BOLD] Webhook: SALE_APPROVED — payment_id=%s referencia=%s email=%s monto=%d",
|
||||
paymentID, referencia, payerEmail, monto)
|
||||
|
||||
// ─── 7. Aquí se conecta la lógica de negocio ─────────────────────────────
|
||||
// Ejemplo: actualizar estado de pago de un contrato de renovación.
|
||||
// procesarPagoBold(paymentID, referencia, payerEmail, monto)
|
||||
|
||||
models.MarkBoldWebhookProcessed(notificationID)
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── API: crear payment link ──────────────────────────────────────────────────
|
||||
|
||||
// BoldCreatePaymentLink crea un link de pago en Bold y devuelve la URL.
|
||||
func BoldCreatePaymentLink(c *fiber.Ctx) error {
|
||||
type bodyReq struct {
|
||||
TotalAmount int64 `json:"total_amount"`
|
||||
Description string `json:"description"`
|
||||
PayerEmail string `json:"payer_email"`
|
||||
Reference string `json:"reference"`
|
||||
CallbackURL string `json:"callback_url"`
|
||||
}
|
||||
var req bodyReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
if req.TotalAmount <= 0 || req.Description == "" || req.Reference == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "total_amount, description y reference son requeridos",
|
||||
})
|
||||
}
|
||||
|
||||
cfg, err := models.GetBoldConfig()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||
"error": "No hay configuración activa de Bold",
|
||||
})
|
||||
}
|
||||
|
||||
linkReq := services.BoldPaymentLinkRequest{
|
||||
AmountType: "CLOSE",
|
||||
Amount: services.BoldAmountField{
|
||||
Currency: "COP",
|
||||
TotalAmount: req.TotalAmount,
|
||||
},
|
||||
Description: req.Description,
|
||||
PayerEmail: req.PayerEmail,
|
||||
Reference: req.Reference,
|
||||
CallbackURL: req.CallbackURL,
|
||||
}
|
||||
|
||||
result, err := services.CreateBoldPaymentLink(cfg, linkReq)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"payment_link": result.Payload.PaymentLink,
|
||||
"url": result.Payload.URL,
|
||||
})
|
||||
}
|
||||
|
||||
// BoldGetLinkStatus consulta el estado de un payment link existente.
|
||||
func BoldGetLinkStatus(c *fiber.Ctx) error {
|
||||
linkID := c.Params("linkID")
|
||||
if linkID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "linkID requerido"})
|
||||
}
|
||||
|
||||
cfg, err := models.GetBoldConfig()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||
"error": "No hay configuración activa de Bold",
|
||||
})
|
||||
}
|
||||
|
||||
data, err := services.GetBoldPaymentLinkStatus(cfg, linkID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"data": string(data)})
|
||||
}
|
||||
Reference in New Issue
Block a user