up
This commit is contained in:
@@ -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})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user