From 048ebf5d70b7c61e07d85a6a1d533e6bbb2b9bff Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Wed, 6 May 2026 15:44:51 -0500 Subject: [PATCH] up --- main.go | 1 + migrations/migrate.go | 58 ++++++ resources/views/pasarelas_pago.html | 290 ++++++++++++++++++++++++++-- resources/views/saas_api.html | 46 +++-- 4 files changed, 365 insertions(+), 30 deletions(-) diff --git a/main.go b/main.go index 6c08841..d51da8d 100755 --- a/main.go +++ b/main.go @@ -66,6 +66,7 @@ func main() { migrations.SeedRenovaciones() migrations.SeedIntegraciones() migrations.SeedPasarelas() + migrations.SeedSaas() migrations.SeedServidores() migrations.SeedAdministracion() // Iniciar cron de vencimientos diff --git a/migrations/migrate.go b/migrations/migrate.go index 47d95dd..9056703 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -95,6 +95,9 @@ func Migrate() { // Insertar submódulo "Pasarelas de Pago" en el módulo Integraciones SeedPasarelas() + // Insertar submódulos de SaaS (Productos + Integraciones) en el módulo Integraciones + SeedSaas() + log.Println("Migration Completed...") } @@ -476,3 +479,58 @@ func SeedAdministracion() { log.Println("[SEED] Seed de Administración completado.") } + +// SeedSaas agrega los submódulos de gestión de productos SaaS e integraciones +// de pago al módulo "Integraciones" ya existente. Es idempotente. +func SeedSaas() { + db := app.Http.Database.DB + + // Reutilizar el módulo "Integraciones" (creado por SeedIntegraciones) + var modulo models.Modules + if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil { + log.Printf("[SEED] Módulo 'Integraciones' no encontrado para SeedSaas: %v", err) + return + } + + entries := []struct{ title, desc, url string }{ + {"Productos SaaS", "Gestión de productos SaaS y su documentación asociada", "/app/saas"}, + {"Integraciones SaaS", "Callbacks de pago hacia APIs externas y logs de despacho", "/app/saas-api"}, + } + + var insertados []models.Submodules + for _, e := range entries { + var sub models.Submodules + if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { + sub = models.Submodules{ + Title: e.title, + Description: e.desc, + Url: e.url, + ModuleId: modulo.ID, + ModifiedAt: time.Now(), + } + if err := db.Create(&sub).Error; err != nil { + log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) + continue + } + log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) + } else { + log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) + } + insertados = append(insertados, sub) + } + + var roles []models.Roles + if err := db.Find(&roles).Error; err != nil { + log.Printf("[SEED] Error obteniendo roles: %v", err) + return + } + for _, rol := range roles { + if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil { + log.Printf("[SEED] Error asignando submódulos SaaS al rol '%s': %v", rol.Name, err) + } else { + log.Printf("[SEED] Submódulos SaaS asignados al rol '%s'", rol.Name) + } + } + + log.Println("[SEED] Seed de SaaS completado.") +} diff --git a/resources/views/pasarelas_pago.html b/resources/views/pasarelas_pago.html index de22a4b..5bc7b79 100644 --- a/resources/views/pasarelas_pago.html +++ b/resources/views/pasarelas_pago.html @@ -530,6 +530,20 @@
+ +
+ +
+ + +
@@ -635,28 +649,167 @@
-

Endpoints disponibles

-
-
- POST - /v1/dlocal/subscription/crear-plan -
-
- GET - /v1/dlocal/subscription/ver-plan/:id -
-
- PATCH - /v1/dlocal/subscription/actualizar-plan/:id -
-
- POST - /v1/dlocal/payment/crear-pago -
+

+ + + + URL del Webhook +

+

Registra esta URL en el panel dLocal → Integraciones → Webhook

+
+ /webhooks/dlocal +
+
+ + +
+ +
+

Planes almacenados en dLocal directamente. Los cambios se reflejan en tiempo real.

+ +
+ + +
Consultando planes en dLocal…
+
+ + +
+
+ Sin planes creados en dLocal aún. +
+
+ + + + + + + + + + + + + + +
IDNombreMontoFrecuenciaEstado
+
+
+ + + + +
+ @@ -723,6 +876,23 @@ function pasarelasApp() { url_dev: '', }, + // Sub-tabs dLocal + dlocalTab: 'config', + + // Planes dLocal + dlocalPlanes: [], + dlocalPlanesLoading: false, + dlocalPlanesError: '', + showPlanModal: false, + planEditMode: false, + planSaving: false, + planError: '', + planEditID: '', + planForm: {}, + defaultPlanForm() { + return { name:'', description:'', currency:'COP', amount:0, frequency_type:'MONTHLY', frequency_value:1, day_of_month:0, country:'CO', notification_url:'', success_url:'', back_url:'', error_url:'' }; + }, + // ─── Init ─────────────────────────────────────────────────────── init() { this.loadBold(); @@ -849,6 +1019,90 @@ function pasarelasApp() { } }, + async loadDlocalPlanes() { + this.dlocalPlanesLoading = true; + this.dlocalPlanesError = ''; + try { + const r = await axios.get('/app/dlocal/planes'); + // dLocal devuelve { message, response } donde response es el JSON raw + const raw = r.data.response; + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; + // La API de dLocal devuelve { plans: [...] } o directamente un array + this.dlocalPlanes = Array.isArray(parsed) ? parsed : (parsed.plans || parsed.data || []); + } catch (e) { + this.dlocalPlanesError = e.response?.data?.error || 'No se pudo obtener los planes de dLocal.'; + } finally { + this.dlocalPlanesLoading = false; + } + }, + + openPlanAdd() { + this.planEditMode = false; + this.planEditID = ''; + this.planForm = this.defaultPlanForm(); + this.planError = ''; + this.showPlanModal = true; + }, + + openPlanEdit(plan) { + this.planEditMode = true; + this.planEditID = plan.id; + this.planForm = { + name: plan.name || '', + description: plan.description || '', + currency: plan.currency || 'COP', + amount: plan.amount || 0, + frequency_type: plan.frequency_type || 'MONTHLY', + frequency_value: plan.frequency_value || 1, + day_of_month: plan.day_of_month || 0, + country: plan.country || '', + notification_url: plan.notification_url || '', + success_url: plan.success_url || '', + back_url: plan.back_url || '', + error_url: plan.error_url || '', + }; + this.planError = ''; + this.showPlanModal = true; + }, + + async savePlan() { + this.planError = ''; + if (!this.planForm.name) { this.planError = 'El nombre es requerido.'; return; } + if (!this.planForm.currency) { this.planError = 'La moneda es requerida.'; return; } + if (!this.planForm.amount) { this.planError = 'El monto es requerido.'; return; } + this.planSaving = true; + try { + if (this.planEditMode) { + // Solo campos editables en PATCH + const patch = { + name: this.planForm.name, + description: this.planForm.description, + amount: this.planForm.amount, + notification_url: this.planForm.notification_url, + back_url: this.planForm.back_url, + success_url: this.planForm.success_url, + error_url: this.planForm.error_url, + }; + await axios.patch(`/app/dlocal/planes/${this.planEditID}`, patch); + this.showToast('Plan actualizado en dLocal ✓'); + } else { + await axios.post('/app/dlocal/planes', this.planForm); + this.showToast('Plan creado en dLocal ✓'); + } + this.showPlanModal = false; + await this.loadDlocalPlanes(); + } catch (e) { + this.planError = e.response?.data?.error || 'Error al guardar el plan.'; + } finally { + this.planSaving = false; + } + }, + + copyDlocalWebhook() { + const url = window.location.origin + '/webhooks/dlocal'; + navigator.clipboard.writeText(url).then(() => this.showToast('URL copiada: ' + url)); + }, + // ─── Toast ─────────────────────────────────────────────────────── showToast(msg, type = 'success') { this.toast = { show: true, msg, type }; diff --git a/resources/views/saas_api.html b/resources/views/saas_api.html index e378f27..4fcee33 100644 --- a/resources/views/saas_api.html +++ b/resources/views/saas_api.html @@ -19,6 +19,7 @@ Nombre SaaS + Pasarela Endpoint Método Estado @@ -27,12 +28,21 @@