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 @@
+
+
+
+
@@ -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)