Files
CDC-Fulgencio-V2.0/Chat de WhatsApp - Propuestas Ejecutadas - Evolution API.json
T
2026-08-07 14:19:13 +00:00

10112 lines
484 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"name": "Chat de WhatsApp - Propuestas Ejecutadas - Evolution API",
"nodes": [
{
"parameters": {
"method": "POST",
"url": "={{'https://wsp.gomezleemarketing.com'}}/message/sendText/{{'botsoporte'}}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{'C267126ABB45-4C12-B626-6BAB1833F5D7'}}"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "number",
"value": "={{ (() => {\n const limpiar = (valor) => String(valor ?? '').trim();\n\n const candidatos = [\n $json.whatsapp_to,\n $json.group_jid,\n $json.whatsapp_remote_jid,\n $json.sesion_activa?.group_jid,\n $json.sesion_activa?.whatsapp_to,\n $json.sesion_activa?.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ];\n\n let raw = limpiar(candidatos.find(v => limpiar(v)));\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n // Si ya viene como grupo, conservarlo.\n if (raw.includes('@g.us')) {\n return raw;\n }\n\n // Si viene como usuario WhatsApp, validar si realmente era un grupo.\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n // Los grupos de WhatsApp suelen venir como 120363...\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n})() }}"
},
{
"name": "delay",
"value": "={{ 1000 }}"
},
{
"name": "text",
"value": "={{ $json.whatsapp_text || $json.telegram_text || $json.texto_respuesta || $json.mensaje || $json.text || 'Mensaje sin contenido' }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
15248,
1696
],
"id": "dad797ac-f70a-4550-a1bb-297d2f296dc4",
"name": "WhatsApp - Enviar mensaje TEST",
"disabled": true
},
{
"parameters": {
"jsCode": "const input = $json || {};\nconst body = input.body || input;\nconst data = body.data || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst limpiarNumero = (valor) =>\n texto(valor)\n .replace('@s.whatsapp.net', '')\n .replace('@c.us', '')\n .replace('@g.us', '')\n .replace('@lid', '')\n .replace(/\\D/g, '');\n\nconst key = data.key || {};\nconst message = data.message || {};\n\n// --------------------------------------------------\n// 1. Identificar origen real del mensaje\n// --------------------------------------------------\n\nconst remoteJid = texto(\n key.remoteJid ||\n data.remoteJid ||\n body.remoteJid ||\n ''\n);\n\nconst isGroup = remoteJid.endsWith('@g.us');\n\nconst participantRaw = texto(\n key.participant ||\n data.participant ||\n body.participant ||\n data.sender ||\n data.participantJid ||\n ''\n);\n\nconst participantAlt = texto(\n key.participantAlt ||\n data.participantAlt ||\n body.participantAlt ||\n data.senderAlt ||\n ''\n);\n\n// En grupos, Evolution puede mandar:\n// participant = 1655...@lid\n// participantAlt = número real@s.whatsapp.net\n// Para sesiones y permisos necesitamos preferir el número real.\nconst participantJid = participantAlt || participantRaw;\n\n// En grupo, el sender real es participantAlt si existe.\n// En chat individual, el sender real es remoteJid.\nconst senderJid = isGroup\n ? participantJid\n : remoteJid;\n\nconst senderPhone = limpiarNumero(senderJid);\nconst groupJid = isGroup ? remoteJid : '';\n\nconst pushName =\n texto(data.pushName) ||\n texto(body.pushName) ||\n texto(data.senderName) ||\n texto(body.senderName) ||\n 'Usuario WhatsApp';\n\n// El bot debe responder al grupo si el mensaje vino de grupo.\n// Si vino de chat individual, responde al usuario.\nconst whatsappTo = isGroup ? groupJid : remoteJid;\n\n// --------------------------------------------------\n// 2. Extraer texto o payload de botones\n// --------------------------------------------------\n\nconst buttonText =\n texto(message.buttonsResponseMessage?.selectedButtonId) ||\n texto(message.buttonsResponseMessage?.selectedDisplayText) ||\n texto(message.listResponseMessage?.singleSelectReply?.selectedRowId) ||\n texto(message.listResponseMessage?.title) ||\n texto(message.templateButtonReplyMessage?.selectedId) ||\n texto(message.templateButtonReplyMessage?.selectedDisplayText) ||\n '';\n\nconst textoMensaje =\n buttonText ||\n texto(message.conversation) ||\n texto(message.extendedTextMessage?.text) ||\n texto(data.messageText) ||\n texto(data.text) ||\n texto(body.text) ||\n '';\n\n// --------------------------------------------------\n// 3. Detectar media\n// --------------------------------------------------\n\nconst audioMsg = message.audioMessage;\nconst imageMsg = message.imageMessage;\nconst videoMsg = message.videoMessage;\nconst documentMsg = message.documentMessage;\n\nconst documentMime = texto(documentMsg?.mimetype);\nconst documentFileName = texto(documentMsg?.fileName);\n\nlet messageType = 'unknown';\n\nif (audioMsg) {\n messageType = 'audio';\n} else if (imageMsg) {\n messageType = 'image';\n} else if (videoMsg) {\n messageType = 'video';\n} else if (documentMsg && documentMime.startsWith('video/')) {\n messageType = 'video';\n} else if (documentMsg && documentMime.startsWith('image/')) {\n messageType = 'image';\n} else if (documentMsg && documentMime.startsWith('audio/')) {\n messageType = 'audio';\n} else if (documentMsg) {\n messageType = 'document';\n} else if (textoMensaje) {\n messageType = 'text';\n}\n\nconst tieneMedia = ['audio', 'image', 'video', 'document'].includes(messageType);\n\nconst mediaMimeType =\n texto(audioMsg?.mimetype) ||\n texto(imageMsg?.mimetype) ||\n texto(videoMsg?.mimetype) ||\n documentMime ||\n '';\n\nconst mediaSourceId =\n texto(key.id) ||\n texto(data.id) ||\n texto(data.messageId) ||\n '';\n\n// --------------------------------------------------\n// 4. Normalizar comandos internos\n// --------------------------------------------------\n\nconst normalizarTexto = (valor) =>\n texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst normalizarComando = (valor) => {\n const t = normalizarTexto(valor);\n\n // Inicio oficial\n if (t === 'HEY') {\n return 'START';\n }\n\n // Cancelación oficial ES/EN\n if (\n t === 'CANCELAR' ||\n t === 'CANCEL'\n ) {\n return 'CANCELAR';\n }\n\n // Cierre de imágenes oficial ES/EN\n if (\n t === 'FOTOS LISTAS' ||\n t === 'FOTOS_LISTAS' ||\n t === 'PHOTOS READY' ||\n t === 'PHOTOS_READY'\n ) {\n return 'FOTOS_LISTAS';\n }\n\n // Cierre sin video oficial ES/EN\n if (\n t === 'SIN VIDEO' ||\n t === 'SIN_VIDEO' ||\n t === 'NO VIDEO' ||\n t === 'NO_VIDEO'\n ) {\n return 'SIN_VIDEO';\n }\n\n // Cierre con videos oficial ES/EN\n if (\n t === 'LISTO' ||\n t === 'DONE'\n ) {\n return 'LISTO';\n }\n\n return 'NORMAL';\n};\n\n// --------------------------------------------------\n// 5. Detectar país/idioma base por teléfono\n// --------------------------------------------------\n\nconst detectarIdiomaPorTelefono = (phone) => {\n const n = limpiarNumero(phone);\n\n // Países GLM principalmente hispanohablantes\n if (n.startsWith('502')) return { country_code: '502', pais_detectado: 'Guatemala', idioma_flujo: 'ES' };\n if (n.startsWith('503')) return { country_code: '503', pais_detectado: 'El Salvador', idioma_flujo: 'ES' };\n if (n.startsWith('504')) return { country_code: '504', pais_detectado: 'Honduras', idioma_flujo: 'ES' };\n if (n.startsWith('505')) return { country_code: '505', pais_detectado: 'Nicaragua', idioma_flujo: 'ES' };\n if (n.startsWith('506')) return { country_code: '506', pais_detectado: 'Costa Rica', idioma_flujo: 'ES' };\n if (n.startsWith('507')) return { country_code: '507', pais_detectado: 'Panamá', idioma_flujo: 'ES' };\n\n // República Dominicana: +1 809 / 829 / 849\n if (n.startsWith('1809') || n.startsWith('1829') || n.startsWith('1849')) {\n return { country_code: '1', pais_detectado: 'República Dominicana', idioma_flujo: 'ES' };\n }\n\n // Default para +1 no dominicano: inglés hasta que hagamos tabla formal\n if (n.startsWith('1')) {\n return { country_code: '1', pais_detectado: 'País +1 no identificado', idioma_flujo: 'EN' };\n }\n\n return { country_code: '', pais_detectado: 'No identificado', idioma_flujo: 'ES' };\n};\n\nconst idiomaInfo = detectarIdiomaPorTelefono(senderPhone);\n\n// --------------------------------------------------\n// 6. Resultado normalizado\n// --------------------------------------------------\n\nreturn [\n {\n json: {\n event_id: texto(key.id || data.id || Date.now()),\n fecha_recepcion: new Date().toISOString(),\n\n canal_origen: 'WHATSAPP',\n\n // Compatibilidad con el flujo actual:\n // seguimos llenando manager_telefono / manager_nombre,\n // pero ahora apuntan al sender real, no al grupo.\n manager_telefono: senderPhone,\n manager_nombre: pushName,\n usuario_id_origen: senderPhone,\n\n // Campos nuevos para grupo\n is_group: isGroup,\n group_jid: groupJid,\n group_name: texto(data.groupName || body.groupName || ''),\n sender_jid: senderJid,\n sender_phone: senderPhone,\n sender_name: pushName,\n\n whatsapp_remote_jid: remoteJid,\n whatsapp_to: whatsappTo,\n\n from_me: Boolean(key.fromMe || data.fromMe || false),\n\n message_type: messageType,\n texto: textoMensaje,\n accion_flujo: normalizarComando(textoMensaje),\n\n tiene_media: tieneMedia,\n media_count: tieneMedia ? 1 : 0,\n media_source_id: mediaSourceId,\n media_mime_type: mediaMimeType,\n media_file_name: documentFileName,\n\n country_code: idiomaInfo.country_code,\n pais_detectado: idiomaInfo.pais_detectado,\n idioma_flujo: idiomaInfo.idioma_flujo,\n\n raw_preview: JSON.stringify(body).slice(0, 4000),\n raw_event: body,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-9296,
-1024
],
"id": "5758e021-8dc1-4541-8cb9-be855b01bdbc",
"name": "Code - Normalizar evento WhatsApp TEST"
},
{
"parameters": {
"httpMethod": "POST",
"path": "factura",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-9504,
-1024
],
"id": "8985b821-561b-4927-9528-1080bb264d83",
"name": "Webhook - Evolution WhatsApp TEST",
"webhookId": "6d9cdd90-a105-4e00-b42a-dcd7c4e5a7e1",
"disabled": true
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-8112,
-1072
],
"id": "2553dc5c-8ca5-416c-b08a-8e44f6e549a6",
"name": "Sheets - Leer sesiones existentes",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const evento = $('Code - Normalizar evento WhatsApp TEST').first().json || {};\nconst sesiones = $input.all().map((item) => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) =>\n texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst estadosAbiertos = new Set([\n 'ACTIVA',\n 'PENDIENTE',\n 'EN_PROCESO',\n 'PROCESANDO',\n 'LISTO_PARA_RECUPERAR_MEDIA',\n 'LISTO_PARA_ANALIZAR',\n]);\n\nconst etapasAbiertas = new Set([\n 'ESPERANDO_AUDIO',\n 'ESPERANDO_IMAGENES',\n 'ESPERANDO_VIDEOS',\n 'PROCESANDO',\n]);\n\nconst managerTelefono = texto(evento.manager_telefono || evento.sender_phone);\nconst senderPhone = texto(evento.sender_phone || managerTelefono);\nconst groupJid = texto(evento.group_jid);\nconst isGroup = Boolean(evento.is_group);\nconst accion = texto(evento.accion_flujo || 'NORMAL');\n\n// Idioma únicamente desde Code - Normalizar evento WhatsApp TEST.\n// Ese nodo lo calcula por número de teléfono.\nconst idiomaFlujo = normalizar(evento.idioma_flujo || 'ES') === 'EN'\n ? 'EN'\n : 'ES';\n\n// --------------------------------------------------\n// Buscar sesión activa\n// --------------------------------------------------\n// En grupo: sesión única por grupo + sender.\n// En chat individual: sesión por manager_telefono.\nconst sesionActiva = sesiones\n .filter((row) => {\n const rowManager = texto(row.manager_telefono);\n const rowSender = texto(row.sender_phone || row.manager_telefono);\n const rowGroup = texto(row.group_jid);\n\n if (isGroup) {\n return rowGroup === groupJid && rowSender === senderPhone;\n }\n\n return rowManager === managerTelefono;\n })\n .filter((row) => {\n const estado = normalizar(row.estado);\n const etapa = normalizar(row.etapa);\n\n return estadosAbiertos.has(estado) || etapasAbiertas.has(etapa);\n })\n .sort((a, b) => {\n return new Date(b.ultima_actividad || b.fecha_inicio || 0) - new Date(a.ultima_actividad || a.fecha_inicio || 0);\n })[0] || null;\n\n// --------------------------------------------------\n// Crear IDs\n// --------------------------------------------------\n\nconst suffix = Math.random().toString(36).slice(2, 6).toUpperCase();\nconst phoneSuffix = senderPhone.slice(-4) || managerTelefono.slice(-4) || '0000';\nconst sessionId = `WA_${Date.now()}_${phoneSuffix}_${suffix}`;\n\nlet decision = '';\n\nif (accion === 'START') {\n decision = sesionActiva ? 'AVISO_SESION_ACTIVA' : 'CREAR_SESION';\n} else if (accion === 'CANCELAR') {\n decision = sesionActiva ? 'CANCELAR_SESION' : 'SIN_SESION_PARA_CANCELAR';\n} else {\n decision = sesionActiva ? 'CONTINUAR_SESION' : 'SIN_SESION_ACTIVA';\n}\n\nreturn [\n {\n json: {\n ...evento,\n\n idioma_flujo: idiomaFlujo,\n\n sesion_activa_encontrada: Boolean(sesionActiva),\n sesion_activa: sesionActiva,\n\n session_id: sesionActiva ? texto(sesionActiva.session_id) : sessionId,\n ejecucion_id: sesionActiva ? texto(sesionActiva.ejecucion_id || sesionActiva.session_id) : sessionId,\n\n decision_flujo: decision,\n\n nueva_sesion: {\n session_id: sessionId,\n\n manager_telefono: managerTelefono,\n manager_nombre: texto(evento.manager_nombre),\n\n canal_origen: 'WHATSAPP',\n fecha_inicio: new Date().toISOString(),\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_AUDIO',\n\n audio_count: 0,\n imagenes_count: 0,\n videos_count: 0,\n\n estado: 'ACTIVA',\n ejecucion_id: sessionId,\n motivo_revision: '',\n\n is_group: isGroup,\n group_jid: groupJid,\n group_name: texto(evento.group_name),\n sender_jid: texto(evento.sender_jid),\n sender_phone: senderPhone,\n sender_name: texto(evento.sender_name),\n\n country_code: texto(evento.country_code),\n pais_detectado: texto(evento.pais_detectado),\n idioma_flujo: idiomaFlujo,\n\n tipo_reporte: ''\n },\n\n resolver_sesion_debug: {\n idioma_origen: 'Code - Normalizar evento WhatsApp TEST',\n idioma_final_usado: idiomaFlujo,\n pais_detectado: texto(evento.pais_detectado),\n country_code: texto(evento.country_code),\n manager_telefono: managerTelefono,\n sender_phone: senderPhone,\n group_jid: groupJid,\n is_group: isGroup,\n accion,\n decision_flujo: decision,\n total_sesiones_leidas: sesiones.length,\n sesion_activa_encontrada: Boolean(sesionActiva)\n }\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-7920,
-1072
],
"id": "062d75cf-28b9-44b1-b119-d619d83b5bfd",
"name": "Code - Resolver sesión WhatsApp TEST"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"leftValue": "={{ $json.decision_flujo === 'CREAR_SESION' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"id": "acec6506-977a-412d-b08a-fbcc8de4b0b8"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "CREAR_SESION"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "5c9dcfa5-9b15-428c-ad80-c277b0671658",
"leftValue": "={{ $json.decision_flujo === 'AVISO_SESION_ACTIVA' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "AVISO_SESION_ACTIVA"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "966402b5-4517-45c2-8a87-0a7aa4e5e682",
"leftValue": "={{ $json.decision_flujo === 'SIN_SESION_ACTIVA' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "SIN_SESION_ACTIVA"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "698f3da3-29d2-472f-b625-af387891035e",
"leftValue": "={{ $json.decision_flujo === 'CANCELAR_SESION' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "CANCELAR_SESION"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "d5feac06-4493-494e-ac1e-36c007f0de79",
"leftValue": "={{ $json.decision_flujo === 'SIN_SESION_PARA_CANCELAR' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "SIN_SESION_PARA_CANCELAR"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "950979b7-b930-4260-a392-52277a742e43",
"leftValue": "={{ $json.decision_flujo === 'CONTINUAR_SESION' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "CONTINUAR_SESION"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
-7696,
-1136
],
"id": "cad5616f-22a5-4572-a816-253c2f48544c",
"name": "Switch - Decisión sesión WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const datos = $('Code - Resolver sesión WhatsApp TEST').first().json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.nueva_sesion?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst nombre =\n datos.manager_nombre ||\n datos.sender_name ||\n 'equipo';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '📸 *GLM Proposal Evidence Registry*',\n '',\n '*Step 1 of 3 — Voice note*',\n '',\n `Hello, ${nombre}.`,\n '',\n 'Use this flow to register evidence for Fulgencio Fumado.',\n '',\n '*Report Type:*',\n '1. *Executed Proposal*: Already implemented.',\n '2. *External Proposal*: Received outside the internal bank.',\n '',\n 'Send a voice note and clearly say if this is an *executed* or *external* proposal.',\n '',\n 'Include:',\n '• Proposal/reference',\n '• Brand/client',\n '• Country',\n '• Location',\n '• Date',\n '• What was implemented or reported',\n '• Results/comments',\n '',\n '*Important:* One proposal/report at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n '📸 *Registro de Evidencias GLM*',\n '',\n '*Paso 1 de 3 — Nota de voz*',\n '',\n `Hola, ${nombre}.`,\n '',\n 'Usa este flujo para registrar evidencias de Fulgencio Fumado.',\n '',\n '*Tipo de Reporte:*',\n '1. *Propuesta Ejecutada*: Ya implementada.',\n '2. *Propuesta Externa*: Recibida fuera del banco interno.',\n '',\n 'Envía una nota de voz e indica claramente si es *ejecutada* o *externa*.',\n '',\n 'Incluye:',\n '• Propuesta/referencia',\n '• Marca/cliente',\n '• País',\n '• Ubicación',\n '• Fecha',\n '• Qué se implementó o reportó',\n '• Resultados/comentarios',\n '',\n '*Importante:* Una sola propuesta o reporte a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid,\n whatsapp_text: mensaje,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4944,
-4608
],
"id": "0c8f0c1a-f57d-4b62-a5e7-91715c1dcaa6",
"name": "Code - Preparar bienvenida WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst etapa = String(datos.sesion_activa?.etapa || '').trim() || 'pendiente';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '⚠️ You already have an active proposal report in progress.',\n '',\n `Current step: ${etapa}`,\n '',\n 'Please complete that report before starting a new one.',\n '',\n 'To cancel it, use the cancel option.',\n ].join('\\n');\n} else {\n mensaje = [\n '⚠️ Ya tienes un reporte de propuesta en proceso.',\n '',\n `Etapa actual: ${etapa}`,\n '',\n 'Completa ese reporte antes de iniciar uno nuevo.',\n '',\n 'Para cancelarlo, usa la opción de cancelar.',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.whatsapp_remote_jid,\n whatsapp_text: mensaje,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4912,
-4160
],
"id": "6413892b-21b2-4d3a-be5e-b200b13c1de1",
"name": "Code - Preparar aviso sesión activa WhatsApp TEST"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.nueva_sesion.session_id }}",
"manager_telefono": "={{ $json.nueva_sesion.manager_telefono }}",
"manager_nombre": "={{ $json.nueva_sesion.manager_nombre }}",
"canal_origen": "={{ $json.nueva_sesion.canal_origen }}",
"fecha_inicio": "={{ $json.nueva_sesion.fecha_inicio }}",
"ultima_actividad": "={{ $json.nueva_sesion.ultima_actividad }}",
"etapa": "={{ $json.nueva_sesion.etapa }}",
"audio_count": "={{ $json.nueva_sesion.audio_count }}",
"imagenes_count": "={{ $json.nueva_sesion.imagenes_count }}",
"videos_count": "={{ $json.nueva_sesion.videos_count }}",
"estado": "={{ $json.nueva_sesion.estado }}",
"ejecucion_id": "={{ $json.nueva_sesion.ejecucion_id }}",
"motivo_revision": "={{ $json.nueva_sesion.motivo_revision }}",
"is_group": "={{ $json.nueva_sesion.is_group }}",
"group_jid": "={{ $json.nueva_sesion.group_jid }}",
"group_name": "={{ $json.nueva_sesion.group_name }}",
"sender_jid": "={{ $json.nueva_sesion.sender_jid }}",
"sender_phone": "={{ $json.nueva_sesion.sender_phone }}",
"sender_name": "={{ $json.nueva_sesion.sender_name }}",
"country_code": "={{ $json.nueva_sesion.country_code }}",
"pais_detectado": "={{ $json.nueva_sesion.pais_detectado }}",
"idioma_flujo": "={{ $json.nueva_sesion.idioma_flujo }}",
"tipo_reporte": "={{ $json.nueva_sesion.tipo_reporte }}"
},
"matchingColumns": [],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-5152,
-4608
],
"id": "66b65b89-24b9-484a-9693-a5acecdf77a4",
"name": "Sheets - Crear sesión WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const datos = $json || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst etapa = texto(datos.sesion_activa?.etapa).toUpperCase();\nconst tipoMensaje = texto(datos.message_type).toLowerCase();\nconst accion = texto(datos.accion_flujo).toUpperCase();\n\nconst imagenesCount = Number(datos.sesion_activa?.imagenes_count || 0);\nconst videosCount = Number(datos.sesion_activa?.videos_count || 0);\n\nlet paso_decision = 'NO_DEFINIDO';\n\n// --------------------------------------------------\n// 1. Cancelación global\n// --------------------------------------------------\nif (accion === 'CANCELAR') {\n paso_decision = 'CANCELAR_SESION';\n\n// --------------------------------------------------\n// 2. Paso 1: esperando nota de voz\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_AUDIO') {\n if (tipoMensaje === 'audio') {\n paso_decision = 'GUARDAR_AUDIO';\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_AUDIO';\n } else {\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 3. Paso 2: esperando imágenes obligatorias\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_IMAGENES') {\n if (tipoMensaje === 'image') {\n paso_decision = 'GUARDAR_IMAGEN';\n\n } else if (accion === 'FOTOS_LISTAS') {\n // La validación real se hace después leyendo wa_ejecuciones_eventos.\n paso_decision = 'FOTOS_LISTAS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_IMAGENES';\n\n } else {\n // Si WhatsApp manda eventos raros durante imágenes, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 4. Paso 3: esperando videos opcionales\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_VIDEOS') {\n if (tipoMensaje === 'video') {\n paso_decision = 'GUARDAR_VIDEO';\n\n } else if (accion === 'SIN_VIDEO') {\n paso_decision = 'CERRAR_SIN_VIDEO';\n\n } else if (accion === 'LISTO') {\n paso_decision = 'CERRAR_CON_VIDEOS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_VIDEOS';\n\n } else {\n // Si WhatsApp manda eventos raros durante videos, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 5. Sesión en procesamiento\n// --------------------------------------------------\n} else if (etapa === 'PROCESANDO') {\n paso_decision = 'AVISO_PROCESANDO';\n\n// --------------------------------------------------\n// 6. Etapa no reconocida\n// --------------------------------------------------\n} else {\n paso_decision = 'ETAPA_NO_RECONOCIDA';\n}\n\nreturn [\n {\n json: {\n ...datos,\n\n etapa_actual: etapa,\n tipo_mensaje_actual: tipoMensaje,\n accion_actual: accion,\n\n imagenes_count_actual: imagenesCount,\n videos_count_actual: videosCount,\n\n paso_decision,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-7392,
-576
],
"id": "99ec1dc9-7dde-42fa-8be2-9dc1b3986b12",
"name": "Code - Resolver paso activo WhatsApp TEST"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"id": "c7fef2d7-50ce-467a-b125-30ae7fba3a58"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "GUARDAR_AUDIO"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "8da042cf-54e6-460f-b505-2b17c4f4ff3c",
"leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "PEDIR_AUDIO"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "ecf52241-ef7e-4a9a-8823-b9e3b438da61",
"leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "GUARDAR_AUDIO"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "04433553-d0ee-4391-a4f4-926197ac4c0c",
"leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "PEDIR_AUDIO"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "8637dc0f-59b6-40ac-8bc4-baf869829598",
"leftValue": "={{ $json.paso_decision === 'GUARDAR_IMAGEN' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "GUARDAR_IMAGEN"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "4096e616-2517-48ab-b6c4-2712f763ec1d",
"leftValue": "={{ $json.paso_decision === 'FOTOS_LISTAS' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "FOTOS_LISTAS"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "d1dd1d5c-8e25-4147-876e-5e7227784f6f",
"leftValue": "={{ $json.paso_decision === 'PEDIR_IMAGENES' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "PEDIR_IMAGENES"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "bddbb16a-0463-47dd-9a6c-03c3635043f1",
"leftValue": "={{ $json.paso_decision === 'GUARDAR_VIDEO' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "GUARDAR_VIDEO"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "fe05ba35-0ccc-417a-a4e8-b907611932e4",
"leftValue": "={{ $json.paso_decision === 'CERRAR_SIN_VIDEO' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "CERRAR_SIN_VIDEO"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "94647ac0-ee80-4c96-9f25-8a847704b6a0",
"leftValue": "={{ $json.paso_decision === 'CERRAR_CON_VIDEOS' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "CERRAR_CON_VIDEOS"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "3038a656-50ae-4738-b2ff-a6276bd06522",
"leftValue": "={{ $json.paso_decision === 'PEDIR_VIDEOS' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "PEDIR_VIDEOS"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "1b20a524-2289-4d37-8372-9a839dadc971",
"leftValue": "={{ $json.paso_decision === 'AVISO_PROCESANDO' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "AVISO_PROCESANDO"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "0943bbd0-f5b7-43c4-9b01-7f6bea244894",
"leftValue": "={{ $json.paso_decision === 'ETAPA_NO_RECONOCIDA' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "ETAPA_NO_RECONOCIDA"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "84c163a3-3c08-4636-9104-c0e4c55b11e6",
"leftValue": "={{ $json.paso_decision === 'IGNORAR_EVENTO' }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "IGNORAR_EVENTO"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
-7216,
-384
],
"id": "7599f552-a8d6-416c-9664-6acc127fef5b",
"name": "Switch - Paso activo WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '🎙️ *Step 1 of 3 — Voice note*',\n '',\n 'I still need the required voice note to start the report.',\n '',\n 'Please send one voice note and clearly say whether this is:',\n '',\n '1. *Executed Proposal*',\n '2. *External Proposal*',\n '',\n 'Include:',\n '• Proposal/reference',\n '• Brand/client',\n '• Country',\n '• Location',\n '• Date',\n '• What was implemented or reported',\n '• Results/comments',\n '',\n '*Important:* One proposal/report at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n '🎙️ *Paso 1 de 3 — Nota de voz*',\n '',\n 'Aún necesito la nota de voz obligatoria para iniciar el reporte.',\n '',\n 'Envía una sola nota de voz e indica claramente si es:',\n '',\n '1. *Propuesta Ejecutada*',\n '2. *Propuesta Externa*',\n '',\n 'Incluye:',\n '• Propuesta/referencia',\n '• Marca/cliente',\n '• País',\n '• Ubicación',\n '• Fecha',\n '• Qué se implementó o reportó',\n '• Resultados/comentarios',\n '',\n '*Importante:* Una sola propuesta o reporte a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid,\n whatsapp_text: mensaje,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4784,
-2368
],
"id": "ae1227b1-221a-459a-9dde-32d9d2881f00",
"name": "Code - Preparar solicitud audio WhatsApp TEST"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 306129743,
"mode": "list",
"cachedResultName": "wa_ejecuciones_eventos",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"event_id": "={{ $json.event_id }}",
"fecha_recepcion": "={{ $json.fecha_recepcion }}",
"manager_telefono": "={{ $json.manager_telefono }}",
"manager_nombre": "={{ $json.manager_nombre }}",
"message_type": "={{ $json.message_type }}",
"texto": "={{ $json.texto }}",
"comando": "={{ $json.accion_flujo }}",
"tiene_media": "={{ $json.tiene_media }}",
"media_count": "={{ $json.media_count }}",
"raw_preview": "={{ $json.raw_preview }}",
"estado": "={{ $json.estado }}",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"fecha_procesado": "={{ $json.fecha_procesado }}",
"session_id": "={{ $json.session_id }}",
"canal_origen": "={{ $json.canal_origen }}",
"etapa_recibida": "={{ $json.etapa_recibida }}",
"media_source_id": "={{ $json.media_source_id }}",
"media_mime_type": "={{ $json.media_mime_type }}",
"media_file_name": "={{ $json.media_file_name }}",
"whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}"
},
"matchingColumns": [],
"schema": [
{
"id": "event_id",
"displayName": "event_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_recepcion",
"displayName": "fecha_recepcion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "message_type",
"displayName": "message_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "texto",
"displayName": "texto",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "comando",
"displayName": "comando",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "tiene_media",
"displayName": "tiene_media",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_count",
"displayName": "media_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "raw_preview",
"displayName": "raw_preview",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_procesado",
"displayName": "fecha_procesado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa_recibida",
"displayName": "etapa_recibida",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_source_id",
"displayName": "media_source_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "media_mime_type",
"displayName": "media_mime_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "media_file_name",
"displayName": "media_file_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "whatsapp_remote_jid",
"displayName": "whatsapp_remote_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-5072,
-2752
],
"id": "4882de04-332a-4d43-be18-e00085f1144a",
"name": "Sheets - Guardar evento audio WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const datos = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = datos.sesion_activa || datos.nueva_sesion || {};\n\nreturn [\n {\n json: {\n ...datos,\n\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'ACTIVA',\n\n audio_count: Number(sesion.audio_count || 0) + 1,\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n manager_telefono: datos.manager_telefono || sesion.manager_telefono || datos.sender_phone || '',\n manager_nombre: datos.manager_nombre || sesion.manager_nombre || datos.sender_name || '',\n\n is_group: datos.is_group ?? sesion.is_group ?? false,\n group_jid: datos.group_jid || sesion.group_jid || '',\n group_name: datos.group_name || sesion.group_name || '',\n sender_jid: datos.sender_jid || sesion.sender_jid || '',\n sender_phone: datos.sender_phone || sesion.sender_phone || datos.manager_telefono || '',\n sender_name: datos.sender_name || sesion.sender_name || datos.manager_nombre || '',\n\n country_code: datos.country_code || sesion.country_code || '',\n pais_detectado: datos.pais_detectado || sesion.pais_detectado || '',\n idioma_flujo: datos.idioma_flujo || sesion.idioma_flujo || 'ES',\n\n tipo_reporte: sesion.tipo_reporte || datos.tipo_reporte || '',\n\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid || ''\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4864,
-2752
],
"id": "00052ad8-be3a-4164-a1ec-d583c244078d",
"name": "Code - Preparar actualización audio recibido WhatsApp TEST"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"ultima_actividad": "={{ $json.ultima_actividad }}",
"etapa": "={{ $json.etapa }}",
"estado": "={{ $json.estado }}",
"audio_count": "={{ $json.audio_count }}",
"imagenes_count": "={{ $json.imagenes_count }}",
"videos_count": "={{ $json.videos_count }}",
"is_group": "={{ $json.is_group }}",
"group_jid": "={{ $json.group_jid }}",
"group_name": "={{ $json.group_name }}",
"sender_jid": "={{ $json.sender_jid }}",
"sender_phone": "={{ $json.sender_phone }}",
"sender_name": "={{ $json.sender_name }}",
"country_code": "={{ $json.country_code }}",
"pais_detectado": "={{ $json.pais_detectado }}",
"idioma_flujo": "={{ $json.idioma_flujo }}",
"tipo_reporte": "={{ $json.tipo_reporte }}",
"manager_telefono": "={{ $json.manager_telefono }}",
"manager_nombre": "={{ $json.manager_nombre }}",
"ejecucion_id": "={{ $json.ejecucion_id }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-4656,
-2752
],
"id": "3fa46865-3a79-47f4-baa2-54338425a397",
"name": "Sheets - Actualizar sesión audio recibido WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const datos = $('Code - Preparar actualización audio recibido WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ Voice note received.',\n '',\n '*Step 2 of 3 — Required images*',\n '',\n 'Now send one or more photos related to the proposal or report.',\n '',\n 'You can send multiple images together or in separate messages.',\n '',\n 'When you finish sending images, write: PHOTOS READY'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ Nota de voz recibida.',\n '',\n '*Paso 2 de 3 — Imágenes obligatorias*',\n '',\n 'Ahora envía una o varias fotos relacionadas a la propuesta o reporte.',\n '',\n 'Puedes enviar varias imágenes juntas o en mensajes separados.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS'\n ].join('\\n');\n}\n\nconst whatsappTo = limpiar(\n datos.whatsapp_to ||\n datos.group_jid ||\n datos.whatsapp_remote_jid ||\n datos.sender_jid ||\n ''\n);\n\nif (!whatsappTo) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar solicitud de imágenes.');\n}\n\nreturn [\n {\n json: {\n ...datos,\n\n whatsapp_to: whatsappTo,\n\n // Campo principal usado por el nodo WhatsApp\n whatsapp_text: mensaje,\n\n // Campos compatibles por seguridad\n texto_respuesta: mensaje,\n mensaje,\n text: mensaje,\n message_text: mensaje,\n\n estado_mensaje: 'SOLICITUD_IMAGENES_PREPARADA',\n solicitud_imagenes_debug: {\n idioma,\n whatsapp_to: whatsappTo,\n permite_imagenes_juntas: true\n }\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4432,
-2752
],
"id": "f11e8263-5435-4b96-b0fa-ca669945c715",
"name": "Code - Preparar solicitud imágenes WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const datos = $json || {};\n\nreturn [\n {\n json: {\n ...datos,\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n etapa: 'CANCELADA',\n estado: 'CANCELADA',\n motivo_revision: 'CANCELADO_POR_USUARIO',\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5040,
-3344
],
"id": "0f480c5f-c26a-48ca-aaa9-05e89c54558a",
"name": "Code - Preparar cancelación sesión WhatsApp TEST"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"ultima_actividad": "={{ $json.ultima_actividad }}",
"etapa": "={{ $json.etapa }}",
"estado": "={{ $json.estado }}",
"motivo_revision": "={{ $json.motivo_revision }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-4832,
-3344
],
"id": "f1c30bbb-3fd0-4080-bf86-1c2f3d418b2a",
"name": "Sheets - Cancelar sesión WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver sesión WhatsApp TEST');\n\nconst sesion =\n base.sesion_activa ||\n actual.sesion_activa ||\n {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = limpiar(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n ''\n);\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar mensaje de cancelación.');\n}\n\nlet whatsappTo = destinoBase;\n\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ *Report cancelled successfully.*',\n '',\n 'The current report was closed and no additional evidence will be added to that session.',\n '',\n 'When you are ready, you can start a new registration by writing:',\n '',\n '*Hey*'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ *Reporte cancelado correctamente.*',\n '',\n 'El reporte actual fue cerrado y no se agregará más evidencia a esa sesión.',\n '',\n 'Cuando estés listo, puedes iniciar un nuevo registro escribiendo:',\n '',\n '*Hey*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: mensaje\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4576,
-3408
],
"id": "c4735257-3f2c-49ff-ae03-0f9260e45fd7",
"name": "Code - Preparar mensaje cancelación WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '📸 I still need at least one image.',\n '',\n 'The voice note was already received, but one evidence photo is required to complete the report.',\n '',\n '⚠️ Please send at least one image, one per message.',\n 'Do not send multiple images together.',\n '',\n 'When you finish sending images, write: PHOTOS READY'\n ].join('\\n');\n} else {\n mensaje = [\n '📸 Necesito que me envíes al menos una imagen.',\n '',\n 'La nota de voz ya fue recibida, pero para completar el reporte hace falta una foto de evidencia.',\n '',\n '⚠️ Por favor envía al menos una imagen, una por mensaje.',\n 'No envíes varias imágenes juntas.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4064,
-320
],
"id": "99f86945-1098-4c41-a57f-da8ba11f28bb",
"name": "Code - Preparar aviso falta imagen WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || base.nueva_sesion || {};\n\nconst rows = $input.all().map(item => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizarNumero = (valor) =>\n texto(valor)\n .replace('@s.whatsapp.net', '')\n .replace('@c.us', '')\n .replace('@g.us', '')\n .replace('@lid', '')\n .replace(/\\D/g, '');\n\nconst sessionId = texto(base.session_id || sesion.session_id);\nconst ejecucionId = texto(sesion.ejecucion_id || base.ejecucion_id || sessionId);\n\nconst ahora = new Date().toISOString();\n\nconst fechaInicioSesion = texto(\n sesion.fecha_inicio ||\n base.fecha_inicio ||\n base.sesion_activa?.fecha_inicio ||\n ''\n);\n\nconst fechaInicioMs = fechaInicioSesion\n ? new Date(fechaInicioSesion).getTime()\n : 0;\n\nconst fechaAhoraMs = Date.now();\n\nconst groupJid = texto(\n base.group_jid ||\n sesion.group_jid ||\n base.whatsapp_remote_jid ||\n ''\n);\n\nconst managerTelefono = normalizarNumero(\n base.manager_telefono ||\n base.sender_phone ||\n sesion.manager_telefono ||\n sesion.sender_phone ||\n ''\n);\n\n// Ventana máxima de seguridad para esta sesión.\n// Así no mezclamos fotos viejas del mismo grupo/persona.\n// 45 minutos es suficiente para una ejecución normal.\nconst ventanaMaximaMs = 45 * 60 * 1000;\n\nconst imagenesUnicas = [];\nconst vistos = new Set();\n\nfor (const row of rows) {\n const tipo = texto(row.message_type).toLowerCase();\n\n if (tipo !== 'image') continue;\n\n const rowSessionId = texto(row.session_id);\n const rowGroupJid = texto(row.whatsapp_remote_jid);\n const rowManager = normalizarNumero(row.manager_telefono || row.sender_phone || '');\n\n const rowFecha = texto(row.fecha_recepcion || row.fecha_procesado || '');\n const rowFechaMs = rowFecha ? new Date(rowFecha).getTime() : 0;\n\n const mismaSesion = sessionId && rowSessionId === sessionId;\n\n const mismoGrupoYManager =\n groupJid &&\n rowGroupJid === groupJid &&\n managerTelefono &&\n rowManager === managerTelefono;\n\n const dentroDeVentana =\n rowFechaMs &&\n fechaInicioMs &&\n rowFechaMs >= fechaInicioMs - 15000 &&\n rowFechaMs <= fechaAhoraMs + 15000 &&\n rowFechaMs - fechaInicioMs <= ventanaMaximaMs;\n\n const perteneceALaEjecucion =\n mismaSesion ||\n (mismoGrupoYManager && dentroDeVentana);\n\n if (!perteneceALaEjecucion) continue;\n\n const mediaId = texto(row.media_source_id || row.event_id);\n\n if (!mediaId) continue;\n if (vistos.has(mediaId)) continue;\n\n vistos.add(mediaId);\n imagenesUnicas.push(row);\n}\n\nconst countSesion = Number(sesion.imagenes_count || base.imagenes_count_actual || 0);\n\nconst imagenesCountReal = imagenesUnicas.length > 0\n ? imagenesUnicas.length\n : countSesion;\n\nconst idsImagenes = imagenesUnicas\n .map(row => texto(row.media_source_id || row.event_id))\n .filter(Boolean);\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n ultima_actividad: ahora,\n\n etapa: 'ESPERANDO_VIDEOS',\n estado: 'ACTIVA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: imagenesCountReal,\n videos_count: Number(sesion.videos_count || 0),\n\n manager_telefono: base.manager_telefono || sesion.manager_telefono || base.sender_phone || '',\n manager_nombre: base.manager_nombre || sesion.manager_nombre || base.sender_name || '',\n\n is_group: base.is_group ?? sesion.is_group ?? false,\n group_jid: base.group_jid || sesion.group_jid || '',\n group_name: base.group_name || sesion.group_name || '',\n\n sender_jid: base.sender_jid || sesion.sender_jid || '',\n sender_phone: base.sender_phone || sesion.sender_phone || base.manager_telefono || '',\n sender_name: base.sender_name || sesion.sender_name || base.manager_nombre || '',\n\n country_code: base.country_code || sesion.country_code || '',\n pais_detectado: base.pais_detectado || sesion.pais_detectado || '',\n idioma_flujo: base.idioma_flujo || sesion.idioma_flujo || 'ES',\n\n tipo_reporte: sesion.tipo_reporte || base.tipo_reporte || '',\n\n whatsapp_to: base.whatsapp_to || base.group_jid || base.whatsapp_remote_jid || '',\n\n imagenes_eventos_detectados: imagenesUnicas.length,\n imagenes_media_source_ids: idsImagenes.join(','),\n\n debug_fotos_listas: {\n session_id_actual: sessionId,\n fecha_inicio_sesion: fechaInicioSesion,\n group_jid: groupJid,\n manager_telefono: managerTelefono,\n total_rows_leidas: rows.length,\n total_imagenes_detectadas: imagenesUnicas.length,\n ids_imagenes: idsImagenes\n }\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3440,
-1168
],
"id": "7a4c049a-def7-4dff-95aa-62e4399ebb32",
"name": "Preparar actualización fotos listas"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"ultima_actividad": "={{ $json.ultima_actividad }}",
"etapa": "={{ $json.etapa }}",
"audio_count": "={{ $json.audio_count }}",
"imagenes_count": "={{ $json.imagenes_count }}",
"videos_count": "={{ $json.videos_count }}",
"estado": "={{ $json.estado }}",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"motivo_revision": "={{ $json.motivo_revision }}",
"is_group": "={{ $json.is_group }}",
"group_jid": "={{ $json.group_jid }}",
"group_name": "={{ $json.group_name }}",
"sender_jid": "={{ $json.sender_jid }}",
"sender_phone": "={{ $json.sender_phone }}",
"sender_name": "={{ $json.sender_name }}",
"country_code": "={{ $json.country_code }}",
"pais_detectado": "={{ $json.pais_detectado }}",
"idioma_flujo": "={{ $json.idioma_flujo }}",
"tipo_reporte": "={{ $json.tipo_reporte }}",
"manager_telefono": "={{ $json.manager_telefono }}",
"manager_nombre": "={{ $json.manager_nombre }}",
"canal_origen": "={{ $json.canal_origen }}",
"fecha_inicio": "={{ $json.fecha_inicio }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-3248,
-1168
],
"id": "47d1bd24-d5d1-4004-b3a9-44f322412c11",
"name": "Sheets - Actualizar sesión fotos listas WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const actual = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst idioma = String(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = String(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n ''\n).trim();\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar solicitud de videos.');\n}\n\nlet whatsappTo = destinoBase;\n\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '*Step 3 of 3 — Optional videos*',\n '',\n 'You can send one or more videos of the execution if you have them.',\n '',\n '⚠️ Important: send videos one by one, in separate messages.',\n 'Do not send multiple videos together.',\n '',\n 'If you do not have videos, write: NO VIDEO',\n '',\n 'If you already sent videos and finished, write: DONE',\n ].join('\\n');\n} else {\n mensaje = [\n '*Paso 3 de 3 — Videos opcionales*',\n '',\n 'Puedes enviar uno o varios videos de la ejecución si tienes.',\n '',\n '⚠️ Importante: envía los videos uno por uno, en mensajes separados.',\n 'No envíes varios videos juntos.',\n '',\n 'Si no tienes videos, escribe: SIN VIDEO',\n '',\n 'Si enviaste videos y ya terminaste, escribe: LISTO',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n session_id: actual.session_id || base.session_id || sesion.session_id || '',\n ejecucion_id: actual.ejecucion_id || base.ejecucion_id || sesion.ejecucion_id || '',\n\n manager_telefono: actual.manager_telefono || base.manager_telefono || sesion.manager_telefono || '',\n manager_nombre: actual.manager_nombre || base.manager_nombre || sesion.manager_nombre || '',\n\n is_group: actual.is_group ?? base.is_group ?? sesion.is_group ?? false,\n group_jid: actual.group_jid || base.group_jid || sesion.group_jid || '',\n sender_phone: actual.sender_phone || base.sender_phone || sesion.sender_phone || '',\n idioma_flujo: idioma,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: mensaje,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3024,
-1168
],
"id": "b039c705-d2cf-4d73-bc03-06d93432cdb2",
"name": "Code - Preparar solicitud videos WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '🎥 We are already on the optional videos step.',\n '',\n 'You can send one or more videos of the execution.',\n '',\n '⚠️ Remember: send each video in a separate message.',\n 'Do not send multiple videos together.',\n '',\n 'If you do not have videos, write: *NO VIDEO*',\n '',\n 'If you already sent videos and finished, write: *DONE*'\n ].join('\\n');\n} else {\n mensaje = [\n '🎥 Ya estamos en el paso de videos opcionales.',\n '',\n 'Puedes enviar uno o varios videos de la ejecución.',\n '',\n '⚠️ Recuerda: envía cada video en un mensaje separado.',\n 'No envíes varios videos juntos.',\n '',\n 'Si no tienes videos, escribe: *SIN VIDEO*',\n '',\n 'Si ya enviaste videos y terminaste, escribe: *LISTO*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5936,
1504
],
"id": "a82ee843-061b-4462-a4f0-14f8c6975463",
"name": "Code - Preparar recordatorio videos WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'SIN_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-6288,
896
],
"id": "875805e7-2d5e-4113-8895-ae3704d90c1c",
"name": "CERRAR_SIN_VIDEO"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"ultima_actividad": "={{ $json.ultima_actividad }}",
"etapa": "={{ $json.etapa }}",
"audio_count": "={{ $json.audio_count }}",
"imagenes_count": "={{ $json.imagenes_count }}",
"videos_count": "={{ $json.videos_count }}",
"estado": "={{ $json.estado }}",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"motivo_revision": "={{ $json.motivo_revision }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-6080,
896
],
"id": "9eb38dee-9c6d-472c-9e39-8c9e943b3fc3",
"name": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 306129743,
"mode": "list",
"cachedResultName": "wa_ejecuciones_eventos",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"event_id": "={{ $json.event_id }}",
"fecha_recepcion": "={{ $json.fecha_recepcion }}",
"manager_telefono": "={{ $json.manager_telefono }}",
"manager_nombre": "={{ $json.manager_nombre }}",
"message_type": "={{ $json.message_type }}",
"texto": "={{ $json.texto }}",
"comando": "={{ $json.accion_flujo }}",
"tiene_media": "={{ $json.tiene_media }}",
"media_count": "={{ $json.media_count }}",
"raw_preview": "={{ $json.raw_preview }}",
"estado": "=VIDEO_RECIBIDO",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"session_id": "={{ $json.session_id }}",
"canal_origen": "={{ $json.canal_origen }}",
"etapa_recibida": "={{ $json.etapa_actual }}",
"media_source_id": "={{ $json.media_source_id }}",
"media_mime_type": "={{ $json.media_mime_type }}",
"media_file_name": "={{ $json.media_file_name }}",
"whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}"
},
"matchingColumns": [],
"schema": [
{
"id": "event_id",
"displayName": "event_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_recepcion",
"displayName": "fecha_recepcion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "message_type",
"displayName": "message_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "texto",
"displayName": "texto",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "comando",
"displayName": "comando",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "tiene_media",
"displayName": "tiene_media",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_count",
"displayName": "media_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "raw_preview",
"displayName": "raw_preview",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_procesado",
"displayName": "fecha_procesado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa_recibida",
"displayName": "etapa_recibida",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_source_id",
"displayName": "media_source_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "media_mime_type",
"displayName": "media_mime_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "media_file_name",
"displayName": "media_file_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "whatsapp_remote_jid",
"displayName": "whatsapp_remote_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-3936,
-80
],
"id": "7e36d0b7-09ab-4744-aaf4-9d8f01ab17fe",
"name": "Sheets - Guardar evento video WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const evento = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst videosActuales = Number(sesion.videos_count || 0);\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n evento.whatsapp_to ||\n evento.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n ...evento,\n\n session_id: base.session_id || sesion.session_id || evento.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || evento.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || evento.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n // Seguimos en videos hasta que el usuario escriba LISTO\n etapa: 'ESPERANDO_VIDEOS',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: videosActuales + 1,\n\n estado: sesion.estado || 'ACTIVA',\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: sesion.motivo_revision || '',\n\n whatsapp_to: whatsappTo\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3712,
-144
],
"id": "807724d9-20f0-4285-96b7-104fdcc9b752",
"name": "Code - Preparar actualización video recibido WhatsApp TEST"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"motivo_revision": "={{ $json.motivo_revision }}",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"estado": "={{ $json.estado }}",
"videos_count": "={{ $json.videos_count }}",
"imagenes_count": "={{ $json.imagenes_count }}",
"audio_count": "={{ $json.audio_count }}",
"etapa": "={{ $json.etapa }}",
"ultima_actividad": "={{ $json.ultima_actividad }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-3504,
-144
],
"id": "23bf12aa-4a65-4fbb-8ff5-236ff367e50e",
"name": "Sheets - Actualizar sesión video recibido WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n actual.whatsapp_to ||\n actual.group_jid ||\n actual.whatsapp_remote_jid ||\n base.whatsapp_to ||\n base.group_jid ||\n base.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar confirmación de video.');\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ Video received.',\n '',\n 'You can send more videos if needed.',\n '',\n '⚠️ Remember: send each video in a separate message.',\n 'Do not send multiple videos together.',\n '',\n 'When you finish sending videos, write: DONE',\n '',\n 'If you do not have more videos, you can also write: DONE'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ Video recibido.',\n '',\n 'Puedes enviar más videos si hace falta.',\n '',\n '⚠️ Recuerda: envía cada video en un mensaje separado.',\n 'No envíes varios videos juntos.',\n '',\n 'Cuando termines de enviar los videos, escribe: LISTO',\n '',\n 'Si no tienes más videos, también puedes escribir: LISTO'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3296,
-144
],
"id": "20b53cc1-f5e4-4ecd-9886-20256440653e",
"name": "Code - Preparar confirmación video WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'CON_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-6368,
1184
],
"id": "6ed742b7-db83-4cd6-8652-7efbd7f991ca",
"name": "Code - Preparar cierre con videos WhatsApp TEST"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"ultima_actividad": "={{ $json.ultima_actividad }}",
"etapa": "={{ $json.etapa }}",
"audio_count": "={{ $json.audio_count }}",
"imagenes_count": "={{ $json.imagenes_count }}",
"videos_count": "={{ $json.videos_count }}",
"estado": "={{ $json.estado }}",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"motivo_revision": "={{ $json.motivo_revision }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-6160,
1184
],
"id": "8bfcdbc6-1cdb-4452-8bd2-16ac984f6a9d",
"name": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const data = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n data.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n ...data,\n\n whatsapp_to: whatsappTo,\n\n whatsapp_text: `⏳ Tu reporte ya fue recibido y está pendiente de procesamiento.\n\nPor favor espera mientras se prepara el análisis de la evidencia.`\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5872,
1744
],
"id": "542f9202-df07-47a4-a4e2-707f24a2ffa3",
"name": "Code - Preparar aviso procesando WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const data = $json || {};\n\nreturn [\n {\n json: {\n ...data,\n\n session_id_busqueda: data.session_id || '',\n estado_procesamiento: 'BUSCAR_EVENTOS_MEDIA'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5104,
1104
],
"id": "27e3a13d-e6e9-431d-a24f-43b08f572562",
"name": "Code - Preparar búsqueda eventos media WhatsApp TEST"
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 306129743,
"mode": "list",
"cachedResultName": "wa_ejecuciones_eventos",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-4848,
1104
],
"id": "16b7850c-34c9-4d39-91fd-10e3fd3d8d97",
"name": "Sheets - Leer eventos WhatsApp TEST",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const cierre = $('Code - Preparar búsqueda eventos media WhatsApp TEST').first().json || {};\nconst sessionId = String(cierre.session_id_busqueda || cierre.session_id || '').trim();\n\nconst eventos = $input.all().map(item => item.json || {});\n\nconst eventosSesion = eventos.filter(evento => {\n return String(evento.session_id || '').trim() === sessionId;\n});\n\nconst eventosMedia = eventosSesion.filter(evento => {\n const tipo = String(evento.message_type || '').toLowerCase();\n return ['audio', 'image', 'video'].includes(tipo);\n});\n\nconst audios = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'audio');\nconst imagenes = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'image');\nconst videos = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...cierre,\n\n total_eventos_sesion: eventosSesion.length,\n total_media: eventosMedia.length,\n\n audio_count_real: audios.length,\n imagenes_count_real: imagenes.length,\n videos_count_real: videos.length,\n\n eventos_audio: audios,\n eventos_imagenes: imagenes,\n eventos_videos: videos,\n\n media_lista: eventosMedia,\n\n estado_procesamiento: 'EVENTOS_MEDIA_FILTRADOS'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4608,
1104
],
"id": "48fffd2b-c91c-424f-823d-b11785b45673",
"name": "Code - Filtrar eventos media de sesión WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const data = $json || {};\nconst mediaLista = Array.isArray(data.media_lista) ? data.media_lista : [];\n\nconst sessionId = String(data.session_id || data.session_id_busqueda || '').trim();\nconst ejecucionId = String(data.ejecucion_id || sessionId).trim();\n\nconst items = mediaLista.map((media, index) => {\n const tipo = String(media.message_type || '').toLowerCase();\n const mime = String(media.media_mime_type || '').trim();\n\n let extension = 'bin';\n\n if (mime.includes('ogg')) extension = 'ogg';\n else if (mime.includes('mpeg')) extension = 'mp3';\n else if (mime.includes('mp4')) extension = 'mp4';\n else if (mime.includes('jpeg') || mime.includes('jpg')) extension = 'jpg';\n else if (mime.includes('png')) extension = 'png';\n else if (tipo === 'audio') extension = 'ogg';\n else if (tipo === 'image') extension = 'jpg';\n else if (tipo === 'video') extension = 'mp4';\n\n const mediaSourceId = String(\n media.media_source_id ||\n media.event_id ||\n ''\n ).trim();\n\n const remoteJid = String(\n media.whatsapp_remote_jid ||\n data.whatsapp_remote_jid ||\n data.whatsapp_to ||\n media.manager_telefono ||\n data.manager_telefono ||\n ''\n ).trim();\n\n return {\n json: {\n ...data,\n\n media_index: index + 1,\n media_total: mediaLista.length,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: media.event_id || '',\n media_source_id: mediaSourceId,\n media_type: tipo,\n media_mime_type: mime,\n media_extension: extension,\n\n whatsapp_remote_jid: remoteJid,\n\n media_file_name: `${ejecucionId}_${tipo}_${index + 1}.${extension}`,\n\n evento_original: media,\n\n estado_procesamiento: 'MEDIA_ITEM_PREPARADO'\n }\n };\n});\n\nreturn items;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4368,
1104
],
"id": "4f0e23fe-0c3d-4cc1-8069-716b76ea3ce2",
"name": "Code - Separar media en items WhatsApp TEST"
},
{
"parameters": {
"method": "POST",
"url": "https://wsp.gomezleemarketing.com/chat/getBase64FromMediaMessage/botsoporte",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{'C267126ABB45-4C12-B626-6BAB1833F5D7'}}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"message\": {\n \"key\": {\n \"id\": \"{{ $json.media_source_id }}\"\n }\n },\n \"convertToMp4\": true\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
-4160,
1104
],
"id": "a7b7a1fa-3c27-49e2-9606-3b8148b1c443",
"name": "HTTP Request - Obtener media base64 Evolution TEST",
"alwaysOutputData": true,
"disabled": true
},
{
"parameters": {
"jsCode": "const httpItems = $input.all();\nconst mediaItems = $('Code - Separar media en items WhatsApp TEST').all();\n\nconst buscarBase64 = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.base64 === 'string') return obj.base64;\n if (typeof obj.data === 'string') return obj.data;\n if (typeof obj.media === 'string') return obj.media;\n\n if (obj.message && typeof obj.message.base64 === 'string') return obj.message.base64;\n if (obj.response && typeof obj.response.base64 === 'string') return obj.response.base64;\n if (obj.data && typeof obj.data.base64 === 'string') return obj.data.base64;\n\n return '';\n};\n\nreturn httpItems.map((item, index) => {\n const respuesta = item.json || {};\n const mediaItem = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n let base64 = buscarBase64(respuesta);\n\n base64 = String(base64 || '').trim();\n base64 = base64.replace(/^data:.*?;base64,/, '');\n\n if (!base64) {\n throw new Error(`No se encontró base64 para media_source_id: ${mediaItem.media_source_id || 'SIN_ID'}`);\n }\n\n return {\n json: {\n ...mediaItem,\n\n base64_length: base64.length,\n estado_procesamiento: 'MEDIA_BASE64_RECUPERADA'\n },\n binary: {\n data: {\n data: base64,\n mimeType: mediaItem.media_mime_type || 'application/octet-stream',\n fileName: mediaItem.media_file_name || 'media_whatsapp.bin'\n }\n }\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3952,
1104
],
"id": "61204b9e-0419-444a-9e39-c87a56b88895",
"name": "Code - Convertir base64 a binario WhatsApp TEST"
},
{
"parameters": {
"name": "={{ $json.media_file_name }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"folderId": {
"__rl": true,
"value": "=1G11WZnOwPp7UC2moPE6rEvlPFl7_OoSB",
"mode": "id"
},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
-3712,
1104
],
"id": "87632ce1-965c-4354-b06f-ac8ce766e7d4",
"name": "Drive - Subir media WhatsApp TEST",
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
}
},
{
"parameters": {
"jsCode": "const driveItems = $input.all();\nconst mediaItems = $('Code - Convertir base64 a binario WhatsApp TEST').all();\n\nreturn driveItems.map((item, index) => {\n const drive = item.json || {};\n const media = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n const fileId =\n drive.id ||\n drive.fileId ||\n drive.file_id ||\n '';\n\n const webViewLink =\n drive.webViewLink ||\n drive.webContentLink ||\n (fileId ? `https://drive.google.com/file/d/${fileId}/view` : '');\n\n return {\n json: {\n ...media,\n\n drive_file_id: fileId,\n drive_file_name: media.media_file_name,\n drive_file_url: webViewLink,\n\n estado_procesamiento: 'MEDIA_GUARDADA_EN_DRIVE'\n }\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3504,
1104
],
"id": "ae4d3939-49c5-4794-bbbb-eda739313673",
"name": "Code - Preparar registro media Drive WhatsApp TEST"
},
{
"parameters": {
"jsCode": "const mediaItems = $('Code - Preparar registro media Drive WhatsApp TEST').all();\n\nconst media = mediaItems.map(item => item.json || {});\n\nconst base = media[0] || {};\n\nconst audios = media.filter(m => String(m.media_type || '').toLowerCase() === 'audio');\nconst imagenes = media.filter(m => String(m.media_type || '').toLowerCase() === 'image');\nconst videos = media.filter(m => String(m.media_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || '',\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n\n audio_drive_urls: audios.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n imagenes_drive_urls: imagenes.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n videos_drive_urls: videos.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n\n total_media_drive: media.length,\n total_audio_drive: audios.length,\n total_imagenes_drive: imagenes.length,\n total_videos_drive: videos.length,\n\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_ANALIZAR',\n motivo_revision: 'MEDIA_RECUPERADA',\n\n estado_procesamiento: 'MEDIA_RECUPERADA_Y_REGISTRADA'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3088,
1104
],
"id": "5e6b2d61-89b0-4d31-a371-51e38b01d88c",
"name": "Code - Consolidar media recuperada WhatsApp TEST"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 1962428033,
"mode": "list",
"cachedResultName": "wa_ejecuciones_media",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1962428033"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"media_event_id": "={{ $json.media_event_id }}",
"media_type": "={{ $json.media_type }}",
"media_mime_type": "={{ $json.media_mime_type }}",
"media_file_name": "={{ $json.drive_file_name }}",
"drive_file_id": "={{ $json.drive_file_id }}",
"drive_file_url": "={{ $json.drive_file_url }}",
"fecha_guardado": "={{ new Date().toISOString() }}",
"estado": "=MEDIA_GUARDADA_EN_DRIVE",
"media_index": "={{ $json.media_index }}",
"media_total": "={{ $json.media_total }}",
"whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}"
},
"matchingColumns": [],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_event_id",
"displayName": "media_event_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_type",
"displayName": "media_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_mime_type",
"displayName": "media_mime_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_file_name",
"displayName": "media_file_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "drive_file_id",
"displayName": "drive_file_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "drive_file_url",
"displayName": "drive_file_url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_guardado",
"displayName": "fecha_guardado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_index",
"displayName": "media_index",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_total",
"displayName": "media_total",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "whatsapp_remote_jid",
"displayName": "whatsapp_remote_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-3296,
1104
],
"id": "71354a86-ea50-48df-8262-5882092dcb85",
"name": "Sheets - Guardar media Drive WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"motivo_revision": "={{ $json.motivo_revision }}",
"estado": "={{ $json.estado }}",
"etapa": "={{ $json.etapa }}",
"ultima_actividad": "={{ $json.ultima_actividad }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-2880,
1104
],
"id": "e23d8301-e104-4a02-9203-60d9f5af599b",
"name": "Sheets - Actualizar sesión media recuperada WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const data = $('Code - Consolidar media recuperada WhatsApp TEST').first().json || {};\n\nconst audioUrls = String(data.audio_drive_urls || '').trim();\nconst imagenesUrls = String(data.imagenes_drive_urls || '').trim();\nconst videosUrls = String(data.videos_drive_urls || '').trim();\n\nconst prompt = `\nAnaliza esta ejecución de propuesta usando la evidencia disponible.\n\nIMPORTANTE:\n- La nota de voz contiene la descripción principal de la ejecución.\n- Las imágenes muestran evidencia visual obligatoria.\n- Los videos son evidencia opcional adicional.\n- No inventes datos que no estén claros.\n- Si un dato no aparece, marca \"No identificado\".\n\nDebes devolver SOLO un JSON válido con esta estructura:\n\n{\n \"propuesta_referencia\": \"\",\n \"marca\": \"\",\n \"cliente\": \"\",\n \"pais\": \"\",\n \"ubicacion\": \"\",\n \"fecha_ejecucion\": \"\",\n \"que_se_implemento\": \"\",\n \"comentarios_resultados\": \"\",\n \"resumen_ejecucion\": \"\",\n \"nivel_confianza\": \"\",\n \"requiere_revision\": \"\",\n \"motivo_revision\": \"\"\n}\n\nDatos de control:\nsession_id: ${data.session_id}\nejecucion_id: ${data.ejecucion_id}\nmanager_nombre: ${data.manager_nombre}\nmanager_telefono: ${data.manager_telefono}\n\nArchivos de audio en Drive:\n${audioUrls || 'No hay audio registrado'}\n\nImágenes en Drive:\n${imagenesUrls || 'No hay imágenes registradas'}\n\nVideos en Drive:\n${videosUrls || 'No hay videos registrados'}\n`.trim();\n\nreturn [\n {\n json: {\n ...data,\n gemini_prompt: prompt,\n estado_procesamiento: 'PAQUETE_GEMINI_PREPARADO'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2672,
1104
],
"id": "28417401-35f1-4448-9305-34d82352836f",
"name": "Code - Preparar paquete análisis Gemini WhatsApp TEST"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"leftValue": "={{ String($json.media_type || '').toLowerCase() }}",
"rightValue": "audio",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "ddf19bdc-2a61-4261-8950-02ea3026ba01"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "audio"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "dcf1d733-7b80-4f67-b6f6-9e965a5bae24",
"leftValue": "={{ String($json.media_type || '').toLowerCase() }}",
"rightValue": "image",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "=image"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "bb7e48e1-8383-463c-a43e-7e8e5d1ae21d",
"leftValue": "={{ String($json.media_type || '').toLowerCase() }}",
"rightValue": "video",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "VIDEO"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
-1424,
2224
],
"id": "5095c259-4595-4537-9913-9eeb1e4ce964",
"name": "Switch - Tipo media para Gemini WhatsApp TEST"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item;\n\nconst data = item.json || {};\nconst binaryData = item.binary?.data;\n\nif (!binaryData) {\n throw new Error('No llegó binary.data al nodo de audio para Gemini');\n}\n\nconst geminiPromptAudio = `\nAnaliza esta nota de voz enviada por WhatsApp para el flujo de registro de propuestas de Fulgencio Fumado.\n\nObjetivo:\nExtraer información útil del reporte y clasificar correctamente el tipo de reporte.\n\nPrimero, identifica el tipo de reporte usando exactamente uno de estos valores:\n\n1. PROPUESTA_EJECUTADA\nCuando el manager reporta evidencia de una propuesta que ya fue implementada, instalada, ejecutada, realizada o activada.\n\n2. PROPUESTA_EXTERNA\nCuando el manager reporta una propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n\n3. NO_DETERMINADO\nCuando la nota de voz no permite saber claramente si es una propuesta ejecutada o una propuesta externa.\n\nReglas para clasificar:\n- Si el manager dice explícitamente \"propuesta ejecutada\", clasifica como PROPUESTA_EJECUTADA.\n- Si el manager dice que recibió una propuesta externa, referencia externa o algo fuera del banco, clasifica como PROPUESTA_EXTERNA.\n- Si solo hay una prueba de audio, saludo, ruido, conteo, información incompleta o no se entiende el objetivo, clasifica como NO_DETERMINADO.\n- No inventes el tipo de reporte.\n- Si hay duda, usa NO_DETERMINADO.\n\nAdemás, extrae la mayor cantidad posible de información útil:\n- Nombre o referencia de la propuesta\n- Marca o cliente\n- País\n- Ubicación\n- Fecha de ejecución o fecha del reporte\n- Qué se implementó o qué se está reportando\n- Comentarios o resultados observados\n- Si el manager menciona que es ejecutada, externa o no queda claro\n\nDevuelve el análisis en texto claro y estructurado, incluyendo obligatoriamente estas líneas:\n\nTipo de reporte detectado: PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\nConfianza del tipo de reporte: ALTA | MEDIA | BAJA\nMotivo del tipo de reporte: explicación breve\n\nNo inventes datos. Si algo no está claro, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_audio: geminiPromptAudio\n },\n binary: {\n data: binaryData\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-944,
2048
],
"id": "7404bc52-fd09-40e9-94ab-27157f4be4ed",
"name": "Code - Preparar Gemini audio WhatsApp TEST"
},
{
"parameters": {
"resource": "audio",
"operation": "analyze",
"modelId": {
"__rl": true,
"value": "models/gemini-2.5-pro",
"mode": "list",
"cachedResultName": "models/gemini-2.5-pro"
},
"text": "={{ $json.gemini_prompt_audio }}",
"inputType": "binary",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1.2,
"position": [
-736,
2048
],
"id": "353aba0e-8ed5-484a-b8fc-65ee7829a951",
"name": "Gemini - Analizar audio WhatsApp TEST",
"retryOnFail": true,
"waitBetweenTries": 5000,
"credentials": {
"googlePalmApi": {
"id": "jvsXYwL6IOoY2DBU",
"name": "Isaac - Gemini Api Pago"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const geminiItems = $input.all();\nconst prepItems = $('Code - Preparar Gemini audio WhatsApp TEST').all();\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction safeJson(value) {\n try {\n return JSON.stringify(value);\n } catch (e) {\n return String(value ?? '');\n }\n}\n\nfunction extractTextFromGemini(obj) {\n if (!obj) return '';\n\n if (typeof obj === 'string') {\n return clean(obj);\n }\n\n if (typeof obj.text === 'string') return clean(obj.text);\n if (typeof obj.output === 'string') return clean(obj.output);\n if (typeof obj.response === 'string') return clean(obj.response);\n if (typeof obj.content === 'string') return clean(obj.content);\n\n const possibleParts =\n obj?.content?.parts ||\n obj?.candidates?.[0]?.content?.parts ||\n obj?.response?.candidates?.[0]?.content?.parts ||\n obj?.parts ||\n [];\n\n if (Array.isArray(possibleParts)) {\n const text = possibleParts\n .map(part => clean(part?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (text) return text;\n }\n\n const jsonText = safeJson(obj);\n return jsonText && jsonText !== '{}' ? jsonText : '';\n}\n\nfunction detectGeminiError(obj) {\n if (!obj || typeof obj !== 'object') return '';\n\n const candidates = [\n obj.error,\n obj.message,\n obj.description,\n obj.fullMessage,\n obj.full_message,\n obj.errorMessage,\n obj.error_message,\n obj?.error?.message,\n obj?.error?.description,\n obj?.error?.cause,\n obj?.json?.error,\n obj?.json?.message,\n ];\n\n const found = candidates\n .map(clean)\n .filter(Boolean)\n .join(' | ');\n\n const serialized = safeJson(obj);\n\n if (\n found ||\n serialized.includes('Internal error') ||\n serialized.includes('INTERNAL') ||\n serialized.includes('500') ||\n serialized.includes('service was not able to process')\n ) {\n return found || serialized;\n }\n\n return '';\n}\n\nconst output = [];\n\nconst total = Math.max(geminiItems.length, prepItems.length);\n\nfor (let i = 0; i < total; i++) {\n const geminiItem = geminiItems[i] || {};\n const prepItem = prepItems[i] || {};\n\n const geminiJson = geminiItem.json || {};\n const mediaJson = prepItem.json || geminiJson || {};\n\n const sessionId = clean(mediaJson.session_id);\n const ejecucionId = clean(mediaJson.ejecucion_id || sessionId);\n const mediaEventId = clean(mediaJson.media_event_id || mediaJson.event_id);\n const mediaFileName = clean(mediaJson.media_file_name || mediaJson.file_name || mediaJson.nombre_archivo);\n const driveFileId = clean(mediaJson.drive_file_id || mediaJson.file_id);\n const driveFileUrl = clean(mediaJson.drive_file_url || mediaJson.media_drive_url || mediaJson.webViewLink);\n\n const errorGemini = detectGeminiError(geminiJson);\n\n let analisisTexto = '';\n let estado = '';\n let requiereRevision = false;\n\n if (errorGemini) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_ERROR_GEMINI';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini no pudo procesar esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar propuesta, cliente, marca, país, ubicación o contexto.',\n '',\n `Detalle técnico: ${errorGemini}`,\n ].join('\\n');\n } else {\n analisisTexto = extractTextFromGemini(geminiJson);\n\n if (!analisisTexto) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_VACIO';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini respondió, pero no devolvió texto útil para esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar la propuesta.',\n ].join('\\n');\n } else {\n estado = 'ANALISIS_AUDIO_COMPLETADO';\n }\n }\n\n if (!sessionId) {\n throw new Error('No llegó session_id al normalizar análisis de audio.');\n }\n\n if (!mediaEventId) {\n throw new Error('No llegó media_event_id al normalizar análisis de audio.');\n }\n\n output.push({\n json: {\n ...mediaJson,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: 'audio',\n media_file_name: mediaFileName,\n drive_file_id: driveFileId,\n drive_file_url: driveFileUrl,\n\n analisis_texto: analisisTexto,\n fecha_analisis: nowIso(),\n estado,\n\n gemini_audio_error: errorGemini || '',\n audio_requiere_revision: requiereRevision,\n\n media_index: Number(mediaJson.media_index || 0),\n media_total: Number(mediaJson.media_total || mediaJson.media_total_esperado || 0),\n },\n });\n}\n\nreturn output;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-528,
2048
],
"id": "fa99c19f-45ca-408d-883a-ffeb4d32ca9a",
"name": "Code - Normalizar análisis audio WhatsApp TEST"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const input = $input.item;\nconst data = input.json || {};\nconst binaries = input.binary || {};\n\nconst binaryKeys = Object.keys(binaries);\nconst sourceBinaryKey = binaries.data ? 'data' : binaryKeys[0];\n\nif (!sourceBinaryKey || !binaries[sourceBinaryKey]) {\n throw new Error(\n `No llegó archivo binario para Gemini imagen. Binary keys recibidas: ${binaryKeys.join(', ') || 'NINGUNA'}`\n );\n}\n\nconst binaryData = {\n ...binaries[sourceBinaryKey],\n};\n\nbinaryData.fileName =\n binaryData.fileName ||\n data.media_file_name ||\n `imagen_${data.media_index || Date.now()}.jpg`;\n\nbinaryData.mimeType =\n binaryData.mimeType ||\n data.mime_type ||\n data.mimetype ||\n 'image/jpeg';\n\nconst geminiPromptImagen = `\nAnaliza esta imagen como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en la imagen\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Posible ubicación visible\n- Calidad de la implementación\n- Detalles relevantes para documentar la ejecución\n\nNo inventes datos que no sean visibles.\nSi algo no se puede identificar, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_imagen: geminiPromptImagen,\n binary_input_field: 'data',\n debug_binary_imagen: {\n binary_keys_recibidas: binaryKeys,\n source_binary_key: sourceBinaryKey,\n output_binary_key: 'data',\n file_name: binaryData.fileName,\n mime_type: binaryData.mimeType,\n },\n },\n binary: {\n data: binaryData,\n },\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-928,
2272
],
"id": "3e673d5d-5aa4-4d74-8a09-eeac3fb67c97",
"name": "Code - Preparar Gemini imagen WhatsApp TEST"
},
{
"parameters": {
"resource": "image",
"operation": "analyze",
"modelId": {
"__rl": true,
"value": "models/gemini-2.5-pro",
"mode": "list",
"cachedResultName": "models/gemini-2.5-pro"
},
"text": "={{ $json.gemini_prompt_imagen }}",
"inputType": "binary",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1.2,
"position": [
-720,
2272
],
"id": "212e7998-4787-445d-824e-5e7b3911e206",
"name": "Gemini - Analizar imagen WhatsApp TEST",
"credentials": {
"googlePalmApi": {
"id": "jvsXYwL6IOoY2DBU",
"name": "Isaac - Gemini Api Pago"
}
}
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini imagen WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'image',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_IMAGEN_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-512,
2272
],
"id": "7b2891dd-09dc-40e1-a470-486debfc6418",
"name": "Code - Normalizar análisis imagen WhatsApp TEST"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const data = $json || {};\nconst binary = $binary || {};\n\nif (!binary.data) {\n throw new Error('No llegó binary.data al nodo Code - Preparar Gemini video.');\n}\n\nconst fileName =\n binary.data.fileName ||\n data.media_file_name ||\n data.file_name ||\n `video_${Date.now()}.mp4`;\n\nconst mimeType =\n binary.data.mimeType ||\n data.mime_type ||\n 'video/mp4';\n\n// Fuerza a n8n a cargar el contenido real del binario.\n// Esto evita el error de Gemini: \"Received undefined\".\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n\nif (!buffer || !Buffer.isBuffer(buffer) || buffer.length === 0) {\n throw new Error('El video llegó con metadata, pero sin contenido binario real.');\n}\n\nconst preparedBinary = await this.helpers.prepareBinaryData(\n buffer,\n fileName,\n mimeType\n);\n\nconst geminiPromptVideo = `\nAnaliza este video como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en el video\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Interacciones o movimientos relevantes\n- Posible ubicación visible\n- Calidad de la implementación\n- Resultados observables\n\nNo inventes datos que no sean visibles.\n`.trim();\n\nreturn {\n json: {\n ...data,\n media_type: 'video',\n media_file_name: fileName,\n mime_type: mimeType,\n gemini_prompt_video: geminiPromptVideo,\n },\n binary: {\n data: preparedBinary,\n },\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1056,
2512
],
"id": "7992ddf7-5394-4126-902c-0eec813ca6f3",
"name": "Code - Preparar Gemini video WhatsApp TEST"
},
{
"parameters": {
"resource": "video",
"operation": "analyze",
"modelId": {
"__rl": true,
"value": "models/gemini-2.5-pro",
"mode": "list",
"cachedResultName": "models/gemini-2.5-pro"
},
"text": "={{ $json.gemini_prompt_video }}",
"inputType": "binary",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1.2,
"position": [
-848,
2512
],
"id": "4e0c63b2-429f-4908-8c57-69093c2db609",
"name": "Analyze video",
"credentials": {
"googlePalmApi": {
"id": "jvsXYwL6IOoY2DBU",
"name": "Isaac - Gemini Api Pago"
}
}
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini video WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'video',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_VIDEO_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-640,
2512
],
"id": "a05f39fa-f0df-4e8b-81f7-8ea4b9b5febc",
"name": "Code - Normalizar análisis video WhatsApp TEST"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 1107394537,
"mode": "list",
"cachedResultName": "wa_ejecuciones_analisis_media",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"media_event_id": "={{ $json.media_event_id }}",
"media_type": "={{ $json.media_type }}",
"media_file_name": "={{ $json.media_file_name }}",
"analisis_texto": "={{ $json.analisis_texto }}",
"fecha_analisis": "={{ $json.fecha_analisis }}",
"estado": "={{ $json.estado }}",
"media_index": "={{ $json.media_index }}",
"media_total": "={{ $json.media_total }}"
},
"matchingColumns": [],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_event_id",
"displayName": "media_event_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_type",
"displayName": "media_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_file_name",
"displayName": "media_file_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "analisis_texto",
"displayName": "analisis_texto",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_analisis",
"displayName": "fecha_analisis",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_index",
"displayName": "media_index",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_total",
"displayName": "media_total",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
144,
2304
],
"id": "7a6a54c1-687f-479a-b512-8490a433b60e",
"name": "Sheets - Guardar análisis media Gemini WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 1107394537,
"mode": "list",
"cachedResultName": "wa_ejecuciones_analisis_media",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
3936,
2112
],
"id": "73ae1420-d16f-425b-b6dd-ab4f92dd81ad",
"name": "Sheets - Leer análisis media Gemini WhatsApp TEST",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst inputRows = $input.all().map(item => item.json || {});\nconst actual = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst extraerTextoGemini = (valor) => {\n if (valor === null || valor === undefined) return '';\n\n if (typeof valor === 'object') {\n const partes =\n valor?.content?.parts ||\n valor?.response?.content?.parts ||\n valor?.candidates?.[0]?.content?.parts ||\n valor?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(valor);\n }\n\n const raw = limpiar(valor);\n if (!raw) return '';\n\n try {\n const parsed = JSON.parse(raw);\n\n const partes =\n parsed?.content?.parts ||\n parsed?.response?.content?.parts ||\n parsed?.candidates?.[0]?.content?.parts ||\n parsed?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(parsed);\n } catch (error) {\n return raw;\n }\n};\n\nconst normalizarTipoMedia = (valor) => {\n const t = limpiar(valor).toLowerCase();\n\n if (t.includes('audio')) return 'audio';\n if (t.includes('image') || t.includes('imagen')) return 'image';\n if (t.includes('video')) return 'video';\n\n return t || 'unknown';\n};\n\nconst obtenerFecha = (row) => {\n const fecha = new Date(row.fecha_analisis || row.fecha_recepcion || row.ultima_actividad || 0);\n const time = fecha.getTime();\n return Number.isFinite(time) ? time : 0;\n};\n\n// Contextos posibles del cierre actual.\n// IMPORTANTE: NO usamos $json como prioridad, porque aquí $json puede ser la primera fila del Sheet.\nconst decisionPorSheet = getNodeJson('Code - Decidir cierre análisis por Sheet Redis TEST');\nconst validarLock = getNodeJson('Code - Validar lock análisis final TEST');\nconst validarConteo = getNodeJson('Code - Validar conteo análisis Redis TEST');\nconst cierreMedia = getNodeJson('Code - Consolidar media recuperada WhatsApp TEST');\nconst eventoPaso = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst eventoSesion = getNodeJson('Code - Resolver sesión WhatsApp TEST');\n\nlet sessionId = limpiar(\n decisionPorSheet.session_id ||\n validarLock.session_id ||\n validarConteo.session_id ||\n cierreMedia.session_id ||\n eventoPaso.session_id ||\n eventoSesion.session_id ||\n ''\n);\n\nlet ejecucionId = limpiar(\n decisionPorSheet.ejecucion_id ||\n validarLock.ejecucion_id ||\n validarConteo.ejecucion_id ||\n cierreMedia.ejecucion_id ||\n eventoPaso.ejecucion_id ||\n eventoSesion.ejecucion_id ||\n sessionId\n);\n\n// Si por cualquier razón no llegó session_id desde el contexto,\n// usamos la sesión más reciente del Sheet leído.\nif (!sessionId) {\n const grupos = {};\n\n for (const row of inputRows) {\n const sid = limpiar(row.session_id);\n if (!sid) continue;\n\n if (!grupos[sid]) {\n grupos[sid] = {\n session_id: sid,\n ejecucion_id: limpiar(row.ejecucion_id || sid),\n rows: [],\n ultima_fecha: 0\n };\n }\n\n grupos[sid].rows.push(row);\n grupos[sid].ultima_fecha = Math.max(grupos[sid].ultima_fecha, obtenerFecha(row));\n }\n\n const grupoMasReciente = Object.values(grupos)\n .sort((a, b) => b.ultima_fecha - a.ultima_fecha)[0];\n\n if (grupoMasReciente) {\n sessionId = grupoMasReciente.session_id;\n ejecucionId = grupoMasReciente.ejecucion_id || sessionId;\n }\n}\n\nif (!sessionId) {\n throw new Error('No llegó session_id para consolidar análisis de media.');\n}\n\n// Primero intentamos usar las filas filtradas por el nodo de cierre.\n// Si no vienen, usamos las filas leídas del Sheet.\nlet rows = [];\n\nif (\n Array.isArray(decisionPorSheet.analisis_rows_filtrados) &&\n decisionPorSheet.analisis_rows_filtrados.length > 0\n) {\n rows = decisionPorSheet.analisis_rows_filtrados;\n} else {\n rows = inputRows;\n}\n\n// Filtrar solo la sesión correcta.\nrows = rows.filter(row => limpiar(row.session_id) === sessionId);\n\n// Si todavía no encontró nada, usar fallback por ejecución.\nif (rows.length === 0 && ejecucionId) {\n rows = inputRows.filter(row => limpiar(row.ejecucion_id) === ejecucionId);\n}\n\n// Deduplicar por media_event_id + media_type.\nconst vistos = new Set();\nconst analisisUnicos = [];\n\nfor (const row of rows) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaType = normalizarTipoMedia(row.media_type);\n\n if (!mediaEventId || !mediaType) continue;\n\n const key = `${mediaEventId}_${mediaType}`;\n if (vistos.has(key)) continue;\n\n const textoAnalisis = extraerTextoGemini(row.analisis_texto);\n\n if (!textoAnalisis) continue;\n\n vistos.add(key);\n\n analisisUnicos.push({\n ...row,\n media_type: mediaType,\n media_index: Number(row.media_index || 0),\n media_total: Number(row.media_total || row.media_total_esperado || 0),\n analisis_texto_limpio: textoAnalisis\n });\n}\n\nanalisisUnicos.sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst audios = analisisUnicos.filter(row => row.media_type === 'audio');\nconst imagenes = analisisUnicos.filter(row => row.media_type === 'image');\nconst videos = analisisUnicos.filter(row => row.media_type === 'video');\n\nconst audiosCount = audios.length;\nconst fotosCount = imagenes.length;\nconst videosCount = videos.length;\n\nconst mediaTotalEsperado = Number(\n decisionPorSheet.media_total_esperado ||\n validarConteo.media_total_esperado ||\n cierreMedia.total_media_drive ||\n cierreMedia.total_media ||\n analisisUnicos[0]?.media_total ||\n analisisUnicos.length ||\n 0\n);\n\nconst analisisAudioTexto = audios\n .map(row => row.analisis_texto_limpio)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisImagenesTexto = imagenes\n .map((row, index) => `IMAGEN ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisVideosTexto = videos\n .map((row, index) => `VIDEO ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisConsolidado = [\n analisisAudioTexto ? `ANÁLISIS DE AUDIO:\\n${analisisAudioTexto}` : '',\n analisisImagenesTexto ? `ANÁLISIS DE IMÁGENES:\\n${analisisImagenesTexto}` : '',\n analisisVideosTexto ? `ANÁLISIS DE VIDEOS:\\n${analisisVideosTexto}` : ''\n].filter(Boolean).join('\\n\\n');\n\nif (!analisisConsolidado) {\n throw new Error(\n `No hay análisis multimedia consolidado para enviar a Gemini final. session_id=${sessionId}, ejecucion_id=${ejecucionId}, rows_filtradas=${rows.length}, input_rows=${inputRows.length}`\n );\n}\n\nconst transcripcionAudio = analisisAudioTexto || '';\n\nconst geminiPromptFinal = `\nEres un analista de evidencias de GomezLee Marketing / Fulgencio Fumado.\n\nTu tarea es consolidar el análisis de audio, imágenes y videos de un reporte recibido por WhatsApp.\n\nIMPORTANTE:\n- No inventes información.\n- Si un dato no aparece claramente, responde \"No identificado\".\n- El audio tiene prioridad para identificar propuesta, marca, país, ubicación, fecha y tipo de reporte.\n- Las imágenes y videos sirven como evidencia visual.\n- Clasifica el reporte usando exactamente uno de estos valores:\n - PROPUESTA_EJECUTADA\n - PROPUESTA_EXTERNA\n - NO_DETERMINADO\n\nDefiniciones:\n- PROPUESTA_EJECUTADA: evidencia de una propuesta ya implementada, instalada, ejecutada, realizada o activada.\n- PROPUESTA_EXTERNA: propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n- NO_DETERMINADO: la información no permite saber claramente si es ejecutada o externa.\n\nDATOS DEL REPORTE:\n- session_id: ${sessionId}\n- ejecucion_id: ${ejecucionId}\n- audios_count: ${audiosCount}\n- fotos_count: ${fotosCount}\n- videos_count: ${videosCount}\n\nANÁLISIS MULTIMEDIA CONSOLIDADO:\n${analisisConsolidado}\n\nDevuelve ÚNICAMENTE un JSON válido, sin markdown, sin explicación adicional y sin texto fuera del JSON.\n\nEl JSON debe tener exactamente esta estructura:\n\n{\n \"tipo_reporte\": \"PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\",\n \"tipo_reporte_confianza\": \"ALTA | MEDIA | BAJA\",\n \"motivo_tipo_reporte\": \"texto breve\",\n \"propuesta_referencia\": \"texto o No identificado\",\n \"marca\": \"texto o No identificado\",\n \"cliente\": \"texto o No identificado\",\n \"pais\": \"texto o No identificado\",\n \"ubicacion\": \"texto o No identificado\",\n \"fecha_ejecucion\": \"texto o No identificado\",\n \"descripcion_ejecucion\": \"texto claro y profesional\",\n \"elementos_detectados\": \"lista resumida en texto\",\n \"resumen_ia\": \"resumen ejecutivo del reporte\",\n \"comentarios_resultados\": \"texto o No identificado\",\n \"tags\": \"tags separados por coma\"\n}\n`.trim();\n\nreturn [\n {\n json: {\n ...cierreMedia,\n ...eventoSesion,\n ...eventoPaso,\n ...validarConteo,\n ...validarLock,\n ...decisionPorSheet,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_media: analisisUnicos.length,\n\n audio_count: audiosCount,\n audios_count: audiosCount,\n imagenes_count: fotosCount,\n fotos_count: fotosCount,\n videos_count: videosCount,\n\n transcripcion_audio: transcripcionAudio,\n\n analisis_audio: analisisAudioTexto,\n analisis_imagenes: analisisImagenesTexto,\n analisis_videos: analisisVideosTexto,\n\n analisis_audio_texto: analisisAudioTexto,\n analisis_imagenes_texto: analisisImagenesTexto,\n analisis_videos_texto: analisisVideosTexto,\n\n analisis_media_consolidado: analisisConsolidado,\n analisis_multimedia: analisisConsolidado,\n analisis_consolidado: analisisConsolidado,\n\n gemini_prompt_final: geminiPromptFinal,\n\n analisis_rows_filtrados: analisisUnicos,\n\n estado_analisis_media: 'ANALISIS_MEDIA_CONSOLIDADO',\n\n consolidacion_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n input_rows: inputRows.length,\n rows_filtradas: rows.length,\n total_analisis_unicos: analisisUnicos.length,\n media_total_esperado: mediaTotalEsperado,\n audios: audiosCount,\n imagenes: fotosCount,\n videos: videosCount,\n gemini_prompt_final_generado: Boolean(geminiPromptFinal),\n gemini_prompt_final_length: geminiPromptFinal.length,\n uso_decision_por_sheet: Object.keys(decisionPorSheet).length > 0,\n uso_validar_lock: Object.keys(validarLock).length > 0,\n uso_validar_conteo: Object.keys(validarConteo).length > 0,\n media: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado,\n media_file_name: row.media_file_name\n }))\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4832,
2144
],
"id": "43fee68c-e52d-4323-8175-fa95b0ed8531",
"name": "Code - Consolidar análisis media Gemini WhatsApp TEST"
},
{
"parameters": {
"modelId": {
"__rl": true,
"value": "models/gemini-2.5-pro",
"mode": "list",
"cachedResultName": "models/gemini-2.5-pro"
},
"messages": {
"values": [
{
"content": "={{ $json.gemini_prompt_final }}"
}
]
},
"builtInTools": {},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1.2,
"position": [
5024,
2144
],
"id": "73c6e7dd-5ac4-4673-9780-7ab545a4e8c5",
"name": "Gemini - Generar JSON final propuesta ejecutada TEST",
"credentials": {
"googlePalmApi": {
"id": "jvsXYwL6IOoY2DBU",
"name": "Isaac - Gemini Api Pago"
}
}
},
{
"parameters": {
"jsCode": "const gemini = $json || {};\nconst base = $('Code - Consolidar análisis media Gemini WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst esNoIdentificado = (valor) => {\n const texto = normalizarPlano(valor);\n\n return (\n !texto ||\n texto === 'NO IDENTIFICADO' ||\n texto === 'NO DISPONIBLE' ||\n texto === 'N/A' ||\n texto === 'NA' ||\n texto === 'NULL' ||\n texto === 'UNDEFINED'\n );\n};\n\nconst valor = (campo, fallback = 'No identificado') => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return fallback;\n\n return v;\n};\n\nconst valorOpcional = (campo) => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return '';\n\n return v;\n};\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (Array.isArray(obj.content?.parts)) {\n const txt = obj.content.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.parts)) {\n const txt = obj.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.candidates)) {\n const txt = obj.candidates\n .flatMap(c => c.content?.parts || [])\n .map(p => p.text || '')\n .join('\\n')\n .trim();\n\n if (txt) return txt;\n }\n\n if (obj.message && typeof obj.message === 'object') {\n const txt = extraerTexto(obj.message);\n if (txt) return txt;\n }\n\n if (obj.data && typeof obj.data === 'object') {\n const txt = extraerTexto(obj.data);\n if (txt) return txt;\n }\n\n return JSON.stringify(obj);\n};\n\nlet texto = extraerTexto(gemini).trim();\n\ntexto = texto\n .replace(/^```json/i, '')\n .replace(/^```/i, '')\n .replace(/```$/i, '')\n .trim();\n\nlet parsed = {};\n\ntry {\n parsed = JSON.parse(texto);\n} catch (error) {\n parsed = {\n tipo_reporte: 'NO_DETERMINADO',\n tipo_reporte_confianza: 'BAJA',\n tipo_reporte_motivo: `No se pudo parsear el JSON final de Gemini: ${error.message}`,\n\n propuesta_referencia: 'No identificado',\n marca: 'No identificado',\n cliente: 'No identificado',\n pais: 'No identificado',\n ubicacion: 'No identificado',\n fecha_ejecucion: 'No identificado',\n que_se_implemento: 'No identificado',\n comentarios_resultados: '',\n resumen_ejecucion: texto || 'No se pudo parsear el JSON de Gemini.',\n evidencia_audio_resumen: base.texto_audio || '',\n evidencia_imagenes_resumen: base.texto_imagenes || '',\n evidencia_videos_resumen: base.texto_videos || '',\n nivel_confianza: 'BAJA',\n requiere_revision: 'SI',\n motivo_revision: `Error parseando JSON final: ${error.message}`\n };\n}\n\nconst normalizarTipoReporte = (valorTipo) => {\n const texto = String(valorTipo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, '_');\n\n if (\n texto === 'PROPUESTA_EJECUTADA' ||\n texto === 'EJECUTADA' ||\n texto === 'EJECUTADO'\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n if (\n texto === 'PROPUESTA_EXTERNA' ||\n texto === 'EXTERNA' ||\n texto === 'EXTERNO'\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n return 'NO_DETERMINADO';\n};\n\nconst normalizarConfianza = (valorConfianza) => {\n const texto = String(valorConfianza ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['ALTA', 'MEDIA', 'BAJA'].includes(texto)) {\n return texto;\n }\n\n return 'BAJA';\n};\n\nconst normalizarSiNo = (valorSiNo, fallback = 'SI') => {\n const texto = String(valorSiNo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['SI', 'SÍ', 'YES', 'TRUE'].includes(texto)) return 'SI';\n if (['NO', 'FALSE'].includes(texto)) return 'NO';\n\n return fallback;\n};\n\nconst limpiarReferenciaVisual = (valorRef) => {\n let textoRef = limpiar(valorRef);\n\n if (esNoIdentificado(textoRef)) return 'No identificado';\n\n textoRef = textoRef\n .replace(/_/g, ' ')\n .replace(/\\+/g, '+')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return textoRef || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valorUbicacion) => {\n const textoUbicacion = normalizarPlano(valorUbicacion);\n\n if (!textoUbicacion) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bVERSIÓN\\b/,\n /\\bCODIGO\\b/,\n /\\bCÓDIGO\\b/,\n /\\bGUATE WMC\\b/,\n /\\bPOLLO CAMPERO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(textoUbicacion));\n};\n\nconst normalizarUbicacion = (valorUbicacion) => {\n const textoUbicacion = limpiar(valorUbicacion);\n\n if (esNoIdentificado(textoUbicacion)) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(textoUbicacion)) {\n return 'No identificado';\n }\n\n return textoUbicacion;\n};\n\nconst normalizarFechaEjecucion = (valorFecha) => {\n const textoFecha = limpiar(valorFecha);\n\n if (esNoIdentificado(textoFecha)) return 'No identificado';\n\n // Si solo viene el año, no es una fecha de ejecución real.\n if (/^20\\d{2}$/.test(textoFecha)) {\n return 'No identificado';\n }\n\n return textoFecha;\n};\n\nconst limitarTexto = (valorTexto, max = 1200, fallback = '') => {\n const textoLimpio = valor(valorTexto, fallback);\n\n if (!textoLimpio) return '';\n\n return textoLimpio.length > max\n ? `${textoLimpio.slice(0, max - 3)}...`\n : textoLimpio;\n};\n\nconst ahora = new Date().toISOString();\n\nconst tipoReporte = normalizarTipoReporte(parsed.tipo_reporte);\nconst tipoReporteConfianza = normalizarConfianza(parsed.tipo_reporte_confianza);\nconst tipoReporteMotivo = valor(parsed.tipo_reporte_motivo, 'No identificado');\n\nconst requiereRevision = normalizarSiNo(parsed.requiere_revision, 'SI');\n\nconst estadoRevision = requiereRevision === 'SI'\n ? 'PENDIENTE_REVISION'\n : 'ANALISIS_COMPLETADO';\n\nconst propuestaReferenciaOriginal = valor(parsed.propuesta_referencia);\nconst propuestaReferenciaLimpia = limpiarReferenciaVisual(propuestaReferenciaOriginal);\n\nconst fechaEjecucionOriginal = valor(parsed.fecha_ejecucion);\nconst fechaEjecucionLimpia = normalizarFechaEjecucion(fechaEjecucionOriginal);\n\nconst ubicacionOriginal = valor(parsed.ubicacion);\nconst ubicacionLimpia = normalizarUbicacion(ubicacionOriginal);\n\nconst evidenciaAudio = valorOpcional(parsed.evidencia_audio_resumen || base.texto_audio);\nconst evidenciaImagenes = valorOpcional(parsed.evidencia_imagenes_resumen || base.texto_imagenes);\nconst evidenciaVideos = valorOpcional(parsed.evidencia_videos_resumen || base.texto_videos);\n\nconst elementosDetectados = [\n evidenciaImagenes,\n evidenciaVideos\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .join('\\n\\n');\n\nconst tagsSet = new Set();\n\n[\n parsed.marca,\n parsed.cliente,\n parsed.pais,\n ubicacionLimpia\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .forEach(v => tagsSet.add(v));\n\nconst tags = [...tagsSet].join(', ');\n\nconst resumenIa = limitarTexto(parsed.resumen_ejecucion, 1200, '');\nconst descripcionEjecucion = limitarTexto(parsed.que_se_implemento, 1200, 'No identificado');\nconst comentariosResultados = limitarTexto(parsed.comentarios_resultados, 900, '');\n\nreturn [\n {\n json: {\n ...base,\n\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n session_id: base.session_id || '',\n\n fecha_recepcion: base.fecha_recepcion || ahora,\n fecha_ejecucion: fechaEjecucionLimpia,\n fecha_ejecucion_original_ia: fechaEjecucionOriginal,\n\n manager_nombre: valor(base.manager_nombre),\n manager_telefono: valor(base.manager_telefono),\n canal_origen: base.canal_origen || 'WHATSAPP',\n\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza,\n tipo_reporte_motivo: tipoReporteMotivo,\n\n propuesta_referencia: propuestaReferenciaLimpia,\n propuesta_referencia_original_ia: propuestaReferenciaOriginal,\n\n propuesta_match_estado: 'PENDIENTE',\n propuesta_match_confianza: '',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_match_revision: 'PENDIENTE_MATCH_BANCO',\n\n marca: valor(parsed.marca),\n cliente: valor(parsed.cliente),\n pais: valor(parsed.pais),\n ubicacion: ubicacionLimpia,\n ubicacion_original_ia: ubicacionOriginal,\n\n comentario_original: evidenciaAudio || comentariosResultados || '',\n resumen_ia: resumenIa,\n descripcion_ejecucion: descripcionEjecucion,\n comentarios_resultados: comentariosResultados,\n elementos_detectados: elementosDetectados,\n tags,\n\n media_folder_url: base.media_folder_url || '',\n presentacion_ejecucion_url: '',\n\n fotos_count: base.total_analisis_imagenes || 0,\n videos_count: base.total_analisis_videos || 0,\n audios_count: base.total_analisis_audio || 0,\n\n estado_revision: estadoRevision,\n motivo_revision: valor(parsed.motivo_revision, ''),\n ultima_actualizacion: ahora,\n\n transcripcion_audio: evidenciaAudio,\n\n nivel_confianza: normalizarConfianza(parsed.nivel_confianza),\n requiere_revision: requiereRevision,\n\n gemini_json_raw: texto,\n\n normalizacion_json_final_debug: {\n propuesta_referencia_original: propuestaReferenciaOriginal,\n propuesta_referencia_limpia: propuestaReferenciaLimpia,\n fecha_ejecucion_original: fechaEjecucionOriginal,\n fecha_ejecucion_limpia: fechaEjecucionLimpia,\n ubicacion_original: ubicacionOriginal,\n ubicacion_limpia: ubicacionLimpia,\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5376,
2144
],
"id": "fb3640a6-3044-43a0-a3dc-21ea0221bc08",
"name": "Code - Normalizar JSON final propuesta ejecutada TEST"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 737153956,
"mode": "list",
"cachedResultName": "propuestas_ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"ejecucion_id": "={{ $json.ejecucion_id }}",
"fecha_recepcion": "={{ $json.fecha_recepcion }}",
"fecha_ejecucion": "={{ $json.fecha_ejecucion }}",
"manager_nombre": "={{ $json.manager_nombre }}",
"manager_telefono": "={{ $json.manager_telefono }}",
"propuesta_referencia": "={{ $json.propuesta_referencia }}",
"propuesta_match_estado": "={{ $json.propuesta_match_estado }}",
"propuesta_match_confianza": "={{ $json.propuesta_match_confianza }}",
"propuesta_nombre_banco": "={{ $json.propuesta_nombre_banco }}",
"propuesta_link_banco": "={{ $json.propuesta_link_banco }}",
"marca": "={{ $json.marca }}",
"cliente": "={{ $json.cliente }}",
"pais": "={{ $json.pais }}",
"ubicacion": "={{ $json.ubicacion }}",
"comentario_original": "={{ $json.comentario_original }}",
"resumen_ia": "={{ $json.resumen_ia }}",
"descripcion_ejecucion": "={{ $json.descripcion_ejecucion }}",
"elementos_detectados": "={{ $json.elementos_detectados }}",
"tags": "={{ $json.tags }}",
"media_folder_url": "={{ $json.media_folder_url }}",
"presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}",
"fotos_count": "={{ $json.fotos_count }}",
"videos_count": "={{ $json.videos_count }}",
"audios_count": "={{ $json.audios_count }}",
"estado_revision": "={{ $json.estado_revision }}",
"motivo_revision": "={{ $json.motivo_revision }}",
"ultima_actualizacion": "={{ $json.ultima_actualizacion }}",
"session_id": "={{ $json.session_id }}",
"canal_origen": "={{ $json.canal_origen }}",
"transcripcion_audio": "={{ $json.transcripcion_audio }}",
"propuesta_match_revision": "={{ $json.propuesta_match_revision }}",
"tipo_reporte": "={{ $json.tipo_reporte }}",
"tipo_reporte_confianza": "={{ $json.tipo_reporte_confianza }}",
"tipo_reporte_motivo": "={{ $json.tipo_reporte_motivo }}",
"decision_automatica_banco": "={{ $json.decision_automatica_banco }}",
"motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}",
"banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}"
},
"matchingColumns": [],
"schema": [
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_recepcion",
"displayName": "fecha_recepcion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_ejecucion",
"displayName": "fecha_ejecucion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_referencia",
"displayName": "propuesta_referencia",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_match_estado",
"displayName": "propuesta_match_estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_match_confianza",
"displayName": "propuesta_match_confianza",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_nombre_banco",
"displayName": "propuesta_nombre_banco",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_link_banco",
"displayName": "propuesta_link_banco",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "marca",
"displayName": "marca",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "cliente",
"displayName": "cliente",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "pais",
"displayName": "pais",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ubicacion",
"displayName": "ubicacion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "comentario_original",
"displayName": "comentario_original",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "resumen_ia",
"displayName": "resumen_ia",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "descripcion_ejecucion",
"displayName": "descripcion_ejecucion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "elementos_detectados",
"displayName": "elementos_detectados",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "tags",
"displayName": "tags",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_folder_url",
"displayName": "media_folder_url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "presentacion_ejecucion_url",
"displayName": "presentacion_ejecucion_url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fotos_count",
"displayName": "fotos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audios_count",
"displayName": "audios_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado_revision",
"displayName": "estado_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ultima_actualizacion",
"displayName": "ultima_actualizacion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "transcripcion_audio",
"displayName": "transcripcion_audio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_match_revision",
"displayName": "propuesta_match_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte_confianza",
"displayName": "tipo_reporte_confianza",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte_motivo",
"displayName": "tipo_reporte_motivo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "decision_automatica_banco",
"displayName": "decision_automatica_banco",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "motivo_decision_automatica",
"displayName": "motivo_decision_automatica",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "banco_actualizado_auto",
"displayName": "banco_actualizado_auto",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
7120,
2224
],
"id": "d69911ec-0d8c-478d-b88b-740b7d56c8d1",
"name": "Sheets - Guardar propuesta ejecutada final TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"ultima_actividad": "={{ new Date().toISOString() }}",
"etapa": "={{ 'COMPLETADO' }}",
"estado": "={{ 'ANALISIS_COMPLETADO' }}",
"motivo_revision": "={{ $json.motivo_revision || 'ANALISIS_COMPLETADO' }}",
"tipo_reporte": "={{ $json.tipo_reporte }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
14448,
2112
],
"id": "f420de04-b294-4232-aebd-fd1ac4346058",
"name": "Sheets - Actualizar sesión análisis completado WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst normalizado = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst presentacion = getNodeJson('Code - Normalizar link presentación ejecución TEST');\nconst basePaso = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst actual = $json || {};\n\nlet data = {\n ...basePaso,\n ...normalizado,\n ...actual,\n ...presentacion,\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst destinoBase = limpiar(\n data.whatsapp_to ||\n data.group_jid ||\n data.whatsapp_remote_jid ||\n data.sesion_activa?.group_jid ||\n data.sesion_activa?.whatsapp_to ||\n data.sesion_activa?.whatsapp_remote_jid ||\n data.manager_telefono ||\n data.sender_phone ||\n ''\n);\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar mensaje final.');\n}\n\nlet whatsappTo = destinoBase;\n\n// IMPORTANTE:\n// Si es grupo, conservar @g.us.\n// No convertirlo a @s.whatsapp.net.\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nconst presentacionUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n ''\n);\n\nconst bloquePresentacion = presentacionUrl\n ? `\\n\\nPresentación:\\n${presentacionUrl}`\n : '';\n\nconst idioma = limpiar(\n data.idioma_flujo ||\n data.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet whatsappText = '';\n\nif (idioma === 'EN') {\n whatsappText = `✅ Report received and processed successfully.\n\nYour evidence was registered in the proposal evidence bank.${bloquePresentacion}\n\nThank you for completing the report.`;\n} else {\n whatsappText = `✅ Reporte recibido y procesado correctamente.\n\nTu evidencia fue registrada en el banco de propuestas.${bloquePresentacion}\n\nGracias por completar el reporte.`;\n}\n\nreturn [\n {\n json: {\n ...data,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: whatsappText,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
14736,
2080
],
"id": "bcb3c93a-9807-450d-999d-a6cbccf497c7",
"name": "Code - Preparar mensaje final WhatsApp TEST"
},
{
"parameters": {},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
3744,
2112
],
"id": "fa6815ea-190b-441f-99a3-ee23c379e9b9",
"name": "Wait",
"webhookId": "3d858751-a035-4915-8ee0-caf8f19c78a5"
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 737153956,
"mode": "list",
"cachedResultName": "propuestas_ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
6336,
2176
],
"id": "89509a04-841b-4300-a100-1fa9464f6017",
"name": "Sheets - Leer propuestas ejecutadas final TEST",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const base = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\nconst rows = $input.all().map(item => item.json || {});\n\nconst sessionId = String(base.session_id || '').trim();\nconst ejecucionId = String(base.ejecucion_id || '').trim();\n\nconst existente = rows.find(row => {\n const rowSessionId = String(row.session_id || '').trim();\n const rowEjecucionId = String(row.ejecucion_id || '').trim();\n\n return (\n (sessionId && rowSessionId === sessionId) ||\n (ejecucionId && rowEjecucionId === ejecucionId)\n );\n});\n\nreturn [\n {\n json: {\n ...base,\n\n propuesta_final_ya_existe: Boolean(existente),\n propuesta_final_row_number: existente?.row_number || '',\n propuesta_final_existente: existente || null,\n\n estado_anti_duplicado: existente\n ? 'PROPUESTA_FINAL_YA_EXISTE'\n : 'PROPUESTA_FINAL_NUEVA'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6544,
2176
],
"id": "52a97f82-0554-4fae-b744-4588a07f7d60",
"name": "Code - Verificar duplicado propuesta ejecutada TEST"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "80785332-9e50-44d5-9fd5-ecf9610876fd",
"leftValue": "={{ $json.propuesta_final_ya_existe === true }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
6752,
2176
],
"id": "6afd6903-aac3-42bd-b0e4-583a43f92a68",
"name": "IF - Propuesta final ya existe TEST"
},
{
"parameters": {
"jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '1gHgQBDL2uFaa7yNwcyWrtE24B_UrCU1Dy-n63TPurPE';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7328,
2192
],
"id": "1edd49c5-4078-4e39-b68e-56798507020d",
"name": "Code - Preparar copia presentación ejecución TEST"
},
{
"parameters": {
"operation": "copy",
"fileId": {
"__rl": true,
"value": "={{ $json.slides_template_id }}",
"mode": "id"
},
"name": "={{ $json.presentation_name }}",
"sameFolder": false,
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"folderId": {
"__rl": true,
"value": "={{ $json.execution_folder_id }}",
"mode": "id"
},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
8880,
2192
],
"id": "d9274651-56f7-49d6-a4fe-ae4c05386bb9",
"name": "Drive - Copiar plantilla presentación ejecución TEST",
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
}
},
{
"parameters": {
"jsCode": "const base = $('Code - Preparar copia presentación ejecución TEST').first().json || {};\nconst drive = $json || {};\n\nconst presentationId =\n drive.id ||\n drive.fileId ||\n drive.presentationId ||\n drive.data?.id ||\n '';\n\nif (!presentationId) {\n throw new Error('No se encontró el ID de la presentación copiada.');\n}\n\nconst presentationUrl =\n drive.webViewLink ||\n drive.webUrl ||\n `https://docs.google.com/presentation/d/${presentationId}/edit`;\n\nreturn [\n {\n json: {\n ...base,\n\n presentation_id: presentationId,\n presentacion_ejecucion_url: presentationUrl,\n\n estado_presentacion: 'PRESENTACION_CREADA'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
9152,
2192
],
"id": "8195f1d0-b75f-4335-a6a4-84c66875c9d6",
"name": "Code - Normalizar link presentación ejecución TEST"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 737153956,
"mode": "list",
"cachedResultName": "propuestas_ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}",
"ultima_actualizacion": "={{ new Date().toISOString() }}",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"session_id": "={{ $json.session_id }}",
"media_folder_url": "={{ $json.media_folder_url }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_recepcion",
"displayName": "fecha_recepcion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_ejecucion",
"displayName": "fecha_ejecucion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_referencia",
"displayName": "propuesta_referencia",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_match_estado",
"displayName": "propuesta_match_estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_match_confianza",
"displayName": "propuesta_match_confianza",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_nombre_banco",
"displayName": "propuesta_nombre_banco",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_link_banco",
"displayName": "propuesta_link_banco",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "marca",
"displayName": "marca",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "cliente",
"displayName": "cliente",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "pais",
"displayName": "pais",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ubicacion",
"displayName": "ubicacion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "comentario_original",
"displayName": "comentario_original",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "resumen_ia",
"displayName": "resumen_ia",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "descripcion_ejecucion",
"displayName": "descripcion_ejecucion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "elementos_detectados",
"displayName": "elementos_detectados",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "tags",
"displayName": "tags",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_folder_url",
"displayName": "media_folder_url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "presentacion_ejecucion_url",
"displayName": "presentacion_ejecucion_url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fotos_count",
"displayName": "fotos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "audios_count",
"displayName": "audios_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado_revision",
"displayName": "estado_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ultima_actualizacion",
"displayName": "ultima_actualizacion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "transcripcion_audio",
"displayName": "transcripcion_audio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "propuesta_match_revision",
"displayName": "propuesta_match_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte_confianza",
"displayName": "tipo_reporte_confianza",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tipo_reporte_motivo",
"displayName": "tipo_reporte_motivo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "decision_automatica_banco",
"displayName": "decision_automatica_banco",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "motivo_decision_automatica",
"displayName": "motivo_decision_automatica",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "banco_actualizado_auto",
"displayName": "banco_actualizado_auto",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
12368,
2048
],
"id": "2bbb6c3f-2aa6-4093-9312-ac01e02dd67c",
"name": "Sheets - Actualizar link presentación ejecución TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst carpetaPreparada = getNodeJson('Code - Preparar carpeta ejecución TEST');\nconst carpetaDrive = getNodeJson('Drive - Crear carpeta ejecución TEST');\nconst carpetaNormalizada = getNodeJson('Code - Normalizar carpeta ejecución TEST');\n\nconst copiaPresentacion = getNodeJson('Drive - Copiar plantilla presentación ejecución TEST');\nconst linkPresentacionNormalizado = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...carpetaPreparada,\n ...carpetaDrive,\n ...carpetaNormalizada,\n ...copiaPresentacion,\n ...linkPresentacionNormalizado,\n ...actual,\n ...match,\n ...enriquecido\n};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = limpiar(valor);\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n if (texto.toLowerCase() === 'no disponible') return fallback;\n\n return texto;\n};\n\nconst cortar = (valor, max = 900) => {\n const texto = limpiarTexto(valor, 'No disponible');\n return texto.length > max ? texto.slice(0, max - 3) + '...' : texto;\n};\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n ''\n);\n\nconst matchConfianza = Number(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n 0\n);\n\nconst matchAlta =\n matchEstado === 'MATCH_ALTA_CONFIANZA' ||\n matchConfianza >= 85;\n\nconst propuestaNombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n ''\n);\n\nconst propuestaReferenciaEnriquecida = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n jsonFinal.propuesta_referencia ||\n ''\n);\n\nconst propuestaReferenciaSlides = limpiarReferenciaVisual(\n matchAlta && propuestaNombreBanco\n ? propuestaNombreBanco\n : propuestaReferenciaEnriquecida\n);\n\nconst ubicacionSlides = limpiarTexto(\n enriquecido.ubicacion ||\n data.ubicacion ||\n jsonFinal.ubicacion,\n 'No identificado'\n);\n\nconst fechaEjecucionSlides = limpiarTexto(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n jsonFinal.fecha_ejecucion,\n 'No identificado'\n);\n\n// --------------------------------------------------\n// Resolver carpeta de evidencias\n// --------------------------------------------------\n\nconst mediaFolderId = limpiar(\n data.media_folder_id ||\n data.execution_folder_id ||\n data.folder_id ||\n data.id ||\n carpetaNormalizada.media_folder_id ||\n carpetaNormalizada.execution_folder_id ||\n carpetaNormalizada.folder_id ||\n carpetaDrive.id ||\n ''\n);\n\nlet mediaFolderUrl = limpiar(\n data.media_folder_url ||\n data.execution_folder_url ||\n data.folder_url ||\n carpetaNormalizada.media_folder_url ||\n carpetaNormalizada.execution_folder_url ||\n carpetaNormalizada.folder_url ||\n carpetaDrive.webViewLink ||\n ''\n);\n\nif (!mediaFolderUrl && mediaFolderId) {\n mediaFolderUrl = `https://drive.google.com/drive/folders/${mediaFolderId}`;\n}\n\n// --------------------------------------------------\n// Resolver link de presentación\n// --------------------------------------------------\n\nconst presentationId = limpiar(\n data.presentation_id ||\n data.presentacion_id ||\n linkPresentacionNormalizado.presentation_id ||\n copiaPresentacion.id ||\n actual.presentation_id ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n data.presentationUrl ||\n data.link_presentacion ||\n linkPresentacionNormalizado.presentacion_ejecucion_url ||\n linkPresentacionNormalizado.presentation_url ||\n linkPresentacionNormalizado.webViewLink ||\n copiaPresentacion.webViewLink ||\n actual.presentacion_ejecucion_url ||\n actual.presentation_url ||\n actual.webViewLink ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\nif (!presentationId) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nconst objetivoSlides = cortar(\n data.objetivo ||\n jsonFinal.objetivo ||\n data.descripcion_ejecucion ||\n jsonFinal.descripcion_ejecucion ||\n data.resumen_ia ||\n jsonFinal.resumen_ia ||\n 'No disponible',\n 900\n);\n\nconst replacements = {\n '{{MARCA}}': limpiarTexto(data.marca || jsonFinal.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente || jsonFinal.cliente),\n '{{PAIS}}': limpiarTexto(data.pais || jsonFinal.pais),\n '{{UBICACION}}': ubicacionSlides,\n\n '{{FECHA_EJECUCION}}': fechaEjecucionSlides,\n '{{PROPUESTA_REFERENCIA}}': propuestaReferenciaSlides,\n '{{OBJETIVO}}': objetivoSlides,\n\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
9360,
2192
],
"id": "192b9018-dba7-4b23-9c7c-6503704186a6",
"name": "Code - Preparar reemplazos Slides ejecución TEST"
},
{
"parameters": {
"method": "POST",
"url": "={{ 'https://slides.googleapis.com/v1/presentations/' + $json.presentation_id + ':batchUpdate' }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { requests: $json.slides_replace_requests } }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
9632,
2128
],
"id": "3fd9c926-7369-4d05-a470-827e813a4d76",
"name": "HTTP Request - Reemplazar textos Slides ejecución TEST",
"credentials": {
"googleOAuth2Api": {
"id": "eHseMeH39kRcXgOF",
"name": "Google account 2"
}
}
},
{
"parameters": {
"jsCode": "const base = $('Code - Preparar reemplazos Slides ejecución TEST').first().json || {};\nconst respuestaSlides = $json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n slides_batch_update_response: respuestaSlides,\n estado_presentacion: 'TEXTOS_SLIDES_REEMPLAZADOS'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
10240,
2128
],
"id": "47ecd981-3ce3-4462-87f5-fc4e3d5d628c",
"name": "Code - Normalizar respuesta Slides ejecución TEST"
},
{
"parameters": {
"jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver sesión WhatsApp TEST');\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = limpiar(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n ''\n);\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar mensaje de cancelación sin sesión.');\n}\n\nlet whatsappTo = destinoBase;\n\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '️ *There is no active report to cancel.*',\n '',\n 'You can start a new evidence registration whenever you are ready.',\n '',\n 'Write:',\n '',\n '*Hey*'\n ].join('\\n');\n} else {\n mensaje = [\n '️ *No hay un reporte activo para cancelar.*',\n '',\n 'Puedes iniciar un nuevo registro de evidencias cuando estés listo.',\n '',\n 'Escribe:',\n '',\n '*Hey*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: mensaje\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4832,
-3792
],
"id": "3247cab6-8e96-4c06-a7f2-9f504d6e5005",
"name": "Code - Preparar sin sesión para cancelar WhatsApp TEST"
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 306129743,
"mode": "list",
"cachedResultName": "wa_ejecuciones_eventos",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-3712,
-1168
],
"id": "41f82dcf-7d4b-4c40-b3a9-98a03a322dcf",
"name": "Sheets - Leer eventos sesión fotos WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const data = $json || {};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = String(valor ?? '').trim();\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n\n return texto;\n};\n\nconst limpiarNombreArchivo = (valor) => {\n return limpiarTexto(valor)\n .replace(/[\\\\/:*?\"<>|#%{}~&]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 120);\n};\n\nconst fechaBase = limpiarTexto(\n data.fecha_ejecucion ||\n data.fecha_recepcion ||\n data.ultima_actualizacion ||\n new Date().toISOString()\n);\n\nconst fecha = fechaBase.slice(0, 10);\n\nconst marca = limpiarNombreArchivo(data.marca);\nconst cliente = limpiarNombreArchivo(data.cliente);\nconst sessionId = limpiarNombreArchivo(data.session_id || data.ejecucion_id);\n\nconst folderName = `${fecha} - ${marca} - ${cliente} - ${sessionId}`;\n\n// Carpeta base PROPUESTAS EJECUTADAS\nconst parentFolderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nreturn [\n {\n json: {\n ...data,\n\n execution_folder_name: folderName,\n execution_parent_folder_id: parentFolderId\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7536,
2192
],
"id": "07051572-ab5b-4482-957d-da55178ec248",
"name": "Code - Preparar carpeta ejecución TEST"
},
{
"parameters": {
"resource": "folder",
"name": "={{ $json.execution_folder_name }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"folderId": {
"__rl": true,
"value": "={{ $json.execution_parent_folder_id }}",
"mode": "id"
},
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
7744,
2192
],
"id": "c46d6636-05b2-4cd2-9db0-4cc8270786ac",
"name": "Drive - Crear carpeta ejecución TEST",
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
}
},
{
"parameters": {
"jsCode": "const carpeta = $json || {};\nconst base = $('Code - Preparar carpeta ejecución TEST').first().json || {};\n\nconst folderId =\n carpeta.id ||\n carpeta.fileId ||\n carpeta.folderId ||\n '';\n\nconst folderUrl = folderId\n ? `https://drive.google.com/drive/folders/${folderId}`\n : '';\n\nif (!folderId) {\n throw new Error('No se pudo obtener el ID de la carpeta de ejecución.');\n}\n\nreturn [\n {\n json: {\n ...base,\n\n execution_folder_id: folderId,\n execution_folder_url: folderUrl,\n\n media_folder_id: folderId,\n media_folder_url: folderUrl\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7952,
2192
],
"id": "78fded8e-3963-458f-bafe-001eaf553022",
"name": "Code - Normalizar carpeta ejecución TEST"
},
{
"parameters": {
"jsCode": "const data = $json || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst folderId = texto(data.execution_folder_id || data.media_folder_id);\n\nif (!folderId) {\n throw new Error('No existe execution_folder_id/media_folder_id para mover la multimedia.');\n}\n\nconst archivos = [];\nconst vistos = new Set();\n\nconst extraerFileIdDesdeUrl = (valor) => {\n const url = texto(valor);\n if (!url) return '';\n\n // Formato: https://drive.google.com/file/d/FILE_ID/view\n const matchFile = url.match(/\\/file\\/d\\/([^/]+)/);\n if (matchFile?.[1]) return matchFile[1];\n\n // Formato: https://drive.google.com/open?id=FILE_ID\n const matchOpen = url.match(/[?&]id=([^&]+)/);\n if (matchOpen?.[1]) return matchOpen[1];\n\n // Si ya viene como ID limpio\n if (!url.includes('http') && url.length > 15) return url;\n\n return '';\n};\n\nconst separarLista = (valor) => {\n return texto(valor)\n .split(/[\\n,]+/)\n .map(v => texto(v))\n .filter(Boolean);\n};\n\nconst agregarArchivo = (fileIdOrUrl, tipo) => {\n const fileId = extraerFileIdDesdeUrl(fileIdOrUrl);\n\n if (!fileId) return;\n if (vistos.has(fileId)) return;\n\n vistos.add(fileId);\n\n archivos.push({\n json: {\n ...data,\n\n move_file_id: fileId,\n move_file_type: tipo,\n\n execution_folder_id: folderId,\n media_folder_id: folderId,\n media_folder_url: data.media_folder_url,\n execution_folder_url: data.execution_folder_url\n }\n });\n};\n\n// 1. Archivo actual del item, si existe\nagregarArchivo(data.drive_file_id, data.media_type || 'media');\n\n// 2. URLs acumuladas por tipo\nfor (const url of separarLista(data.audio_drive_urls)) {\n agregarArchivo(url, 'audio');\n}\n\nfor (const url of separarLista(data.imagenes_drive_urls)) {\n agregarArchivo(url, 'image');\n}\n\nfor (const url of separarLista(data.videos_drive_urls)) {\n agregarArchivo(url, 'video');\n}\n\n// 3. Si tienes otros campos futuros\nfor (const url of separarLista(data.media_drive_urls)) {\n agregarArchivo(url, 'media');\n}\n\nif (archivos.length === 0) {\n return [\n {\n json: {\n ...data,\n media_move_status: 'SIN_ARCHIVOS_PARA_MOVER',\n media_move_debug: {\n drive_file_id: data.drive_file_id || '',\n audio_drive_urls: data.audio_drive_urls || '',\n imagenes_drive_urls: data.imagenes_drive_urls || '',\n videos_drive_urls: data.videos_drive_urls || ''\n }\n }\n }\n ];\n}\n\nreturn archivos;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
8160,
2192
],
"id": "a8b7b615-fb63-4e02-9b80-31efb5109ad8",
"name": "Code - Preparar media para mover carpeta TEST"
},
{
"parameters": {
"operation": "move",
"fileId": {
"__rl": true,
"value": "={{ $json.move_file_id }}",
"mode": "id"
},
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"folderId": {
"__rl": true,
"value": "={{ $json.execution_folder_id }}",
"mode": "id"
}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
8368,
2192
],
"id": "7797be31-dc35-4c58-b38e-224c0449c435",
"name": "Drive - Mover media a carpeta ejecución TEST",
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
}
},
{
"parameters": {
"jsCode": "const movido = $json || {};\nconst base = $('Code - Preparar media para mover carpeta TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n media_move_status: 'MEDIA_MOVIDA_A_CARPETA',\n moved_file_id: base.move_file_id,\n moved_file_type: base.move_file_type,\n moved_file_url: base.move_file_url,\n\n drive_move_response_id: movido.id || movido.fileId || base.move_file_id\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
8576,
2192
],
"id": "ff7562ea-6586-4dbf-955e-8b4ef4061a14",
"name": "Code - Confirmar media movida carpeta TEST"
},
{
"parameters": {
"jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Preparar reemplazos Slides ejecución TEST');\n\nconst data = {\n ...base,\n ...actual,\n\n // Aseguramos estos campos desde el nodo base si el nodo Slides no los devuelve\n presentation_id: actual.presentation_id || base.presentation_id,\n imagenes_drive_urls: actual.imagenes_drive_urls || base.imagenes_drive_urls,\n imagen_drive_urls: actual.imagen_drive_urls || base.imagen_drive_urls,\n media_drive_urls: actual.media_drive_urls || base.media_drive_urls,\n};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst extraerFileIdDesdeUrl = (valor) => {\n const url = texto(valor);\n if (!url) return '';\n\n const matchFile = url.match(/\\/file\\/d\\/([^/]+)/);\n if (matchFile?.[1]) return matchFile[1];\n\n const matchOpen = url.match(/[?&]id=([^&]+)/);\n if (matchOpen?.[1]) return matchOpen[1];\n\n if (!url.includes('http') && url.length > 15) return url;\n\n return '';\n};\n\nconst separarLista = (valor) => {\n return texto(valor)\n .split(/[\\n,]+/)\n .map(v => texto(v))\n .filter(Boolean);\n};\n\nconst imagenesUrls = separarLista(\n data.imagenes_drive_urls ||\n data.imagen_drive_urls ||\n data.media_drive_urls ||\n ''\n);\n\nconst imageFileIds = imagenesUrls\n .map(extraerFileIdDesdeUrl)\n .filter(Boolean)\n .slice(0, 25)\n\nif (!data.presentation_id) {\n throw new Error('No llegó presentation_id para insertar imágenes en Slides.');\n}\n\nreturn [\n {\n json: {\n ...data,\n\n apps_script_insert_images_payload: {\n secret: 'glm_fulgencio_slides_2026_seguro',\n presentation_id: data.presentation_id,\n image_file_ids: imageFileIds\n },\n\n imagenes_para_slides_count: imageFileIds.length,\n imagenes_para_slides_ids: imageFileIds,\n\n estado_insertar_imagenes: 'PAYLOAD_INSERTAR_IMAGENES_PREPARADO'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
9840,
2128
],
"id": "93846b66-2092-43ec-b57d-1347e3947c42",
"name": "Code - Preparar payload insertar imágenes Slides TEST"
},
{
"parameters": {
"method": "POST",
"url": "https://script.google.com/macros/s/AKfycbyw76HYjYMvq0KXA9IN5S8nRWk0drPkjKSK2SAbCL9Ha8is3vFZMyR6Ldby4c3YVs7J/exec",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.apps_script_insert_images_payload }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
10048,
2128
],
"id": "573fc25d-7dae-42e1-8025-1e38d3394c17",
"name": "HTTP - Insertar imágenes en Slides TEST"
},
{
"parameters": {
"jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n actual.whatsapp_to ||\n actual.group_jid ||\n actual.whatsapp_remote_jid ||\n base.whatsapp_to ||\n base.group_jid ||\n base.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar aviso de procesamiento.');\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '⏳ *Report received.*',\n '',\n 'We are now processing your evidence.',\n '',\n 'This may take a moment while we analyze the media, organize the files and generate the presentation.',\n '',\n 'I will send you the final link when everything is ready.'\n ].join('\\n');\n} else {\n mensaje = [\n '⏳ *Reporte recibido.*',\n '',\n 'Estamos procesando tus evidencias.',\n '',\n 'Esto puede tomar un momento mientras analizamos los archivos, organizamos la carpeta y generamos la presentación.',\n '',\n 'Te enviaré el link final cuando todo esté listo.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje,\n\n aviso_procesando_preparado: true\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5824,
1072
],
"id": "65715659-8334-4447-9e92-8aa372ed345b",
"name": "Code - Preparar aviso procesando WhatsApp TEST1"
},
{
"parameters": {
"jsCode": "const respuesta = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nlet base =\n getNodeJson('Code - Preparar aviso procesando WhatsApp TEST') ||\n getNodeJson('Code - Preparar aviso procesando WhatsApp TEST1') ||\n {};\n\nif (!base || Object.keys(base).length === 0) {\n base = getNodeJson('Code - Resolver paso activo WhatsApp TEST') || {};\n}\n\nreturn [\n {\n json: {\n ...base,\n\n aviso_procesando_enviado: true,\n aviso_procesando_response: respuesta\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5376,
1072
],
"id": "e9bac53c-7375-4375-830c-dee36d28b1e4",
"name": "Code - Restaurar contexto aviso procesando TEST"
},
{
"parameters": {
"method": "POST",
"url": "={{'https://wsp.gomezleemarketing.com'}}/message/sendText/{{'botsoporte'}}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{'C267126ABB45-4C12-B626-6BAB1833F5D7'}}"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "number",
"value": "={{ (() => {\n const limpiar = (valor) => String(valor ?? '').trim();\n\n const candidatos = [\n $json.whatsapp_to,\n $json.group_jid,\n $json.whatsapp_remote_jid,\n $json.sesion_activa?.group_jid,\n $json.sesion_activa?.whatsapp_to,\n $json.sesion_activa?.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ];\n\n let raw = limpiar(candidatos.find(v => limpiar(v)));\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n // Si ya viene como grupo, conservarlo.\n if (raw.includes('@g.us')) {\n return raw;\n }\n\n // Si viene como usuario WhatsApp, validar si realmente era un grupo.\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n // Los grupos de WhatsApp suelen venir como 120363...\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n})() }}"
},
{
"name": "delay",
"value": "={{ 1000 }}"
},
{
"name": "text",
"value": "={{ $json.whatsapp_text }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
-5600,
1072
],
"id": "bafc62ac-98e2-476f-ac16-0aa906c03f33",
"name": "HTTP - Enviar aviso procesando WhatsApp TEST",
"disabled": true
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng",
"mode": "list",
"cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "propuestas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0"
},
"options": {
"dataLocationOnSheet": {
"values": {
"rangeDefinition": "detectAutomatically"
}
},
"outputFormatting": {
"values": {
"general": "UNFORMATTED_VALUE",
"date": "FORMATTED_STRING"
}
},
"returnFirstMatch": false
}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
5584,
2144
],
"id": "9bbb9e2c-1e27-4911-a836-e392c7e523b3",
"name": "Sheets - Leer banco propuestas Fulgencio TEST",
"alwaysOutputData": true,
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst propuesta =\n getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST') ||\n $json ||\n {};\n\nconst bancoRows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) => {\n return limpiar(valor)\n .replace(/\\+/g, ' mas ')\n .replace(/_/g, ' ')\n .replace(/-/g, ' ')\n .replace(/\\//g, ' ')\n .replace(/\\./g, ' ')\n .replace(/\\bGUATE\\b/gi, ' Guatemala ')\n .replace(/\\bGUA\\b/gi, ' Guatemala ')\n .replace(/\\bGT\\b/gi, ' Guatemala ')\n .replace(/\\bRD\\b/gi, ' Republica Dominicana ')\n .replace(/\\bREP DOM\\b/gi, ' Republica Dominicana ')\n .replace(/\\bREP DOMINICANA\\b/gi, ' Republica Dominicana ')\n .replace(/\\bDOMINICANA\\b/gi, ' Republica Dominicana ')\n .replace(/\\bCR\\b/gi, ' Costa Rica ')\n .replace(/\\bPAN\\b/gi, ' Panama ')\n .replace(/\\bPTY\\b/gi, ' Panama ')\n .replace(/\\bSV\\b/gi, ' El Salvador ')\n .replace(/\\bSALVADOR\\b/gi, ' El Salvador ')\n .replace(/\\bHND\\b/gi, ' Honduras ')\n .replace(/\\bNIC\\b/gi, ' Nicaragua ')\n .replace(/\\bWMC\\b/gi, ' WMC ')\n .replace(/\\bRTM\\b/gi, ' RTM ')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst tokens = (valor) => {\n const stopwords = new Set([\n 'de', 'del', 'la', 'el', 'los', 'las', 'y', 'en', 'para', 'por', 'con',\n 'una', 'un', 'uno', 'dos', 'tres', 'the', 'of', 'and', 'for', 'to', 'a',\n 'propuesta', 'presentacion', 'presentación', 'ejecucion', 'ejecución',\n 'ejecutada', 'ejecutado', 'reporte', 'evidencia', 'version', 'versión',\n 'archivo', 'link', 'slide', 'slides', 'prueba'\n ]);\n\n return normalizar(valor)\n .split(' ')\n .map(t => t.trim())\n .filter(Boolean)\n .filter(t => t.length >= 3 || /^v\\d+$/i.test(t))\n .filter(t => !stopwords.has(t));\n};\n\nconst tokenSet = (valor) => new Set(tokens(valor));\n\nconst similitudTokens = (a, b) => {\n const ta = tokenSet(a);\n const tb = tokenSet(b);\n\n if (ta.size === 0 || tb.size === 0) return 0;\n\n let interseccion = 0;\n\n for (const token of ta) {\n if (tb.has(token)) interseccion++;\n }\n\n const union = new Set([...ta, ...tb]).size;\n\n return union === 0 ? 0 : interseccion / union;\n};\n\nconst coberturaTokens = (textoBase, textoContra) => {\n const base = [...tokenSet(textoBase)];\n const contra = tokenSet(textoContra);\n\n if (base.length === 0) return 0;\n\n const encontrados = base.filter(t => contra.has(t));\n\n return encontrados.length / base.length;\n};\n\nconst contarTokensComunes = (a, b) => {\n const ta = tokenSet(a);\n const tb = tokenSet(b);\n\n let comunes = 0;\n\n for (const token of ta) {\n if (tb.has(token)) comunes++;\n }\n\n return comunes;\n};\n\nconst obtenerCampo = (row, posiblesNombres) => {\n const keys = Object.keys(row || {});\n\n for (const nombre of posiblesNombres) {\n const exacto = keys.find(k => normalizar(k) === normalizar(nombre));\n if (exacto && limpiar(row[exacto])) return limpiar(row[exacto]);\n }\n\n for (const nombre of posiblesNombres) {\n const parcial = keys.find(k => normalizar(k).includes(normalizar(nombre)));\n if (parcial && limpiar(row[parcial])) return limpiar(row[parcial]);\n }\n\n return '';\n};\n\nconst extraerVersion = (valor) => {\n const texto = normalizar(valor);\n const match = texto.match(/\\bv\\d+\\b/);\n return match ? match[0].toUpperCase() : '';\n};\n\nconst extraerAnio = (valor) => {\n const texto = limpiar(valor);\n const match = texto.match(/\\b20\\d{2}\\b/);\n return match ? match[0] : '';\n};\n\nconst contienePais = (texto, pais) => {\n const t = normalizar(texto);\n const p = normalizar(pais);\n\n if (!p) return false;\n\n if (t.includes(p)) return true;\n\n if (p.includes('guatemala') && t.includes('guatemala')) return true;\n if (p.includes('republica dominicana') && t.includes('republica dominicana')) return true;\n if (p.includes('costa rica') && t.includes('costa rica')) return true;\n if (p.includes('panama') && t.includes('panama')) return true;\n if (p.includes('salvador') && t.includes('salvador')) return true;\n if (p.includes('honduras') && t.includes('honduras')) return true;\n if (p.includes('nicaragua') && t.includes('nicaragua')) return true;\n\n return false;\n};\n\nconst tipoReporte = limpiar(propuesta.tipo_reporte).toUpperCase();\n\nif (tipoReporte !== 'PROPUESTA_EJECUTADA') {\n return [\n {\n json: {\n ...propuesta,\n\n propuesta_match_estado:\n tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'NO_APLICA_PROPUESTA_EXTERNA'\n : 'NO_APLICA_NO_DETERMINADO',\n\n propuesta_match_confianza: '0',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_banco_row_number: '',\n propuesta_banco_enlaces_existentes: '',\n propuesta_banco_file_id: '',\n\n propuesta_match_revision:\n tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'NO_REQUIERE_MATCH_BANCO'\n : 'REQUIERE_REVISION_TIPO_NO_DETERMINADO',\n\n match_banco_debug: {\n motivo: 'No se ejecutó match porque el tipo_reporte no es PROPUESTA_EJECUTADA.',\n tipo_reporte: tipoReporte || 'NO_DETERMINADO'\n }\n }\n }\n ];\n}\n\nconst referencia = limpiar(propuesta.propuesta_referencia);\nconst marca = limpiar(propuesta.marca);\nconst cliente = limpiar(propuesta.cliente);\nconst pais = limpiar(propuesta.pais);\nconst ubicacion = limpiar(propuesta.ubicacion);\nconst descripcion = limpiar(propuesta.descripcion_ejecucion || propuesta.resumen_ia);\nconst elementosDetectados = limpiar(propuesta.elementos_detectados);\nconst comentarioOriginal = limpiar(propuesta.comentario_original);\nconst transcripcionAudio = limpiar(propuesta.transcripcion_audio);\nconst textoAudio = limpiar(propuesta.texto_audio);\nconst textoImagenes = limpiar(propuesta.texto_imagenes);\nconst tags = limpiar(propuesta.tags);\n\nconst textoPropuestaCompleto = [\n referencia,\n marca,\n cliente,\n pais,\n ubicacion,\n descripcion,\n elementosDetectados,\n comentarioOriginal,\n transcripcionAudio,\n textoAudio,\n textoImagenes,\n tags\n].filter(Boolean).join(' ');\n\nconst textoClavePropuesta = [\n referencia,\n marca,\n cliente,\n pais,\n elementosDetectados,\n textoImagenes,\n tags\n].filter(Boolean).join(' ');\n\nconst versionPropuesta = extraerVersion(textoPropuestaCompleto);\nconst anioPropuesta = extraerAnio(textoPropuestaCompleto);\n\nconst candidatos = [];\n\nfor (const [index, row] of bancoRows.entries()) {\n const bancoRowNumber = String(index + 2);\n\n const bancoNombre = obtenerCampo(row, [\n 'NOMBRE',\n 'propuesta',\n 'nombre propuesta',\n 'nombre de propuesta',\n 'titulo',\n 'título',\n 'nombre',\n 'referencia',\n 'brief',\n 'idea'\n ]);\n\n const bancoTipoAccion = obtenerCampo(row, [\n 'TIPO DE ACCION',\n 'TIPO DE ACCIÓN',\n 'tipo accion',\n 'tipo acción'\n ]);\n\n const bancoCliente = obtenerCampo(row, [\n 'CLIENTE',\n 'cliente',\n 'client'\n ]);\n\n const bancoMarca = obtenerCampo(row, [\n 'MARCA',\n 'marca',\n 'brand'\n ]);\n\n const bancoPais = obtenerCampo(row, [\n 'PAIS',\n 'PAÍS',\n 'pais',\n 'país',\n 'country'\n ]);\n\n const bancoCanal = obtenerCampo(row, [\n 'CANAL',\n 'canal'\n ]);\n\n const bancoAmbiente = obtenerCampo(row, [\n 'AMBIENTE DE COMPRA',\n 'ambiente de compra',\n 'ambiente'\n ]);\n\n const bancoDescripcion = obtenerCampo(row, [\n 'Descripcion',\n 'Descripción',\n 'descripcion',\n 'descripción',\n 'detalle',\n 'mecanica',\n 'mecánica',\n 'comentario',\n 'observacion',\n 'observación'\n ]);\n\n const bancoLink = obtenerCampo(row, [\n 'Enlace a la propuesta',\n 'link',\n 'enlace',\n 'url',\n 'brief link',\n 'link brief',\n 'presentacion',\n 'presentación',\n 'drive'\n ]);\n\n const bancoFileId = obtenerCampo(row, [\n 'file_id',\n 'File ID',\n 'archivo_id'\n ]);\n\n const bancoEnlacesEjecutadas = obtenerCampo(row, [\n 'Enlaces a propuestas ejecutadas',\n 'Enlace a propuestas ejecutadas',\n 'Links propuestas ejecutadas',\n 'Link propuestas ejecutadas',\n 'Ejecuciones',\n 'Propuestas ejecutadas'\n ]);\n\n if (!bancoNombre && !bancoMarca && !bancoCliente && !bancoDescripcion) {\n continue;\n }\n\n const textoBancoCompleto = [\n bancoNombre,\n bancoTipoAccion,\n bancoCliente,\n bancoMarca,\n bancoPais,\n bancoCanal,\n bancoAmbiente,\n bancoDescripcion\n ].filter(Boolean).join(' ');\n\n const textoClaveBanco = [\n bancoNombre,\n bancoCliente,\n bancoMarca,\n bancoPais\n ].filter(Boolean).join(' ');\n\n const scoreReferenciaNombre = similitudTokens(referencia, bancoNombre);\n const scoreEvidenciaNombre = similitudTokens(textoClavePropuesta, bancoNombre);\n const coberturaNombreEnEvidencia = coberturaTokens(bancoNombre, textoPropuestaCompleto);\n const coberturaReferenciaEnBanco = coberturaTokens(referencia, textoBancoCompleto);\n\n const scoreNombre = Math.max(\n scoreReferenciaNombre,\n scoreEvidenciaNombre,\n coberturaNombreEnEvidencia,\n coberturaReferenciaEnBanco\n );\n\n const scoreMarcaContraBanco = Math.max(\n similitudTokens(marca, bancoMarca),\n similitudTokens(marca, bancoNombre),\n similitudTokens(marca, textoBancoCompleto)\n );\n\n const scoreClienteContraBanco = Math.max(\n similitudTokens(cliente, bancoCliente),\n similitudTokens(cliente, bancoNombre),\n similitudTokens(cliente, textoBancoCompleto)\n );\n\n const scorePais = Math.max(\n similitudTokens(pais, bancoPais),\n contienePais(bancoNombre, pais) ? 1 : 0,\n contienePais(textoBancoCompleto, pais) ? 1 : 0\n );\n\n const scoreGeneral = similitudTokens(textoPropuestaCompleto, textoBancoCompleto);\n const scoreClave = similitudTokens(textoClavePropuesta, textoClaveBanco);\n const coberturaBancoEnPropuesta = coberturaTokens(bancoNombre, textoPropuestaCompleto);\n const comunesNombreEvidencia = contarTokensComunes(bancoNombre, textoPropuestaCompleto);\n\n const versionBanco = extraerVersion(textoBancoCompleto);\n const anioBanco = extraerAnio(textoBancoCompleto);\n\n const versionCoincide =\n versionPropuesta &&\n versionBanco &&\n versionPropuesta === versionBanco;\n\n const versionContradice =\n versionPropuesta &&\n versionBanco &&\n versionPropuesta !== versionBanco;\n\n const anioCoincide =\n anioPropuesta &&\n anioBanco &&\n anioPropuesta === anioBanco;\n\n const anioContradice =\n anioPropuesta &&\n anioBanco &&\n anioPropuesta !== anioBanco;\n\n const paisCoincide =\n scorePais >= 0.80 ||\n contienePais(bancoNombre, pais) ||\n contienePais(textoBancoCompleto, pais);\n\n const marcaCoincide =\n scoreMarcaContraBanco >= 0.45 ||\n normalizar(textoBancoCompleto).includes(normalizar(marca)) ||\n normalizar(textoPropuestaCompleto).includes(normalizar(bancoMarca));\n\n const scorePonderado =\n scoreNombre * 0.46 +\n scoreClave * 0.22 +\n scoreMarcaContraBanco * 0.08 +\n scoreClienteContraBanco * 0.04 +\n scorePais * 0.10 +\n scoreGeneral * 0.10;\n\n let scoreFinal = scorePonderado;\n let reglaMatch = 'PONDERADO';\n\n // Regla 1: el nombre/referencia de la propuesta está claramente contenido en la evidencia.\n if (coberturaBancoEnPropuesta >= 0.85 && (paisCoincide || anioCoincide || versionCoincide)) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.97 : 0.93);\n reglaMatch = versionCoincide\n ? 'NOMBRE_BANCO_EN_EVIDENCIA_CON_VERSION'\n : 'NOMBRE_BANCO_EN_EVIDENCIA';\n }\n\n // Regla 2: combinación fuerte de clave propuesta-banco.\n if (scoreClave >= 0.85 && (paisCoincide || anioCoincide || marcaCoincide)) {\n scoreFinal = Math.max(scoreFinal, 0.95);\n reglaMatch = 'CLAVE_COMPLETA_PROPUESTA_BANCO';\n } else if (scoreClave >= 0.75 && (paisCoincide || versionCoincide)) {\n scoreFinal = Math.max(scoreFinal, 0.90);\n reglaMatch = 'CLAVE_FUERTE_PROPUESTA_BANCO';\n }\n\n // Regla 3: referencia parcial + país + versión.\n if (scoreNombre >= 0.60 && paisCoincide && versionCoincide) {\n scoreFinal = Math.max(scoreFinal, 0.94);\n reglaMatch = 'REFERENCIA_PAIS_VERSION';\n }\n\n // Regla 4: muchos tokens comunes, aunque Gemini haya escrito el nombre distinto.\n if (comunesNombreEvidencia >= 5 && (paisCoincide || anioCoincide)) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.96 : 0.88);\n reglaMatch = versionCoincide\n ? 'TOKENS_COMUNES_PAIS_VERSION'\n : 'TOKENS_COMUNES_PAIS';\n }\n\n // Regla 5: para casos como RTM+ vs RTM más, GUATE vs Guatemala.\n if (\n normalizar(textoPropuestaCompleto).includes('rtm') &&\n normalizar(textoBancoCompleto).includes('rtm') &&\n normalizar(textoPropuestaCompleto).includes('wmc') &&\n normalizar(textoBancoCompleto).includes('wmc') &&\n (paisCoincide || normalizar(textoBancoCompleto).includes('guatemala')) &&\n (marcaCoincide || normalizar(textoBancoCompleto).includes('campero'))\n ) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.96 : 0.89);\n reglaMatch = versionCoincide\n ? 'RTM_WMC_MARCA_PAIS_VERSION'\n : 'RTM_WMC_MARCA_PAIS';\n }\n\n // Penalizaciones de seguridad.\n if (anioContradice) {\n scoreFinal = Math.min(scoreFinal, 0.59);\n reglaMatch = `${reglaMatch}_PENALIZADO_ANIO`;\n }\n\n if (versionContradice && scoreFinal >= 0.85) {\n scoreFinal = Math.min(scoreFinal, 0.79);\n reglaMatch = `${reglaMatch}_PENALIZADO_VERSION`;\n }\n\n // Si no coincide ni país, ni marca, ni año, no lo dejamos como alta confianza.\n if (\n scoreFinal >= 0.85 &&\n !paisCoincide &&\n !marcaCoincide &&\n !anioCoincide\n ) {\n scoreFinal = 0.74;\n reglaMatch = `${reglaMatch}_PENALIZADO_SIN_ANCLA`;\n }\n\n let bonusOrden = 0;\n\n if (versionCoincide) bonusOrden += 0.04;\n if (anioCoincide) bonusOrden += 0.02;\n if (paisCoincide) bonusOrden += 0.02;\n if (marcaCoincide) bonusOrden += 0.01;\n if (bancoFileId) bonusOrden += 0.005;\n\n candidatos.push({\n row,\n score: scoreFinal,\n score_orden: scoreFinal + bonusOrden,\n score_ponderado: scorePonderado,\n score_porcentaje: Math.round(scoreFinal * 100),\n\n score_nombre: Math.round(scoreNombre * 100),\n score_referencia_nombre: Math.round(scoreReferenciaNombre * 100),\n score_evidencia_nombre: Math.round(scoreEvidenciaNombre * 100),\n cobertura_nombre_en_evidencia: Math.round(coberturaNombreEnEvidencia * 100),\n cobertura_referencia_en_banco: Math.round(coberturaReferenciaEnBanco * 100),\n score_marca: Math.round(scoreMarcaContraBanco * 100),\n score_cliente: Math.round(scoreClienteContraBanco * 100),\n score_pais: Math.round(scorePais * 100),\n score_general: Math.round(scoreGeneral * 100),\n score_clave: Math.round(scoreClave * 100),\n cobertura_banco_en_propuesta: Math.round(coberturaBancoEnPropuesta * 100),\n comunes_nombre_evidencia: comunesNombreEvidencia,\n\n version_propuesta: versionPropuesta,\n version_banco: versionBanco,\n version_coincide: Boolean(versionCoincide),\n version_contradice: Boolean(versionContradice),\n\n anio_propuesta: anioPropuesta,\n anio_banco: anioBanco,\n anio_coincide: Boolean(anioCoincide),\n anio_contradice: Boolean(anioContradice),\n\n pais_coincide: Boolean(paisCoincide),\n marca_coincide: Boolean(marcaCoincide),\n\n regla_match: reglaMatch,\n\n banco_row_number: bancoRowNumber,\n banco_nombre: bancoNombre,\n banco_tipo_accion: bancoTipoAccion,\n banco_marca: bancoMarca,\n banco_cliente: bancoCliente,\n banco_pais: bancoPais,\n banco_canal: bancoCanal,\n banco_link: bancoLink,\n banco_file_id: bancoFileId,\n banco_enlaces_ejecutadas: bancoEnlacesEjecutadas\n });\n}\n\nconst candidatosValidos = candidatos\n .filter(c => c.score > 0)\n .sort((a, b) => {\n if (b.score_orden !== a.score_orden) return b.score_orden - a.score_orden;\n return Number(b.banco_row_number || 0) - Number(a.banco_row_number || 0);\n });\n\nconst mejor = candidatosValidos[0] || null;\nconst segundo = candidatosValidos[1] || null;\n\nlet propuestaMatchEstado = 'SIN_MATCH';\nlet propuestaMatchRevision = 'REQUIERE_REVISION_MANUAL';\nlet propuestaMatchConfianza = '0';\nlet propuestaNombreBanco = '';\nlet propuestaLinkBanco = '';\nlet propuestaBancoRowNumber = '';\nlet propuestaBancoEnlacesExistentes = '';\nlet propuestaBancoFileId = '';\n\nif (mejor) {\n let confianzaFinal = mejor.score_porcentaje;\n\n // Si el segundo está demasiado cerca, mantenemos match pero pedimos revisión.\n const segundoMuyCerca =\n segundo &&\n mejor.score_porcentaje >= 85 &&\n segundo.score_porcentaje >= 85 &&\n Math.abs(mejor.score_porcentaje - segundo.score_porcentaje) <= 2 &&\n normalizar(mejor.banco_nombre) !== normalizar(segundo.banco_nombre);\n\n propuestaNombreBanco = mejor.banco_nombre || 'No identificado';\n propuestaLinkBanco = mejor.banco_link || '';\n propuestaBancoRowNumber = mejor.banco_row_number || '';\n propuestaBancoEnlacesExistentes = mejor.banco_enlaces_ejecutadas || '';\n propuestaBancoFileId = mejor.banco_file_id || '';\n\n if (confianzaFinal >= 85 && !segundoMuyCerca) {\n propuestaMatchEstado = 'MATCH_ALTA_CONFIANZA';\n propuestaMatchRevision = 'NO_REQUIERE_REVISION';\n } else if (confianzaFinal >= 85 && segundoMuyCerca) {\n propuestaMatchEstado = 'MATCH_MEDIA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_VALIDACION_MANUAL';\n confianzaFinal = Math.min(confianzaFinal, 84);\n } else if (confianzaFinal >= 60) {\n propuestaMatchEstado = 'MATCH_MEDIA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_VALIDACION_MANUAL';\n } else {\n propuestaMatchEstado = 'MATCH_BAJA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_REVISION_MANUAL';\n }\n\n propuestaMatchConfianza = String(confianzaFinal);\n}\n\nreturn [\n {\n json: {\n ...propuesta,\n\n propuesta_match_estado: propuestaMatchEstado,\n propuesta_match_confianza: propuestaMatchConfianza,\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_link_banco: propuestaLinkBanco,\n propuesta_match_revision: propuestaMatchRevision,\n\n propuesta_banco_row_number: propuestaBancoRowNumber,\n propuesta_banco_enlaces_existentes: propuestaBancoEnlacesExistentes,\n propuesta_banco_file_id: propuestaBancoFileId,\n\n match_banco_debug: {\n total_filas_banco: bancoRows.length,\n total_candidatos: candidatosValidos.length,\n propuesta_normalizada: {\n referencia,\n marca,\n cliente,\n pais,\n version_propuesta: versionPropuesta,\n anio_propuesta: anioPropuesta,\n texto_clave: textoClavePropuesta\n },\n mejor_match: mejor\n ? {\n score: mejor.score_porcentaje,\n score_orden: Math.round(mejor.score_orden * 100),\n regla_match: mejor.regla_match,\n row_number: mejor.banco_row_number,\n nombre: mejor.banco_nombre,\n tipo_accion: mejor.banco_tipo_accion,\n marca: mejor.banco_marca,\n cliente: mejor.banco_cliente,\n pais: mejor.banco_pais,\n canal: mejor.banco_canal,\n link: mejor.banco_link,\n file_id: mejor.banco_file_id,\n enlaces_ejecutadas_existentes: mejor.banco_enlaces_ejecutadas,\n score_nombre: mejor.score_nombre,\n score_referencia_nombre: mejor.score_referencia_nombre,\n score_evidencia_nombre: mejor.score_evidencia_nombre,\n cobertura_nombre_en_evidencia: mejor.cobertura_nombre_en_evidencia,\n cobertura_referencia_en_banco: mejor.cobertura_referencia_en_banco,\n score_marca: mejor.score_marca,\n score_cliente: mejor.score_cliente,\n score_pais: mejor.score_pais,\n score_general: mejor.score_general,\n score_clave: mejor.score_clave,\n cobertura_banco_en_propuesta: mejor.cobertura_banco_en_propuesta,\n comunes_nombre_evidencia: mejor.comunes_nombre_evidencia,\n version_banco: mejor.version_banco,\n version_coincide: mejor.version_coincide,\n anio_banco: mejor.anio_banco,\n anio_coincide: mejor.anio_coincide,\n pais_coincide: mejor.pais_coincide,\n marca_coincide: mejor.marca_coincide\n }\n : null,\n segundo_match: segundo\n ? {\n score: segundo.score_porcentaje,\n regla_match: segundo.regla_match,\n row_number: segundo.banco_row_number,\n nombre: segundo.banco_nombre,\n marca: segundo.banco_marca,\n cliente: segundo.banco_cliente,\n pais: segundo.banco_pais,\n file_id: segundo.banco_file_id\n }\n : null,\n top_5: candidatosValidos.slice(0, 5).map(c => ({\n score: c.score_porcentaje,\n score_orden: Math.round(c.score_orden * 100),\n regla_match: c.regla_match,\n row_number: c.banco_row_number,\n nombre: c.banco_nombre,\n marca: c.banco_marca,\n cliente: c.banco_cliente,\n pais: c.banco_pais,\n file_id: c.banco_file_id,\n score_nombre: c.score_nombre,\n score_clave: c.score_clave,\n cobertura_banco_en_propuesta: c.cobertura_banco_en_propuesta,\n comunes_nombre_evidencia: c.comunes_nombre_evidencia,\n version_banco: c.version_banco,\n version_coincide: c.version_coincide,\n anio_banco: c.anio_banco,\n anio_coincide: c.anio_coincide\n }))\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5792,
2144
],
"id": "1e40c947-c41c-499f-9dd7-cd0ac4caa2d3",
"name": "Code - Match propuesta banco Fulgencio TEST"
},
{
"parameters": {
"jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst toNumber = (valor) => {\n const limpio = limpiar(valor).replace(',', '.');\n const n = Number(limpio);\n return Number.isFinite(n) ? n : 0;\n};\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst slidesNormalizado1 = getNodeJson('Code - Normalizar respuesta Slides TEST');\nconst slidesNormalizado2 = getNodeJson('Code - Normalizar respuesta Slides ejecución TEST');\nconst slidesNormalizado3 = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...match,\n ...enriquecido,\n ...actual,\n ...slidesNormalizado1,\n ...slidesNormalizado2,\n ...slidesNormalizado3\n};\n\nconst matchDebug =\n match.match_banco_debug ||\n actual.match_banco_debug ||\n data.match_banco_debug ||\n {};\n\nconst mejorMatch = matchDebug.mejor_match || {};\n\n// --------------------------------------------------\n// 1. Resolver datos principales\n// --------------------------------------------------\n\nconst tipoReporte = normalizarPlano(\n jsonFinal.tipo_reporte ||\n data.tipo_reporte ||\n actual.tipo_reporte ||\n ''\n);\n\nlet matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n actual.propuesta_match_estado ||\n ''\n);\n\nlet matchConfianza = toNumber(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n actual.propuesta_match_confianza ||\n mejorMatch.score ||\n 0\n);\n\nlet nombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n actual.propuesta_nombre_banco ||\n mejorMatch.nombre ||\n ''\n);\n\nconst linkBancoOriginal = limpiar(\n match.propuesta_link_banco ||\n data.propuesta_link_banco ||\n actual.propuesta_link_banco ||\n mejorMatch.link ||\n ''\n);\n\nconst fileIdBanco = limpiar(\n match.propuesta_banco_file_id ||\n data.propuesta_banco_file_id ||\n actual.propuesta_banco_file_id ||\n mejorMatch.file_id ||\n ''\n);\n\nconst rowNumberBanco = limpiar(\n match.propuesta_banco_row_number ||\n data.propuesta_banco_row_number ||\n actual.propuesta_banco_row_number ||\n mejorMatch.row_number ||\n ''\n);\n\nconst enlacesExistentes = limpiar(\n match.propuesta_banco_enlaces_existentes ||\n data.propuesta_banco_enlaces_existentes ||\n actual.propuesta_banco_enlaces_existentes ||\n data.banco_enlaces_existentes ||\n actual.banco_enlaces_existentes ||\n mejorMatch.enlaces_ejecutadas_existentes ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n actual.presentacion_ejecucion_url ||\n data.presentation_url ||\n actual.presentation_url ||\n data.presentationUrl ||\n actual.presentationUrl ||\n data.webViewLink ||\n actual.webViewLink ||\n data.link_presentacion ||\n actual.link_presentacion ||\n slidesNormalizado3.presentacion_ejecucion_url ||\n slidesNormalizado3.presentation_url ||\n slidesNormalizado3.webViewLink ||\n ''\n);\n\nconst presentationId = limpiar(\n data.presentation_id ||\n actual.presentation_id ||\n slidesNormalizado3.presentation_id ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\n// --------------------------------------------------\n// 2. Normalizar estado de match de forma defensiva\n// --------------------------------------------------\n\nif (\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 85\n) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n}\n\nif (\n matchEstado !== 'MATCH_MEDIA_CONFIANZA' &&\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 60 &&\n matchConfianza < 85\n) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n}\n\nif (!matchEstado || matchEstado === 'PENDIENTE') {\n if (matchConfianza >= 85) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n } else if (matchConfianza >= 60) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n } else if (matchConfianza > 0) {\n matchEstado = 'MATCH_BAJA_CONFIANZA';\n } else {\n matchEstado = 'SIN_MATCH';\n }\n}\n\n// --------------------------------------------------\n// 3. Mantener valores enriquecidos\n// --------------------------------------------------\n\nconst propuestaReferenciaFinal = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n actual.propuesta_referencia ||\n ''\n);\n\nconst ubicacionFinal = limpiar(\n enriquecido.ubicacion ||\n data.ubicacion ||\n actual.ubicacion ||\n ''\n);\n\nconst fechaEjecucionFinal = limpiar(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n actual.fecha_ejecucion ||\n ''\n);\n\n// --------------------------------------------------\n// 4. Detectar duplicado de link de forma robusta\n// --------------------------------------------------\n\nconst limpiarUrl = (valor) => {\n return limpiar(valor)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '')\n .trim();\n};\n\nconst normalizarUrlComparacion = (valor) => {\n return limpiarUrl(valor)\n .replace(/\\/edit.*$/i, '')\n .replace(/\\/view.*$/i, '')\n .replace(/\\?.*$/i, '')\n .replace(/#.*$/i, '')\n .replace(/\\/$/, '')\n .toLowerCase();\n};\n\nconst extraerGoogleId = (valor) => {\n const url = limpiarUrl(valor);\n\n if (!url) return '';\n\n const patrones = [\n /\\/presentation\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/document\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/spreadsheets\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,\n /[?&]id=([a-zA-Z0-9_-]+)/\n ];\n\n for (const patron of patrones) {\n const match = url.match(patron);\n if (match?.[1]) return match[1];\n }\n\n return '';\n};\n\nconst extraerLinks = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return [];\n\n return texto\n .split(/[\\n\\r\\t;, ]+/)\n .map(limpiarUrl)\n .filter(Boolean)\n .filter(link => {\n return (\n link.startsWith('http://') ||\n link.startsWith('https://') ||\n link.includes('docs.google.com') ||\n link.includes('drive.google.com')\n );\n });\n};\n\nconst enlacesExistentesLista = extraerLinks(enlacesExistentes);\n\nconst presentationIdDetectado =\n presentationId ||\n extraerGoogleId(presentationUrl);\n\nconst presentationUrlNormalizada = normalizarUrlComparacion(presentationUrl);\n\nconst linkDuplicado = Boolean(\n presentationUrl &&\n enlacesExistentesLista.some((linkExistente) => {\n const idExistente = extraerGoogleId(linkExistente);\n\n if (\n presentationIdDetectado &&\n idExistente &&\n idExistente === presentationIdDetectado\n ) {\n return true;\n }\n\n return normalizarUrlComparacion(linkExistente) === presentationUrlNormalizada;\n })\n);\n\n// --------------------------------------------------\n// 5. Calcular enlaces propuestos\n// --------------------------------------------------\n\nlet enlacesPropuestos = enlacesExistentes;\n\nif (presentationUrl && !linkDuplicado) {\n enlacesPropuestos = enlacesExistentes\n ? `${enlacesExistentes}\\n${presentationUrl}`\n : presentationUrl;\n}\n\n// --------------------------------------------------\n// 6. Decisión automática final\n// --------------------------------------------------\n\nconst motivos = [];\n\nconst esPropuestaEjecutada = tipoReporte === 'PROPUESTA_EJECUTADA';\nconst esPropuestaExterna = tipoReporte === 'PROPUESTA_EXTERNA';\nconst matchAlta = matchEstado === 'MATCH_ALTA_CONFIANZA' && matchConfianza >= 85;\nconst hayMatchBanco = Boolean(rowNumberBanco && nombreBanco);\n\nlet decisionAutomaticaBanco = '';\nlet motivoDecisionAutomatica = '';\n\nif (!presentationUrl) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_ERROR_DATOS';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no existe link de presentación ejecutada.';\n motivos.push('no existe presentacion_ejecucion_url');\n\n} else if (esPropuestaExterna) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_PROPUESTA_EXTERNA';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el reporte fue clasificado como propuesta externa. Se genera presentación y se guarda el registro ejecutado.';\n\n} else if (!esPropuestaEjecutada) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_TIPO_NO_DETERMINADO';\n motivoDecisionAutomatica = `No se actualiza banco original porque tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}.`;\n motivos.push(`tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}`);\n\n} else if (linkDuplicado) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_LINK_DUPLICADO';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el link de presentación ejecutada ya existe en la columna de enlaces ejecutados.';\n motivos.push('el link ya existe en la columna de enlaces ejecutados');\n\n} else if (!hayMatchBanco) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_SIN_MATCH';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no se encontró una propuesta compatible con fila de banco válida.';\n if (!nombreBanco) motivos.push('no llegó propuesta_nombre_banco');\n if (!rowNumberBanco) motivos.push('no llegó propuesta_banco_row_number');\n\n} else if (!matchAlta) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_CONFIANZA_INSUFICIENTE';\n motivoDecisionAutomatica = `No se actualiza banco original porque el match no tiene confianza suficiente. Estado: ${matchEstado || 'SIN_MATCH'}, confianza: ${matchConfianza}.`;\n motivos.push(`propuesta_match_estado es ${matchEstado || 'SIN_MATCH'}`);\n motivos.push(`propuesta_match_confianza es ${matchConfianza}`);\n\n} else {\n decisionAutomaticaBanco = 'ACTUALIZADO_BANCO_AUTO';\n motivoDecisionAutomatica = 'Banco original actualizado automáticamente: propuesta ejecutada, match alto, confianza suficiente y link nuevo.';\n}\n\nconst actualizarBanco = decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO';\n\nconst bancoActualizadoAuto = actualizarBanco ? 'SI' : 'NO';\n\nconst motivoNoActualizaBanco = actualizarBanco\n ? ''\n : motivoDecisionAutomatica;\n\n// --------------------------------------------------\n// 7. Seguridad para evitar updates accidentales\n// --------------------------------------------------\n// Si NO se actualiza, dejamos row_number y banco_match_value vacíos.\n// Así, aunque alguien conecte mal el nodo de Google Sheets,\n// no debería encontrar fila válida para actualizar.\nconst rowNumberSeguro = actualizarBanco ? rowNumberBanco : '';\nconst bancoMatchValueSeguro = actualizarBanco ? rowNumberBanco : '';\nconst enlacesActualizadosSeguro = actualizarBanco ? enlacesPropuestos : enlacesExistentes;\n\n// Campo compatible con lógica anterior.\n// Ya no significa revisión humana; es una decisión automática del sistema.\nlet propuestaMatchRevision = '';\n\nif (decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_LINK_DUPLICADO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION_LINK_DUPLICADO';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_PROPUESTA_EXTERNA') {\n propuestaMatchRevision = 'NO_APLICA_PROPUESTA_EXTERNA';\n} else {\n propuestaMatchRevision = 'NO_ACTUALIZADO_AUTOMATICAMENTE';\n}\n\n// --------------------------------------------------\n// 8. Salida final\n// --------------------------------------------------\n\nreturn [\n {\n json: {\n ...data,\n\n // Mantener explícitamente los valores enriquecidos.\n propuesta_referencia: propuestaReferenciaFinal || data.propuesta_referencia || '',\n ubicacion: ubicacionFinal || data.ubicacion || '',\n fecha_ejecucion: fechaEjecucionFinal || data.fecha_ejecucion || '',\n\n // Mantener explícitamente el link final para nodos posteriores.\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n // Decisión automática nueva.\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n // Compatibilidad con flujo actual.\n actualizar_banco_original: actualizarBanco,\n motivo_no_actualiza_banco: motivoNoActualizaBanco,\n\n propuesta_match_estado: matchEstado || data.propuesta_match_estado || 'SIN_MATCH',\n propuesta_match_confianza: String(matchConfianza || 0),\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n propuesta_banco_enlaces_existentes: enlacesExistentes,\n propuesta_match_revision: propuestaMatchRevision,\n\n banco_match_column: 'row_number',\n banco_match_value: bancoMatchValueSeguro,\n\n row_number: rowNumberSeguro,\n 'Enlaces a propuestas ejecutadas': enlacesActualizadosSeguro,\n\n banco_enlaces_existentes: enlacesExistentes,\n banco_enlaces_propuestos: enlacesPropuestos,\n banco_enlaces_actualizados: enlacesActualizadosSeguro,\n banco_presentacion_url: presentationUrl,\n\n banco_link_duplicado_detectado: linkDuplicado,\n banco_presentacion_id_detectado: presentationIdDetectado,\n banco_enlaces_existentes_lista: enlacesExistentesLista,\n\n datos_enriquecidos_match_banco: Boolean(\n enriquecido.datos_enriquecidos_match_banco ||\n data.datos_enriquecidos_match_banco\n ),\n\n banco_update_debug: {\n tipo_reporte: tipoReporte,\n\n propuesta_match_estado: matchEstado,\n propuesta_match_confianza: matchConfianza,\n match_alta: matchAlta,\n hay_match_banco: hayMatchBanco,\n\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n propuesta_referencia_final: propuestaReferenciaFinal,\n ubicacion_final: ubicacionFinal,\n fecha_ejecucion_final: fechaEjecucionFinal,\n\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n\n presentationUrl,\n presentationId,\n presentationIdDetectado,\n\n enlacesExistentes,\n enlacesExistentesLista,\n enlacesPropuestos,\n enlacesActualizadosSeguro,\n\n linkDuplicado,\n\n rowNumberSeguro,\n bancoMatchValueSeguro,\n\n actualizarBanco,\n motivoNoActualizaBanco,\n\n motivos,\n\n mejor_match: mejorMatch,\n match_debug: matchDebug,\n\n enriquecido_debug: {\n propuesta_referencia: enriquecido.propuesta_referencia || '',\n ubicacion: enriquecido.ubicacion || '',\n fecha_ejecucion: enriquecido.fecha_ejecucion || '',\n datos_enriquecidos_match_banco: enriquecido.datos_enriquecidos_match_banco || false\n }\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
12688,
1952
],
"id": "df339298-bbca-4b0e-9db7-41323b2c44c9",
"name": "Code - Preparar actualización banco propuesta ejecutada TEST"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "dccbea1d-e9cf-4f51-b94d-7b0162737548",
"leftValue": "={{ $json.actualizar_banco_original === true }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
13456,
2000
],
"id": "da284028-5f31-4cda-b1b6-e27cf04b5c3b",
"name": "IF - Actualizar banco match alta confianza TEST"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng",
"mode": "list",
"cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "propuestas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"Enlaces a propuestas ejecutadas": "={{ $json.banco_enlaces_actualizados }}",
"row_number": "={{ $json.propuesta_banco_row_number }}"
},
"matchingColumns": [
"row_number"
],
"schema": [
{
"id": "NOMBRE",
"displayName": "NOMBRE",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "TIPO DE ACCION",
"displayName": "TIPO DE ACCION",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "CLIENTE",
"displayName": "CLIENTE",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "MARCA",
"displayName": "MARCA",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "PAIS",
"displayName": "PAIS",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "CANAL",
"displayName": "CANAL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "AMBIENTE DE COMPRA (RE)",
"displayName": "AMBIENTE DE COMPRA (RE)",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "TÁCTICA PROMOCIONAL",
"displayName": "TÁCTICA PROMOCIONAL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "APROBADA",
"displayName": "APROBADA",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "ETIQUETAS",
"displayName": "ETIQUETAS",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "AÑO",
"displayName": "AÑO",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "Enlace a la propuesta",
"displayName": "Enlace a la propuesta",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "Descripcion",
"displayName": "Descripcion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "file_id",
"displayName": "file_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "nombre_archivo",
"displayName": "nombre_archivo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "mime_type",
"displayName": "mime_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "fuente_pais",
"displayName": "fuente_pais",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "confianza_pais",
"displayName": "confianza_pais",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "requiere_revision",
"displayName": "requiere_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "procesado_ia",
"displayName": "procesado_ia",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "ultima_actualizacion",
"displayName": "ultima_actualizacion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "motivos_revision",
"displayName": "motivos_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "Enlaces a propuestas ejecutadas",
"displayName": "Enlaces a propuestas ejecutadas",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "tablero_sync_key",
"displayName": "tablero_sync_key",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "tablero_project_id",
"displayName": "tablero_project_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "tablero_origen",
"displayName": "tablero_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "drive_modified_time",
"displayName": "drive_modified_time",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "ultimo_refresco_ia",
"displayName": "ultimo_refresco_ia",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "refresh_status",
"displayName": "refresh_status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "refresh_attempts",
"displayName": "refresh_attempts",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "converted_google_slides_id",
"displayName": "converted_google_slides_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "converted_google_slides_link",
"displayName": "converted_google_slides_link",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
13808,
1872
],
"id": "9932dc19-3202-4354-aae7-c33677259f41",
"name": "Sheets - Actualizar link ejecutada en banco TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const respuestaBanco = $json || {};\n\nconst base = $('Code - Preparar actualización banco propuesta ejecutada TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n banco_original_actualizado: true,\n banco_original_response: respuestaBanco\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
14096,
1872
],
"id": "b2470e42-2786-491b-8f8d-84ac5cf7f31a",
"name": "Code - Restaurar contexto actualización banco TEST"
},
{
"parameters": {
"jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valor) => {\n const texto = normalizar(valor);\n\n if (!texto) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bCODIGO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(texto));\n};\n\nconst normalizarUbicacion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst normalizarFechaEjecucion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (/^20\\d{2}$/.test(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst matchAltaOMedia = [\n 'MATCH_ALTA_CONFIANZA',\n 'MATCH_MEDIA_CONFIANZA'\n].includes(normalizar(data.propuesta_match_estado));\n\nconst nombreBanco = limpiar(data.propuesta_nombre_banco);\n\nconst referenciaOriginal = limpiar(data.propuesta_referencia);\n\nconst referenciaCanonica = matchAltaOMedia && nombreBanco\n ? nombreBanco\n : referenciaOriginal;\n\nconst ubicacionLimpia = normalizarUbicacion(data.ubicacion);\nconst fechaLimpia = normalizarFechaEjecucion(data.fecha_ejecucion);\n\nreturn [\n {\n json: {\n ...data,\n\n propuesta_referencia_original_ia: referenciaOriginal,\n ubicacion_original_ia: data.ubicacion || '',\n fecha_ejecucion_original_ia: data.fecha_ejecucion || '',\n\n propuesta_referencia: limpiarReferenciaVisual(referenciaCanonica),\n ubicacion: ubicacionLimpia,\n fecha_ejecucion: fechaLimpia,\n\n datos_enriquecidos_match_banco: true\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6000,
2160
],
"id": "cb2a57aa-9f34-454e-a77f-8096bd49228b",
"name": "Code - Enriquecer datos con match banco TEST"
},
{
"parameters": {
"jsCode": "const evento = $('Code - Normalizar evento WhatsApp TEST').first().json || {};\nconst grupos = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarTexto = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst normalizarJid = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return numero ? `${numero}@s.whatsapp.net` : '';\n};\n\nconst getCampo = (row, nombres) => {\n const keys = Object.keys(row || {});\n\n const normalizarKey = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '');\n };\n\n for (const nombre of nombres) {\n const nombreNorm = normalizarKey(nombre);\n\n const keyExacta = keys.find(k => normalizarKey(k) === nombreNorm);\n\n if (keyExacta !== undefined && limpiar(row[keyExacta]) !== '') {\n return limpiar(row[keyExacta]);\n }\n }\n\n return '';\n};\n\n// --------------------------------------------------\n// 1. Identificar origen del evento\n// --------------------------------------------------\n\nconst rawEvent = evento.raw_event || {};\nconst rawData = rawEvent.data || {};\nconst rawMessage = rawData.message || {};\n\nconst remoteJid = normalizarJid(\n evento.whatsapp_remote_jid ||\n evento.group_jid ||\n rawData.key?.remoteJid ||\n ''\n);\n\nconst groupJid = normalizarJid(\n evento.group_jid ||\n rawData.key?.remoteJid ||\n ''\n);\n\nconst isGroup = Boolean(evento.is_group || remoteJid.endsWith('@g.us'));\n\nconst messageType = limpiar(evento.message_type).toLowerCase();\nconst accion = limpiar(evento.accion_flujo).toUpperCase();\nconst texto = limpiar(evento.texto);\n\n// --------------------------------------------------\n// 2. Detectar eventos que nunca deben procesarse\n// --------------------------------------------------\n\nconst esReaccion = Boolean(\n rawMessage.reactionMessage ||\n rawMessage.protocolMessage?.type === 14 ||\n messageType === 'reaction' ||\n messageType === 'message_reaction' ||\n messageType === 'reactionmessage'\n);\n\nconst esMensajeDelBot = Boolean(\n evento.from_me ||\n rawData.key?.fromMe\n);\n\nconst esEventoSinContenido = !texto &&\n !evento.tiene_media &&\n !['audio', 'image', 'video', 'document'].includes(messageType);\n\n// --------------------------------------------------\n// 3. Leer grupos permitidos desde Google Sheets\n// --------------------------------------------------\n\nconst gruposNormalizados = grupos.map((row, index) => {\n const groupId = normalizarJid(\n getCampo(row, [\n 'group_jid',\n 'grupo_jid',\n 'jid',\n 'grupo',\n 'group',\n 'id_grupo'\n ])\n );\n\n const nombreGrupo = getCampo(row, [\n 'nombre_grupo',\n 'nombre',\n 'name'\n ]);\n\n const proyecto = getCampo(row, [\n 'proyecto',\n 'project'\n ]);\n\n const activo = getCampo(row, [\n 'activo',\n 'active',\n 'habilitado',\n 'permitido'\n ]);\n\n const activoNormalizado = normalizarTexto(activo);\n\n const estaActivo = [\n 'SI',\n 'SÍ',\n 'YES',\n 'TRUE',\n 'ACTIVO',\n '1'\n ].includes(activoNormalizado);\n\n return {\n index,\n row,\n group_jid: groupId,\n nombre_grupo: nombreGrupo,\n proyecto,\n activo,\n activo_normalizado: activoNormalizado,\n esta_activo: estaActivo\n };\n});\n\nconst grupoEncontrado = gruposNormalizados.find(grupo => {\n return grupo.group_jid && grupo.group_jid === (groupJid || remoteJid);\n}) || null;\n\nconst grupoPermitido = Boolean(\n isGroup &&\n grupoEncontrado &&\n grupoEncontrado.esta_activo\n);\n\n// Si en el futuro quieres permitir DM directo al bot, cambia esto a true.\nconst permitirChatIndividual = false;\nconst chatIndividualPermitido = !isGroup && permitirChatIndividual;\n\n// --------------------------------------------------\n// 4. Decidir si procesar o ignorar\n// --------------------------------------------------\n\nconst motivosIgnorar = [];\n\nif (esMensajeDelBot) {\n motivosIgnorar.push('mensaje enviado por el propio bot');\n}\n\nif (esReaccion) {\n motivosIgnorar.push('evento de reacción');\n}\n\nif (esEventoSinContenido) {\n motivosIgnorar.push('evento sin texto ni media útil');\n}\n\nif (isGroup && !grupoEncontrado) {\n motivosIgnorar.push(`grupo no encontrado en wa_grupos_permitidos: ${groupJid || remoteJid || 'SIN_GRUPO'}`);\n}\n\nif (isGroup && grupoEncontrado && !grupoEncontrado.esta_activo) {\n motivosIgnorar.push(`grupo encontrado pero inactivo: ${groupJid || remoteJid || 'SIN_GRUPO'}`);\n}\n\nif (!isGroup && !chatIndividualPermitido) {\n motivosIgnorar.push('chat individual no permitido');\n}\n\nconst procesar = motivosIgnorar.length === 0;\n\nreturn [\n {\n json: {\n ...evento,\n\n filtro_origen_procesar: procesar,\n filtro_origen_decision: procesar ? 'PROCESAR' : 'IGNORAR',\n filtro_origen_motivo: motivosIgnorar.join('; '),\n\n grupo_permitido: grupoPermitido,\n grupo_permiso_estado: grupoPermitido ? 'PERMITIDO' : 'DENEGADO',\n grupo_autorizado: grupoEncontrado?.row || null,\n grupo_nombre_autorizado: grupoEncontrado?.nombre_grupo || evento.group_name || '',\n grupo_proyecto_autorizado: grupoEncontrado?.proyecto || '',\n\n filtro_origen_debug: {\n remoteJid,\n groupJid,\n isGroup,\n messageType,\n accion,\n texto,\n\n esReaccion,\n esMensajeDelBot,\n esEventoSinContenido,\n\n total_grupos_leidos: grupos.length,\n grupo_encontrado: Boolean(grupoEncontrado),\n grupo_permitido: grupoPermitido,\n grupo_encontrado_debug: grupoEncontrado\n ? {\n group_jid: grupoEncontrado.group_jid,\n nombre_grupo: grupoEncontrado.nombre_grupo,\n proyecto: grupoEncontrado.proyecto,\n activo: grupoEncontrado.activo,\n activo_normalizado: grupoEncontrado.activo_normalizado,\n esta_activo: grupoEncontrado.esta_activo\n }\n : null,\n\n grupos_leidos_debug: gruposNormalizados.slice(0, 10).map(grupo => ({\n index: grupo.index,\n group_jid: grupo.group_jid,\n nombre_grupo: grupo.nombre_grupo,\n proyecto: grupo.proyecto,\n activo: grupo.activo,\n activo_normalizado: grupo.activo_normalizado,\n esta_activo: grupo.esta_activo\n })),\n\n chatIndividualPermitido\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-8880,
-1024
],
"id": "f472e804-82c8-443f-b847-e3adb44aafdf",
"name": "Code - Filtro origen WhatsApp TEST"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "330d3843-6562-4ba6-afce-9a4bef9666b1",
"leftValue": "={{ $json.filtro_origen_procesar }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
-8368,
-1056
],
"id": "146eed23-6f86-47a5-89fb-56b7ac54646f",
"name": "IF - Procesar solo grupo permitido TEST"
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 50533208,
"mode": "list",
"cachedResultName": "wa_grupos_permitidos",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=50533208"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-9088,
-1024
],
"id": "542039fa-a89d-4655-b767-05dd55a0014b",
"name": "Sheets - Leer grupos permitidos WhatsApp TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 737153956,
"mode": "list",
"cachedResultName": "propuestas_ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"decision_automatica_banco": "={{ $json.decision_automatica_banco }}",
"banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}",
"propuesta_match_revision": "={{ $json.propuesta_match_revision }}",
"motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "fecha_recepcion",
"displayName": "fecha_recepcion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "fecha_ejecucion",
"displayName": "fecha_ejecucion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "propuesta_referencia",
"displayName": "propuesta_referencia",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "propuesta_match_estado",
"displayName": "propuesta_match_estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "propuesta_match_confianza",
"displayName": "propuesta_match_confianza",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "propuesta_nombre_banco",
"displayName": "propuesta_nombre_banco",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "propuesta_link_banco",
"displayName": "propuesta_link_banco",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "marca",
"displayName": "marca",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "cliente",
"displayName": "cliente",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "pais",
"displayName": "pais",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "ubicacion",
"displayName": "ubicacion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "comentario_original",
"displayName": "comentario_original",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "resumen_ia",
"displayName": "resumen_ia",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "descripcion_ejecucion",
"displayName": "descripcion_ejecucion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "elementos_detectados",
"displayName": "elementos_detectados",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "tags",
"displayName": "tags",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "media_folder_url",
"displayName": "media_folder_url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "presentacion_ejecucion_url",
"displayName": "presentacion_ejecucion_url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "fotos_count",
"displayName": "fotos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "audios_count",
"displayName": "audios_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "estado_revision",
"displayName": "estado_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "ultima_actualizacion",
"displayName": "ultima_actualizacion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "transcripcion_audio",
"displayName": "transcripcion_audio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "propuesta_match_revision",
"displayName": "propuesta_match_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "tipo_reporte_confianza",
"displayName": "tipo_reporte_confianza",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "tipo_reporte_motivo",
"displayName": "tipo_reporte_motivo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "decision_automatica_banco",
"displayName": "decision_automatica_banco",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "motivo_decision_automatica",
"displayName": "motivo_decision_automatica",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "banco_actualizado_auto",
"displayName": "banco_actualizado_auto",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
12944,
2032
],
"id": "cd1d8a04-7c10-400f-955d-05824392abc9",
"name": "Sheets - Actualizar decisión automática banco TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst decisionOriginal = getNodeJson('Code - Preparar actualización banco propuesta ejecutada TEST');\nconst updateDecisionSheet = $json || {};\n\nreturn [\n {\n json: {\n ...decisionOriginal,\n\n decision_sheet_actualizada: true,\n decision_sheet_update_debug: {\n row_number: updateDecisionSheet.row_number || '',\n session_id: updateDecisionSheet.session_id || decisionOriginal.session_id || '',\n decision_automatica_banco:\n updateDecisionSheet.decision_automatica_banco ||\n decisionOriginal.decision_automatica_banco ||\n '',\n banco_actualizado_auto:\n updateDecisionSheet.banco_actualizado_auto ||\n decisionOriginal.banco_actualizado_auto ||\n ''\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
13152,
2032
],
"id": "2e297186-0656-4331-9df0-28a61b933861",
"name": "Code - Restaurar contexto decisión automática banco TEST"
},
{
"parameters": {
"jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst sessionId = limpiar(data.session_id);\nconst eventId = limpiar(data.event_id || data.media_source_id || `${Date.now()}`);\nconst mediaSourceId = limpiar(data.media_source_id || data.event_id || eventId);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para crear buffer Redis de imagen.');\n}\n\nif (!mediaSourceId) {\n throw new Error('No llegó media_source_id/event_id para crear buffer Redis de imagen.');\n}\n\nconst redisKey = `fulgencio:media:${sessionId}:image`;\n\nconst bufferItem = {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n\n event_id: eventId,\n media_source_id: mediaSourceId,\n message_type: 'image',\n media_type: 'image',\n\n buffer_received_at: new Date().toISOString(),\n buffer_received_at_ms: Date.now()\n};\n\nreturn [\n {\n json: {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n redis_payload: JSON.stringify(bufferItem),\n\n estado_buffer_redis: 'IMAGEN_ENVIADA_A_BUFFER'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4576,
-1648
],
"id": "64b18b2d-f601-41c6-a041-4b9c8386581c",
"name": "Code - Preparar buffer imagen Redis TEST"
},
{
"parameters": {
"operation": "push",
"list": "={{ $json.redis_buffer_key }}",
"messageData": "={{ $json.redis_payload }}",
"tail": true
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-4368,
-1648
],
"id": "a23af96d-d5a1-4186-a9f4-0fd217ac6f5e",
"name": "Redis - Push buffer imagen TEST",
"credentials": {
"redis": {
"id": "Fu2IevwVPaoyw9OS",
"name": "Redis Fulgencio"
}
}
},
{
"parameters": {
"jsCode": "const original = $('Code - Preparar buffer imagen Redis TEST').first().json || {};\nconst redisPushResult = $json || {};\n\nreturn [\n {\n json: {\n ...original,\n redis_push_result: redisPushResult,\n redis_push_ok: true,\n estado_buffer_redis: 'IMAGEN_GUARDADA_EN_REDIS'\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4160,
-1648
],
"id": "5b888e51-25ea-4c3d-b1b3-a75053c95498",
"name": "Code - Restaurar contexto buffer imagen TEST"
},
{
"parameters": {},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
-3952,
-1648
],
"id": "d96a4efc-a786-451c-b1f4-d2e838b8e8be",
"name": "Wait - Esperar cierre buffer imagen TEST",
"webhookId": "5c73c08e-d77a-448d-a8ab-11080d08b476"
},
{
"parameters": {
"operation": "get",
"propertyName": "redis_buffer_items",
"key": "={{ $('Code - Restaurar contexto buffer imagen TEST').first().json.redis_buffer_key }}",
"keyType": "list",
"options": {}
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-3744,
-1648
],
"id": "a6cfe645-a160-4e56-9f9e-6ef5a8912bb7",
"name": "Redis - Leer buffer imagen TEST",
"credentials": {
"redis": {
"id": "Fu2IevwVPaoyw9OS",
"name": "Redis Fulgencio"
}
}
},
{
"parameters": {
"jsCode": "const contexto = $('Code - Restaurar contexto buffer imagen TEST').first().json || {};\nconst redisOutput = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst currentBufferId = limpiar(contexto.redis_buffer_id);\nconst sessionId = limpiar(contexto.session_id);\nconst redisKey = limpiar(contexto.redis_buffer_key);\n\nlet rawItems = redisOutput.redis_buffer_items || redisOutput.message || [];\n\nif (!Array.isArray(rawItems)) {\n rawItems = rawItems ? [rawItems] : [];\n}\n\nconst parsed = [];\n\nfor (const raw of rawItems) {\n try {\n const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;\n if (obj && typeof obj === 'object') {\n parsed.push(obj);\n }\n } catch (error) {\n // Ignorar items corruptos del buffer\n }\n}\n\n// Deduplicar por media_source_id/event_id\nconst vistos = new Set();\nconst lote = [];\n\nfor (const item of parsed) {\n const id = limpiar(item.media_source_id || item.event_id || item.redis_buffer_id);\n if (!id) continue;\n if (vistos.has(id)) continue;\n\n vistos.add(id);\n\n lote.push({\n ...item,\n redis_buffer_key: redisKey,\n session_id: limpiar(item.session_id || sessionId),\n media_source_id: limpiar(item.media_source_id || id),\n event_id: limpiar(item.event_id || id),\n message_type: 'image',\n media_type: 'image'\n });\n}\n\nlote.sort((a, b) => {\n return Number(a.buffer_received_at_ms || 0) - Number(b.buffer_received_at_ms || 0);\n});\n\nconst ultimo = lote[lote.length - 1] || {};\nconst ultimoId = limpiar(ultimo.redis_buffer_id || ultimo.event_id || ultimo.media_source_id);\n\nconst debeProcesar =\n lote.length > 0 &&\n currentBufferId &&\n ultimoId &&\n currentBufferId === ultimoId;\n\nconst imagenesPrevias = Number(\n contexto.sesion_activa?.imagenes_count ??\n contexto.imagenes_count ??\n 0\n) || 0;\n\nconst imagenesNuevas = lote.length;\nconst imagenesTotal = imagenesPrevias + imagenesNuevas;\n\nreturn [\n {\n json: {\n ...contexto,\n\n redis_buffer_items_count: rawItems.length,\n redis_buffer_lote_count: lote.length,\n redis_buffer_lote: lote,\n\n redis_buffer_ultimo_id: ultimoId,\n redis_buffer_current_id: currentBufferId,\n\n redis_debe_procesar_lote: debeProcesar,\n redis_decision: debeProcesar ? 'PROCESAR_LOTE_IMAGEN' : 'IGNORAR_EJECUCION_INTERMEDIA',\n\n imagenes_previas: imagenesPrevias,\n imagenes_nuevas_lote: imagenesNuevas,\n imagenes_count: imagenesTotal,\n\n estado_buffer_redis: debeProcesar\n ? 'LOTE_IMAGEN_LISTO_PARA_PROCESAR'\n : 'IMAGEN_EN_BUFFER_ESPERANDO_ULTIMA_EJECUCION',\n\n redis_debug: {\n redisKey,\n rawItemsCount: rawItems.length,\n loteCount: lote.length,\n currentBufferId,\n ultimoId,\n debeProcesar\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3536,
-1648
],
"id": "3dd5e31b-2cee-4b91-9d21-091161591242",
"name": "Code - Decidir procesar lote imagen Redis TEST"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "f2394967-cd4b-4c6e-89be-7d9410d8a68b",
"leftValue": "={{ $json.redis_debe_procesar_lote }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
-3312,
-1648
],
"id": "ad472bb4-83c4-4980-bdcc-ab1b185ffe71",
"name": "IF - Procesar lote imagen Redis TEST"
},
{
"parameters": {
"operation": "delete",
"key": "={{ $json.redis_buffer_key }}"
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-3040,
-1664
],
"id": "46b07f3c-4e6a-4c32-be24-a6bbfcf2ea69",
"name": "Redis - Borrar buffer imagen TEST",
"credentials": {
"redis": {
"id": "Fu2IevwVPaoyw9OS",
"name": "Redis Fulgencio"
}
}
},
{
"parameters": {
"jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst lote = Array.isArray(contexto.redis_buffer_lote)\n ? contexto.redis_buffer_lote\n : [];\n\nif (!lote.length) {\n return [];\n}\n\nreturn lote.map((item, index) => {\n return {\n json: {\n ...contexto,\n ...item,\n\n media_index: index + 1,\n media_total: lote.length,\n\n estado: 'IMAGEN_RECIBIDA',\n fecha_procesado: new Date().toISOString(),\n\n lote_imagen_redis_total: lote.length,\n redis_lote_procesado: true\n }\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2832,
-1664
],
"id": "a28b6faa-d06a-45cb-9f43-7a159eb5d4e4",
"name": "Code - Expandir lote imagen Redis TEST"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 306129743,
"mode": "list",
"cachedResultName": "wa_ejecuciones_eventos",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"event_id": "={{ $json.event_id }}",
"manager_telefono": "={{ $json.manager_telefono }}",
"manager_nombre": "={{ $json.manager_nombre }}",
"tiene_media": "={{ $json.tiene_media }}",
"estado": "=IMAGEN_RECIBIDA",
"session_id": "={{ $json.session_id }}",
"canal_origen": "=WHATSAPP",
"fecha_recepcion": "={{ $json.fecha_recepcion }}",
"message_type": "={{ $json.message_type }}",
"texto": "={{ $json.texto }}",
"media_count": "={{ $json.media_count }}",
"raw_preview": "={{ $json.raw_preview }}",
"etapa_recibida": "={{ $json.etapa_actual }}",
"media_source_id": "={{ $json.media_source_id }}",
"media_mime_type": "={{ $json.media_mime_type }}",
"media_file_name": "={{ $json.media_file_name }}",
"whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}",
"comando": "={{ $json.accion_flujo }}",
"ejecucion_id": "={{ $json.ejecucion_id }}",
"fecha_procesado": "={{ new Date().toISOString() }}"
},
"matchingColumns": [],
"schema": [
{
"id": "event_id",
"displayName": "event_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_recepcion",
"displayName": "fecha_recepcion",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "message_type",
"displayName": "message_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "texto",
"displayName": "texto",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "comando",
"displayName": "comando",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "tiene_media",
"displayName": "tiene_media",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_count",
"displayName": "media_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "raw_preview",
"displayName": "raw_preview",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "fecha_procesado",
"displayName": "fecha_procesado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "etapa_recibida",
"displayName": "etapa_recibida",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "media_source_id",
"displayName": "media_source_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "media_mime_type",
"displayName": "media_mime_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "media_file_name",
"displayName": "media_file_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "whatsapp_remote_jid",
"displayName": "whatsapp_remote_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-2592,
-1664
],
"id": "8b7ff874-bd05-499e-8655-381366cdba2e",
"name": "Sheets - Guardar eventos imagen lote Redis TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst guardados = $input.all().map(item => item.json || {});\n\nconst loteCount = Number(contexto.redis_buffer_lote_count || guardados.length || 0);\nconst imagenesPrevias = Number(contexto.imagenes_previas || 0) || 0;\nconst imagenesTotal = Number(contexto.imagenes_count || (imagenesPrevias + loteCount)) || loteCount;\n\nreturn [\n {\n json: {\n ...contexto,\n\n eventos_imagen_guardados: guardados.length,\n imagenes_nuevas_lote: loteCount,\n imagenes_count: imagenesTotal,\n\n ultima_actividad: new Date().toISOString(),\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'IMAGENES_RECIBIDAS',\n motivo_revision: `IMAGENES_RECIBIDAS_LOTE_${loteCount}`,\n\n redis_lote_imagen_guardado: true\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2384,
-1664
],
"id": "b75fb900-5602-4d29-9a8e-9418a8262ee2",
"name": "Code - Consolidar lote imagen guardado Redis TEST"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 616166581,
"mode": "list",
"cachedResultName": "wa_ejecuciones_sesiones",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"session_id": "={{ $json.session_id }}",
"estado": "={{ $json.estado }}",
"imagenes_count": "={{ $json.imagenes_count }}",
"etapa": "={{ $json.etapa }}",
"ultima_actividad": "={{ $json.ultima_actividad }}",
"motivo_revision": "={{ $json.motivo_revision }}"
},
"matchingColumns": [
"session_id"
],
"schema": [
{
"id": "session_id",
"displayName": "session_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "manager_telefono",
"displayName": "manager_telefono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "manager_nombre",
"displayName": "manager_nombre",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "canal_origen",
"displayName": "canal_origen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "fecha_inicio",
"displayName": "fecha_inicio",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "ultima_actividad",
"displayName": "ultima_actividad",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "etapa",
"displayName": "etapa",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "audio_count",
"displayName": "audio_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "imagenes_count",
"displayName": "imagenes_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "videos_count",
"displayName": "videos_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "estado",
"displayName": "estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "ejecucion_id",
"displayName": "ejecucion_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "motivo_revision",
"displayName": "motivo_revision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "is_group",
"displayName": "is_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "group_jid",
"displayName": "group_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "group_name",
"displayName": "group_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "sender_jid",
"displayName": "sender_jid",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "sender_phone",
"displayName": "sender_phone",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "sender_name",
"displayName": "sender_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "country_code",
"displayName": "country_code",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "pais_detectado",
"displayName": "pais_detectado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "idioma_flujo",
"displayName": "idioma_flujo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "tipo_reporte",
"displayName": "tipo_reporte",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-2128,
-1664
],
"id": "fb406c50-cea0-46bd-8ddd-cdd0b6b796ee",
"name": "Sheets - Actualizar sesión imágenes lote Redis TEST",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const contexto = $('Code - Consolidar lote imagen guardado Redis TEST').first().json || {};\nconst data = {\n ...contexto,\n ...($json || {})\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(data.idioma_flujo || 'ES').toUpperCase();\n\nconst to = limpiar(\n data.whatsapp_to ||\n data.group_jid ||\n data.whatsapp_remote_jid ||\n data.sesion_activa?.group_jid ||\n data.sesion_activa?.whatsapp_to ||\n data.sender_jid ||\n ''\n);\n\nconst count = Number(\n data.imagenes_nuevas_lote ||\n data.redis_buffer_lote_count ||\n data.eventos_imagen_guardados ||\n 1\n) || 1;\n\nif (!to) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar confirmación de imágenes.');\n}\n\nconst pluralEs = count === 1 ? 'imagen recibida' : 'imágenes recibidas';\nconst pluralEn = count === 1 ? 'image received' : 'images received';\n\nconst mensajeEs =\n`✅ ${count} ${pluralEs}.\n\nPuedes enviar más imágenes si hace falta.\n\nCuando termines de enviar las imágenes, escribe: FOTOS LISTAS`;\n\nconst mensajeEn =\n`✅ ${count} ${pluralEn}.\n\nYou can send more images if needed.\n\nWhen you finish sending images, write: PHOTOS READY`;\n\nconst mensajeFinal = idioma === 'EN' ? mensajeEn : mensajeEs;\n\nreturn [\n {\n json: {\n ...data,\n\n whatsapp_to: to,\n\n // Campos compatibles con tu nodo WhatsApp actual\n whatsapp_text: mensajeFinal,\n texto_respuesta: mensajeFinal,\n mensaje: mensajeFinal,\n text: mensajeFinal,\n\n // Campos debug\n mensaje_whatsapp: mensajeFinal,\n message_text: mensajeFinal,\n estado_mensaje: 'CONFIRMACION_IMAGENES_LOTE_PREPARADA',\n confirmacion_imagenes_lote_debug: {\n idioma,\n whatsapp_to: to,\n imagenes_nuevas_lote: count\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1792,
-1648
],
"id": "96bf8413-9d42-4f7c-a3da-acb387f9caa5",
"name": "Code - Preparar confirmación imágenes lote Redis TEST"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "2cb38fef-879c-45f3-8886-b2f0a35d067b",
"leftValue": "={{ $json.analisis_debe_intentar_cierre }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1280,
2208
],
"id": "7995206c-6cce-441d-a03f-9ffb145d4449",
"name": "IF - Análisis completo Redis TEST"
},
{
"parameters": {
"operation": "incr",
"key": "={{ $json.redis_lock_key }}",
"expire": true,
"ttl": 86400
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
2880,
2112
],
"id": "b6846811-0e07-472f-9680-7ce231a836a2",
"name": "Redis - Lock análisis final TEST",
"credentials": {
"redis": {
"id": "Fu2IevwVPaoyw9OS",
"name": "Redis Fulgencio"
}
}
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const actual = $input.item.json || {};\n\nconst extraerNumero = (valor) => {\n if (typeof valor === 'number') return valor;\n\n if (typeof valor === 'string') {\n const n = Number(valor);\n if (!Number.isNaN(n)) return n;\n }\n\n if (valor && typeof valor === 'object') {\n const valores = Object.values(valor);\n for (const v of valores) {\n const n = extraerNumero(v);\n if (n) return n;\n }\n }\n\n return 0;\n};\n\nconst redisActual = actual.redis_lock_response || actual;\n\nconst lockValor = extraerNumero(redisActual);\nconst lockAdquirido = lockValor === 1;\n\nreturn {\n json: {\n ...actual,\n\n redis_lock_response: redisActual,\n redis_lock_value: lockValor,\n\n lock_analisis_adquirido: lockAdquirido,\n decision_lock_analisis: lockAdquirido\n ? 'LOCK_ADQUIRIDO_GENERAR_REPORTE_FINAL'\n : 'LOCK_YA_EXISTIA_NO_GENERAR_DUPLICADO',\n\n lock_analisis_debug: {\n session_id: actual.session_id || '',\n redis_lock_key: actual.redis_lock_key || '',\n redis_lock_value: lockValor,\n lock_analisis_adquirido: lockAdquirido,\n redis_lock_response: redisActual,\n },\n },\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3088,
2112
],
"id": "69448b97-ba47-40ec-b7f6-2e43f2fca593",
"name": "Code - Validar lock análisis final TEST"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "432f82ae-e1bb-4ea7-a964-0cc4bebef5de",
"leftValue": "={{ $json.lock_analisis_adquirido }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
3296,
2112
],
"id": "110f816e-13e6-42ef-b8b1-9396d0217f4c",
"name": "IF - Lock análisis final adquirido TEST"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const actual = $input.item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n actual.session_id ||\n actual.sesion_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n actual.ejecucion_id ||\n sessionId\n);\n\nconst mediaEventId = limpiar(\n actual.media_event_id ||\n actual.message_id ||\n actual.id ||\n ''\n);\n\nconst mediaType = limpiar(\n actual.media_type ||\n actual.tipo_media ||\n ''\n).toLowerCase();\n\nconst mediaIndex = toNumber(\n actual.media_index ||\n actual.index ||\n 1,\n 1\n);\n\nconst mediaTotal = toNumber(\n actual.media_total_esperado ||\n actual.media_total ||\n actual.total_media ||\n 1,\n 1\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nif (!mediaEventId) {\n throw new Error('No llegó media_event_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nconst redisCountKey = `fulgencio:analisis-count:${sessionId}`;\nconst redisLockKey = `fulgencio:analisis-final-lock:${sessionId}`;\n\nreturn {\n json: {\n ...actual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: mediaEventId,\n media_type: mediaType,\n\n media_index: mediaIndex,\n media_total: mediaTotal,\n media_total_esperado: mediaTotal,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n contador_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: mediaType,\n media_index: mediaIndex,\n media_total: mediaTotal,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey\n }\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
416,
2304
],
"id": "5e9bef81-fa4c-4c86-9a85-3f15927d32de",
"name": "Code - Preparar contador análisis Redis TEST"
},
{
"parameters": {
"operation": "incr",
"key": "={{ $json.redis_count_key }}",
"expire": true,
"ttl": 86400
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
672,
2304
],
"id": "6fe212ba-75bb-4e41-b35e-9d7d222ad407",
"name": "Redis - Contar análisis media TEST",
"credentials": {
"redis": {
"id": "Fu2IevwVPaoyw9OS",
"name": "Redis Fulgencio"
}
}
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const redisActual = $input.item.json || {};\nconst ctx = $('Code - Preparar contador análisis Redis TEST').item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(ctx.session_id);\nconst ejecucionId = limpiar(ctx.ejecucion_id || sessionId);\n\nconst mediaTotalEsperado = toNumber(\n ctx.media_total_esperado ||\n ctx.media_total ||\n 1,\n 1\n);\n\nconst redisCountKey = limpiar(\n ctx.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n ctx.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\n// El Redis Increment puede devolver:\n// { \"fulgencio:analisis-count:SESSION\": 2 }\n// o puede devolver otro nombre según n8n.\nlet redisConteoActual = 0;\n\nif (redisCountKey && redisActual[redisCountKey] !== undefined) {\n redisConteoActual = toNumber(redisActual[redisCountKey], 0);\n} else {\n const valoresNumericos = Object.values(redisActual)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n redisConteoActual = valoresNumericos.length ? valoresNumericos[0] : 0;\n}\n\nconst analisisCompleto =\n mediaTotalEsperado > 0 &&\n redisConteoActual >= mediaTotalEsperado;\n\nreturn {\n json: {\n ...ctx,\n\n redis_increment_response: redisActual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_contados_redis: redisConteoActual,\n\n analisis_debe_intentar_cierre: analisisCompleto,\n decision_cierre_analisis: analisisCompleto\n ? 'ANALISIS_COMPLETO_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_ESPERAR_OTRA_EJECUCION',\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n cierre_analisis_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n redisConteoActual,\n analisisCompleto,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n media_actual: {\n media_event_id: ctx.media_event_id,\n media_type: ctx.media_type,\n media_index: ctx.media_index,\n media_total: ctx.media_total\n },\n redis_increment_response: redisActual\n }\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
912,
2304
],
"id": "f7041b6a-682a-4470-a76e-7dc7efd8586f",
"name": "Code - Validar conteo análisis Redis TEST"
},
{
"parameters": {
"amount": 4
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
1568,
2384
],
"id": "53135800-1202-4d11-b16e-71c7f8442f84",
"name": "Wait - Verificar cierre análisis Redis TEST",
"webhookId": "b17e9d5b-ec57-49ea-a0a7-91bf65e5e41a"
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk",
"mode": "list",
"cachedResultName": "Fulgencio - Propuestas Ejecutadas",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 1107394537,
"mode": "list",
"cachedResultName": "wa_ejecuciones_analisis_media",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537"
},
"filtersUI": {
"values": [
{
"lookupColumn": "session_id",
"lookupValue": "={{ $json.session_id }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1840,
2416
],
"id": "7d1d5352-258d-493e-a6e9-3579fbe54be0",
"name": "Sheets - Leer análisis media cierre Redis TEST",
"alwaysOutputData": true,
"executeOnce": true,
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const inputRows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst contextos = [\n getNodeJson('Code - Validar reintento cierre análisis TEST'),\n getNodeJson('Wait - Reintento cierre análisis Redis TEST'),\n getNodeJson('Wait - Verificar cierre análisis Redis TEST'),\n getNodeJson('Code - Validar conteo análisis Redis TEST'),\n getNodeJson('Code - Preparar contador análisis Redis TEST')\n];\n\nconst contexto =\n contextos.find(ctx => limpiar(ctx.session_id)) ||\n inputRows.find(row => limpiar(row.session_id)) ||\n {};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n inputRows[0]?.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n inputRows[0]?.ejecucion_id ||\n sessionId\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para decidir cierre de análisis por Sheet.');\n}\n\nconst redisCountKey = limpiar(\n contexto.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n contexto.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\nconst rowsSesion = inputRows.filter(row => {\n return limpiar(row.session_id) === sessionId;\n});\n\nconst rowsCompletados = rowsSesion.filter(row => {\n const estado = limpiar(row.estado).toUpperCase();\n\n return (\n estado.includes('COMPLETADO') &&\n (\n estado.includes('ANALISIS_AUDIO') ||\n estado.includes('ANALISIS_IMAGEN') ||\n estado.includes('ANALISIS_VIDEO')\n )\n );\n});\n\nconst mapaUnicos = new Map();\n\nfor (const row of rowsCompletados) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaIndex = limpiar(row.media_index);\n const mediaType = limpiar(row.media_type).toLowerCase();\n\n const key = mediaEventId || `${mediaType}_${mediaIndex}`;\n\n if (!key) continue;\n\n if (!mapaUnicos.has(key)) {\n mapaUnicos.set(key, row);\n }\n}\n\nconst analisisUnicos = Array.from(mapaUnicos.values()).sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst esperadoDesdeContexto = Number(\n contexto.media_total_esperado ||\n contexto.media_total ||\n 0\n);\n\nconst maxMediaTotalSheet = Math.max(\n 0,\n ...analisisUnicos\n .map(row => Number(row.media_total || row.media_total_esperado || 0))\n .filter(n => Number.isFinite(n))\n);\n\nconst mediaTotalEsperado = esperadoDesdeContexto || maxMediaTotalSheet || 0;\nconst totalAnalisisSheet = analisisUnicos.length;\n\nconst analisisCompletoPorSheet =\n mediaTotalEsperado > 0 &&\n totalAnalisisSheet >= mediaTotalEsperado;\n\nreturn [\n {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n media_total_esperado: mediaTotalEsperado,\n\n total_analisis_encontrados_sheet: totalAnalisisSheet,\n analisis_completo_por_sheet: analisisCompletoPorSheet,\n analisis_debe_intentar_cierre: analisisCompletoPorSheet,\n\n decision_cierre_analisis: analisisCompletoPorSheet\n ? 'ANALISIS_COMPLETO_POR_SHEET_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_POR_SHEET_ESPERAR_REINTENTO',\n\n analisis_rows_filtrados: analisisUnicos,\n\n cierre_analisis_sheet_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n totalAnalisisSheet,\n analisisCompletoPorSheet,\n rows_leidas_sheet: inputRows.length,\n rows_misma_sesion: rowsSesion.length,\n rows_completadas: rowsCompletados.length,\n analisis_unicos: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado\n }))\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2048,
2416
],
"id": "cf0e29f2-c2d4-4538-b4cd-2543d8361cf6",
"name": "Code - Decidir cierre análisis por Sheet Redis TEST"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "5a5b3564-6292-486b-8154-5df844083a7d",
"leftValue": "={{ $json.analisis_debe_intentar_cierre }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2256,
2416
],
"id": "4dad5977-e20f-48d6-b7f6-0ecdcb84ad88",
"name": "IF - Cierre análisis por Sheet Redis TEST"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const redisActual = $input.item.json || {};\n\nlet contexto = {};\ntry {\n contexto = $('IF - Cierre análisis por Sheet Redis TEST').item.json || {};\n} catch (error) {\n contexto = {};\n}\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n redisActual.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n redisActual.ejecucion_id ||\n sessionId\n);\n\nconst retryKey = limpiar(\n contexto.redis_retry_key ||\n redisActual.redis_retry_key ||\n `fulgencio:analisis-retry:${sessionId}`\n);\n\nconst extraerNumeroRedis = (obj) => {\n if (!obj || typeof obj !== 'object') return 0;\n\n if (retryKey && obj[retryKey] !== undefined) {\n return toNumber(obj[retryKey], 0);\n }\n\n const valoresNumericos = Object.values(obj)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n return valoresNumericos.length ? valoresNumericos[0] : 0;\n};\n\nconst retryCount = extraerNumeroRedis(redisActual);\n\nconst maxReintentos = 12;\nconst reintentar = retryCount <= maxReintentos;\n\nreturn {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_retry_key: retryKey,\n cierre_retry_count: retryCount,\n cierre_retry_max: maxReintentos,\n cierre_retry_permitido: reintentar,\n\n decision_reintento_cierre: reintentar\n ? 'REINTENTAR_LECTURA_ANALISIS'\n : 'REINTENTOS_AGOTADOS',\n\n cierre_retry_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n retry_key: retryKey,\n redis_respuesta: redisActual,\n retry_count: retryCount,\n max_reintentos: maxReintentos,\n reintentar\n }\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2784,
2496
],
"id": "30c22af2-a6a4-4bd9-89fb-c5c3e8beb74e",
"name": "Code - Validar reintento cierre análisis TEST"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "ad4f88ac-eda8-4cb1-bd08-34aca455e07c",
"leftValue": "={{ $json.cierre_retry_permitido }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2992,
2496
],
"id": "bd7ab438-286c-45e2-aff2-4ab7602bf6da",
"name": "IF - Reintentar cierre análisis Redis TEST"
},
{
"parameters": {
"amount": 4
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
3360,
2480
],
"id": "7c544ae1-5de8-4dcd-9591-35a9fc39420f",
"name": "Wait - Reintento cierre análisis Redis TEST",
"webhookId": "b54b49e7-ede0-48e7-8973-0d45d8482cc8"
},
{
"parameters": {
"operation": "incr",
"key": "={{ 'fulgencio:analisis-retry:' + $json.session_id }}",
"expire": true,
"ttl": 900
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
2576,
2496
],
"id": "391a22f9-f721-4404-a042-086bcdd18062",
"name": "Redis - Contar reintento cierre análisis TEST",
"credentials": {
"redis": {
"id": "Fu2IevwVPaoyw9OS",
"name": "Redis Fulgencio"
}
}
},
{
"parameters": {
"url": "={{ \n 'https://slides.googleapis.com/v1/presentations/' + \n (\n $json.presentation_id ||\n $json.presentacion_id ||\n $json.slides_presentation_id ||\n $json.presentacion_ejecucion_id ||\n String($json.presentacion_ejecucion_url || $json.presentation_url || '').match(/\\/presentation\\/d\\/([^/]+)/)?.[1]\n )\n}}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
10480,
2128
],
"id": "4df34af3-8e2a-4b7a-841c-675397a836cc",
"name": "HTTP - Obtener presentación Slides limpiar TEST",
"credentials": {
"googleOAuth2Api": {
"id": "eHseMeH39kRcXgOF",
"name": "Google account 2"
}
}
},
{
"parameters": {
"jsCode": "const inputItems = $input.all();\nconst presentation = inputItems[0]?.json || {};\n\n// Contexto anterior: aquí está el link, presentation_id, conteos, etc.\nconst contexto = $('Code - Normalizar respuesta Slides ejecución TEST').first().json || {};\n\nconst limpiar = (v) => String(v ?? '').trim();\n\nconst extraerIdPresentacion = (...valores) => {\n for (const valor of valores) {\n const txt = limpiar(valor);\n if (!txt) continue;\n\n const match = txt.match(/\\/presentation\\/d\\/([^/]+)/);\n if (match?.[1]) return match[1];\n\n // Si ya viene como ID limpio\n if (!txt.includes('/') && txt.length > 20) return txt;\n }\n return '';\n};\n\nconst presentationId = extraerIdPresentacion(\n contexto.presentation_id,\n contexto.presentacion_id,\n contexto.slides_presentation_id,\n contexto.presentacion_ejecucion_id,\n contexto.presentacion_ejecucion_url,\n contexto.presentation_url,\n presentation.presentationId\n);\n\nif (!presentationId) {\n throw new Error('No se pudo detectar presentation_id para limpiar slides vacíos.');\n}\n\n// Detectar cantidad de fotos reales.\n// Usamos varios posibles nombres porque el contexto puede variar según el nodo anterior.\nlet fotosCount = Number(\n contexto.fotos_count ??\n contexto.imagenes_count ??\n contexto.images_count ??\n contexto.media_fotos_count ??\n contexto.media_imagenes_count ??\n contexto.analisis_multimedia?.imagenes?.length ??\n contexto.analisis_multimedia?.fotos?.length ??\n 0\n);\n\n// Seguridad: si llega vacío, intenta contar imágenes insertadas desde posibles arrays.\nif (!fotosCount && Array.isArray(contexto.imagenes)) {\n fotosCount = contexto.imagenes.length;\n}\n\nif (!fotosCount && Array.isArray(contexto.fotos)) {\n fotosCount = contexto.fotos.length;\n}\n\n// Máximo de placeholders esperados en plantilla\nconst maxFotosPlantilla = 25;\n\n// Si por alguna razón no tenemos conteo, NO borramos nada.\nif (!fotosCount || fotosCount < 0) {\n fotosCount = 0;\n}\n\nfotosCount = Math.min(Number(fotosCount || 0), maxFotosPlantilla);\n\nconst extraerTextoSlide = (slide) => {\n const partes = [];\n\n const elementos = slide.pageElements || [];\n for (const el of elementos) {\n const textElements = el?.shape?.text?.textElements || [];\n for (const te of textElements) {\n const content = te?.textRun?.content;\n if (content) partes.push(content);\n }\n }\n\n return partes.join(' ').replace(/\\s+/g, ' ').trim();\n};\n\nconst slides = presentation.slides || [];\n\nconst slidesDetectados = [];\nconst slidesABorrar = [];\n\nfor (const slide of slides) {\n const objectId = slide.objectId;\n const textoSlide = extraerTextoSlide(slide);\n\n // Busca FOTO 1, FOTO 2, etc. Tolerante a espacios.\n const match = textoSlide.match(/\\bFOTO\\s*(\\d{1,2})\\b/i);\n\n if (!match) continue;\n\n const numeroFoto = Number(match[1]);\n if (numeroFoto < 1 || numeroFoto > maxFotosPlantilla) continue;\n\n slidesDetectados.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n\n if (numeroFoto > fotosCount) {\n slidesABorrar.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n }\n}\n\nconst deleteRequests = slidesABorrar.map((s) => ({\n deleteObject: {\n objectId: s.objectId,\n },\n}));\n\nreturn [\n {\n json: {\n ...contexto,\n\n presentation_id: presentationId,\n slides_cleanup_fotos_count: fotosCount,\n slides_cleanup_total_slides: slides.length,\n\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n slides_fotos_vacias_count: slidesABorrar.length,\n\n delete_slide_requests: deleteRequests,\n\n slides_cleanup_debug: {\n presentation_id: presentationId,\n fotos_count_detectado: fotosCount,\n total_slides_presentacion: slides.length,\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n delete_requests_count: deleteRequests.length,\n },\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
10688,
2128
],
"id": "278be3f3-bd01-4aac-b145-8b7a2a24c3f6",
"name": "Code - Preparar borrado slides fotos vacías TEST"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "dca8dbb7-b14a-4def-9e43-27a3c1360d78",
"leftValue": "={{ Number($json.slides_fotos_vacias_count || 0) > 0 }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
10896,
2128
],
"id": "3fac8441-4cc4-4332-9a34-94c55a8d624a",
"name": "IF - Hay slides fotos vacías para borrar TEST"
},
{
"parameters": {
"method": "POST",
"url": "={{ 'https://slides.googleapis.com/v1/presentations/' + $json.presentation_id + ':batchUpdate' }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ \n {\n requests: $json.delete_slide_requests || []\n }\n}}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
11184,
1936
],
"id": "72d010e7-5aa8-4650-bdb4-36c3546c03c0",
"name": "HTTP - Borrar slides fotos vacías TEST",
"credentials": {
"googleOAuth2Api": {
"id": "eHseMeH39kRcXgOF",
"name": "Google account 2"
}
}
},
{
"parameters": {
"jsCode": "const contexto = $('Code - Preparar borrado slides fotos vacías TEST').first().json || {};\nconst respuestaBorrado = $input.first()?.json || {};\n\nreturn [\n {\n json: {\n ...contexto,\n slides_cleanup_done: true,\n slides_delete_response: respuestaBorrado,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
11392,
1936
],
"id": "e845eb74-403a-4762-b81c-c0ff06db6e59",
"name": "Code - Restaurar contexto slides limpios TEST"
},
{
"parameters": {
"content": "## 📥 ENTRADA WHATSAPP Y GRUPO OFICIAL\n\nEste bloque recibe eventos desde Evolution API y normaliza el mensaje recibido.\n\nResponsabilidades:\n- Detectar si viene de grupo o chat individual.\n- Identificar sender real en grupo.\n- Resolver whatsapp_to para responder al grupo correcto.\n- Detectar texto, audio, imagen, video o documento.\n- Normalizar comandos oficiales.\n- Validar que el mensaje venga de un grupo permitido.\n\nRegla principal:\nsolo se procesa si el grupo está autorizado.",
"height": 528,
"width": 848
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-9568,
-1344
],
"id": "d86e235c-45ef-48c5-a816-6c32f33fd9ae",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## 🧾 CONTROL DE SESIONES\n\nEste bloque controla si el usuario puede iniciar, continuar o cancelar un reporte.\n\nDecisiones:\n- CREAR_SESION: inicia nuevo reporte con Hey.\n- AVISO_SESION_ACTIVA: ya existe una sesión abierta.\n- CONTINUAR_SESION: sigue el reporte actual.\n- CANCELAR_SESION: cancela sesión activa.\n- SIN_SESION_ACTIVA: no hay reporte abierto.\n- SIN_SESION_PARA_CANCELAR: intento de cancelar sin sesión.\n\nRegla:\nuna propuesta/reporte activo por usuario dentro del grupo.",
"height": 624,
"width": 1024,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-8448,
-1440
],
"id": "4b1e674e-b24f-43b9-894c-14f6c018b516",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## 👋 CREACIÓN Y MENSAJES DE CONTROL\n\nEste bloque crea sesiones nuevas y responde a casos operativos básicos.\n\nIncluye:\n- Crear sesión en Google Sheets.\n- Enviar bienvenida y pedir nota de voz.\n- Avisar si ya existe una sesión activa.\n- Avisar si no hay sesión activa.\n- Cancelar sesión cuando el usuario escribe CANCELAR.\n\nLa bienvenida inicia el flujo guiado de 3 pasos:\n1. Audio obligatorio\n2. Imágenes obligatorias\n3. Videos opcionales",
"height": 1904,
"width": 1200,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-5392,
-4928
],
"id": "17ae92c2-aaff-4121-9646-aef8939cfb48",
"name": "Sticky Note2"
},
{
"parameters": {
"content": "## 🧭 ROUTER DEL REPORTE ACTIVO\n\nEste bloque decide qué hacer según la etapa actual de la sesión.\n\nEtapas:\n- ESPERANDO_AUDIO\n- ESPERANDO_IMAGENES\n- ESPERANDO_VIDEOS\n- PROCESANDO\n\nDecisiones principales:\n- Guardar audio\n- Pedir audio\n- Guardar imágenes\n- Validar FOTOS LISTAS\n- Guardar videos\n- Cerrar sin video\n- Cerrar con videos\n- Avisar procesamiento\n- Ignorar eventos no válidos\n\nRegla:\ncada mensaje se procesa según la etapa activa, no de forma aislada.",
"height": 912,
"width": 1024,
"color": 2
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-7968,
-704
],
"id": "09ccf504-4fb9-4a72-94fd-de1af944cbc0",
"name": "Sticky Note3"
},
{
"parameters": {
"content": "## 🎙️ PASO 1 — AUDIO OBLIGATORIO\n\nEl reporte no avanza sin nota de voz.\n\nCuando llega un audio:\n- Se guarda el evento en wa_ejecuciones_eventos.\n- Se actualiza la sesión.\n- Se incrementa audio_count.\n- La etapa cambia a ESPERANDO_IMAGENES.\n- El bot pide las imágenes obligatorias.\n\nLa nota de voz debe explicar:\n- Propuesta o referencia\n- Marca/cliente\n- País\n- Ubicación\n- Qué se ejecutó o reportó\n- Comentarios o resultados",
"height": 640,
"width": 1408,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-5616,
-2832
],
"id": "cf2e46cc-9bb7-469c-9335-a578a24fc23e",
"name": "Sticky Note4"
},
{
"parameters": {
"content": "## 🖼️ PASO 2 — IMÁGENES OBLIGATORIAS\n\nEste bloque recibe imágenes y soporta varias imágenes en un mismo mensaje.\n\nUso de Redis:\n- Guarda temporalmente imágenes recibidas.\n- Espera un pequeño cierre de lote.\n- Expande el lote en eventos individuales.\n- Evita perder imágenes cuando WhatsApp las manda juntas.\n\nCuando el usuario termina:\ndebe escribir FOTOS LISTAS.\n\nRegla:\nel reporte no puede avanzar a videos si no hay imágenes guardadas.",
"height": 416,
"width": 3744,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-5232,
-1808
],
"id": "89dc9ab6-f069-4c71-a4fa-db52c0dab336",
"name": "Sticky Note5"
},
{
"parameters": {
"content": "## 🎥 PASO 3 — VIDEOS OPCIONALES\n\nLos videos son opcionales.\n\nOpciones:\n- Si no hay videos: usuario escribe SIN VIDEO.\n- Si envió videos: usuario escribe LISTO.\n- Los videos deben enviarse uno por uno, en mensajes separados.\n\nAl cerrar:\n- La sesión pasa a PROCESANDO.\n- Se envía aviso de reporte recibido.\n- El flujo inicia recuperación, análisis y generación de presentación.\n\nRegla:\nlas imágenes sí pueden ir juntas; los videos deben ir separados.",
"height": 768,
"width": 1632,
"color": "#1D6362"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-4672,
-656
],
"id": "a74fc961-8836-4f46-ad9e-e943ca01f3a6",
"name": "Sticky Note6"
},
{
"parameters": {
"content": "## 📂 RECUPERACIÓN Y ORGANIZACIÓN DE EVIDENCIAS\n\nEste bloque recupera todos los archivos de la sesión.\n\nProceso:\n1. Lee eventos multimedia de la sesión.\n2. Recupera media desde Evolution API.\n3. Convierte base64 a binario.\n4. Sube audio, imágenes y videos a Google Drive.\n5. Guarda metadata en wa_ejecuciones_media.\n6. Consolida la media recuperada.\n\nCada archivo queda asociado a:\n- session_id\n- ejecucion_id\n- media_event_id\n- tipo de media\n- link de Drive\n- nombre del archivo",
"height": 1728,
"width": 4400,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-6688,
272
],
"id": "3908b74a-74bb-4333-9e16-7e90c5c74b3c",
"name": "Sticky Note7"
},
{
"parameters": {
"content": "## 🤖 ANÁLISIS IA Y CONTROL REDIS\n\nGemini analiza cada evidencia multimedia:\n- Audio\n- Imágenes\n- Videos opcionales\n\nCada análisis se guarda en:\nwa_ejecuciones_analisis_media\n\nRedis controla el cierre:\n- Cuenta análisis completados.\n- Verifica si ya están todos listos.\n- Usa lock para evitar cierre duplicado.\n- Si faltan análisis, espera y reintenta.\n\nRegla:\nel JSON final solo se genera cuando el análisis multimedia está completo.",
"height": 1504,
"width": 5712,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1520,
1440
],
"id": "4400f1ea-39b9-4302-86b4-a0c463f8e76c",
"name": "Sticky Note8"
},
{
"parameters": {
"content": "## ✅ CIERRE FINAL DEL REPORTE\n\nEste bloque genera el resultado final del reporte.\n\nProceso:\n1. Consolida análisis multimedia.\n2. Gemini genera JSON final estructurado.\n3. Se normaliza la respuesta.\n4. Se hace match contra el Banco de Propuestas.\n5. Se verifica duplicado.\n6. Se guarda en propuestas_ejecutadas.\n7. Se crea carpeta de ejecución en Drive.\n8. Se copia la plantilla de Slides.\n9. Se insertan imágenes y se borran slides vacíos.\n10. Se actualiza el link final.\n11. Si el match es alto, se actualiza el banco original.\n12. Se envía el link final por WhatsApp.\n\nRegla:\npropuestas externas o match bajo no actualizan el banco original.",
"height": 1024,
"width": 10912,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
4704,
1488
],
"id": "a3245658-be74-4ea6-8a58-a104803896ac",
"name": "Sticky Note9"
},
{
"parameters": {
"content": "## Fotos Listas\n\nActualizar a Fotos listas y preparar solicitud de videos",
"height": 288,
"width": 944,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-3792,
-1264
],
"id": "0ce97fde-e030-4175-baa9-b804cdc18803",
"name": "Sticky Note10"
}
],
"pinData": {},
"connections": {
"Code - Normalizar evento WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Leer grupos permitidos WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Evolution WhatsApp TEST": {
"main": [
[
{
"node": "Code - Normalizar evento WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"WhatsApp - Enviar mensaje TEST": {
"main": [
[]
]
},
"Sheets - Leer sesiones existentes": {
"main": [
[
{
"node": "Code - Resolver sesión WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Resolver sesión WhatsApp TEST": {
"main": [
[
{
"node": "Switch - Decisión sesión WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Switch - Decisión sesión WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Crear sesión WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar aviso sesión activa WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar sin sesión para cancelar WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar cancelación sesión WhatsApp TEST",
"type": "main",
"index": 0
}
],
[],
[
{
"node": "Code - Resolver paso activo WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar bienvenida WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Crear sesión WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar bienvenida WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Resolver paso activo WhatsApp TEST": {
"main": [
[
{
"node": "Switch - Paso activo WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Switch - Paso activo WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Guardar evento audio WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar solicitud audio WhatsApp TEST",
"type": "main",
"index": 0
}
],
[],
[],
[
{
"node": "Code - Preparar buffer imagen Redis TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Sheets - Leer eventos sesión fotos WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar aviso falta imagen WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Sheets - Guardar evento video WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "CERRAR_SIN_VIDEO",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar cierre con videos WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar recordatorio videos WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar aviso procesando WhatsApp TEST",
"type": "main",
"index": 0
}
],
[],
[]
]
},
"Code - Preparar solicitud audio WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Guardar evento audio WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar actualización audio recibido WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar actualización audio recibido WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Actualizar sesión audio recibido WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar sesión audio recibido WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar solicitud imágenes WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar aviso sesión activa WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar solicitud imágenes WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar cancelación sesión WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Cancelar sesión WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Cancelar sesión WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar mensaje cancelación WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar mensaje cancelación WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar aviso falta imagen WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Preparar actualización fotos listas": {
"main": [
[
{
"node": "Sheets - Actualizar sesión fotos listas WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar sesión fotos listas WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar solicitud videos WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar solicitud videos WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar recordatorio videos WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"CERRAR_SIN_VIDEO": {
"main": [
[
{
"node": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar sesión cierre sin video WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar aviso procesando WhatsApp TEST1",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Guardar evento video WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar actualización video recibido WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar actualización video recibido WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Actualizar sesión video recibido WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar sesión video recibido WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar confirmación video WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar confirmación video WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar cierre con videos WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar sesión cierre con videos WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar aviso procesando WhatsApp TEST1",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar aviso procesando WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar búsqueda eventos media WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Leer eventos WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Leer eventos WhatsApp TEST": {
"main": [
[
{
"node": "Code - Filtrar eventos media de sesión WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Filtrar eventos media de sesión WhatsApp TEST": {
"main": [
[
{
"node": "Code - Separar media en items WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Separar media en items WhatsApp TEST": {
"main": [
[
{
"node": "HTTP Request - Obtener media base64 Evolution TEST",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request - Obtener media base64 Evolution TEST": {
"main": [
[
{
"node": "Code - Convertir base64 a binario WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Convertir base64 a binario WhatsApp TEST": {
"main": [
[
{
"node": "Drive - Subir media WhatsApp TEST",
"type": "main",
"index": 0
},
{
"node": "Switch - Tipo media para Gemini WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Drive - Subir media WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar registro media Drive WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar registro media Drive WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Guardar media Drive WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Guardar media Drive WhatsApp TEST": {
"main": [
[
{
"node": "Code - Consolidar media recuperada WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Consolidar media recuperada WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Actualizar sesión media recuperada WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar sesión media recuperada WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar paquete análisis Gemini WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Switch - Tipo media para Gemini WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar Gemini audio WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar Gemini imagen WhatsApp TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Code - Preparar Gemini video WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar Gemini audio WhatsApp TEST": {
"main": [
[
{
"node": "Gemini - Analizar audio WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Gemini - Analizar audio WhatsApp TEST": {
"main": [
[
{
"node": "Code - Normalizar análisis audio WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar Gemini imagen WhatsApp TEST": {
"main": [
[
{
"node": "Gemini - Analizar imagen WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Gemini - Analizar imagen WhatsApp TEST": {
"main": [
[
{
"node": "Code - Normalizar análisis imagen WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar Gemini video WhatsApp TEST": {
"main": [
[
{
"node": "Analyze video",
"type": "main",
"index": 0
}
]
]
},
"Analyze video": {
"main": [
[
{
"node": "Code - Normalizar análisis video WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Normalizar análisis video WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Guardar análisis media Gemini WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Normalizar análisis imagen WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Guardar análisis media Gemini WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Normalizar análisis audio WhatsApp TEST": {
"main": [
[
{
"node": "Sheets - Guardar análisis media Gemini WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Guardar análisis media Gemini WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar contador análisis Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Leer análisis media Gemini WhatsApp TEST": {
"main": [
[
{
"node": "Code - Consolidar análisis media Gemini WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Consolidar análisis media Gemini WhatsApp TEST": {
"main": [
[
{
"node": "Gemini - Generar JSON final propuesta ejecutada TEST",
"type": "main",
"index": 0
}
]
]
},
"Gemini - Generar JSON final propuesta ejecutada TEST": {
"main": [
[
{
"node": "Code - Normalizar JSON final propuesta ejecutada TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Normalizar JSON final propuesta ejecutada TEST": {
"main": [
[
{
"node": "Sheets - Leer banco propuestas Fulgencio TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Guardar propuesta ejecutada final TEST": {
"main": [
[
{
"node": "Code - Preparar copia presentación ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar sesión análisis completado WhatsApp TEST": {
"main": [
[
{
"node": "Code - Preparar mensaje final WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar mensaje final WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "Sheets - Leer análisis media Gemini WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Leer propuestas ejecutadas final TEST": {
"main": [
[
{
"node": "Code - Verificar duplicado propuesta ejecutada TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Verificar duplicado propuesta ejecutada TEST": {
"main": [
[
{
"node": "IF - Propuesta final ya existe TEST",
"type": "main",
"index": 0
}
]
]
},
"IF - Propuesta final ya existe TEST": {
"main": [
[],
[
{
"node": "Sheets - Guardar propuesta ejecutada final TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar copia presentación ejecución TEST": {
"main": [
[
{
"node": "Code - Preparar carpeta ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"Drive - Copiar plantilla presentación ejecución TEST": {
"main": [
[
{
"node": "Code - Normalizar link presentación ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Normalizar link presentación ejecución TEST": {
"main": [
[
{
"node": "Code - Preparar reemplazos Slides ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar link presentación ejecución TEST": {
"main": [
[
{
"node": "Code - Preparar actualización banco propuesta ejecutada TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar reemplazos Slides ejecución TEST": {
"main": [
[
{
"node": "HTTP Request - Reemplazar textos Slides ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request - Reemplazar textos Slides ejecución TEST": {
"main": [
[
{
"node": "Code - Preparar payload insertar imágenes Slides TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Normalizar respuesta Slides ejecución TEST": {
"main": [
[
{
"node": "HTTP - Obtener presentación Slides limpiar TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar sin sesión para cancelar WhatsApp TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Leer eventos sesión fotos WhatsApp TEST": {
"main": [
[
{
"node": "Preparar actualización fotos listas",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar carpeta ejecución TEST": {
"main": [
[
{
"node": "Drive - Crear carpeta ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"Drive - Crear carpeta ejecución TEST": {
"main": [
[
{
"node": "Code - Normalizar carpeta ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Normalizar carpeta ejecución TEST": {
"main": [
[
{
"node": "Code - Preparar media para mover carpeta TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar media para mover carpeta TEST": {
"main": [
[
{
"node": "Drive - Mover media a carpeta ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"Drive - Mover media a carpeta ejecución TEST": {
"main": [
[
{
"node": "Code - Confirmar media movida carpeta TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Confirmar media movida carpeta TEST": {
"main": [
[
{
"node": "Drive - Copiar plantilla presentación ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar payload insertar imágenes Slides TEST": {
"main": [
[
{
"node": "HTTP - Insertar imágenes en Slides TEST",
"type": "main",
"index": 0
}
]
]
},
"HTTP - Insertar imágenes en Slides TEST": {
"main": [
[
{
"node": "Code - Normalizar respuesta Slides ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar aviso procesando WhatsApp TEST1": {
"main": [
[
{
"node": "HTTP - Enviar aviso procesando WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Restaurar contexto aviso procesando TEST": {
"main": [
[
{
"node": "Code - Preparar búsqueda eventos media WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"HTTP - Enviar aviso procesando WhatsApp TEST": {
"main": [
[
{
"node": "Code - Restaurar contexto aviso procesando TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Leer banco propuestas Fulgencio TEST": {
"main": [
[
{
"node": "Code - Match propuesta banco Fulgencio TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Match propuesta banco Fulgencio TEST": {
"main": [
[
{
"node": "Code - Enriquecer datos con match banco TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar actualización banco propuesta ejecutada TEST": {
"main": [
[
{
"node": "Sheets - Actualizar decisión automática banco TEST",
"type": "main",
"index": 0
}
]
]
},
"IF - Actualizar banco match alta confianza TEST": {
"main": [
[
{
"node": "Sheets - Actualizar link ejecutada en banco TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar link ejecutada en banco TEST": {
"main": [
[
{
"node": "Code - Restaurar contexto actualización banco TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Restaurar contexto actualización banco TEST": {
"main": [
[
{
"node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Enriquecer datos con match banco TEST": {
"main": [
[
{
"node": "Sheets - Leer propuestas ejecutadas final TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Filtro origen WhatsApp TEST": {
"main": [
[
{
"node": "IF - Procesar solo grupo permitido TEST",
"type": "main",
"index": 0
}
]
]
},
"IF - Procesar solo grupo permitido TEST": {
"main": [
[
{
"node": "Sheets - Leer sesiones existentes",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Leer grupos permitidos WhatsApp TEST": {
"main": [
[
{
"node": "Code - Filtro origen WhatsApp TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar decisión automática banco TEST": {
"main": [
[
{
"node": "Code - Restaurar contexto decisión automática banco TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Restaurar contexto decisión automática banco TEST": {
"main": [
[
{
"node": "IF - Actualizar banco match alta confianza TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar buffer imagen Redis TEST": {
"main": [
[
{
"node": "Redis - Push buffer imagen TEST",
"type": "main",
"index": 0
}
]
]
},
"Redis - Push buffer imagen TEST": {
"main": [
[
{
"node": "Code - Restaurar contexto buffer imagen TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Restaurar contexto buffer imagen TEST": {
"main": [
[
{
"node": "Wait - Esperar cierre buffer imagen TEST",
"type": "main",
"index": 0
}
]
]
},
"Wait - Esperar cierre buffer imagen TEST": {
"main": [
[
{
"node": "Redis - Leer buffer imagen TEST",
"type": "main",
"index": 0
}
]
]
},
"Redis - Leer buffer imagen TEST": {
"main": [
[
{
"node": "Code - Decidir procesar lote imagen Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Decidir procesar lote imagen Redis TEST": {
"main": [
[
{
"node": "IF - Procesar lote imagen Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"IF - Procesar lote imagen Redis TEST": {
"main": [
[
{
"node": "Redis - Borrar buffer imagen TEST",
"type": "main",
"index": 0
}
]
]
},
"Redis - Borrar buffer imagen TEST": {
"main": [
[
{
"node": "Code - Expandir lote imagen Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Expandir lote imagen Redis TEST": {
"main": [
[
{
"node": "Sheets - Guardar eventos imagen lote Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Guardar eventos imagen lote Redis TEST": {
"main": [
[
{
"node": "Code - Consolidar lote imagen guardado Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Consolidar lote imagen guardado Redis TEST": {
"main": [
[
{
"node": "Sheets - Actualizar sesión imágenes lote Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Actualizar sesión imágenes lote Redis TEST": {
"main": [
[
{
"node": "Code - Preparar confirmación imágenes lote Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar confirmación imágenes lote Redis TEST": {
"main": [
[
{
"node": "WhatsApp - Enviar mensaje TEST",
"type": "main",
"index": 0
}
]
]
},
"IF - Análisis completo Redis TEST": {
"main": [
[
{
"node": "Redis - Lock análisis final TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait - Verificar cierre análisis Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Redis - Lock análisis final TEST": {
"main": [
[
{
"node": "Code - Validar lock análisis final TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Validar lock análisis final TEST": {
"main": [
[
{
"node": "IF - Lock análisis final adquirido TEST",
"type": "main",
"index": 0
}
]
]
},
"IF - Lock análisis final adquirido TEST": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar contador análisis Redis TEST": {
"main": [
[
{
"node": "Redis - Contar análisis media TEST",
"type": "main",
"index": 0
}
]
]
},
"Redis - Contar análisis media TEST": {
"main": [
[
{
"node": "Code - Validar conteo análisis Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Validar conteo análisis Redis TEST": {
"main": [
[
{
"node": "IF - Análisis completo Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Wait - Verificar cierre análisis Redis TEST": {
"main": [
[
{
"node": "Sheets - Leer análisis media cierre Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Leer análisis media cierre Redis TEST": {
"main": [
[
{
"node": "Code - Decidir cierre análisis por Sheet Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Decidir cierre análisis por Sheet Redis TEST": {
"main": [
[
{
"node": "IF - Cierre análisis por Sheet Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"IF - Cierre análisis por Sheet Redis TEST": {
"main": [
[
{
"node": "Redis - Lock análisis final TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Redis - Contar reintento cierre análisis TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Validar reintento cierre análisis TEST": {
"main": [
[
{
"node": "IF - Reintentar cierre análisis Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"IF - Reintentar cierre análisis Redis TEST": {
"main": [
[
{
"node": "Wait - Reintento cierre análisis Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Wait - Reintento cierre análisis Redis TEST": {
"main": [
[
{
"node": "Sheets - Leer análisis media cierre Redis TEST",
"type": "main",
"index": 0
}
]
]
},
"Redis - Contar reintento cierre análisis TEST": {
"main": [
[
{
"node": "Code - Validar reintento cierre análisis TEST",
"type": "main",
"index": 0
}
]
]
},
"HTTP - Obtener presentación Slides limpiar TEST": {
"main": [
[
{
"node": "Code - Preparar borrado slides fotos vacías TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Preparar borrado slides fotos vacías TEST": {
"main": [
[
{
"node": "IF - Hay slides fotos vacías para borrar TEST",
"type": "main",
"index": 0
}
]
]
},
"IF - Hay slides fotos vacías para borrar TEST": {
"main": [
[
{
"node": "HTTP - Borrar slides fotos vacías TEST",
"type": "main",
"index": 0
}
],
[
{
"node": "Sheets - Actualizar link presentación ejecución TEST",
"type": "main",
"index": 0
}
]
]
},
"HTTP - Borrar slides fotos vacías TEST": {
"main": [
[
{
"node": "Code - Restaurar contexto slides limpios TEST",
"type": "main",
"index": 0
}
]
]
},
"Code - Restaurar contexto slides limpios TEST": {
"main": [
[
{
"node": "Sheets - Actualizar link presentación ejecución TEST",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": true,
"timeSavedMode": "fixed",
"callerPolicy": "workflowsFromSameOwner",
"timezone": "America/Santo_Domingo"
},
"versionId": "14dfae53-7771-4e60-945c-f5707b162c00",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
},
"id": "CdosDIWBgnDMnPdr",
"tags": []
}