feat(soporte): avisar por Telegram cuando entra un correo, con resumen si es largo
Los tickets del portal ya avisaban por Telegram; los que entran por correo solo mandaban un mail al admin, que es justo el canal que nadie mira a tiempo. Ahora pasan por el mismo despachador de notificaciones, así que se prenden y apagan desde /app/notif-config como cualquier otro evento — nuevo ticket y respuesta del cliente por separado. El cuerpo del aviso es un resumen cuando el correo pasa los 700 caracteres (hilos reenviados, texto pegado); abajo de eso va tal cual, porque resumir tres renglones es gastar una llamada de IA para decir lo mismo. Si la IA falla, se manda recortado. El ticket guarda siempre el texto completo. DispatchTicketNuevo ahora acepta portalUser nil: un correo no tiene usuario de portal detrás y no por eso hay que dejar de avisar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ac47769c27
commit
d1bbb4a508
@@ -10,10 +10,13 @@ import (
|
||||
|
||||
// ─── DispatchTicketNuevo ──────────────────────────────────────────────────────
|
||||
// Notifica al admin cuando un portal user crea un ticket.
|
||||
// portalUser puede ser nil: los tickets que entran por correo no tienen usuario
|
||||
// de portal detrás, y no por eso hay que dejar de avisar.
|
||||
func DispatchTicketNuevo(ticket *models.ProyectoTicket, portalUser *models.PortalUser, proyectoNombre string) {
|
||||
if ticket == nil || portalUser == nil {
|
||||
if ticket == nil {
|
||||
return
|
||||
}
|
||||
_ = portalUser
|
||||
cfg := models.GetNotifConfig("ticket_nuevo", "admin")
|
||||
if cfg == nil {
|
||||
return
|
||||
|
||||
@@ -70,6 +70,7 @@ func IngestarCorreoSoporte(cfg *models.SoporteWebhookConfig, e CorreoSoporte) bo
|
||||
_ = models.UpdateTicketEstado(hilo.ID, "abierto")
|
||||
}
|
||||
log.Printf("[Soporte] Respuesta agregada al ticket #%d (%s)", hilo.ID, fromEmail)
|
||||
notificarTicketDeCorreo(hilo, contenido, true)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -91,7 +92,7 @@ func IngestarCorreoSoporte(cfg *models.SoporteWebhookConfig, e CorreoSoporte) bo
|
||||
}
|
||||
log.Printf("[Soporte] Ticket #%d creado desde email (%s): %s", ticket.ID, fromEmail, e.Subject)
|
||||
|
||||
SendSoporteNotifAdmin(ticket)
|
||||
notificarTicketDeCorreo(ticket, contenido, false)
|
||||
if cfg != nil && cfg.ResponderAuto {
|
||||
SendSoporteAutoRespuesta(ticket)
|
||||
}
|
||||
@@ -142,3 +143,60 @@ func ExtraerNombre(s string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// notificarTicketDeCorreo avisa al staff de un correo entrante por los canales
|
||||
// que estén prendidos en /app/notif-config (in-app, correo y Telegram), con el
|
||||
// mismo formato que los tickets del portal.
|
||||
//
|
||||
// El cuerpo que viaja en la notificación es el resumen, no el correo entero:
|
||||
// el ticket guarda el texto completo igual.
|
||||
func notificarTicketDeCorreo(ticket *models.ProyectoTicket, contenido string, esRespuesta bool) {
|
||||
resumen := ResumirTextoSoporte(contenido)
|
||||
origen := "Correo"
|
||||
if ticket.EmailFrom != "" {
|
||||
origen = "Correo · " + ticket.EmailFrom
|
||||
}
|
||||
|
||||
if esRespuesta {
|
||||
DispatchTicketRespuestaCliente(ticket, resumen, origen)
|
||||
return
|
||||
}
|
||||
|
||||
// Copia para no pisar el texto completo que ya se guardó en el ticket.
|
||||
paraNotificar := *ticket
|
||||
paraNotificar.Descripcion = resumen
|
||||
|
||||
if models.GetNotifConfig("ticket_nuevo", "admin") == nil {
|
||||
// Sin configuración de notificaciones se mantiene lo de siempre: correo
|
||||
// al admin. Si no, activar el módulo dejaría a alguien sin avisos.
|
||||
SendSoporteNotifAdmin(¶Notificar)
|
||||
return
|
||||
}
|
||||
DispatchTicketNuevo(¶Notificar, nil, origen)
|
||||
}
|
||||
|
||||
// ResumirTextoSoporte deja el correo en algo que se pueda leer de un vistazo en
|
||||
// Telegram. Los correos cortos van tal cual: pedirle a la IA que resuma tres
|
||||
// renglones es gastar una llamada para decir lo mismo. Los largos —hilos
|
||||
// reenviados, capturas pegadas— sí se resumen, y si la IA falla se recorta.
|
||||
func ResumirTextoSoporte(texto string) string {
|
||||
t := strings.TrimSpace(texto)
|
||||
if len([]rune(t)) <= 700 {
|
||||
return t
|
||||
}
|
||||
|
||||
resumen, err := CompletarTextoIA("ia",
|
||||
"Resumís correos de soporte para avisarle al equipo por Telegram. "+
|
||||
"Máximo 3 renglones. Decí qué pide o reporta la persona y, si los hay, "+
|
||||
"incluí datos concretos (número de factura, pedido, fecha, monto). "+
|
||||
"Sin saludos, sin despedidas, sin repetir el asunto, sin inventar nada.",
|
||||
t)
|
||||
if err != nil {
|
||||
log.Printf("[Soporte] No se pudo resumir el correo, se manda recortado: %v", err)
|
||||
return string([]rune(t)[:700]) + "…"
|
||||
}
|
||||
if r := strings.TrimSpace(resumen); r != "" {
|
||||
return r
|
||||
}
|
||||
return string([]rune(t)[:700]) + "…"
|
||||
}
|
||||
|
||||
@@ -78,3 +78,15 @@ func TestExtractEmailEdgeCases(t *testing.T) {
|
||||
t.Errorf("ExtraerEmail('<a><b@c.com>') = %q, want 'b@c.com'", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResumirTextoSoporteCortoNoLlamaALaIA(t *testing.T) {
|
||||
// Sin base de datos, cualquier llamada a la IA explota: que este caso pase
|
||||
// es la prueba de que un correo corto no la usa.
|
||||
corto := "Hola, no me llega la factura de marzo. Gracias."
|
||||
if got := ResumirTextoSoporte(corto); got != corto {
|
||||
t.Errorf("ResumirTextoSoporte devolvió %q, esperaba el texto tal cual", got)
|
||||
}
|
||||
if got := ResumirTextoSoporte(" " + corto + "\n"); got != corto {
|
||||
t.Errorf("no recortó los espacios: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user