diff --git a/pkg/services/notif_dispatch.go b/pkg/services/notif_dispatch.go index dd26e9e..1938c99 100644 --- a/pkg/services/notif_dispatch.go +++ b/pkg/services/notif_dispatch.go @@ -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 diff --git a/pkg/services/soporte_ingesta_service.go b/pkg/services/soporte_ingesta_service.go index 2874dd9..0941f39 100644 --- a/pkg/services/soporte_ingesta_service.go +++ b/pkg/services/soporte_ingesta_service.go @@ -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]) + "…" +} diff --git a/pkg/services/soporte_ingesta_test.go b/pkg/services/soporte_ingesta_test.go index 9f9f74d..f8e915d 100644 --- a/pkg/services/soporte_ingesta_test.go +++ b/pkg/services/soporte_ingesta_test.go @@ -78,3 +78,15 @@ func TestExtractEmailEdgeCases(t *testing.T) { t.Errorf("ExtraerEmail('') = %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) + } +}