Saltar a contenido

SDD — Bot CDS · Mesa de ayuda a estaciones

Ficha técnica

Campo Valor
Repositorio / carpeta salesiq/bot-cds/
Plataforma Zoho SalesIQ · Zobot con código Deluge
Disparo Mensaje entrante por WhatsApp
Código flujos/message_handler.dg · flujos/context_handler.dg · flujos/error_handler.dg
Integraciones Kira2, Convenia, Zoho Sheet
Proceso de negocio PDD

Cómo usar este documento

Aquí se describe la estructura del bot y cómo aplicar los cambios que se piden con frecuencia: agregar estaciones, cambiar seguidores o encargados, y agregar opciones. El inventario actual (qué estaciones, qué opciones, qué seguidores) está en el código, que es la fuente de verdad. Este documento solo se actualiza si cambia la estructura: un área nueva, una integración nueva o un tipo de salida nuevo.

1. Arquitectura general

flowchart LR
    WA[WhatsApp] --> SIQ[Zoho SalesIQ<br/>Zobot Deluge]
    SIQ -->|service-login · crear-ticket<br/>send-file · editar-ticket<br/>cumplimientos| K[Kira2 API]
    SIQ -->|login · depositos · cierres-pos<br/>imagen-depositos-cierres| C[Convenia API]
    SIQ -->|createRecords| S[Zoho Sheet<br/>etiquetascds]
    SIQ -->|forward| AG[Agentes SalesIQ]

El bot no tiene servidor propio: toda la lógica vive en los tres handlers de SalesIQ y se ejecuta en la nube de Zoho en cada mensaje. No guarda estado fuera de la conversación; el avance de cada flujo se reconstruye en cada turno a partir de context_id y del mapa answers (ver sección 6).

2. Componentes

Handler Responsabilidad
message_handler Punto de entrada. Siempre responde con el menú principal y abre el contexto menu_principal
context_handler Toda la lógica: enrutamiento por área, selección de región y estación, preguntas, llamadas a APIs y bitácora
error_handler Ante cualquier excepción no controlada responde "inconveniente técnico" y hace forward a un agente

El context_handler arranca con un bloque global que intercepta "Ir al menú principal", y después tiene un bloque if / else if(context_id.equals("…")) por área:

context_id Área ¿Pide estación?
menu_principal Menú de áreas y "Ver más opciones" No
soporte_it Soporte IT Sí
grh Recursos Humanos Sí
cr Cierres Sí
mv Manejo de Valores Sí
cc Control de Calidad Sí
lg Legal Sí
cdc Cartera de clientes Sí
compras Compras Sí
cds Contactar con CDS Sí
sso SSO Sí
conta Contabilidad Sí
th Tiendas: imágenes a Convenia No
rino Plan Rino Referidos No

Los códigos de contexto no coinciden con el nombre del área

cr es Cierres (no Cartera) y cdc es Cartera de clientes (no Control de Calidad, que es cc). Para ubicarte en el archivo, busca context_id.equals("<id>").

3. Flujo detallado

3.1 Esqueleto de un contexto de área

Todos los contextos que piden estación tienen exactamente el mismo esqueleto, copiado en cada uno:

  1. Lee region y estacion de answers.
  2. Declara los catálogos (sección 6.1): regionIdMap, estMap, stIdMap y userIdMap.
  3. Si la estación ya viene elegida, deduce la región recorriendo estMap.
  4. Calcula region_id, estacion_id y usuario_kira = ifnull(userIdMap.get(estacion),"740").
  5. Si falta la región, muestra un select con las regiones en dos páginas.
  6. Si falta la estación, muestra un select con las estaciones de la región. Con más de 9, pagina sola: primeras 8 + ➡️ Ver más (<IdRegion>), y la segunda página se reconoce por ese prefijo. Siempre incluye ⬅️ Volver a regiones.
  7. Con región y estación resueltas, lee opcionnumerica2 y entra a la rama de la solicitud. Si un área tiene submenú (por ejemplo POS dentro de Soporte IT), la rama lee una segunda variable (opcion_pos, opcion_traslados…) con la misma lógica.

3.2 Recolección de datos

Cada solicitud pregunta un campo por turno, en orden, y pregunta el primero que falte. Hay dos estilos que conviven y funcionan igual:

  • Leer cada campo a una variable (x = ""; if(map != null) x = map.get("text")) y preguntar el primero vacío.
  • Encadenar if(!answers.containsKey("campo")) en el orden de las preguntas.

Para validar un dato, se elimina la respuesta con answers.remove("campo") y se vuelve a preguntar con un mensaje de error. Las respuestas siempre se leen del text, no del value.

