Skip to main content

Adjuntos de tickets

Los tickets de soporte pueden llevar archivos adjuntos (fotos de incidencias, facturas y presupuestos en PDF, etc.). A diferencia de los adjuntos de Zendesk, estos viven en nuestro Cloudinary y son la fuente de verdad propia: cuando apaguemos Zendesk, los archivos siguen siendo nuestros.

Modelo

Los adjuntos se asocian al ticket completo, no a un comentario concreto. Se almacenan en la tabla chat_ticket_attachments y los binarios en Cloudinary, dentro del folder chat/tickets/{nº} (usa el número de Zendesk cuando existe; si no, el id del ticket). Acepta imágenes, vídeos y otros tipos de archivo (PDF, documentos…). Las imágenes se re-codifican a WebP optimizado antes de subirse a Cloudinary; el resto de archivos (vídeo incluido) se suben tal cual — Cloudinary usa resource_type='auto' y los clasifica como video/raw según corresponda.
ColumnaDescripción
idUUID, PK
ticket_idFK a chat_tickets (ON DELETE CASCADE)
cloudinary_public_idID del asset en Cloudinary (para borrarlo después)
resource_typeimage | raw | video — namespace de Cloudinary
urlsecure_url de Cloudinary
filenameNombre original del archivo
content_typeMIME type
size / width / heightMetadatos del archivo
sourceOrigen del adjunto (ver abajo)
uploaded_byFK a users — quién lo subió (solo en upload)
zendesk_attachment_idID del adjunto de Zendesk re-alojado (idempotencia del import)
zendesk_upload_tokenToken devuelto por el write-back a Zendesk (trazabilidad)
metadataJSONB libre
created_atTimestamp de creación
Un índice único parcial sobre zendesk_attachment_id garantiza que un adjunto de Zendesk se re-aloje una sola vez.

Orígenes (source)

sourceCómo llega
uploadSubida manual desde Vivla Tools (al crear el ticket o desde el detalle)
chatImagen de un mensaje de chat adjuntada al ticket y re-alojada en Cloudinary
zendesk_importRe-alojado desde un adjunto de Zendesk
Los orígenes chat y zendesk_import entran por el método importFromUrl(): descarga el archivo desde una URL externa (con Authorization si hace falta), lo sube a nuestro Cloudinary y crea la fila. Es best-effort e idempotente por zendesk_attachment_id (no rompe el import/sync si falla).

Endpoints

Bajo chat/tickets/:id/attachments. Todos requieren Auth0 + tool-chat (rol viewer).
MétodoRutaDescripción
GET/chat/tickets/:id/attachmentsLista los adjuntos del ticket (orden por created_at asc)
POST/chat/tickets/:id/attachmentsSube uno o varios archivos (campo files, multipart)
DELETE/chat/tickets/:id/attachments/:attachmentIdBorra un adjunto (Cloudinary + fila)
El POST acepta hasta 10 archivos de 50 MB cada uno. El borrado elimina primero el asset de Cloudinary (best-effort, según resource_type) y luego la fila.

Bidireccional con Zendesk

Al subir desde Tools, además de alojarlo en Cloudinary se hace write-back a Zendesk para que el archivo no quede huérfano durante la transición:
  1. Se sube el archivo original (no el WebP optimizado) a Zendesk vía uploadAttachment, que devuelve un token.
  2. Tras procesar todo el lote, se crea una única nota interna (no pública) en el ticket de Zendesk con todos los tokens del lote:
📎 N archivo(s) adjuntado(s) desde Vivla Tools
El write-back solo ocurre si el ticket tiene zendesk_ticket_id, y es best-effort: si Zendesk falla, el adjunto se guarda igual en Cloudinary (solo se loguea el warning).

Proxy de adjuntos legacy de Zendesk

Los adjuntos viejos que viven en comentarios de Zendesk apuntan a URLs del tipo https://vivlahelp.zendesk.com/attachments/token/.../, que tienen dos problemas para mostrarlos en el navegador:
  • Requieren Basic auth: puestas directas en un <img> devuelven 401.
  • Las subidas de iOS llegan en HEIC/HEIF, que Chrome/Firefox no renderizan inline.
El controlador ZendeskAttachmentProxyController (GET /chat/zendesk-attachments/proxy?url=…) resuelve ambos:
  1. Valida que la URL sea https y del host de Zendesk configurado (anti-SSRF).
  2. Descarga con nuestra Basic auth vía ZendeskProvider.
  3. Si el archivo es HEIC/HEIF, lo transcodifica a JPEG con sharp (q85). Si libheif no está disponible en el servidor, devuelve 415 para que el frontend caiga a un enlace de descarga.
  4. Devuelve los bytes con Cache-Control: private, max-age=3600.
Requiere la misma auth que el resto de endpoints de tickets (Auth0 + tool-chat viewer). No es un proxy público.

UI

El componente TicketAttachments (frontend) muestra una galería en el detalle del ticket:
  • Drag & drop, pegar (⌘/Ctrl+V) o botón Subir (acepta image/*, video/* y application/pdf, máx. 50 MB).
  • Preview en miniatura para imágenes; chip con icono y nombre de archivo para PDFs.
  • Botón de borrar por adjunto (visible al hover).
  • Cada tile enlaza al archivo en una pestaña nueva y muestra su tamaño.
También hay una caja de subida en el formulario de alta del ticket. El copy lo deja claro: “Se guardan en el ticket y se sincronizan con Zendesk.”

Pegar desde el portapapeles

Como en Zendesk, se puede pegar una captura o imagen del portapapeles con ⌘/Ctrl+V y se adjunta directamente, sin pasar por el selector de archivos. Funciona tanto en el modal de alta como en la sección de adjuntos del detalle, y comparte la misma validación de tipo/tamaño que el drag & drop (mismo ACCEPT y límite de 50 MB). Detalles de comportamiento (hook usePasteFiles):
  • Solo actúa sobre archivos del portapapeles (clipboardData con kind === 'file'); un pegado de solo texto sigue su curso normal.
  • Filtra por mimetype (image/*, video/*, application/pdf) y por los 50 MB; descarta en silencio lo que no encaje.
  • Renombra las capturas con nombre genérico del portapapeles (image.png) a captura-<yyyyMMdd-HHmmss>.<ext> para que el filename mostrado en Zendesk/DB sea reconocible. Un archivo con nombre propio se respeta.
  • En el modal, con el diálogo abierto pegar desde cualquier campo adjunta la imagen. En el detalle (página completa con otros campos de texto) el pegado se ignora si el foco está en un input/textarea/contenteditable, para no subir un adjunto por sorpresa mientras se escribe.

Follow-ups

  • Re-hosting de adjuntos del import de Zendesk (source='zendesk_import'): gateado a comentarios nuevos que entran vía webhook. Pendiente.
  • Backfill de los adjuntos viejos ya existentes en Zendesk. Pendiente.