419 lines
14 KiB
Go
419 lines
14 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
|
)
|
|
|
|
// Función para manejar la solicitud de creación de un plan
|
|
func CreatePlan(c *fiber.Ctx) error {
|
|
var plan models.PlanRequest
|
|
|
|
// Parsear el cuerpo de la solicitud a la estructura PlanRequest
|
|
if err := c.BodyParser(&plan); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al parsear el cuerpo de la solicitud: %v", err),
|
|
})
|
|
}
|
|
|
|
// Obtener la configuración activa de DlocalApi
|
|
dlocalConfig, err := models.GetLastActiveDlocalApi()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "Error al obtener la configuración activa de DlocalApi.",
|
|
})
|
|
}
|
|
|
|
// Llamar al servicio para crear el plan
|
|
response, err := services.CreatePlan(*dlocalConfig, plan)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al crear el plan: %v", err),
|
|
})
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"message": "Plan creado exitosamente.",
|
|
"response": string(response),
|
|
})
|
|
}
|
|
|
|
func SeePlan(c *fiber.Ctx) error {
|
|
planID := c.Params("planID")
|
|
|
|
// Verifica si se recibió correctamente
|
|
if planID == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"error": "PlanID es requerido.",
|
|
})
|
|
}
|
|
|
|
// Obtener la configuración activa de DlocalApi
|
|
dlocalConfig, err := models.GetLastActiveDlocalApi()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "Error al obtener la configuración activa de DlocalApi.",
|
|
})
|
|
}
|
|
|
|
// Llamar al servicio para ver el plan
|
|
response, err := services.SeePlan(*dlocalConfig, planID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al obtener el plan: %v", err),
|
|
})
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"message": "Plan obtenido exitosamente.",
|
|
"response": string(response),
|
|
})
|
|
}
|
|
|
|
func UpdatedPlan(c *fiber.Ctx) error {
|
|
planID := c.Params("planID")
|
|
var plan services.PlanUpdated
|
|
// Parsear el cuerpo de la solicitud a la estructura PlanRequest
|
|
if err := c.BodyParser(&plan); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al parsear el cuerpo de la solicitud: %v", err),
|
|
})
|
|
}
|
|
|
|
// Obtener la configuración activa de DlocalApi
|
|
dlocalConfig, err := models.GetLastActiveDlocalApi()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "Error al obtener la configuración activa de DlocalApi.",
|
|
})
|
|
}
|
|
|
|
// Llamar al servicio para crear el plan
|
|
response, err := services.UpdatedPlan(*dlocalConfig, plan, planID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al crear el plan: %v", err),
|
|
})
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"message": "Plan actualizado exitosamente.",
|
|
"response": string(response),
|
|
})
|
|
}
|
|
|
|
func DeactivatePlan(c *fiber.Ctx) error {
|
|
planID := c.Params("planID")
|
|
subscriptionId := c.Params("subscriptionId")
|
|
|
|
// Obtener la configuración activa de DlocalApi
|
|
dlocalConfig, err := models.GetLastActiveDlocalApi()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "Error al obtener la configuración activa de DlocalApi.",
|
|
})
|
|
}
|
|
|
|
// Llamar al servicio para crear el plan
|
|
response, err := services.DeactivatePlan(*dlocalConfig, planID, subscriptionId)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al crear el plan: %v", err),
|
|
})
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"message": "Suscripción cancelada exitosamente.",
|
|
"response": string(response),
|
|
})
|
|
}
|
|
|
|
func SeePlanes(c *fiber.Ctx) error {
|
|
|
|
// Obtener la configuración activa de DlocalApi
|
|
dlocalConfig, err := models.GetLastActiveDlocalApi()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "Error al obtener la configuración activa de DlocalApi.",
|
|
})
|
|
}
|
|
|
|
// Llamar al servicio para ver el plan
|
|
response, err := services.SeePlanes(*dlocalConfig)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al obtener los planes: %v", err),
|
|
})
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"message": "Planes obtenido exitosamente.",
|
|
"response": string(response),
|
|
})
|
|
}
|
|
func SeeSubscription(c *fiber.Ctx) error {
|
|
subscriptionId := c.Params("subscriptionId")
|
|
invoiceId := c.Params("invoiceId")
|
|
|
|
// Obtener la configuración activa de DlocalApi
|
|
dlocalConfig, err := models.GetLastActiveDlocalApi()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "Error al obtener la configuración activa de DlocalApi.",
|
|
})
|
|
}
|
|
|
|
// Llamar al servicio para ver suscripción
|
|
response, err := services.SeeSubscription(*dlocalConfig, subscriptionId, invoiceId)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al obtener la suscripción: %v", err),
|
|
})
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"message": "Suscripción obtenida exitosamente.",
|
|
"response": string(response),
|
|
})
|
|
}
|
|
|
|
func CreatePago(c *fiber.Ctx) error {
|
|
var pago services.PagoRequest
|
|
|
|
// Parsear el cuerpo de la solicitud a la estructura PagoRequest
|
|
if err := c.BodyParser(&pago); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al parsear el cuerpo de la solicitud: %v", err),
|
|
})
|
|
}
|
|
|
|
// Obtener la configuración activa de DlocalApi
|
|
dlocalConfig, err := models.GetLastActiveDlocalApi()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": "Error al obtener la configuración activa de DlocalApi.",
|
|
})
|
|
}
|
|
|
|
// Llamar al servicio para crear el pago
|
|
response, err := services.CreatePago(*dlocalConfig, pago)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": fmt.Sprintf("Error al crear el pago: %v", err),
|
|
})
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"message": "Pago creado exitosamente.",
|
|
"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 ok, 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 (nuevo=%v)", contratoID, ok)
|
|
if ok {
|
|
go services.DispatchSaasPaymentNotification(contratoID, payerEmail, "dlocal", monto, moneda)
|
|
go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 _, markErr := models.MarcarContratoPagado(contratoID); markErr != nil {
|
|
log.Printf("[DLOCAL] Registro manual: error marcando contrato %d: %v", contratoID, markErr)
|
|
} else {
|
|
log.Printf("[DLOCAL] Registro manual: contrato %d marcado como pagado", contratoID)
|
|
}
|
|
}
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"message": "Pago registrado", "notification_id": notifID})
|
|
}
|