3.3 Creación del ticket en Kira2

  1. POST /v1/auth/service-login con el usuario de servicio → token.
  2. Arma descripcion como una tabla HTML con los datos recogidos y al final agrega EmpleadoID: <id> por cada seguidor: los fijos del tipo de ticket más el encargado de la estación (usuario_kira).
  3. POST /v1/tareas/crear-ticket con Ticket (título), Descripcion, IdUsuario (creador), FechaInicio y FechaEntrega (hoy), IdPrioridad, IdCategoria, IdSubcategoria (si aplica), IdArea, IdRegion e IdEstacion.
  4. Lee IdTarea de la respuesta.
  5. Registra la fila en la bitácora (sección 8).
  6. Responde con el número de ticket y hace forward a un agente.

Algunos tipos deciden IdCategoria o IdSubcategoria según una respuesta (por ejemplo, la forma de pago o la red del POS). En esos casos la variable se asigna con if justo antes del ticketParams.

3.4 Solicitudes con adjuntos

Las solicitudes que llevan varios archivos piden primero la cantidad y luego los reciben de uno en uno. Como Deluge no guarda variables entre turnos, el número de ticket se guarda en el nombre de la pregunta:

  1. El primer archivo se pide con el nombre img_1_tk0 (tk0 = aún no hay ticket).
  2. Al recibirlo, se busca en answers la clave img_N_tk… con el índice más alto.
  3. Si el sufijo es 0, se crea el ticket. Si no, el sufijo es el IdTarea.
  4. Se sube el archivo con POST /v1/files/send-file (multipart).
  5. Se leen los adjuntos actuales con GET /v1/tareas/<id>?Detalle=true.
  6. Se reenvía la lista completa (anteriores + nuevo) con PUT /v1/tareas/editar-ticket/<id>, porque la API reemplaza la lista en lugar de agregar.
  7. Si faltan archivos, se pide img_<N+1>_tk<IdTarea>; si no, se registra en la bitácora y se cierra con forward.

Formatos: imágenes (jpg, jpeg, png, gif, bmp) y pdf; según el flujo también docx o xlsx. Límite de 24.41 MB por archivo.

3.5 Cumplimiento en ticket existente

Para agregar información a un ticket que ya existe (por ejemplo, el número de TR en Manejo de Valores): pide el número de ticket, valida que sea numérico y hace PATCH /v1/cumplimientos-tareas/crear-multiples. Si CumplimientosCreados viene vacío, informa que el ID es incorrecto.

3.6 Imágenes a Convenia (th)

  1. Pide el número de corte (numero_corte_1, numero_corte_2… en cada reintento).
  2. POST /v1/zoho/login → token. La respuesta puede venir como objeto o como lista; el código acepta ambas.
  3. Consulta GET /v1/zoho/depositos/<corte> o GET /v1/zoho/cierres-pos/<corte>.
  4. Si es 404 o vacío, vuelve a pedir el corte. Si hay resultados, muestra hasta 10 en un select con monto, boleta y usuario.
  5. Pide una imagen y la envía a POST /v1/zoho/imagen-depositos-cierres como multipart con data = {"tipo": 1|2, "id": <DepositoID>}. Tipo 1 = depósito o transferencia, tipo 2 = cierre POS.
  6. Termina con end (sin agente).

4. Sistemas y dependencias

Sistema Uso Acceso Notas
Kira2 Crear tickets, adjuntar archivos, agregar cumplimientos API REST kira2-api.somosdonarturo.com/v1 Login en cada flujo; no se reutiliza token
Convenia Consultar depósitos y cierres por corte, subir imagen API REST convenia-api.somosdonarturo.com/v1/zoho Solo contexto th
Zoho Sheet Bitácora de atenciones zoho.sheet.createRecords, conexión etiquetascds Una hoja por mes (yyyy-MM)
Archivos en Kira2 PDFs de consulta enviados como enlace (por ejemplo, Cartera Sinergia) URL fija en el texto de la pregunta Si se publica una versión nueva, hay que cambiar la URL en el código

5. Credenciales y configuración

Nombre Tipo Dónde se guarda
Usuario de servicio zoho en Kira2 Usuario y contraseña En texto plano dentro de context_handler, en cada bloque de login
Usuario de servicio zoho en Convenia Usuario y contraseña En texto plano dentro de context_handler
Conexión etiquetascds Conexión de Zoho Configurada en SalesIQ
Libro de bitácora ID de Zoho Sheet Fijo en el código, en cada registro de bitácora

Contraseña en el código y en el repositorio

La contraseña del usuario de servicio está escrita en el Deluge. Al copiar el código a flujos/ queda también en el historial de Git. En la copia del repositorio reemplázala por un marcador (<<KIRA2_PASSWORD>>) antes de cada commit. A mediano plazo conviene moverla a una conexión de Zoho o a una función con la credencial centralizada, y rotarla. Si ya se hizo commit con el valor real, rotarla es obligatorio.

6. Estados y datos persistentes

