Files
soft_usite/pkg/models/soporte_webhook.go
T
Lizandro GuarnizoandClaude Opus 5 1b599606f7 feat(soporte): botón de borrador que redacta con la base de conocimiento
En cada ticket, " Borrador" propone una respuesta usando los fragmentos más
parecidos de la base de conocimiento del agente de uMind que se elija en la
configuración de soporte, más el hilo de la conversación.

El borrador cae en el cuadro de respuesta y no se manda: lo revisa una persona
y aprieta enviar. Contestarle a un cliente con lo que dijo un modelo, sin que
nadie lo lea, es la forma más rápida de perderlo.

Sin agente configurado el borrador se arma igual, solo con la conversación —
peor, pero mejor que un error. El prompt le prohíbe inventar precios, plazos o
pasos que no estén en la documentación, y le pide decir qué falta preguntar
cuando no alcanza para resolver.

El cuadro de respuesta pasa a textarea: un borrador de varios renglones no
entraba en un input de una línea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:45:38 -05:00

83 lines
4.4 KiB
Go

package models
import (
"github.com/sujit-baniya/fiber-boilerplate/app"
"gorm.io/gorm"
)
type SoporteWebhookConfig struct {
gorm.Model
Activo bool `json:"activo" gorm:"column:activo;default:true"`
Nombre string `json:"nombre" gorm:"column:nombre;size:100"`
Provider string `json:"provider" gorm:"column:provider;size:50;default:'sendgrid'"` // sendgrid|mailgun|generic
ApiKey string `json:"api_key" gorm:"column:api_key;size:255"`
EmailDestino string `json:"email_destino" gorm:"column:email_destino;size:255"` // ej: soporte@u-s.app
ResponderAuto bool `json:"responder_auto" gorm:"column:responder_auto;default:true"`
MensajeAuto string `json:"mensaje_auto" gorm:"column:mensaje_auto;type:text"`
AsignarA *uint `json:"asignar_a" gorm:"column:asignar_a;index"` // auto-asignar tickets a este user
// SMTP salida para notificaciones y auto-respuesta
SmtpHost string `json:"smtp_host" gorm:"column:smtp_host;size:255"`
SmtpPort int `json:"smtp_port" gorm:"column:smtp_port;default:587"`
SmtpUsername string `json:"smtp_username" gorm:"column:smtp_username;size:255"`
SmtpPassword string `json:"smtp_password" gorm:"column:smtp_password;size:255"` // cifrado AES
SmtpEncryption string `json:"smtp_encryption" gorm:"column:smtp_encryption;size:20;default:'tls'"` // tls|starttls|none
SmtpFromAddr string `json:"smtp_from_addr" gorm:"column:smtp_from_addr;size:255"`
SmtpFromName string `json:"smtp_from_name" gorm:"column:smtp_from_name;size:255"`
// IMAP entrante: leer el buzón directamente en vez de depender de que un
// proveedor nos haga POST. Es lo único que hace falta para responderle a un
// cliente que escribe a soporte@ desde su correo de siempre.
ImapActivo bool `json:"imap_activo" gorm:"column:imap_activo;default:false"`
ImapHost string `json:"imap_host" gorm:"column:imap_host;size:255"`
ImapPort int `json:"imap_port" gorm:"column:imap_port;default:993"`
ImapUsername string `json:"imap_username" gorm:"column:imap_username;size:255"`
ImapPasswordEnc string `json:"-" gorm:"column:imap_password_enc;type:text"` // AES-GCM con APP_KEY
ImapEncryption string `json:"imap_encryption" gorm:"column:imap_encryption;size:20;default:'ssl'"` // ssl|starttls
ImapCarpeta string `json:"imap_carpeta" gorm:"column:imap_carpeta;size:100;default:'INBOX'"`
// Ventana hacia atrás, en horas: solo se leen los correos recibidos dentro
// de ella. 0 = sin límite (todo el buzón sin leer). Es lo que evita que la
// primera corrida se coma años de correo viejo.
ImapHorasAtras int `json:"imap_horas_atras" gorm:"column:imap_horas_atras;default:12"`
// Filtro con IA: no todo lo que llega al buzón es soporte (newsletters,
// notificaciones de bancos, facturas de proveedores). Si está prendido, se
// clasifica cada correo nuevo antes de abrir ticket.
ClasificarConIA bool `json:"clasificar_con_ia" gorm:"column:clasificar_con_ia;default:false"`
ContextoNegocio string `json:"contexto_negocio" gorm:"column:contexto_negocio;type:text"`
// Agente de uMind cuya base de conocimiento se usa para redactar borradores
// de respuesta. Sin agente el borrador se arma solo con la conversación.
AgenteBorradorID *uint `json:"agente_borrador_id" gorm:"column:agente_borrador_id;index"`
// Solo para la vista: dice si ya hay contraseña guardada sin exponerla, para
// que el formulario sepa que puede mandar el campo vacío sin borrarla.
TieneImapPassword bool `json:"tiene_imap_password" gorm:"-"`
}
func (SoporteWebhookConfig) TableName() string { return "soporte_webhook_config" }
func GetSoporteWebhookActivo() (*SoporteWebhookConfig, error) {
var item SoporteWebhookConfig
err := app.Http.Database.DB.Where("activo = ?", true).First(&item).Error
return &item, err
}
func GetAllSoporteWebhookConfigs() ([]SoporteWebhookConfig, error) {
var items []SoporteWebhookConfig
err := app.Http.Database.DB.Order("created_at DESC").Find(&items).Error
return items, err
}
func SaveSoporteWebhookConfig(s *SoporteWebhookConfig) error {
if s.ID > 0 {
// Select("*") para que los booleanos en false (activo, responder_auto,
// imap_activo) también se guarden: Updates con struct ignora los ceros,
// así que desactivar algo no tenía efecto.
return app.Http.Database.DB.Model(&SoporteWebhookConfig{}).
Where("id = ?", s.ID).
Select("*").Omit("id", "created_at", "deleted_at").
Updates(s).Error
}
return app.Http.Database.DB.Create(s).Error
}