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.
+
+
+
+
+
+ | ID |
+ Nombre |
+ Monto |
+ Frecuencia |
+ Estado |
+ |
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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 @@
- | Sin registros |
+ | Sin registros |
|
|
+
+
+ |
|
@@ -86,16 +96,27 @@
-
-
-
-
-
El SaaS debe tener un Servicio vinculado para que se active al confirmar contratos.
+
+
+
+
+
+
Debe tener un Servicio vinculado para activarse al confirmar contratos.
+
+
+
+
+
Qué pasarela dispara esta notificación.
+
@@ -170,7 +191,7 @@ function saasApiApp() {
saving: false, error: '',
form: {},
defaultForm() {
- return { saas_id: 0, nombre: '', endpoint_url: '', metodo: 'POST', api_key_header: '', api_key_value: '', payload_template: '', timeout_seg: 10, activo: true };
+ return { saas_id: 0, nombre: '', pasarela: 'ambas', endpoint_url: '', metodo: 'POST', api_key_header: '', api_key_value: '', payload_template: '', timeout_seg: 10, activo: true };
},
async init() { await this.load(1); },
async load(p) {
@@ -197,6 +218,7 @@ function saasApiApp() {
id: item.ID,
saas_id: item.saas_id,
nombre: item.nombre,
+ pasarela: item.pasarela || 'ambas',
endpoint_url: item.endpoint_url,
metodo: item.metodo || 'POST',
api_key_header: item.api_key_header || '',