Commit Graph
6 Commits
Author SHA1 Message Date
Lizandro GuarnizoandClaude Opus 5 4674413257 fix(soporte): el filtro se estaba comiendo correos nuevos, y nadie se enteraba
Una respuesta a un hilo existente saltea el clasificador; un correo nuevo no.
Por eso el sistema parecía funcionar solo con las respuestas: los correos
nuevos que el modelo consideraba "no soporte" desaparecían sin ticket, sin
acuse al cliente y sin más rastro que un renglón de log que nadie mira.

Dos cambios:

- El filtro solo opina sobre desconocidos. Si el remitente es un cliente o un
  usuario del portal, siempre se abre el ticket. Para eso la resolución del
  remitente pasa a correr antes del filtro. A un cliente registrado no se le
  descarta el correo por lo que diga un modelo.

- Lo descartado se ve. "Revisar buzón ahora" informa cuántos dejó afuera el
  filtro, con asunto, remitente y motivo. Guardado en memoria: es diagnóstico
  de hace un rato, no algo que valga una tabla.

Además, si la respuesta automática está apagada, el log lo dice al crear el
ticket — era la otra explicación posible de "no me contestó" y no se distinguía.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:56:32 -05:00
Lizandro GuarnizoandClaude Opus 5 bfcc320b7c 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>
2026-08-17 19:43:28 -05:00
Lizandro GuarnizoandClaude Opus 5 d7c266f111 feat(soporte): filtro con IA para los correos entrantes, y que un correo no pueda abrir dos tickets
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>
2026-08-17 19:36:01 -05:00
Lizandro GuarnizoandClaude Opus 5 5875f9e36b fix(soporte): el buzón IMAP nunca se leía — la búsqueda pedía UIDs a una búsqueda por secuencia
Usé c.Search, que devuelve números de secuencia, y después le pedí los UIDs al
resultado. go-imap devuelve nil ahí, así que la lista salía siempre vacía: el
cron conectaba bien, buscaba bien, y se iba sin leer nada ni escribir un solo
error en el log. Va con UIDSearch.

Con eso se arreglan las dos cosas: no leía y no creaba tickets.

Además:
- Fetch con Peek: el correo queda marcado como leído solo si se llegó a
  procesar. Antes, un fallo a mitad de camino lo perdía para siempre.
- "Probar conexión" ahora informa cuántos mensajes hay en la carpeta y cuántos
  sin leer; "Revisar buzón ahora" informa cuántos encontró y cuántos convirtió
  en ticket, y avisa si la lectura está desactivada.
- Un correo sin asunto abre ticket como "(sin asunto)" en vez de descartarse en
  silencio, y los descartes quedan en el log.

Los tres tests nuevos corren contra un servidor IMAP en memoria: bajar los no
leídos, que marcar como leído los saque de la próxima corrida, y que bajarlos
sin procesarlos no los marque. El primero falla si se vuelve a poner Search.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:20:27 -05:00
Lizandro GuarnizoandClaude Opus 5 d1bbb4a508 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>
2026-08-17 19:12:36 -05:00
Lizandro GuarnizoandClaude Opus 5 8c3ab79e88 feat(soporte): leer el buzón por IMAP, no solo esperar el webhook
Los clientes escriben a soporte@ desde su correo de siempre. Hasta ahora eso
solo llegaba si un proveedor (SendGrid/Mailgun) nos hacía POST; si nadie lo
configuraba, los correos quedaban sin leer en el buzón.

Ahora el cron entra al buzón cada 2 minutos, baja los no leídos, abre ticket
(o los engancha al hilo si son respuesta) y los marca como leídos. La lógica
de ingesta se movió a services para que webhook e IMAP se comporten igual.

La contraseña del buzón se guarda cifrada (AES-GCM con APP_KEY) y no vuelve
al navegador.

De paso, dos bugs que impedían guardar la configuración: el formulario mandaba
id=0 (gorm.Model serializa "ID"), así que cada guardado creaba una fila nueva
en vez de editar la que se usa; y Updates con struct ignoraba los booleanos en
false, así que desactivar algo no tenía efecto.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:36:08 -05:00