package controllers import ( "fmt" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // PasarelasPage renderiza la vista unificada de pasarelas de pago. func PasarelasPage(c *fiber.Ctx) error { boldCfg, _ := models.GetBoldConfig() dlocalCfg, _ := models.GetLastActiveDlocalApi() paypalCfg, _ := models.GetPaypalConfig() data := fiber.Map{ "Title": "Pasarelas de Pago", "Bold": boldCfg, "Dlocal": dlocalCfg, "Paypal": paypalCfg, "user": c.Locals("user"), "modules": c.Locals("userModules"), } if err := c.Render("pasarelas_pago", data, "layouts/main"); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return nil } // ─── Bold ───────────────────────────────────────────────────────────────────── // SaveBoldConfig guarda o actualiza la configuración de Bold. func SaveBoldConfig(c *fiber.Ctx) error { type body struct { ID uint `json:"id" form:"id"` ApiKeyProd string `json:"api_key_prod" form:"api_key_prod"` SecretKeyProd string `json:"secret_key_prod" form:"secret_key_prod"` ApiKeyTest string `json:"api_key_test" form:"api_key_test"` SecretKeyTest string `json:"secret_key_test" form:"secret_key_test"` Modo string `json:"modo" form:"modo"` CallbackUrl string `json:"callback_url" form:"callback_url"` Nota string `json:"nota" form:"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.Modo == "" { b.Modo = "test" } cfg := models.BoldConfig{ ApiKeyProd: b.ApiKeyProd, SecretKeyProd: b.SecretKeyProd, ApiKeyTest: b.ApiKeyTest, SecretKeyTest: b.SecretKeyTest, Modo: b.Modo, CallbackUrl: b.CallbackUrl, Nota: b.Nota, } cfg.ID = b.ID if err := models.SaveBoldConfig(cfg); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"message": "Configuración Bold guardada"}) } // GetBoldConfig devuelve la configuración activa de Bold (para cargar el form). func GetBoldConfigAPI(c *fiber.Ctx) error { cfg, err := models.GetBoldConfig() if err != nil { return c.JSON(fiber.Map{"data": nil}) } return c.JSON(fiber.Map{"data": cfg}) } // ─── dLocal ─────────────────────────────────────────────────────────────────── // SaveDlocalConfig guarda o actualiza la configuración de dLocal. func SaveDlocalConfigWeb(c *fiber.Ctx) error { type body struct { ID uint `json:"id" form:"id"` AccessKeyID string `json:"access_key_id" form:"access_key_id"` AccessKeySecret string `json:"access_key_secret" form:"access_key_secret"` AccessKeyIDdev string `json:"access_key_id_dev" form:"access_key_id_dev"` AccessKeySecretdev string `json:"access_key_secret_dev" form:"access_key_secret_dev"` UrlProd string `json:"url_prod" form:"url_prod"` UrlDev string `json:"url_dev" form:"url_dev"` Modo string `json:"modo" form:"modo"` } var b body if err := c.BodyParser(&b); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } if b.Modo == "" { b.Modo = "dev" } cfg := models.DlocalApi{ AccessKeyID: b.AccessKeyID, AccessKeySecret: b.AccessKeySecret, AccessKeyIDdev: b.AccessKeyIDdev, AccessKeySecretdev: b.AccessKeySecretdev, UrlProd: b.UrlProd, UrlDev: b.UrlDev, Modo: b.Modo, IsActive: true, } cfg.ID = b.ID // Desactivar configs previas if cfg.ID == 0 { if err := models.CreateDlocalApi(&cfg); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } } else { if err := models.UpdateDlocalApi(&cfg); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } } return c.JSON(fiber.Map{"message": "Configuración dLocal guardada"}) } // GetDlocalConfigAPI devuelve la configuración activa de dLocal. func GetDlocalConfigAPI(c *fiber.Ctx) error { cfg, err := models.GetLastActiveDlocalApi() if err != nil { return c.JSON(fiber.Map{"data": nil}) } return c.JSON(fiber.Map{"data": cfg}) } // ─── Logs Bold ──────────────────────────────────────────────────────────────── // BoldWebhookLogs devuelve los logs de notificaciones con paginación y filtro por tipo. // Query params: page (default 1), limit (default 25), tipo (SALE_APPROVED|SALE_REJECTED|...|TODOS) func BoldWebhookLogs(c *fiber.Ctx) error { page := c.QueryInt("page", 1) limit := c.QueryInt("limit", 25) tipo := c.Query("tipo", "") if page < 1 { page = 1 } logs, total, err := models.GetBoldWebhookLogsPaginated(page, limit, tipo) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "data": logs, "total": total, "page": page, "limit": limit, }) } // BoldCallbackLogs devuelve los intentos de pago (visitas al callback) con paginación y filtro. // Query params: page (default 1), limit (default 25), estado (pendiente|pagado|fallido|revertido|TODOS) func BoldCallbackLogs(c *fiber.Ctx) error { page := c.QueryInt("page", 1) limit := c.QueryInt("limit", 25) estado := c.Query("estado", "") if page < 1 { page = 1 } logs, total, err := models.GetBoldCallbackLogsPaginated(page, limit, estado) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "data": logs, "total": total, "page": page, "limit": limit, }) } // ─── Logs dLocal ────────────────────────────────────────────────────────────── // DlocalPaymentLogs devuelve los últimos 100 registros de pagos de dLocal (legacy). 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}) } // DlocalPaymentLogsPaginated devuelve los logs con paginación y filtro por estado. // Query params: page (default 1), limit (default 25), estado (PAID|PENDING|REJECTED|APROBADOS|TODOS) func DlocalPaymentLogsPaginated(c *fiber.Ctx) error { page := c.QueryInt("page", 1) limit := c.QueryInt("limit", 25) estado := c.Query("estado", "") if page < 1 { page = 1 } logs, total, err := models.GetDlocalPaymentLogsPaginated(page, limit, estado) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "data": logs, "total": total, "page": page, "limit": limit, }) } // ValidarDlocalLog verifica el estado de pago real de un log dLocal consultando // todas las fuentes disponibles (dlocal_payment_log → Bold API → dLocal API). // Si se confirma el pago, marca el contrato como pagado y envía confirmación. // POST /app/pasarelas/dlocal/logs/:id/validar func ValidarDlocalLog(c *fiber.Ctx) error { logID, err := c.ParamsInt("id") if err != nil || logID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID inválido"}) } var entry models.DlocalPaymentLog if err := models.GetDlocalPaymentLogByID(uint(logID), &entry); err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Log no encontrado"}) } ref := entry.Referencia if ref == "" { ref = entry.OrderID } var contratoID uint if _, err := fmt.Sscanf(ref, "contrato-%d", &contratoID); err != nil || contratoID == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Referencia no tiene formato contrato-{id}"}) } contrato, err := models.GetContratoParaVerificacion(contratoID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Contrato no encontrado"}) } payerEmail := entry.PayerEmail if payerEmail == "" { payerEmail = contrato.Cliente.Email } monto := entry.Monto pagoConfirmado := contrato.PagoConfirmado fuenteConfirmacion := "" // ─── 1. Ya confirmado en DB ─────────────────────────────────────────────── if pagoConfirmado { fuenteConfirmacion = "db" } // ─── 2. Mismo log si ya tiene estado PAID ──────────────────────────────── if !pagoConfirmado && (entry.Estado == "PAID" || entry.Estado == "AUTHORIZED") { pagoConfirmado = true fuenteConfirmacion = "dlocal_log" } // ─── 3. Bold API ───────────────────────────────────────────────────────── if !pagoConfirmado && contrato.EnlacePagoLinkID != "" { boldCfg, boldErr := models.GetBoldConfig() if boldErr == nil { paid, _, apiMonto, apiErr := services.CheckBoldLinkPaid(boldCfg, contrato.EnlacePagoLinkID) if apiErr == nil && paid { pagoConfirmado = true fuenteConfirmacion = "bold_api" if apiMonto > 0 { monto = float64(apiMonto) } } } } // ─── 4. dLocal API ─────────────────────────────────────────────────────── if !pagoConfirmado { dlocalCfg, dlErr := models.GetLastActiveDlocalApi() if dlErr == nil { paid, _, apiMonto, apiEmail, apiErr := services.CheckDlocalPaymentByOrderID(*dlocalCfg, ref) if apiErr == nil && paid { pagoConfirmado = true fuenteConfirmacion = "dlocal_api" if apiMonto > 0 { monto = apiMonto } if apiEmail != "" { payerEmail = apiEmail } } } } // Si el pago se confirmó ahora (no estaba marcado antes), actualizar contrato if pagoConfirmado && !contrato.PagoConfirmado { if ok, _ := models.MarcarContratoPagado(contratoID); ok { go services.EnviarCorreoConfirmacionPago(contratoID, fuenteConfirmacion) } } _ = models.UpdateDlocalPaymentLogDatos(uint(logID), payerEmail, monto) _ = models.GetDlocalPaymentLogByID(uint(logID), &entry) msg := "Datos actualizados" if pagoConfirmado && !contrato.PagoConfirmado { msg = fmt.Sprintf("Pago confirmado via %s — contrato marcado como pagado", fuenteConfirmacion) } else if pagoConfirmado { msg = fmt.Sprintf("Pago ya confirmado (%s)", fuenteConfirmacion) } else { msg = "Pago aún no confirmado — datos actualizados" } return c.JSON(fiber.Map{ "ok": true, "confirmado": pagoConfirmado, "fuente": fuenteConfirmacion, "mensaje": msg, "data": entry, }) } // ─── Validación de intento de pago (callback) ─────────────────────────────── // ValidarBoldCallback verifica si el pago de un intento pendiente realmente se realizó. // Recorre las mismas fuentes que verificarPago() y, si confirma, marca el contrato + actualiza el callback. // POST /app/pasarelas/bold/callbacks/:id/validar func ValidarBoldCallback(c *fiber.Ctx) error { cbID, err := c.ParamsInt("id") if err != nil || cbID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID inválido"}) } var cb models.BoldCallbackLog if err := models.GetBoldCallbackLogByID(uint(cbID), &cb); err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Intento no encontrado"}) } var contratoID uint if _, err := fmt.Sscanf(cb.Referencia, "contrato-%d", &contratoID); err != nil || contratoID == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Referencia no tiene formato contrato-{id}"}) } contrato, err := models.GetContratoParaVerificacion(contratoID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Contrato no encontrado"}) } pagoConfirmado := contrato.PagoConfirmado fuente := "" // ─── 1. Ya confirmado en DB ─────────────────────────────────────────────── if pagoConfirmado { fuente = "db" } // ─── 2. dlocal_payment_log ─────────────────────────────────────────────── if !pagoConfirmado { if dlLogs, dlErr := models.GetDlocalPaymentLogsByRef(cb.Referencia); dlErr == nil { for _, l := range dlLogs { if l.Estado == "PAID" || l.Estado == "AUTHORIZED" { pagoConfirmado = true fuente = "dlocal_log" break } } } } // ─── 3. Bold API ───────────────────────────────────────────────────────── if !pagoConfirmado && contrato.EnlacePagoLinkID != "" { if boldCfg, boldErr := models.GetBoldConfig(); boldErr == nil { if paid, _, _, apiErr := services.CheckBoldLinkPaid(boldCfg, contrato.EnlacePagoLinkID); apiErr == nil && paid { pagoConfirmado = true fuente = "bold_api" } } } // ─── 4. dLocal API ─────────────────────────────────────────────────────── if !pagoConfirmado { if dlocalCfg, dlErr := models.GetLastActiveDlocalApi(); dlErr == nil { if paid, _, _, _, apiErr := services.CheckDlocalPaymentByOrderID(*dlocalCfg, cb.Referencia); apiErr == nil && paid { pagoConfirmado = true fuente = "dlocal_api" } } } // Actualizar contrato si se confirmó ahora if pagoConfirmado && !contrato.PagoConfirmado { if ok, _ := models.MarcarContratoPagado(contratoID); ok { go services.EnviarCorreoConfirmacionPago(contratoID, fuente) } } // Actualizar estado del callback nuevoEstado := cb.Estado if pagoConfirmado && cb.Estado == "pendiente" { nuevoEstado = "pagado" _ = models.UpdateBoldCallbackEstadoByID(uint(cbID), "pagado") cb.Estado = "pagado" } msg := "Pago aún no confirmado" if pagoConfirmado && !contrato.PagoConfirmado { msg = fmt.Sprintf("Pago confirmado via %s — contrato marcado como pagado", fuente) } else if pagoConfirmado { msg = fmt.Sprintf("Pago ya confirmado (%s)", fuente) } _ = nuevoEstado return c.JSON(fiber.Map{ "ok": true, "confirmado": pagoConfirmado, "fuente": fuente, "mensaje": msg, "data": cb, }) } // ─── Validación de log API_CHECK ───────────────────────────────────────────── // ValidarBoldLog verifica el estado de pago real de un log API_CHECK consultando // todas las fuentes disponibles (dlocal_payment_log → Bold API → dLocal API). // Si se confirma el pago, marca el contrato como pagado y envía el email de confirmación. // También rellena email y monto faltantes. Funciona para cualquier tipo de log Bold. // POST /app/pasarelas/bold/logs/:id/validar func ValidarBoldLog(c *fiber.Ctx) error { logID, err := c.ParamsInt("id") if err != nil || logID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID inválido"}) } // Cargar el registro existente var entry models.BoldWebhookLog if err := models.GetBoldWebhookLogByID(uint(logID), &entry); err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Log no encontrado"}) } // Parsear contrato ID de la referencia (contrato-{id}) var contratoID uint if _, err := fmt.Sscanf(entry.Referencia, "contrato-%d", &contratoID); err != nil || contratoID == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Referencia no tiene formato contrato-{id}"}) } // Cargar contrato con cliente contrato, err := models.GetContratoParaVerificacion(contratoID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Contrato no encontrado"}) } // Valores iniciales: reutilizar lo que ya hay en el log, fallback al cliente payerEmail := entry.PayerEmail if payerEmail == "" { payerEmail = contrato.Cliente.Email } monto := entry.Monto pagoConfirmado := contrato.PagoConfirmado fuenteConfirmacion := "" // ─── 1. Ya confirmado en DB ─────────────────────────────────────────────── if pagoConfirmado { fuenteConfirmacion = "db" } // ─── 2. dlocal_payment_log ──────────────────────────────────────────────── if !pagoConfirmado { dlocalLogs, dlErr := models.GetDlocalPaymentLogsByRef(entry.Referencia) if dlErr == nil { for _, l := range dlocalLogs { if l.Estado == "PAID" || l.Estado == "AUTHORIZED" { pagoConfirmado = true fuenteConfirmacion = "dlocal_log" if l.PayerEmail != "" { payerEmail = l.PayerEmail } if l.Monto > 0 { monto = int64(l.Monto) } break } } } } // ─── 3. Bold API ───────────────────────────────────────────────────────── if !pagoConfirmado && contrato.EnlacePagoLinkID != "" { boldCfg, boldErr := models.GetBoldConfig() if boldErr == nil { paid, _, apiMonto, apiErr := services.CheckBoldLinkPaid(boldCfg, contrato.EnlacePagoLinkID) if apiErr == nil && paid { pagoConfirmado = true fuenteConfirmacion = "bold_api" if apiMonto > 0 { monto = apiMonto } } } } // ─── 4. dLocal API ─────────────────────────────────────────────────────── if !pagoConfirmado { dlocalCfg, dlErr := models.GetLastActiveDlocalApi() if dlErr == nil { paid, _, apiMonto, apiEmail, apiErr := services.CheckDlocalPaymentByOrderID(*dlocalCfg, entry.Referencia) if apiErr == nil && paid { pagoConfirmado = true fuenteConfirmacion = "dlocal_api" if apiMonto > 0 { monto = int64(apiMonto) } if apiEmail != "" { payerEmail = apiEmail } } } } // Si el pago se confirmó ahora (no estaba marcado antes), actualizar contrato if pagoConfirmado && !contrato.PagoConfirmado { if ok, _ := models.MarcarContratoPagado(contratoID); ok { go services.EnviarCorreoConfirmacionPago(contratoID, fuenteConfirmacion) } } // Actualizar log con los mejores datos disponibles _ = models.UpdateBoldWebhookLogDatos(uint(logID), payerEmail, monto) // Devolver el log actualizado _ = models.GetBoldWebhookLogByID(uint(logID), &entry) msg := "Datos actualizados" if pagoConfirmado && !contrato.PagoConfirmado { msg = fmt.Sprintf("Pago confirmado via %s — contrato marcado como pagado", fuenteConfirmacion) } else if pagoConfirmado { msg = fmt.Sprintf("Pago ya confirmado (%s)", fuenteConfirmacion) } else { msg = "Pago aún no confirmado — datos del contrato actualizados" } return c.JSON(fiber.Map{ "ok": true, "confirmado": pagoConfirmado, "fuente": fuenteConfirmacion, "mensaje": msg, "data": entry, }) } // ─── PayPal ─────────────────────────────────────────────────────────────────── // GetPaypalConfigAPI devuelve la configuración activa de PayPal. func GetPaypalConfigAPI(c *fiber.Ctx) error { cfg, err := models.GetPaypalConfig() if err != nil { return c.JSON(fiber.Map{"data": nil}) } return c.JSON(fiber.Map{"data": cfg}) } // SavePaypalConfigWeb guarda o actualiza la configuración de PayPal. func SavePaypalConfigWeb(c *fiber.Ctx) error { type body struct { ID uint `json:"id" form:"id"` ClientIDSandbox string `json:"client_id_sandbox" form:"client_id_sandbox"` ClientSecretSandbox string `json:"client_secret_sandbox" form:"client_secret_sandbox"` ClientIDProd string `json:"client_id_prod" form:"client_id_prod"` ClientSecretProd string `json:"client_secret_prod" form:"client_secret_prod"` Modo string `json:"modo" form:"modo"` WebhookID string `json:"webhook_id" form:"webhook_id"` ReturnURL string `json:"return_url" form:"return_url"` CancelURL string `json:"cancel_url" form:"cancel_url"` Nota string `json:"nota" form:"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.Modo == "" { b.Modo = "sandbox" } cfg := models.PaypalConfig{ ClientIDSandbox: b.ClientIDSandbox, ClientSecretSandbox: b.ClientSecretSandbox, ClientIDProd: b.ClientIDProd, ClientSecretProd: b.ClientSecretProd, Modo: b.Modo, WebhookID: b.WebhookID, ReturnURL: b.ReturnURL, CancelURL: b.CancelURL, Nota: b.Nota, } cfg.ID = b.ID if err := models.SavePaypalConfig(cfg); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"message": "Configuración PayPal guardada"}) }