El bot no persiste nada propio. El estado vive en la sesión de SalesIQ:

Elemento Uso
context_id Área actual de la conversación
answers Todas las respuestas acumuladas, indexadas por el name de cada pregunta
img_<N>_tk<IdTarea> Índice del archivo y número de ticket en los flujos con adjuntos
numero_corte_<N> Intentos de número de corte en Convenia

6.1 Catálogos dentro del código

Mapa Contenido De dónde sale cada valor
regionIdMap Nombre de región → IdRegion Kira2
estMap Región → lista de estaciones, en el orden en que se muestran Nombre visible para el usuario
stIdMap Estación → IdEstacion Kira2
userIdMap Estación → id del encargado Kira2 (usuario del encargado)

El nombre de la estación es la llave que une los tres mapas: tiene que escribirse igual, con tildes y mayúsculas, en estMap, stIdMap y userIdMap. Estos mapas están copiados en cada contexto que pide estación.

7. Manejo de errores y reintentos

Tipo Ejemplo Comportamiento
Negocio Opción no reconocida en el menú Repite el menú principal
Negocio Dato con formato inválido Elimina la respuesta y vuelve a preguntar
Negocio Corte inexistente en Convenia Pide el corte otra vez con un nuevo name
Aplicación Kira2 no devuelve IdTarea Mensaje de error y forward
Aplicación Falla al escribir en Zoho Sheet Se captura con try/catch y solo se registra con info; no afecta al usuario
Aplicación Error no controlado (nulo, toLong() sobre vacío, etc.) Lo atrapa error_handler: mensaje genérico y forward
Infraestructura Login fallido en Kira2 o Convenia Mensaje "no se pudo autenticar" y forward (Kira2) o end (Convenia)

No hay reintentos automáticos contra las APIs: un fallo se resuelve pasando la conversación a un agente.

8. Monitoreo y notificaciones

Cada atención agrega una fila al libro de Zoho Sheet, en la hoja del mes (yyyy-MM), con las columnas Fecha, Bot ("CDS"), Area, Opcion, Visitante, Estación elegida, Región y Ticket. Es la fuente para las métricas del catálogo (atenciones por área y opción) y también el inventario real de qué opciones se usan.

Los mensajes info (LOG_ERROR, CAMBIO_PC …) solo se ven en los registros de ejecución de SalesIQ.

9. Cambios frecuentes

Estas son las solicitudes que llegan por ticket. Al terminar cualquiera de ellas, aplica siempre la sección 9.6.

9.1 Agregar una estación

Datos que necesitas: nombre visible, región, IdEstacion y id del encargado en Kira2.

  1. En cada contexto que pide estación, agrega el nombre a la lista de su región en estMap, en la posición en que debe aparecer.
  2. En los mismos contextos, agrega stIdMap.put("<nombre>","<IdEstacion>") y userIdMap.put("<nombre>","<id encargado>").
  3. La paginación se ajusta sola. No hay que tocar nada más.

Si falta en un mapa

Sin stIdMap, estacion_id.toLong() falla y la conversación cae al error_handler sin crear ticket. Sin userIdMap, el ticket se crea pero el seguidor queda como el usuario por defecto 740. Al terminar, busca el nombre en el archivo: debe aparecer tres veces por contexto.

Renombrar o dar de baja una estación es lo mismo a la inversa, en los tres mapas y en todos los contextos.

9.2 Cambiar el encargado de una estación

