From 72f2d567dee9015740649167f472662fc9f58c70 Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Sun, 21 Jun 2026 04:00:27 +0000 Subject: [PATCH] Update backup: Chat de WhatsApp - Propuestas Ejecutadas --- chat-de-whatsapp-propuestas-ejecutadas.json | 6928 +++++++++++++++++-- 1 file changed, 6417 insertions(+), 511 deletions(-) diff --git a/chat-de-whatsapp-propuestas-ejecutadas.json b/chat-de-whatsapp-propuestas-ejecutadas.json index 0d660e0..33b4b18 100644 --- a/chat-de-whatsapp-propuestas-ejecutadas.json +++ b/chat-de-whatsapp-propuestas-ejecutadas.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-06-15T13:19:17.505Z", + "updatedAt": "2026-06-20T16:19:11.789Z", "createdAt": "2026-06-08T20:09:28.132Z", "id": "sXH75CXIQ0sEzm1Y", "name": "Chat de WhatsApp - Propuestas Ejecutadas", @@ -25,7 +25,7 @@ "parameters": [ { "name": "number", - "value": "={{ $json.whatsapp_to || $json.whatsapp_remote_jid || $json.manager_telefono }}" + "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", @@ -42,21 +42,21 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 7744, - -112 + 9984, + 704 ], "id": "b1d4c2c3-c77f-4b9d-b4ca-c40535c26299", "name": "WhatsApp - Enviar mensaje TEST" }, { "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(/\\D/g, '');\n\nconst key = data.key || {};\n\nconst remoteJid = texto(key.remoteJid || data.remoteJid || body.remoteJid || '');\nconst managerTelefono = limpiarNumero(remoteJid);\n\nconst pushName =\n texto(data.pushName) ||\n texto(body.pushName) ||\n 'Usuario WhatsApp';\n\nconst message = data.message || {};\n\nconst textoMensaje =\n texto(message.conversation) ||\n texto(message.extendedTextMessage?.text) ||\n texto(data.messageText) ||\n texto(data.text) ||\n texto(body.text) ||\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\nconst normalizarComando = (valor) => {\n const t = texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (t === '/START' || t === 'START' || t === 'INICIAR') return 'START';\n if (t === 'CANCELAR') return 'CANCELAR';\n if (t === 'FOTOS LISTAS' || t === 'FOTOS_LISTAS') return 'FOTOS_LISTAS';\n if (t === 'SIN VIDEO' || t === 'SIN_VIDEO') return 'SIN_VIDEO';\n if (t === 'LISTO') return 'LISTO';\n\n return 'NORMAL';\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 manager_telefono: managerTelefono,\n manager_nombre: pushName,\n usuario_id_origen: managerTelefono,\n\n whatsapp_remote_jid: remoteJid,\n whatsapp_to: remoteJid,\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 raw_preview: JSON.stringify(body).slice(0, 4000),\n raw_event: body,\n },\n },\n];" + "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\nconst normalizarComando = (valor) => {\n const t = normalizarTexto(valor);\n\n // Nuevo inicio oficial\n if (t === 'HEY') return 'START';\n\n // Compatibilidad temporal\n if (\n t === '/START' ||\n t === 'START' ||\n t === 'INICIAR'\n) {\n return 'START';\n}\n\n if (t === 'CANCELAR' || t === 'CANCEL') {\n return 'CANCELAR';\n }\n\n if (\n t === 'FOTOS LISTAS' ||\n t === 'FOTOS_LISTAS' ||\n t === 'IMAGENES LISTAS' ||\n t === 'IMAGENES_LISTAS' ||\n t === 'PHOTOS READY' ||\n t === 'PHOTOS_READY'\n ) {\n return 'FOTOS_LISTAS';\n }\n\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 if (\n t === 'LISTO' ||\n t === 'DONE' ||\n t === 'FINISH' ||\n t === 'FINALIZAR'\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": [ - 208, - -256 + -9200, + -608 ], "id": "35e401ba-acce-429c-b811-58a45c032869", "name": "Code - Normalizar evento WhatsApp TEST" @@ -73,7 +73,7 @@ "conditions": [ { "id": "f2be1fb5-b047-4614-a1f2-9a4d65ef5cc7", - "leftValue": "={{ $json.manager_telefono === '18298946645' }}", + "leftValue": "={{ $json.manager_telefono === '18298946645' || $json.sender_phone === '18298946645' }}", "rightValue": "={{ $json.manager_telefono === '18298946645' }}", "operator": { "type": "boolean", @@ -89,8 +89,8 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 480, - -256 + -8928, + -608 ], "id": "3b65e795-a7f8-415c-8c04-7cde544c4350", "name": "Mi numero TESTING" @@ -104,8 +104,8 @@ "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - 0, - -256 + -9408, + -608 ], "id": "bbdd46ca-d2f0-4c42-8e14-d9591168cf4d", "name": "Webhook - Evolution WhatsApp TEST", @@ -132,8 +132,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 848, - -272 + -8560, + -624 ], "id": "653ca576-1661-49aa-80cb-62b7ec46a1c0", "name": "Sheets - Leer sesiones existentes", @@ -147,13 +147,13 @@ }, { "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 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);\nconst accion = texto(evento.accion_flujo || 'NORMAL');\n\nconst sesionActiva = sesiones\n .filter((row) => texto(row.manager_telefono) === managerTelefono)\n .filter((row) => {\n const estado = texto(row.estado).toUpperCase();\n const etapa = texto(row.etapa).toUpperCase();\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\nconst suffix = Math.random().toString(36).slice(2, 6).toUpperCase();\nconst sessionId = `WA_${Date.now()}_${managerTelefono.slice(-4)}_${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 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 manager_telefono: managerTelefono,\n manager_nombre: texto(evento.manager_nombre),\n canal_origen: 'WHATSAPP',\n fecha_inicio: new Date().toISOString(),\n ultima_actividad: new Date().toISOString(),\n etapa: 'ESPERANDO_AUDIO',\n audio_count: 0,\n imagenes_count: 0,\n videos_count: 0,\n estado: 'ACTIVA',\n ejecucion_id: sessionId,\n motivo_revision: '',\n },\n },\n },\n];" + "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 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// --------------------------------------------------\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 = texto(row.estado).toUpperCase();\n const etapa = texto(row.etapa).toUpperCase();\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 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 // Nuevos campos para grupo, idioma y clasificación futura\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: texto(evento.idioma_flujo || 'ES'),\n\n tipo_reporte: ''\n },\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1040, - -272 + -8368, + -624 ], "id": "91675542-137d-4976-9c1c-3ba26bb1ddab", "name": "Code - Resolver sesión WhatsApp TEST" @@ -319,34 +319,34 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ - 1264, - -336 + -8144, + -688 ], "id": "50d7bb39-3a6d-4906-b695-e6b09ba5c545", "name": "Switch - Decisión sesión WhatsApp TEST" }, { "parameters": { - "jsCode": "const datos = $('Code - Resolver sesión WhatsApp TEST').first().json || {};\n\nconst mensaje = [\n '📸 *Propuestas Ejecutadas - GLM*',\n '',\n `¡Hola, ${datos.manager_nombre || 'equipo'}! Bienvenido al registro de propuestas ejecutadas.`,\n '',\n 'Este proceso permite documentar ejecuciones reales para alimentar el banco de evidencias de Fulgencio Fumado.',\n '',\n '*Paso 1 de 3 — Nota de voz obligatoria*',\n '',\n 'Envía una nota de voz indicando:',\n '',\n '• Nombre o referencia de la propuesta ejecutada',\n '• Marca o cliente',\n '• País',\n '• Ubicación de la ejecución',\n '• Fecha de ejecución',\n '• Qué se implementó',\n '• Comentarios o resultados observados',\n '',\n 'Importante: registra una sola propuesta ejecutada a la vez.',\n].join('\\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];" + "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 `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 '*Step 1 of 3 — Voice note*',\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 `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 '*Paso 1 de 3 — Nota de voz*',\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": [ - 3104, - -2304 + -5552, + -2608 ], "id": "304baef0-fda7-43ce-8bfe-24ed684ced3b", "name": "Code - Preparar bienvenida WhatsApp TEST" }, { "parameters": { - "jsCode": "const datos = $json || {};\n\nconst etapa = String(datos.sesion_activa?.etapa || '').trim() || 'pendiente';\n\nconst mensaje = [\n '⚠️ Ya tienes un reporte de propuesta ejecutada en proceso.',\n '',\n `Etapa actual: ${etapa}`,\n '',\n 'Completa ese reporte antes de iniciar uno nuevo.',\n '',\n 'Si deseas cancelarlo, escribe: CANCELAR',\n].join('\\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];" + "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": [ - 3136, - -1856 + -5520, + -2160 ], "id": "bf9701ac-00d2-47c3-a495-ddaad66ed0be", "name": "Code - Preparar aviso sesión activa WhatsApp TEST" @@ -383,7 +383,17 @@ "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 }}" + "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": [ @@ -503,6 +513,106 @@ "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, @@ -513,8 +623,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 2896, - -2304 + -5760, + -2608 ], "id": "cff69027-2e70-4be6-ad78-80ae6680ca75", "name": "Sheets - Crear sesión WhatsApp TEST", @@ -527,13 +637,13 @@ }, { "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 {\n paso_decision = 'PEDIR_AUDIO';\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 } else if (accion === 'FOTOS_LISTAS') {\n // Solo permite avanzar si ya hay al menos una imagen registrada.\n if (imagenesCount > 0) {\n paso_decision = 'FOTOS_LISTAS';\n } else {\n paso_decision = 'PEDIR_IMAGENES';\n }\n } else {\n paso_decision = 'PEDIR_IMAGENES';\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 } else if (accion === 'SIN_VIDEO') {\n paso_decision = 'CERRAR_SIN_VIDEO';\n } else if (accion === 'LISTO') {\n paso_decision = 'CERRAR_CON_VIDEOS';\n } else {\n paso_decision = 'PEDIR_VIDEOS';\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];" + "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": [ - 1552, - 144 + -7856, + -208 ], "id": "28aaa567-1c39-49ac-802d-38e001d71cc2", "name": "Code - Resolver paso activo WhatsApp TEST" @@ -866,6 +976,31 @@ }, "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" } ] }, @@ -874,21 +1009,21 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ - 1904, - 416 + -7664, + 64 ], "id": "b8b395eb-8f3f-4006-8ad8-d426f7e0792a", "name": "Switch - Paso activo WhatsApp TEST" }, { "parameters": { - "jsCode": "const datos = $json || {};\n\nconst mensaje = [\n '🎙️ Aún necesito la nota de voz obligatoria para iniciar el reporte.',\n '',\n 'Por favor envía una nota de voz indicando:',\n '',\n '• Nombre o referencia de la propuesta ejecutada',\n '• Marca o cliente',\n '• País',\n '• Ubicación de la ejecución',\n '• Fecha de ejecución',\n '• Qué se implementó',\n '• Comentarios o resultados observados',\n].join('\\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];" + "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 '🎙️ I still need the required voice note to start the report.',\n '',\n 'Please send a voice note including:',\n '',\n '• Proposal name or reference',\n '• Brand or client',\n '• Country',\n '• Execution/location details',\n '• Date',\n '• What was implemented or reported',\n '• Any relevant comments or observed results',\n ].join('\\n');\n} else {\n mensaje = [\n '🎙️ Aún necesito la nota de voz obligatoria para iniciar el reporte.',\n '',\n 'Por favor envía una nota de voz indicando:',\n '',\n '• Nombre o referencia de la propuesta',\n '• Marca o cliente',\n '• País',\n '• Ubicación o contexto de la ejecución',\n '• Fecha',\n '• Qué se implementó o qué se está reportando',\n '• Comentarios o resultados observados',\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": [ - 2912, - -224 + -5744, + -528 ], "id": "30196ae5-1d82-46e3-9296-32ffa6122ae1", "name": "Code - Preparar solicitud audio WhatsApp TEST" @@ -1129,8 +1264,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 2624, - -608 + -6032, + -912 ], "id": "4b7309e9-5602-4c88-b29f-42e44179f445", "name": "Sheets - Guardar evento audio WhatsApp TEST", @@ -1143,13 +1278,13 @@ }, { "parameters": { - "jsCode": "const datos = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...datos,\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'ACTIVA',\n audio_count: Number(datos.sesion_activa?.audio_count || 0) + 1,\n },\n },\n];" + "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": [ - 2832, - -608 + -5824, + -912 ], "id": "25599b76-fe73-44b6-80f5-1d8ba9616fd1", "name": "Code - Preparar actualización audio recibido WhatsApp TEST" @@ -1178,7 +1313,22 @@ "ultima_actividad": "={{ $json.ultima_actividad }}", "etapa": "={{ $json.etapa }}", "estado": "={{ $json.estado }}", - "audio_count": "={{ $json.audio_count }}" + "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" @@ -1202,7 +1352,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "manager_nombre", @@ -1212,7 +1362,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "canal_origen", @@ -1269,7 +1419,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "videos_count", @@ -1279,7 +1429,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "estado", @@ -1298,7 +1448,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "motivo_revision", @@ -1310,6 +1460,106 @@ "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", @@ -1330,8 +1580,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3040, - -608 + -5616, + -912 ], "id": "35b5002d-7c69-4f31-89de-dc5457961a42", "name": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", @@ -1344,13 +1594,13 @@ }, { "parameters": { - "jsCode": "const datos = $('Code - Preparar actualización audio recibido WhatsApp TEST').first().json || {};\n\nconst mensaje = [\n '✅ Nota de voz recibida.',\n '',\n '*Paso 2 de 3 — Imágenes obligatorias*',\n '',\n 'Ahora envía al menos una foto de la propuesta ejecutada.',\n '',\n 'Puedes enviar varias imágenes seguidas.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS',\n].join('\\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];" + "jsCode": "const datos = $('Code - Preparar actualización audio recibido WhatsApp TEST').first().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 '✅ Voice note received.',\n '',\n '*Step 2 of 3 — Required images*',\n '',\n 'Now send at least one photo related to the proposal or report.',\n '',\n '⚠️ Important: send images one by one, in separate messages.',\n 'Do not send multiple images together in the same WhatsApp album/message.',\n '',\n 'You can send multiple images, but each one must be sent separately.',\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 al menos una foto relacionada a la propuesta o reporte.',\n '',\n '⚠️ Importante: envía las imágenes una por una, en mensajes separados.',\n 'No envíes varias imágenes juntas en el mismo álbum/mensaje de WhatsApp.',\n '',\n 'Puedes enviar varias imágenes, pero cada una debe ir por separado.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS'\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": [ - 3264, - -608 + -5392, + -912 ], "id": "7c1c09aa-4670-47d0-a5a5-3713adf6b6fb", "name": "Code - Preparar solicitud imágenes WhatsApp TEST" @@ -1362,8 +1612,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3008, - -1040 + -5648, + -1344 ], "id": "dd118cad-907f-4bab-89ef-8d75dc2d70cb", "name": "Code - Preparar cancelación sesión WhatsApp TEST" @@ -1544,8 +1794,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3216, - -1040 + -5440, + -1344 ], "id": "ac83d38f-7d91-408a-bb43-f8950ccc40a6", "name": "Sheets - Cancelar sesión WhatsApp TEST", @@ -1558,39 +1808,26 @@ }, { "parameters": { - "jsCode": "const datos = $('Code - Preparar cancelación sesión WhatsApp TEST').first().json || {};\n\nconst mensaje = [\n '✅ Reporte cancelado correctamente.',\n '',\n 'Ya puedes iniciar un nuevo reporte escribiendo:',\n '',\n '/start',\n].join('\\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];" + "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": [ - 3472, - -1104 + -5184, + -1408 ], "id": "119da00c-cea5-4184-90e5-ad4a408271bb", "name": "Code - Preparar mensaje cancelación WhatsApp TEST" }, { "parameters": { - "jsCode": "const datos = $json || {};\n\nconst mensaje = [\n '⚠️ No tienes un reporte activo para cancelar.',\n '',\n 'Para iniciar uno nuevo, escribe:',\n '',\n '/start',\n].join('\\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];" + "jsCode": "const evento = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || base.nueva_sesion || {};\n\nconst ahora = new Date().toISOString();\n\nconst imagenesActuales = Number(sesion.imagenes_count || 0);\n\nreturn [\n {\n json: {\n ...base,\n ...evento,\n\n session_id: base.session_id || sesion.session_id || evento.session_id || '',\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n\n manager_telefono: base.manager_telefono || sesion.manager_telefono || evento.manager_telefono || base.sender_phone || '',\n manager_nombre: base.manager_nombre || sesion.manager_nombre || evento.manager_nombre || base.sender_name || '',\n\n canal_origen: base.canal_origen || sesion.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || base.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'ESPERANDO_IMAGENES',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: imagenesActuales + 1,\n videos_count: Number(sesion.videos_count || 0),\n\n estado: sesion.estado || 'ACTIVA',\n motivo_revision: sesion.motivo_revision || '',\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 }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3216, - -1488 - ], - "id": "be56aeb8-916d-46b2-b2a2-b544a4bdbd01", - "name": "ode - Preparar sin sesión para cancelar WhatsApp TEST" - }, - { - "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 imagenesActuales = Number(sesion.imagenes_count || 0);\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 // IMPORTANTE:\n // Después de recibir una imagen, todavía seguimos esperando más imágenes\n // hasta que el usuario escriba FOTOS LISTAS.\n etapa: 'ESPERANDO_IMAGENES',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: imagenesActuales + 1,\n videos_count: Number(sesion.videos_count || 0),\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: base.whatsapp_to || base.whatsapp_remote_jid || evento.whatsapp_to || evento.whatsapp_remote_jid || ''\n }\n }\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3904, - -16 + -4752, + -320 ], "id": "dee83381-1061-405c-978f-33403362f74b", "name": "Code — Preparar actualización imagen recibida" @@ -1616,14 +1853,25 @@ "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 }}" + "ultima_actividad": "={{ $json.ultima_actividad }}", + "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 }}" }, "matchingColumns": [ "session_id" @@ -1647,7 +1895,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "manager_nombre", @@ -1657,7 +1905,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "canal_origen", @@ -1757,6 +2005,106 @@ "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 }, { @@ -1779,8 +2127,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 4112, - -16 + -4544, + -320 ], "id": "392c7a6c-3ab8-4f56-8742-8ab4ffdd5a4c", "name": "Sheets — Actualizar sesión imagen recibida", @@ -1793,13 +2141,13 @@ }, { "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\n\nconst nombre = String(data.manager_nombre || base.manager_nombre || '').trim();\nconst saludo = nombre ? `, ${nombre}` : '';\n\nconst telefono = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\n data.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefono.includes('@s.whatsapp.net')\n ? telefono\n : `${telefono.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n ...data,\n\n whatsapp_to: whatsappTo,\n\n whatsapp_text: `✅ Imagen recibida${saludo}.\n\nPuedes enviar más fotos si hace falta.\n\nCuando termines de enviar las imágenes, escribe *FOTOS LISTAS*.`\n }\n }\n];" + "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 '✅ Image received.',\n '',\n 'You can send more images if needed.',\n '',\n '⚠️ Remember: send each image in a separate 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 '✅ Imagen recibida.',\n '',\n 'Puedes enviar más imágenes si hace falta.',\n '',\n '⚠️ Recuerda: envía cada imagen en un mensaje separado.',\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 ...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": [ - 4320, - -16 + -4336, + -320 ], "id": "ebc7dadc-6ddc-4806-8c19-7d4e2ce567cf", "name": "Code — Preparar confirmación imagen" @@ -1824,10 +2172,10 @@ "columns": { "mappingMode": "defineBelow", "value": { - "event_id": "=", - "manager_telefono": "=", - "manager_nombre": "=", - "tiene_media": "=", + "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", @@ -1840,7 +2188,10 @@ "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 }}" + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}", + "comando": "={{ $json.accion_flujo }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_procesado": "={{ new Date().toISOString() }}" }, "matchingColumns": [], "schema": [ @@ -2037,8 +2388,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3632, - -16 + -5024, + -320 ], "id": "f8cf4436-cf35-45d6-acc7-7adfb9e3a2dc", "name": "Sheets - Guardar evento imagen WhatsApp TEST", @@ -2051,26 +2402,26 @@ }, { "parameters": { - "jsCode": "const data = $json || {};\n\nconst telefono = data.manager_telefono || data.whatsapp_to || '';\nconst nombre = String(data.manager_nombre || '').trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\n\nreturn [\n {\n json: {\n ...data,\n\n whatsapp_to: telefono,\n\n whatsapp_text: `📸 Necesito que me envíes al menos una imagen${saludo}.\n\nLa nota de voz ya fue recibida, pero para completar el reporte de propuesta ejecutada hace falta una foto de evidencia.\n\nPor favor envía una imagen y luego escribe *LISTO* cuando termines.`\n }\n }\n];" + "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": [ - 3200, - 512 + -5456, + 208 ], "id": "75998ea0-d2ec-4614-b18e-824019627d72", "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 || {};\n\nconst ahora = new Date().toISOString();\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 // Avanzamos al paso 3\n etapa: 'ESPERANDO_VIDEOS',\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 estado: 'ACTIVA',\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: sesion.motivo_revision || '',\n\n whatsapp_to: base.whatsapp_to || base.whatsapp_remote_jid || ''\n }\n }\n];" + "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": [ - 3536, - 288 + -5120, + -16 ], "id": "b0e3e960-8cc9-453e-bbf2-4e410ab4a178", "name": "Preparar actualización fotos listas" @@ -2103,7 +2454,21 @@ "videos_count": "={{ $json.videos_count }}", "estado": "={{ $json.estado }}", "ejecucion_id": "={{ $json.ejecucion_id }}", - "motivo_revision": "={{ $json.motivo_revision }}" + "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" @@ -2227,6 +2592,106 @@ "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", @@ -2247,8 +2712,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3744, - 288 + -4928, + -16 ], "id": "dea8b556-e019-40b3-97fc-04e1a47c1ed6", "name": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", @@ -2261,26 +2726,26 @@ }, { "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst nombre = String(\n data.manager_nombre ||\n sesion.manager_nombre ||\n base.manager_nombre ||\n ''\n).trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\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: `✅ Fotos registradas correctamente${saludo}.\n\n*Paso 3 de 3 — Videos opcionales*\n\nPuedes enviar uno o varios videos de la ejecución si tienes.\n\nSi no tienes videos, escribe: *SIN VIDEO*\n\nSi enviaste videos y ya terminaste, escribe: *LISTO*`\n }\n }\n];" + "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": [ - 3952, - 288 + -4704, + -16 ], "id": "50b9bdba-c99b-4150-9a85-e57c18e24f5a", "name": "Code - Preparar solicitud videos WhatsApp TEST" }, { "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: `🎥 Ya estamos en el paso de videos opcionales.\n\nPuedes enviar uno o varios videos de la ejecución.\n\nSi no tienes videos, escribe: *SIN VIDEO*\n\nSi ya enviaste videos y terminaste, escribe: *LISTO*`\n }\n }\n];" + "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": [ - 3648, - 1824 + -5008, + 1520 ], "id": "08b3a87f-00c6-439b-bcbe-131d86d100d5", "name": "Code - Preparar recordatorio videos WhatsApp TEST" @@ -2292,8 +2757,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3072, - 1056 + -6432, + 512 ], "id": "c4db0aed-c18e-429c-a6ff-299bd94d2797", "name": "CERRAR_SIN_VIDEO" @@ -2470,8 +2935,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3280, - 1056 + -6224, + 512 ], "id": "27195f01-98fa-4772-b0c6-d82b8241cc83", "name": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", @@ -2482,19 +2947,6 @@ } } }, - { - "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('CERRAR_SIN_VIDEO').first().json || {};\n\nconst nombre = String(\n data.manager_nombre ||\n base.manager_nombre ||\n ''\n).trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\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: `✅ Reporte recibido completo${saludo}.\n\nRegistré que no hay videos para esta ejecución.\n\nAhora voy a preparar el procesamiento de la nota de voz y las imágenes recibidas.`\n }\n }\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3488, - 1056 - ], - "id": "6384ba4f-b6d0-46c3-bfbe-63cf5f59a7e3", - "name": "Code - Preparar confirmación cierre sin video WhatsApp TEST" - }, { "parameters": { "operation": "append", @@ -2730,8 +3182,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3344, - 688 + -5312, + 384 ], "id": "c98b3d5e-cf77-46c6-9d5e-2b6222949656", "name": "Sheets - Guardar evento video WhatsApp TEST", @@ -2749,8 +3201,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3552, - 688 + -5104, + 384 ], "id": "7bc3bb63-803e-43e9-9df7-1ed53a7597cb", "name": "Code - Preparar actualización video recibido WhatsApp TEST" @@ -2927,8 +3379,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3760, - 688 + -4896, + 384 ], "id": "27556223-2f6f-464f-819a-a035a66e3aad", "name": "Sheets - Actualizar sesión video recibido WhatsApp TEST", @@ -2941,13 +3393,13 @@ }, { "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('Code - Preparar actualización video recibido WhatsApp TEST').first().json || {};\n\nconst nombre = String(\n data.manager_nombre ||\n base.manager_nombre ||\n ''\n).trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\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: `✅ Video recibido${saludo}.\n\nPuedes enviar más videos si hace falta.\n\nCuando termines, escribe *LISTO* para cerrar el reporte.`\n }\n }\n];" + "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": [ - 3968, - 688 + -4688, + 384 ], "id": "ce363661-867f-4c31-b730-a7aaf88453cc", "name": "Code - Preparar confirmación video WhatsApp TEST" @@ -2959,8 +3411,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3408, - 1472 + -6512, + 800 ], "id": "b2d8579c-f7b8-486a-a502-3b0e91912628", "name": "Code - Preparar cierre con videos WhatsApp TEST" @@ -3137,8 +3589,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3616, - 1472 + -6304, + 800 ], "id": "ccb46bd5-6e01-42d5-89ab-54e4622265be", "name": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", @@ -3149,19 +3601,6 @@ } } }, - { - "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('Code - Preparar cierre con videos WhatsApp TEST').first().json || {};\n\nconst nombre = String(\n data.manager_nombre ||\n base.manager_nombre ||\n ''\n).trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\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: `✅ Reporte recibido completo${saludo}.\n\nRegistré la nota de voz, las imágenes y los videos enviados.\n\nAhora voy a preparar el procesamiento de toda la evidencia recibida.`\n }\n }\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3824, - 1472 - ], - "id": "19fa8906-79eb-4845-a969-66d3b661d27d", - "name": "Code - Preparar confirmación cierre con videos WhatsApp TEST" - }, { "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];" @@ -3169,8 +3608,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3712, - 2064 + -4944, + 1760 ], "id": "b5960269-5fca-4d10-b2ef-eda72eb68b10", "name": "Code - Preparar aviso procesando WhatsApp TEST" @@ -3182,8 +3621,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3936, - 1232 + -4720, + 928 ], "id": "6cee8abe-abb5-4818-ac8c-1f49f0905bcc", "name": "Code - Preparar búsqueda eventos media WhatsApp TEST" @@ -3209,8 +3648,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 4192, - 1232 + -4464, + 928 ], "id": "e49fc551-3ddb-4041-88fd-c4329ef64d54", "name": "Sheets - Leer eventos WhatsApp TEST", @@ -3229,8 +3668,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 4432, - 1232 + -4224, + 928 ], "id": "6c7e217e-6200-46d8-892a-adc9b273e4a7", "name": "Code - Filtrar eventos media de sesión WhatsApp TEST" @@ -3242,8 +3681,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 4672, - 1232 + -3984, + 928 ], "id": "6d74dd89-9530-44d7-ae69-2e771bf8aab9", "name": "Code - Separar media en items WhatsApp TEST" @@ -3273,8 +3712,8 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 4880, - 1232 + -3776, + 928 ], "id": "6c936089-6193-479f-b87b-a98e69e4fa92", "name": "HTTP Request - Obtener media base64 Evolution TEST", @@ -3287,8 +3726,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5088, - 1232 + -3568, + 928 ], "id": "73f2d7f0-bc17-47cc-9b7a-f477f942bf63", "name": "Code - Convertir base64 a binario WhatsApp TEST" @@ -3311,8 +3750,8 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 5328, - 1232 + -3328, + 928 ], "id": "c9d49cee-9f7f-4dfe-bf4a-5729a3f64298", "name": "Drive - Subir media WhatsApp TEST", @@ -3330,8 +3769,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5536, - 1232 + -3120, + 928 ], "id": "4bfe263f-272f-4738-8664-4f60990ad359", "name": "Code - Preparar registro media Drive WhatsApp TEST" @@ -3343,8 +3782,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5952, - 1232 + -2704, + 928 ], "id": "8ecc5a7e-5520-49e8-909c-1a59a47f28f9", "name": "Code - Consolidar media recuperada WhatsApp TEST" @@ -3511,8 +3950,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 5744, - 1232 + -2912, + 928 ], "id": "900504c8-6e4d-4ce4-bb36-55739dae8408", "name": "Sheets - Guardar media Drive WhatsApp TEST", @@ -3691,8 +4130,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 6160, - 1232 + -2496, + 928 ], "id": "6f48b6b3-21bb-4f03-af72-68f67c2e0574", "name": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", @@ -3710,8 +4149,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6368, - 1232 + -2288, + 928 ], "id": "6fefc255-e85b-4028-b9e9-b3f71abe1457", "name": "Code - Preparar paquete análisis Gemini WhatsApp TEST" @@ -3800,21 +4239,21 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ - 5408, - 1680 + -3248, + 1376 ], "id": "235d1178-5ecf-4225-b1c5-792512a22877", "name": "Switch - Tipo media para Gemini WhatsApp TEST" }, { "parameters": { - "jsCode": "const input = $input.first();\nconst data = input.json || {};\nconst binary = input.binary || {};\n\nif (!binary.data) {\n throw new Error('No llegó binary.data al nodo de audio para Gemini');\n}\n\nreturn [\n {\n json: {\n ...data,\n gemini_prompt_audio: `Analiza esta nota de voz de una propuesta ejecutada.\n\nExtrae 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\n- Qué se implementó\n- Comentarios o resultados observados\n\nNo inventes datos. Si algo no está claro, indica \"No identificado\".`\n },\n binary\n }\n];" + "jsCode": "const input = $input.first();\nconst data = input.json || {};\nconst binary = input.binary || {};\n\nif (!binary.data) {\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 explícitamente \"ejecutada\", \"ya se ejecutó\", \"se implementó\", \"se realizó\", \"se instaló\" o expresiones equivalentes, clasifica como PROPUESTA_EJECUTADA.\n- Si el manager dice explícitamente \"propuesta externa\", clasifica como PROPUESTA_EXTERNA.\n- Si el manager dice \"externa\", \"fuera del banco\", \"recibida fuera del banco interno\", \"referencia externa\" o expresiones equivalentes, 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 {\n json: {\n ...data,\n gemini_prompt_audio: geminiPromptAudio\n },\n binary\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5888, - 1504 + -2768, + 1200 ], "id": "e3cfe03a-110b-4470-9011-e4705dcde4a7", "name": "Code - Preparar Gemini audio WhatsApp TEST" @@ -3825,9 +4264,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-2.5-flash-lite", + "value": "models/gemini-3.1-flash-lite", "mode": "list", - "cachedResultName": "models/gemini-2.5-flash-lite" + "cachedResultName": "models/gemini-3.1-flash-lite" }, "text": "={{ $json.gemini_prompt_audio }}", "inputType": "binary", @@ -3836,8 +4275,8 @@ "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ - 6096, - 1504 + -2560, + 1200 ], "id": "18ddf06b-efd7-49b0-b4bf-0f68e4c91a20", "name": "Gemini - Analizar audio WhatsApp TEST", @@ -3855,8 +4294,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6304, - 1504 + -2352, + 1200 ], "id": "2587e982-2403-450c-9419-a9de41d3c5a1", "name": "Code - Normalizar análisis audio WhatsApp TEST" @@ -3868,8 +4307,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5904, - 1728 + -2752, + 1424 ], "id": "68e8b0de-373c-48fa-a762-8c0e45d64c15", "name": "Code - Preparar Gemini imagen WhatsApp TEST" @@ -3880,9 +4319,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-2.5-flash-lite", + "value": "models/gemini-3.1-flash-lite", "mode": "list", - "cachedResultName": "models/gemini-2.5-flash-lite" + "cachedResultName": "models/gemini-3.1-flash-lite" }, "text": "={{ $json.gemini_prompt_imagen }}", "inputType": "binary", @@ -3891,8 +4330,8 @@ "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ - 6112, - 1728 + -2544, + 1424 ], "id": "1baf8c8e-95cf-42fe-af4f-fcbc71a557fc", "name": "Gemini - Analizar imagen WhatsApp TEST", @@ -3910,8 +4349,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6320, - 1728 + -2336, + 1424 ], "id": "e1bc474b-82b0-4a4c-9ef7-e5f52b2b7503", "name": "Code - Normalizar análisis imagen WhatsApp TEST" @@ -3923,8 +4362,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5776, - 1968 + -2880, + 1664 ], "id": "5bfc997b-a538-4d96-8d7d-e4abe8e4cf9f", "name": "Code - Preparar Gemini video WhatsApp TEST" @@ -3935,9 +4374,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-2.5-flash-lite", + "value": "models/gemini-3.1-flash-lite", "mode": "list", - "cachedResultName": "models/gemini-2.5-flash-lite" + "cachedResultName": "models/gemini-3.1-flash-lite" }, "text": "={{ $json.gemini_prompt_video }}", "inputType": "binary", @@ -3946,8 +4385,8 @@ "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ - 5984, - 1968 + -2672, + 1664 ], "id": "5767efd8-bfb6-4221-964d-743b417be1aa", "name": "Analyze video", @@ -3965,8 +4404,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6192, - 1968 + -2464, + 1664 ], "id": "c035db53-87b9-4fc2-9346-d5228bbac7b7", "name": "Code - Normalizar análisis video WhatsApp TEST" @@ -4103,8 +4542,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 6784, - 1680 + -1872, + 1376 ], "id": "55f75826-cb50-4857-b58f-11bce7703b47", "name": "Sheets - Guardar análisis media Gemini WhatsApp TEST", @@ -4114,6 +4553,2125 @@ "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": [ + -1184, + 1360 + ], + "id": "d68f5e66-a1cb-48cc-afbd-07677f3145cb", + "name": "Sheets - Leer análisis media Gemini WhatsApp TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const cierre = $('Code - Consolidar media recuperada WhatsApp TEST').first().json || {};\nconst evento = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\n\nconst sessionId = String(cierre.session_id || evento.session_id || '').trim();\nconst ejecucionId = String(cierre.ejecucion_id || evento.ejecucion_id || sessionId).trim();\n\nconst rows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst extraerTextoAnalisis = (valor) => {\n if (!valor) return '';\n\n if (typeof valor === 'object') {\n return extraerDesdeObjeto(valor);\n }\n\n const texto = String(valor || '').trim();\n\n if (!texto) return '';\n\n try {\n const obj = JSON.parse(texto);\n const extraido = extraerDesdeObjeto(obj);\n if (extraido) return extraido;\n } catch (error) {}\n\n return texto;\n};\n\nconst extraerDesdeObjeto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text.trim();\n if (typeof obj.output === 'string') return obj.output.trim();\n if (typeof obj.response === 'string') return obj.response.trim();\n if (typeof obj.content === 'string') return obj.content.trim();\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 = extraerDesdeObjeto(obj.message);\n if (txt) return txt;\n }\n\n if (obj.data && typeof obj.data === 'object') {\n const txt = extraerDesdeObjeto(obj.data);\n if (txt) return txt;\n }\n\n return '';\n};\n\nconst analisisSesion = rows.filter(row => {\n return String(row.session_id || '').trim() === sessionId;\n});\n\n// Evitar duplicados por pruebas manuales\nconst vistos = new Set();\nconst analisisUnicos = [];\n\nfor (const row of analisisSesion) {\n const key = `${row.media_event_id || ''}_${row.media_type || ''}`;\n\n if (vistos.has(key)) continue;\n\n vistos.add(key);\n\n analisisUnicos.push({\n ...row,\n analisis_texto_limpio: extraerTextoAnalisis(row.analisis_texto)\n });\n}\n\nconst audios = analisisUnicos.filter(r => String(r.media_type || '').toLowerCase() === 'audio');\nconst imagenes = analisisUnicos.filter(r => String(r.media_type || '').toLowerCase() === 'image');\nconst videos = analisisUnicos.filter(r => String(r.media_type || '').toLowerCase() === 'video');\n\nconst textoAudio = audios.map(r => r.analisis_texto_limpio).filter(Boolean).join('\\n\\n---\\n\\n');\nconst textoImagenes = imagenes.map(r => r.analisis_texto_limpio).filter(Boolean).join('\\n\\n---\\n\\n');\nconst textoVideos = videos.map(r => r.analisis_texto_limpio).filter(Boolean).join('\\n\\n---\\n\\n');\n\nconst textoTotal = [textoAudio, textoImagenes, textoVideos].filter(Boolean).join('\\n\\n');\n\nif (!textoTotal) {\n throw new Error(`No hay texto limpio de análisis para la sesión ${sessionId}. Revisa wa_ejecuciones_analisis_media.`);\n}\n\nconst managerNombre =\n limpiar(cierre.manager_nombre) ||\n limpiar(evento.manager_nombre) ||\n 'No identificado';\n\nconst managerTelefono =\n limpiar(cierre.manager_telefono) ||\n limpiar(evento.manager_telefono) ||\n 'No identificado';\n\nconst promptFinal = `\nEres un analista de propuestas de GomezLee Marketing.\n\nCon base en el análisis del audio, imágenes y videos, genera un JSON final estructurado para registrar el reporte.\n\nReglas generales:\n- Devuelve SOLO JSON válido.\n- No uses markdown.\n- No inventes información.\n- Si un dato no está claro, usa \"No identificado\".\n- El audio tiene prioridad para datos declarados por el manager.\n- Las imágenes y videos sirven para validar evidencia visual.\n- Si hay dudas importantes, requiere_revision debe ser \"SI\"; si todo está claro, \"NO\".\n- nivel_confianza debe ser \"ALTA\", \"MEDIA\" o \"BAJA\".\n- resumen_ejecucion debe ser una síntesis útil para un reporte ejecutivo.\n- que_se_implemento debe describir claramente lo ejecutado o reportado.\n\nClasifica también el tipo de reporte.\n\ntipo_reporte debe ser exactamente uno de estos valores:\n- PROPUESTA_EJECUTADA\n- PROPUESTA_EXTERNA\n- NO_DETERMINADO\n\ntipo_reporte_confianza debe ser exactamente uno de estos valores:\n- ALTA\n- MEDIA\n- BAJA\n\ntipo_reporte_motivo debe explicar brevemente por qué se eligió ese tipo.\n\nReglas para tipo_reporte:\n- Si el audio menciona claramente que es una propuesta ejecutada, usa PROPUESTA_EJECUTADA.\n- Si el audio menciona que ya se ejecutó, se implementó, se instaló, se realizó o se activó una propuesta, usa PROPUESTA_EJECUTADA.\n- Si el audio menciona claramente que es una propuesta externa, usa PROPUESTA_EXTERNA.\n- Si el audio menciona algo recibido fuera del banco interno, referencia externa, idea externa, actividad externa o material externo que debe documentarse, usa PROPUESTA_EXTERNA.\n- Si el audio es una prueba, saludo, ruido, información incompleta o no permite clasificar, usa NO_DETERMINADO.\n- No uses las imágenes para inventar el tipo si el audio no lo confirma.\n- El audio tiene prioridad para determinar el tipo de reporte.\n- Si hay duda entre ejecutada y externa, usa NO_DETERMINADO.\n\nEstructura obligatoria:\n\n{\n \"tipo_reporte\": \"\",\n \"tipo_reporte_confianza\": \"\",\n \"tipo_reporte_motivo\": \"\",\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 \"evidencia_audio_resumen\": \"\",\n \"evidencia_imagenes_resumen\": \"\",\n \"evidencia_videos_resumen\": \"\",\n \"nivel_confianza\": \"\",\n \"requiere_revision\": \"\",\n \"motivo_revision\": \"\"\n}\n\nDatos de control:\nsession_id: ${sessionId}\nejecucion_id: ${ejecucionId}\nmanager_nombre: ${managerNombre}\nmanager_telefono: ${managerTelefono}\n\nANÁLISIS DEL AUDIO:\n${textoAudio || 'No hay análisis de audio disponible.'}\n\nANÁLISIS DE IMÁGENES:\n${textoImagenes || 'No hay análisis de imágenes disponible.'}\n\nANÁLISIS DE VIDEOS:\n${textoVideos || 'No hay análisis de videos disponible.'}\n`.trim();\n\nreturn [\n {\n json: {\n ...cierre,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n manager_nombre: managerNombre,\n manager_telefono: managerTelefono,\n canal_origen: cierre.canal_origen || evento.canal_origen || 'WHATSAPP',\n\n total_analisis_media: analisisUnicos.length,\n total_analisis_audio: audios.length,\n total_analisis_imagenes: imagenes.length,\n total_analisis_videos: videos.length,\n\n texto_audio: textoAudio,\n texto_imagenes: textoImagenes,\n texto_videos: textoVideos,\n\n gemini_prompt_final: promptFinal,\n\n estado_procesamiento: 'ANALISIS_MEDIA_CONSOLIDADO'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -976, + 1360 + ], + "id": "2a9d4918-8e52-4633-8369-c4c8397e9129", + "name": "Code - Consolidar análisis media Gemini WhatsApp TEST" + }, + { + "parameters": { + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "messages": { + "values": [ + { + "content": "={{ $json.gemini_prompt_final }}" + } + ] + }, + "builtInTools": {}, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -768, + 1360 + ], + "id": "76e59ce7-1844-40e3-bd7d-1e4101838aad", + "name": "Gemini - Generar JSON final propuesta ejecutada TEST", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "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 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 valor = (campo, fallback = 'No identificado') => {\n const v = limpiar(campo);\n if (!v) return fallback;\n if (v.toLowerCase() === 'undefined') return fallback;\n if (v.toLowerCase() === 'null') return fallback;\n return v;\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 ahora = new Date().toISOString();\n\nconst requiereRevision = valor(parsed.requiere_revision, 'SI').toUpperCase();\n\nconst estadoRevision = requiereRevision === 'SI'\n ? 'PENDIENTE_REVISION'\n : 'ANALISIS_COMPLETADO';\n\nconst evidenciaImagenes = valor(parsed.evidencia_imagenes_resumen, '');\nconst evidenciaVideos = valor(parsed.evidencia_videos_resumen, '');\n\nconst elementosDetectados = [\n evidenciaImagenes,\n evidenciaVideos\n].filter(Boolean).join('\\n\\n');\n\nconst tags = [\n parsed.marca,\n parsed.cliente,\n parsed.pais,\n parsed.ubicacion\n]\n .map(v => valor(v, ''))\n .filter(Boolean)\n .join(', ');\n\nconst tipoReporte = normalizarTipoReporte(parsed.tipo_reporte);\nconst tipoReporteConfianza = normalizarConfianza(parsed.tipo_reporte_confianza);\nconst tipoReporteMotivo = valor(parsed.tipo_reporte_motivo, 'No identificado');\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: valor(parsed.fecha_ejecucion),\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: valor(parsed.propuesta_referencia),\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: valor(parsed.ubicacion),\n\n comentario_original: valor(parsed.evidencia_audio_resumen || base.texto_audio, ''),\n resumen_ia: valor(parsed.resumen_ejecucion, ''),\n descripcion_ejecucion: valor(parsed.que_se_implemento),\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: valor(parsed.evidencia_audio_resumen || base.texto_audio, ''),\n\n nivel_confianza: valor(parsed.nivel_confianza, 'BAJA'),\n requiere_revision: requiereRevision,\n\n gemini_json_raw: texto\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -416, + 1360 + ], + "id": "891ffabf-cb1a-415a-8b29-3facb39f4efa", + "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 }}" + }, + "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 + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 2144, + 1536 + ], + "id": "705689dd-2988-4aa4-835b-3386b5d24e13", + "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": [ + 8800, + 1440 + ], + "id": "49036fad-dbd7-44d8-8597-b098eadf0ed3", + "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": [ + 9088, + 1408 + ], + "id": "914441cf-483f-4aab-a30e-7c619a34e49e", + "name": "Code - Preparar mensaje final WhatsApp TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "7591a797-d76a-48e7-8e75-9a41cff6e974", + "leftValue": "={{ Number($json.media_index || 0) === Number($json.media_total || 0) }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -1600, + 1360 + ], + "id": "9f291944-bbf8-4584-b997-9e842caca9cb", + "name": "IF - Es último análisis media WhatsApp TEST" + }, + { + "parameters": { + "amount": 15 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + -1376, + 1360 + ], + "id": "fef0aeef-418b-419d-a6ca-6a9440494139", + "name": "Wait", + "webhookId": "c6d9670c-1a1f-43c9-89b3-9952b1501850" + }, + { + "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": [ + 384, + 1376 + ], + "id": "7c547eeb-0528-460f-a9a2-d3533e43bb55", + "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": [ + 592, + 1376 + ], + "id": "dc39153f-ddcc-4f21-abc8-7a4b42074807", + "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": [ + 800, + 1376 + ], + "id": "210119ea-e335-4d9d-abd2-b2aa5b0af24a", + "name": "IF - Propuesta final ya existe TEST" + }, + { + "parameters": { + "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '10PZtoMFuiXczGM9Itbcjce4EKBUhiztFbXZiTWX2DKc';\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": [ + 2528, + 1504 + ], + "id": "d7e6d879-f330-4944-a006-aa5735fe0177", + "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": [ + 4448, + 1552 + ], + "id": "f3543da5-0b8b-4de9-a934-8ed6daaa849c", + "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": [ + 4720, + 1552 + ], + "id": "1b0f80c2-16cd-499a-aadf-3f44c3753ce4", + "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": "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": [ + 6480, + 1424 + ], + "id": "cb35219a-46a0-49c2-a404-91c5c91a30d2", + "name": "Sheets - Actualizar link presentación ejecución 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 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 normalizarTipoReporte = (valor) => {\n const texto = limpiarTexto(valor, '')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, '_');\n\n if (\n texto === 'PROPUESTA_EXTERNA' ||\n texto === 'EXTERNA' ||\n texto === 'REPORTE_EXTERNO'\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n if (\n texto === 'PROPUESTA_EJECUTADA' ||\n texto === 'EJECUTADA' ||\n texto === 'REPORTE_EJECUTADO'\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n return 'PROPUESTA_EJECUTADA';\n};\n\nconst tipoReporte = normalizarTipoReporte(\n data.tipo_reporte ||\n data.tipo_reporte_detectado ||\n data.reporte_tipo ||\n data.clasificacion_reporte ||\n ''\n);\n\nconst textoTipoReporte = tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'Propuesta Externa'\n : 'Propuesta Ejecutada';\n\nconst tituloReporteTipo = tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'REPORTE DE PROPUESTA EXTERNA'\n : 'REPORTE DE PROPUESTA EJECUTADA';\n\nconst encabezadoTipoReporte = `Fulgencio Fumado · ${textoTipoReporte}`;\n\nconst replacements = {\n '{{TITULO_REPORTE_TIPO}}': tituloReporteTipo,\n '{{ENCABEZADO_TIPO_REPORTE}}': encabezadoTipoReporte,\n '{{TIPO_REPORTE}}': textoTipoReporte,\n\n '{{MARCA}}': limpiarTexto(data.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente),\n '{{PAIS}}': limpiarTexto(data.pais),\n '{{UBICACION}}': limpiarTexto(data.ubicacion),\n\n '{{FECHA_EJECUCION}}': limpiarTexto(data.fecha_ejecucion),\n '{{PROPUESTA_REFERENCIA}}': limpiarTexto(data.propuesta_referencia),\n\n // Se dejan en blanco para no mostrar IDs técnicos en la presentación.\n // Los labels fijos \"ID de Ejecución\" / \"Ejecución ID\" deben eliminarse en la plantilla.\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(data.media_folder_url, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(data.presentacion_ejecucion_url, '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\nif (!data.presentation_id) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nreturn [\n {\n json: {\n ...data,\n\n tipo_reporte_normalizado: tipoReporte,\n tipo_reporte_texto: textoTipoReporte,\n titulo_reporte_tipo: tituloReporteTipo,\n encabezado_tipo_reporte: encabezadoTipoReporte,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 4928, + 1552 + ], + "id": "72e00ab4-ed9e-4852-a89f-fdef9ba6551d", + "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": [ + 5616, + 1536 + ], + "id": "447d49f8-bac0-4c12-bbd0-89dbd2a43b2c", + "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": [ + 6224, + 1536 + ], + "id": "9c252b51-c657-4020-bae8-fd79824feb32", + "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": [ + -5440, + -1792 + ], + "id": "be56aeb8-916d-46b2-b2a2-b544a4bdbd01", + "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": [ + -5392, + -16 + ], + "id": "cc41848d-df63-499a-ba01-16155e30fffe", + "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": [ + 2736, + 1504 + ], + "id": "3a7cf341-e7e6-4d90-bdd0-9e1abde12b10", + "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": [ + 2944, + 1504 + ], + "id": "ba2e9788-c593-4097-96be-e7f6d447be69", + "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": [ + 3152, + 1504 + ], + "id": "3861110f-3334-4e89-954f-b8d86f9b4708", + "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": [ + 3360, + 1504 + ], + "id": "1ccc2d93-f72d-453a-a1b8-7a204d5a5108", + "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": [ + 3568, + 1504 + ], + "id": "fa4bc0cf-498d-4332-8d12-72f4eb7e5ce4", + "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": [ + 3776, + 1504 + ], + "id": "2b0b6839-7d9b-4dc1-9969-a5b3b015653e", + "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, 6);\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": [ + 5824, + 1536 + ], + "id": "2606aa33-73fd-4af3-9497-cea6932760c9", + "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": [ + 6032, + 1536 + ], + "id": "4ed89dd2-2849-419e-95e4-e061939e0002", + "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": [ + -5936, + 688 + ], + "id": "fdbbc5d5-a162-4bfa-ab41-47f4087dba08", + "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": [ + -5520, + 688 + ], + "id": "78fdf65a-3ff9-4219-b22f-9088baa65aeb", + "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": [ + -5744, + 688 + ], + "id": "eeb80d93-128f-488e-814e-86170b8049f7", + "name": "HTTP - Enviar aviso procesando WhatsApp TEST" + }, + { + "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": [ + -208, + 1360 + ], + "id": "0eb5f707-d1a2-4147-a195-a88ed9b5903e", + "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": [ + 0, + 1360 + ], + "id": "2043ba80-2789-4cc7-a261-943c464dd2d6", + "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 jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio 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 ...actual,\n ...slidesNormalizado1,\n ...slidesNormalizado2,\n ...slidesNormalizado3,\n ...match\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\nconst tipoReporte = limpiar(\n match.tipo_reporte ||\n data.tipo_reporte ||\n actual.tipo_reporte ||\n ''\n).toUpperCase();\n\nlet matchEstado = limpiar(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n actual.propuesta_match_estado ||\n ''\n).toUpperCase();\n\nlet matchConfianza = Number(\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 ''\n);\n\nconst presentationId = limpiar(\n data.presentation_id ||\n actual.presentation_id ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\n// Fallback defensivo por si el estado quedó viejo.\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 }\n}\n\n// Formato limpio para el banco original:\n// Solo links, uno por línea.\nlet enlacesActualizados = enlacesExistentes;\n\nif (presentationUrl && !enlacesExistentes.includes(presentationUrl)) {\n enlacesActualizados = enlacesExistentes\n ? `${enlacesExistentes}\\n${presentationUrl}`\n : presentationUrl;\n}\n\nconst motivos = [];\n\nif (tipoReporte !== 'PROPUESTA_EJECUTADA') {\n motivos.push(`tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}`);\n}\n\nif (matchEstado !== 'MATCH_ALTA_CONFIANZA') {\n motivos.push(`propuesta_match_estado es ${matchEstado || 'SIN_MATCH'}`);\n}\n\nif (matchConfianza < 85) {\n motivos.push(`propuesta_match_confianza es ${matchConfianza}`);\n}\n\nif (!presentationUrl) {\n motivos.push('no existe presentacion_ejecucion_url');\n}\n\nif (!nombreBanco) {\n motivos.push('no llegó propuesta_nombre_banco');\n}\n\nif (!rowNumberBanco) {\n motivos.push('no llegó propuesta_banco_row_number');\n}\n\nif (presentationUrl && enlacesExistentes.includes(presentationUrl)) {\n motivos.push('el link ya existe en la columna de enlaces ejecutados');\n}\n\nconst actualizarBanco = motivos.length === 0;\n\nconst motivoNoActualizaBanco = actualizarBanco\n ? ''\n : `No se actualiza banco porque ${motivos.join('; ')}.`;\n\nreturn [\n {\n json: {\n ...data,\n\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\n banco_match_column: 'row_number',\n banco_match_value: rowNumberBanco,\n\n row_number: rowNumberBanco,\n 'Enlaces a propuestas ejecutadas': enlacesActualizados,\n\n banco_enlaces_existentes: enlacesExistentes,\n banco_enlaces_actualizados: enlacesActualizados,\n banco_presentacion_url: presentationUrl,\n\n banco_update_debug: {\n tipo_reporte: tipoReporte,\n propuesta_match_estado: matchEstado,\n propuesta_match_confianza: matchConfianza,\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n presentationUrl,\n enlacesExistentes,\n enlacesActualizados,\n actualizarBanco,\n motivoNoActualizaBanco,\n mejor_match: mejorMatch,\n match_debug: matchDebug\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6928, + 1232 + ], + "id": "3dd8a8af-4902-4a0f-92b5-94e921262ae4", + "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": [ + 7696, + 1280 + ], + "id": "92a6348b-03df-4edc-b1e7-994621c331b7", + "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": [ + 8208, + 1072 + ], + "id": "7c0dc504-1094-461d-82d6-469db24200b9", + "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": [ + 8416, + 1072 + ], + "id": "f9965d96-e94f-4a44-8604-b098fe9a08b9", + "name": "Code - Restaurar contexto actualización banco TEST" } ], "connections": { @@ -4195,7 +6753,7 @@ ], [ { - "node": "ode - Preparar sin sesión para cancelar WhatsApp TEST", + "node": "Code - Preparar sin sesión para cancelar WhatsApp TEST", "type": "main", "index": 0 } @@ -4277,7 +6835,7 @@ ], [ { - "node": "Preparar actualización fotos listas", + "node": "Sheets - Leer eventos sesión fotos WhatsApp TEST", "type": "main", "index": 0 } @@ -4324,6 +6882,7 @@ "index": 0 } ], + [], [] ] }, @@ -4426,17 +6985,6 @@ ] ] }, - "ode - Preparar sin sesión para cancelar WhatsApp TEST": { - "main": [ - [ - { - "node": "WhatsApp - Enviar mensaje TEST", - "type": "main", - "index": 0 - } - ] - ] - }, "Code — Preparar actualización imagen recibida": { "main": [ [ @@ -4551,23 +7099,7 @@ "main": [ [ { - "node": "Code - Preparar confirmación cierre sin video WhatsApp TEST", - "type": "main", - "index": 0 - }, - { - "node": "Code - Preparar búsqueda eventos media WhatsApp TEST", - "type": "main", - "index": 0 - } - ] - ] - }, - "Code - Preparar confirmación cierre sin video WhatsApp TEST": { - "main": [ - [ - { - "node": "WhatsApp - Enviar mensaje TEST", + "node": "Code - Preparar aviso procesando WhatsApp TEST1", "type": "main", "index": 0 } @@ -4633,23 +7165,7 @@ "main": [ [ { - "node": "Code - Preparar confirmación cierre con videos WhatsApp TEST", - "type": "main", - "index": 0 - }, - { - "node": "Code - Preparar búsqueda eventos media WhatsApp TEST", - "type": "main", - "index": 0 - } - ] - ] - }, - "Code - Preparar confirmación cierre con videos WhatsApp TEST": { - "main": [ - [ - { - "node": "WhatsApp - Enviar mensaje TEST", + "node": "Code - Preparar aviso procesando WhatsApp TEST1", "type": "main", "index": 0 } @@ -4916,6 +7432,443 @@ } ] ] + }, + "Sheets - Guardar análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "IF - Es último análisis media WhatsApp 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 + } + ] + ] + }, + "IF - Es último análisis media WhatsApp TEST": { + "main": [ + [ + { + "node": "Wait", + "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": "Sheets - Actualizar link presentación ejecución 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": "Sheets - Leer propuestas ejecutadas final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización banco propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "IF - Actualizar banco match alta confianza 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 + } + ] + ] } }, "settings": { @@ -4926,9 +7879,9 @@ "meta": { "templateCredsSetupCompleted": true }, - "versionId": "f3b250eb-aec9-4861-b8aa-319f56ae5e72", - "activeVersionId": "f3b250eb-aec9-4861-b8aa-319f56ae5e72", - "versionCounter": 906, + "versionId": "94d48ce3-27cf-4be2-8d30-467fb212e28b", + "activeVersionId": "94d48ce3-27cf-4be2-8d30-467fb212e28b", + "versionCounter": 1727, "triggerCount": 1, "shared": [ { @@ -4951,9 +7904,9 @@ ], "tags": [], "activeVersion": { - "updatedAt": "2026-06-15T13:19:24.000Z", - "createdAt": "2026-06-15T13:19:17.509Z", - "versionId": "f3b250eb-aec9-4861-b8aa-319f56ae5e72", + "updatedAt": "2026-06-20T16:00:09.000Z", + "createdAt": "2026-06-20T15:59:44.174Z", + "versionId": "94d48ce3-27cf-4be2-8d30-467fb212e28b", "workflowId": "sXH75CXIQ0sEzm1Y", "nodes": [ { @@ -4974,7 +7927,7 @@ "parameters": [ { "name": "number", - "value": "={{ $json.whatsapp_to || $json.whatsapp_remote_jid || $json.manager_telefono }}" + "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", @@ -4991,21 +7944,21 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 7744, - -112 + 9984, + 704 ], "id": "b1d4c2c3-c77f-4b9d-b4ca-c40535c26299", "name": "WhatsApp - Enviar mensaje TEST" }, { "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(/\\D/g, '');\n\nconst key = data.key || {};\n\nconst remoteJid = texto(key.remoteJid || data.remoteJid || body.remoteJid || '');\nconst managerTelefono = limpiarNumero(remoteJid);\n\nconst pushName =\n texto(data.pushName) ||\n texto(body.pushName) ||\n 'Usuario WhatsApp';\n\nconst message = data.message || {};\n\nconst textoMensaje =\n texto(message.conversation) ||\n texto(message.extendedTextMessage?.text) ||\n texto(data.messageText) ||\n texto(data.text) ||\n texto(body.text) ||\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\nconst normalizarComando = (valor) => {\n const t = texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (t === '/START' || t === 'START' || t === 'INICIAR') return 'START';\n if (t === 'CANCELAR') return 'CANCELAR';\n if (t === 'FOTOS LISTAS' || t === 'FOTOS_LISTAS') return 'FOTOS_LISTAS';\n if (t === 'SIN VIDEO' || t === 'SIN_VIDEO') return 'SIN_VIDEO';\n if (t === 'LISTO') return 'LISTO';\n\n return 'NORMAL';\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 manager_telefono: managerTelefono,\n manager_nombre: pushName,\n usuario_id_origen: managerTelefono,\n\n whatsapp_remote_jid: remoteJid,\n whatsapp_to: remoteJid,\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 raw_preview: JSON.stringify(body).slice(0, 4000),\n raw_event: body,\n },\n },\n];" + "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\nconst normalizarComando = (valor) => {\n const t = normalizarTexto(valor);\n\n // Nuevo inicio oficial\n if (t === 'HEY') return 'START';\n\n // Compatibilidad temporal\n if (\n t === '/START' ||\n t === 'START' ||\n t === 'INICIAR'\n) {\n return 'START';\n}\n\n if (t === 'CANCELAR' || t === 'CANCEL') {\n return 'CANCELAR';\n }\n\n if (\n t === 'FOTOS LISTAS' ||\n t === 'FOTOS_LISTAS' ||\n t === 'IMAGENES LISTAS' ||\n t === 'IMAGENES_LISTAS' ||\n t === 'PHOTOS READY' ||\n t === 'PHOTOS_READY'\n ) {\n return 'FOTOS_LISTAS';\n }\n\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 if (\n t === 'LISTO' ||\n t === 'DONE' ||\n t === 'FINISH' ||\n t === 'FINALIZAR'\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": [ - 208, - -256 + -9200, + -608 ], "id": "35e401ba-acce-429c-b811-58a45c032869", "name": "Code - Normalizar evento WhatsApp TEST" @@ -5022,7 +7975,7 @@ "conditions": [ { "id": "f2be1fb5-b047-4614-a1f2-9a4d65ef5cc7", - "leftValue": "={{ $json.manager_telefono === '18298946645' }}", + "leftValue": "={{ $json.manager_telefono === '18298946645' || $json.sender_phone === '18298946645' }}", "rightValue": "={{ $json.manager_telefono === '18298946645' }}", "operator": { "type": "boolean", @@ -5038,8 +7991,8 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 480, - -256 + -8928, + -608 ], "id": "3b65e795-a7f8-415c-8c04-7cde544c4350", "name": "Mi numero TESTING" @@ -5053,8 +8006,8 @@ "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - 0, - -256 + -9408, + -608 ], "id": "bbdd46ca-d2f0-4c42-8e14-d9591168cf4d", "name": "Webhook - Evolution WhatsApp TEST", @@ -5081,8 +8034,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 848, - -272 + -8560, + -624 ], "id": "653ca576-1661-49aa-80cb-62b7ec46a1c0", "name": "Sheets - Leer sesiones existentes", @@ -5096,13 +8049,13 @@ }, { "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 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);\nconst accion = texto(evento.accion_flujo || 'NORMAL');\n\nconst sesionActiva = sesiones\n .filter((row) => texto(row.manager_telefono) === managerTelefono)\n .filter((row) => {\n const estado = texto(row.estado).toUpperCase();\n const etapa = texto(row.etapa).toUpperCase();\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\nconst suffix = Math.random().toString(36).slice(2, 6).toUpperCase();\nconst sessionId = `WA_${Date.now()}_${managerTelefono.slice(-4)}_${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 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 manager_telefono: managerTelefono,\n manager_nombre: texto(evento.manager_nombre),\n canal_origen: 'WHATSAPP',\n fecha_inicio: new Date().toISOString(),\n ultima_actividad: new Date().toISOString(),\n etapa: 'ESPERANDO_AUDIO',\n audio_count: 0,\n imagenes_count: 0,\n videos_count: 0,\n estado: 'ACTIVA',\n ejecucion_id: sessionId,\n motivo_revision: '',\n },\n },\n },\n];" + "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 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// --------------------------------------------------\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 = texto(row.estado).toUpperCase();\n const etapa = texto(row.etapa).toUpperCase();\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 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 // Nuevos campos para grupo, idioma y clasificación futura\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: texto(evento.idioma_flujo || 'ES'),\n\n tipo_reporte: ''\n },\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1040, - -272 + -8368, + -624 ], "id": "91675542-137d-4976-9c1c-3ba26bb1ddab", "name": "Code - Resolver sesión WhatsApp TEST" @@ -5268,34 +8221,34 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ - 1264, - -336 + -8144, + -688 ], "id": "50d7bb39-3a6d-4906-b695-e6b09ba5c545", "name": "Switch - Decisión sesión WhatsApp TEST" }, { "parameters": { - "jsCode": "const datos = $('Code - Resolver sesión WhatsApp TEST').first().json || {};\n\nconst mensaje = [\n '📸 *Propuestas Ejecutadas - GLM*',\n '',\n `¡Hola, ${datos.manager_nombre || 'equipo'}! Bienvenido al registro de propuestas ejecutadas.`,\n '',\n 'Este proceso permite documentar ejecuciones reales para alimentar el banco de evidencias de Fulgencio Fumado.',\n '',\n '*Paso 1 de 3 — Nota de voz obligatoria*',\n '',\n 'Envía una nota de voz indicando:',\n '',\n '• Nombre o referencia de la propuesta ejecutada',\n '• Marca o cliente',\n '• País',\n '• Ubicación de la ejecución',\n '• Fecha de ejecución',\n '• Qué se implementó',\n '• Comentarios o resultados observados',\n '',\n 'Importante: registra una sola propuesta ejecutada a la vez.',\n].join('\\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];" + "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 `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 '*Step 1 of 3 — Voice note*',\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 `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 '*Paso 1 de 3 — Nota de voz*',\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": [ - 3104, - -2304 + -5552, + -2608 ], "id": "304baef0-fda7-43ce-8bfe-24ed684ced3b", "name": "Code - Preparar bienvenida WhatsApp TEST" }, { "parameters": { - "jsCode": "const datos = $json || {};\n\nconst etapa = String(datos.sesion_activa?.etapa || '').trim() || 'pendiente';\n\nconst mensaje = [\n '⚠️ Ya tienes un reporte de propuesta ejecutada en proceso.',\n '',\n `Etapa actual: ${etapa}`,\n '',\n 'Completa ese reporte antes de iniciar uno nuevo.',\n '',\n 'Si deseas cancelarlo, escribe: CANCELAR',\n].join('\\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];" + "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": [ - 3136, - -1856 + -5520, + -2160 ], "id": "bf9701ac-00d2-47c3-a495-ddaad66ed0be", "name": "Code - Preparar aviso sesión activa WhatsApp TEST" @@ -5332,7 +8285,17 @@ "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 }}" + "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": [ @@ -5452,6 +8415,106 @@ "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, @@ -5462,8 +8525,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 2896, - -2304 + -5760, + -2608 ], "id": "cff69027-2e70-4be6-ad78-80ae6680ca75", "name": "Sheets - Crear sesión WhatsApp TEST", @@ -5476,13 +8539,13 @@ }, { "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 {\n paso_decision = 'PEDIR_AUDIO';\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 } else if (accion === 'FOTOS_LISTAS') {\n // Solo permite avanzar si ya hay al menos una imagen registrada.\n if (imagenesCount > 0) {\n paso_decision = 'FOTOS_LISTAS';\n } else {\n paso_decision = 'PEDIR_IMAGENES';\n }\n } else {\n paso_decision = 'PEDIR_IMAGENES';\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 } else if (accion === 'SIN_VIDEO') {\n paso_decision = 'CERRAR_SIN_VIDEO';\n } else if (accion === 'LISTO') {\n paso_decision = 'CERRAR_CON_VIDEOS';\n } else {\n paso_decision = 'PEDIR_VIDEOS';\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];" + "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": [ - 1552, - 144 + -7856, + -208 ], "id": "28aaa567-1c39-49ac-802d-38e001d71cc2", "name": "Code - Resolver paso activo WhatsApp TEST" @@ -5815,6 +8878,31 @@ }, "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" } ] }, @@ -5823,21 +8911,21 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ - 1904, - 416 + -7664, + 64 ], "id": "b8b395eb-8f3f-4006-8ad8-d426f7e0792a", "name": "Switch - Paso activo WhatsApp TEST" }, { "parameters": { - "jsCode": "const datos = $json || {};\n\nconst mensaje = [\n '🎙️ Aún necesito la nota de voz obligatoria para iniciar el reporte.',\n '',\n 'Por favor envía una nota de voz indicando:',\n '',\n '• Nombre o referencia de la propuesta ejecutada',\n '• Marca o cliente',\n '• País',\n '• Ubicación de la ejecución',\n '• Fecha de ejecución',\n '• Qué se implementó',\n '• Comentarios o resultados observados',\n].join('\\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];" + "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 '🎙️ I still need the required voice note to start the report.',\n '',\n 'Please send a voice note including:',\n '',\n '• Proposal name or reference',\n '• Brand or client',\n '• Country',\n '• Execution/location details',\n '• Date',\n '• What was implemented or reported',\n '• Any relevant comments or observed results',\n ].join('\\n');\n} else {\n mensaje = [\n '🎙️ Aún necesito la nota de voz obligatoria para iniciar el reporte.',\n '',\n 'Por favor envía una nota de voz indicando:',\n '',\n '• Nombre o referencia de la propuesta',\n '• Marca o cliente',\n '• País',\n '• Ubicación o contexto de la ejecución',\n '• Fecha',\n '• Qué se implementó o qué se está reportando',\n '• Comentarios o resultados observados',\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": [ - 2912, - -224 + -5744, + -528 ], "id": "30196ae5-1d82-46e3-9296-32ffa6122ae1", "name": "Code - Preparar solicitud audio WhatsApp TEST" @@ -6078,8 +9166,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 2624, - -608 + -6032, + -912 ], "id": "4b7309e9-5602-4c88-b29f-42e44179f445", "name": "Sheets - Guardar evento audio WhatsApp TEST", @@ -6092,13 +9180,13 @@ }, { "parameters": { - "jsCode": "const datos = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...datos,\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'ACTIVA',\n audio_count: Number(datos.sesion_activa?.audio_count || 0) + 1,\n },\n },\n];" + "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": [ - 2832, - -608 + -5824, + -912 ], "id": "25599b76-fe73-44b6-80f5-1d8ba9616fd1", "name": "Code - Preparar actualización audio recibido WhatsApp TEST" @@ -6127,7 +9215,22 @@ "ultima_actividad": "={{ $json.ultima_actividad }}", "etapa": "={{ $json.etapa }}", "estado": "={{ $json.estado }}", - "audio_count": "={{ $json.audio_count }}" + "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" @@ -6151,7 +9254,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "manager_nombre", @@ -6161,7 +9264,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "canal_origen", @@ -6218,7 +9321,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "videos_count", @@ -6228,7 +9331,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "estado", @@ -6247,7 +9350,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "motivo_revision", @@ -6259,6 +9362,106 @@ "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", @@ -6279,8 +9482,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3040, - -608 + -5616, + -912 ], "id": "35b5002d-7c69-4f31-89de-dc5457961a42", "name": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", @@ -6293,13 +9496,13 @@ }, { "parameters": { - "jsCode": "const datos = $('Code - Preparar actualización audio recibido WhatsApp TEST').first().json || {};\n\nconst mensaje = [\n '✅ Nota de voz recibida.',\n '',\n '*Paso 2 de 3 — Imágenes obligatorias*',\n '',\n 'Ahora envía al menos una foto de la propuesta ejecutada.',\n '',\n 'Puedes enviar varias imágenes seguidas.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS',\n].join('\\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];" + "jsCode": "const datos = $('Code - Preparar actualización audio recibido WhatsApp TEST').first().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 '✅ Voice note received.',\n '',\n '*Step 2 of 3 — Required images*',\n '',\n 'Now send at least one photo related to the proposal or report.',\n '',\n '⚠️ Important: send images one by one, in separate messages.',\n 'Do not send multiple images together in the same WhatsApp album/message.',\n '',\n 'You can send multiple images, but each one must be sent separately.',\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 al menos una foto relacionada a la propuesta o reporte.',\n '',\n '⚠️ Importante: envía las imágenes una por una, en mensajes separados.',\n 'No envíes varias imágenes juntas en el mismo álbum/mensaje de WhatsApp.',\n '',\n 'Puedes enviar varias imágenes, pero cada una debe ir por separado.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS'\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": [ - 3264, - -608 + -5392, + -912 ], "id": "7c1c09aa-4670-47d0-a5a5-3713adf6b6fb", "name": "Code - Preparar solicitud imágenes WhatsApp TEST" @@ -6311,8 +9514,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3008, - -1040 + -5648, + -1344 ], "id": "dd118cad-907f-4bab-89ef-8d75dc2d70cb", "name": "Code - Preparar cancelación sesión WhatsApp TEST" @@ -6493,8 +9696,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3216, - -1040 + -5440, + -1344 ], "id": "ac83d38f-7d91-408a-bb43-f8950ccc40a6", "name": "Sheets - Cancelar sesión WhatsApp TEST", @@ -6507,39 +9710,26 @@ }, { "parameters": { - "jsCode": "const datos = $('Code - Preparar cancelación sesión WhatsApp TEST').first().json || {};\n\nconst mensaje = [\n '✅ Reporte cancelado correctamente.',\n '',\n 'Ya puedes iniciar un nuevo reporte escribiendo:',\n '',\n '/start',\n].join('\\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];" + "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": [ - 3472, - -1104 + -5184, + -1408 ], "id": "119da00c-cea5-4184-90e5-ad4a408271bb", "name": "Code - Preparar mensaje cancelación WhatsApp TEST" }, { "parameters": { - "jsCode": "const datos = $json || {};\n\nconst mensaje = [\n '⚠️ No tienes un reporte activo para cancelar.',\n '',\n 'Para iniciar uno nuevo, escribe:',\n '',\n '/start',\n].join('\\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];" + "jsCode": "const evento = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || base.nueva_sesion || {};\n\nconst ahora = new Date().toISOString();\n\nconst imagenesActuales = Number(sesion.imagenes_count || 0);\n\nreturn [\n {\n json: {\n ...base,\n ...evento,\n\n session_id: base.session_id || sesion.session_id || evento.session_id || '',\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n\n manager_telefono: base.manager_telefono || sesion.manager_telefono || evento.manager_telefono || base.sender_phone || '',\n manager_nombre: base.manager_nombre || sesion.manager_nombre || evento.manager_nombre || base.sender_name || '',\n\n canal_origen: base.canal_origen || sesion.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || base.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'ESPERANDO_IMAGENES',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: imagenesActuales + 1,\n videos_count: Number(sesion.videos_count || 0),\n\n estado: sesion.estado || 'ACTIVA',\n motivo_revision: sesion.motivo_revision || '',\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 }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3216, - -1488 - ], - "id": "be56aeb8-916d-46b2-b2a2-b544a4bdbd01", - "name": "ode - Preparar sin sesión para cancelar WhatsApp TEST" - }, - { - "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 imagenesActuales = Number(sesion.imagenes_count || 0);\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 // IMPORTANTE:\n // Después de recibir una imagen, todavía seguimos esperando más imágenes\n // hasta que el usuario escriba FOTOS LISTAS.\n etapa: 'ESPERANDO_IMAGENES',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: imagenesActuales + 1,\n videos_count: Number(sesion.videos_count || 0),\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: base.whatsapp_to || base.whatsapp_remote_jid || evento.whatsapp_to || evento.whatsapp_remote_jid || ''\n }\n }\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3904, - -16 + -4752, + -320 ], "id": "dee83381-1061-405c-978f-33403362f74b", "name": "Code — Preparar actualización imagen recibida" @@ -6565,14 +9755,25 @@ "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 }}" + "ultima_actividad": "={{ $json.ultima_actividad }}", + "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 }}" }, "matchingColumns": [ "session_id" @@ -6596,7 +9797,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "manager_nombre", @@ -6606,7 +9807,7 @@ "display": true, "type": "string", "canBeUsedToMatch": true, - "removed": true + "removed": false }, { "id": "canal_origen", @@ -6706,6 +9907,106 @@ "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 }, { @@ -6728,8 +10029,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 4112, - -16 + -4544, + -320 ], "id": "392c7a6c-3ab8-4f56-8742-8ab4ffdd5a4c", "name": "Sheets — Actualizar sesión imagen recibida", @@ -6742,13 +10043,13 @@ }, { "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\n\nconst nombre = String(data.manager_nombre || base.manager_nombre || '').trim();\nconst saludo = nombre ? `, ${nombre}` : '';\n\nconst telefono = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\n data.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefono.includes('@s.whatsapp.net')\n ? telefono\n : `${telefono.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n ...data,\n\n whatsapp_to: whatsappTo,\n\n whatsapp_text: `✅ Imagen recibida${saludo}.\n\nPuedes enviar más fotos si hace falta.\n\nCuando termines de enviar las imágenes, escribe *FOTOS LISTAS*.`\n }\n }\n];" + "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 '✅ Image received.',\n '',\n 'You can send more images if needed.',\n '',\n '⚠️ Remember: send each image in a separate 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 '✅ Imagen recibida.',\n '',\n 'Puedes enviar más imágenes si hace falta.',\n '',\n '⚠️ Recuerda: envía cada imagen en un mensaje separado.',\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 ...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": [ - 4320, - -16 + -4336, + -320 ], "id": "ebc7dadc-6ddc-4806-8c19-7d4e2ce567cf", "name": "Code — Preparar confirmación imagen" @@ -6773,10 +10074,10 @@ "columns": { "mappingMode": "defineBelow", "value": { - "event_id": "=", - "manager_telefono": "=", - "manager_nombre": "=", - "tiene_media": "=", + "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", @@ -6789,7 +10090,10 @@ "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 }}" + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}", + "comando": "={{ $json.accion_flujo }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_procesado": "={{ new Date().toISOString() }}" }, "matchingColumns": [], "schema": [ @@ -6986,8 +10290,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3632, - -16 + -5024, + -320 ], "id": "f8cf4436-cf35-45d6-acc7-7adfb9e3a2dc", "name": "Sheets - Guardar evento imagen WhatsApp TEST", @@ -7000,26 +10304,26 @@ }, { "parameters": { - "jsCode": "const data = $json || {};\n\nconst telefono = data.manager_telefono || data.whatsapp_to || '';\nconst nombre = String(data.manager_nombre || '').trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\n\nreturn [\n {\n json: {\n ...data,\n\n whatsapp_to: telefono,\n\n whatsapp_text: `📸 Necesito que me envíes al menos una imagen${saludo}.\n\nLa nota de voz ya fue recibida, pero para completar el reporte de propuesta ejecutada hace falta una foto de evidencia.\n\nPor favor envía una imagen y luego escribe *LISTO* cuando termines.`\n }\n }\n];" + "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": [ - 3200, - 512 + -5456, + 208 ], "id": "75998ea0-d2ec-4614-b18e-824019627d72", "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 || {};\n\nconst ahora = new Date().toISOString();\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 // Avanzamos al paso 3\n etapa: 'ESPERANDO_VIDEOS',\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 estado: 'ACTIVA',\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: sesion.motivo_revision || '',\n\n whatsapp_to: base.whatsapp_to || base.whatsapp_remote_jid || ''\n }\n }\n];" + "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": [ - 3536, - 288 + -5120, + -16 ], "id": "b0e3e960-8cc9-453e-bbf2-4e410ab4a178", "name": "Preparar actualización fotos listas" @@ -7052,7 +10356,21 @@ "videos_count": "={{ $json.videos_count }}", "estado": "={{ $json.estado }}", "ejecucion_id": "={{ $json.ejecucion_id }}", - "motivo_revision": "={{ $json.motivo_revision }}" + "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" @@ -7176,6 +10494,106 @@ "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", @@ -7196,8 +10614,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3744, - 288 + -4928, + -16 ], "id": "dea8b556-e019-40b3-97fc-04e1a47c1ed6", "name": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", @@ -7210,26 +10628,26 @@ }, { "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst nombre = String(\n data.manager_nombre ||\n sesion.manager_nombre ||\n base.manager_nombre ||\n ''\n).trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\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: `✅ Fotos registradas correctamente${saludo}.\n\n*Paso 3 de 3 — Videos opcionales*\n\nPuedes enviar uno o varios videos de la ejecución si tienes.\n\nSi no tienes videos, escribe: *SIN VIDEO*\n\nSi enviaste videos y ya terminaste, escribe: *LISTO*`\n }\n }\n];" + "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": [ - 3952, - 288 + -4704, + -16 ], "id": "50b9bdba-c99b-4150-9a85-e57c18e24f5a", "name": "Code - Preparar solicitud videos WhatsApp TEST" }, { "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: `🎥 Ya estamos en el paso de videos opcionales.\n\nPuedes enviar uno o varios videos de la ejecución.\n\nSi no tienes videos, escribe: *SIN VIDEO*\n\nSi ya enviaste videos y terminaste, escribe: *LISTO*`\n }\n }\n];" + "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": [ - 3648, - 1824 + -5008, + 1520 ], "id": "08b3a87f-00c6-439b-bcbe-131d86d100d5", "name": "Code - Preparar recordatorio videos WhatsApp TEST" @@ -7241,8 +10659,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3072, - 1056 + -6432, + 512 ], "id": "c4db0aed-c18e-429c-a6ff-299bd94d2797", "name": "CERRAR_SIN_VIDEO" @@ -7419,8 +10837,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3280, - 1056 + -6224, + 512 ], "id": "27195f01-98fa-4772-b0c6-d82b8241cc83", "name": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", @@ -7431,19 +10849,6 @@ } } }, - { - "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('CERRAR_SIN_VIDEO').first().json || {};\n\nconst nombre = String(\n data.manager_nombre ||\n base.manager_nombre ||\n ''\n).trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\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: `✅ Reporte recibido completo${saludo}.\n\nRegistré que no hay videos para esta ejecución.\n\nAhora voy a preparar el procesamiento de la nota de voz y las imágenes recibidas.`\n }\n }\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3488, - 1056 - ], - "id": "6384ba4f-b6d0-46c3-bfbe-63cf5f59a7e3", - "name": "Code - Preparar confirmación cierre sin video WhatsApp TEST" - }, { "parameters": { "operation": "append", @@ -7679,8 +11084,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3344, - 688 + -5312, + 384 ], "id": "c98b3d5e-cf77-46c6-9d5e-2b6222949656", "name": "Sheets - Guardar evento video WhatsApp TEST", @@ -7698,8 +11103,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3552, - 688 + -5104, + 384 ], "id": "7bc3bb63-803e-43e9-9df7-1ed53a7597cb", "name": "Code - Preparar actualización video recibido WhatsApp TEST" @@ -7876,8 +11281,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3760, - 688 + -4896, + 384 ], "id": "27556223-2f6f-464f-819a-a035a66e3aad", "name": "Sheets - Actualizar sesión video recibido WhatsApp TEST", @@ -7890,13 +11295,13 @@ }, { "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('Code - Preparar actualización video recibido WhatsApp TEST').first().json || {};\n\nconst nombre = String(\n data.manager_nombre ||\n base.manager_nombre ||\n ''\n).trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\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: `✅ Video recibido${saludo}.\n\nPuedes enviar más videos si hace falta.\n\nCuando termines, escribe *LISTO* para cerrar el reporte.`\n }\n }\n];" + "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": [ - 3968, - 688 + -4688, + 384 ], "id": "ce363661-867f-4c31-b730-a7aaf88453cc", "name": "Code - Preparar confirmación video WhatsApp TEST" @@ -7908,8 +11313,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3408, - 1472 + -6512, + 800 ], "id": "b2d8579c-f7b8-486a-a502-3b0e91912628", "name": "Code - Preparar cierre con videos WhatsApp TEST" @@ -8086,8 +11491,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 3616, - 1472 + -6304, + 800 ], "id": "ccb46bd5-6e01-42d5-89ab-54e4622265be", "name": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", @@ -8098,19 +11503,6 @@ } } }, - { - "parameters": { - "jsCode": "const data = $json || {};\nconst base = $('Code - Preparar cierre con videos WhatsApp TEST').first().json || {};\n\nconst nombre = String(\n data.manager_nombre ||\n base.manager_nombre ||\n ''\n).trim();\n\nconst saludo = nombre ? `, ${nombre}` : '';\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\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: `✅ Reporte recibido completo${saludo}.\n\nRegistré la nota de voz, las imágenes y los videos enviados.\n\nAhora voy a preparar el procesamiento de toda la evidencia recibida.`\n }\n }\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3824, - 1472 - ], - "id": "19fa8906-79eb-4845-a969-66d3b661d27d", - "name": "Code - Preparar confirmación cierre con videos WhatsApp TEST" - }, { "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];" @@ -8118,8 +11510,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3712, - 2064 + -4944, + 1760 ], "id": "b5960269-5fca-4d10-b2ef-eda72eb68b10", "name": "Code - Preparar aviso procesando WhatsApp TEST" @@ -8131,8 +11523,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3936, - 1232 + -4720, + 928 ], "id": "6cee8abe-abb5-4818-ac8c-1f49f0905bcc", "name": "Code - Preparar búsqueda eventos media WhatsApp TEST" @@ -8158,8 +11550,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 4192, - 1232 + -4464, + 928 ], "id": "e49fc551-3ddb-4041-88fd-c4329ef64d54", "name": "Sheets - Leer eventos WhatsApp TEST", @@ -8178,8 +11570,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 4432, - 1232 + -4224, + 928 ], "id": "6c7e217e-6200-46d8-892a-adc9b273e4a7", "name": "Code - Filtrar eventos media de sesión WhatsApp TEST" @@ -8191,8 +11583,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 4672, - 1232 + -3984, + 928 ], "id": "6d74dd89-9530-44d7-ae69-2e771bf8aab9", "name": "Code - Separar media en items WhatsApp TEST" @@ -8222,8 +11614,8 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ - 4880, - 1232 + -3776, + 928 ], "id": "6c936089-6193-479f-b87b-a98e69e4fa92", "name": "HTTP Request - Obtener media base64 Evolution TEST", @@ -8236,8 +11628,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5088, - 1232 + -3568, + 928 ], "id": "73f2d7f0-bc17-47cc-9b7a-f477f942bf63", "name": "Code - Convertir base64 a binario WhatsApp TEST" @@ -8260,8 +11652,8 @@ "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ - 5328, - 1232 + -3328, + 928 ], "id": "c9d49cee-9f7f-4dfe-bf4a-5729a3f64298", "name": "Drive - Subir media WhatsApp TEST", @@ -8279,8 +11671,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5536, - 1232 + -3120, + 928 ], "id": "4bfe263f-272f-4738-8664-4f60990ad359", "name": "Code - Preparar registro media Drive WhatsApp TEST" @@ -8292,8 +11684,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5952, - 1232 + -2704, + 928 ], "id": "8ecc5a7e-5520-49e8-909c-1a59a47f28f9", "name": "Code - Consolidar media recuperada WhatsApp TEST" @@ -8460,8 +11852,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 5744, - 1232 + -2912, + 928 ], "id": "900504c8-6e4d-4ce4-bb36-55739dae8408", "name": "Sheets - Guardar media Drive WhatsApp TEST", @@ -8640,8 +12032,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 6160, - 1232 + -2496, + 928 ], "id": "6f48b6b3-21bb-4f03-af72-68f67c2e0574", "name": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", @@ -8659,8 +12051,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6368, - 1232 + -2288, + 928 ], "id": "6fefc255-e85b-4028-b9e9-b3f71abe1457", "name": "Code - Preparar paquete análisis Gemini WhatsApp TEST" @@ -8749,21 +12141,21 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ - 5408, - 1680 + -3248, + 1376 ], "id": "235d1178-5ecf-4225-b1c5-792512a22877", "name": "Switch - Tipo media para Gemini WhatsApp TEST" }, { "parameters": { - "jsCode": "const input = $input.first();\nconst data = input.json || {};\nconst binary = input.binary || {};\n\nif (!binary.data) {\n throw new Error('No llegó binary.data al nodo de audio para Gemini');\n}\n\nreturn [\n {\n json: {\n ...data,\n gemini_prompt_audio: `Analiza esta nota de voz de una propuesta ejecutada.\n\nExtrae 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\n- Qué se implementó\n- Comentarios o resultados observados\n\nNo inventes datos. Si algo no está claro, indica \"No identificado\".`\n },\n binary\n }\n];" + "jsCode": "const input = $input.first();\nconst data = input.json || {};\nconst binary = input.binary || {};\n\nif (!binary.data) {\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 explícitamente \"ejecutada\", \"ya se ejecutó\", \"se implementó\", \"se realizó\", \"se instaló\" o expresiones equivalentes, clasifica como PROPUESTA_EJECUTADA.\n- Si el manager dice explícitamente \"propuesta externa\", clasifica como PROPUESTA_EXTERNA.\n- Si el manager dice \"externa\", \"fuera del banco\", \"recibida fuera del banco interno\", \"referencia externa\" o expresiones equivalentes, 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 {\n json: {\n ...data,\n gemini_prompt_audio: geminiPromptAudio\n },\n binary\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5888, - 1504 + -2768, + 1200 ], "id": "e3cfe03a-110b-4470-9011-e4705dcde4a7", "name": "Code - Preparar Gemini audio WhatsApp TEST" @@ -8774,9 +12166,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-2.5-flash-lite", + "value": "models/gemini-3.1-flash-lite", "mode": "list", - "cachedResultName": "models/gemini-2.5-flash-lite" + "cachedResultName": "models/gemini-3.1-flash-lite" }, "text": "={{ $json.gemini_prompt_audio }}", "inputType": "binary", @@ -8785,8 +12177,8 @@ "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ - 6096, - 1504 + -2560, + 1200 ], "id": "18ddf06b-efd7-49b0-b4bf-0f68e4c91a20", "name": "Gemini - Analizar audio WhatsApp TEST", @@ -8804,8 +12196,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6304, - 1504 + -2352, + 1200 ], "id": "2587e982-2403-450c-9419-a9de41d3c5a1", "name": "Code - Normalizar análisis audio WhatsApp TEST" @@ -8817,8 +12209,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5904, - 1728 + -2752, + 1424 ], "id": "68e8b0de-373c-48fa-a762-8c0e45d64c15", "name": "Code - Preparar Gemini imagen WhatsApp TEST" @@ -8829,9 +12221,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-2.5-flash-lite", + "value": "models/gemini-3.1-flash-lite", "mode": "list", - "cachedResultName": "models/gemini-2.5-flash-lite" + "cachedResultName": "models/gemini-3.1-flash-lite" }, "text": "={{ $json.gemini_prompt_imagen }}", "inputType": "binary", @@ -8840,8 +12232,8 @@ "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ - 6112, - 1728 + -2544, + 1424 ], "id": "1baf8c8e-95cf-42fe-af4f-fcbc71a557fc", "name": "Gemini - Analizar imagen WhatsApp TEST", @@ -8859,8 +12251,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6320, - 1728 + -2336, + 1424 ], "id": "e1bc474b-82b0-4a4c-9ef7-e5f52b2b7503", "name": "Code - Normalizar análisis imagen WhatsApp TEST" @@ -8872,8 +12264,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 5776, - 1968 + -2880, + 1664 ], "id": "5bfc997b-a538-4d96-8d7d-e4abe8e4cf9f", "name": "Code - Preparar Gemini video WhatsApp TEST" @@ -8884,9 +12276,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-2.5-flash-lite", + "value": "models/gemini-3.1-flash-lite", "mode": "list", - "cachedResultName": "models/gemini-2.5-flash-lite" + "cachedResultName": "models/gemini-3.1-flash-lite" }, "text": "={{ $json.gemini_prompt_video }}", "inputType": "binary", @@ -8895,8 +12287,8 @@ "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ - 5984, - 1968 + -2672, + 1664 ], "id": "5767efd8-bfb6-4221-964d-743b417be1aa", "name": "Analyze video", @@ -8914,8 +12306,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 6192, - 1968 + -2464, + 1664 ], "id": "c035db53-87b9-4fc2-9346-d5228bbac7b7", "name": "Code - Normalizar análisis video WhatsApp TEST" @@ -9052,8 +12444,8 @@ "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ - 6784, - 1680 + -1872, + 1376 ], "id": "55f75826-cb50-4857-b58f-11bce7703b47", "name": "Sheets - Guardar análisis media Gemini WhatsApp TEST", @@ -9063,6 +12455,2125 @@ "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": [ + -1184, + 1360 + ], + "id": "d68f5e66-a1cb-48cc-afbd-07677f3145cb", + "name": "Sheets - Leer análisis media Gemini WhatsApp TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const cierre = $('Code - Consolidar media recuperada WhatsApp TEST').first().json || {};\nconst evento = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\n\nconst sessionId = String(cierre.session_id || evento.session_id || '').trim();\nconst ejecucionId = String(cierre.ejecucion_id || evento.ejecucion_id || sessionId).trim();\n\nconst rows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst extraerTextoAnalisis = (valor) => {\n if (!valor) return '';\n\n if (typeof valor === 'object') {\n return extraerDesdeObjeto(valor);\n }\n\n const texto = String(valor || '').trim();\n\n if (!texto) return '';\n\n try {\n const obj = JSON.parse(texto);\n const extraido = extraerDesdeObjeto(obj);\n if (extraido) return extraido;\n } catch (error) {}\n\n return texto;\n};\n\nconst extraerDesdeObjeto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text.trim();\n if (typeof obj.output === 'string') return obj.output.trim();\n if (typeof obj.response === 'string') return obj.response.trim();\n if (typeof obj.content === 'string') return obj.content.trim();\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 = extraerDesdeObjeto(obj.message);\n if (txt) return txt;\n }\n\n if (obj.data && typeof obj.data === 'object') {\n const txt = extraerDesdeObjeto(obj.data);\n if (txt) return txt;\n }\n\n return '';\n};\n\nconst analisisSesion = rows.filter(row => {\n return String(row.session_id || '').trim() === sessionId;\n});\n\n// Evitar duplicados por pruebas manuales\nconst vistos = new Set();\nconst analisisUnicos = [];\n\nfor (const row of analisisSesion) {\n const key = `${row.media_event_id || ''}_${row.media_type || ''}`;\n\n if (vistos.has(key)) continue;\n\n vistos.add(key);\n\n analisisUnicos.push({\n ...row,\n analisis_texto_limpio: extraerTextoAnalisis(row.analisis_texto)\n });\n}\n\nconst audios = analisisUnicos.filter(r => String(r.media_type || '').toLowerCase() === 'audio');\nconst imagenes = analisisUnicos.filter(r => String(r.media_type || '').toLowerCase() === 'image');\nconst videos = analisisUnicos.filter(r => String(r.media_type || '').toLowerCase() === 'video');\n\nconst textoAudio = audios.map(r => r.analisis_texto_limpio).filter(Boolean).join('\\n\\n---\\n\\n');\nconst textoImagenes = imagenes.map(r => r.analisis_texto_limpio).filter(Boolean).join('\\n\\n---\\n\\n');\nconst textoVideos = videos.map(r => r.analisis_texto_limpio).filter(Boolean).join('\\n\\n---\\n\\n');\n\nconst textoTotal = [textoAudio, textoImagenes, textoVideos].filter(Boolean).join('\\n\\n');\n\nif (!textoTotal) {\n throw new Error(`No hay texto limpio de análisis para la sesión ${sessionId}. Revisa wa_ejecuciones_analisis_media.`);\n}\n\nconst managerNombre =\n limpiar(cierre.manager_nombre) ||\n limpiar(evento.manager_nombre) ||\n 'No identificado';\n\nconst managerTelefono =\n limpiar(cierre.manager_telefono) ||\n limpiar(evento.manager_telefono) ||\n 'No identificado';\n\nconst promptFinal = `\nEres un analista de propuestas de GomezLee Marketing.\n\nCon base en el análisis del audio, imágenes y videos, genera un JSON final estructurado para registrar el reporte.\n\nReglas generales:\n- Devuelve SOLO JSON válido.\n- No uses markdown.\n- No inventes información.\n- Si un dato no está claro, usa \"No identificado\".\n- El audio tiene prioridad para datos declarados por el manager.\n- Las imágenes y videos sirven para validar evidencia visual.\n- Si hay dudas importantes, requiere_revision debe ser \"SI\"; si todo está claro, \"NO\".\n- nivel_confianza debe ser \"ALTA\", \"MEDIA\" o \"BAJA\".\n- resumen_ejecucion debe ser una síntesis útil para un reporte ejecutivo.\n- que_se_implemento debe describir claramente lo ejecutado o reportado.\n\nClasifica también el tipo de reporte.\n\ntipo_reporte debe ser exactamente uno de estos valores:\n- PROPUESTA_EJECUTADA\n- PROPUESTA_EXTERNA\n- NO_DETERMINADO\n\ntipo_reporte_confianza debe ser exactamente uno de estos valores:\n- ALTA\n- MEDIA\n- BAJA\n\ntipo_reporte_motivo debe explicar brevemente por qué se eligió ese tipo.\n\nReglas para tipo_reporte:\n- Si el audio menciona claramente que es una propuesta ejecutada, usa PROPUESTA_EJECUTADA.\n- Si el audio menciona que ya se ejecutó, se implementó, se instaló, se realizó o se activó una propuesta, usa PROPUESTA_EJECUTADA.\n- Si el audio menciona claramente que es una propuesta externa, usa PROPUESTA_EXTERNA.\n- Si el audio menciona algo recibido fuera del banco interno, referencia externa, idea externa, actividad externa o material externo que debe documentarse, usa PROPUESTA_EXTERNA.\n- Si el audio es una prueba, saludo, ruido, información incompleta o no permite clasificar, usa NO_DETERMINADO.\n- No uses las imágenes para inventar el tipo si el audio no lo confirma.\n- El audio tiene prioridad para determinar el tipo de reporte.\n- Si hay duda entre ejecutada y externa, usa NO_DETERMINADO.\n\nEstructura obligatoria:\n\n{\n \"tipo_reporte\": \"\",\n \"tipo_reporte_confianza\": \"\",\n \"tipo_reporte_motivo\": \"\",\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 \"evidencia_audio_resumen\": \"\",\n \"evidencia_imagenes_resumen\": \"\",\n \"evidencia_videos_resumen\": \"\",\n \"nivel_confianza\": \"\",\n \"requiere_revision\": \"\",\n \"motivo_revision\": \"\"\n}\n\nDatos de control:\nsession_id: ${sessionId}\nejecucion_id: ${ejecucionId}\nmanager_nombre: ${managerNombre}\nmanager_telefono: ${managerTelefono}\n\nANÁLISIS DEL AUDIO:\n${textoAudio || 'No hay análisis de audio disponible.'}\n\nANÁLISIS DE IMÁGENES:\n${textoImagenes || 'No hay análisis de imágenes disponible.'}\n\nANÁLISIS DE VIDEOS:\n${textoVideos || 'No hay análisis de videos disponible.'}\n`.trim();\n\nreturn [\n {\n json: {\n ...cierre,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n manager_nombre: managerNombre,\n manager_telefono: managerTelefono,\n canal_origen: cierre.canal_origen || evento.canal_origen || 'WHATSAPP',\n\n total_analisis_media: analisisUnicos.length,\n total_analisis_audio: audios.length,\n total_analisis_imagenes: imagenes.length,\n total_analisis_videos: videos.length,\n\n texto_audio: textoAudio,\n texto_imagenes: textoImagenes,\n texto_videos: textoVideos,\n\n gemini_prompt_final: promptFinal,\n\n estado_procesamiento: 'ANALISIS_MEDIA_CONSOLIDADO'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -976, + 1360 + ], + "id": "2a9d4918-8e52-4633-8369-c4c8397e9129", + "name": "Code - Consolidar análisis media Gemini WhatsApp TEST" + }, + { + "parameters": { + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "messages": { + "values": [ + { + "content": "={{ $json.gemini_prompt_final }}" + } + ] + }, + "builtInTools": {}, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -768, + 1360 + ], + "id": "76e59ce7-1844-40e3-bd7d-1e4101838aad", + "name": "Gemini - Generar JSON final propuesta ejecutada TEST", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "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 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 valor = (campo, fallback = 'No identificado') => {\n const v = limpiar(campo);\n if (!v) return fallback;\n if (v.toLowerCase() === 'undefined') return fallback;\n if (v.toLowerCase() === 'null') return fallback;\n return v;\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 ahora = new Date().toISOString();\n\nconst requiereRevision = valor(parsed.requiere_revision, 'SI').toUpperCase();\n\nconst estadoRevision = requiereRevision === 'SI'\n ? 'PENDIENTE_REVISION'\n : 'ANALISIS_COMPLETADO';\n\nconst evidenciaImagenes = valor(parsed.evidencia_imagenes_resumen, '');\nconst evidenciaVideos = valor(parsed.evidencia_videos_resumen, '');\n\nconst elementosDetectados = [\n evidenciaImagenes,\n evidenciaVideos\n].filter(Boolean).join('\\n\\n');\n\nconst tags = [\n parsed.marca,\n parsed.cliente,\n parsed.pais,\n parsed.ubicacion\n]\n .map(v => valor(v, ''))\n .filter(Boolean)\n .join(', ');\n\nconst tipoReporte = normalizarTipoReporte(parsed.tipo_reporte);\nconst tipoReporteConfianza = normalizarConfianza(parsed.tipo_reporte_confianza);\nconst tipoReporteMotivo = valor(parsed.tipo_reporte_motivo, 'No identificado');\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: valor(parsed.fecha_ejecucion),\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: valor(parsed.propuesta_referencia),\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: valor(parsed.ubicacion),\n\n comentario_original: valor(parsed.evidencia_audio_resumen || base.texto_audio, ''),\n resumen_ia: valor(parsed.resumen_ejecucion, ''),\n descripcion_ejecucion: valor(parsed.que_se_implemento),\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: valor(parsed.evidencia_audio_resumen || base.texto_audio, ''),\n\n nivel_confianza: valor(parsed.nivel_confianza, 'BAJA'),\n requiere_revision: requiereRevision,\n\n gemini_json_raw: texto\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -416, + 1360 + ], + "id": "891ffabf-cb1a-415a-8b29-3facb39f4efa", + "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 }}" + }, + "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 + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 2144, + 1536 + ], + "id": "705689dd-2988-4aa4-835b-3386b5d24e13", + "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": [ + 8800, + 1440 + ], + "id": "49036fad-dbd7-44d8-8597-b098eadf0ed3", + "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": [ + 9088, + 1408 + ], + "id": "914441cf-483f-4aab-a30e-7c619a34e49e", + "name": "Code - Preparar mensaje final WhatsApp TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "7591a797-d76a-48e7-8e75-9a41cff6e974", + "leftValue": "={{ Number($json.media_index || 0) === Number($json.media_total || 0) }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -1600, + 1360 + ], + "id": "9f291944-bbf8-4584-b997-9e842caca9cb", + "name": "IF - Es último análisis media WhatsApp TEST" + }, + { + "parameters": { + "amount": 15 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + -1376, + 1360 + ], + "id": "fef0aeef-418b-419d-a6ca-6a9440494139", + "name": "Wait", + "webhookId": "c6d9670c-1a1f-43c9-89b3-9952b1501850" + }, + { + "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": [ + 384, + 1376 + ], + "id": "7c547eeb-0528-460f-a9a2-d3533e43bb55", + "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": [ + 592, + 1376 + ], + "id": "dc39153f-ddcc-4f21-abc8-7a4b42074807", + "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": [ + 800, + 1376 + ], + "id": "210119ea-e335-4d9d-abd2-b2aa5b0af24a", + "name": "IF - Propuesta final ya existe TEST" + }, + { + "parameters": { + "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '10PZtoMFuiXczGM9Itbcjce4EKBUhiztFbXZiTWX2DKc';\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": [ + 2528, + 1504 + ], + "id": "d7e6d879-f330-4944-a006-aa5735fe0177", + "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": [ + 4448, + 1552 + ], + "id": "f3543da5-0b8b-4de9-a934-8ed6daaa849c", + "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": [ + 4720, + 1552 + ], + "id": "1b0f80c2-16cd-499a-aadf-3f44c3753ce4", + "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": "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": [ + 6480, + 1424 + ], + "id": "cb35219a-46a0-49c2-a404-91c5c91a30d2", + "name": "Sheets - Actualizar link presentación ejecución 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 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 normalizarTipoReporte = (valor) => {\n const texto = limpiarTexto(valor, '')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, '_');\n\n if (\n texto === 'PROPUESTA_EXTERNA' ||\n texto === 'EXTERNA' ||\n texto === 'REPORTE_EXTERNO'\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n if (\n texto === 'PROPUESTA_EJECUTADA' ||\n texto === 'EJECUTADA' ||\n texto === 'REPORTE_EJECUTADO'\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n return 'PROPUESTA_EJECUTADA';\n};\n\nconst tipoReporte = normalizarTipoReporte(\n data.tipo_reporte ||\n data.tipo_reporte_detectado ||\n data.reporte_tipo ||\n data.clasificacion_reporte ||\n ''\n);\n\nconst textoTipoReporte = tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'Propuesta Externa'\n : 'Propuesta Ejecutada';\n\nconst tituloReporteTipo = tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'REPORTE DE PROPUESTA EXTERNA'\n : 'REPORTE DE PROPUESTA EJECUTADA';\n\nconst encabezadoTipoReporte = `Fulgencio Fumado · ${textoTipoReporte}`;\n\nconst replacements = {\n '{{TITULO_REPORTE_TIPO}}': tituloReporteTipo,\n '{{ENCABEZADO_TIPO_REPORTE}}': encabezadoTipoReporte,\n '{{TIPO_REPORTE}}': textoTipoReporte,\n\n '{{MARCA}}': limpiarTexto(data.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente),\n '{{PAIS}}': limpiarTexto(data.pais),\n '{{UBICACION}}': limpiarTexto(data.ubicacion),\n\n '{{FECHA_EJECUCION}}': limpiarTexto(data.fecha_ejecucion),\n '{{PROPUESTA_REFERENCIA}}': limpiarTexto(data.propuesta_referencia),\n\n // Se dejan en blanco para no mostrar IDs técnicos en la presentación.\n // Los labels fijos \"ID de Ejecución\" / \"Ejecución ID\" deben eliminarse en la plantilla.\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(data.media_folder_url, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(data.presentacion_ejecucion_url, '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\nif (!data.presentation_id) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nreturn [\n {\n json: {\n ...data,\n\n tipo_reporte_normalizado: tipoReporte,\n tipo_reporte_texto: textoTipoReporte,\n titulo_reporte_tipo: tituloReporteTipo,\n encabezado_tipo_reporte: encabezadoTipoReporte,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 4928, + 1552 + ], + "id": "72e00ab4-ed9e-4852-a89f-fdef9ba6551d", + "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": [ + 5616, + 1536 + ], + "id": "447d49f8-bac0-4c12-bbd0-89dbd2a43b2c", + "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": [ + 6224, + 1536 + ], + "id": "9c252b51-c657-4020-bae8-fd79824feb32", + "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": [ + -5440, + -1792 + ], + "id": "be56aeb8-916d-46b2-b2a2-b544a4bdbd01", + "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": [ + -5392, + -16 + ], + "id": "cc41848d-df63-499a-ba01-16155e30fffe", + "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": [ + 2736, + 1504 + ], + "id": "3a7cf341-e7e6-4d90-bdd0-9e1abde12b10", + "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": [ + 2944, + 1504 + ], + "id": "ba2e9788-c593-4097-96be-e7f6d447be69", + "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": [ + 3152, + 1504 + ], + "id": "3861110f-3334-4e89-954f-b8d86f9b4708", + "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": [ + 3360, + 1504 + ], + "id": "1ccc2d93-f72d-453a-a1b8-7a204d5a5108", + "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": [ + 3568, + 1504 + ], + "id": "fa4bc0cf-498d-4332-8d12-72f4eb7e5ce4", + "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": [ + 3776, + 1504 + ], + "id": "2b0b6839-7d9b-4dc1-9969-a5b3b015653e", + "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, 6);\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": [ + 5824, + 1536 + ], + "id": "2606aa33-73fd-4af3-9497-cea6932760c9", + "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": [ + 6032, + 1536 + ], + "id": "4ed89dd2-2849-419e-95e4-e061939e0002", + "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": [ + -5936, + 688 + ], + "id": "fdbbc5d5-a162-4bfa-ab41-47f4087dba08", + "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": [ + -5520, + 688 + ], + "id": "78fdf65a-3ff9-4219-b22f-9088baa65aeb", + "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": [ + -5744, + 688 + ], + "id": "eeb80d93-128f-488e-814e-86170b8049f7", + "name": "HTTP - Enviar aviso procesando WhatsApp TEST" + }, + { + "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": [ + -208, + 1360 + ], + "id": "0eb5f707-d1a2-4147-a195-a88ed9b5903e", + "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": [ + 0, + 1360 + ], + "id": "2043ba80-2789-4cc7-a261-943c464dd2d6", + "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 jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio 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 ...actual,\n ...slidesNormalizado1,\n ...slidesNormalizado2,\n ...slidesNormalizado3,\n ...match\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\nconst tipoReporte = limpiar(\n match.tipo_reporte ||\n data.tipo_reporte ||\n actual.tipo_reporte ||\n ''\n).toUpperCase();\n\nlet matchEstado = limpiar(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n actual.propuesta_match_estado ||\n ''\n).toUpperCase();\n\nlet matchConfianza = Number(\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 ''\n);\n\nconst presentationId = limpiar(\n data.presentation_id ||\n actual.presentation_id ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\n// Fallback defensivo por si el estado quedó viejo.\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 }\n}\n\n// Formato limpio para el banco original:\n// Solo links, uno por línea.\nlet enlacesActualizados = enlacesExistentes;\n\nif (presentationUrl && !enlacesExistentes.includes(presentationUrl)) {\n enlacesActualizados = enlacesExistentes\n ? `${enlacesExistentes}\\n${presentationUrl}`\n : presentationUrl;\n}\n\nconst motivos = [];\n\nif (tipoReporte !== 'PROPUESTA_EJECUTADA') {\n motivos.push(`tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}`);\n}\n\nif (matchEstado !== 'MATCH_ALTA_CONFIANZA') {\n motivos.push(`propuesta_match_estado es ${matchEstado || 'SIN_MATCH'}`);\n}\n\nif (matchConfianza < 85) {\n motivos.push(`propuesta_match_confianza es ${matchConfianza}`);\n}\n\nif (!presentationUrl) {\n motivos.push('no existe presentacion_ejecucion_url');\n}\n\nif (!nombreBanco) {\n motivos.push('no llegó propuesta_nombre_banco');\n}\n\nif (!rowNumberBanco) {\n motivos.push('no llegó propuesta_banco_row_number');\n}\n\nif (presentationUrl && enlacesExistentes.includes(presentationUrl)) {\n motivos.push('el link ya existe en la columna de enlaces ejecutados');\n}\n\nconst actualizarBanco = motivos.length === 0;\n\nconst motivoNoActualizaBanco = actualizarBanco\n ? ''\n : `No se actualiza banco porque ${motivos.join('; ')}.`;\n\nreturn [\n {\n json: {\n ...data,\n\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\n banco_match_column: 'row_number',\n banco_match_value: rowNumberBanco,\n\n row_number: rowNumberBanco,\n 'Enlaces a propuestas ejecutadas': enlacesActualizados,\n\n banco_enlaces_existentes: enlacesExistentes,\n banco_enlaces_actualizados: enlacesActualizados,\n banco_presentacion_url: presentationUrl,\n\n banco_update_debug: {\n tipo_reporte: tipoReporte,\n propuesta_match_estado: matchEstado,\n propuesta_match_confianza: matchConfianza,\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n presentationUrl,\n enlacesExistentes,\n enlacesActualizados,\n actualizarBanco,\n motivoNoActualizaBanco,\n mejor_match: mejorMatch,\n match_debug: matchDebug\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6928, + 1232 + ], + "id": "3dd8a8af-4902-4a0f-92b5-94e921262ae4", + "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": [ + 7696, + 1280 + ], + "id": "92a6348b-03df-4edc-b1e7-994621c331b7", + "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": [ + 8208, + 1072 + ], + "id": "7c0dc504-1094-461d-82d6-469db24200b9", + "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": [ + 8416, + 1072 + ], + "id": "f9965d96-e94f-4a44-8604-b098fe9a08b9", + "name": "Code - Restaurar contexto actualización banco TEST" } ], "connections": { @@ -9144,7 +14655,7 @@ ], [ { - "node": "ode - Preparar sin sesión para cancelar WhatsApp TEST", + "node": "Code - Preparar sin sesión para cancelar WhatsApp TEST", "type": "main", "index": 0 } @@ -9226,7 +14737,7 @@ ], [ { - "node": "Preparar actualización fotos listas", + "node": "Sheets - Leer eventos sesión fotos WhatsApp TEST", "type": "main", "index": 0 } @@ -9273,6 +14784,7 @@ "index": 0 } ], + [], [] ] }, @@ -9375,17 +14887,6 @@ ] ] }, - "ode - Preparar sin sesión para cancelar WhatsApp TEST": { - "main": [ - [ - { - "node": "WhatsApp - Enviar mensaje TEST", - "type": "main", - "index": 0 - } - ] - ] - }, "Code — Preparar actualización imagen recibida": { "main": [ [ @@ -9500,23 +15001,7 @@ "main": [ [ { - "node": "Code - Preparar confirmación cierre sin video WhatsApp TEST", - "type": "main", - "index": 0 - }, - { - "node": "Code - Preparar búsqueda eventos media WhatsApp TEST", - "type": "main", - "index": 0 - } - ] - ] - }, - "Code - Preparar confirmación cierre sin video WhatsApp TEST": { - "main": [ - [ - { - "node": "WhatsApp - Enviar mensaje TEST", + "node": "Code - Preparar aviso procesando WhatsApp TEST1", "type": "main", "index": 0 } @@ -9582,23 +15067,7 @@ "main": [ [ { - "node": "Code - Preparar confirmación cierre con videos WhatsApp TEST", - "type": "main", - "index": 0 - }, - { - "node": "Code - Preparar búsqueda eventos media WhatsApp TEST", - "type": "main", - "index": 0 - } - ] - ] - }, - "Code - Preparar confirmación cierre con videos WhatsApp TEST": { - "main": [ - [ - { - "node": "WhatsApp - Enviar mensaje TEST", + "node": "Code - Preparar aviso procesando WhatsApp TEST1", "type": "main", "index": 0 } @@ -9865,18 +15334,455 @@ } ] ] + }, + "Sheets - Guardar análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "IF - Es último análisis media WhatsApp 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 + } + ] + ] + }, + "IF - Es último análisis media WhatsApp TEST": { + "main": [ + [ + { + "node": "Wait", + "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": "Sheets - Actualizar link presentación ejecución 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": "Sheets - Leer propuestas ejecutadas final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización banco propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "IF - Actualizar banco match alta confianza 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 + } + ] + ] } }, "authors": "Isaac Aracena", - "name": "Version f3b250eb", + "name": "Version 94d48ce3", "description": "", "autosaved": true, "workflowPublishHistory": [ { - "createdAt": "2026-06-15T13:19:24.505Z", - "id": 1393, + "createdAt": "2026-06-20T16:00:09.191Z", + "id": 1680, "workflowId": "sXH75CXIQ0sEzm1Y", - "versionId": "f3b250eb-aec9-4861-b8aa-319f56ae5e72", + "versionId": "94d48ce3-27cf-4be2-8d30-467fb212e28b", "event": "activated", "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" }