Filtro (opcional, apagado por defecto): antes de abrir ticket, la IA lee el correo y decide si es soporte o ruido —newsletters, notificaciones de plataformas, facturas de proveedores, spam—. Falla abierto: si la IA no está configurada o se cae, el ticket se abre igual. Ignorar a un cliente es peor que tener un ticket de más. Hay un campo de contexto del negocio para los casos raros de cada uno. Los correos masivos se descartan por sus propios encabezados (List-Unsubscribe, Precedence, Auto-Submitted) sin gastar una llamada de IA. Y el motivo por el que el mismo correo se leía y se contestaba de nuevo: la deduplicación era solo un SELECT previo, que no sirve si el flag de leído no llegó a guardarse o si dos instancias leen el buzón a la vez. Ahora hay un índice único parcial sobre message_id: la base rechaza el segundo, y ese rechazo se trata como "ya estaba", no como error — sin ticket repetido y sin segunda auto-respuesta. Si algún correo no se pudo marcar como leído, "Revisar buzón ahora" lo dice en vez de dejarlo solo en el log: es la señal de que van a volver a leerse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1659 lines
61 KiB
Go
Executable File
1659 lines
61 KiB
Go
Executable File
package migrations
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"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{},
|
|
&models.UmindUso{},
|
|
// 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 }{
|
|
// La página real es la pública /status (rest/routes/publicas.go):
|
|
// /app/statuspage nunca existió y el ítem daba 404 al entrar.
|
|
{"Statuspage", "Monitoreo de estado de servicios vía Atlassian Statuspage", "/status"},
|
|
}
|
|
|
|
// La URL vieja apuntaba a /app/statuspage, que no existe: se corrige el
|
|
// registro ya creado en vez de dejar un ítem de menú que da 404.
|
|
db.Model(&models.Submodules{}).Where("url = ?", "/app/statuspage").Update("url", "/status")
|
|
|
|
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ú apunta al SPA. La URL pasó por /app/umind → /orchestrator →
|
|
// /studio: se busca por cualquiera de las tres y se migra el mismo
|
|
// registro, en vez de dejar submódulos duplicados en el menú.
|
|
url := "/studio"
|
|
var sub models.Submodules
|
|
if err := db.Where("url IN ?", []string{url, "/orchestrator", "/app/umind"}).First(&sub).Error; err != nil {
|
|
sub = models.Submodules{
|
|
Title: "uMind Studio",
|
|
Description: "Creación y gestión de agentes de IA: conocimiento, canales, herramientas y consumo",
|
|
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)
|
|
}
|
|
}
|
|
|
|
// LiberarColumnasHuerfanasUmind quita el NOT NULL de las columnas que dejaron
|
|
// de mapearse cuando uMind pasó a multi-agente.
|
|
//
|
|
// GORM agrega columnas pero nunca las borra ni les cambia las restricciones.
|
|
// Al sacar SiteKey de UmindTenant y renombrar TenantID→AgenteID en seis
|
|
// tablas, las columnas viejas quedaron en la base CON su NOT NULL original.
|
|
// El INSERT nuevo ya no las incluye, así que Postgres lo rechaza:
|
|
//
|
|
// null value in column "site_key" violates not-null constraint (23502)
|
|
//
|
|
// Sin esto no se puede crear un tenant, ni un documento, ni un chunk, ni
|
|
// guardar un mensaje: es decir, uMind queda inutilizable después de
|
|
// desplegar el refactor.
|
|
//
|
|
// No se hace DROP COLUMN a propósito: los datos viejos siguen ahí por si hay
|
|
// que reconciliar algo. Solo se libera la restricción.
|
|
// ColumnasHuerfanasUmind es la lista que recorre LiberarColumnasHuerfanasUmind.
|
|
// Está afuera de la función para poder verificar en un test que cada nombre de
|
|
// tabla coincide con el TableName() real del modelo: un typo acá haría que la
|
|
// migración no encuentre la columna y no haga nada, en silencio.
|
|
var ColumnasHuerfanasUmind = []struct{ Tabla, Columna string }{
|
|
{"umind_tenants", "site_key"},
|
|
{"umind_documentos", "tenant_id"},
|
|
{"umind_chunks", "tenant_id"},
|
|
{"umind_mensajes", "tenant_id"},
|
|
{"umind_herramientas", "tenant_id"},
|
|
{"umind_canales", "tenant_id"},
|
|
{"umind_conexiones", "tenant_id"},
|
|
}
|
|
|
|
func LiberarColumnasHuerfanasUmind() {
|
|
db := app.Http.Database.DB
|
|
|
|
for _, h := range ColumnasHuerfanasUmind {
|
|
// Se consulta information_schema en vez de intentar el ALTER a ciegas:
|
|
// en una instalación nueva la columna no existe y el error sería ruido
|
|
// en cada arranque.
|
|
var nullable string
|
|
err := db.Raw(`
|
|
SELECT is_nullable FROM information_schema.columns
|
|
WHERE table_name = ? AND column_name = ?
|
|
`, h.Tabla, h.Columna).Scan(&nullable).Error
|
|
if err != nil || nullable == "" || nullable == "YES" {
|
|
continue
|
|
}
|
|
|
|
sql := fmt.Sprintf("ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL", h.Tabla, h.Columna)
|
|
if err := db.Exec(sql).Error; err != nil {
|
|
log.Printf("[MIGRACION] no se pudo liberar %s.%s: %v", h.Tabla, h.Columna, err)
|
|
continue
|
|
}
|
|
log.Printf("[MIGRACION] %s.%s ya no es NOT NULL (columna huérfana del refactor multi-agente)", h.Tabla, h.Columna)
|
|
}
|
|
}
|
|
|
|
// VerificarUrlsDeSubmodulos avisa en el arranque de los ítems del menú cuya
|
|
// URL no corresponde a ninguna ruta registrada.
|
|
//
|
|
// Un submódulo es una URL escrita a mano (en un seed o desde /app/submodules).
|
|
// Si no coincide con una ruta real, el ítem igual aparece en el menú y se le
|
|
// puede asignar permiso a un rol — y recién al hacer clic aparece un 404, sin
|
|
// nada que explique por qué. Esto lo convierte en una línea de log al
|
|
// arrancar, en vez de un misterio en producción.
|
|
//
|
|
// Solo informa: no borra ni modifica nada, porque una URL "rota" puede ser una
|
|
// ruta servida por otro lado.
|
|
func VerificarUrlsDeSubmodulos(rutasGET map[string]bool) {
|
|
var subs []models.Submodules
|
|
if err := app.Http.Database.DB.Find(&subs).Error; err != nil {
|
|
log.Printf("[MENU] no se pudieron revisar las URLs de los submódulos: %v", err)
|
|
return
|
|
}
|
|
|
|
rotos := 0
|
|
for _, s := range subs {
|
|
url := strings.TrimSpace(s.Url)
|
|
if url == "" {
|
|
log.Printf("[MENU] el submódulo %q (ID %d) no tiene URL", s.Title, s.ID)
|
|
rotos++
|
|
continue
|
|
}
|
|
if !rutasGET[url] {
|
|
log.Printf("[MENU] el submódulo %q (ID %d) apunta a %q, que no es ninguna ruta: va a dar 404", s.Title, s.ID, url)
|
|
rotos++
|
|
}
|
|
}
|
|
if rotos == 0 {
|
|
log.Printf("[MENU] %d submódulos revisados, todas las URLs resuelven", len(subs))
|
|
} else {
|
|
log.Printf("[MENU] %d de %d submódulos apuntan a una URL inexistente (ver líneas anteriores)", rotos, len(subs))
|
|
}
|
|
}
|
|
|
|
// SeedDocumentacion registra los submódulos de Documentación.
|
|
//
|
|
// Existían las rutas y las vistas pero nunca el seed, así que había que crear
|
|
// el ítem del menú a mano desde /app/submodules — y una URL mal tipeada ahí es
|
|
// indistinguible de un permiso mal asignado: en los dos casos se ve un 404.
|
|
func SeedDocumentacion() {
|
|
db := app.Http.Database.DB
|
|
|
|
var modulo models.Modules
|
|
if err := db.Where("title = ?", "Documentación").First(&modulo).Error; err != nil {
|
|
modulo = models.Modules{
|
|
Title: "Documentación",
|
|
Description: "Páginas y categorías de documentación interna",
|
|
ModifiedAt: time.Now(),
|
|
}
|
|
if err := db.Create(&modulo).Error; err != nil {
|
|
log.Printf("[SEED] Error creando módulo Documentación: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
entries := []struct{ title, desc, url string }{
|
|
{"Doc: Páginas", "Páginas de documentación", "/app/doc/paginas"},
|
|
{"Doc: Categorías", "Categorías de documentación", "/app/doc/categorias"},
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// IndicesUnicosMessageID crea índices únicos parciales sobre el Message-Id de
|
|
// los correos ya procesados. Es lo que hace que un mismo correo no pueda abrir
|
|
// dos tickets pase lo que pase: si el buzón vuelve a entregarlo, si el flag de
|
|
// leído no se guardó, o si dos instancias de la app lo leen a la vez, la base
|
|
// rechaza el segundo.
|
|
//
|
|
// Parcial (WHERE message_id <> ”) porque los tickets del portal no tienen
|
|
// Message-Id y todos comparten la cadena vacía.
|
|
func IndicesUnicosMessageID() {
|
|
db := app.Http.Database.DB
|
|
indices := []struct{ nombre, tabla string }{
|
|
{"idx_tickets_message_id_unico", "proyecto_tickets"},
|
|
{"idx_ticket_mensajes_message_id_unico", "ticket_mensajes"},
|
|
}
|
|
for _, ix := range indices {
|
|
var existe bool
|
|
if err := db.Raw(
|
|
`SELECT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = ?)`, ix.nombre,
|
|
).Scan(&existe).Error; err != nil || existe {
|
|
continue
|
|
}
|
|
// Si ya hay duplicados de antes, el índice no se puede crear. Se avisa y
|
|
// se sigue: el chequeo previo en código igual filtra la mayoría.
|
|
sql := fmt.Sprintf(
|
|
`CREATE UNIQUE INDEX %s ON %s (message_id) WHERE message_id <> '' AND deleted_at IS NULL`,
|
|
ix.nombre, ix.tabla)
|
|
if err := db.Exec(sql).Error; err != nil {
|
|
log.Printf("[MIGRATE] No se pudo crear %s (¿hay correos duplicados de antes?): %v", ix.nombre, err)
|
|
continue
|
|
}
|
|
log.Printf("[MIGRATE] Índice %s creado.", ix.nombre)
|
|
}
|
|
}
|