up
This commit is contained in:
@@ -66,6 +66,7 @@ func main() {
|
|||||||
migrations.SeedRenovaciones()
|
migrations.SeedRenovaciones()
|
||||||
migrations.SeedIntegraciones()
|
migrations.SeedIntegraciones()
|
||||||
migrations.SeedPasarelas()
|
migrations.SeedPasarelas()
|
||||||
|
migrations.SeedSaas()
|
||||||
migrations.SeedServidores()
|
migrations.SeedServidores()
|
||||||
migrations.SeedAdministracion()
|
migrations.SeedAdministracion()
|
||||||
// Iniciar cron de vencimientos
|
// Iniciar cron de vencimientos
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ func Migrate() {
|
|||||||
// Insertar submódulo "Pasarelas de Pago" en el módulo Integraciones
|
// Insertar submódulo "Pasarelas de Pago" en el módulo Integraciones
|
||||||
SeedPasarelas()
|
SeedPasarelas()
|
||||||
|
|
||||||
|
// Insertar submódulos de SaaS (Productos + Integraciones) en el módulo Integraciones
|
||||||
|
SeedSaas()
|
||||||
|
|
||||||
log.Println("Migration Completed...")
|
log.Println("Migration Completed...")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,3 +479,58 @@ func SeedAdministracion() {
|
|||||||
|
|
||||||
log.Println("[SEED] Seed de Administración completado.")
|
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.")
|
||||||
|
}
|
||||||
|
|||||||
@@ -530,6 +530,20 @@
|
|||||||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||||||
<div x-show="activeTab === 'dlocal'" x-transition>
|
<div x-show="activeTab === 'dlocal'" x-transition>
|
||||||
|
|
||||||
|
<!-- Sub-tabs dLocal -->
|
||||||
|
<div class="border-b border-gray-100 mb-5">
|
||||||
|
<nav class="-mb-px flex gap-5 text-xs">
|
||||||
|
<button @click="dlocalTab = 'config'"
|
||||||
|
:class="dlocalTab==='config' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-400 hover:text-gray-600'"
|
||||||
|
class="pb-2 transition-colors">Configuración</button>
|
||||||
|
<button @click="dlocalTab = 'planes'; loadDlocalPlanes()"
|
||||||
|
:class="dlocalTab==='planes' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-400 hover:text-gray-600'"
|
||||||
|
class="pb-2 transition-colors">Planes de suscripción</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Sub-tab: Configuración ─────────────────────────────────── -->
|
||||||
|
<div x-show="dlocalTab === 'config'">
|
||||||
<div class="grid lg:grid-cols-2 gap-6">
|
<div class="grid lg:grid-cols-2 gap-6">
|
||||||
|
|
||||||
<!-- Formulario dLocal -->
|
<!-- Formulario dLocal -->
|
||||||
@@ -635,28 +649,167 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border border-gray-200 rounded-xl p-5">
|
<div class="border border-gray-200 rounded-xl p-5">
|
||||||
<h3 class="text-sm font-semibold mb-2 text-gray-700">Endpoints disponibles</h3>
|
<h3 class="text-sm font-semibold mb-2 flex items-center gap-2 text-gray-700">
|
||||||
<div class="space-y-1.5 text-xs">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-[#8eb02f]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<div class="flex items-center gap-2">
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
|
||||||
<span class="bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-mono text-[10px]">POST</span>
|
</svg>
|
||||||
<span class="text-gray-600">/v1/dlocal/subscription/crear-plan</span>
|
URL del Webhook
|
||||||
</div>
|
</h3>
|
||||||
<div class="flex items-center gap-2">
|
<p class="text-xs text-gray-500 mb-3">Registra esta URL en el panel dLocal → Integraciones → Webhook</p>
|
||||||
<span class="bg-green-50 text-green-600 px-1.5 py-0.5 rounded font-mono text-[10px]">GET</span>
|
<div class="flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-lg px-3 py-2">
|
||||||
<span class="text-gray-600">/v1/dlocal/subscription/ver-plan/:id</span>
|
<span class="text-xs font-mono text-gray-700 flex-1 truncate">/webhooks/dlocal</span>
|
||||||
</div>
|
<button @click="copyDlocalWebhook()" title="Copiar URL" class="text-[#8eb02f] hover:text-[#6d8c24] flex-shrink-0">
|
||||||
<div class="flex items-center gap-2">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<span class="bg-yellow-50 text-yellow-600 px-1.5 py-0.5 rounded font-mono text-[10px]">PATCH</span>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||||
<span class="text-gray-600">/v1/dlocal/subscription/actualizar-plan/:id</span>
|
</svg>
|
||||||
</div>
|
</button>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-mono text-[10px]">POST</span>
|
|
||||||
<span class="text-gray-600">/v1/dlocal/payment/crear-pago</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div><!-- /sub-tab config -->
|
||||||
|
|
||||||
|
<!-- ── Sub-tab: Planes de suscripción ─────────────────────────── -->
|
||||||
|
<div x-show="dlocalTab === 'planes'">
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<p class="text-xs text-gray-500">Planes almacenados en dLocal directamente. Los cambios se reflejan en tiempo real.</p>
|
||||||
|
<button @click="openPlanAdd()" class="bg-[#8eb02f] text-white px-4 py-2 rounded text-sm">+ Nuevo plan</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading planes -->
|
||||||
|
<div x-show="dlocalPlanesLoading" class="py-8 text-center text-gray-400 text-sm">Consultando planes en dLocal…</div>
|
||||||
|
<div x-show="dlocalPlanesError" class="py-4 px-4 bg-red-50 text-red-600 rounded text-sm" x-text="dlocalPlanesError"></div>
|
||||||
|
|
||||||
|
<!-- Tabla de planes -->
|
||||||
|
<div x-show="!dlocalPlanesLoading && !dlocalPlanesError" class="border border-gray-200 rounded-xl overflow-hidden">
|
||||||
|
<div x-show="dlocalPlanes.length === 0" class="py-12 text-center text-gray-400 text-sm">
|
||||||
|
Sin planes creados en dLocal aún.
|
||||||
|
</div>
|
||||||
|
<div x-show="dlocalPlanes.length > 0">
|
||||||
|
<table class="w-full text-xs table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50 text-left border-b">
|
||||||
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">ID</th>
|
||||||
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Nombre</th>
|
||||||
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Monto</th>
|
||||||
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Frecuencia</th>
|
||||||
|
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Estado</th>
|
||||||
|
<th class="py-3 px-4"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="plan in dlocalPlanes" :key="plan.id">
|
||||||
|
<tr class="border-b last:border-0 hover:bg-gray-50 transition-colors">
|
||||||
|
<td class="py-3 px-4 font-mono text-gray-400 text-[10px]" x-text="(plan.id||'—').slice(0,12)+'…'"></td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<div class="font-medium text-gray-800" x-text="plan.name || '—'"></div>
|
||||||
|
<div class="text-gray-400 text-[10px] mt-0.5" x-text="plan.description || ''"></div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 font-semibold text-gray-800">
|
||||||
|
<span x-text="plan.currency + ' ' + (plan.amount || 0).toLocaleString('es-CO')"></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-gray-600">
|
||||||
|
<span x-text="plan.frequency_type || '—'"></span>
|
||||||
|
<span x-show="plan.frequency_value" x-text="' x' + plan.frequency_value" class="text-gray-400"></span>
|
||||||
|
<span x-show="plan.day_of_month" x-text="' (día ' + plan.day_of_month + ')'" class="text-gray-400"></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<span x-text="plan.status || 'active'"
|
||||||
|
:class="(plan.status === 'inactive' || plan.status === 'INACTIVE') ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-700'"
|
||||||
|
class="px-2 py-0.5 rounded-full text-[10px] font-semibold capitalize"></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<button @click="openPlanEdit(plan)" title="Editar" class="text-blue-500 hover:text-blue-700 mr-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal crear / editar plan -->
|
||||||
|
<div x-show="showPlanModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" style="display:none">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-xl mx-4 max-h-screen overflow-y-auto">
|
||||||
|
<div class="flex justify-between items-center p-5 border-b">
|
||||||
|
<h2 class="text-lg font-semibold" x-text="planEditMode ? 'Editar plan dLocal' : 'Nuevo plan dLocal'"></h2>
|
||||||
|
<button @click="showPlanModal = false" class="text-gray-400 hover:text-gray-600 text-xl">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 space-y-3">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">Nombre <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" x-model="planForm.name" class="border rounded w-full p-2 text-sm" placeholder="Ej: Plan mensual VCard">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">Descripción</label>
|
||||||
|
<input type="text" x-model="planForm.description" class="border rounded w-full p-2 text-sm" placeholder="Descripción breve">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">Moneda <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" x-model="planForm.currency" class="border rounded w-full p-2 text-sm font-mono" placeholder="COP / USD">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">Monto <span class="text-red-500">*</span></label>
|
||||||
|
<input type="number" x-model.number="planForm.amount" class="border rounded w-full p-2 text-sm" placeholder="50000">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">Tipo de frecuencia <span class="text-red-500">*</span></label>
|
||||||
|
<select x-model="planForm.frequency_type" class="border rounded w-full p-2 text-sm">
|
||||||
|
<option value="MONTHLY">Mensual</option>
|
||||||
|
<option value="WEEKLY">Semanal</option>
|
||||||
|
<option value="DAILY">Diario</option>
|
||||||
|
<option value="YEARLY">Anual</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">Valor frecuencia <span class="text-gray-400 font-normal">(ej: 1)</span></label>
|
||||||
|
<input type="number" x-model.number="planForm.frequency_value" class="border rounded w-full p-2 text-sm" min="1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">Día del mes <span class="text-gray-400 font-normal">(opcional)</span></label>
|
||||||
|
<input type="number" x-model.number="planForm.day_of_month" class="border rounded w-full p-2 text-sm" min="1" max="31">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">País <span class="text-gray-400 font-normal">(opcional)</span></label>
|
||||||
|
<input type="text" x-model="planForm.country" class="border rounded w-full p-2 text-sm font-mono" placeholder="CO">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">URL de notificación (webhook)</label>
|
||||||
|
<input type="url" x-model="planForm.notification_url" class="border rounded w-full p-2 text-sm font-mono" placeholder="https://…/webhooks/dlocal">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">URL éxito</label>
|
||||||
|
<input type="url" x-model="planForm.success_url" class="border rounded w-full p-2 text-sm font-mono">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">URL retorno</label>
|
||||||
|
<input type="url" x-model="planForm.back_url" class="border rounded w-full p-2 text-sm font-mono">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">URL error</label>
|
||||||
|
<input type="url" x-model="planForm.error_url" class="border rounded w-full p-2 text-sm font-mono">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="planError" x-text="planError" class="text-red-500 text-sm"></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3 p-5 border-t">
|
||||||
|
<button @click="showPlanModal = false" class="px-4 py-2 border rounded text-sm">Cancelar</button>
|
||||||
|
<button @click="savePlan()" :disabled="planSaving" class="bg-[#8eb02f] text-white px-4 py-2 rounded text-sm disabled:opacity-50">
|
||||||
|
<span x-show="!planSaving">Guardar en dLocal</span>
|
||||||
|
<span x-show="planSaving">Guardando…</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /sub-tab planes -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- /container -->
|
</div><!-- /container -->
|
||||||
@@ -723,6 +876,23 @@ function pasarelasApp() {
|
|||||||
url_dev: '',
|
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 ───────────────────────────────────────────────────────
|
||||||
init() {
|
init() {
|
||||||
this.loadBold();
|
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 ───────────────────────────────────────────────────────
|
// ─── Toast ───────────────────────────────────────────────────────
|
||||||
showToast(msg, type = 'success') {
|
showToast(msg, type = 'success') {
|
||||||
this.toast = { show: true, msg, type };
|
this.toast = { show: true, msg, type };
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="py-2 px-4 border-b">Nombre</th>
|
<th class="py-2 px-4 border-b">Nombre</th>
|
||||||
<th class="py-2 px-4 border-b">SaaS</th>
|
<th class="py-2 px-4 border-b">SaaS</th>
|
||||||
|
<th class="py-2 px-4 border-b">Pasarela</th>
|
||||||
<th class="py-2 px-4 border-b">Endpoint</th>
|
<th class="py-2 px-4 border-b">Endpoint</th>
|
||||||
<th class="py-2 px-4 border-b">Método</th>
|
<th class="py-2 px-4 border-b">Método</th>
|
||||||
<th class="py-2 px-4 border-b">Estado</th>
|
<th class="py-2 px-4 border-b">Estado</th>
|
||||||
@@ -27,12 +28,21 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-gray-500">
|
<tbody class="text-gray-500">
|
||||||
<template x-if="items.length === 0 && !loading">
|
<template x-if="items.length === 0 && !loading">
|
||||||
<tr><td colspan="6" class="py-4 text-center text-gray-400">Sin registros</td></tr>
|
<tr><td colspan="7" class="py-4 text-center text-gray-400">Sin registros</td></tr>
|
||||||
</template>
|
</template>
|
||||||
<template x-for="item in items" :key="item.ID">
|
<template x-for="item in items" :key="item.ID">
|
||||||
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
||||||
<td class="py-2 px-4 font-medium text-gray-800" x-text="item.nombre"></td>
|
<td class="py-2 px-4 font-medium text-gray-800" x-text="item.nombre"></td>
|
||||||
<td class="py-2 px-4 text-xs text-gray-600" x-text="item.SaasProducto ? item.SaasProducto.nombre : '-'"></td>
|
<td class="py-2 px-4 text-xs text-gray-600" x-text="item.SaasProducto ? item.SaasProducto.nombre : '-'"></td>
|
||||||
|
<td class="py-2 px-4">
|
||||||
|
<span x-text="item.pasarela || 'ambas'"
|
||||||
|
:class="{
|
||||||
|
'bg-purple-100 text-purple-700': item.pasarela === 'dlocal',
|
||||||
|
'bg-orange-100 text-orange-700': item.pasarela === 'bold',
|
||||||
|
'bg-gray-100 text-gray-600': !item.pasarela || item.pasarela === 'ambas'
|
||||||
|
}"
|
||||||
|
class="text-xs px-2 py-0.5 rounded-full capitalize"></span>
|
||||||
|
</td>
|
||||||
<td class="py-2 px-4">
|
<td class="py-2 px-4">
|
||||||
<span class="font-mono text-xs text-blue-700 break-all" x-text="item.endpoint_url"></span>
|
<span class="font-mono text-xs text-blue-700 break-all" x-text="item.endpoint_url"></span>
|
||||||
</td>
|
</td>
|
||||||
@@ -86,16 +96,27 @@
|
|||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre / etiqueta <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre / etiqueta <span class="text-red-500">*</span></label>
|
||||||
<input type="text" x-model="form.nombre" class="border rounded w-full p-2 text-sm" placeholder="Ej: VCard – confirmar pago">
|
<input type="text" x-model="form.nombre" class="border rounded w-full p-2 text-sm" placeholder="Ej: VCard – confirmar pago">
|
||||||
</div>
|
</div>
|
||||||
<!-- SaaS -->
|
<!-- SaaS + Pasarela -->
|
||||||
<div>
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Producto SaaS <span class="text-red-500">*</span></label>
|
<div>
|
||||||
<select x-model.number="form.saas_id" class="border rounded w-full p-2 text-sm">
|
<label class="block text-sm font-medium text-gray-700 mb-1">Producto SaaS <span class="text-red-500">*</span></label>
|
||||||
<option value="">— Seleccionar —</option>
|
<select x-model.number="form.saas_id" class="border rounded w-full p-2 text-sm">
|
||||||
<template x-for="s in saasOpts" :key="s.ID">
|
<option value="">— Seleccionar —</option>
|
||||||
<option :value="s.ID" x-text="s.nombre"></option>
|
<template x-for="s in saasOpts" :key="s.ID">
|
||||||
</template>
|
<option :value="s.ID" x-text="s.nombre"></option>
|
||||||
</select>
|
</template>
|
||||||
<p class="text-xs text-gray-400 mt-0.5">El SaaS debe tener un Servicio vinculado para que se active al confirmar contratos.</p>
|
</select>
|
||||||
|
<p class="text-xs text-gray-400 mt-0.5">Debe tener un Servicio vinculado para activarse al confirmar contratos.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Pasarela de pago</label>
|
||||||
|
<select x-model="form.pasarela" class="border rounded w-full p-2 text-sm">
|
||||||
|
<option value="ambas">Ambas (Bold + dLocal)</option>
|
||||||
|
<option value="dlocal">Solo dLocal</option>
|
||||||
|
<option value="bold">Solo Bold</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-gray-400 mt-0.5">Qué pasarela dispara esta notificación.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Endpoint URL -->
|
<!-- Endpoint URL -->
|
||||||
<div>
|
<div>
|
||||||
@@ -170,7 +191,7 @@ function saasApiApp() {
|
|||||||
saving: false, error: '',
|
saving: false, error: '',
|
||||||
form: {},
|
form: {},
|
||||||
defaultForm() {
|
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 init() { await this.load(1); },
|
||||||
async load(p) {
|
async load(p) {
|
||||||
@@ -197,6 +218,7 @@ function saasApiApp() {
|
|||||||
id: item.ID,
|
id: item.ID,
|
||||||
saas_id: item.saas_id,
|
saas_id: item.saas_id,
|
||||||
nombre: item.nombre,
|
nombre: item.nombre,
|
||||||
|
pasarela: item.pasarela || 'ambas',
|
||||||
endpoint_url: item.endpoint_url,
|
endpoint_url: item.endpoint_url,
|
||||||
metodo: item.metodo || 'POST',
|
metodo: item.metodo || 'POST',
|
||||||
api_key_header: item.api_key_header || '',
|
api_key_header: item.api_key_header || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user