From 36c9e0a6d529c5d000a3cdd4bf0a6c13bca787a5 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 26 May 2026 21:55:48 -0500 Subject: [PATCH] up --- migrations/migrate.go | 1 + pkg/models/paypal_config.go | 62 ++++++ resources/views/pasarelas_pago.html | 261 +++++++++++++++++++++++ rest/controllers/pasarelas_controller.go | 55 +++++ rest/routes/user.go | 3 + 5 files changed, 382 insertions(+) create mode 100644 pkg/models/paypal_config.go diff --git a/migrations/migrate.go b/migrations/migrate.go index 3574acd..2374013 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -60,6 +60,7 @@ func Migrate() { &models.BoldWebhookLog{}, &models.BoldCallbackLog{}, &models.DlocalPaymentLog{}, + &models.PaypalConfig{}, // Documentación por SaaS &models.SaasProducto{}, &models.DocCategoria{}, diff --git a/pkg/models/paypal_config.go b/pkg/models/paypal_config.go new file mode 100644 index 0000000..724f3d1 --- /dev/null +++ b/pkg/models/paypal_config.go @@ -0,0 +1,62 @@ +package models + +import ( + "github.com/sujit-baniya/fiber-boilerplate/app" + "gorm.io/gorm" +) + +// PaypalConfig almacena las credenciales de PayPal. +// Solo un registro activo a la vez. +type PaypalConfig struct { + gorm.Model + // Credenciales Sandbox + ClientIDSandbox string `json:"client_id_sandbox" gorm:"column:client_id_sandbox;type:text"` + ClientSecretSandbox string `json:"client_secret_sandbox" gorm:"column:client_secret_sandbox;type:text"` + // Credenciales Producción + ClientIDProd string `json:"client_id_prod" gorm:"column:client_id_prod;type:text"` + ClientSecretProd string `json:"client_secret_prod" gorm:"column:client_secret_prod;type:text"` + // Modo activo: "sandbox" | "live" + Modo string `json:"modo" gorm:"column:modo;default:'sandbox'"` + // Webhook ID para verificación de firmas + WebhookID string `json:"webhook_id" gorm:"column:webhook_id;type:text"` + // URLs de retorno + ReturnURL string `json:"return_url" gorm:"column:return_url;type:text"` + CancelURL string `json:"cancel_url" gorm:"column:cancel_url;type:text"` + // Nota interna + Nota string `json:"nota" gorm:"column:nota;type:text"` + Activo bool `json:"activo" gorm:"column:activo;default:true"` +} + +func (PaypalConfig) TableName() string { return "paypal_config" } + +// GetPaypalConfig retorna la configuración activa. +func GetPaypalConfig() (*PaypalConfig, error) { + var item PaypalConfig + if err := app.Http.Database.DB.Where("activo = ?", true).Order("id DESC").First(&item).Error; err != nil { + return nil, err + } + return &item, nil +} + +// SavePaypalConfig desactiva la config previa y guarda la nueva (o actualiza si ya tiene ID). +func SavePaypalConfig(s PaypalConfig) error { + app.Http.Database.DB.Model(&PaypalConfig{}). + Where("activo = ?", true). + Update("activo", false) + s.Activo = true + if s.ID > 0 { + return app.Http.Database.DB.Model(&s).Updates(map[string]interface{}{ + "client_id_sandbox": s.ClientIDSandbox, + "client_secret_sandbox": s.ClientSecretSandbox, + "client_id_prod": s.ClientIDProd, + "client_secret_prod": s.ClientSecretProd, + "modo": s.Modo, + "webhook_id": s.WebhookID, + "return_url": s.ReturnURL, + "cancel_url": s.CancelURL, + "nota": s.Nota, + "activo": true, + }).Error + } + return app.Http.Database.DB.Create(&s).Error +} diff --git a/resources/views/pasarelas_pago.html b/resources/views/pasarelas_pago.html index 8323077..fa1c241 100644 --- a/resources/views/pasarelas_pago.html +++ b/resources/views/pasarelas_pago.html @@ -37,6 +37,12 @@ :class="dlocalModo === 'prod' ? 'bg-green-500' : 'bg-yellow-500'"> dLocal — + + + PayPal — + @@ -60,6 +66,15 @@ dLocal + @@ -475,6 +490,196 @@ + + + +
+
+ + +
+

+ + + + Credenciales PayPal +

+

Panel PayPal → Apps & Credentials → Create App

+ + +
+ +
+ + +
+
+ + +
+

+ + + + Sandbox +

+
+
+ + +
+
+ + +
+
+
+ + +
+

+ + + + Producción +

+
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+

Estado actual

+
+
+ Entorno + +
+
+ Client ID (activo) + +
+
+ Webhook ID + +
+
+
+ + +
+

+ + + + URL del Webhook +

+

Registra esta URL en PayPal → Apps → Webhooks

+
+ /webhooks/paypal + +
+
+ + +
+

Eventos del webhook

+
+
+ PAYMENT.CAPTURE.COMPLETED + Pago completado ✅ +
+
+ PAYMENT.CAPTURE.DENIED + Pago denegado ❌ +
+
+ PAYMENT.CAPTURE.REFUNDED + Devolución 🔄 +
+
+ CUSTOMER.DISPUTE.CREATED + Disputa iniciada ⚠️ +
+
+
+ + +
+

Guía rápida

+
    +
  1. Entra a developer.paypal.comApps & Credentials.
  2. +
  3. Crea o selecciona una app y copia el Client ID y Secret.
  4. +
  5. En la misma app activa Webhooks y pega la URL de arriba.
  6. +
  7. Copia el Webhook ID generado y pégalo en este formulario.
  8. +
  9. Cuando estés listo para producción cambia a Live y repite con las credenciales de producción.
  10. +
+
+
+
+
+
@@ -1044,6 +1249,20 @@ function pasarelasApp() { validandoLogID: 0, validandoCallbackID: 0, + // ─── PayPal ────────────────────────────────────────────────────── + paypalModo: 'sandbox', + paypal: { + id: 0, + client_id_sandbox: '', + client_secret_sandbox: '', + client_id_prod: '', + client_secret_prod: '', + webhook_id: '', + return_url: '', + cancel_url: '', + nota: '', + }, + // ─── dLocal ───────────────────────────────────────────────────── dlocalModo: 'dev', dlocal: { @@ -1093,6 +1312,7 @@ function pasarelasApp() { init() { this.loadBold(); this.loadDlocal(); + this.loadPaypal(); // Carga perezosa: se carga cuando el usuario abre esos sub-tabs this.loadBoldLogs(); }, @@ -1374,6 +1594,47 @@ function pasarelasApp() { } }, + // ─── PayPal helpers ────────────────────────────────────────────── + async loadPaypal() { + try { + const r = await axios.get('/app/pasarelas/paypal/config'); + if (r.data.data) { + const d = r.data.data; + this.paypal = { + id: d.ID || 0, + client_id_sandbox: d.client_id_sandbox || '', + client_secret_sandbox: d.client_secret_sandbox || '', + client_id_prod: d.client_id_prod || '', + client_secret_prod: d.client_secret_prod || '', + webhook_id: d.webhook_id || '', + return_url: d.return_url || '', + cancel_url: d.cancel_url || '', + nota: d.nota || '', + }; + this.paypalModo = d.modo || 'sandbox'; + } + } catch (_) {} + }, + + async savePaypal() { + this.loading = true; + try { + const payload = { ...this.paypal, modo: this.paypalModo }; + await axios.post('/app/pasarelas/paypal/save', payload); + this.showToast('Configuración PayPal guardada ✓'); + this.loadPaypal(); + } catch (e) { + this.showToast(e.response?.data?.error || 'Error guardando PayPal', 'error'); + } finally { + this.loading = false; + } + }, + + copyPaypalWebhook() { + const url = window.location.origin + '/webhooks/paypal'; + navigator.clipboard.writeText(url).then(() => this.showToast('URL copiada: ' + url)); + }, + // ─── Toast ─────────────────────────────────────────────────────── showToast(msg, type = 'success') { this.toast = { show: true, msg, type }; diff --git a/rest/controllers/pasarelas_controller.go b/rest/controllers/pasarelas_controller.go index 2623891..59cd53e 100644 --- a/rest/controllers/pasarelas_controller.go +++ b/rest/controllers/pasarelas_controller.go @@ -12,11 +12,13 @@ import ( 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"), } @@ -543,3 +545,56 @@ func ValidarBoldLog(c *fiber.Ctx) error { "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"}) +} + diff --git a/rest/routes/user.go b/rest/routes/user.go index 51b400f..51fcfde 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -228,6 +228,9 @@ func UserRoutes(app fiber.Router) { protected.Get("/pasarelas/dlocal/logs", controllers.DlocalPaymentLogsPaginated) protected.Post("/pasarelas/dlocal/logs/:id/validar", controllers.ValidarDlocalLog) protected.Post("/pasarelas/dlocal/registro-pago", apiControllers.DlocalRegistrarPago) + // PayPal + protected.Get("/pasarelas/paypal/config", controllers.GetPaypalConfigAPI) + protected.Post("/pasarelas/paypal/save", controllers.SavePaypalConfigWeb) // Bold API (crear link, consultar estado) protected.Post("/pasarelas/bold/crear-link", apiControllers.BoldCreatePaymentLink) protected.Get("/pasarelas/bold/link/:linkID", apiControllers.BoldGetLinkStatus)