Base para que el cliente administre uMind desde su portal y para el cobro por consumo. Todavía no cambia nada del comportamiento actual. - UmindTenant gana ClienteID (punteros, sin not null: los tenants creados antes del portal quedan sin asignar y el ALTER TABLE no falla sobre datos existentes) y PlanID. - GetUmindTenantsByClientes es fail-closed: sin clientes, no ve nada. - UmindPlan define el máximo de agentes y los precios por 1k tokens, por imagen OCR, por transcripción y la mensualidad, más un tope de consumo que solo avisa. CRUD para staff en /app/umind-planes. - El modelo va en AMBAS listas de AutoMigrate (main.go y migrations), no repetir el error de agregarlo solo en una. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1481 lines
54 KiB
Go
Executable File
1481 lines
54 KiB
Go
Executable File
package migrations
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Session structure for managing sessions
|
|
type Session struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
SessionID string `gorm:"unique;not null"`
|
|
Data string `gorm:"type:text"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
ExpireAt time.Time
|
|
}
|
|
|
|
// Migrate performs the database migrations
|
|
func Migrate() {
|
|
log.Println("Initiating migration...")
|
|
|
|
// Migrate the main application models.
|
|
// Un solo AutoMigrate(a, b, c...) se detiene en el primer modelo que falle:
|
|
// todo lo que viene después en la lista se queda sin tabla. Migrando uno por
|
|
// uno, un modelo roto no bloquea al resto (mismo fix que en main.go).
|
|
modelosPrincipales := []interface{}{
|
|
&models.Roles{},
|
|
&models.Users{},
|
|
&models.Modules{},
|
|
&models.Submodules{},
|
|
&models.Inicio{},
|
|
&models.TipoServidor{},
|
|
&models.TipoDb{},
|
|
&models.Servidor{},
|
|
&models.ConxDb{},
|
|
&models.ConxSsh{},
|
|
&models.ProvServidor{},
|
|
&models.QrVcard{},
|
|
&models.OssApi{},
|
|
&models.VcfVcard{},
|
|
&models.DlocalApi{},
|
|
// Módulo de Renovaciones
|
|
&models.Servicio{},
|
|
&models.Cliente{},
|
|
&models.PlantillaCorreo{},
|
|
&models.NotificacionRegla{},
|
|
&models.Contrato{},
|
|
&models.NotificacionLog{},
|
|
&models.SmtpConfig{},
|
|
// Integraciones externas
|
|
&models.HostingerConfig{},
|
|
&models.CloudflareConfig{},
|
|
// Query Runner
|
|
&models.QueryHistory{},
|
|
// Pasarelas de pago
|
|
&models.BoldConfig{},
|
|
&models.BoldWebhookLog{},
|
|
&models.BoldCallbackLog{},
|
|
&models.DlocalPaymentLog{},
|
|
&models.PaypalConfig{},
|
|
// Documentación por SaaS
|
|
&models.SaasProducto{},
|
|
&models.DocCategoria{},
|
|
&models.DocPagina{},
|
|
// Integraciones SaaS (dispatcher de pagos)
|
|
&models.SaasApiConfig{},
|
|
&models.SaasDispatchLog{},
|
|
&models.SaasWebhookInLog{},
|
|
// Documentos de clientes
|
|
&models.ClienteDocumento{},
|
|
// Telegram
|
|
&models.TelegramConfig{},
|
|
&models.TelegramLog{},
|
|
// Portal de Clientes
|
|
&models.PortalUser{},
|
|
&models.PortalAcceso{},
|
|
&models.Proyecto{},
|
|
&models.ProyectoFase{},
|
|
&models.ProyectoAvance{},
|
|
&models.ProyectoEntregable{},
|
|
&models.ProyectoDocumento{},
|
|
&models.ProyectoTicket{},
|
|
&models.TicketMensaje{},
|
|
&models.Factura{},
|
|
// Sistema de notificaciones por evento
|
|
&models.NotifEventoConfig{},
|
|
&models.SistemaNotificacion{},
|
|
&models.ServidorAlertaUmbral{},
|
|
// Submódulo Partner
|
|
&models.PartnerRecurso{},
|
|
&models.PartnerComunicado{},
|
|
// Integración VCard API Admin (Laravel)
|
|
&models.VcardApiConfig{},
|
|
// Integración Coolify
|
|
&models.CoolifyConfig{},
|
|
// Historial de métricas del agente
|
|
&models.ServidorMetricasHistory{},
|
|
// Monitor de disponibilidad de URLs
|
|
&models.UrlMonitor{},
|
|
&models.UrlMonitorLog{},
|
|
// Tablero de tareas
|
|
&models.Tarea{},
|
|
&models.TareaComentario{},
|
|
// Pagos externos: API de cobros para aplicaciones de terceros
|
|
&models.ServicioPagoExterno{},
|
|
&models.SolicitudPagoExterna{},
|
|
// Automatización de cotizaciones, contratos, actas y cuentas de cobro con IA
|
|
&models.PlantillaDocumento{},
|
|
&models.Tarifa{},
|
|
&models.DocumentoGenerado{},
|
|
&models.Arquitectura{},
|
|
// Vinculación de Telegram para staff interno
|
|
&models.TelegramStaffToken{},
|
|
// uMind: chat con IA embebible por tenant (widget web + RAG)
|
|
&models.UmindTenant{},
|
|
&models.UmindAgente{},
|
|
&models.UmindDocumento{},
|
|
&models.UmindChunk{},
|
|
&models.UmindMensaje{},
|
|
&models.UmindHerramienta{},
|
|
&models.UmindCanal{},
|
|
&models.UmindConexion{},
|
|
&models.UmindEventoLog{},
|
|
&models.UmindPlan{},
|
|
// API Keys de /api/v2 (token + IP obligatoria + scopes)
|
|
&models.ApiKey{},
|
|
// Integraciones: OCR y transcripción de audio (servicios propios)
|
|
&models.OcrConfig{},
|
|
&models.WhisperAsrConfig{},
|
|
}
|
|
for _, m := range modelosPrincipales {
|
|
if err := app.Http.Database.DB.Migrator().AutoMigrate(m); err != nil {
|
|
log.Printf("[MIGRATE] Error migrando %T: %v", m, err)
|
|
}
|
|
}
|
|
|
|
// Open (or create) a SQLite database for sessions
|
|
db, err := gorm.Open(sqlite.Open("session.db"), &gorm.Config{})
|
|
if err != nil {
|
|
log.Fatalf("Error connecting to the database: %v", err)
|
|
return
|
|
}
|
|
|
|
// Migrate the Session structure
|
|
if err := db.AutoMigrate(&Session{}); err != nil {
|
|
log.Fatalf("Error migrating session database: %v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Println("Session database created and table migrated successfully.")
|
|
|
|
// Insertar módulo y submódulos de Renovaciones
|
|
SeedRenovaciones()
|
|
|
|
// Insertar módulo y submódulos de Integraciones externas
|
|
SeedIntegraciones()
|
|
|
|
// 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()
|
|
|
|
// Insertar submódulo "Statuspage" en el módulo Integraciones
|
|
SeedStatuspage()
|
|
|
|
// Crear plantillas y reglas mínimas si no existen (bienvenida, pago_recibido)
|
|
SeedPlantillasBase()
|
|
|
|
// Agregar submódulo "Telegram" al módulo Integraciones
|
|
SeedTelegram()
|
|
|
|
// Crear módulo y submódulos de Portal de Clientes
|
|
SeedPortalClientes()
|
|
|
|
// Agregar submódulo "Partner Recursos" al módulo Portal de Clientes
|
|
SeedPartnerRecursos()
|
|
|
|
// Crear módulo y submódulos de Automatización IA (cotizaciones, contratos, asistente)
|
|
SeedAutomatizacionIA()
|
|
|
|
log.Println("Migration Completed...")
|
|
}
|
|
|
|
// asignarSubmodulosSiFaltan agrega submódulos a un rol solo si el rol todavía no
|
|
// tiene NINGUNO de ellos.
|
|
//
|
|
// Antes los seeds hacían Append incondicional en cada arranque: si un admin le
|
|
// quitaba un módulo a un rol desde el panel, el siguiente despliegue se lo
|
|
// devolvía y la gestión de permisos era, en la práctica, imposible de sostener.
|
|
func asignarSubmodulosSiFaltan(rol *models.Roles, subs []models.Submodules) {
|
|
if rol == nil || len(subs) == 0 {
|
|
return
|
|
}
|
|
db := app.Http.Database.DB
|
|
|
|
var ids []uint
|
|
for _, s := range subs {
|
|
if s.ID > 0 {
|
|
ids = append(ids, s.ID)
|
|
}
|
|
}
|
|
if len(ids) == 0 {
|
|
return
|
|
}
|
|
|
|
var yaAsignados int64
|
|
db.Table("roles_submodules").
|
|
Where("roles_id = ? AND submodules_id IN ?", rol.ID, ids).
|
|
Count(&yaAsignados)
|
|
if yaAsignados > 0 {
|
|
// El rol ya conoce este grupo de submódulos: lo que tenga hoy es una
|
|
// decisión del administrador y no se toca.
|
|
return
|
|
}
|
|
|
|
if err := db.Model(rol).Association("Submodules").Append(&subs); err != nil {
|
|
log.Printf("[SEED] Error asignando submódulos al rol %s: %v", rol.Name, err)
|
|
}
|
|
}
|
|
|
|
// SeedStatuspage agrega el submódulo "Statuspage" (Atlassian) al módulo "Integraciones"
|
|
// y lo asigna a todos los roles. Es idempotente.
|
|
func SeedStatuspage() {
|
|
db := app.Http.Database.DB
|
|
|
|
// Obtener el módulo "Integraciones" (debe existir luego de 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 SeedStatuspage: %v", err)
|
|
return
|
|
}
|
|
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Statuspage", "Monitoreo de estado de servicios vía Atlassian Statuspage", "/app/statuspage"},
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Solo se asigna al rol Administrador: los demás roles pueden estar
|
|
// restringidos a propósito y no deben recibir módulos nuevos por su cuenta
|
|
// — hay que habilitarlos manualmente desde /app/roles si corresponde.
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err)
|
|
return
|
|
}
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
|
|
log.Println("[SEED] Seed de Statuspage completado.")
|
|
}
|
|
|
|
// SeedPlantillasBase crea una plantilla mínima y la regla de pago_recibido si no existen.
|
|
// Es idempotente — no sobreescribe registros existentes.
|
|
func SeedPlantillasBase() {
|
|
db := app.Http.Database.DB
|
|
|
|
plantillasBase := []struct {
|
|
nombre string
|
|
asunto string
|
|
tipo string
|
|
cuerpo string
|
|
}{
|
|
{
|
|
nombre: "Pago recibido (base)",
|
|
asunto: "Pago recibido — gracias por tu pago",
|
|
tipo: "pago",
|
|
cuerpo: `<!DOCTYPE html><html><body style="font-family:sans-serif;color:#333;max-width:600px;margin:auto;padding:24px">
|
|
<h2 style="color:#5a7d2b">Pago recibido ✔</h2>
|
|
<p>Hola <strong>{{.ClienteNombre}}</strong>,</p>
|
|
<p>Hemos recibido y confirmado tu pago. Tu servicio ha sido renovado correctamente.</p>
|
|
<table style="width:100%;border-collapse:collapse;margin:16px 0">
|
|
{{range .Servicios}}
|
|
<tr style="border-bottom:1px solid #eee">
|
|
<td style="padding:8px">{{.Nombre}}</td>
|
|
<td style="padding:8px;text-align:right">{{.Moneda}} {{.Precio}}</td>
|
|
<td style="padding:8px;text-align:right;color:#888">Vence: {{.FechaVenc}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</table>
|
|
<p style="font-size:13px;color:#888">Si tienes alguna pregunta, responde a este correo.</p>
|
|
</body></html>`,
|
|
},
|
|
{
|
|
nombre: "Bienvenida (base)",
|
|
asunto: "Bienvenido — tu servicio está activo",
|
|
tipo: "bienvenida",
|
|
cuerpo: `<!DOCTYPE html><html><body style="font-family:sans-serif;color:#333;max-width:600px;margin:auto;padding:24px">
|
|
<h2 style="color:#5a7d2b">¡Bienvenido!</h2>
|
|
<p>Hola <strong>{{.ClienteNombre}}</strong>,</p>
|
|
<p>Tu servicio ha sido activado. A continuación el detalle:</p>
|
|
<table style="width:100%;border-collapse:collapse;margin:16px 0">
|
|
{{range .Servicios}}
|
|
<tr style="border-bottom:1px solid #eee">
|
|
<td style="padding:8px">{{.Nombre}}</td>
|
|
<td style="padding:8px;text-align:right">{{.Moneda}} {{.Precio}}</td>
|
|
<td style="padding:8px;text-align:right;color:#888">Vence: {{.FechaVenc}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</table>
|
|
<p style="font-size:13px;color:#888">Gracias por elegirnos.</p>
|
|
</body></html>`,
|
|
},
|
|
}
|
|
|
|
for _, p := range plantillasBase {
|
|
var existing models.PlantillaCorreo
|
|
if err := db.Where("nombre = ?", p.nombre).First(&existing).Error; err != nil {
|
|
nueva := models.PlantillaCorreo{
|
|
Nombre: p.nombre,
|
|
Asunto: p.asunto,
|
|
Tipo: p.tipo,
|
|
CuerpoHTML: p.cuerpo,
|
|
}
|
|
if err := db.Create(&nueva).Error; err != nil {
|
|
log.Printf("[SEED] Error creando plantilla '%s': %v", p.nombre, err)
|
|
continue
|
|
}
|
|
log.Printf("[SEED] Plantilla '%s' creada (ID %d)", p.nombre, nueva.ID)
|
|
} else {
|
|
log.Printf("[SEED] Plantilla '%s' ya existe (ID %d)", existing.Nombre, existing.ID)
|
|
}
|
|
}
|
|
|
|
// Crear regla pago_recibido si no existe
|
|
var reglaExistente models.NotificacionRegla
|
|
if err := db.Where("tipo_evento = ?", "pago_recibido").First(®laExistente).Error; err != nil {
|
|
// Buscar la plantilla de pago
|
|
var plantilla models.PlantillaCorreo
|
|
if err2 := db.Where("tipo = ?", "pago").First(&plantilla).Error; err2 != nil {
|
|
log.Printf("[SEED] No se encontró plantilla de tipo 'pago' para crear regla pago_recibido")
|
|
return
|
|
}
|
|
regla := models.NotificacionRegla{
|
|
Nombre: "Confirmación de pago",
|
|
TipoEvento: "pago_recibido",
|
|
PlantillaID: plantilla.ID,
|
|
Activo: true,
|
|
AplicaA: "todos",
|
|
PasarelaEnlace: "ninguna",
|
|
}
|
|
if err := db.Create(®la).Error; err != nil {
|
|
log.Printf("[SEED] Error creando regla pago_recibido: %v", err)
|
|
} else {
|
|
log.Printf("[SEED] Regla 'pago_recibido' creada (ID %d) con plantilla ID %d", regla.ID, plantilla.ID)
|
|
}
|
|
} else {
|
|
log.Printf("[SEED] Regla 'pago_recibido' ya existe (ID %d)", reglaExistente.ID)
|
|
}
|
|
|
|
log.Println("[SEED] SeedPlantillasBase completado.")
|
|
}
|
|
|
|
// MigrateRenovaciones crea/actualiza las tablas del módulo de Renovaciones.
|
|
// Es idempotente: GORM AutoMigrate solo añade columnas/tablas nuevas, nunca las borra.
|
|
func MigrateRenovaciones() {
|
|
db := app.Http.Database.DB
|
|
if err := db.AutoMigrate(
|
|
&models.Servicio{},
|
|
&models.Cliente{},
|
|
&models.PlantillaCorreo{},
|
|
&models.NotificacionRegla{},
|
|
&models.Contrato{}, // incluye columnas enlace_pago y enlace_pago_link_id
|
|
&models.NotificacionLog{},
|
|
&models.SmtpConfig{},
|
|
); err != nil {
|
|
log.Printf("[MIGRATE] Error en MigrateRenovaciones: %v", err)
|
|
} else {
|
|
log.Println("[MIGRATE] Tablas de Renovaciones OK")
|
|
}
|
|
}
|
|
|
|
// SeedRenovaciones inserta el módulo "Renovaciones" y sus submódulos si no existen,
|
|
// y los asigna a todos los roles presentes en la BD.
|
|
func SeedRenovaciones() {
|
|
db := app.Http.Database.DB
|
|
|
|
// 1. Crear/obtener módulo
|
|
var modulo models.Modules
|
|
result := db.Where("title = ?", "Renovaciones").First(&modulo)
|
|
if result.Error != nil {
|
|
modulo = models.Modules{
|
|
Title: "Renovaciones",
|
|
Description: "Gestión de contratos, clientes y notificaciones de vencimiento",
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&modulo).Error; err != nil {
|
|
log.Printf("[SEED] Error creando módulo Renovaciones: %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Módulo 'Renovaciones' creado con ID %d", modulo.ID)
|
|
} else {
|
|
log.Printf("[SEED] Módulo 'Renovaciones' ya existe (ID %d)", modulo.ID)
|
|
}
|
|
|
|
// 2. Definir submódulos
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Contratos", "Gestión de contratos y vencimientos", "/app/contratos"},
|
|
{"Clientes", "Clientes y datos de contacto", "/app/clientes"},
|
|
{"Servicios", "Catálogo de servicios", "/app/servicios"},
|
|
{"Plantillas Correo", "Editor de plantillas de correo", "/app/plantillas-correo"},
|
|
{"Reglas de Aviso", "Reglas de notificación automática", "/app/reglas-notificacion"},
|
|
{"Historial Notif.", "Historial de correos enviados", "/app/historial-notificaciones"},
|
|
{"Config. SMTP", "Configuración del servidor de correo", "/app/smtp-config"},
|
|
}
|
|
|
|
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 {
|
|
// Asegurar que el submódulo apunte al módulo correcto (Renovaciones)
|
|
if sub.ModuleId != modulo.ID {
|
|
if err := db.Model(&sub).Update("module_id", modulo.ID).Error; err != nil {
|
|
log.Printf("[SEED] Error actualizando module_id de '%s': %v", sub.Title, err)
|
|
} else {
|
|
log.Printf("[SEED] Submódulo '%s' reasignado a módulo Renovaciones", sub.Title)
|
|
}
|
|
} else {
|
|
log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID)
|
|
}
|
|
}
|
|
insertados = append(insertados, sub)
|
|
}
|
|
|
|
// 3. Asignar solo al rol Administrador
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
log.Printf("[SEED] Submódulos de Renovaciones asignados al rol 'Administrador'")
|
|
}
|
|
|
|
log.Println("[SEED] Seed de Renovaciones completado.")
|
|
}
|
|
|
|
// SeedAutomatizacionIA inserta el módulo "Automatización IA" y sus submódulos
|
|
// (Asistente, Plantillas de Documento, Tarifas) si no existen, y los asigna al
|
|
// rol Administrador. Es idempotente.
|
|
func SeedAutomatizacionIA() {
|
|
db := app.Http.Database.DB
|
|
|
|
// 1. Crear/obtener módulo
|
|
var modulo models.Modules
|
|
result := db.Where("title = ?", "Automatización IA").First(&modulo)
|
|
if result.Error != nil {
|
|
modulo = models.Modules{
|
|
Title: "Automatización IA",
|
|
Description: "Cotizaciones, contratos, actas y cuentas de cobro generados por IA",
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&modulo).Error; err != nil {
|
|
log.Printf("[SEED] Error creando módulo Automatización IA: %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Módulo 'Automatización IA' creado con ID %d", modulo.ID)
|
|
} else {
|
|
log.Printf("[SEED] Módulo 'Automatización IA' ya existe (ID %d)", modulo.ID)
|
|
}
|
|
|
|
// 2. Definir submódulos
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Asistente", "Chat propio: mismo motor y tools que el bot de Telegram", "/app/asistente"},
|
|
{"Chats autorizados", "Quién puede hablarle al bot de Telegram y ejecutar acciones", "/app/agente/chats-autorizados"},
|
|
{"Plantillas de Documento", "Fuente de verdad de cotización, contrato, acta y cuenta de cobro", "/app/automatizacion/plantillas"},
|
|
{"Tarifas", "Valor por hora, licencias, VMs y márgenes usados al cotizar", "/app/automatizacion/tarifas"},
|
|
}
|
|
|
|
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 {
|
|
if sub.ModuleId != modulo.ID {
|
|
if err := db.Model(&sub).Update("module_id", modulo.ID).Error; err != nil {
|
|
log.Printf("[SEED] Error actualizando module_id de '%s': %v", sub.Title, err)
|
|
} else {
|
|
log.Printf("[SEED] Submódulo '%s' reasignado a módulo Automatización IA", sub.Title)
|
|
}
|
|
} else {
|
|
log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID)
|
|
}
|
|
}
|
|
insertados = append(insertados, sub)
|
|
}
|
|
|
|
// 3. Asignar solo al rol Administrador
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
log.Printf("[SEED] Submódulos de Automatización IA asignados al rol 'Administrador'")
|
|
}
|
|
|
|
log.Println("[SEED] Seed de Automatización IA completado.")
|
|
}
|
|
|
|
// SeedIntegraciones crea el módulo "Integraciones" con los submódulos
|
|
// de Hostinger y Cloudflare, y los asigna a todos los roles existentes.
|
|
// Es idempotente: si el registro ya existe (por URL) lo reutiliza sin duplicar.
|
|
func SeedIntegraciones() {
|
|
db := app.Http.Database.DB
|
|
|
|
// 1. Crear/obtener módulo
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil {
|
|
modulo = models.Modules{
|
|
Title: "Integraciones",
|
|
Description: "Conexión con servicios externos: Hostinger, Cloudflare y más",
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&modulo).Error; err != nil {
|
|
log.Printf("[SEED] Error creando módulo Integraciones: %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Módulo 'Integraciones' creado con ID %d", modulo.ID)
|
|
} else {
|
|
log.Printf("[SEED] Módulo 'Integraciones' ya existe (ID %d)", modulo.ID)
|
|
}
|
|
|
|
// 2. Definir submódulos
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Hostinger", "Panel de VPS, dominios, hosting y DNS de Hostinger", "/app/hostinger"},
|
|
{"Cloudflare", "Gestión de zonas, DNS, SSL y firewall en Cloudflare", "/app/cloudflare"},
|
|
{"VCard API", "Integración con el sistema VCard externo (Laravel + Sanctum): usuarios, membresías, vcards, pagos y más", "/app/vcard-api"},
|
|
{"OCR", "Extracción de texto de imágenes (comprobantes, capturas) vía servicio OCR propio", "/app/ocr"},
|
|
{"Whisper ASR", "Transcripción de audio vía servicio propio de reconocimiento de voz (self-hosted)", "/app/whisper-asr"},
|
|
}
|
|
|
|
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 {
|
|
if sub.ModuleId != modulo.ID {
|
|
if err := db.Model(&sub).Update("module_id", modulo.ID).Error; err != nil {
|
|
log.Printf("[SEED] Error actualizando module_id de '%s': %v", sub.Title, err)
|
|
} else {
|
|
log.Printf("[SEED] Submódulo '%s' reasignado a módulo Integraciones", sub.Title)
|
|
}
|
|
} else {
|
|
log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID)
|
|
}
|
|
}
|
|
insertados = append(insertados, sub)
|
|
}
|
|
|
|
// 3. Asignar a todos los roles existentes
|
|
// Solo se asigna al rol Administrador: los demás roles pueden estar
|
|
// restringidos a propósito y no deben recibir módulos nuevos por su cuenta
|
|
// — hay que habilitarlos manualmente desde /app/roles si corresponde.
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err)
|
|
return
|
|
}
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
|
|
log.Println("[SEED] Seed de Integraciones completado.")
|
|
}
|
|
|
|
// SeedPasarelas agrega el submódulo "Pasarelas de Pago" al módulo "Integraciones"
|
|
// y lo asigna a todos los roles. Es idempotente.
|
|
func SeedPasarelas() {
|
|
db := app.Http.Database.DB
|
|
|
|
// Obtener el módulo "Integraciones" (debe existir luego de 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 SeedPasarelas: %v", err)
|
|
return
|
|
}
|
|
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Pasarelas de Pago", "Configuración de Bold, dLocal y otras pasarelas", "/app/pasarelas-pago"},
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Solo se asigna al rol Administrador: los demás roles pueden estar
|
|
// restringidos a propósito y no deben recibir módulos nuevos por su cuenta
|
|
// — hay que habilitarlos manualmente desde /app/roles si corresponde.
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err)
|
|
return
|
|
}
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
|
|
log.Println("[SEED] Seed de Pasarelas completado.")
|
|
}
|
|
|
|
// SeedServidores crea el módulo "Servidores" con los submódulos de gestión
|
|
// de servidores, conexiones SSH/DB, tipos y el Query Runner. Es idempotente.
|
|
func SeedServidores() {
|
|
db := app.Http.Database.DB
|
|
|
|
// 1. Crear/obtener módulo
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Servidores").First(&modulo).Error; err != nil {
|
|
modulo = models.Modules{
|
|
Title: "Servidores",
|
|
Description: "Gestión de servidores, conexiones de base de datos y Query Runner",
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&modulo).Error; err != nil {
|
|
log.Printf("[SEED] Error creando módulo Servidores: %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Módulo 'Servidores' creado con ID %d", modulo.ID)
|
|
} else {
|
|
log.Printf("[SEED] Módulo 'Servidores' ya existe (ID %d)", modulo.ID)
|
|
}
|
|
|
|
// 2. Definir submódulos
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Servidores", "Lista y gestión de servidores registrados", "/app/servidor"},
|
|
{"Conexión DB", "Conexiones de base de datos a servidores", "/app/conexion_db"},
|
|
{"Conexión SSH", "Conexiones SSH a servidores", "/app/conexion_ssh"},
|
|
{"Tipo DB", "Catálogo de tipos de base de datos", "/app/tipo_db"},
|
|
{"Tipo Servidor", "Catálogo de tipos de servidor", "/app/tipo_servidor"},
|
|
{"Proveedor Servidor", "Catálogo de proveedores de servidor", "/app/prov_servidor"},
|
|
{"Query Runner", "Editor SQL con historial y exportación", "/app/query-runner"},
|
|
}
|
|
|
|
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 {
|
|
if sub.ModuleId != modulo.ID {
|
|
db.Model(&sub).Update("module_id", modulo.ID)
|
|
log.Printf("[SEED] Submódulo '%s' reasignado a módulo Servidores", sub.Title)
|
|
} else {
|
|
log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID)
|
|
}
|
|
}
|
|
insertados = append(insertados, sub)
|
|
}
|
|
|
|
// 3. Asignar a todos los roles existentes
|
|
// Solo se asigna al rol Administrador: los demás roles pueden estar
|
|
// restringidos a propósito y no deben recibir módulos nuevos por su cuenta
|
|
// — hay que habilitarlos manualmente desde /app/roles si corresponde.
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err)
|
|
return
|
|
}
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
|
|
log.Println("[SEED] Seed de Servidores completado.")
|
|
}
|
|
|
|
// SeedAdministracion crea el módulo "Administración" con sus submódulos
|
|
// (Roles, Módulos, Submódulos, Usuarios) y los asigna a todos los roles. Es idempotente.
|
|
func SeedAdministracion() {
|
|
db := app.Http.Database.DB
|
|
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Administración").First(&modulo).Error; err != nil {
|
|
modulo = models.Modules{
|
|
Title: "Administración",
|
|
Description: "Gestión de usuarios, roles, módulos y permisos del sistema",
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&modulo).Error; err != nil {
|
|
log.Printf("[SEED] Error creando módulo Administración: %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Módulo 'Administración' creado con ID %d", modulo.ID)
|
|
} else {
|
|
log.Printf("[SEED] Módulo 'Administración' ya existe (ID %d)", modulo.ID)
|
|
}
|
|
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Roles", "Gestión de roles y permisos", "/app/roles"},
|
|
{"Módulos", "Gestión de módulos del menú lateral", "/app/modules"},
|
|
{"Submódulos", "Gestión de submódulos y URLs", "/app/submodules"},
|
|
{"Usuarios", "Gestión de usuarios del sistema", "/app/users"},
|
|
}
|
|
|
|
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 {
|
|
if sub.ModuleId != modulo.ID {
|
|
db.Model(&sub).Update("module_id", modulo.ID)
|
|
log.Printf("[SEED] Submódulo '%s' reasignado a módulo Administración", sub.Title)
|
|
} else {
|
|
log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID)
|
|
}
|
|
}
|
|
insertados = append(insertados, sub)
|
|
}
|
|
|
|
// Solo se asigna al rol Administrador: los demás roles pueden estar
|
|
// restringidos a propósito y no deben recibir módulos nuevos por su cuenta
|
|
// — hay que habilitarlos manualmente desde /app/roles si corresponde.
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err)
|
|
return
|
|
}
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
|
|
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)
|
|
}
|
|
|
|
// Solo se asigna al rol Administrador: los demás roles pueden estar
|
|
// restringidos a propósito y no deben recibir módulos nuevos por su cuenta
|
|
// — hay que habilitarlos manualmente desde /app/roles si corresponde.
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err)
|
|
return
|
|
}
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
|
|
log.Println("[SEED] Seed de SaaS completado.")
|
|
}
|
|
|
|
// MigratePortal ejecuta AutoMigrate para los modelos del portal de clientes.
|
|
func MigratePortal() {
|
|
db := app.Http.Database.DB
|
|
if err := db.AutoMigrate(
|
|
&models.Proyecto{},
|
|
&models.ProyectoFase{},
|
|
&models.ProyectoAvance{},
|
|
&models.ProyectoEntregable{},
|
|
&models.ProyectoDocumento{},
|
|
&models.ProyectoTicket{},
|
|
&models.TicketMensaje{},
|
|
&models.PortalUser{},
|
|
&models.PortalAcceso{},
|
|
&models.TelegramPortalToken{},
|
|
&models.Factura{},
|
|
); err != nil {
|
|
log.Printf("[MIGRATE] Error en MigratePortal: %v", err)
|
|
} else {
|
|
log.Println("[MIGRATE] MigratePortal completado.")
|
|
}
|
|
}
|
|
|
|
// MigrateLanding crea/actualiza la tabla de sesiones del Landing Generator.
|
|
func MigrateLanding() {
|
|
db := app.Http.Database.DB
|
|
if err := db.AutoMigrate(&models.LandingSession{}, &models.AiConfig{}); err != nil {
|
|
log.Printf("[MIGRATE] Error en MigrateLanding: %v", err)
|
|
} else {
|
|
log.Println("[MIGRATE] Tablas landing_sessions y ai_configs OK")
|
|
}
|
|
}
|
|
|
|
// SeedTelegram agrega el submódulo de Telegram al módulo "Integraciones". Es idempotente.
|
|
func SeedTelegram() {
|
|
db := app.Http.Database.DB
|
|
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil {
|
|
log.Printf("[SEED] Módulo 'Integraciones' no encontrado para SeedTelegram: %v", err)
|
|
return
|
|
}
|
|
|
|
url := "/app/telegram"
|
|
var sub models.Submodules
|
|
if err := db.Where("url = ?", url).First(&sub).Error; err != nil {
|
|
sub = models.Submodules{
|
|
Title: "Telegram",
|
|
Description: "Configuración de bots de Telegram para notificaciones",
|
|
Url: url,
|
|
ModuleId: modulo.ID,
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err2 := db.Create(&sub).Error; err2 != nil {
|
|
log.Printf("[SEED] Error creando submódulo Telegram: %v", err2)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Submódulo 'Telegram' creado (ID %d)", sub.ID)
|
|
} else {
|
|
log.Printf("[SEED] Submódulo 'Telegram' ya existe (ID %d)", sub.ID)
|
|
}
|
|
|
|
// Solo se asigna al rol Administrador: los demás roles pueden estar
|
|
// restringidos a propósito y no deben recibir módulos nuevos por su cuenta
|
|
// — hay que habilitarlos manualmente desde /app/roles si corresponde.
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err)
|
|
return
|
|
}
|
|
asignarSubmodulosSiFaltan(&rolAdmin, []models.Submodules{sub})
|
|
log.Println("[SEED] Seed de Telegram completado.")
|
|
}
|
|
|
|
// SeedPortalClientes agrega el módulo "Portal de Clientes" con sus submódulos. Es idempotente.
|
|
func SeedPortalClientes() {
|
|
db := app.Http.Database.DB
|
|
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Portal de Clientes").First(&modulo).Error; err != nil {
|
|
modulo = models.Modules{
|
|
Title: "Portal de Clientes",
|
|
Description: "Gestión del portal de acceso para clientes",
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err2 := db.Create(&modulo).Error; err2 != nil {
|
|
log.Printf("[SEED] Error creando módulo 'Portal de Clientes': %v", err2)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Módulo 'Portal de Clientes' creado (ID %d)", modulo.ID)
|
|
} else {
|
|
log.Printf("[SEED] Módulo 'Portal de Clientes' ya existe (ID %d)", modulo.ID)
|
|
}
|
|
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Proyectos", "Gestión de proyectos y roadmap para clientes", "/app/proyectos"},
|
|
{"Facturas", "Gestión de facturas y cobros del portal", "/app/facturas"},
|
|
{"Portal Usuarios", "Gestión de usuarios del portal de clientes", "/app/portal-usuarios"},
|
|
}
|
|
|
|
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 err2 := db.Create(&sub).Error; err2 != nil {
|
|
log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err2)
|
|
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)
|
|
}
|
|
|
|
// Solo se asigna al rol Administrador: los demás roles pueden estar
|
|
// restringidos a propósito y no deben recibir módulos nuevos por su cuenta
|
|
// — hay que habilitarlos manualmente desde /app/roles si corresponde.
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err)
|
|
return
|
|
}
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
log.Println("[SEED] Seed de Portal de Clientes completado.")
|
|
}
|
|
|
|
// SeedNotifDefaults inserta configuraciones de notificación por defecto (idempotente).
|
|
// Solo crea registros que no existen; no sobreescribe cambios del admin.
|
|
func SeedNotifDefaults() {
|
|
db := app.Http.Database.DB
|
|
defaults := []models.NotifEventoConfig{
|
|
// Tickets
|
|
{Evento: "ticket_nuevo", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente abre un ticket"},
|
|
{Evento: "ticket_respuesta_cliente", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente responde un ticket"},
|
|
{Evento: "ticket_respuesta_admin", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Portal user recibe cuando el admin responde"},
|
|
{Evento: "factura_subida", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Portal user recibe cuando se sube una factura"},
|
|
// Servidores
|
|
{Evento: "servidor_caido", Destinatario: "admin", CanalEmail: false, CanalTelegram: true, CanalSistema: true, Descripcion: "Admin recibe cuando un servidor deja de reportar"},
|
|
{Evento: "servidor_vence_pronto", Destinatario: "admin", CanalEmail: true, CanalTelegram: true, CanalSistema: true, Descripcion: "Admin recibe cuando un VPS está próximo a vencer"},
|
|
{Evento: "servidor_recurso_alto", Destinatario: "admin", CanalEmail: false, CanalTelegram: true, CanalSistema: true, Descripcion: "Admin recibe cuando CPU/RAM/Disco supera el umbral"},
|
|
// Tareas
|
|
{Evento: "tarea_asignada", Destinatario: "admin", CanalEmail: true, CanalTelegram: true, CanalSistema: true, Descripcion: "Notifica cuando se asigna una tarea a un usuario"},
|
|
{Evento: "tarea_estado", Destinatario: "admin", CanalEmail: false, CanalTelegram: false, CanalSistema: true, Descripcion: "Notifica cuando cambia el estado de una tarea"},
|
|
{Evento: "tarea_comentario", Destinatario: "admin", CanalEmail: false, CanalTelegram: false, CanalSistema: true, Descripcion: "Notifica cuando hay un nuevo comentario en una tarea"},
|
|
}
|
|
created := 0
|
|
for _, d := range defaults {
|
|
var existing models.NotifEventoConfig
|
|
if err := db.Where("evento = ? AND destinatario = ?", d.Evento, d.Destinatario).First(&existing).Error; err != nil {
|
|
db.Create(&d)
|
|
created++
|
|
}
|
|
}
|
|
log.Printf("[SEED] SeedNotifDefaults: %d nuevas configuraciones creadas.", created)
|
|
|
|
// Corrección puntual (una sola vez): tarea_asignada/admin se sembró originalmente
|
|
// con canal_telegram=false, dejando la notificación muerta desde su creación.
|
|
// Si nadie tocó el registro desde entonces, se activa por defecto.
|
|
var taRow models.NotifEventoConfig
|
|
if err := db.Where("evento = ? AND destinatario = ?", "tarea_asignada", "admin").First(&taRow).Error; err == nil {
|
|
if !taRow.CanalTelegram && taRow.UpdatedAt.Equal(taRow.CreatedAt) {
|
|
db.Model(&taRow).Update("canal_telegram", true)
|
|
log.Println("[SEED] tarea_asignada/admin: canal_telegram activado (corrección de default)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// SeedShield es un stub idempotente para la configuración de Shield.
|
|
func SeedShield() {
|
|
log.Println("[SEED] SeedShield: sin entradas por defecto definidas aún.")
|
|
}
|
|
|
|
// SeedPartnerRecursos agrega el submódulo "Partner Recursos" al módulo "Portal de Clientes". Es idempotente.
|
|
func SeedPartnerRecursos() {
|
|
db := app.Http.Database.DB
|
|
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Portal de Clientes").First(&modulo).Error; err != nil {
|
|
log.Printf("[SEED] Módulo 'Portal de Clientes' no encontrado para SeedPartnerRecursos: %v", err)
|
|
return
|
|
}
|
|
|
|
url := "/app/partner-recursos"
|
|
var sub models.Submodules
|
|
if err := db.Where("url = ?", url).First(&sub).Error; err != nil {
|
|
sub = models.Submodules{
|
|
Title: "Partner Recursos",
|
|
Description: "Documentación y comunicados exclusivos para partners",
|
|
Url: url,
|
|
ModuleId: modulo.ID,
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err2 := db.Create(&sub).Error; err2 != nil {
|
|
log.Printf("[SEED] Error creando submódulo Partner Recursos: %v", err2)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Submódulo 'Partner Recursos' creado (ID %d)", sub.ID)
|
|
} else {
|
|
log.Printf("[SEED] Submódulo 'Partner Recursos' ya existe (ID %d)", sub.ID)
|
|
}
|
|
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
asignarSubmodulosSiFaltan(&rolAdmin, []models.Submodules{sub})
|
|
log.Printf("[SEED] Submódulo 'Partner Recursos' asignado al rol 'Administrador'")
|
|
}
|
|
log.Println("[SEED] Seed de Partner Recursos completado.")
|
|
}
|
|
|
|
// SeedContabilidadMenu crea el módulo "Contabilidad" y sus submódulos en el menú lateral.
|
|
func SeedContabilidadMenu() {
|
|
db := app.Http.Database.DB
|
|
var modulo models.Modules
|
|
result := db.Where("title = ?", "Contabilidad").First(&modulo)
|
|
if result.Error != nil {
|
|
modulo = models.Modules{
|
|
Title: "Contabilidad",
|
|
Description: "Gestión contable: ingresos, egresos, cuentas por cobrar/pagar",
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&modulo).Error; err != nil {
|
|
log.Printf("[SEED] Error creando módulo Contabilidad: %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Módulo 'Contabilidad' creado con ID %d", modulo.ID)
|
|
} else {
|
|
log.Printf("[SEED] Módulo 'Contabilidad' ya existe (ID %d)", modulo.ID)
|
|
}
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Dashboard", "Resumen mensual", "/app/contabilidad"},
|
|
{"Transacciones", "Ingresos y egresos", "/app/contabilidad/transacciones"},
|
|
{"Categorías", "Plan de cuentas", "/app/contabilidad/cuentas"},
|
|
{"Entidades", "Clientes y proveedores", "/app/contabilidad/entidades"},
|
|
{"Cuentas por cobrar", "Pendientes de cobro", "/app/contabilidad/cuentas-cobro"},
|
|
{"Cuentas por pagar", "Pendientes de pago", "/app/contabilidad/cuentas-pagar"},
|
|
}
|
|
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
|
|
}
|
|
} else if sub.ModuleId != modulo.ID {
|
|
db.Model(&sub).Update("module_id", modulo.ID)
|
|
}
|
|
insertados = append(insertados, sub)
|
|
}
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
asignarSubmodulosSiFaltan(&rolAdmin, insertados)
|
|
log.Printf("[SEED] Submódulos de Contabilidad asignados al rol 'Administrador'")
|
|
}
|
|
log.Println("[SEED] Seed de Contabilidad completado.")
|
|
}
|
|
|
|
// SeedWebSms crea el submódulo de WebSMS bajo "Integraciones".
|
|
func SeedWebSms() {
|
|
db := app.Http.Database.DB
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil {
|
|
log.Println("[SEED] Módulo 'Integraciones' no encontrado, se creará")
|
|
modulo = models.Modules{
|
|
Title: "Integraciones", Description: "Conexión con servicios externos",
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
db.Create(&modulo)
|
|
}
|
|
url := "/app/websms"
|
|
var sub models.Submodules
|
|
if err := db.Where("url = ?", url).First(&sub).Error; err != nil {
|
|
sub = models.Submodules{
|
|
Title: "WebSMS", Description: "Notificaciones SMS vía LabsMobile",
|
|
Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&sub).Error; err != nil {
|
|
log.Printf("[SEED] Error creando submódulo WebSMS: %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Submódulo 'WebSMS' creado")
|
|
} else if sub.ModuleId != modulo.ID {
|
|
db.Model(&sub).Update("module_id", modulo.ID)
|
|
}
|
|
var rolAdmin models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
asignarSubmodulosSiFaltan(&rolAdmin, []models.Submodules{sub})
|
|
log.Printf("[SEED] Submódulo 'WebSMS' asignado al rol 'Administrador'")
|
|
}
|
|
log.Println("[SEED] Seed de WebSMS completado.")
|
|
}
|
|
|
|
// SeedUrlMonitor agrega el submódulo "Monitor de URLs" al módulo "Servidores". Es idempotente.
|
|
func SeedUrlMonitor() {
|
|
db := app.Http.Database.DB
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Servidores").First(&modulo).Error; err != nil {
|
|
log.Println("[SEED] Módulo 'Servidores' no encontrado, se omite SeedUrlMonitor")
|
|
return
|
|
}
|
|
url := "/app/url-monitor"
|
|
var sub models.Submodules
|
|
if err := db.Where("url = ?", url).First(&sub).Error; err != nil {
|
|
sub = models.Submodules{
|
|
Title: "Monitor de URLs",
|
|
Description: "Monitoreo de disponibilidad y latencia de sitios y APIs",
|
|
Url: url,
|
|
ModuleId: modulo.ID,
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&sub).Error; err != nil {
|
|
log.Printf("[SEED] Error creando submódulo 'Monitor de URLs': %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Submódulo 'Monitor de URLs' creado")
|
|
} else if sub.ModuleId != modulo.ID {
|
|
db.Model(&sub).Update("module_id", modulo.ID)
|
|
}
|
|
var rol models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub})
|
|
log.Printf("[SEED] Submódulo 'Monitor de URLs' asignado al rol 'Administrador'")
|
|
}
|
|
log.Println("[SEED] Seed de Monitor de URLs completado.")
|
|
}
|
|
|
|
// SeedTareas agrega el submódulo "Tareas" al módulo "Administración" solo para el rol Administrador. Es idempotente.
|
|
func SeedTareas() {
|
|
db := app.Http.Database.DB
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Administración").First(&modulo).Error; err != nil {
|
|
log.Println("[SEED] Módulo 'Administración' no encontrado, se omite SeedTareas")
|
|
return
|
|
}
|
|
url := "/app/tareas"
|
|
var sub models.Submodules
|
|
if err := db.Where("url = ?", url).First(&sub).Error; err != nil {
|
|
sub = models.Submodules{
|
|
Title: "Tareas",
|
|
Description: "Tablero kanban de tareas internas",
|
|
Url: url,
|
|
ModuleId: modulo.ID,
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&sub).Error; err != nil {
|
|
log.Printf("[SEED] Error creando submódulo 'Tareas': %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Submódulo 'Tareas' creado")
|
|
} else if sub.ModuleId != modulo.ID {
|
|
db.Model(&sub).Update("module_id", modulo.ID)
|
|
}
|
|
var rol models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub})
|
|
log.Printf("[SEED] Submódulo 'Tareas' asignado al rol 'Administrador'")
|
|
}
|
|
log.Println("[SEED] Seed de Tareas completado.")
|
|
}
|
|
|
|
// SeedPagosExternos registra el submódulo del panel de administración de
|
|
// servicios de pago externos (emisión de tokens de API para terceros).
|
|
func SeedPagosExternos() {
|
|
db := app.Http.Database.DB
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Administración").First(&modulo).Error; err != nil {
|
|
log.Println("[SEED] Módulo 'Administración' no encontrado, se omite SeedPagosExternos")
|
|
return
|
|
}
|
|
url := "/app/pagos-externos"
|
|
var sub models.Submodules
|
|
if err := db.Where("url = ?", url).First(&sub).Error; err != nil {
|
|
sub = models.Submodules{
|
|
Title: "Pagos externos",
|
|
Description: "Tokens de API para que apps de terceros pidan cobros por Bold/dLocal/PayPal",
|
|
Url: url,
|
|
ModuleId: modulo.ID,
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&sub).Error; err != nil {
|
|
log.Printf("[SEED] Error creando submódulo 'Pagos externos': %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Submódulo 'Pagos externos' creado")
|
|
} else if sub.ModuleId != modulo.ID {
|
|
db.Model(&sub).Update("module_id", modulo.ID)
|
|
}
|
|
var rol models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub})
|
|
log.Printf("[SEED] Submódulo 'Pagos externos' asignado al rol 'Administrador'")
|
|
}
|
|
log.Println("[SEED] Seed de Pagos externos completado.")
|
|
}
|
|
|
|
// SeedUmind registra el submódulo del panel de administración de uMind
|
|
// (tenants, base de conocimiento y conversaciones del widget embebible).
|
|
func SeedUmind() {
|
|
db := app.Http.Database.DB
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Automatización IA").First(&modulo).Error; err != nil {
|
|
log.Println("[SEED] Módulo 'Automatización IA' no encontrado, se omite SeedUmind")
|
|
return
|
|
}
|
|
// El panel viejo (Alpine, /app/umind) sigue existiendo y respondiendo, pero
|
|
// el menú ahora apunta al orquestador (SPA Vue). Si ya existía el submódulo
|
|
// apuntando a la URL vieja, se migra en el mismo registro en vez de crear
|
|
// uno duplicado.
|
|
url := "/orchestrator"
|
|
urlVieja := "/app/umind"
|
|
var sub models.Submodules
|
|
if err := db.Where("url = ? OR url = ?", url, urlVieja).First(&sub).Error; err != nil {
|
|
sub = models.Submodules{
|
|
Title: "uMind",
|
|
Description: "Chat con IA embebible por sitio, con base de conocimiento propia (RAG)",
|
|
Url: url,
|
|
ModuleId: modulo.ID,
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&sub).Error; err != nil {
|
|
log.Printf("[SEED] Error creando submódulo 'uMind': %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Submódulo 'uMind' creado")
|
|
} else {
|
|
updates := map[string]interface{}{}
|
|
if sub.ModuleId != modulo.ID {
|
|
updates["module_id"] = modulo.ID
|
|
}
|
|
if sub.Url != url {
|
|
updates["url"] = url
|
|
}
|
|
if len(updates) > 0 {
|
|
db.Model(&sub).Updates(updates)
|
|
}
|
|
}
|
|
// Planes de uMind: límite de agentes y precios por consumo. Solo staff.
|
|
var subPlanes models.Submodules
|
|
if err := db.Where("url = ?", "/app/umind-planes").First(&subPlanes).Error; err != nil {
|
|
subPlanes = models.Submodules{
|
|
Title: "Planes uMind",
|
|
Description: "Límite de agentes y precios por consumo (IA, OCR, transcripción, mensualidad)",
|
|
Url: "/app/umind-planes",
|
|
ModuleId: modulo.ID,
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&subPlanes).Error; err != nil {
|
|
log.Printf("[SEED] Error creando submódulo 'Planes uMind': %v", err)
|
|
} else {
|
|
log.Printf("[SEED] Submódulo 'Planes uMind' creado")
|
|
}
|
|
}
|
|
|
|
var rol models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
subs := []models.Submodules{sub}
|
|
if subPlanes.ID > 0 {
|
|
subs = append(subs, subPlanes)
|
|
}
|
|
asignarSubmodulosSiFaltan(&rol, subs)
|
|
log.Printf("[SEED] Submódulos de uMind asignados al rol 'Administrador'")
|
|
}
|
|
log.Println("[SEED] Seed de uMind completado.")
|
|
}
|
|
|
|
// SeedApiKeys registra el submódulo del panel de administración de API Keys
|
|
// (credenciales scoped para /api/v2, alternativa al ADMIN_API_KEY maestro).
|
|
func SeedApiKeys() {
|
|
db := app.Http.Database.DB
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Administración").First(&modulo).Error; err != nil {
|
|
log.Println("[SEED] Módulo 'Administración' no encontrado, se omite SeedApiKeys")
|
|
return
|
|
}
|
|
url := "/app/api-keys"
|
|
var sub models.Submodules
|
|
if err := db.Where("url = ?", url).First(&sub).Error; err != nil {
|
|
sub = models.Submodules{
|
|
Title: "API Keys",
|
|
Description: "Credenciales scoped para /api/v2 (token + IP + alcance), alternativa a la llave maestra",
|
|
Url: url,
|
|
ModuleId: modulo.ID,
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&sub).Error; err != nil {
|
|
log.Printf("[SEED] Error creando submódulo 'API Keys': %v", err)
|
|
return
|
|
}
|
|
log.Printf("[SEED] Submódulo 'API Keys' creado")
|
|
} else if sub.ModuleId != modulo.ID {
|
|
db.Model(&sub).Update("module_id", modulo.ID)
|
|
}
|
|
var rol models.Roles
|
|
if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil {
|
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
|
} else {
|
|
asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub})
|
|
log.Printf("[SEED] Submódulo 'API Keys' asignado al rol 'Administrador'")
|
|
}
|
|
log.Println("[SEED] Seed de API Keys completado.")
|
|
}
|
|
|
|
// MigrarUmindAgentes crea un agente "Principal" por cada UmindTenant que
|
|
// todavía no tenga ninguno, heredando lo que antes vivía en el tenant
|
|
// (site_key, config de IA, tono, mensaje de bienvenida, color) y mueve los
|
|
// datos que ya tenía (documentos, chunks, tools, canales, conexiones,
|
|
// mensajes) de tenant_id a agente_id. Idempotente: un tenant que ya tiene
|
|
// al menos un agente se salta — así corre sola en cada arranque sin
|
|
// duplicar nada, mismo criterio que los Seed* de este archivo.
|
|
func MigrarUmindAgentes() {
|
|
db := app.Http.Database.DB
|
|
|
|
var tenants []models.UmindTenant
|
|
if err := db.Find(&tenants).Error; err != nil {
|
|
log.Printf("[MIGRACION] Error leyendo umind_tenants: %v", err)
|
|
return
|
|
}
|
|
|
|
tablasConAgenteID := []string{
|
|
"umind_documentos", "umind_chunks", "umind_herramientas",
|
|
"umind_canales", "umind_conexiones", "umind_mensajes",
|
|
}
|
|
|
|
for _, t := range tenants {
|
|
var yaTieneAgente int64
|
|
if err := db.Model(&models.UmindAgente{}).Where("tenant_id = ?", t.ID).Count(&yaTieneAgente).Error; err != nil {
|
|
log.Printf("[MIGRACION] Error contando agentes del tenant %d: %v", t.ID, err)
|
|
continue
|
|
}
|
|
if yaTieneAgente > 0 {
|
|
continue
|
|
}
|
|
|
|
// Las columnas viejas (site_key, ai_config_id, tono,
|
|
// mensaje_bienvenida, color) ya no están en el struct Go UmindTenant,
|
|
// pero AutoMigrate nunca las borró de la tabla — se leen directo.
|
|
var viejo struct {
|
|
SiteKey string
|
|
AiConfigID *uint
|
|
Tono string
|
|
MensajeBienvenida string
|
|
Color string
|
|
}
|
|
if err := db.Table("umind_tenants").
|
|
Select("site_key, ai_config_id, tono, mensaje_bienvenida, color").
|
|
Where("id = ?", t.ID).Scan(&viejo).Error; err != nil {
|
|
log.Printf("[MIGRACION] Error leyendo datos viejos del tenant %d: %v", t.ID, err)
|
|
continue
|
|
}
|
|
|
|
agente := models.UmindAgente{
|
|
TenantID: t.ID,
|
|
Nombre: "Principal",
|
|
SiteKey: viejo.SiteKey,
|
|
AiConfigID: viejo.AiConfigID,
|
|
Tono: viejo.Tono,
|
|
MensajeBienvenida: viejo.MensajeBienvenida,
|
|
Color: viejo.Color,
|
|
Activo: true,
|
|
}
|
|
if agente.SiteKey == "" {
|
|
key, err := models.GenerarSiteKey()
|
|
if err != nil {
|
|
log.Printf("[MIGRACION] Error generando site_key para el agente del tenant %d: %v", t.ID, err)
|
|
continue
|
|
}
|
|
agente.SiteKey = key
|
|
}
|
|
if agente.Color == "" {
|
|
agente.Color = "#8eb02f"
|
|
}
|
|
if err := db.Create(&agente).Error; err != nil {
|
|
log.Printf("[MIGRACION] Error creando agente Principal del tenant %d: %v", t.ID, err)
|
|
continue
|
|
}
|
|
|
|
for _, tabla := range tablasConAgenteID {
|
|
sql := fmt.Sprintf("UPDATE %s SET agente_id = ? WHERE tenant_id = ? AND (agente_id IS NULL OR agente_id = 0)", tabla)
|
|
if err := db.Exec(sql, agente.ID, t.ID).Error; err != nil {
|
|
log.Printf("[MIGRACION] Error moviendo %s del tenant %d al agente %d: %v", tabla, t.ID, agente.ID, err)
|
|
}
|
|
}
|
|
log.Printf("[MIGRACION] Tenant %d (%s): agente 'Principal' creado (id=%d), datos migrados", t.ID, t.Nombre, agente.ID)
|
|
}
|
|
}
|