Files
soft_usite/pkg/services/umind_activacion_service.go
T
Lizandro GuarnizoandClaude Sonnet 5 8ef49c5169 feat: bucles de crecimiento y activación de uMind
Cuatro cosas que ya estaban a medio construir y no se usaban como canal.

1. El badge del widget ahora es un enlace con UTM. Cada cliente ya te
   estaba dando exposición en su sitio y no se capitalizaba. El host sale
   de la URL del propio script, así funciona igual en cualquier entorno.

2. Reporte del mes por agente en Excel: conversaciones atendidas, con qué
   las abrió el visitante, y el consumo desglosado. Es lo que el cliente
   necesita para justificar el gasto puertas adentro — un panel al que hay
   que entrar no sirve para eso, un archivo que se reenvía sí. Reusa el
   generador de xlsx del cronograma.

3. Aviso automático de agentes sin base de conocimiento (cron diario). Un
   agente sin fuentes responde de memoria e inventa datos, el cliente
   concluye que el producto no sirve y se va. Es el punto donde más gente
   se cae y se detecta solo. Se avisa UNA vez por agente, usando el log de
   auditoría como registro de envío para no necesitar tabla nueva ni
   convertir el recordatorio en spam.

4. Tarjeta de uMind en el dashboard del portal: atajo si el cliente ya lo
   tiene, oferta si no. Es el punto de contacto más barato que hay — ya
   entró, ya confía, y el cobro se suma a la factura que ya recibe. El
   mensaje lidera con notas de voz y fotos, que es lo más difícil de
   copiar de lo que tenemos.

La consulta de conversaciones agrupa en SQL en vez de traerse el historial
entero para agrupar en Go.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 22:40:28 -05:00

109 lines
4.7 KiB
Go

package services
import (
"fmt"
"log"
"time"
"github.com/sujit-baniya/fiber-boilerplate/app"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
// Un agente creado pero sin fuentes de conocimiento no le sirve a nadie:
// responde de memoria, inventa datos, y el cliente concluye que el producto no
// funciona. Es el punto donde más gente se cae, y se detecta solo — así que en
// vez de esperar a que alguien lo note, se avisa.
//
// El aviso se manda UNA sola vez por agente (UmindEventoLog hace de registro,
// sin tabla nueva): insistir sobre algo que el cliente ya decidió ignorar
// convierte un recordatorio útil en spam.
const origenActivacion = "activacion"
// diasParaAvisar es la espera antes del aviso. Lo suficiente para no molestar
// a quien está configurando el agente en ese momento.
const diasParaAvisar = 3
// AvisarAgentesSinConocimiento recorre los agentes activos que llevan varios
// días sin ninguna fuente cargada y le escribe al cliente dueño.
func AvisarAgentesSinConocimiento() {
db := app.Http.Database.DB
corte := time.Now().AddDate(0, 0, -diasParaAvisar)
var agentes []models.UmindAgente
if err := db.Where("activo = ? AND created_at < ? AND deleted_at IS NULL", true, corte).
Find(&agentes).Error; err != nil {
log.Printf("[ACTIVACION] no se pudieron listar los agentes: %v", err)
return
}
for _, a := range agentes {
var docs int64
db.Model(&models.UmindDocumento{}).
Where("agente_id = ? AND deleted_at IS NULL", a.ID).Count(&docs)
if docs > 0 {
continue
}
if yaSeAviso(a.ID) {
continue
}
if enviarAvisoSinConocimiento(&a) {
models.RegistrarEventoUmind(a.ID, "warn", origenActivacion,
"Se avisó al cliente que el agente no tiene base de conocimiento",
fmt.Sprintf("El agente lleva más de %d días activo sin ninguna fuente cargada.", diasParaAvisar))
}
}
}
// yaSeAviso usa el log de auditoría como registro de envío: el evento que
// deja enviarAvisoSinConocimiento es la marca de "ya se mandó".
func yaSeAviso(agenteID uint) bool {
var n int64
app.Http.Database.DB.Model(&models.UmindEventoLog{}).
Where("agente_id = ? AND origen = ?", agenteID, origenActivacion).Count(&n)
return n > 0
}
// enviarAvisoSinConocimiento devuelve true solo si el correo salió, para no
// marcar como avisado a alguien que nunca recibió nada.
func enviarAvisoSinConocimiento(a *models.UmindAgente) bool {
tenant, err := models.GetUmindTenantByID(a.TenantID)
if err != nil || tenant.ClienteID == nil {
return false // sin cliente asignado no hay a quién escribirle
}
destinatarios, err := models.GetPortalUsersByClienteID(*tenant.ClienteID)
if err != nil || len(destinatarios) == 0 {
return false
}
url := GetPublicURL() + "/portal/studio"
asunto := fmt.Sprintf("Tu asistente %q todavía no tiene información cargada", a.Nombre)
enviadoAlguno := false
for _, u := range destinatarios {
if u.Email == "" {
continue
}
html := fmt.Sprintf(`<!DOCTYPE html>
<html><body style="font-family:Inter,sans-serif;background:#f1f5f9;padding:32px">
<div style="max-width:520px;margin:0 auto;background:#fff;border-radius:16px;padding:32px;border:1px solid #e2e8f0">
<div style="text-align:center;margin-bottom:24px">
<div style="width:48px;height:48px;border-radius:50%%;background:#8eb02f;display:inline-flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:20px">U</div>
<h2 style="margin:12px 0 4px;color:#1e293b">Falta un paso para que tu asistente responda bien</h2>
<p style="color:#64748b;font-size:14px;margin:0">uMind Studio · U-site</p>
</div>
<p style="color:#334155;font-size:15px">Hola <strong>%s</strong>,</p>
<p style="color:#334155;font-size:14px">Tu asistente <strong>%s</strong> ya está creado, pero todavía no tiene ninguna fuente de información cargada.</p>
<p style="color:#334155;font-size:14px">Sin eso solo puede dar respuestas genéricas: no conoce tus precios, tus horarios ni tus servicios. Cargarle tu sitio web toma un minuto y es lo que hace la diferencia entre un chat que responde cualquier cosa y uno que de verdad atiende.</p>
<a href="%s" style="display:block;text-align:center;background:#8eb02f;color:#fff;padding:12px 24px;border-radius:10px;font-weight:600;text-decoration:none;font-size:15px;margin:24px 0">Cargar la información</a>
<p style="color:#64748b;font-size:13px">¿Preferís que lo hagamos nosotros? Respondé este correo con la dirección de tu sitio y lo dejamos listo.</p>
</div></body></html>`, u.Nombre, a.Nombre, url)
if err := app.Http.Mail.Send(u.Email, asunto, html); err != nil {
log.Printf("[ACTIVACION] no se pudo avisar a %s por el agente %d: %v", u.Email, a.ID, err)
continue
}
enviadoAlguno = true
}
return enviadoAlguno
}