Sin ventana, la primera corrida convierte en tickets todo lo que haya sin leer, que en una casilla de años es una avalancha. Ahora hay un desplegable de antigüedad máxima —6h, 12h, 24h, 3 días o todo— y por defecto 12 horas. El corte se hace en dos pasos porque IMAP no da para más: al servidor se le pide SINCE con un día de margen (SINCE compara solo la fecha, no la hora) y el corte fino por hora se aplica contra la fecha real de cada mensaje. Afinar el SINCE sería perder los correos del borde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80 lines
4.2 KiB
Go
80 lines
4.2 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"`
|
|
|
|
// 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
|
|
}
|