feat(soporte): el ticket de correo ya sabe de quién es, y llega con prioridad
Un ticket que entraba por correo guardaba la dirección como texto suelto: no se podía cruzar con nada, el cliente no lo veía en su portal y no había forma de pedir "todos los tickets de Acme". Ahora al crearlo se resuelve el remitente — primero contra los usuarios del portal, si no contra el correo del cliente— y el ticket queda atado a quien escribió. El que no matchea queda como contacto externo, que también es información. Solo coincidencia exacta de dirección, nunca por dominio: con gmail.com de por medio, adivinar ata el ticket al cliente equivocado. Y la llamada de IA que ya clasificaba el correo ahora devuelve también prioridad y categoría en el mismo JSON: sin costo ni latencia extra. El campo prioridad existía desde siempre y nadie lo llenaba. La urgencia la define el problema descrito, no el tono del mensaje — está dicho en el prompt. En la lista de tickets se ven los tres datos nuevos: cliente (o "externo"), categoría y la prioridad que ya se mostraba. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6afad025c2
commit
bfcc320b7c
@@ -89,16 +89,6 @@ func IngestarCorreoSoporte(cfg *models.SoporteWebhookConfig, e CorreoSoporte) bo
|
||||
return true
|
||||
}
|
||||
|
||||
// Filtro con IA: solo para tickets nuevos. Una respuesta a un hilo ya
|
||||
// existente es soporte por definición, no hace falta preguntárselo a nadie.
|
||||
if cfg != nil && cfg.ClasificarConIA {
|
||||
if cl := ClasificarCorreoSoporte(e, cfg.ContextoNegocio); !cl.EsSoporte {
|
||||
log.Printf("[Soporte] Descartado por el filtro (%s): %q de %s — %s",
|
||||
cl.Categoria, e.Subject, fromEmail, cl.Motivo)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
ticket := &models.ProyectoTicket{
|
||||
AutorNombre: fromName,
|
||||
EmailFrom: fromEmail,
|
||||
@@ -108,6 +98,24 @@ func IngestarCorreoSoporte(cfg *models.SoporteWebhookConfig, e CorreoSoporte) bo
|
||||
Origen: "email",
|
||||
MessageID: e.MessageID,
|
||||
}
|
||||
|
||||
// Filtro y triage con IA: solo para tickets nuevos. Una respuesta a un hilo
|
||||
// ya existente es soporte por definición, no hace falta preguntárselo a nadie.
|
||||
if cfg != nil && cfg.ClasificarConIA {
|
||||
cl := ClasificarCorreoSoporte(e, cfg.ContextoNegocio)
|
||||
if !cl.EsSoporte {
|
||||
log.Printf("[Soporte] Descartado por el filtro (%s): %q de %s — %s",
|
||||
cl.Categoria, e.Subject, fromEmail, cl.Motivo)
|
||||
return false
|
||||
}
|
||||
ticket.Categoria = cl.Categoria
|
||||
ticket.Prioridad = cl.Prioridad
|
||||
}
|
||||
|
||||
// De quién es: sin esto un ticket de correo es un texto suelto que no se
|
||||
// puede cruzar con nada. Si no matchea nadie, queda como contacto externo.
|
||||
resolverRemitente(ticket, fromEmail)
|
||||
|
||||
if cfg != nil && cfg.AsignarA != nil {
|
||||
ticket.AsignadoA = cfg.AsignarA
|
||||
}
|
||||
@@ -242,3 +250,25 @@ func esCorreoDuplicado(err error) bool {
|
||||
}
|
||||
return strings.Contains(strings.ToLower(err.Error()), "duplicate key value")
|
||||
}
|
||||
|
||||
// resolverRemitente ata el ticket a quien escribió: primero por usuario del
|
||||
// portal (que además le deja ver el ticket desde su portal) y si no, por el
|
||||
// correo del cliente. Solo coincidencia exacta de dirección — ver el comentario
|
||||
// de models.GetClientePorEmail sobre por qué no se busca por dominio.
|
||||
func resolverRemitente(ticket *models.ProyectoTicket, email string) {
|
||||
if email == "" {
|
||||
return
|
||||
}
|
||||
if pu, err := models.GetPortalUserByEmail(email); err == nil && pu != nil {
|
||||
id := pu.ID
|
||||
ticket.PortalUserID = &id
|
||||
if ids := models.GetClienteIDsForPortalUser(pu); len(ids) == 1 {
|
||||
ticket.ClienteID = &ids[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
if cli, err := models.GetClientePorEmail(email); err == nil && cli != nil {
|
||||
id := cli.ID
|
||||
ticket.ClienteID = &id
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user