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>
76 lines
3.9 KiB
Go
76 lines
3.9 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'"`
|
|
|
|
// 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"`
|
|
|
|
// 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
|
|
}
|