Cambia el valor en userIdMap en todos los contextos. Busca userIdMap.put("<estación>" para ubicarlos.

9.3 Agregar o quitar un seguidor de un tipo de ticket

  1. Busca el título del ticket (ticketParams.put("Ticket","<título>")).
  2. Unas líneas arriba está la asignación de descripcion. Al final de esa línea están los EmpleadoID: <id>: agrega o quita el que corresponda, separado por un espacio.
  3. No quites EmpleadoID: " + usuario_kira: es el encargado de la estación.

En los flujos con adjuntos, la descripción se arma dentro del bloque if(ticket_tag.equals("0")).

9.4 Agregar una opción a un área existente

Datos que necesitas: texto del botón, preguntas en orden, título del ticket, IdArea, IdCategoria, IdSubcategoria (si aplica), seguidores y si lleva adjuntos.

  1. Botón. La lista de sugerencias del área aparece en dos lugares: en la rama del área dentro de menu_principal y en la primera entrada del contexto (if(opcionnumerica2.equals(""))). Agrega el botón en ambas, antes de "Ir al menú principal". Si la opción va dentro de un submenú, el botón va en la lista de ese submenú.
  2. Rama. Agrega un else if(opcionnumerica2.equalsIgnoreCase("<texto del botón>")) copiando la rama de una opción del mismo tipo:

    Tipo de salida Copia una rama como
    Ticket sin adjuntos "Fallo en impresora" (Soporte IT)
    Ticket con varios adjuntos "Requerimientos MEM" (Legal)
    Traspaso a agente sin ticket "Error en mangueras" (Cierres)
    Cumplimiento en ticket existente "Registro de TR" (Manejo de Valores)
  3. Ajusta las preguntas (name único y en el orden deseado), la tabla de descripcion, los seguidores, el título y los ids de Kira2, y los campos Area y Opcion de la bitácora.

  4. Si la rama copiada tiene response.put("context_id","<área>"), cámbialo al contexto donde la estás pegando. Si no, la conversación salta al área de origen.

El texto del botón y el de la condición deben coincidir

El enrutamiento compara el texto visible, no el value. Un emoji o una palabra distinta hacen que la opción no entre a su rama y el usuario vuelva al menú sin error.

9.5 Agregar un área nueva

  1. Agrega el botón al menú principal o a "Ver más opciones" en menu_principal. El menú principal se repite en message_handler, en el bloque global de retorno y en las ramas de menu_principal que lo reconstruyen: agrégalo en todas.
  2. Agrega su rama en menu_principal que abre el nuevo context_id con sus opciones.
  3. Crea el bloque else if(context_id.equals("<nuevo>")) copiando completo uno que pida estación (por ejemplo conta, que tiene una sola opción) y cambia el context_id en todas sus apariciones dentro del bloque.
  4. Este es un cambio de estructura: agrega el área a la tabla de la sección 2 y al catálogo del PDD.

9.6 Después de cualquier cambio

  1. Prueba el flujo en SalesIQ hasta ver el ticket en Kira2 con sus seguidores.
  2. Copia los handlers a flujos/, reemplaza la contraseña por el marcador y actualiza sincronizado_el en meta.yml.
  3. Haz commit con el número de ticket del requerimiento en el mensaje, por ejemplo bot-cds: agrega estación X (KIRA 12345). El historial de Git es la bitácora de cambios del bot; este documento no se toca.

10. Restricciones conocidas y trampas

Catálogos y bloques copiados

Los mapas de estaciones, el login, la subida de archivos y la bitácora están copiados en cada contexto. Un cambio en cualquiera de ellos (una estación, la URL de Kira2, una columna de la bitácora) hay que replicarlo en todas las copias. Las copias ya tienen pequeñas diferencias entre sí (por ejemplo, el orden de algunas estaciones), así que al copiar un bloque conviene tomarlo del contexto más reciente. Si algún día se refactoriza, el primer candidato es mover catálogos y login a funciones de Deluge.

Después del ticket, la conversación también pasa a un agente

Los flujos que crean ticket terminan con forward. Esas conversaciones entran a la cola de SalesIQ aunque la solicitud ya esté en Kira2, y las que nadie toma terminan como chats perdidos. [confirmar si es intencional o si debería ser end]

Bitácora en opciones sin ticket

Las ramas de traspaso a agente escriben idTarea en la bitácora aunque en esa rama no se crea ticket. Está dentro del try/catch y no afecta al usuario, pero la columna Ticket de esas filas no es confiable. Al copiar una rama de traspaso, puedes quitar esa línea.

Depósito y transferencia son el mismo flujo en Convenia

En th, "Imagen depósitos" e "Imagen transferencia" consultan el mismo endpoint y envían el mismo tipo: 1; solo cambian los textos. [confirmar con Convenia]

Nombre de la opción en el menú

La opción de imágenes de Convenia se muestra como "Tiendas/Hoteles 🏨" y el código compara contra ese texto exacto.

11. Operación (runbook)

El bot responde con 'Tuvimos un inconveniente técnico'
  1. Es el error_handler. Revisa en SalesIQ los registros de ejecución del Zobot para ver la línea que falló.
  2. Si ocurre solo con una estación, revisa que esté en stIdMap de ese contexto con el mismo nombre exacto que en estMap.
  3. Si ocurre en todas las opciones, revisa si cambió la contraseña del usuario de servicio en Kira2 o si la API está caída.
Los tickets se crean pero no le llega al encargado
  1. Busca la estación en userIdMap del contexto. Si no está, se usó el 740.
  2. Verifica que el id siga siendo válido en Kira2.
Un botón dejó de funcionar y el bot regresa al menú

Compara el texto de la sugerencia con el de la condición equalsIgnoreCase. Un emoji o una palabra distinta es suficiente para que no coincida.

Falla el registro de imágenes en Convenia
  1. Verifica el login (/v1/zoho/login) y que el corte exista en Convenia.
  2. Si el corte tiene más de 10 depósitos, solo se muestran los primeros 10.

12. Historial de cambios

Solo cambios de estructura. Los cambios de contenido (estaciones, opciones, seguidores) quedan en el historial de Git de flujos/.

Versión Fecha Cambio Autor
1.0 2026-10-07 Documento inicial a partir del código Angel