up
This commit is contained in:
@@ -52,6 +52,7 @@ func main() {
|
||||
&models.CloudflareConfig{},
|
||||
&models.BoldConfig{},
|
||||
&models.BoldWebhookLog{},
|
||||
&models.DlocalPaymentLog{},
|
||||
)
|
||||
// Seed automático (idempotente) de módulos del sistema
|
||||
migrations.SeedRenovaciones()
|
||||
|
||||
@@ -58,6 +58,7 @@ func Migrate() {
|
||||
// Pasarelas de pago
|
||||
&models.BoldConfig{},
|
||||
&models.BoldWebhookLog{},
|
||||
&models.DlocalPaymentLog{},
|
||||
); err != nil {
|
||||
log.Fatalf("Error during main migration: %v", err)
|
||||
}
|
||||
|
||||
@@ -179,3 +179,15 @@ func GetEstadoPago(contratoID uint) (bool, *time.Time, error) {
|
||||
}
|
||||
return c.PagoConfirmado, c.FechaPago, nil
|
||||
}
|
||||
|
||||
// GetContratoParaVerificacion devuelve los campos necesarios para verificar el pago directamente
|
||||
// con la pasarela (pago_confirmado, enlace_pago_link_id).
|
||||
func GetContratoParaVerificacion(contratoID uint) (*Contrato, error) {
|
||||
var c Contrato
|
||||
if err := app.Http.Database.DB.
|
||||
Select("id", "pago_confirmado", "fecha_pago", "enlace_pago_link_id").
|
||||
First(&c, contratoID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
@@ -96,3 +96,56 @@ func GetLastActiveDlocalApi() (*DlocalApi, error) {
|
||||
}
|
||||
return &dlocalConfig, nil
|
||||
}
|
||||
|
||||
// ─── DlocalPaymentLog ─────────────────────────────────────────────────────────
|
||||
|
||||
// DlocalPaymentLog registra cada pago/notificación de dLocal.
|
||||
// Cubre webhooks automáticos y registros manuales del backoffice.
|
||||
type DlocalPaymentLog struct {
|
||||
gorm.Model
|
||||
// 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"`
|
||||
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, …
|
||||
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)"`
|
||||
OrderID string `json:"order_id" gorm:"column:order_id;type:varchar(120)"`
|
||||
Referencia string `json:"referencia" gorm:"column:referencia;type:varchar(120)"`
|
||||
PayerEmail string `json:"payer_email" gorm:"column:payer_email;type:varchar(255)"`
|
||||
Monto float64 `json:"monto" gorm:"column:monto"`
|
||||
Moneda string `json:"moneda" gorm:"column:moneda;type:varchar(10)"`
|
||||
Procesado bool `json:"procesado" gorm:"column:procesado;default:false"`
|
||||
Nota string `json:"nota" gorm:"column:nota;type:text"`
|
||||
Raw string `json:"raw" gorm:"column:raw;type:text"`
|
||||
}
|
||||
|
||||
func (DlocalPaymentLog) TableName() string { return "dlocal_payment_log" }
|
||||
|
||||
// IsDlocalNotificationDuplicate devuelve true si el notification_id ya existe.
|
||||
func IsDlocalNotificationDuplicate(notificationID string) bool {
|
||||
result := app.Http.Database.DB.
|
||||
Where("notification_id = ?", notificationID).
|
||||
First(&DlocalPaymentLog{})
|
||||
return result.Error == nil
|
||||
}
|
||||
|
||||
// SaveDlocalPaymentLog inserta un nuevo registro de pago.
|
||||
func SaveDlocalPaymentLog(entry DlocalPaymentLog) error {
|
||||
return app.Http.Database.DB.Create(&entry).Error
|
||||
}
|
||||
|
||||
// MarkDlocalPaymentProcessed marca el registro como procesado.
|
||||
func MarkDlocalPaymentProcessed(notificationID string) {
|
||||
app.Http.Database.DB.Model(&DlocalPaymentLog{}).
|
||||
Where("notification_id = ?", notificationID).
|
||||
Update("procesado", true)
|
||||
}
|
||||
|
||||
// GetDlocalPaymentLogs devuelve los últimos N registros ordenados por fecha.
|
||||
func GetDlocalPaymentLogs(limit int) ([]DlocalPaymentLog, error) {
|
||||
var logs []DlocalPaymentLog
|
||||
if err := app.Http.Database.DB.Order("id DESC").Limit(limit).Find(&logs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
@@ -157,3 +157,33 @@ func ParseBoldWebhookEvent(rawBody []byte) (*BoldWebhookEvent, error) {
|
||||
}
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
// ─── Consulta directa de estado de link ──────────────────────────────────────
|
||||
|
||||
// BoldLinkStatus refleja el campo status del payload de la API de Bold.
|
||||
type BoldLinkStatus struct {
|
||||
Payload struct {
|
||||
Status string `json:"status"` // ACTIVE | PAID | EXPIRED | CANCELLED
|
||||
PaymentID string `json:"payment_id"` // presente cuando está pagado
|
||||
Amount struct {
|
||||
Total int64 `json:"total_amount"`
|
||||
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.
|
||||
// Devuelve (pagado bool, paymentID string, error).
|
||||
func CheckBoldLinkPaid(cfg *models.BoldConfig, linkID string) (bool, string, error) {
|
||||
raw, err := GetBoldPaymentLinkStatus(cfg, linkID)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
var result BoldLinkStatus
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
return false, "", fmt.Errorf("bold: parse link status: %w", err)
|
||||
}
|
||||
paid := result.Payload.Status == "PAID" || result.Payload.Status == "APPROVED"
|
||||
return paid, result.Payload.PaymentID, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -531,3 +534,131 @@ func CreatePago(cfg models.DlocalApi, pago PagoRequest) ([]byte, error) {
|
||||
|
||||
return responseBody, nil
|
||||
}
|
||||
|
||||
// ─── Webhook dLocal ───────────────────────────────────────────────────────────
|
||||
|
||||
// DlocalWebhookNotification es la estructura del JSON que dLocal envía en cada notificación.
|
||||
type DlocalWebhookNotification struct {
|
||||
ID string `json:"id"` // notification ID único
|
||||
Type string `json:"type"` // PAYMENT, SUBSCRIPTION_CHARGE, etc.
|
||||
Status string `json:"status"` // PAID, PENDING, REJECTED, CANCELLED, EXPIRED, AUTHORIZED
|
||||
CreatedDate time.Time `json:"created_date"`
|
||||
Payment struct {
|
||||
ID string `json:"id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
OrderID string `json:"order_id"`
|
||||
Payer struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
} `json:"payer"`
|
||||
} `json:"payment"`
|
||||
// Para notificaciones de suscripción
|
||||
Subscription struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"subscription"`
|
||||
Invoice struct {
|
||||
ID string `json:"id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
} `json:"invoice"`
|
||||
}
|
||||
|
||||
// ParseDlocalWebhookNotification parsea el body crudo en la estructura de notificación.
|
||||
func ParseDlocalWebhookNotification(rawBody []byte) (*DlocalWebhookNotification, error) {
|
||||
var n DlocalWebhookNotification
|
||||
if err := json.Unmarshal(rawBody, &n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// VerifyDlocalSignature verifica la firma HMAC-SHA256 enviada por dLocal.
|
||||
// dLocal calcula: HMAC-SHA256(secretKey, rawBody) → hex.
|
||||
// El header es "X-dLocal-Signature".
|
||||
func VerifyDlocalSignature(rawBody []byte, signature string, cfg models.DlocalApi) bool {
|
||||
secret := cfg.AccessKeySecretdev
|
||||
if cfg.Modo == "prod" {
|
||||
secret = cfg.AccessKeySecret
|
||||
}
|
||||
if secret == "" {
|
||||
return false
|
||||
}
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(rawBody)
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
return hmac.Equal([]byte(expected), []byte(signature))
|
||||
}
|
||||
|
||||
// ─── Consulta directa de pagos dLocal ────────────────────────────────────────
|
||||
|
||||
// dlocalPaymentResp mapea la respuesta de GET /v1/payments?order_id=...
|
||||
type dlocalPaymentResp struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"` // PAID, PENDING, REJECTED, CANCELLED, EXPIRED, AUTHORIZED
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
OrderID string `json:"order_id"`
|
||||
Payer struct {
|
||||
Email string `json:"email"`
|
||||
} `json:"payer"`
|
||||
}
|
||||
|
||||
type dlocalPaymentListResp struct {
|
||||
Data []dlocalPaymentResp `json:"data"`
|
||||
}
|
||||
|
||||
// CheckDlocalPaymentByOrderID busca en la API de dLocal un pago con el order_id dado
|
||||
// y devuelve (pagado bool, paymentID string, error).
|
||||
func CheckDlocalPaymentByOrderID(cfg models.DlocalApi, orderID string) (bool, string, error) {
|
||||
baseURL := cfg.UrlDev
|
||||
accessKeyID := cfg.AccessKeyIDdev
|
||||
accessKeySecret := cfg.AccessKeySecretdev
|
||||
if cfg.Modo == "prod" {
|
||||
baseURL = cfg.UrlProd
|
||||
accessKeyID = cfg.AccessKeyID
|
||||
accessKeySecret = cfg.AccessKeySecret
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/payments?order_id=%s", baseURL, orderID)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret)
|
||||
req.Header.Set("Authorization", authToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, "", fmt.Errorf("dlocal status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// La respuesta puede ser lista o un objeto directo
|
||||
var list dlocalPaymentListResp
|
||||
if err := json.Unmarshal(body, &list); err == nil && len(list.Data) > 0 {
|
||||
for _, p := range list.Data {
|
||||
if p.Status == "PAID" || p.Status == "AUTHORIZED" {
|
||||
return true, p.ID, nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
// Si no es lista, intentar objeto directo
|
||||
var single dlocalPaymentResp
|
||||
if err := json.Unmarshal(body, &single); err == nil && single.ID != "" {
|
||||
paid := single.Status == "PAID" || single.Status == "AUTHORIZED"
|
||||
return paid, single.ID, nil
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
@@ -218,3 +221,194 @@ func CreatePago(c *fiber.Ctx) error {
|
||||
"response": string(response),
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Webhook dLocal ───────────────────────────────────────────────────────────
|
||||
|
||||
// DlocalWebhook recibe notificaciones de pago de dLocal y las registra de forma idempotente.
|
||||
// dLocal espera HTTP 200; se responde inmediatamente y se procesa a continuación.
|
||||
func DlocalWebhook(c *fiber.Ctx) error {
|
||||
rawBody := c.Body()
|
||||
|
||||
// ─── 1. Configuración activa ─────────────────────────────────────────────
|
||||
cfg, err := models.GetLastActiveDlocalApi()
|
||||
if err != nil {
|
||||
log.Println("[DLOCAL] Webhook: sin configuración activa")
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── 2. Verificar firma HMAC-SHA256 (opcional según config) ──────────────
|
||||
signature := c.Get("X-dLocal-Signature")
|
||||
if signature != "" {
|
||||
if !services.VerifyDlocalSignature(rawBody, signature, *cfg) {
|
||||
log.Println("[DLOCAL] Webhook: firma inválida — descartado")
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 3. Parsear notificación ─────────────────────────────────────────────
|
||||
notif, err := services.ParseDlocalWebhookNotification(rawBody)
|
||||
if err != nil {
|
||||
log.Printf("[DLOCAL] Webhook: error parseando body: %v", err)
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
notificationID := notif.ID
|
||||
if notificationID == "" {
|
||||
log.Println("[DLOCAL] Webhook: notification_id vacío")
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── 4. Idempotencia ─────────────────────────────────────────────────────
|
||||
if models.IsDlocalNotificationDuplicate(notificationID) {
|
||||
log.Printf("[DLOCAL] Webhook: notificación duplicada %s", notificationID)
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── 5. Extraer campos (pago directo o suscripción) ──────────────────────
|
||||
paymentID := notif.Payment.ID
|
||||
orderID := notif.Payment.OrderID
|
||||
payerEmail := notif.Payment.Payer.Email
|
||||
monto := notif.Payment.Amount
|
||||
moneda := notif.Payment.Currency
|
||||
tipo := notif.Type
|
||||
estado := notif.Status
|
||||
|
||||
// Si es suscripción, tomar amounts del invoice
|
||||
if tipo == "SUBSCRIPTION_CHARGE" || tipo == "SUBSCRIPTION_PAYMENT" {
|
||||
if notif.Invoice.Amount > 0 {
|
||||
monto = notif.Invoice.Amount
|
||||
moneda = notif.Invoice.Currency
|
||||
}
|
||||
if paymentID == "" {
|
||||
paymentID = notif.Invoice.ID
|
||||
}
|
||||
if orderID == "" {
|
||||
orderID = notif.Subscription.ID
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 6. Guardar log ──────────────────────────────────────────────────────
|
||||
logEntry := models.DlocalPaymentLog{
|
||||
NotificationID: notificationID,
|
||||
Fuente: "webhook",
|
||||
Tipo: tipo,
|
||||
Estado: estado,
|
||||
PaymentID: paymentID,
|
||||
OrderID: orderID,
|
||||
Referencia: orderID, // order_id coincide con la referencia del contrato
|
||||
PayerEmail: payerEmail,
|
||||
Monto: monto,
|
||||
Moneda: moneda,
|
||||
Procesado: false,
|
||||
Raw: string(rawBody),
|
||||
}
|
||||
if err := models.SaveDlocalPaymentLog(logEntry); err != nil {
|
||||
log.Printf("[DLOCAL] Webhook: error guardando log: %v", err)
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── 7. Procesar si el estado es PAID ────────────────────────────────────
|
||||
if estado != "PAID" {
|
||||
log.Printf("[DLOCAL] Webhook: estado '%s' — registrado sin procesar", estado)
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
log.Printf("[DLOCAL] Webhook: PAID — payment_id=%s order=%s email=%s monto=%.2f %s",
|
||||
paymentID, orderID, payerEmail, monto, moneda)
|
||||
|
||||
// Intentar vincular con contrato si el order_id tiene formato "contrato-{id}"
|
||||
if orderID != "" {
|
||||
var contratoID uint
|
||||
if _, err := fmt.Sscanf(orderID, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
||||
if err := models.MarcarContratoPagado(contratoID); err != nil {
|
||||
log.Printf("[DLOCAL] Webhook: error marcando contrato %d como pagado: %v", contratoID, err)
|
||||
} else {
|
||||
log.Printf("[DLOCAL] Webhook: contrato %d marcado como pagado", contratoID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
models.MarkDlocalPaymentProcessed(notificationID)
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── Registro manual de pago dLocal ──────────────────────────────────────────
|
||||
|
||||
// DlocalRegistrarPago permite registrar manualmente un pago dLocal desde el backoffice
|
||||
// cuando la notificación no llegó por webhook (actualización directa, confirmación manual, etc.).
|
||||
func DlocalRegistrarPago(c *fiber.Ctx) error {
|
||||
type body struct {
|
||||
PaymentID string `json:"payment_id"`
|
||||
OrderID string `json:"order_id"`
|
||||
Referencia string `json:"referencia"`
|
||||
PayerEmail string `json:"payer_email"`
|
||||
Monto float64 `json:"monto"`
|
||||
Moneda string `json:"moneda"`
|
||||
Estado string `json:"estado"` // PAID, PENDING, REJECTED, …
|
||||
Tipo string `json:"tipo"` // PAYMENT, SUBSCRIPTION_CHARGE, manual, …
|
||||
Nota string `json:"nota"`
|
||||
}
|
||||
var b body
|
||||
if err := c.BodyParser(&b); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
if b.Estado == "" {
|
||||
b.Estado = "PAID"
|
||||
}
|
||||
if b.Tipo == "" {
|
||||
b.Tipo = "manual"
|
||||
}
|
||||
if b.Moneda == "" {
|
||||
b.Moneda = "USD"
|
||||
}
|
||||
|
||||
// Generar un notification_id único para entradas manuales
|
||||
notifID := "manual-" + strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||
if b.PaymentID != "" {
|
||||
notifID = "manual-" + b.PaymentID
|
||||
}
|
||||
|
||||
// No deduplicar entradas manuales con el mismo payment_id — pueden ser actualizaciones
|
||||
// Se usa prefijo diferente por cada llamada si payment_id es el mismo
|
||||
if models.IsDlocalNotificationDuplicate(notifID) {
|
||||
notifID = "manual-" + strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||
}
|
||||
|
||||
referencia := b.Referencia
|
||||
if referencia == "" {
|
||||
referencia = b.OrderID
|
||||
}
|
||||
|
||||
entry := models.DlocalPaymentLog{
|
||||
NotificationID: notifID,
|
||||
Fuente: "manual",
|
||||
Tipo: b.Tipo,
|
||||
Estado: b.Estado,
|
||||
PaymentID: b.PaymentID,
|
||||
OrderID: b.OrderID,
|
||||
Referencia: referencia,
|
||||
PayerEmail: b.PayerEmail,
|
||||
Monto: b.Monto,
|
||||
Moneda: b.Moneda,
|
||||
Nota: b.Nota,
|
||||
Procesado: b.Estado == "PAID",
|
||||
}
|
||||
if err := models.SaveDlocalPaymentLog(entry); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Si el estado es PAID y referencia tiene formato "contrato-{id}", marcar contrato
|
||||
if b.Estado == "PAID" && referencia != "" {
|
||||
var contratoID uint
|
||||
if _, err := fmt.Sscanf(referencia, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
||||
if err := models.MarcarContratoPagado(contratoID); err != nil {
|
||||
log.Printf("[DLOCAL] Registro manual: error marcando contrato %d: %v", contratoID, err)
|
||||
} else {
|
||||
log.Printf("[DLOCAL] Registro manual: contrato %d marcado como pagado", contratoID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "Pago registrado", "notification_id": notifID})
|
||||
}
|
||||
|
||||
@@ -137,3 +137,14 @@ func BoldWebhookLogs(c *fiber.Ctx) error {
|
||||
}
|
||||
return c.JSON(fiber.Map{"data": logs})
|
||||
}
|
||||
|
||||
// ─── Logs dLocal ──────────────────────────────────────────────────────────────
|
||||
|
||||
// DlocalPaymentLogs devuelve los últimos 100 registros de pagos de dLocal.
|
||||
func DlocalPaymentLogs(c *fiber.Ctx) error {
|
||||
logs, err := models.GetDlocalPaymentLogs(100)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"data": logs})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ func RutasPublicas(web fiber.Router) {
|
||||
// ─── Webhooks públicos (sin autenticación) ────────────────────────────
|
||||
// Bold requiere respuesta HTTP 200 rápida, sin middleware de sesión.
|
||||
web.Post("/webhooks/bold", apiControllers.BoldWebhook)
|
||||
// dLocal requiere respuesta HTTP 200 inmediata.
|
||||
web.Post("/webhooks/dlocal", apiControllers.DlocalWebhook)
|
||||
|
||||
// ─── Página de confirmación de pago ───────────────────────────────────
|
||||
web.Get("/pago-exitoso", apiControllers.PagoExitosoPage)
|
||||
|
||||
@@ -138,6 +138,8 @@ func UserRoutes(app fiber.Router) {
|
||||
// dLocal
|
||||
protected.Get("/pasarelas/dlocal/config", controllers.GetDlocalConfigAPI)
|
||||
protected.Post("/pasarelas/dlocal/save", controllers.SaveDlocalConfigWeb)
|
||||
protected.Get("/pasarelas/dlocal/logs", controllers.DlocalPaymentLogs)
|
||||
protected.Post("/pasarelas/dlocal/registro-pago", apiControllers.DlocalRegistrarPago)
|
||||
// Bold API (crear link, consultar estado)
|
||||
protected.Post("/pasarelas/bold/crear-link", apiControllers.BoldCreatePaymentLink)
|
||||
protected.Get("/pasarelas/bold/link/:linkID", apiControllers.BoldGetLinkStatus)
|
||||
|
||||
Reference in New Issue
Block a user