{ "name": "Chat de WhatsApp de Propuestas Ejecutadas - API Oficial", "nodes": [ { "parameters": { "method": "POST", "url": "https://graph.facebook.com/v26.0/1151359118067218/messages", "authentication": "predefinedCredentialType", "nodeCredentialType": "whatsAppApi", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ (() => {\n const text = (value) => String(value ?? '');\n\n const cleanMultiline = (value) =>\n text(value)\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\\\r\\\\n/g, '\\n')\n .replace(/\\\\n/g, '\\n')\n .replace(/[ \\t]+\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n\n const cleanSingleLine = (value) =>\n cleanMultiline(value)\n .replace(/\\s*\\n\\s*/g, ' ')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n\n const digits = (value) => text(value).replace(/\\D/g, '');\n\n const destination = digits(\n $json.whatsapp_to ||\n $json.sender_phone ||\n $json.manager_telefono ||\n $json.whatsapp_remote_jid ||\n ''\n );\n\n if (!destination) {\n throw new Error('No se encontró el número privado de destino de WhatsApp.');\n }\n\n const bodyText = cleanMultiline(\n $json.whatsapp_text ||\n $json.texto_respuesta ||\n $json.mensaje ||\n $json.text ||\n $json.message_text ||\n ''\n );\n\n const headerText = cleanSingleLine(\n $json.whatsapp_header || ''\n ).slice(0, 60);\n\n const footerText = cleanSingleLine(\n $json.whatsapp_footer ||\n 'GomezLee Marketing · Fulgencio'\n ).slice(0, 60);\n\n const buttons = Array.isArray($json.whatsapp_buttons)\n ? $json.whatsapp_buttons\n .map((button, index) => ({\n id: cleanSingleLine(\n button?.id || `OPTION_${index + 1}`\n ).slice(0, 256),\n title: cleanSingleLine(\n button?.title || `Opción ${index + 1}`\n ).slice(0, 20)\n }))\n .filter((button) => button.id && button.title)\n .slice(0, 3)\n : [];\n\n if (buttons.length > 0) {\n const interactive = {\n type: 'button',\n body: {\n text: (bodyText || 'Selecciona una opción:').slice(0, 1024)\n },\n action: {\n buttons: buttons.map((button) => ({\n type: 'reply',\n reply: {\n id: button.id,\n title: button.title\n }\n }))\n }\n };\n\n if (headerText) {\n interactive.header = {\n type: 'text',\n text: headerText\n };\n }\n\n if (footerText) {\n interactive.footer = {\n text: footerText\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: destination,\n type: 'interactive',\n interactive\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: destination,\n type: 'text',\n text: {\n preview_url: true,\n body: (bodyText || 'Mensaje sin contenido').slice(0, 4096)\n }\n };\n})() }}", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 71280, 31760 ], "id": "0d1e3e42-3f9a-4e16-b847-faa6aacbb503", "name": "WhatsApp - Enviar mensaje API Oficial", "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 5000, "credentials": { "whatsAppApi": { "id": "t14kVayc9FurLReq", "name": "WhatsApp API - GLM CDC" } } }, { "parameters": { "jsCode": "const input = $json || {};\n\n// --------------------------------------------------\n// 1. Resolver payload de WhatsApp Cloud API\n// --------------------------------------------------\nconst entryValue =\n input.entry?.[0]?.changes?.[0]?.value ||\n input.value ||\n input.body?.entry?.[0]?.changes?.[0]?.value ||\n input.body?.value ||\n input;\n\nconst metadata = entryValue.metadata || input.metadata || {};\nconst contacts = entryValue.contacts || input.contacts || [];\nconst messages = entryValue.messages || input.messages || [];\nconst message = messages[0] || {};\n\nconst clean = (value) => String(value ?? '').trim();\nconst digits = (value) => clean(value).replace(/\\D/g, '');\n\nconst normalizeText = (value) =>\n clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst contact = contacts[0] || {};\nconst profileName =\n clean(contact.profile?.name) ||\n clean(message.profile?.name) ||\n 'Usuario WhatsApp';\n\n// Groups API entrega group_id en el mensaje.\n// En conversación individual, \"from\" es el número del usuario.\nconst senderPhone = digits(\n message.from ||\n entryValue.from ||\n input.from ||\n contact.wa_id ||\n ''\n);\n\nconst groupId = clean(\n message.group_id ||\n message.groupId ||\n entryValue.group_id ||\n entryValue.groupId ||\n input.group_id ||\n input.groupId ||\n ''\n);\n\nconst isGroup = Boolean(groupId);\nconst recipientType = isGroup ? 'group' : 'individual';\nconst whatsappTo = isGroup ? groupId : senderPhone;\n\n// --------------------------------------------------\n// 2. Extraer texto, botones y listas\n// --------------------------------------------------\nconst interactiveText =\n clean(message.interactive?.button_reply?.id) ||\n clean(message.interactive?.button_reply?.title) ||\n clean(message.interactive?.list_reply?.id) ||\n clean(message.interactive?.list_reply?.title) ||\n clean(message.button?.payload) ||\n clean(message.button?.text) ||\n '';\n\nconst textMessage =\n interactiveText ||\n clean(message.text?.body) ||\n clean(message.caption) ||\n clean(entryValue.text?.body) ||\n clean(input.text?.body) ||\n clean(input.text) ||\n '';\n\n// --------------------------------------------------\n// 3. Detectar media oficial\n// --------------------------------------------------\nconst audio = message.audio || null;\nconst image = message.image || null;\nconst video = message.video || null;\nconst document = message.document || null;\nconst sticker = message.sticker || null;\n\nlet messageType = clean(message.type).toLowerCase();\n\nif (!messageType) {\n if (audio) messageType = 'audio';\n else if (image) messageType = 'image';\n else if (video) messageType = 'video';\n else if (document) messageType = 'document';\n else if (sticker) messageType = 'sticker';\n else if (textMessage) messageType = 'text';\n else messageType = 'unknown';\n}\n\nconst mediaObject =\n audio ||\n image ||\n video ||\n document ||\n sticker ||\n null;\n\nconst hasMedia = Boolean(mediaObject?.id);\nconst mediaSourceId = clean(mediaObject?.id);\nconst mediaMimeType = clean(mediaObject?.mime_type);\nconst mediaFileName =\n clean(document?.filename) ||\n `${messageType}_${mediaSourceId || Date.now()}`;\n\n// --------------------------------------------------\n// 4. Comandos oficiales\n// --------------------------------------------------\nconst normalizeCommand = (value) => {\n const t = normalizeText(value);\n\n if (t === 'HEY') return 'START';\n if (t === 'CANCELAR' || t === 'CANCEL') return 'CANCELAR';\n\n if (\n t === 'FOTOS LISTAS' ||\n t === 'FOTOS_LISTAS' ||\n t === 'PHOTOS READY' ||\n t === 'PHOTOS_READY'\n ) return 'FOTOS_LISTAS';\n\n if (\n t === 'SIN VIDEO' ||\n t === 'SIN_VIDEO' ||\n t === 'NO VIDEO' ||\n t === 'NO_VIDEO'\n ) return 'SIN_VIDEO';\n\n if (t === 'LISTO' || t === 'DONE') return 'LISTO';\n\n // Único comando oficial del leaderboard.\n if (t === 'RANKING') return 'LEADERBOARD';\n\n return 'NORMAL';\n};\n\n// --------------------------------------------------\n// 5. País e idioma por teléfono\n// --------------------------------------------------\nconst detectCountry = (phone) => {\n const n = digits(phone);\n\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 if (n.startsWith('57')) return { country_code: '57', pais_detectado: 'Colombia', idioma_flujo: 'ES' };\n if (n.startsWith('52')) return { country_code: '52', pais_detectado: 'México', idioma_flujo: 'ES' };\n if (n.startsWith('58')) return { country_code: '58', pais_detectado: 'Venezuela', idioma_flujo: 'ES' };\n\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 if (n.startsWith('1876')) {\n return { country_code: '1', pais_detectado: 'Jamaica', idioma_flujo: 'EN' };\n }\n\n if (n.startsWith('1868')) {\n return { country_code: '1', pais_detectado: 'Trinidad y Tobago', idioma_flujo: 'EN' };\n }\n\n if (n.startsWith('1787') || n.startsWith('1939')) {\n return { country_code: '1', pais_detectado: 'Puerto Rico', idioma_flujo: 'ES' };\n }\n\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 countryInfo = detectCountry(senderPhone);\n\nreturn [\n {\n json: {\n event_id: clean(message.id || input.id || Date.now()),\n fecha_recepcion: new Date(\n Number(message.timestamp || 0) > 0\n ? Number(message.timestamp) * 1000\n : Date.now()\n ).toISOString(),\n\n canal_origen: isGroup ? 'WHATSAPP_OFICIAL_GRUPO' : 'WHATSAPP_OFICIAL_PRIVADO',\n api_origen: 'META_WHATSAPP_CLOUD_API',\n\n manager_telefono: senderPhone,\n manager_nombre: profileName,\n usuario_id_origen: senderPhone,\n\n is_group: isGroup,\n group_jid: groupId,\n group_id: groupId,\n group_name: clean(entryValue.group_name || input.group_name || ''),\n sender_jid: senderPhone,\n sender_phone: senderPhone,\n sender_name: profileName,\n\n whatsapp_remote_jid: whatsappTo,\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: recipientType,\n\n from_me: false,\n phone_number_id: clean(metadata.phone_number_id),\n display_phone_number: clean(metadata.display_phone_number),\n\n message_type: messageType,\n texto: textMessage,\n accion_flujo: normalizeCommand(textMessage),\n\n tiene_media: hasMedia,\n media_count: hasMedia ? 1 : 0,\n media_source_id: mediaSourceId,\n media_mime_type: mediaMimeType,\n media_file_name: mediaFileName,\n\n country_code: countryInfo.country_code,\n pais_detectado: countryInfo.pais_detectado,\n idioma_flujo: countryInfo.idioma_flujo,\n\n raw_preview: JSON.stringify(entryValue).slice(0, 4000),\n raw_event: entryValue,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 46736, 29040 ], "id": "1d30f3a0-94f3-4510-a16e-94faa8109103", "name": "Code - Normalizar evento WhatsApp TEST" }, { "parameters": { "updates": [ "messages" ], "options": {} }, "type": "n8n-nodes-base.whatsAppTrigger", "typeVersion": 1, "position": [ 46528, 29040 ], "id": "c052dcd0-876a-4ea0-b1d5-fa6b50b36e4a", "name": "WhatsApp Trigger - API Oficial", "webhookId": "f7699eff-45d9-4f7d-831a-78280c1b6ac3", "credentials": { "whatsAppTriggerApi": { "id": "gTh5IGhZ1rhxTgAY", "name": "WhatsApp API - GLM CDC" } } }, { "parameters": { "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 47920, 28992 ], "id": "d3b8ca0e-a551-4596-a92b-0ef06097cac9", "name": "Sheets - Leer sesiones existentes", "alwaysOutputData": true, "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const evento = $('Code - Validar chat privado WhatsApp TEST').first().json || {};\nconst sesiones = $input.all().map((item) => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) =>\n texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst esVerdadero = (valor) =>\n ['TRUE', 'SI', 'SÍ', 'YES', '1'].includes(normalizar(valor));\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(\n evento.manager_telefono ||\n evento.sender_phone ||\n evento.whatsapp_to\n).replace(/\\D/g, '');\n\nconst senderPhone = managerTelefono;\nconst accion = texto(evento.accion_flujo || 'NORMAL');\n\nconst idiomaFlujo =\n normalizar(evento.idioma_flujo || 'ES') === 'EN'\n ? 'EN'\n : 'ES';\n\nif (!managerTelefono) {\n throw new Error('No llegó el número del usuario para resolver la sesión privada.');\n}\n\n// Solo reutiliza sesiones privadas. Esto evita tomar una sesión histórica\n// creada cuando el workflow operaba dentro de un grupo.\nconst sesionActiva = sesiones\n .filter((row) => {\n const rowManager = texto(\n row.manager_telefono ||\n row.sender_phone\n ).replace(/\\D/g, '');\n\n const rowGroup = texto(row.group_jid || row.group_id);\n const rowIsGroup = esVerdadero(row.is_group) || Boolean(rowGroup);\n\n return rowManager === managerTelefono && !rowIsGroup;\n })\n .filter((row) => {\n const estado = normalizar(row.estado);\n const etapa = normalizar(row.etapa);\n\n return estadosAbiertos.has(estado) || etapasAbiertas.has(etapa);\n })\n .sort((a, b) => {\n return (\n new Date(b.ultima_actividad || b.fecha_inicio || 0) -\n new Date(a.ultima_actividad || a.fecha_inicio || 0)\n );\n })[0] || null;\n\nconst suffix = Math.random().toString(36).slice(2, 6).toUpperCase();\nconst phoneSuffix = senderPhone.slice(-4) || '0000';\nconst sessionId = `WA_${Date.now()}_${phoneSuffix}_${suffix}`;\n\nlet decision = '';\n\nif (accion === 'LEADERBOARD') {\n decision = 'MOSTRAR_LEADERBOARD';\n} else if (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 is_group: false,\n group_id: '',\n group_jid: '',\n group_name: '',\n whatsapp_to: managerTelefono,\n whatsapp_remote_jid: managerTelefono,\n whatsapp_recipient_type: 'individual',\n\n idioma_flujo: idiomaFlujo,\n\n sesion_activa_encontrada: Boolean(sesionActiva),\n sesion_activa: sesionActiva,\n\n session_id: sesionActiva\n ? texto(sesionActiva.session_id)\n : sessionId,\n\n ejecucion_id: sesionActiva\n ? texto(sesionActiva.ejecucion_id || sesionActiva.session_id)\n : 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_OFICIAL_PRIVADO',\n fecha_inicio: new Date().toISOString(),\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_AUDIO',\n\n audio_count: 0,\n imagenes_count: 0,\n videos_count: 0,\n\n estado: 'ACTIVA',\n ejecucion_id: sessionId,\n motivo_revision: '',\n\n is_group: false,\n group_jid: '',\n group_name: '',\n sender_jid: senderPhone,\n sender_phone: senderPhone,\n sender_name: texto(evento.sender_name),\n\n country_code: texto(evento.country_code),\n pais_detectado: texto(evento.pais_detectado),\n idioma_flujo: idiomaFlujo,\n\n tipo_reporte: ''\n },\n\n resolver_sesion_debug: {\n modo: 'CHAT_PRIVADO_API_OFICIAL',\n idioma_final_usado: idiomaFlujo,\n pais_detectado: texto(evento.pais_detectado),\n country_code: texto(evento.country_code),\n manager_telefono: managerTelefono,\n accion,\n decision_flujo: decision,\n total_sesiones_leidas: sesiones.length,\n sesion_privada_activa_encontrada: Boolean(sesionActiva)\n }\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 48112, 28992 ], "id": "445c260e-28e4-4df1-878e-3e3389d272e2", "name": "Code - Resolver sesión WhatsApp TEST" }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "leftValue": "={{ $json.decision_flujo === 'CREAR_SESION' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true }, "id": "acec6506-977a-412d-b08a-fbcc8de4b0b8" } ], "combinator": "and" }, "renameOutput": true, "outputKey": "CREAR_SESION" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "5c9dcfa5-9b15-428c-ad80-c277b0671658", "leftValue": "={{ $json.decision_flujo === 'AVISO_SESION_ACTIVA' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "AVISO_SESION_ACTIVA" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "966402b5-4517-45c2-8a87-0a7aa4e5e682", "leftValue": "={{ $json.decision_flujo === 'SIN_SESION_ACTIVA' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "SIN_SESION_ACTIVA" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "698f3da3-29d2-472f-b625-af387891035e", "leftValue": "={{ $json.decision_flujo === 'CANCELAR_SESION' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "CANCELAR_SESION" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "d5feac06-4493-494e-ac1e-36c007f0de79", "leftValue": "={{ $json.decision_flujo === 'SIN_SESION_PARA_CANCELAR' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "SIN_SESION_PARA_CANCELAR" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "950979b7-b930-4260-a392-52277a742e43", "leftValue": "={{ $json.decision_flujo === 'CONTINUAR_SESION' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "CONTINUAR_SESION" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "ad9d6c34-b481-4d43-b680-26cbd0579601", "leftValue": "={{ $json.decision_flujo === 'MOSTRAR_LEADERBOARD' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "MOSTRAR_LEADERBOARD" } ] }, "options": {} }, "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ 48336, 28928 ], "id": "6d6ba7a9-452f-4ec0-8f23-cf5fadebfa71", "name": "Switch - Decisión sesión WhatsApp TEST" }, { "parameters": { "jsCode": "const datos = $('Code - Resolver sesión WhatsApp TEST').first().json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.nueva_sesion?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst nombre =\n datos.manager_nombre ||\n datos.sender_name ||\n 'equipo';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n `Hello, *${nombre}*.`,\n '',\n '🎙️ *Current step: 1 of 3 — Voice note*',\n 'Send one voice note and clearly state whether the proposal is:',\n '',\n '1️⃣ *Executed Proposal* — already implemented.',\n '2️⃣ *External Proposal* — received outside the internal bank.',\n '',\n 'Include proposal/reference, brand/client, country, location, date, implementation and results.',\n '',\n '🖼️ Step 2: required images',\n '🎥 Step 3: optional videos',\n '',\n '🏆 Select *View ranking* to see the country leaderboard.',\n '',\n '⚠️ One proposal at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n `Hola, *${nombre}*.`,\n '',\n '🎙️ *Paso actual: 1 de 3 — Nota de voz*',\n 'Envía una nota de voz e indica claramente si la propuesta es:',\n '',\n '1️⃣ *Propuesta Ejecutada* — propuesta interna que ya fue implementada.',\n '2️⃣ *Propuesta Externa* — propuesta que no pertenece a la empresa pero que puede servir de inspiración en futuros proyectos.',\n '',\n '*Debes mencionar en la nota de voz:*',\n'',\n'- Nombre de la propuesta',\n'- Cliente',\n'- Marca',\n'- País',\n'- Lugar',\n'- Fecha de la ejecución',\n'- Detalles adicionales',\n\n '',\n '🖼️ Paso 2: imágenes obligatorias',\n '🎥 Paso 3: videos opcionales',\n '',\n '🏆 Selecciona *Ver ranking* si desea ver el leaderboard por país.',\n '',\n '⚠️ Una sola propuesta a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to:\n datos.whatsapp_to ||\n datos.group_id ||\n datos.group_jid ||\n datos.whatsapp_remote_jid,\n whatsapp_recipient_type:\n datos.whatsapp_recipient_type ||\n (datos.is_group ? 'group' : 'individual'),\n whatsapp_header: idioma === 'EN' ? 'GLM Evidence Registry' : 'Registro de Evidencias GLM',\n whatsapp_footer: 'GomezLee Marketing · Fulgencio',\n whatsapp_text: mensaje,\n whatsapp_buttons: [\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' }\n ]\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51088, 25456 ], "id": "77a5874c-054e-4262-aea0-d634d5bfc2c7", "name": "Code - Preparar bienvenida WhatsApp TEST" }, { "parameters": { "jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst etapa = String(datos.sesion_activa?.etapa || '').trim() || 'pendiente';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '⚠️ You already have an active proposal report in progress.',\n '',\n `Current step: ${etapa}`,\n '',\n 'Please complete that report before starting a new one.',\n '',\n 'To cancel it, use the cancel option.',\n ].join('\\n');\n} else {\n mensaje = [\n '⚠️ Ya tienes un reporte de propuesta en proceso.',\n '',\n `Etapa actual: ${etapa}`,\n '',\n 'Completa ese reporte antes de iniciar uno nuevo.',\n '',\n 'Para cancelarlo, usa la opción de cancelar.',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.whatsapp_remote_jid,\n\n whatsapp_header: idioma === 'EN' ? \"Active report\" : \"Reporte activo\",\n whatsapp_buttons: [\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51120, 25904 ], "id": "9c674091-bd91-47f6-9da9-e394b3d8e728", "name": "Code - Preparar aviso sesión activa WhatsApp TEST" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.nueva_sesion.session_id }}", "manager_telefono": "={{ $json.nueva_sesion.manager_telefono }}", "manager_nombre": "={{ $json.nueva_sesion.manager_nombre }}", "canal_origen": "={{ $json.nueva_sesion.canal_origen }}", "fecha_inicio": "={{ $json.nueva_sesion.fecha_inicio }}", "ultima_actividad": "={{ $json.nueva_sesion.ultima_actividad }}", "etapa": "={{ $json.nueva_sesion.etapa }}", "audio_count": "={{ $json.nueva_sesion.audio_count }}", "imagenes_count": "={{ $json.nueva_sesion.imagenes_count }}", "videos_count": "={{ $json.nueva_sesion.videos_count }}", "estado": "={{ $json.nueva_sesion.estado }}", "ejecucion_id": "={{ $json.nueva_sesion.ejecucion_id }}", "motivo_revision": "={{ $json.nueva_sesion.motivo_revision }}", "is_group": "={{ $json.nueva_sesion.is_group }}", "group_jid": "={{ $json.nueva_sesion.group_jid }}", "group_name": "={{ $json.nueva_sesion.group_name }}", "sender_jid": "={{ $json.nueva_sesion.sender_jid }}", "sender_phone": "={{ $json.nueva_sesion.sender_phone }}", "sender_name": "={{ $json.nueva_sesion.sender_name }}", "country_code": "={{ $json.nueva_sesion.country_code }}", "pais_detectado": "={{ $json.nueva_sesion.pais_detectado }}", "idioma_flujo": "={{ $json.nueva_sesion.idioma_flujo }}", "tipo_reporte": "={{ $json.nueva_sesion.tipo_reporte }}" }, "matchingColumns": [], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_inicio", "displayName": "fecha_inicio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ultima_actividad", "displayName": "ultima_actividad", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa", "displayName": "etapa", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audio_count", "displayName": "audio_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "imagenes_count", "displayName": "imagenes_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "is_group", "displayName": "is_group", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_jid", "displayName": "group_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_name", "displayName": "group_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_jid", "displayName": "sender_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_phone", "displayName": "sender_phone", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_name", "displayName": "sender_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "country_code", "displayName": "country_code", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "pais_detectado", "displayName": "pais_detectado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "idioma_flujo", "displayName": "idioma_flujo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 50880, 25456 ], "id": "e1fda6f9-7fcc-478c-bd94-e56aa5253562", "name": "Sheets - Crear sesión WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const datos = $json || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst etapa = texto(datos.sesion_activa?.etapa).toUpperCase();\nconst tipoMensaje = texto(datos.message_type).toLowerCase();\nconst accion = texto(datos.accion_flujo).toUpperCase();\n\nconst imagenesCount = Number(datos.sesion_activa?.imagenes_count || 0);\nconst videosCount = Number(datos.sesion_activa?.videos_count || 0);\n\nlet paso_decision = 'NO_DEFINIDO';\n\n// --------------------------------------------------\n// 1. Cancelación global\n// --------------------------------------------------\nif (accion === 'CANCELAR') {\n paso_decision = 'CANCELAR_SESION';\n\n// --------------------------------------------------\n// 2. Paso 1: esperando nota de voz\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_AUDIO') {\n if (tipoMensaje === 'audio') {\n paso_decision = 'GUARDAR_AUDIO';\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_AUDIO';\n } else {\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 3. Paso 2: esperando imágenes obligatorias\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_IMAGENES') {\n if (tipoMensaje === 'image') {\n paso_decision = 'GUARDAR_IMAGEN';\n\n } else if (accion === 'FOTOS_LISTAS') {\n // La validación real se hace después leyendo wa_ejecuciones_eventos.\n paso_decision = 'FOTOS_LISTAS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_IMAGENES';\n\n } else {\n // Si WhatsApp manda eventos raros durante imágenes, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 4. Paso 3: esperando videos opcionales\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_VIDEOS') {\n if (tipoMensaje === 'video') {\n paso_decision = 'GUARDAR_VIDEO';\n\n } else if (accion === 'SIN_VIDEO') {\n paso_decision = 'CERRAR_SIN_VIDEO';\n\n } else if (accion === 'LISTO') {\n paso_decision = 'CERRAR_CON_VIDEOS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_VIDEOS';\n\n } else {\n // Si WhatsApp manda eventos raros durante videos, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 5. Sesión en procesamiento\n// --------------------------------------------------\n} else if (etapa === 'PROCESANDO') {\n paso_decision = 'AVISO_PROCESANDO';\n\n// --------------------------------------------------\n// 6. Etapa no reconocida\n// --------------------------------------------------\n} else {\n paso_decision = 'ETAPA_NO_RECONOCIDA';\n}\n\nreturn [\n {\n json: {\n ...datos,\n\n etapa_actual: etapa,\n tipo_mensaje_actual: tipoMensaje,\n accion_actual: accion,\n\n imagenes_count_actual: imagenesCount,\n videos_count_actual: videosCount,\n\n paso_decision,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 48640, 29488 ], "id": "534d0221-5bf9-42fc-85ce-ccf79c03cd8c", "name": "Code - Resolver paso activo WhatsApp TEST" }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true }, "id": "c7fef2d7-50ce-467a-b125-30ae7fba3a58" } ], "combinator": "and" }, "renameOutput": true, "outputKey": "GUARDAR_AUDIO" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "8da042cf-54e6-460f-b505-2b17c4f4ff3c", "leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "PEDIR_AUDIO" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "ecf52241-ef7e-4a9a-8823-b9e3b438da61", "leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "GUARDAR_AUDIO" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "04433553-d0ee-4391-a4f4-926197ac4c0c", "leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "PEDIR_AUDIO" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "8637dc0f-59b6-40ac-8bc4-baf869829598", "leftValue": "={{ $json.paso_decision === 'GUARDAR_IMAGEN' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "GUARDAR_IMAGEN" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4096e616-2517-48ab-b6c4-2712f763ec1d", "leftValue": "={{ $json.paso_decision === 'FOTOS_LISTAS' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "FOTOS_LISTAS" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "d1dd1d5c-8e25-4147-876e-5e7227784f6f", "leftValue": "={{ $json.paso_decision === 'PEDIR_IMAGENES' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "PEDIR_IMAGENES" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "bddbb16a-0463-47dd-9a6c-03c3635043f1", "leftValue": "={{ $json.paso_decision === 'GUARDAR_VIDEO' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "GUARDAR_VIDEO" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "fe05ba35-0ccc-417a-a4e8-b907611932e4", "leftValue": "={{ $json.paso_decision === 'CERRAR_SIN_VIDEO' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "CERRAR_SIN_VIDEO" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "94647ac0-ee80-4c96-9f25-8a847704b6a0", "leftValue": "={{ $json.paso_decision === 'CERRAR_CON_VIDEOS' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "CERRAR_CON_VIDEOS" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "3038a656-50ae-4738-b2ff-a6276bd06522", "leftValue": "={{ $json.paso_decision === 'PEDIR_VIDEOS' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "PEDIR_VIDEOS" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "1b20a524-2289-4d37-8372-9a839dadc971", "leftValue": "={{ $json.paso_decision === 'AVISO_PROCESANDO' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "AVISO_PROCESANDO" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "0943bbd0-f5b7-43c4-9b01-7f6bea244894", "leftValue": "={{ $json.paso_decision === 'ETAPA_NO_RECONOCIDA' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "ETAPA_NO_RECONOCIDA" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "84c163a3-3c08-4636-9104-c0e4c55b11e6", "leftValue": "={{ $json.paso_decision === 'IGNORAR_EVENTO' }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "IGNORAR_EVENTO" } ] }, "options": {} }, "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ 48816, 29680 ], "id": "f46f27d2-5404-4b35-a19a-ccb9981217a5", "name": "Switch - Paso activo WhatsApp TEST" }, { "parameters": { "jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '🎙️ *Step 1 of 3 — Voice note*',\n '',\n 'I still need the required voice note to start the report.',\n '',\n 'Please send one voice note and clearly say whether this is:',\n '',\n '1. *Executed Proposal*',\n '2. *External Proposal*',\n '',\n 'Include:',\n '• Proposal/reference',\n '• Brand/client',\n '• Country',\n '• Location',\n '• Date',\n '• What was implemented or reported',\n '• Results/comments',\n '',\n '*Important:* One proposal/report at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n '🎙️ *Paso 1 de 3 — Nota de voz*',\n '',\n 'Aún necesito la nota de voz obligatoria para iniciar el reporte.',\n '',\n 'Envía una sola nota de voz e indica claramente si es:',\n '',\n '1. *Propuesta Ejecutada*',\n '2. *Propuesta Externa*',\n '',\n 'Incluye:',\n '• Propuesta/referencia',\n '• Marca/cliente',\n '• País',\n '• Ubicación',\n '• Fecha',\n '• Qué se implementó o reportó',\n '• Resultados/comentarios',\n '',\n '*Importante:* Una sola propuesta o reporte a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid,\n\n whatsapp_header: idioma === 'EN' ? \"Step 1 of 3\" : \"Paso 1 de 3\",\n whatsapp_buttons: [\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51248, 27696 ], "id": "79b0aeae-4f64-417a-a880-85a090efa4c4", "name": "Code - Preparar solicitud audio WhatsApp TEST" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 306129743, "mode": "list", "cachedResultName": "wa_ejecuciones_eventos", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" }, "columns": { "mappingMode": "defineBelow", "value": { "event_id": "={{ $json.event_id }}", "fecha_recepcion": "={{ $json.fecha_recepcion }}", "manager_telefono": "={{ $json.manager_telefono }}", "manager_nombre": "={{ $json.manager_nombre }}", "message_type": "={{ $json.message_type }}", "texto": "={{ $json.texto }}", "comando": "={{ $json.accion_flujo }}", "tiene_media": "={{ $json.tiene_media }}", "media_count": "={{ $json.media_count }}", "raw_preview": "={{ $json.raw_preview }}", "estado": "={{ $json.estado }}", "ejecucion_id": "={{ $json.ejecucion_id }}", "fecha_procesado": "={{ $json.fecha_procesado }}", "session_id": "={{ $json.session_id }}", "canal_origen": "={{ $json.canal_origen }}", "etapa_recibida": "={{ $json.etapa_recibida }}", "media_source_id": "={{ $json.media_source_id }}", "media_mime_type": "={{ $json.media_mime_type }}", "media_file_name": "={{ $json.media_file_name }}", "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" }, "matchingColumns": [], "schema": [ { "id": "event_id", "displayName": "event_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_recepcion", "displayName": "fecha_recepcion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "message_type", "displayName": "message_type", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "texto", "displayName": "texto", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "comando", "displayName": "comando", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "tiene_media", "displayName": "tiene_media", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_count", "displayName": "media_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "raw_preview", "displayName": "raw_preview", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_procesado", "displayName": "fecha_procesado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa_recibida", "displayName": "etapa_recibida", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_source_id", "displayName": "media_source_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "media_mime_type", "displayName": "media_mime_type", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "media_file_name", "displayName": "media_file_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "whatsapp_remote_jid", "displayName": "whatsapp_remote_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 50960, 27312 ], "id": "21e1f693-fbdf-4068-9bfe-22d52f28a62b", "name": "Sheets - Guardar evento audio WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const datos = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = datos.sesion_activa || datos.nueva_sesion || {};\n\nreturn [\n {\n json: {\n ...datos,\n\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'ACTIVA',\n\n audio_count: Number(sesion.audio_count || 0) + 1,\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n manager_telefono: datos.manager_telefono || sesion.manager_telefono || datos.sender_phone || '',\n manager_nombre: datos.manager_nombre || sesion.manager_nombre || datos.sender_name || '',\n\n is_group: datos.is_group ?? sesion.is_group ?? false,\n group_jid: datos.group_jid || sesion.group_jid || '',\n group_name: datos.group_name || sesion.group_name || '',\n sender_jid: datos.sender_jid || sesion.sender_jid || '',\n sender_phone: datos.sender_phone || sesion.sender_phone || datos.manager_telefono || '',\n sender_name: datos.sender_name || sesion.sender_name || datos.manager_nombre || '',\n\n country_code: datos.country_code || sesion.country_code || '',\n pais_detectado: datos.pais_detectado || sesion.pais_detectado || '',\n idioma_flujo: datos.idioma_flujo || sesion.idioma_flujo || 'ES',\n\n tipo_reporte: sesion.tipo_reporte || datos.tipo_reporte || '',\n\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid || ''\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51168, 27312 ], "id": "9e17aaf9-24d6-44bd-9bff-42cb077d393f", "name": "Code - Preparar actualización audio recibido WhatsApp TEST" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "ultima_actividad": "={{ $json.ultima_actividad }}", "etapa": "={{ $json.etapa }}", "estado": "={{ $json.estado }}", "audio_count": "={{ $json.audio_count }}", "imagenes_count": "={{ $json.imagenes_count }}", "videos_count": "={{ $json.videos_count }}", "is_group": "={{ $json.is_group }}", "group_jid": "={{ $json.group_jid }}", "group_name": "={{ $json.group_name }}", "sender_jid": "={{ $json.sender_jid }}", "sender_phone": "={{ $json.sender_phone }}", "sender_name": "={{ $json.sender_name }}", "country_code": "={{ $json.country_code }}", "pais_detectado": "={{ $json.pais_detectado }}", "idioma_flujo": "={{ $json.idioma_flujo }}", "tipo_reporte": "={{ $json.tipo_reporte }}", "manager_telefono": "={{ $json.manager_telefono }}", "manager_nombre": "={{ $json.manager_nombre }}", "ejecucion_id": "={{ $json.ejecucion_id }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "fecha_inicio", "displayName": "fecha_inicio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "ultima_actividad", "displayName": "ultima_actividad", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa", "displayName": "etapa", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audio_count", "displayName": "audio_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "imagenes_count", "displayName": "imagenes_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "is_group", "displayName": "is_group", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_jid", "displayName": "group_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_name", "displayName": "group_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_jid", "displayName": "sender_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_phone", "displayName": "sender_phone", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_name", "displayName": "sender_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "country_code", "displayName": "country_code", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "pais_detectado", "displayName": "pais_detectado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "idioma_flujo", "displayName": "idioma_flujo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 51376, 27312 ], "id": "89a6f88f-f8c9-4530-afd0-2fa1fbf2d7e6", "name": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const datos = $('Code - Preparar actualización audio recibido WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ Voice note received.',\n '',\n '*Step 2 of 3 — Required images*',\n '',\n 'Now send one or more photos related to the proposal.',\n '',\n 'You can send multiple images together or in separate messages.',\n '',\n 'When you finish sending images, select: Photos ready.'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ Nota de voz recibida.',\n '',\n '*Paso 2 de 3 — Imágenes obligatorias*',\n '',\n 'Ahora envía una o varias fotos relacionadas a la propuesta.',\n '',\n 'Puedes enviar varias imágenes juntas o en mensajes separados.',\n '',\n 'Cuando termines de enviar las imágenes, selecciona: Fotos listas.'\n ].join('\\n');\n}\n\nconst whatsappTo = limpiar(\n datos.whatsapp_to ||\n datos.group_jid ||\n datos.whatsapp_remote_jid ||\n datos.sender_jid ||\n ''\n);\n\nif (!whatsappTo) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar solicitud de imágenes.');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: whatsappTo,\n whatsapp_header: '',\n whatsapp_buttons: [\n { id: 'FOTOS_LISTAS', title: idioma === 'EN' ? 'Photos ready' : 'Fotos listas' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje,\n texto_respuesta: mensaje,\n mensaje,\n text: mensaje,\n message_text: mensaje,\n estado_mensaje: 'SOLICITUD_IMAGENES_PREPARADA',\n solicitud_imagenes_debug: {\n idioma,\n whatsapp_to: whatsappTo,\n permite_imagenes_juntas: true\n }\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51600, 27312 ], "id": "8ee26380-25a4-40f4-9024-026be1751a0b", "name": "Code - Preparar solicitud imágenes WhatsApp TEST" }, { "parameters": { "jsCode": "const datos = $json || {};\n\nreturn [\n {\n json: {\n ...datos,\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n etapa: 'CANCELADA',\n estado: 'CANCELADA',\n motivo_revision: 'CANCELADO_POR_USUARIO',\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 50992, 26720 ], "id": "c657b6f2-9946-46aa-92b9-bd2f4b3b9127", "name": "Code - Preparar cancelación sesión WhatsApp TEST" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "ultima_actividad": "={{ $json.ultima_actividad }}", "etapa": "={{ $json.etapa }}", "estado": "={{ $json.estado }}", "motivo_revision": "={{ $json.motivo_revision }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "fecha_inicio", "displayName": "fecha_inicio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "ultima_actividad", "displayName": "ultima_actividad", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa", "displayName": "etapa", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audio_count", "displayName": "audio_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "imagenes_count", "displayName": "imagenes_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "is_group", "displayName": "is_group", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "group_jid", "displayName": "group_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "group_name", "displayName": "group_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "sender_jid", "displayName": "sender_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "sender_phone", "displayName": "sender_phone", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "sender_name", "displayName": "sender_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "country_code", "displayName": "country_code", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "pais_detectado", "displayName": "pais_detectado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "idioma_flujo", "displayName": "idioma_flujo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 51200, 26720 ], "id": "cd2e53ef-91a9-404c-802e-e122421868bf", "name": "Sheets - Cancelar sesión WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver sesión WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\nconst digits = (valor) => limpiar(valor).replace(/\\D/g, '');\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst whatsappTo = digits(\n base.whatsapp_to ||\n actual.whatsapp_to ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone\n);\n\nif (!whatsappTo) {\n throw new Error('No se encontró el número privado para confirmar la cancelación.');\n}\n\nconst mensaje = idioma === 'EN'\n ? [\n '✅ *Report cancelled successfully.*',\n '',\n 'The current report was closed and no additional evidence will be added to that session.',\n '',\n 'You can start another report whenever you are ready.'\n ].join('\\n')\n : [\n '✅ *Reporte cancelado correctamente.*',\n '',\n 'El reporte actual fue cerrado y no se agregará más evidencia a esa sesión.',\n '',\n 'Puedes iniciar otro reporte cuando estés listo.'\n ].join('\\n');\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n is_group: false,\n group_id: '',\n group_jid: '',\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: 'individual',\n whatsapp_header: idioma === 'EN' ? 'Report cancelled' : 'Reporte cancelado',\n whatsapp_text: mensaje,\n whatsapp_buttons: [\n { id: 'HEY', title: idioma === 'EN' ? 'Start report' : 'Iniciar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ]\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51456, 26656 ], "id": "a3c37ea4-2d7a-4530-9e6d-13d309a6f039", "name": "Code - Preparar mensaje cancelación WhatsApp TEST" }, { "parameters": { "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\n\n const esGrupo = Boolean(\n actual.is_group ||\n base.is_group ||\n sesion.is_group ||\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid\n );\n\n if (esGrupo) {\n return limpiar(\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n raw\n );\n }\n\n return raw.replace(/\\D/g, '');\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 '⚠️ Send at least one image.',\n 'You may send images together or in separate messages.',\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.',\n 'Puedes enviarlas juntas o en mensajes separados.',\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_recipient_type: esGrupo ? 'group' : 'individual',\n whatsapp_header: idioma === 'EN' ? 'Image required' : 'Imagen requerida',\n whatsapp_buttons: [\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51968, 29744 ], "id": "6bdca504-2ddf-4c68-972b-c59e6fd06d70", "name": "Code - Preparar aviso falta imagen WhatsApp TEST" }, { "parameters": { "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || base.nueva_sesion || {};\n\nconst rows = $input.all().map(item => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizarNumero = (valor) =>\n texto(valor).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": [ 52592, 28896 ], "id": "fb8ca667-903b-4384-9cf3-d5ef5e82216a", "name": "Preparar actualización fotos listas" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "ultima_actividad": "={{ $json.ultima_actividad }}", "etapa": "={{ $json.etapa }}", "audio_count": "={{ $json.audio_count }}", "imagenes_count": "={{ $json.imagenes_count }}", "videos_count": "={{ $json.videos_count }}", "estado": "={{ $json.estado }}", "ejecucion_id": "={{ $json.ejecucion_id }}", "motivo_revision": "={{ $json.motivo_revision }}", "is_group": "={{ $json.is_group }}", "group_jid": "={{ $json.group_jid }}", "group_name": "={{ $json.group_name }}", "sender_jid": "={{ $json.sender_jid }}", "sender_phone": "={{ $json.sender_phone }}", "sender_name": "={{ $json.sender_name }}", "country_code": "={{ $json.country_code }}", "pais_detectado": "={{ $json.pais_detectado }}", "idioma_flujo": "={{ $json.idioma_flujo }}", "tipo_reporte": "={{ $json.tipo_reporte }}", "manager_telefono": "={{ $json.manager_telefono }}", "manager_nombre": "={{ $json.manager_nombre }}", "canal_origen": "={{ $json.canal_origen }}", "fecha_inicio": "={{ $json.fecha_inicio }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_inicio", "displayName": "fecha_inicio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ultima_actividad", "displayName": "ultima_actividad", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa", "displayName": "etapa", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audio_count", "displayName": "audio_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "imagenes_count", "displayName": "imagenes_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "is_group", "displayName": "is_group", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_jid", "displayName": "group_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_name", "displayName": "group_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_jid", "displayName": "sender_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_phone", "displayName": "sender_phone", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_name", "displayName": "sender_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "country_code", "displayName": "country_code", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "pais_detectado", "displayName": "pais_detectado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "idioma_flujo", "displayName": "idioma_flujo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 52784, 28896 ], "id": "081d8dfd-967d-42aa-ba40-f09423d57285", "name": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const actual = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst idioma = String(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = String(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n ''\n).trim();\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar solicitud de videos.');\n}\n\nconst esGrupo = Boolean(\n actual.is_group ||\n base.is_group ||\n sesion.is_group ||\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n destinoBase\n ).trim()\n : destinoBase.replace(/\\D/g, '');\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 proposal if you have them.',\n '',\n '⚠️ Important: send videos one by one, in separate messages.',\n '',\n 'If you do not have videos, select: No video',\n '',\n 'If you already sent videos and finished, select: 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 propuesta si tienes.',\n '',\n '⚠️ Importante: envía los videos uno por uno, en mensajes separados.',\n '',\n 'Si no tienes videos, selecciona: Sin video',\n '',\n 'Si enviaste videos y ya terminaste, selecciona: Listo',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\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 manager_telefono: actual.manager_telefono || base.manager_telefono || sesion.manager_telefono || '',\n manager_nombre: actual.manager_nombre || base.manager_nombre || sesion.manager_nombre || '',\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 whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n whatsapp_header: '',\n whatsapp_buttons: [\n { id: 'SIN_VIDEO', title: idioma === 'EN' ? 'No video' : 'Sin video' },\n { id: 'LISTO', title: idioma === 'EN' ? 'Done' : 'Listo' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' }\n ],\n whatsapp_text: mensaje,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 53008, 28896 ], "id": "829fa486-382e-4859-8b2d-cabbbeb78618", "name": "Code - Preparar solicitud videos WhatsApp TEST" }, { "parameters": { "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\n\n const esGrupo = Boolean(\n actual.is_group ||\n base.is_group ||\n sesion.is_group ||\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid\n );\n\n if (esGrupo) {\n return limpiar(\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n raw\n );\n }\n\n return raw.replace(/\\D/g, '');\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_recipient_type: esGrupo ? 'group' : 'individual',\n whatsapp_header: idioma === 'EN' ? 'Optional videos' : 'Videos opcionales',\n whatsapp_buttons: [\n { id: 'SIN_VIDEO', title: idioma === 'EN' ? 'No video' : 'Sin video' },\n { id: 'LISTO', title: idioma === 'EN' ? 'Done' : 'Listo' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' }\n ],\n whatsapp_text: mensaje\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 50096, 31568 ], "id": "f1c0b52a-70c5-4e4c-87fe-63e4313d51fa", "name": "Code - Preparar recordatorio videos WhatsApp TEST" }, { "parameters": { "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst esGrupo = Boolean(\n base.is_group ||\n sesion.is_group ||\n base.group_id ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n base.group_id ||\n base.group_jid ||\n sesion.group_jid ||\n telefonoBase\n ).trim()\n : telefonoBase.replace(/\\D/g, '');\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'SIN_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 49744, 30960 ], "id": "7ef84b4c-feda-4e5b-beea-3db1e93a961c", "name": "CERRAR_SIN_VIDEO" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "ultima_actividad": "={{ $json.ultima_actividad }}", "etapa": "={{ $json.etapa }}", "audio_count": "={{ $json.audio_count }}", "imagenes_count": "={{ $json.imagenes_count }}", "videos_count": "={{ $json.videos_count }}", "estado": "={{ $json.estado }}", "ejecucion_id": "={{ $json.ejecucion_id }}", "motivo_revision": "={{ $json.motivo_revision }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_inicio", "displayName": "fecha_inicio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ultima_actividad", "displayName": "ultima_actividad", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa", "displayName": "etapa", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audio_count", "displayName": "audio_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "imagenes_count", "displayName": "imagenes_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "is_group", "displayName": "is_group", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_jid", "displayName": "group_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_name", "displayName": "group_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_jid", "displayName": "sender_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_phone", "displayName": "sender_phone", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_name", "displayName": "sender_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "country_code", "displayName": "country_code", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "pais_detectado", "displayName": "pais_detectado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "idioma_flujo", "displayName": "idioma_flujo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 49952, 30960 ], "id": "24ffbf6a-2bce-49fb-99da-14f9d66379b0", "name": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 306129743, "mode": "list", "cachedResultName": "wa_ejecuciones_eventos", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" }, "columns": { "mappingMode": "defineBelow", "value": { "event_id": "={{ $json.event_id }}", "fecha_recepcion": "={{ $json.fecha_recepcion }}", "manager_telefono": "={{ $json.manager_telefono }}", "manager_nombre": "={{ $json.manager_nombre }}", "message_type": "={{ $json.message_type }}", "texto": "={{ $json.texto }}", "comando": "={{ $json.accion_flujo }}", "tiene_media": "={{ $json.tiene_media }}", "media_count": "={{ $json.media_count }}", "raw_preview": "={{ $json.raw_preview }}", "estado": "=VIDEO_RECIBIDO", "ejecucion_id": "={{ $json.ejecucion_id }}", "session_id": "={{ $json.session_id }}", "canal_origen": "={{ $json.canal_origen }}", "etapa_recibida": "={{ $json.etapa_actual }}", "media_source_id": "={{ $json.media_source_id }}", "media_mime_type": "={{ $json.media_mime_type }}", "media_file_name": "={{ $json.media_file_name }}", "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" }, "matchingColumns": [], "schema": [ { "id": "event_id", "displayName": "event_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_recepcion", "displayName": "fecha_recepcion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "message_type", "displayName": "message_type", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "texto", "displayName": "texto", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "comando", "displayName": "comando", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "tiene_media", "displayName": "tiene_media", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_count", "displayName": "media_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "raw_preview", "displayName": "raw_preview", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_procesado", "displayName": "fecha_procesado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa_recibida", "displayName": "etapa_recibida", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_source_id", "displayName": "media_source_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "media_mime_type", "displayName": "media_mime_type", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "media_file_name", "displayName": "media_file_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "whatsapp_remote_jid", "displayName": "whatsapp_remote_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 52096, 29984 ], "id": "24e27426-8bff-4d9a-8326-37c0300abd49", "name": "Sheets - Guardar evento video WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const evento = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst videosActuales = Number(sesion.videos_count || 0);\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n evento.whatsapp_to ||\n evento.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst esGrupo = Boolean(\n evento.is_group ||\n base.is_group ||\n sesion.is_group ||\n evento.group_id ||\n base.group_id ||\n evento.group_jid ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n evento.group_id ||\n base.group_id ||\n evento.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n telefonoBase\n ).trim()\n : telefonoBase.replace(/\\D/g, '');\n\nreturn [\n {\n json: {\n ...base,\n ...evento,\n\n session_id: base.session_id || sesion.session_id || evento.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || evento.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || evento.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n // Seguimos en videos hasta que el usuario escriba LISTO\n etapa: 'ESPERANDO_VIDEOS',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: videosActuales + 1,\n\n estado: sesion.estado || 'ACTIVA',\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: sesion.motivo_revision || '',\n\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 52320, 29920 ], "id": "389d9bf0-02a8-45ba-9658-7f2ad1da3bd1", "name": "Code - Preparar actualización video recibido WhatsApp TEST" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "motivo_revision": "={{ $json.motivo_revision }}", "ejecucion_id": "={{ $json.ejecucion_id }}", "estado": "={{ $json.estado }}", "videos_count": "={{ $json.videos_count }}", "imagenes_count": "={{ $json.imagenes_count }}", "audio_count": "={{ $json.audio_count }}", "etapa": "={{ $json.etapa }}", "ultima_actividad": "={{ $json.ultima_actividad }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_inicio", "displayName": "fecha_inicio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ultima_actividad", "displayName": "ultima_actividad", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa", "displayName": "etapa", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audio_count", "displayName": "audio_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "imagenes_count", "displayName": "imagenes_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "is_group", "displayName": "is_group", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_jid", "displayName": "group_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_name", "displayName": "group_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_jid", "displayName": "sender_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_phone", "displayName": "sender_phone", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_name", "displayName": "sender_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "country_code", "displayName": "country_code", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "pais_detectado", "displayName": "pais_detectado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "idioma_flujo", "displayName": "idioma_flujo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 52528, 29920 ], "id": "7c94f5bf-fc11-495f-a4a0-684a79c3d7f3", "name": "Sheets - Actualizar sesión video recibido WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\n\n const esGrupo = Boolean(\n actual.is_group ||\n base.is_group ||\n sesion.is_group ||\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid\n );\n\n if (esGrupo) {\n return limpiar(\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n raw\n );\n }\n\n return raw.replace(/\\D/g, '');\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_recipient_type: esGrupo ? 'group' : 'individual',\n whatsapp_header: idioma === 'EN' ? 'Video received' : 'Video recibido',\n whatsapp_buttons: [\n { id: 'LISTO', title: idioma === 'EN' ? 'Done' : 'Listo' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 52736, 29920 ], "id": "e8d56ac9-d966-42a9-82bf-4c30edc263c7", "name": "Code - Preparar confirmación video WhatsApp TEST" }, { "parameters": { "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst esGrupo = Boolean(\n base.is_group ||\n sesion.is_group ||\n base.group_id ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n base.group_id ||\n base.group_jid ||\n sesion.group_jid ||\n telefonoBase\n ).trim()\n : telefonoBase.replace(/\\D/g, '');\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'CON_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 49664, 31248 ], "id": "acece944-d7e8-432b-b614-6b0603fe703a", "name": "Code - Preparar cierre con videos WhatsApp TEST" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "ultima_actividad": "={{ $json.ultima_actividad }}", "etapa": "={{ $json.etapa }}", "audio_count": "={{ $json.audio_count }}", "imagenes_count": "={{ $json.imagenes_count }}", "videos_count": "={{ $json.videos_count }}", "estado": "={{ $json.estado }}", "ejecucion_id": "={{ $json.ejecucion_id }}", "motivo_revision": "={{ $json.motivo_revision }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_inicio", "displayName": "fecha_inicio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ultima_actividad", "displayName": "ultima_actividad", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa", "displayName": "etapa", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audio_count", "displayName": "audio_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "imagenes_count", "displayName": "imagenes_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "is_group", "displayName": "is_group", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_jid", "displayName": "group_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_name", "displayName": "group_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_jid", "displayName": "sender_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_phone", "displayName": "sender_phone", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_name", "displayName": "sender_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "country_code", "displayName": "country_code", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "pais_detectado", "displayName": "pais_detectado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "idioma_flujo", "displayName": "idioma_flujo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 49872, 31248 ], "id": "993b75c7-f768-4761-b63e-b08694d2ab21", "name": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const data = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n data.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst esGrupo = Boolean(\n data.is_group ||\n base.is_group ||\n sesion.is_group ||\n data.group_id ||\n base.group_id ||\n data.group_jid ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n data.group_id ||\n base.group_id ||\n data.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n telefonoBase\n ).trim()\n : telefonoBase.replace(/\\D/g, '');\n\nreturn [\n {\n json: {\n ...base,\n ...data,\n\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n\n whatsapp_text: `⏳ Tu reporte ya fue recibido y está pendiente de procesamiento.\n\nPor favor espera mientras se prepara el análisis de la evidencia.`\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 50160, 31808 ], "id": "d3384354-e562-4f3d-9510-6fe0c226a503", "name": "Code - Preparar aviso procesando WhatsApp TEST" }, { "parameters": { "jsCode": "const data = $json || {};\n\nreturn [\n {\n json: {\n ...data,\n\n session_id_busqueda: data.session_id || '',\n estado_procesamiento: 'BUSCAR_EVENTOS_MEDIA'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 50928, 31168 ], "id": "c6c15e2a-25ee-4fb7-93f5-b1b675cca80a", "name": "Code - Preparar búsqueda eventos media WhatsApp TEST" }, { "parameters": { "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 306129743, "mode": "list", "cachedResultName": "wa_ejecuciones_eventos", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 51184, 31168 ], "id": "f8a25b7b-025d-4b53-bab6-ec5eeb8d47df", "name": "Sheets - Leer eventos WhatsApp TEST", "alwaysOutputData": true, "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const cierre = $('Code - Preparar búsqueda eventos media WhatsApp TEST').first().json || {};\nconst sessionId = String(cierre.session_id_busqueda || cierre.session_id || '').trim();\n\nconst eventos = $input.all().map(item => item.json || {});\n\nconst eventosSesion = eventos.filter(evento => {\n return String(evento.session_id || '').trim() === sessionId;\n});\n\nconst eventosMedia = eventosSesion.filter(evento => {\n const tipo = String(evento.message_type || '').toLowerCase();\n return ['audio', 'image', 'video'].includes(tipo);\n});\n\nconst audios = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'audio');\nconst imagenes = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'image');\nconst videos = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...cierre,\n\n total_eventos_sesion: eventosSesion.length,\n total_media: eventosMedia.length,\n\n audio_count_real: audios.length,\n imagenes_count_real: imagenes.length,\n videos_count_real: videos.length,\n\n eventos_audio: audios,\n eventos_imagenes: imagenes,\n eventos_videos: videos,\n\n media_lista: eventosMedia,\n\n estado_procesamiento: 'EVENTOS_MEDIA_FILTRADOS'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51424, 31168 ], "id": "07afda02-eaa5-4117-ae00-f4a5babfe503", "name": "Code - Filtrar eventos media de sesión WhatsApp TEST" }, { "parameters": { "jsCode": "const data = $json || {};\nconst mediaLista = Array.isArray(data.media_lista) ? data.media_lista : [];\n\nconst sessionId = String(data.session_id || data.session_id_busqueda || '').trim();\nconst ejecucionId = String(data.ejecucion_id || sessionId).trim();\n\nconst items = mediaLista.map((media, index) => {\n const tipo = String(media.message_type || '').toLowerCase();\n const mime = String(media.media_mime_type || '').trim();\n\n let extension = 'bin';\n\n if (mime.includes('ogg')) extension = 'ogg';\n else if (mime.includes('mpeg')) extension = 'mp3';\n else if (mime.includes('mp4')) extension = 'mp4';\n else if (mime.includes('jpeg') || mime.includes('jpg')) extension = 'jpg';\n else if (mime.includes('png')) extension = 'png';\n else if (tipo === 'audio') extension = 'ogg';\n else if (tipo === 'image') extension = 'jpg';\n else if (tipo === 'video') extension = 'mp4';\n\n const mediaSourceId = String(\n media.media_source_id ||\n media.event_id ||\n ''\n ).trim();\n\n const remoteJid = String(\n media.whatsapp_remote_jid ||\n data.whatsapp_remote_jid ||\n data.whatsapp_to ||\n media.manager_telefono ||\n data.manager_telefono ||\n ''\n ).trim();\n\n return {\n json: {\n ...data,\n\n media_index: index + 1,\n media_total: mediaLista.length,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: media.event_id || '',\n media_source_id: mediaSourceId,\n media_type: tipo,\n media_mime_type: mime,\n media_extension: extension,\n\n whatsapp_remote_jid: remoteJid,\n\n media_file_name: `${ejecucionId}_${tipo}_${index + 1}.${extension}`,\n\n evento_original: media,\n\n estado_procesamiento: 'MEDIA_ITEM_PREPARADO'\n }\n };\n});\n\nreturn items;" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51664, 31168 ], "id": "39734cec-0ee3-4874-a668-274d0c8fb38f", "name": "Code - Separar media en items WhatsApp TEST" }, { "parameters": { "resource": "media", "operation": "mediaUrlGet", "mediaGetId": "={{ $json.media_source_id }}" }, "type": "n8n-nodes-base.whatsApp", "typeVersion": 1.1, "position": [ 51872, 31168 ], "id": "84e2ae15-dd4b-4487-abc7-3c500bc2f1fa", "name": "HTTP - Obtener metadata media Meta WhatsApp TEST", "alwaysOutputData": true, "webhookId": "1e8de0c8-0d81-4233-9eb1-61449b259ce5", "credentials": { "whatsAppApi": { "id": "t14kVayc9FurLReq", "name": "WhatsApp API - GLM CDC" } } }, { "parameters": { "jsCode": "const downloadedItems = $input.all();\nconst mediaItems = $('Code - Preparar descarga media Meta WhatsApp TEST').all();\n\nreturn downloadedItems.map((item, index) => {\n const media = mediaItems[index]?.json || mediaItems[0]?.json || {};\n const binary = item.binary || {};\n\n if (!binary.data) {\n throw new Error(`No llegó binary.data para media_source_id: ${media.media_source_id || 'SIN_ID'}`);\n }\n\n binary.data.mimeType =\n media.media_mime_type ||\n binary.data.mimeType ||\n 'application/octet-stream';\n\n binary.data.fileName =\n media.media_file_name ||\n binary.data.fileName ||\n `media_${media.media_source_id || Date.now()}`;\n\n return {\n json: {\n ...media,\n estado_procesamiento: 'MEDIA_OFICIAL_DESCARGADA'\n },\n binary\n };\n});" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 52384, 31168 ], "id": "7f59f571-ae44-4646-b635-4e1d19ac24a3", "name": "Code - Restaurar contexto binario Meta WhatsApp TEST" }, { "parameters": { "name": "={{ $json.media_file_name }}", "driveId": { "__rl": true, "mode": "list", "value": "My Drive" }, "folderId": { "__rl": true, "value": "=1G11WZnOwPp7UC2moPE6rEvlPFl7_OoSB", "mode": "id" }, "options": {} }, "type": "n8n-nodes-base.googleDrive", "typeVersion": 3, "position": [ 52320, 31168 ], "id": "253c7221-838d-41ea-b1d0-b2c4bec6fdab", "name": "Drive - Subir media WhatsApp TEST", "credentials": { "googleDriveOAuth2Api": { "id": "g23xdGLZRzBGqKgH", "name": "Isaac - Google Drive" } } }, { "parameters": { "jsCode": "const driveItems = $input.all();\nconst mediaItems = $('Code - Restaurar contexto binario Meta WhatsApp TEST').all();\n\nreturn driveItems.map((item, index) => {\n const drive = item.json || {};\n const media = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n const fileId =\n drive.id ||\n drive.fileId ||\n drive.file_id ||\n '';\n\n const webViewLink =\n drive.webViewLink ||\n drive.webContentLink ||\n (fileId ? `https://drive.google.com/file/d/${fileId}/view` : '');\n\n return {\n json: {\n ...media,\n\n drive_file_id: fileId,\n drive_file_name: media.media_file_name,\n drive_file_url: webViewLink,\n\n estado_procesamiento: 'MEDIA_GUARDADA_EN_DRIVE'\n }\n };\n});" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 52528, 31168 ], "id": "ffd194c9-020d-4911-8927-4c4061dddfd1", "name": "Code - Preparar registro media Drive WhatsApp TEST" }, { "parameters": { "jsCode": "const mediaItems = $('Code - Preparar registro media Drive WhatsApp TEST').all();\n\nconst media = mediaItems.map(item => item.json || {});\n\nconst base = media[0] || {};\n\nconst audios = media.filter(m => String(m.media_type || '').toLowerCase() === 'audio');\nconst imagenes = media.filter(m => String(m.media_type || '').toLowerCase() === 'image');\nconst videos = media.filter(m => String(m.media_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || '',\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n\n audio_drive_urls: audios.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n imagenes_drive_urls: imagenes.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n videos_drive_urls: videos.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n\n total_media_drive: media.length,\n total_audio_drive: audios.length,\n total_imagenes_drive: imagenes.length,\n total_videos_drive: videos.length,\n\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_ANALIZAR',\n motivo_revision: 'MEDIA_RECUPERADA',\n\n estado_procesamiento: 'MEDIA_RECUPERADA_Y_REGISTRADA'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 52944, 31168 ], "id": "50b617f4-9f9c-4790-8621-3c12588d1ea8", "name": "Code - Consolidar media recuperada WhatsApp TEST" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 1962428033, "mode": "list", "cachedResultName": "wa_ejecuciones_media", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1962428033" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "ejecucion_id": "={{ $json.ejecucion_id }}", "media_event_id": "={{ $json.media_event_id }}", "media_type": "={{ $json.media_type }}", "media_mime_type": "={{ $json.media_mime_type }}", "media_file_name": "={{ $json.drive_file_name }}", "drive_file_id": "={{ $json.drive_file_id }}", "drive_file_url": "={{ $json.drive_file_url }}", "fecha_guardado": "={{ new Date().toISOString() }}", "estado": "=MEDIA_GUARDADA_EN_DRIVE", "media_index": "={{ $json.media_index }}", "media_total": "={{ $json.media_total }}", "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" }, "matchingColumns": [], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_event_id", "displayName": "media_event_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_type", "displayName": "media_type", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_mime_type", "displayName": "media_mime_type", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_file_name", "displayName": "media_file_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "drive_file_id", "displayName": "drive_file_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "drive_file_url", "displayName": "drive_file_url", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_guardado", "displayName": "fecha_guardado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_index", "displayName": "media_index", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_total", "displayName": "media_total", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "whatsapp_remote_jid", "displayName": "whatsapp_remote_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 52736, 31168 ], "id": "61b15082-007c-44df-8dde-d720a19c4167", "name": "Sheets - Guardar media Drive WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "motivo_revision": "={{ $json.motivo_revision }}", "estado": "={{ $json.estado }}", "etapa": "={{ $json.etapa }}", "ultima_actividad": "={{ $json.ultima_actividad }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_inicio", "displayName": "fecha_inicio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ultima_actividad", "displayName": "ultima_actividad", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa", "displayName": "etapa", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audio_count", "displayName": "audio_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "imagenes_count", "displayName": "imagenes_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "is_group", "displayName": "is_group", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_jid", "displayName": "group_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "group_name", "displayName": "group_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_jid", "displayName": "sender_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_phone", "displayName": "sender_phone", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "sender_name", "displayName": "sender_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "country_code", "displayName": "country_code", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "pais_detectado", "displayName": "pais_detectado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "idioma_flujo", "displayName": "idioma_flujo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 53152, 31168 ], "id": "38218f3d-b442-4b98-8832-bb77213fc943", "name": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const data = $('Code - Consolidar media recuperada WhatsApp TEST').first().json || {};\n\nconst audioUrls = String(data.audio_drive_urls || '').trim();\nconst imagenesUrls = String(data.imagenes_drive_urls || '').trim();\nconst videosUrls = String(data.videos_drive_urls || '').trim();\n\nconst prompt = `\nAnaliza esta ejecución de propuesta usando la evidencia disponible.\n\nIMPORTANTE:\n- La nota de voz contiene la descripción principal de la ejecución.\n- Las imágenes muestran evidencia visual obligatoria.\n- Los videos son evidencia opcional adicional.\n- No inventes datos que no estén claros.\n- Si un dato no aparece, marca \"No identificado\".\n\nDebes devolver SOLO un JSON válido con esta estructura:\n\n{\n \"propuesta_referencia\": \"\",\n \"marca\": \"\",\n \"cliente\": \"\",\n \"pais\": \"\",\n \"ubicacion\": \"\",\n \"fecha_ejecucion\": \"\",\n \"que_se_implemento\": \"\",\n \"comentarios_resultados\": \"\",\n \"resumen_ejecucion\": \"\",\n \"nivel_confianza\": \"\",\n \"requiere_revision\": \"\",\n \"motivo_revision\": \"\"\n}\n\nDatos de control:\nsession_id: ${data.session_id}\nejecucion_id: ${data.ejecucion_id}\nmanager_nombre: ${data.manager_nombre}\nmanager_telefono: ${data.manager_telefono}\n\nArchivos de audio en Drive:\n${audioUrls || 'No hay audio registrado'}\n\nImágenes en Drive:\n${imagenesUrls || 'No hay imágenes registradas'}\n\nVideos en Drive:\n${videosUrls || 'No hay videos registrados'}\n`.trim();\n\nreturn [\n {\n json: {\n ...data,\n gemini_prompt: prompt,\n estado_procesamiento: 'PAQUETE_GEMINI_PREPARADO'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 53360, 31168 ], "id": "b86ab1e9-c2cc-48d7-a69c-de7eca4f1fc8", "name": "Code - Preparar paquete análisis Gemini WhatsApp TEST" }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", "rightValue": "audio", "operator": { "type": "string", "operation": "equals" }, "id": "ddf19bdc-2a61-4261-8950-02ea3026ba01" } ], "combinator": "and" }, "renameOutput": true, "outputKey": "audio" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "dcf1d733-7b80-4f67-b6f6-9e965a5bae24", "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", "rightValue": "image", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "=image" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "bb7e48e1-8383-463c-a43e-7e8e5d1ae21d", "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", "rightValue": "video", "operator": { "type": "string", "operation": "equals", "name": "filter.operator.equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "VIDEO" } ] }, "options": {} }, "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ 54608, 32288 ], "id": "656a829f-b21a-4568-8614-eb60318288b5", "name": "Switch - Tipo media para Gemini WhatsApp TEST" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const item = $input.item;\n\nconst data = item.json || {};\nconst binaryData = item.binary?.data;\n\nif (!binaryData) {\n throw new Error('No llegó binary.data al nodo de audio para Gemini');\n}\n\nconst geminiPromptAudio = `\nAnaliza esta nota de voz enviada por WhatsApp para el flujo de registro de propuestas de Fulgencio Fumado.\n\nObjetivo:\nExtraer información útil del reporte y clasificar correctamente el tipo de reporte.\n\nPrimero, identifica el tipo de reporte usando exactamente uno de estos valores:\n\n1. PROPUESTA_EJECUTADA\nCuando el manager reporta evidencia de una propuesta que ya fue implementada, instalada, ejecutada, realizada o activada.\n\n2. PROPUESTA_EXTERNA\nCuando el manager reporta una propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n\n3. NO_DETERMINADO\nCuando la nota de voz no permite saber claramente si es una propuesta ejecutada o una propuesta externa.\n\nReglas para clasificar:\n- Si el manager dice explícitamente \"propuesta ejecutada\", clasifica como PROPUESTA_EJECUTADA.\n- Si el manager dice que recibió una propuesta externa, referencia externa o algo fuera del banco, clasifica como PROPUESTA_EXTERNA.\n- Si solo hay una prueba de audio, saludo, ruido, conteo, información incompleta o no se entiende el objetivo, clasifica como NO_DETERMINADO.\n- No inventes el tipo de reporte.\n- Si hay duda, usa NO_DETERMINADO.\n\nAdemás, extrae la mayor cantidad posible de información útil:\n- Nombre o referencia de la propuesta\n- Marca o cliente\n- País\n- Ubicación\n- Fecha de ejecución o fecha del reporte\n- Qué se implementó o qué se está reportando\n- Comentarios o resultados observados\n- Si el manager menciona que es ejecutada, externa o no queda claro\n\nDevuelve el análisis en texto claro y estructurado, incluyendo obligatoriamente estas líneas:\n\nTipo de reporte detectado: PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\nConfianza del tipo de reporte: ALTA | MEDIA | BAJA\nMotivo del tipo de reporte: explicación breve\n\nNo inventes datos. Si algo no está claro, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_audio: geminiPromptAudio\n },\n binary: {\n data: binaryData\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 55088, 32112 ], "id": "4686e032-679b-4a2a-a472-471aeef83b3a", "name": "Code - Preparar Gemini audio WhatsApp TEST" }, { "parameters": { "resource": "audio", "operation": "analyze", "modelId": { "__rl": true, "value": "models/gemini-2.5-pro", "mode": "list", "cachedResultName": "models/gemini-2.5-pro" }, "text": "={{ $json.gemini_prompt_audio }}", "inputType": "binary", "options": {} }, "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ 55296, 32112 ], "id": "849b5cb4-ab22-402c-baa1-f8686c700dbd", "name": "Gemini - Analizar audio WhatsApp TEST", "retryOnFail": true, "waitBetweenTries": 5000, "credentials": { "googlePalmApi": { "id": "jvsXYwL6IOoY2DBU", "name": "Isaac - Gemini Api Pago" } }, "onError": "continueRegularOutput" }, { "parameters": { "jsCode": "const geminiItems = $input.all();\nconst prepItems = $('Code - Preparar Gemini audio WhatsApp TEST').all();\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction safeJson(value) {\n try {\n return JSON.stringify(value);\n } catch (e) {\n return String(value ?? '');\n }\n}\n\nfunction extractTextFromGemini(obj) {\n if (!obj) return '';\n\n if (typeof obj === 'string') {\n return clean(obj);\n }\n\n if (typeof obj.text === 'string') return clean(obj.text);\n if (typeof obj.output === 'string') return clean(obj.output);\n if (typeof obj.response === 'string') return clean(obj.response);\n if (typeof obj.content === 'string') return clean(obj.content);\n\n const possibleParts =\n obj?.content?.parts ||\n obj?.candidates?.[0]?.content?.parts ||\n obj?.response?.candidates?.[0]?.content?.parts ||\n obj?.parts ||\n [];\n\n if (Array.isArray(possibleParts)) {\n const text = possibleParts\n .map(part => clean(part?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (text) return text;\n }\n\n const jsonText = safeJson(obj);\n return jsonText && jsonText !== '{}' ? jsonText : '';\n}\n\nfunction detectGeminiError(obj) {\n if (!obj || typeof obj !== 'object') return '';\n\n const candidates = [\n obj.error,\n obj.message,\n obj.description,\n obj.fullMessage,\n obj.full_message,\n obj.errorMessage,\n obj.error_message,\n obj?.error?.message,\n obj?.error?.description,\n obj?.error?.cause,\n obj?.json?.error,\n obj?.json?.message,\n ];\n\n const found = candidates\n .map(clean)\n .filter(Boolean)\n .join(' | ');\n\n const serialized = safeJson(obj);\n\n if (\n found ||\n serialized.includes('Internal error') ||\n serialized.includes('INTERNAL') ||\n serialized.includes('500') ||\n serialized.includes('service was not able to process')\n ) {\n return found || serialized;\n }\n\n return '';\n}\n\nconst output = [];\n\nconst total = Math.max(geminiItems.length, prepItems.length);\n\nfor (let i = 0; i < total; i++) {\n const geminiItem = geminiItems[i] || {};\n const prepItem = prepItems[i] || {};\n\n const geminiJson = geminiItem.json || {};\n const mediaJson = prepItem.json || geminiJson || {};\n\n const sessionId = clean(mediaJson.session_id);\n const ejecucionId = clean(mediaJson.ejecucion_id || sessionId);\n const mediaEventId = clean(mediaJson.media_event_id || mediaJson.event_id);\n const mediaFileName = clean(mediaJson.media_file_name || mediaJson.file_name || mediaJson.nombre_archivo);\n const driveFileId = clean(mediaJson.drive_file_id || mediaJson.file_id);\n const driveFileUrl = clean(mediaJson.drive_file_url || mediaJson.media_drive_url || mediaJson.webViewLink);\n\n const errorGemini = detectGeminiError(geminiJson);\n\n let analisisTexto = '';\n let estado = '';\n let requiereRevision = false;\n\n if (errorGemini) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_ERROR_GEMINI';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini no pudo procesar esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar propuesta, cliente, marca, país, ubicación o contexto.',\n '',\n `Detalle técnico: ${errorGemini}`,\n ].join('\\n');\n } else {\n analisisTexto = extractTextFromGemini(geminiJson);\n\n if (!analisisTexto) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_VACIO';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini respondió, pero no devolvió texto útil para esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar la propuesta.',\n ].join('\\n');\n } else {\n estado = 'ANALISIS_AUDIO_COMPLETADO';\n }\n }\n\n if (!sessionId) {\n throw new Error('No llegó session_id al normalizar análisis de audio.');\n }\n\n if (!mediaEventId) {\n throw new Error('No llegó media_event_id al normalizar análisis de audio.');\n }\n\n output.push({\n json: {\n ...mediaJson,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: 'audio',\n media_file_name: mediaFileName,\n drive_file_id: driveFileId,\n drive_file_url: driveFileUrl,\n\n analisis_texto: analisisTexto,\n fecha_analisis: nowIso(),\n estado,\n\n gemini_audio_error: errorGemini || '',\n audio_requiere_revision: requiereRevision,\n\n media_index: Number(mediaJson.media_index || 0),\n media_total: Number(mediaJson.media_total || mediaJson.media_total_esperado || 0),\n },\n });\n}\n\nreturn output;" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 55504, 32112 ], "id": "ffdd7a46-0fa2-41e2-afb7-49b49765e52e", "name": "Code - Normalizar análisis audio WhatsApp TEST" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const input = $input.item;\nconst data = input.json || {};\nconst binaries = input.binary || {};\n\nconst binaryKeys = Object.keys(binaries);\nconst sourceBinaryKey = binaries.data ? 'data' : binaryKeys[0];\n\nif (!sourceBinaryKey || !binaries[sourceBinaryKey]) {\n throw new Error(\n `No llegó archivo binario para Gemini imagen. Binary keys recibidas: ${binaryKeys.join(', ') || 'NINGUNA'}`\n );\n}\n\nconst binaryData = {\n ...binaries[sourceBinaryKey],\n};\n\nbinaryData.fileName =\n binaryData.fileName ||\n data.media_file_name ||\n `imagen_${data.media_index || Date.now()}.jpg`;\n\nbinaryData.mimeType =\n binaryData.mimeType ||\n data.mime_type ||\n data.mimetype ||\n 'image/jpeg';\n\nconst geminiPromptImagen = `\nAnaliza esta imagen como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en la imagen\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Posible ubicación visible\n- Calidad de la implementación\n- Detalles relevantes para documentar la ejecución\n\nNo inventes datos que no sean visibles.\nSi algo no se puede identificar, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_imagen: geminiPromptImagen,\n binary_input_field: 'data',\n debug_binary_imagen: {\n binary_keys_recibidas: binaryKeys,\n source_binary_key: sourceBinaryKey,\n output_binary_key: 'data',\n file_name: binaryData.fileName,\n mime_type: binaryData.mimeType,\n },\n },\n binary: {\n data: binaryData,\n },\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 55104, 32336 ], "id": "d2eaab10-ae91-4748-99c0-dd63e8323495", "name": "Code - Preparar Gemini imagen WhatsApp TEST" }, { "parameters": { "resource": "image", "operation": "analyze", "modelId": { "__rl": true, "value": "models/gemini-2.5-pro", "mode": "list", "cachedResultName": "models/gemini-2.5-pro" }, "text": "={{ $json.gemini_prompt_imagen }}", "inputType": "binary", "options": {} }, "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ 55312, 32336 ], "id": "19f27963-b01f-4ac2-a133-c109e84b8aa2", "name": "Gemini - Analizar imagen WhatsApp TEST", "credentials": { "googlePalmApi": { "id": "jvsXYwL6IOoY2DBU", "name": "Isaac - Gemini Api Pago" } } }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini imagen WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'image',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_IMAGEN_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 55520, 32336 ], "id": "8ca940c9-108d-4676-acc3-29822f52a162", "name": "Code - Normalizar análisis imagen WhatsApp TEST" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const data = $json || {};\nconst binary = $binary || {};\n\nif (!binary.data) {\n throw new Error('No llegó binary.data al nodo Code - Preparar Gemini video.');\n}\n\nconst fileName =\n binary.data.fileName ||\n data.media_file_name ||\n data.file_name ||\n `video_${Date.now()}.mp4`;\n\nconst mimeType =\n binary.data.mimeType ||\n data.mime_type ||\n 'video/mp4';\n\n// Fuerza a n8n a cargar el contenido real del binario.\n// Esto evita el error de Gemini: \"Received undefined\".\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n\nif (!buffer || !Buffer.isBuffer(buffer) || buffer.length === 0) {\n throw new Error('El video llegó con metadata, pero sin contenido binario real.');\n}\n\nconst preparedBinary = await this.helpers.prepareBinaryData(\n buffer,\n fileName,\n mimeType\n);\n\nconst geminiPromptVideo = `\nAnaliza este video como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en el video\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Interacciones o movimientos relevantes\n- Posible ubicación visible\n- Calidad de la implementación\n- Resultados observables\n\nNo inventes datos que no sean visibles.\n`.trim();\n\nreturn {\n json: {\n ...data,\n media_type: 'video',\n media_file_name: fileName,\n mime_type: mimeType,\n gemini_prompt_video: geminiPromptVideo,\n },\n binary: {\n data: preparedBinary,\n },\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 54976, 32576 ], "id": "6cc2ea4e-0fb2-4eaa-a0f5-0e72a51d39d5", "name": "Code - Preparar Gemini video WhatsApp TEST" }, { "parameters": { "resource": "video", "operation": "analyze", "modelId": { "__rl": true, "value": "models/gemini-2.5-pro", "mode": "list", "cachedResultName": "models/gemini-2.5-pro" }, "text": "={{ $json.gemini_prompt_video }}", "inputType": "binary", "options": {} }, "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ 55184, 32576 ], "id": "e3aa489a-510e-4536-af34-4687ecc5e13b", "name": "Analyze video", "credentials": { "googlePalmApi": { "id": "jvsXYwL6IOoY2DBU", "name": "Isaac - Gemini Api Pago" } } }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini video WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'video',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_VIDEO_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 55392, 32576 ], "id": "bc43c81d-c79f-4318-80c0-4991c4e28c8c", "name": "Code - Normalizar análisis video WhatsApp TEST" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 1107394537, "mode": "list", "cachedResultName": "wa_ejecuciones_analisis_media", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "ejecucion_id": "={{ $json.ejecucion_id }}", "media_event_id": "={{ $json.media_event_id }}", "media_type": "={{ $json.media_type }}", "media_file_name": "={{ $json.media_file_name }}", "analisis_texto": "={{ $json.analisis_texto }}", "fecha_analisis": "={{ $json.fecha_analisis }}", "estado": "={{ $json.estado }}", "media_index": "={{ $json.media_index }}", "media_total": "={{ $json.media_total }}" }, "matchingColumns": [], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_event_id", "displayName": "media_event_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_type", "displayName": "media_type", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_file_name", "displayName": "media_file_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "analisis_texto", "displayName": "analisis_texto", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_analisis", "displayName": "fecha_analisis", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_index", "displayName": "media_index", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_total", "displayName": "media_total", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 56176, 32368 ], "id": "9d11dc64-15b2-4e39-8e5d-5c662748f1dc", "name": "Sheets - Guardar análisis media Gemini WhatsApp TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 1107394537, "mode": "list", "cachedResultName": "wa_ejecuciones_analisis_media", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 59968, 32176 ], "id": "612ac4ae-154b-4c7b-b51a-ad952fe2564b", "name": "Sheets - Leer análisis media Gemini WhatsApp TEST", "alwaysOutputData": true, "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst inputRows = $input.all().map(item => item.json || {});\nconst actual = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst extraerTextoGemini = (valor) => {\n if (valor === null || valor === undefined) return '';\n\n if (typeof valor === 'object') {\n const partes =\n valor?.content?.parts ||\n valor?.response?.content?.parts ||\n valor?.candidates?.[0]?.content?.parts ||\n valor?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(valor);\n }\n\n const raw = limpiar(valor);\n if (!raw) return '';\n\n try {\n const parsed = JSON.parse(raw);\n\n const partes =\n parsed?.content?.parts ||\n parsed?.response?.content?.parts ||\n parsed?.candidates?.[0]?.content?.parts ||\n parsed?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(parsed);\n } catch (error) {\n return raw;\n }\n};\n\nconst normalizarTipoMedia = (valor) => {\n const t = limpiar(valor).toLowerCase();\n\n if (t.includes('audio')) return 'audio';\n if (t.includes('image') || t.includes('imagen')) return 'image';\n if (t.includes('video')) return 'video';\n\n return t || 'unknown';\n};\n\nconst obtenerFecha = (row) => {\n const fecha = new Date(row.fecha_analisis || row.fecha_recepcion || row.ultima_actividad || 0);\n const time = fecha.getTime();\n return Number.isFinite(time) ? time : 0;\n};\n\n// Contextos posibles del cierre actual.\n// IMPORTANTE: NO usamos $json como prioridad, porque aquí $json puede ser la primera fila del Sheet.\nconst decisionPorSheet = getNodeJson('Code - Decidir cierre análisis por Sheet Redis TEST');\nconst validarLock = getNodeJson('Code - Validar lock análisis final TEST');\nconst validarConteo = getNodeJson('Code - Validar conteo análisis Redis TEST');\nconst cierreMedia = getNodeJson('Code - Consolidar media recuperada WhatsApp TEST');\nconst eventoPaso = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst eventoSesion = getNodeJson('Code - Resolver sesión WhatsApp TEST');\n\nlet sessionId = limpiar(\n decisionPorSheet.session_id ||\n validarLock.session_id ||\n validarConteo.session_id ||\n cierreMedia.session_id ||\n eventoPaso.session_id ||\n eventoSesion.session_id ||\n ''\n);\n\nlet ejecucionId = limpiar(\n decisionPorSheet.ejecucion_id ||\n validarLock.ejecucion_id ||\n validarConteo.ejecucion_id ||\n cierreMedia.ejecucion_id ||\n eventoPaso.ejecucion_id ||\n eventoSesion.ejecucion_id ||\n sessionId\n);\n\n// Si por cualquier razón no llegó session_id desde el contexto,\n// usamos la sesión más reciente del Sheet leído.\nif (!sessionId) {\n const grupos = {};\n\n for (const row of inputRows) {\n const sid = limpiar(row.session_id);\n if (!sid) continue;\n\n if (!grupos[sid]) {\n grupos[sid] = {\n session_id: sid,\n ejecucion_id: limpiar(row.ejecucion_id || sid),\n rows: [],\n ultima_fecha: 0\n };\n }\n\n grupos[sid].rows.push(row);\n grupos[sid].ultima_fecha = Math.max(grupos[sid].ultima_fecha, obtenerFecha(row));\n }\n\n const grupoMasReciente = Object.values(grupos)\n .sort((a, b) => b.ultima_fecha - a.ultima_fecha)[0];\n\n if (grupoMasReciente) {\n sessionId = grupoMasReciente.session_id;\n ejecucionId = grupoMasReciente.ejecucion_id || sessionId;\n }\n}\n\nif (!sessionId) {\n throw new Error('No llegó session_id para consolidar análisis de media.');\n}\n\n// Primero intentamos usar las filas filtradas por el nodo de cierre.\n// Si no vienen, usamos las filas leídas del Sheet.\nlet rows = [];\n\nif (\n Array.isArray(decisionPorSheet.analisis_rows_filtrados) &&\n decisionPorSheet.analisis_rows_filtrados.length > 0\n) {\n rows = decisionPorSheet.analisis_rows_filtrados;\n} else {\n rows = inputRows;\n}\n\n// Filtrar solo la sesión correcta.\nrows = rows.filter(row => limpiar(row.session_id) === sessionId);\n\n// Si todavía no encontró nada, usar fallback por ejecución.\nif (rows.length === 0 && ejecucionId) {\n rows = inputRows.filter(row => limpiar(row.ejecucion_id) === ejecucionId);\n}\n\n// Deduplicar por media_event_id + media_type.\nconst vistos = new Set();\nconst analisisUnicos = [];\n\nfor (const row of rows) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaType = normalizarTipoMedia(row.media_type);\n\n if (!mediaEventId || !mediaType) continue;\n\n const key = `${mediaEventId}_${mediaType}`;\n if (vistos.has(key)) continue;\n\n const textoAnalisis = extraerTextoGemini(row.analisis_texto);\n\n if (!textoAnalisis) continue;\n\n vistos.add(key);\n\n analisisUnicos.push({\n ...row,\n media_type: mediaType,\n media_index: Number(row.media_index || 0),\n media_total: Number(row.media_total || row.media_total_esperado || 0),\n analisis_texto_limpio: textoAnalisis\n });\n}\n\nanalisisUnicos.sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst audios = analisisUnicos.filter(row => row.media_type === 'audio');\nconst imagenes = analisisUnicos.filter(row => row.media_type === 'image');\nconst videos = analisisUnicos.filter(row => row.media_type === 'video');\n\nconst audiosCount = audios.length;\nconst fotosCount = imagenes.length;\nconst videosCount = videos.length;\n\nconst mediaTotalEsperado = Number(\n decisionPorSheet.media_total_esperado ||\n validarConteo.media_total_esperado ||\n cierreMedia.total_media_drive ||\n cierreMedia.total_media ||\n analisisUnicos[0]?.media_total ||\n analisisUnicos.length ||\n 0\n);\n\nconst analisisAudioTexto = audios\n .map(row => row.analisis_texto_limpio)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisImagenesTexto = imagenes\n .map((row, index) => `IMAGEN ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisVideosTexto = videos\n .map((row, index) => `VIDEO ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisConsolidado = [\n analisisAudioTexto ? `ANÁLISIS DE AUDIO:\\n${analisisAudioTexto}` : '',\n analisisImagenesTexto ? `ANÁLISIS DE IMÁGENES:\\n${analisisImagenesTexto}` : '',\n analisisVideosTexto ? `ANÁLISIS DE VIDEOS:\\n${analisisVideosTexto}` : ''\n].filter(Boolean).join('\\n\\n');\n\nif (!analisisConsolidado) {\n throw new Error(\n `No hay análisis multimedia consolidado para enviar a Gemini final. session_id=${sessionId}, ejecucion_id=${ejecucionId}, rows_filtradas=${rows.length}, input_rows=${inputRows.length}`\n );\n}\n\nconst transcripcionAudio = analisisAudioTexto || '';\n\nconst geminiPromptFinal = `\nEres un analista de evidencias de GomezLee Marketing / Fulgencio Fumado.\n\nTu tarea es consolidar el análisis de audio, imágenes y videos de un reporte recibido por WhatsApp.\n\nIMPORTANTE:\n- No inventes información.\n- Si un dato no aparece claramente, responde \"No identificado\".\n- El audio tiene prioridad para identificar propuesta, marca, país, ubicación, fecha y tipo de reporte.\n- Las imágenes y videos sirven como evidencia visual.\n- Clasifica el reporte usando exactamente uno de estos valores:\n - PROPUESTA_EJECUTADA\n - PROPUESTA_EXTERNA\n - NO_DETERMINADO\n\nDefiniciones:\n- PROPUESTA_EJECUTADA: evidencia de una propuesta ya implementada, instalada, ejecutada, realizada o activada.\n- PROPUESTA_EXTERNA: propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n- NO_DETERMINADO: la información no permite saber claramente si es ejecutada o externa.\n\nDATOS DEL REPORTE:\n- session_id: ${sessionId}\n- ejecucion_id: ${ejecucionId}\n- audios_count: ${audiosCount}\n- fotos_count: ${fotosCount}\n- videos_count: ${videosCount}\n\nANÁLISIS MULTIMEDIA CONSOLIDADO:\n${analisisConsolidado}\n\nDevuelve ÚNICAMENTE un JSON válido, sin markdown, sin explicación adicional y sin texto fuera del JSON.\n\nEl JSON debe tener exactamente esta estructura:\n\n{\n \"tipo_reporte\": \"PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\",\n \"tipo_reporte_confianza\": \"ALTA | MEDIA | BAJA\",\n \"motivo_tipo_reporte\": \"texto breve\",\n \"propuesta_referencia\": \"texto o No identificado\",\n \"marca\": \"texto o No identificado\",\n \"cliente\": \"texto o No identificado\",\n \"pais\": \"texto o No identificado\",\n \"ubicacion\": \"texto o No identificado\",\n \"fecha_ejecucion\": \"texto o No identificado\",\n \"descripcion_ejecucion\": \"texto claro y profesional\",\n \"elementos_detectados\": \"lista resumida en texto\",\n \"resumen_ia\": \"resumen ejecutivo del reporte\",\n \"comentarios_resultados\": \"texto o No identificado\",\n \"tags\": \"tags separados por coma\"\n}\n`.trim();\n\nreturn [\n {\n json: {\n ...cierreMedia,\n ...eventoSesion,\n ...eventoPaso,\n ...validarConteo,\n ...validarLock,\n ...decisionPorSheet,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_media: analisisUnicos.length,\n\n audio_count: audiosCount,\n audios_count: audiosCount,\n imagenes_count: fotosCount,\n fotos_count: fotosCount,\n videos_count: videosCount,\n\n transcripcion_audio: transcripcionAudio,\n\n analisis_audio: analisisAudioTexto,\n analisis_imagenes: analisisImagenesTexto,\n analisis_videos: analisisVideosTexto,\n\n analisis_audio_texto: analisisAudioTexto,\n analisis_imagenes_texto: analisisImagenesTexto,\n analisis_videos_texto: analisisVideosTexto,\n\n analisis_media_consolidado: analisisConsolidado,\n analisis_multimedia: analisisConsolidado,\n analisis_consolidado: analisisConsolidado,\n\n gemini_prompt_final: geminiPromptFinal,\n\n analisis_rows_filtrados: analisisUnicos,\n\n estado_analisis_media: 'ANALISIS_MEDIA_CONSOLIDADO',\n\n consolidacion_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n input_rows: inputRows.length,\n rows_filtradas: rows.length,\n total_analisis_unicos: analisisUnicos.length,\n media_total_esperado: mediaTotalEsperado,\n audios: audiosCount,\n imagenes: fotosCount,\n videos: videosCount,\n gemini_prompt_final_generado: Boolean(geminiPromptFinal),\n gemini_prompt_final_length: geminiPromptFinal.length,\n uso_decision_por_sheet: Object.keys(decisionPorSheet).length > 0,\n uso_validar_lock: Object.keys(validarLock).length > 0,\n uso_validar_conteo: Object.keys(validarConteo).length > 0,\n media: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado,\n media_file_name: row.media_file_name\n }))\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 60864, 32208 ], "id": "4f8a76a2-9311-4337-972a-8fc4378cb5dc", "name": "Code - Consolidar análisis media Gemini WhatsApp TEST" }, { "parameters": { "modelId": { "__rl": true, "value": "models/gemini-2.5-pro", "mode": "list", "cachedResultName": "models/gemini-2.5-pro" }, "messages": { "values": [ { "content": "={{ $json.gemini_prompt_final }}" } ] }, "builtInTools": {}, "options": {} }, "type": "@n8n/n8n-nodes-langchain.googleGemini", "typeVersion": 1.2, "position": [ 61056, 32208 ], "id": "4e5a1a2d-961f-4947-98ab-813a710e12a8", "name": "Gemini - Generar JSON final propuesta ejecutada TEST", "credentials": { "googlePalmApi": { "id": "jvsXYwL6IOoY2DBU", "name": "Isaac - Gemini Api Pago" } } }, { "parameters": { "jsCode": "const gemini = $json || {};\nconst base = $('Code - Consolidar análisis media Gemini WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst esNoIdentificado = (valor) => {\n const texto = normalizarPlano(valor);\n\n return (\n !texto ||\n texto === 'NO IDENTIFICADO' ||\n texto === 'NO DISPONIBLE' ||\n texto === 'N/A' ||\n texto === 'NA' ||\n texto === 'NULL' ||\n texto === 'UNDEFINED'\n );\n};\n\nconst valor = (campo, fallback = 'No identificado') => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return fallback;\n\n return v;\n};\n\nconst valorOpcional = (campo) => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return '';\n\n return v;\n};\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (Array.isArray(obj.content?.parts)) {\n const txt = obj.content.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.parts)) {\n const txt = obj.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.candidates)) {\n const txt = obj.candidates\n .flatMap(c => c.content?.parts || [])\n .map(p => p.text || '')\n .join('\\n')\n .trim();\n\n if (txt) return txt;\n }\n\n if (obj.message && typeof obj.message === 'object') {\n const txt = extraerTexto(obj.message);\n if (txt) return txt;\n }\n\n if (obj.data && typeof obj.data === 'object') {\n const txt = extraerTexto(obj.data);\n if (txt) return txt;\n }\n\n return JSON.stringify(obj);\n};\n\nlet texto = extraerTexto(gemini).trim();\n\ntexto = texto\n .replace(/^```json/i, '')\n .replace(/^```/i, '')\n .replace(/```$/i, '')\n .trim();\n\nlet parsed = {};\n\ntry {\n parsed = JSON.parse(texto);\n} catch (error) {\n parsed = {\n tipo_reporte: 'NO_DETERMINADO',\n tipo_reporte_confianza: 'BAJA',\n tipo_reporte_motivo: `No se pudo parsear el JSON final de Gemini: ${error.message}`,\n\n propuesta_referencia: 'No identificado',\n marca: 'No identificado',\n cliente: 'No identificado',\n pais: 'No identificado',\n ubicacion: 'No identificado',\n fecha_ejecucion: 'No identificado',\n que_se_implemento: 'No identificado',\n comentarios_resultados: '',\n resumen_ejecucion: texto || 'No se pudo parsear el JSON de Gemini.',\n evidencia_audio_resumen: base.texto_audio || '',\n evidencia_imagenes_resumen: base.texto_imagenes || '',\n evidencia_videos_resumen: base.texto_videos || '',\n nivel_confianza: 'BAJA',\n requiere_revision: 'SI',\n motivo_revision: `Error parseando JSON final: ${error.message}`\n };\n}\n\nconst normalizarTipoReporte = (valorTipo) => {\n const texto = String(valorTipo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, '_');\n\n if (\n texto === 'PROPUESTA_EJECUTADA' ||\n texto === 'EJECUTADA' ||\n texto === 'EJECUTADO'\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n if (\n texto === 'PROPUESTA_EXTERNA' ||\n texto === 'EXTERNA' ||\n texto === 'EXTERNO'\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n return 'NO_DETERMINADO';\n};\n\nconst normalizarConfianza = (valorConfianza) => {\n const texto = String(valorConfianza ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['ALTA', 'MEDIA', 'BAJA'].includes(texto)) {\n return texto;\n }\n\n return 'BAJA';\n};\n\nconst normalizarSiNo = (valorSiNo, fallback = 'SI') => {\n const texto = String(valorSiNo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['SI', 'SÍ', 'YES', 'TRUE'].includes(texto)) return 'SI';\n if (['NO', 'FALSE'].includes(texto)) return 'NO';\n\n return fallback;\n};\n\nconst limpiarReferenciaVisual = (valorRef) => {\n let textoRef = limpiar(valorRef);\n\n if (esNoIdentificado(textoRef)) return 'No identificado';\n\n textoRef = textoRef\n .replace(/_/g, ' ')\n .replace(/\\+/g, '+')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return textoRef || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valorUbicacion) => {\n const textoUbicacion = normalizarPlano(valorUbicacion);\n\n if (!textoUbicacion) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bVERSIÓN\\b/,\n /\\bCODIGO\\b/,\n /\\bCÓDIGO\\b/,\n /\\bGUATE WMC\\b/,\n /\\bPOLLO CAMPERO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(textoUbicacion));\n};\n\nconst normalizarUbicacion = (valorUbicacion) => {\n const textoUbicacion = limpiar(valorUbicacion);\n\n if (esNoIdentificado(textoUbicacion)) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(textoUbicacion)) {\n return 'No identificado';\n }\n\n return textoUbicacion;\n};\n\nconst normalizarFechaEjecucion = (valorFecha) => {\n const textoFecha = limpiar(valorFecha);\n\n if (esNoIdentificado(textoFecha)) return 'No identificado';\n\n // Si solo viene el año, no es una fecha de ejecución real.\n if (/^20\\d{2}$/.test(textoFecha)) {\n return 'No identificado';\n }\n\n return textoFecha;\n};\n\nconst limitarTexto = (valorTexto, max = 1200, fallback = '') => {\n const textoLimpio = valor(valorTexto, fallback);\n\n if (!textoLimpio) return '';\n\n return textoLimpio.length > max\n ? `${textoLimpio.slice(0, max - 3)}...`\n : textoLimpio;\n};\n\nconst ahora = new Date().toISOString();\n\nconst tipoReporte = normalizarTipoReporte(parsed.tipo_reporte);\nconst tipoReporteConfianza = normalizarConfianza(parsed.tipo_reporte_confianza);\nconst tipoReporteMotivo = valor(parsed.tipo_reporte_motivo, 'No identificado');\n\nconst requiereRevision = normalizarSiNo(parsed.requiere_revision, 'SI');\n\nconst estadoRevision = requiereRevision === 'SI'\n ? 'PENDIENTE_REVISION'\n : 'ANALISIS_COMPLETADO';\n\nconst propuestaReferenciaOriginal = valor(parsed.propuesta_referencia);\nconst propuestaReferenciaLimpia = limpiarReferenciaVisual(propuestaReferenciaOriginal);\n\nconst fechaEjecucionOriginal = valor(parsed.fecha_ejecucion);\nconst fechaEjecucionLimpia = normalizarFechaEjecucion(fechaEjecucionOriginal);\n\nconst ubicacionOriginal = valor(parsed.ubicacion);\nconst ubicacionLimpia = normalizarUbicacion(ubicacionOriginal);\n\nconst evidenciaAudio = valorOpcional(parsed.evidencia_audio_resumen || base.texto_audio);\nconst evidenciaImagenes = valorOpcional(parsed.evidencia_imagenes_resumen || base.texto_imagenes);\nconst evidenciaVideos = valorOpcional(parsed.evidencia_videos_resumen || base.texto_videos);\n\nconst elementosDetectados = [\n evidenciaImagenes,\n evidenciaVideos\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .join('\\n\\n');\n\nconst tagsSet = new Set();\n\n[\n parsed.marca,\n parsed.cliente,\n parsed.pais,\n ubicacionLimpia\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .forEach(v => tagsSet.add(v));\n\nconst tags = [...tagsSet].join(', ');\n\nconst resumenIa = limitarTexto(parsed.resumen_ejecucion, 1200, '');\nconst descripcionEjecucion = limitarTexto(parsed.que_se_implemento, 1200, 'No identificado');\nconst comentariosResultados = limitarTexto(parsed.comentarios_resultados, 900, '');\n\nreturn [\n {\n json: {\n ...base,\n\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n session_id: base.session_id || '',\n\n fecha_recepcion: base.fecha_recepcion || ahora,\n fecha_ejecucion: fechaEjecucionLimpia,\n fecha_ejecucion_original_ia: fechaEjecucionOriginal,\n\n manager_nombre: valor(base.manager_nombre),\n manager_telefono: valor(base.manager_telefono),\n canal_origen: base.canal_origen || 'WHATSAPP',\n\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza,\n tipo_reporte_motivo: tipoReporteMotivo,\n\n propuesta_referencia: propuestaReferenciaLimpia,\n propuesta_referencia_original_ia: propuestaReferenciaOriginal,\n\n propuesta_match_estado: 'PENDIENTE',\n propuesta_match_confianza: '',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_match_revision: 'PENDIENTE_MATCH_BANCO',\n\n marca: valor(parsed.marca),\n cliente: valor(parsed.cliente),\n pais: valor(parsed.pais),\n ubicacion: ubicacionLimpia,\n ubicacion_original_ia: ubicacionOriginal,\n\n comentario_original: evidenciaAudio || comentariosResultados || '',\n resumen_ia: resumenIa,\n descripcion_ejecucion: descripcionEjecucion,\n comentarios_resultados: comentariosResultados,\n elementos_detectados: elementosDetectados,\n tags,\n\n media_folder_url: base.media_folder_url || '',\n presentacion_ejecucion_url: '',\n\n fotos_count: base.total_analisis_imagenes || 0,\n videos_count: base.total_analisis_videos || 0,\n audios_count: base.total_analisis_audio || 0,\n\n estado_revision: estadoRevision,\n motivo_revision: valor(parsed.motivo_revision, ''),\n ultima_actualizacion: ahora,\n\n transcripcion_audio: evidenciaAudio,\n\n nivel_confianza: normalizarConfianza(parsed.nivel_confianza),\n requiere_revision: requiereRevision,\n\n gemini_json_raw: texto,\n\n normalizacion_json_final_debug: {\n propuesta_referencia_original: propuestaReferenciaOriginal,\n propuesta_referencia_limpia: propuestaReferenciaLimpia,\n fecha_ejecucion_original: fechaEjecucionOriginal,\n fecha_ejecucion_limpia: fechaEjecucionLimpia,\n ubicacion_original: ubicacionOriginal,\n ubicacion_limpia: ubicacionLimpia,\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 61408, 32208 ], "id": "802c8812-f0d2-4a69-ace0-2487f7cf1267", "name": "Code - Normalizar JSON final propuesta ejecutada TEST" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 737153956, "mode": "list", "cachedResultName": "propuestas_ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" }, "columns": { "mappingMode": "defineBelow", "value": { "ejecucion_id": "={{ $json.ejecucion_id }}", "fecha_recepcion": "={{ $json.fecha_recepcion }}", "fecha_ejecucion": "={{ $json.fecha_ejecucion }}", "manager_nombre": "={{ $json.manager_nombre }}", "manager_telefono": "={{ $json.manager_telefono }}", "propuesta_referencia": "={{ $json.propuesta_referencia }}", "propuesta_match_estado": "={{ $json.propuesta_match_estado }}", "propuesta_match_confianza": "={{ $json.propuesta_match_confianza }}", "propuesta_nombre_banco": "={{ $json.propuesta_nombre_banco }}", "propuesta_link_banco": "={{ $json.propuesta_link_banco }}", "marca": "={{ $json.marca }}", "cliente": "={{ $json.cliente }}", "pais": "={{ $json.pais }}", "ubicacion": "={{ $json.ubicacion }}", "comentario_original": "={{ $json.comentario_original }}", "resumen_ia": "={{ $json.resumen_ia }}", "descripcion_ejecucion": "={{ $json.descripcion_ejecucion }}", "elementos_detectados": "={{ $json.elementos_detectados }}", "tags": "={{ $json.tags }}", "media_folder_url": "={{ $json.media_folder_url }}", "presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}", "fotos_count": "={{ $json.fotos_count }}", "videos_count": "={{ $json.videos_count }}", "audios_count": "={{ $json.audios_count }}", "estado_revision": "={{ $json.estado_revision }}", "motivo_revision": "={{ $json.motivo_revision }}", "ultima_actualizacion": "={{ $json.ultima_actualizacion }}", "session_id": "={{ $json.session_id }}", "canal_origen": "={{ $json.canal_origen }}", "transcripcion_audio": "={{ $json.transcripcion_audio }}", "propuesta_match_revision": "={{ $json.propuesta_match_revision }}", "tipo_reporte": "={{ $json.tipo_reporte }}", "tipo_reporte_confianza": "={{ $json.tipo_reporte_confianza }}", "tipo_reporte_motivo": "={{ $json.tipo_reporte_motivo }}", "decision_automatica_banco": "={{ $json.decision_automatica_banco }}", "motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}", "banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}" }, "matchingColumns": [], "schema": [ { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_recepcion", "displayName": "fecha_recepcion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_ejecucion", "displayName": "fecha_ejecucion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_referencia", "displayName": "propuesta_referencia", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_match_estado", "displayName": "propuesta_match_estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_match_confianza", "displayName": "propuesta_match_confianza", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_nombre_banco", "displayName": "propuesta_nombre_banco", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_link_banco", "displayName": "propuesta_link_banco", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "marca", "displayName": "marca", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "cliente", "displayName": "cliente", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "pais", "displayName": "pais", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ubicacion", "displayName": "ubicacion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "comentario_original", "displayName": "comentario_original", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "resumen_ia", "displayName": "resumen_ia", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "descripcion_ejecucion", "displayName": "descripcion_ejecucion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "elementos_detectados", "displayName": "elementos_detectados", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "tags", "displayName": "tags", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_folder_url", "displayName": "media_folder_url", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "presentacion_ejecucion_url", "displayName": "presentacion_ejecucion_url", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fotos_count", "displayName": "fotos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audios_count", "displayName": "audios_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado_revision", "displayName": "estado_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ultima_actualizacion", "displayName": "ultima_actualizacion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "transcripcion_audio", "displayName": "transcripcion_audio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_match_revision", "displayName": "propuesta_match_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte_confianza", "displayName": "tipo_reporte_confianza", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte_motivo", "displayName": "tipo_reporte_motivo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "decision_automatica_banco", "displayName": "decision_automatica_banco", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "motivo_decision_automatica", "displayName": "motivo_decision_automatica", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "banco_actualizado_auto", "displayName": "banco_actualizado_auto", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 63152, 32288 ], "id": "370daef7-6f4d-466a-8df5-0dbc4770a357", "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": [ 70480, 32176 ], "id": "ba545fe7-4742-475b-90e0-f0a26d4e6394", "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\nconst data = {\n ...basePaso,\n ...normalizado,\n ...actual,\n ...presentacion,\n};\n\nconst clean = (value) => String(value ?? '').trim();\nconst digits = (value) => clean(value).replace(/\\D/g, '');\n\nconst whatsappTo = digits(\n data.whatsapp_to ||\n data.sender_phone ||\n data.manager_telefono ||\n data.whatsapp_remote_jid\n);\n\nif (!whatsappTo) {\n throw new Error('No se encontró el número privado para enviar el mensaje final.');\n}\n\nconst presentationUrl = clean(\n data.presentacion_ejecucion_url ||\n data.presentation_url\n);\n\nconst language = clean(\n data.idioma_flujo ||\n data.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst lines = [];\n\nif (language === 'EN') {\n lines.push('✅ *Report processed successfully*');\n} else {\n lines.push('✅ *Reporte procesado correctamente*');\n}\n\nif (presentationUrl) {\n lines.push('');\n lines.push(language === 'EN' ? '🔗 *Presentation:*' : '🔗 *Presentación:*');\n lines.push(presentationUrl);\n}\n\nlines.push('');\nlines.push(\n language === 'EN'\n ? 'You can start a new report or view the country leaderboard.'\n : 'Puedes iniciar un nuevo reporte o consultar el leaderboard por país.'\n);\n\nconst whatsappText = lines.join('\\n');\n\nreturn [\n {\n json: {\n ...data,\n is_group: false,\n group_id: '',\n group_jid: '',\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: 'individual',\n whatsapp_header: '',\n whatsapp_text: whatsappText,\n whatsapp_buttons: [\n { id: 'HEY', title: language === 'EN' ? 'New report' : 'Nuevo reporte' },\n { id: 'RANKING', title: language === 'EN' ? 'View ranking' : 'Ver ranking' }\n ]\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 70768, 32144 ], "id": "d03a9722-6691-46d6-ae9f-f94d18a537f4", "name": "Code - Preparar mensaje final WhatsApp TEST" }, { "parameters": {}, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ 59776, 32176 ], "id": "ba1836dd-c92e-4404-b11a-59884fc35185", "name": "Wait", "webhookId": "3d858751-a035-4915-8ee0-caf8f19c78a5" }, { "parameters": { "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 737153956, "mode": "list", "cachedResultName": "propuestas_ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 62368, 32240 ], "id": "e1f72e60-642b-4592-a88d-21a99b305340", "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": [ 62576, 32240 ], "id": "303939b4-8a6d-4bd1-95c6-01adf62e3a46", "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": [ 62784, 32240 ], "id": "6a7ce17f-e14b-44c4-a844-1dba44e2c04f", "name": "IF - Propuesta final ya existe TEST" }, { "parameters": { "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '1gHgQBDL2uFaa7yNwcyWrtE24B_UrCU1Dy-n63TPurPE';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 63360, 32256 ], "id": "7328afc9-42bb-4c50-b493-b83d72c743eb", "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": [ 64912, 32256 ], "id": "46d29cff-0099-4a6e-8771-cdc70545c541", "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": [ 65184, 32256 ], "id": "3544e093-75f6-4580-a6c6-bc5fb4cfb4ed", "name": "Code - Normalizar link presentación ejecución TEST" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 737153956, "mode": "list", "cachedResultName": "propuestas_ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" }, "columns": { "mappingMode": "defineBelow", "value": { "presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}", "ultima_actualizacion": "={{ new Date().toISOString() }}", "ejecucion_id": "={{ $json.ejecucion_id }}", "session_id": "={{ $json.session_id }}", "media_folder_url": "={{ $json.media_folder_url }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_recepcion", "displayName": "fecha_recepcion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_ejecucion", "displayName": "fecha_ejecucion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_referencia", "displayName": "propuesta_referencia", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_match_estado", "displayName": "propuesta_match_estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_match_confianza", "displayName": "propuesta_match_confianza", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_nombre_banco", "displayName": "propuesta_nombre_banco", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_link_banco", "displayName": "propuesta_link_banco", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "marca", "displayName": "marca", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "cliente", "displayName": "cliente", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "pais", "displayName": "pais", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ubicacion", "displayName": "ubicacion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "comentario_original", "displayName": "comentario_original", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "resumen_ia", "displayName": "resumen_ia", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "descripcion_ejecucion", "displayName": "descripcion_ejecucion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "elementos_detectados", "displayName": "elementos_detectados", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "tags", "displayName": "tags", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_folder_url", "displayName": "media_folder_url", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "presentacion_ejecucion_url", "displayName": "presentacion_ejecucion_url", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fotos_count", "displayName": "fotos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "audios_count", "displayName": "audios_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado_revision", "displayName": "estado_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ultima_actualizacion", "displayName": "ultima_actualizacion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "transcripcion_audio", "displayName": "transcripcion_audio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "propuesta_match_revision", "displayName": "propuesta_match_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte_confianza", "displayName": "tipo_reporte_confianza", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "tipo_reporte_motivo", "displayName": "tipo_reporte_motivo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "decision_automatica_banco", "displayName": "decision_automatica_banco", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "motivo_decision_automatica", "displayName": "motivo_decision_automatica", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "banco_actualizado_auto", "displayName": "banco_actualizado_auto", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 68400, 32112 ], "id": "cf34fb57-81ec-44d9-a5e1-7ada19888936", "name": "Sheets - Actualizar link presentación ejecución TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst carpetaPreparada = getNodeJson('Code - Preparar carpeta ejecución TEST');\nconst carpetaDrive = getNodeJson('Drive - Crear carpeta ejecución TEST');\nconst carpetaNormalizada = getNodeJson('Code - Normalizar carpeta ejecución TEST');\n\nconst copiaPresentacion = getNodeJson('Drive - Copiar plantilla presentación ejecución TEST');\nconst linkPresentacionNormalizado = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...carpetaPreparada,\n ...carpetaDrive,\n ...carpetaNormalizada,\n ...copiaPresentacion,\n ...linkPresentacionNormalizado,\n ...actual,\n ...match,\n ...enriquecido\n};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = limpiar(valor);\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n if (texto.toLowerCase() === 'no disponible') return fallback;\n\n return texto;\n};\n\nconst cortar = (valor, max = 900) => {\n const texto = limpiarTexto(valor, 'No disponible');\n return texto.length > max ? texto.slice(0, max - 3) + '...' : texto;\n};\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n ''\n);\n\nconst matchConfianza = Number(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n 0\n);\n\nconst matchAlta =\n matchEstado === 'MATCH_ALTA_CONFIANZA' ||\n matchConfianza >= 85;\n\nconst propuestaNombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n ''\n);\n\nconst propuestaReferenciaEnriquecida = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n jsonFinal.propuesta_referencia ||\n ''\n);\n\nconst propuestaReferenciaSlides = limpiarReferenciaVisual(\n matchAlta && propuestaNombreBanco\n ? propuestaNombreBanco\n : propuestaReferenciaEnriquecida\n);\n\nconst ubicacionSlides = limpiarTexto(\n enriquecido.ubicacion ||\n data.ubicacion ||\n jsonFinal.ubicacion,\n 'No identificado'\n);\n\nconst fechaEjecucionSlides = limpiarTexto(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n jsonFinal.fecha_ejecucion,\n 'No identificado'\n);\n\n// --------------------------------------------------\n// Resolver carpeta de evidencias\n// --------------------------------------------------\n\nconst mediaFolderId = limpiar(\n data.media_folder_id ||\n data.execution_folder_id ||\n data.folder_id ||\n data.id ||\n carpetaNormalizada.media_folder_id ||\n carpetaNormalizada.execution_folder_id ||\n carpetaNormalizada.folder_id ||\n carpetaDrive.id ||\n ''\n);\n\nlet mediaFolderUrl = limpiar(\n data.media_folder_url ||\n data.execution_folder_url ||\n data.folder_url ||\n carpetaNormalizada.media_folder_url ||\n carpetaNormalizada.execution_folder_url ||\n carpetaNormalizada.folder_url ||\n carpetaDrive.webViewLink ||\n ''\n);\n\nif (!mediaFolderUrl && mediaFolderId) {\n mediaFolderUrl = `https://drive.google.com/drive/folders/${mediaFolderId}`;\n}\n\n// --------------------------------------------------\n// Resolver link de presentación\n// --------------------------------------------------\n\nconst presentationId = limpiar(\n data.presentation_id ||\n data.presentacion_id ||\n linkPresentacionNormalizado.presentation_id ||\n copiaPresentacion.id ||\n actual.presentation_id ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n data.presentationUrl ||\n data.link_presentacion ||\n linkPresentacionNormalizado.presentacion_ejecucion_url ||\n linkPresentacionNormalizado.presentation_url ||\n linkPresentacionNormalizado.webViewLink ||\n copiaPresentacion.webViewLink ||\n actual.presentacion_ejecucion_url ||\n actual.presentation_url ||\n actual.webViewLink ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\nif (!presentationId) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nconst objetivoSlides = cortar(\n data.objetivo ||\n jsonFinal.objetivo ||\n data.descripcion_ejecucion ||\n jsonFinal.descripcion_ejecucion ||\n data.resumen_ia ||\n jsonFinal.resumen_ia ||\n 'No disponible',\n 900\n);\n\nconst replacements = {\n '{{MARCA}}': limpiarTexto(data.marca || jsonFinal.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente || jsonFinal.cliente),\n '{{PAIS}}': limpiarTexto(data.pais || jsonFinal.pais),\n '{{UBICACION}}': ubicacionSlides,\n\n '{{FECHA_EJECUCION}}': fechaEjecucionSlides,\n '{{PROPUESTA_REFERENCIA}}': propuestaReferenciaSlides,\n '{{OBJETIVO}}': objetivoSlides,\n\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 65392, 32256 ], "id": "9164edd0-4c98-4d7b-845a-704c0786bbc8", "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": [ 65664, 32192 ], "id": "a8986614-21f8-4487-81c1-6e54775130d6", "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": [ 66272, 32192 ], "id": "dffec0ed-b2f6-40b9-b809-fc4a85c49965", "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();\nconst digits = (valor) => limpiar(valor).replace(/\\D/g, '');\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst decision = limpiar(\n actual.decision_flujo ||\n base.decision_flujo\n).toUpperCase();\n\nconst whatsappTo = digits(\n base.whatsapp_to ||\n actual.whatsapp_to ||\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\nif (!whatsappTo) {\n throw new Error('No se encontró el número privado para responder.');\n}\n\nconst intentoCancelar = decision === 'SIN_SESION_PARA_CANCELAR';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = intentoCancelar\n ? [\n 'ℹ️ *There is no active report to cancel.*',\n '',\n 'You can start a new evidence registration whenever you are ready.',\n '',\n 'Tap *Start report* or write *Hey*.'\n ].join('\\n')\n : [\n '👋 *There is no active report right now.*',\n '',\n 'To register an executed or external proposal, tap *Start report* or write *Hey*.',\n '',\n 'You can also view the country leaderboard.'\n ].join('\\n');\n} else {\n mensaje = intentoCancelar\n ? [\n 'ℹ️ *No hay un reporte activo para cancelar.*',\n '',\n 'Puedes iniciar un nuevo registro de evidencias cuando estés listo.',\n '',\n 'Pulsa *Iniciar reporte* o escribe *Hey*.'\n ].join('\\n')\n : [\n '👋 *No tienes un reporte activo en este momento.*',\n '',\n 'Para registrar una propuesta ejecutada o externa, pulsa *Iniciar reporte* o escribe *Hey*.',\n '',\n 'También puedes consultar el leaderboard por país.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n is_group: false,\n group_id: '',\n group_jid: '',\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: 'individual',\n whatsapp_header: idioma === 'EN' ? 'Fulgencio · Private chat' : 'Fulgencio · Chat privado',\n whatsapp_text: mensaje,\n whatsapp_buttons: [\n { id: 'HEY', title: idioma === 'EN' ? 'Start report' : 'Iniciar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ]\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51200, 26272 ], "id": "712972fd-8f38-4f43-bba5-9f1e9033e6c1", "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": [ 52320, 28896 ], "id": "82279360-59fa-45dd-90ca-380f65a8aab1", "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": [ 63568, 32256 ], "id": "2266a140-5b35-4f7c-bf59-d26fa1834bd4", "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": [ 63776, 32256 ], "id": "e3ac692e-ffa2-4805-bc96-ccaad95547ec", "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": [ 63984, 32256 ], "id": "2371f279-2ba7-42f1-ad70-5f0651a31f14", "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": [ 64192, 32256 ], "id": "e97f4aa8-3246-44e7-834b-fb5d11d1b8ae", "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": [ 64400, 32256 ], "id": "1ffede49-d832-4faf-8881-67053e7033bc", "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": [ 64608, 32256 ], "id": "bcccfab4-334b-4172-a304-da015f923bcc", "name": "Code - Confirmar media movida carpeta TEST" }, { "parameters": { "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Preparar reemplazos Slides ejecución TEST');\n\nconst data = {\n ...base,\n ...actual,\n\n // Aseguramos estos campos desde el nodo base si el nodo Slides no los devuelve\n presentation_id: actual.presentation_id || base.presentation_id,\n imagenes_drive_urls: actual.imagenes_drive_urls || base.imagenes_drive_urls,\n imagen_drive_urls: actual.imagen_drive_urls || base.imagen_drive_urls,\n media_drive_urls: actual.media_drive_urls || base.media_drive_urls,\n};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst extraerFileIdDesdeUrl = (valor) => {\n const url = texto(valor);\n if (!url) return '';\n\n const matchFile = url.match(/\\/file\\/d\\/([^/]+)/);\n if (matchFile?.[1]) return matchFile[1];\n\n const matchOpen = url.match(/[?&]id=([^&]+)/);\n if (matchOpen?.[1]) return matchOpen[1];\n\n if (!url.includes('http') && url.length > 15) return url;\n\n return '';\n};\n\nconst separarLista = (valor) => {\n return texto(valor)\n .split(/[\\n,]+/)\n .map(v => texto(v))\n .filter(Boolean);\n};\n\nconst imagenesUrls = separarLista(\n data.imagenes_drive_urls ||\n data.imagen_drive_urls ||\n data.media_drive_urls ||\n ''\n);\n\nconst imageFileIds = imagenesUrls\n .map(extraerFileIdDesdeUrl)\n .filter(Boolean)\n .slice(0, 25)\n\nif (!data.presentation_id) {\n throw new Error('No llegó presentation_id para insertar imágenes en Slides.');\n}\n\nreturn [\n {\n json: {\n ...data,\n\n apps_script_insert_images_payload: {\n secret: 'glm_fulgencio_slides_2026_seguro',\n presentation_id: data.presentation_id,\n image_file_ids: imageFileIds\n },\n\n imagenes_para_slides_count: imageFileIds.length,\n imagenes_para_slides_ids: imageFileIds,\n\n estado_insertar_imagenes: 'PAYLOAD_INSERTAR_IMAGENES_PREPARADO'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 65872, 32192 ], "id": "8b5a1035-fbb4-4852-a3ca-c01119d12e9f", "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": [ 66080, 32192 ], "id": "5cad6df0-8176-460e-9f4f-7a67e8388695", "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();\nconst digits = (valor) => limpiar(valor).replace(/\\D/g, '');\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst whatsappTo = digits(\n actual.whatsapp_to ||\n base.whatsapp_to ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone\n);\n\nif (!whatsappTo) {\n throw new Error('No se encontró el número privado para enviar el aviso de procesamiento.');\n}\n\nconst mensaje = idioma === 'EN'\n ? [\n '⏳ *Report received.*',\n '',\n 'We are processing your evidence now.',\n '',\n 'I will send the final link in this private chat when everything is ready.'\n ].join('\\n')\n : [\n '⏳ *Reporte recibido.*',\n '',\n 'Estamos procesando tus evidencias.',\n '',\n 'Te enviaré el enlace final por este chat privado cuando todo esté listo.'\n ].join('\\n');\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n is_group: false,\n group_id: '',\n group_jid: '',\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: 'individual',\n whatsapp_text: mensaje,\n aviso_procesando_preparado: true\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 50208, 31136 ], "id": "3e68bbb5-7c95-4d62-bf50-328cbb4886a5", "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": [ 50656, 31136 ], "id": "5a676700-a3b8-4854-9a16-bdffb7733eda", "name": "Code - Restaurar contexto aviso procesando TEST" }, { "parameters": { "method": "POST", "url": "https://graph.facebook.com/v26.0/1151359118067218/messages", "authentication": "predefinedCredentialType", "nodeCredentialType": "whatsAppApi", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ (() => {\n const text = (value) => String(value ?? '');\n\n const cleanMultiline = (value) =>\n text(value)\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\\\r\\\\n/g, '\\n')\n .replace(/\\\\n/g, '\\n')\n .replace(/[ \\t]+\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n\n const cleanSingleLine = (value) =>\n cleanMultiline(value)\n .replace(/\\s*\\n\\s*/g, ' ')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n\n const digits = (value) => text(value).replace(/\\D/g, '');\n\n const destination = digits(\n $json.whatsapp_to ||\n $json.sender_phone ||\n $json.manager_telefono ||\n $json.whatsapp_remote_jid ||\n ''\n );\n\n if (!destination) {\n throw new Error('No se encontró el número privado de destino de WhatsApp.');\n }\n\n const bodyText = cleanMultiline(\n $json.whatsapp_text ||\n $json.texto_respuesta ||\n $json.mensaje ||\n $json.text ||\n $json.message_text ||\n ''\n );\n\n const headerText = cleanSingleLine(\n $json.whatsapp_header || ''\n ).slice(0, 60);\n\n const footerText = cleanSingleLine(\n $json.whatsapp_footer ||\n 'GomezLee Marketing · Fulgencio'\n ).slice(0, 60);\n\n const buttons = Array.isArray($json.whatsapp_buttons)\n ? $json.whatsapp_buttons\n .map((button, index) => ({\n id: cleanSingleLine(\n button?.id || `OPTION_${index + 1}`\n ).slice(0, 256),\n title: cleanSingleLine(\n button?.title || `Opción ${index + 1}`\n ).slice(0, 20)\n }))\n .filter((button) => button.id && button.title)\n .slice(0, 3)\n : [];\n\n if (buttons.length > 0) {\n const interactive = {\n type: 'button',\n body: {\n text: (bodyText || 'Selecciona una opción:').slice(0, 1024)\n },\n action: {\n buttons: buttons.map((button) => ({\n type: 'reply',\n reply: {\n id: button.id,\n title: button.title\n }\n }))\n }\n };\n\n if (headerText) {\n interactive.header = {\n type: 'text',\n text: headerText\n };\n }\n\n if (footerText) {\n interactive.footer = {\n text: footerText\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: destination,\n type: 'interactive',\n interactive\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: destination,\n type: 'text',\n text: {\n preview_url: true,\n body: (bodyText || 'Mensaje sin contenido').slice(0, 4096)\n }\n };\n})() }}", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 50432, 31136 ], "id": "a3f7a9d8-b0ba-4bb7-87fc-a9f37f7abca3", "name": "WhatsApp - Enviar aviso procesando API Oficial", "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 5000, "credentials": { "whatsAppApi": { "id": "t14kVayc9FurLReq", "name": "WhatsApp API - GLM CDC" } } }, { "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": [ 61616, 32208 ], "id": "f48fc623-48b5-4d0f-a2d4-c2e978d8eeba", "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": [ 61824, 32208 ], "id": "c61e1a94-b6b9-4166-a670-473d407f9d9f", "name": "Code - Match propuesta banco Fulgencio TEST" }, { "parameters": { "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst toNumber = (valor) => {\n const limpio = limpiar(valor).replace(',', '.');\n const n = Number(limpio);\n return Number.isFinite(n) ? n : 0;\n};\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst slidesNormalizado1 = getNodeJson('Code - Normalizar respuesta Slides TEST');\nconst slidesNormalizado2 = getNodeJson('Code - Normalizar respuesta Slides ejecución TEST');\nconst slidesNormalizado3 = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...match,\n ...enriquecido,\n ...actual,\n ...slidesNormalizado1,\n ...slidesNormalizado2,\n ...slidesNormalizado3\n};\n\nconst matchDebug =\n match.match_banco_debug ||\n actual.match_banco_debug ||\n data.match_banco_debug ||\n {};\n\nconst mejorMatch = matchDebug.mejor_match || {};\n\n// --------------------------------------------------\n// 1. Resolver datos principales\n// --------------------------------------------------\n\nconst tipoReporte = normalizarPlano(\n jsonFinal.tipo_reporte ||\n data.tipo_reporte ||\n actual.tipo_reporte ||\n ''\n);\n\nlet matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n actual.propuesta_match_estado ||\n ''\n);\n\nlet matchConfianza = toNumber(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n actual.propuesta_match_confianza ||\n mejorMatch.score ||\n 0\n);\n\nlet nombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n actual.propuesta_nombre_banco ||\n mejorMatch.nombre ||\n ''\n);\n\nconst linkBancoOriginal = limpiar(\n match.propuesta_link_banco ||\n data.propuesta_link_banco ||\n actual.propuesta_link_banco ||\n mejorMatch.link ||\n ''\n);\n\nconst fileIdBanco = limpiar(\n match.propuesta_banco_file_id ||\n data.propuesta_banco_file_id ||\n actual.propuesta_banco_file_id ||\n mejorMatch.file_id ||\n ''\n);\n\nconst rowNumberBanco = limpiar(\n match.propuesta_banco_row_number ||\n data.propuesta_banco_row_number ||\n actual.propuesta_banco_row_number ||\n mejorMatch.row_number ||\n ''\n);\n\nconst enlacesExistentes = limpiar(\n match.propuesta_banco_enlaces_existentes ||\n data.propuesta_banco_enlaces_existentes ||\n actual.propuesta_banco_enlaces_existentes ||\n data.banco_enlaces_existentes ||\n actual.banco_enlaces_existentes ||\n mejorMatch.enlaces_ejecutadas_existentes ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n actual.presentacion_ejecucion_url ||\n data.presentation_url ||\n actual.presentation_url ||\n data.presentationUrl ||\n actual.presentationUrl ||\n data.webViewLink ||\n actual.webViewLink ||\n data.link_presentacion ||\n actual.link_presentacion ||\n slidesNormalizado3.presentacion_ejecucion_url ||\n slidesNormalizado3.presentation_url ||\n slidesNormalizado3.webViewLink ||\n ''\n);\n\nconst presentationId = limpiar(\n data.presentation_id ||\n actual.presentation_id ||\n slidesNormalizado3.presentation_id ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\n// --------------------------------------------------\n// 2. Normalizar estado de match de forma defensiva\n// --------------------------------------------------\n\nif (\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 85\n) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n}\n\nif (\n matchEstado !== 'MATCH_MEDIA_CONFIANZA' &&\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 60 &&\n matchConfianza < 85\n) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n}\n\nif (!matchEstado || matchEstado === 'PENDIENTE') {\n if (matchConfianza >= 85) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n } else if (matchConfianza >= 60) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n } else if (matchConfianza > 0) {\n matchEstado = 'MATCH_BAJA_CONFIANZA';\n } else {\n matchEstado = 'SIN_MATCH';\n }\n}\n\n// --------------------------------------------------\n// 3. Mantener valores enriquecidos\n// --------------------------------------------------\n\nconst propuestaReferenciaFinal = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n actual.propuesta_referencia ||\n ''\n);\n\nconst ubicacionFinal = limpiar(\n enriquecido.ubicacion ||\n data.ubicacion ||\n actual.ubicacion ||\n ''\n);\n\nconst fechaEjecucionFinal = limpiar(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n actual.fecha_ejecucion ||\n ''\n);\n\n// --------------------------------------------------\n// 4. Detectar duplicado de link de forma robusta\n// --------------------------------------------------\n\nconst limpiarUrl = (valor) => {\n return limpiar(valor)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '')\n .trim();\n};\n\nconst normalizarUrlComparacion = (valor) => {\n return limpiarUrl(valor)\n .replace(/\\/edit.*$/i, '')\n .replace(/\\/view.*$/i, '')\n .replace(/\\?.*$/i, '')\n .replace(/#.*$/i, '')\n .replace(/\\/$/, '')\n .toLowerCase();\n};\n\nconst extraerGoogleId = (valor) => {\n const url = limpiarUrl(valor);\n\n if (!url) return '';\n\n const patrones = [\n /\\/presentation\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/document\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/spreadsheets\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,\n /[?&]id=([a-zA-Z0-9_-]+)/\n ];\n\n for (const patron of patrones) {\n const match = url.match(patron);\n if (match?.[1]) return match[1];\n }\n\n return '';\n};\n\nconst extraerLinks = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return [];\n\n return texto\n .split(/[\\n\\r\\t;, ]+/)\n .map(limpiarUrl)\n .filter(Boolean)\n .filter(link => {\n return (\n link.startsWith('http://') ||\n link.startsWith('https://') ||\n link.includes('docs.google.com') ||\n link.includes('drive.google.com')\n );\n });\n};\n\nconst enlacesExistentesLista = extraerLinks(enlacesExistentes);\n\nconst presentationIdDetectado =\n presentationId ||\n extraerGoogleId(presentationUrl);\n\nconst presentationUrlNormalizada = normalizarUrlComparacion(presentationUrl);\n\nconst linkDuplicado = Boolean(\n presentationUrl &&\n enlacesExistentesLista.some((linkExistente) => {\n const idExistente = extraerGoogleId(linkExistente);\n\n if (\n presentationIdDetectado &&\n idExistente &&\n idExistente === presentationIdDetectado\n ) {\n return true;\n }\n\n return normalizarUrlComparacion(linkExistente) === presentationUrlNormalizada;\n })\n);\n\n// --------------------------------------------------\n// 5. Calcular enlaces propuestos\n// --------------------------------------------------\n\nlet enlacesPropuestos = enlacesExistentes;\n\nif (presentationUrl && !linkDuplicado) {\n enlacesPropuestos = enlacesExistentes\n ? `${enlacesExistentes}\\n${presentationUrl}`\n : presentationUrl;\n}\n\n// --------------------------------------------------\n// 6. Decisión automática final\n// --------------------------------------------------\n\nconst motivos = [];\n\nconst esPropuestaEjecutada = tipoReporte === 'PROPUESTA_EJECUTADA';\nconst esPropuestaExterna = tipoReporte === 'PROPUESTA_EXTERNA';\nconst matchAlta = matchEstado === 'MATCH_ALTA_CONFIANZA' && matchConfianza >= 85;\nconst hayMatchBanco = Boolean(rowNumberBanco && nombreBanco);\n\nlet decisionAutomaticaBanco = '';\nlet motivoDecisionAutomatica = '';\n\nif (!presentationUrl) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_ERROR_DATOS';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no existe link de presentación ejecutada.';\n motivos.push('no existe presentacion_ejecucion_url');\n\n} else if (esPropuestaExterna) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_PROPUESTA_EXTERNA';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el reporte fue clasificado como propuesta externa. Se genera presentación y se guarda el registro ejecutado.';\n\n} else if (!esPropuestaEjecutada) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_TIPO_NO_DETERMINADO';\n motivoDecisionAutomatica = `No se actualiza banco original porque tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}.`;\n motivos.push(`tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}`);\n\n} else if (linkDuplicado) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_LINK_DUPLICADO';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el link de presentación ejecutada ya existe en la columna de enlaces ejecutados.';\n motivos.push('el link ya existe en la columna de enlaces ejecutados');\n\n} else if (!hayMatchBanco) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_SIN_MATCH';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no se encontró una propuesta compatible con fila de banco válida.';\n if (!nombreBanco) motivos.push('no llegó propuesta_nombre_banco');\n if (!rowNumberBanco) motivos.push('no llegó propuesta_banco_row_number');\n\n} else if (!matchAlta) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_CONFIANZA_INSUFICIENTE';\n motivoDecisionAutomatica = `No se actualiza banco original porque el match no tiene confianza suficiente. Estado: ${matchEstado || 'SIN_MATCH'}, confianza: ${matchConfianza}.`;\n motivos.push(`propuesta_match_estado es ${matchEstado || 'SIN_MATCH'}`);\n motivos.push(`propuesta_match_confianza es ${matchConfianza}`);\n\n} else {\n decisionAutomaticaBanco = 'ACTUALIZADO_BANCO_AUTO';\n motivoDecisionAutomatica = 'Banco original actualizado automáticamente: propuesta ejecutada, match alto, confianza suficiente y link nuevo.';\n}\n\nconst actualizarBanco = decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO';\n\nconst bancoActualizadoAuto = actualizarBanco ? 'SI' : 'NO';\n\nconst motivoNoActualizaBanco = actualizarBanco\n ? ''\n : motivoDecisionAutomatica;\n\n// --------------------------------------------------\n// 7. Seguridad para evitar updates accidentales\n// --------------------------------------------------\n// Si NO se actualiza, dejamos row_number y banco_match_value vacíos.\n// Así, aunque alguien conecte mal el nodo de Google Sheets,\n// no debería encontrar fila válida para actualizar.\nconst rowNumberSeguro = actualizarBanco ? rowNumberBanco : '';\nconst bancoMatchValueSeguro = actualizarBanco ? rowNumberBanco : '';\nconst enlacesActualizadosSeguro = actualizarBanco ? enlacesPropuestos : enlacesExistentes;\n\n// Campo compatible con lógica anterior.\n// Ya no significa revisión humana; es una decisión automática del sistema.\nlet propuestaMatchRevision = '';\n\nif (decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_LINK_DUPLICADO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION_LINK_DUPLICADO';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_PROPUESTA_EXTERNA') {\n propuestaMatchRevision = 'NO_APLICA_PROPUESTA_EXTERNA';\n} else {\n propuestaMatchRevision = 'NO_ACTUALIZADO_AUTOMATICAMENTE';\n}\n\n// --------------------------------------------------\n// 8. Salida final\n// --------------------------------------------------\n\nreturn [\n {\n json: {\n ...data,\n\n // Mantener explícitamente los valores enriquecidos.\n propuesta_referencia: propuestaReferenciaFinal || data.propuesta_referencia || '',\n ubicacion: ubicacionFinal || data.ubicacion || '',\n fecha_ejecucion: fechaEjecucionFinal || data.fecha_ejecucion || '',\n\n // Mantener explícitamente el link final para nodos posteriores.\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n // Decisión automática nueva.\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n // Compatibilidad con flujo actual.\n actualizar_banco_original: actualizarBanco,\n motivo_no_actualiza_banco: motivoNoActualizaBanco,\n\n propuesta_match_estado: matchEstado || data.propuesta_match_estado || 'SIN_MATCH',\n propuesta_match_confianza: String(matchConfianza || 0),\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n propuesta_banco_enlaces_existentes: enlacesExistentes,\n propuesta_match_revision: propuestaMatchRevision,\n\n banco_match_column: 'row_number',\n banco_match_value: bancoMatchValueSeguro,\n\n row_number: rowNumberSeguro,\n 'Enlaces a propuestas ejecutadas': enlacesActualizadosSeguro,\n\n banco_enlaces_existentes: enlacesExistentes,\n banco_enlaces_propuestos: enlacesPropuestos,\n banco_enlaces_actualizados: enlacesActualizadosSeguro,\n banco_presentacion_url: presentationUrl,\n\n banco_link_duplicado_detectado: linkDuplicado,\n banco_presentacion_id_detectado: presentationIdDetectado,\n banco_enlaces_existentes_lista: enlacesExistentesLista,\n\n datos_enriquecidos_match_banco: Boolean(\n enriquecido.datos_enriquecidos_match_banco ||\n data.datos_enriquecidos_match_banco\n ),\n\n banco_update_debug: {\n tipo_reporte: tipoReporte,\n\n propuesta_match_estado: matchEstado,\n propuesta_match_confianza: matchConfianza,\n match_alta: matchAlta,\n hay_match_banco: hayMatchBanco,\n\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n propuesta_referencia_final: propuestaReferenciaFinal,\n ubicacion_final: ubicacionFinal,\n fecha_ejecucion_final: fechaEjecucionFinal,\n\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n\n presentationUrl,\n presentationId,\n presentationIdDetectado,\n\n enlacesExistentes,\n enlacesExistentesLista,\n enlacesPropuestos,\n enlacesActualizadosSeguro,\n\n linkDuplicado,\n\n rowNumberSeguro,\n bancoMatchValueSeguro,\n\n actualizarBanco,\n motivoNoActualizaBanco,\n\n motivos,\n\n mejor_match: mejorMatch,\n match_debug: matchDebug,\n\n enriquecido_debug: {\n propuesta_referencia: enriquecido.propuesta_referencia || '',\n ubicacion: enriquecido.ubicacion || '',\n fecha_ejecucion: enriquecido.fecha_ejecucion || '',\n datos_enriquecidos_match_banco: enriquecido.datos_enriquecidos_match_banco || false\n }\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 68720, 32016 ], "id": "8921425b-1053-429f-b78c-2c915c061105", "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": [ 69488, 32064 ], "id": "39433c13-8ef4-4693-8dd6-3440302cc7cc", "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": [ 69840, 31936 ], "id": "0284879d-9259-41c5-833b-a5e88150cecd", "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": [ 70128, 31936 ], "id": "70839572-cb01-443f-ae5c-fffe1eebf8f3", "name": "Code - Restaurar contexto actualización banco TEST" }, { "parameters": { "jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valor) => {\n const texto = normalizar(valor);\n\n if (!texto) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bCODIGO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(texto));\n};\n\nconst normalizarUbicacion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst normalizarFechaEjecucion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (/^20\\d{2}$/.test(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst matchAltaOMedia = [\n 'MATCH_ALTA_CONFIANZA',\n 'MATCH_MEDIA_CONFIANZA'\n].includes(normalizar(data.propuesta_match_estado));\n\nconst nombreBanco = limpiar(data.propuesta_nombre_banco);\n\nconst referenciaOriginal = limpiar(data.propuesta_referencia);\n\nconst referenciaCanonica = matchAltaOMedia && nombreBanco\n ? nombreBanco\n : referenciaOriginal;\n\nconst ubicacionLimpia = normalizarUbicacion(data.ubicacion);\nconst fechaLimpia = normalizarFechaEjecucion(data.fecha_ejecucion);\n\nreturn [\n {\n json: {\n ...data,\n\n propuesta_referencia_original_ia: referenciaOriginal,\n ubicacion_original_ia: data.ubicacion || '',\n fecha_ejecucion_original_ia: data.fecha_ejecucion || '',\n\n propuesta_referencia: limpiarReferenciaVisual(referenciaCanonica),\n ubicacion: ubicacionLimpia,\n fecha_ejecucion: fechaLimpia,\n\n datos_enriquecidos_match_banco: true\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 62032, 32224 ], "id": "b64e3f26-964f-4e84-84d8-71389724dff3", "name": "Code - Enriquecer datos con match banco TEST" }, { "parameters": { "jsCode": "const items = $input.all();\n\nconst clean = (value) => String(value ?? '').trim();\nconst digits = (value) => clean(value).replace(/\\D/g, '');\n\nreturn items.map((item) => {\n const evento = item.json || {};\n\n const messageType = clean(evento.message_type).toLowerCase();\n const text = clean(evento.texto);\n const senderPhone = digits(\n evento.sender_phone ||\n evento.manager_telefono ||\n evento.whatsapp_to ||\n evento.whatsapp_remote_jid\n );\n\n const rawMessage =\n evento.raw_event?.messages?.[0] ||\n evento.raw_event ||\n {};\n\n const isGroup = Boolean(\n evento.is_group ||\n evento.group_id ||\n evento.group_jid ||\n evento.whatsapp_recipient_type === 'group'\n );\n\n const isReaction =\n messageType === 'reaction' ||\n Boolean(rawMessage.reaction);\n\n const isStatus =\n messageType === 'status' ||\n Boolean(evento.raw_event?.statuses) ||\n Boolean(evento.raw_event?.status);\n\n const isEmpty =\n !text &&\n !evento.tiene_media &&\n !['audio', 'image', 'video', 'document'].includes(messageType);\n\n const reasons = [];\n\n if (!senderPhone) reasons.push('remitente sin número válido');\n if (evento.from_me) reasons.push('mensaje enviado por el propio número');\n if (isGroup) reasons.push('evento grupal no permitido en modo privado');\n if (isReaction) reasons.push('evento de reacción');\n if (isStatus) reasons.push('evento de estado');\n if (isEmpty) reasons.push('evento sin texto ni media útil');\n\n const process = reasons.length === 0;\n\n return {\n json: {\n ...evento,\n\n // Modo definitivo de esta versión: conversación individual.\n is_group: false,\n group_id: '',\n group_jid: '',\n group_name: '',\n whatsapp_to: senderPhone,\n whatsapp_remote_jid: senderPhone,\n whatsapp_recipient_type: 'individual',\n manager_telefono: senderPhone,\n sender_phone: senderPhone,\n sender_jid: senderPhone,\n canal_origen: 'WHATSAPP_OFICIAL_PRIVADO',\n\n filtro_origen_procesar: process,\n filtro_origen_decision: process ? 'PROCESAR_PRIVADO' : 'IGNORAR',\n filtro_origen_motivo: reasons.join('; '),\n\n chat_privado_permitido: process,\n modo_operacion: 'CHAT_PRIVADO_API_OFICIAL',\n\n filtro_origen_debug: {\n api: 'META_WHATSAPP_CLOUD_API',\n modo: 'CHAT_PRIVADO',\n sender_phone: senderPhone,\n message_type: messageType,\n accion: clean(evento.accion_flujo),\n texto: text,\n evento_grupal_detectado: isGroup,\n procesar: process,\n motivos_bloqueo: reasons\n }\n }\n };\n});" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 47152, 29040 ], "id": "42bb33d4-209b-4b91-aa23-32cec786b796", "name": "Code - Validar chat privado WhatsApp TEST" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "330d3843-6562-4ba6-afce-9a4bef9666b1", "leftValue": "={{ $json.filtro_origen_procesar }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 47664, 29008 ], "id": "8e3ef090-b3d8-4079-a95c-f69b3f867372", "name": "IF - Procesar solo chat privado 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": { "session_id": "={{ $json.session_id }}", "decision_automatica_banco": "={{ $json.decision_automatica_banco }}", "banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}", "propuesta_match_revision": "={{ $json.propuesta_match_revision }}", "motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "fecha_recepcion", "displayName": "fecha_recepcion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "fecha_ejecucion", "displayName": "fecha_ejecucion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "propuesta_referencia", "displayName": "propuesta_referencia", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "propuesta_match_estado", "displayName": "propuesta_match_estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "propuesta_match_confianza", "displayName": "propuesta_match_confianza", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "propuesta_nombre_banco", "displayName": "propuesta_nombre_banco", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "propuesta_link_banco", "displayName": "propuesta_link_banco", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "marca", "displayName": "marca", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "cliente", "displayName": "cliente", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "pais", "displayName": "pais", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "ubicacion", "displayName": "ubicacion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "comentario_original", "displayName": "comentario_original", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "resumen_ia", "displayName": "resumen_ia", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "descripcion_ejecucion", "displayName": "descripcion_ejecucion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "elementos_detectados", "displayName": "elementos_detectados", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "tags", "displayName": "tags", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "media_folder_url", "displayName": "media_folder_url", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "presentacion_ejecucion_url", "displayName": "presentacion_ejecucion_url", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "fotos_count", "displayName": "fotos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "audios_count", "displayName": "audios_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "estado_revision", "displayName": "estado_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "ultima_actualizacion", "displayName": "ultima_actualizacion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "transcripcion_audio", "displayName": "transcripcion_audio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "propuesta_match_revision", "displayName": "propuesta_match_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "tipo_reporte_confianza", "displayName": "tipo_reporte_confianza", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "tipo_reporte_motivo", "displayName": "tipo_reporte_motivo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "decision_automatica_banco", "displayName": "decision_automatica_banco", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "motivo_decision_automatica", "displayName": "motivo_decision_automatica", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "banco_actualizado_auto", "displayName": "banco_actualizado_auto", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 68976, 32096 ], "id": "de293272-8f03-408c-b33a-89bc06d876bc", "name": "Sheets - Actualizar decisión automática banco TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst decisionOriginal = getNodeJson('Code - Preparar actualización banco propuesta ejecutada TEST');\nconst updateDecisionSheet = $json || {};\n\nreturn [\n {\n json: {\n ...decisionOriginal,\n\n decision_sheet_actualizada: true,\n decision_sheet_update_debug: {\n row_number: updateDecisionSheet.row_number || '',\n session_id: updateDecisionSheet.session_id || decisionOriginal.session_id || '',\n decision_automatica_banco:\n updateDecisionSheet.decision_automatica_banco ||\n decisionOriginal.decision_automatica_banco ||\n '',\n banco_actualizado_auto:\n updateDecisionSheet.banco_actualizado_auto ||\n decisionOriginal.banco_actualizado_auto ||\n ''\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 69184, 32096 ], "id": "51622457-b8d4-4e03-a80e-f3bf3eacfb84", "name": "Code - Restaurar contexto decisión automática banco TEST" }, { "parameters": { "jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst sessionId = limpiar(data.session_id);\nconst eventId = limpiar(data.event_id || data.media_source_id || `${Date.now()}`);\nconst mediaSourceId = limpiar(data.media_source_id || data.event_id || eventId);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para crear buffer Redis de imagen.');\n}\n\nif (!mediaSourceId) {\n throw new Error('No llegó media_source_id/event_id para crear buffer Redis de imagen.');\n}\n\nconst redisKey = `fulgencio:media:${sessionId}:image`;\n\nconst bufferItem = {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n\n event_id: eventId,\n media_source_id: mediaSourceId,\n message_type: 'image',\n media_type: 'image',\n\n buffer_received_at: new Date().toISOString(),\n buffer_received_at_ms: Date.now()\n};\n\nreturn [\n {\n json: {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n redis_payload: JSON.stringify(bufferItem),\n\n estado_buffer_redis: 'IMAGEN_ENVIADA_A_BUFFER'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51456, 28416 ], "id": "8d130576-08ce-4164-9639-0c3b9b67c324", "name": "Code - Preparar buffer imagen Redis TEST" }, { "parameters": { "operation": "push", "list": "={{ $json.redis_buffer_key }}", "messageData": "={{ $json.redis_payload }}", "tail": true }, "type": "n8n-nodes-base.redis", "typeVersion": 1, "position": [ 51664, 28416 ], "id": "39c47d49-2734-4552-a654-c483826a7127", "name": "Redis - Push buffer imagen TEST", "credentials": { "redis": { "id": "Fu2IevwVPaoyw9OS", "name": "Redis Fulgencio" } } }, { "parameters": { "jsCode": "const original = $('Code - Preparar buffer imagen Redis TEST').first().json || {};\nconst redisPushResult = $json || {};\n\nreturn [\n {\n json: {\n ...original,\n redis_push_result: redisPushResult,\n redis_push_ok: true,\n estado_buffer_redis: 'IMAGEN_GUARDADA_EN_REDIS'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51872, 28416 ], "id": "209616e6-851a-4e61-b86e-b4900d02625a", "name": "Code - Restaurar contexto buffer imagen TEST" }, { "parameters": {}, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ 52080, 28416 ], "id": "6499785a-3a8b-4762-8fd0-68d6c9246ff8", "name": "Wait - Esperar cierre buffer imagen TEST", "webhookId": "5c73c08e-d77a-448d-a8ab-11080d08b476" }, { "parameters": { "operation": "get", "propertyName": "redis_buffer_items", "key": "={{ $('Code - Restaurar contexto buffer imagen TEST').first().json.redis_buffer_key }}", "keyType": "list", "options": {} }, "type": "n8n-nodes-base.redis", "typeVersion": 1, "position": [ 52288, 28416 ], "id": "3a7c9906-c0f1-4254-b136-57207fc95a43", "name": "Redis - Leer buffer imagen TEST", "credentials": { "redis": { "id": "Fu2IevwVPaoyw9OS", "name": "Redis Fulgencio" } } }, { "parameters": { "jsCode": "const contexto = $('Code - Restaurar contexto buffer imagen TEST').first().json || {};\nconst redisOutput = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst currentBufferId = limpiar(contexto.redis_buffer_id);\nconst sessionId = limpiar(contexto.session_id);\nconst redisKey = limpiar(contexto.redis_buffer_key);\n\nlet rawItems = redisOutput.redis_buffer_items || redisOutput.message || [];\n\nif (!Array.isArray(rawItems)) {\n rawItems = rawItems ? [rawItems] : [];\n}\n\nconst parsed = [];\n\nfor (const raw of rawItems) {\n try {\n const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;\n if (obj && typeof obj === 'object') {\n parsed.push(obj);\n }\n } catch (error) {\n // Ignorar items corruptos del buffer\n }\n}\n\n// Deduplicar por media_source_id/event_id\nconst vistos = new Set();\nconst lote = [];\n\nfor (const item of parsed) {\n const id = limpiar(item.media_source_id || item.event_id || item.redis_buffer_id);\n if (!id) continue;\n if (vistos.has(id)) continue;\n\n vistos.add(id);\n\n lote.push({\n ...item,\n redis_buffer_key: redisKey,\n session_id: limpiar(item.session_id || sessionId),\n media_source_id: limpiar(item.media_source_id || id),\n event_id: limpiar(item.event_id || id),\n message_type: 'image',\n media_type: 'image'\n });\n}\n\nlote.sort((a, b) => {\n return Number(a.buffer_received_at_ms || 0) - Number(b.buffer_received_at_ms || 0);\n});\n\nconst ultimo = lote[lote.length - 1] || {};\nconst ultimoId = limpiar(ultimo.redis_buffer_id || ultimo.event_id || ultimo.media_source_id);\n\nconst debeProcesar =\n lote.length > 0 &&\n currentBufferId &&\n ultimoId &&\n currentBufferId === ultimoId;\n\nconst imagenesPrevias = Number(\n contexto.sesion_activa?.imagenes_count ??\n contexto.imagenes_count ??\n 0\n) || 0;\n\nconst imagenesNuevas = lote.length;\nconst imagenesTotal = imagenesPrevias + imagenesNuevas;\n\nreturn [\n {\n json: {\n ...contexto,\n\n redis_buffer_items_count: rawItems.length,\n redis_buffer_lote_count: lote.length,\n redis_buffer_lote: lote,\n\n redis_buffer_ultimo_id: ultimoId,\n redis_buffer_current_id: currentBufferId,\n\n redis_debe_procesar_lote: debeProcesar,\n redis_decision: debeProcesar ? 'PROCESAR_LOTE_IMAGEN' : 'IGNORAR_EJECUCION_INTERMEDIA',\n\n imagenes_previas: imagenesPrevias,\n imagenes_nuevas_lote: imagenesNuevas,\n imagenes_count: imagenesTotal,\n\n estado_buffer_redis: debeProcesar\n ? 'LOTE_IMAGEN_LISTO_PARA_PROCESAR'\n : 'IMAGEN_EN_BUFFER_ESPERANDO_ULTIMA_EJECUCION',\n\n redis_debug: {\n redisKey,\n rawItemsCount: rawItems.length,\n loteCount: lote.length,\n currentBufferId,\n ultimoId,\n debeProcesar\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 52496, 28416 ], "id": "4b48654c-a989-482f-a2c8-8a5100aae2a4", "name": "Code - Decidir procesar lote imagen Redis TEST" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "f2394967-cd4b-4c6e-89be-7d9410d8a68b", "leftValue": "={{ $json.redis_debe_procesar_lote }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 52720, 28416 ], "id": "6dc2a95b-5f2d-405e-b7ed-4407a1a6ebd0", "name": "IF - Procesar lote imagen Redis TEST" }, { "parameters": { "operation": "delete", "key": "={{ $json.redis_buffer_key }}" }, "type": "n8n-nodes-base.redis", "typeVersion": 1, "position": [ 52992, 28400 ], "id": "64835ce7-38d7-43e7-9c44-2aa491ba9a19", "name": "Redis - Borrar buffer imagen TEST", "credentials": { "redis": { "id": "Fu2IevwVPaoyw9OS", "name": "Redis Fulgencio" } } }, { "parameters": { "jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst lote = Array.isArray(contexto.redis_buffer_lote)\n ? contexto.redis_buffer_lote\n : [];\n\nif (!lote.length) {\n return [];\n}\n\nreturn lote.map((item, index) => {\n return {\n json: {\n ...contexto,\n ...item,\n\n media_index: index + 1,\n media_total: lote.length,\n\n estado: 'IMAGEN_RECIBIDA',\n fecha_procesado: new Date().toISOString(),\n\n lote_imagen_redis_total: lote.length,\n redis_lote_procesado: true\n }\n };\n});" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 53200, 28400 ], "id": "4424e969-d8bf-44f3-859c-5b5b16b1ba81", "name": "Code - Expandir lote imagen Redis TEST" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 306129743, "mode": "list", "cachedResultName": "wa_ejecuciones_eventos", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" }, "columns": { "mappingMode": "defineBelow", "value": { "event_id": "={{ $json.event_id }}", "manager_telefono": "={{ $json.manager_telefono }}", "manager_nombre": "={{ $json.manager_nombre }}", "tiene_media": "={{ $json.tiene_media }}", "estado": "=IMAGEN_RECIBIDA", "session_id": "={{ $json.session_id }}", "canal_origen": "=WHATSAPP", "fecha_recepcion": "={{ $json.fecha_recepcion }}", "message_type": "={{ $json.message_type }}", "texto": "={{ $json.texto }}", "media_count": "={{ $json.media_count }}", "raw_preview": "={{ $json.raw_preview }}", "etapa_recibida": "={{ $json.etapa_actual }}", "media_source_id": "={{ $json.media_source_id }}", "media_mime_type": "={{ $json.media_mime_type }}", "media_file_name": "={{ $json.media_file_name }}", "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}", "comando": "={{ $json.accion_flujo }}", "ejecucion_id": "={{ $json.ejecucion_id }}", "fecha_procesado": "={{ new Date().toISOString() }}" }, "matchingColumns": [], "schema": [ { "id": "event_id", "displayName": "event_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_recepcion", "displayName": "fecha_recepcion", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "message_type", "displayName": "message_type", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "texto", "displayName": "texto", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "comando", "displayName": "comando", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "tiene_media", "displayName": "tiene_media", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_count", "displayName": "media_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "raw_preview", "displayName": "raw_preview", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "fecha_procesado", "displayName": "fecha_procesado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "etapa_recibida", "displayName": "etapa_recibida", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "media_source_id", "displayName": "media_source_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "media_mime_type", "displayName": "media_mime_type", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "media_file_name", "displayName": "media_file_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "whatsapp_remote_jid", "displayName": "whatsapp_remote_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 53440, 28400 ], "id": "aca4b708-b182-43ff-8d15-ad79fc98b611", "name": "Sheets - Guardar eventos imagen lote Redis TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst guardados = $input.all().map(item => item.json || {});\n\nconst loteCount = Number(contexto.redis_buffer_lote_count || guardados.length || 0);\nconst imagenesPrevias = Number(contexto.imagenes_previas || 0) || 0;\nconst imagenesTotal = Number(contexto.imagenes_count || (imagenesPrevias + loteCount)) || loteCount;\n\nreturn [\n {\n json: {\n ...contexto,\n\n eventos_imagen_guardados: guardados.length,\n imagenes_nuevas_lote: loteCount,\n imagenes_count: imagenesTotal,\n\n ultima_actividad: new Date().toISOString(),\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'IMAGENES_RECIBIDAS',\n motivo_revision: `IMAGENES_RECIBIDAS_LOTE_${loteCount}`,\n\n redis_lote_imagen_guardado: true\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 53648, 28400 ], "id": "97e84d15-c4aa-4750-bf09-93568e837d67", "name": "Code - Consolidar lote imagen guardado Redis TEST" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 616166581, "mode": "list", "cachedResultName": "wa_ejecuciones_sesiones", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" }, "columns": { "mappingMode": "defineBelow", "value": { "session_id": "={{ $json.session_id }}", "estado": "={{ $json.estado }}", "imagenes_count": "={{ $json.imagenes_count }}", "etapa": "={{ $json.etapa }}", "ultima_actividad": "={{ $json.ultima_actividad }}", "motivo_revision": "={{ $json.motivo_revision }}" }, "matchingColumns": [ "session_id" ], "schema": [ { "id": "session_id", "displayName": "session_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "manager_telefono", "displayName": "manager_telefono", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "manager_nombre", "displayName": "manager_nombre", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "canal_origen", "displayName": "canal_origen", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "fecha_inicio", "displayName": "fecha_inicio", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "ultima_actividad", "displayName": "ultima_actividad", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "etapa", "displayName": "etapa", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "audio_count", "displayName": "audio_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "imagenes_count", "displayName": "imagenes_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "videos_count", "displayName": "videos_count", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "estado", "displayName": "estado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "ejecucion_id", "displayName": "ejecucion_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "motivo_revision", "displayName": "motivo_revision", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": false }, { "id": "is_group", "displayName": "is_group", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "group_jid", "displayName": "group_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "group_name", "displayName": "group_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "sender_jid", "displayName": "sender_jid", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "sender_phone", "displayName": "sender_phone", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "sender_name", "displayName": "sender_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "country_code", "displayName": "country_code", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "pais_detectado", "displayName": "pais_detectado", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "idioma_flujo", "displayName": "idioma_flujo", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "tipo_reporte", "displayName": "tipo_reporte", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true, "removed": true }, { "id": "row_number", "displayName": "row_number", "required": false, "defaultMatch": false, "display": true, "type": "number", "canBeUsedToMatch": true, "readOnly": true, "removed": true } ], "attemptToConvertTypes": false, "convertFieldsToString": false }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 53904, 28400 ], "id": "b298c7a2-fd28-41a8-bdee-99af2b6fff3e", "name": "Sheets - Actualizar sesión imágenes lote Redis TEST", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const contexto = $('Code - Consolidar lote imagen guardado Redis TEST').first().json || {};\nconst data = {\n ...contexto,\n ...($json || {})\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(data.idioma_flujo || 'ES').toUpperCase();\n\nconst to = limpiar(\n data.whatsapp_to ||\n data.group_jid ||\n data.whatsapp_remote_jid ||\n data.sesion_activa?.group_jid ||\n data.sesion_activa?.whatsapp_to ||\n data.sender_jid ||\n ''\n);\n\nconst count = Number(\n data.imagenes_nuevas_lote ||\n data.redis_buffer_lote_count ||\n data.eventos_imagen_guardados ||\n 1\n) || 1;\n\nif (!to) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar confirmación de imágenes.');\n}\n\nconst pluralEs = count === 1 ? 'imagen recibida' : 'imágenes recibidas';\nconst pluralEn = count === 1 ? 'image received' : 'images received';\n\nconst mensajeEs =\n`✅ ${count} ${pluralEs}.\n\nPuedes enviar más imágenes si hace falta.\n\nCuando termines de enviar las imágenes, selecciona: Fotos listas`;\n\nconst mensajeEn =\n`✅ ${count} ${pluralEn}.\n\nYou can send more images if needed.\n\nWhen you finish sending images, select: Photos ready`;\n\nconst mensajeFinal = idioma === 'EN' ? mensajeEn : mensajeEs;\n\nreturn [\n {\n json: {\n ...data,\n whatsapp_to: to,\n whatsapp_header: '',\n whatsapp_buttons: [\n { id: 'FOTOS_LISTAS', title: idioma === 'EN' ? 'Photos ready' : 'Fotos listas' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensajeFinal,\n texto_respuesta: mensajeFinal,\n mensaje: mensajeFinal,\n text: mensajeFinal,\n mensaje_whatsapp: mensajeFinal,\n message_text: mensajeFinal,\n estado_mensaje: 'CONFIRMACION_IMAGENES_LOTE_PREPARADA',\n confirmacion_imagenes_lote_debug: {\n idioma,\n whatsapp_to: to,\n imagenes_nuevas_lote: count\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 54240, 28416 ], "id": "24c0d658-040e-4c73-9fef-84db85105528", "name": "Code - Preparar confirmación imágenes lote Redis TEST" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "2cb38fef-879c-45f3-8886-b2f0a35d067b", "leftValue": "={{ $json.analisis_debe_intentar_cierre }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 57312, 32272 ], "id": "309753aa-4e4f-4246-8fb2-78b1b9f193fb", "name": "IF - Análisis completo Redis TEST" }, { "parameters": { "operation": "incr", "key": "={{ $json.redis_lock_key }}", "expire": true, "ttl": 86400 }, "type": "n8n-nodes-base.redis", "typeVersion": 1, "position": [ 58912, 32176 ], "id": "5ac7ca64-e3a2-4fdd-83ab-32f06a6fcd16", "name": "Redis - Lock análisis final TEST", "credentials": { "redis": { "id": "Fu2IevwVPaoyw9OS", "name": "Redis Fulgencio" } } }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const actual = $input.item.json || {};\n\nconst extraerNumero = (valor) => {\n if (typeof valor === 'number') return valor;\n\n if (typeof valor === 'string') {\n const n = Number(valor);\n if (!Number.isNaN(n)) return n;\n }\n\n if (valor && typeof valor === 'object') {\n const valores = Object.values(valor);\n for (const v of valores) {\n const n = extraerNumero(v);\n if (n) return n;\n }\n }\n\n return 0;\n};\n\nconst redisActual = actual.redis_lock_response || actual;\n\nconst lockValor = extraerNumero(redisActual);\nconst lockAdquirido = lockValor === 1;\n\nreturn {\n json: {\n ...actual,\n\n redis_lock_response: redisActual,\n redis_lock_value: lockValor,\n\n lock_analisis_adquirido: lockAdquirido,\n decision_lock_analisis: lockAdquirido\n ? 'LOCK_ADQUIRIDO_GENERAR_REPORTE_FINAL'\n : 'LOCK_YA_EXISTIA_NO_GENERAR_DUPLICADO',\n\n lock_analisis_debug: {\n session_id: actual.session_id || '',\n redis_lock_key: actual.redis_lock_key || '',\n redis_lock_value: lockValor,\n lock_analisis_adquirido: lockAdquirido,\n redis_lock_response: redisActual,\n },\n },\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 59120, 32176 ], "id": "7c2dfb7d-e85f-4ca9-9420-81e0d95d3ba0", "name": "Code - Validar lock análisis final TEST" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "432f82ae-e1bb-4ea7-a964-0cc4bebef5de", "leftValue": "={{ $json.lock_analisis_adquirido }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 59328, 32176 ], "id": "1e768f97-7b9d-4bde-a69a-e4e543a3b1bd", "name": "IF - Lock análisis final adquirido TEST" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const actual = $input.item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n actual.session_id ||\n actual.sesion_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n actual.ejecucion_id ||\n sessionId\n);\n\nconst mediaEventId = limpiar(\n actual.media_event_id ||\n actual.message_id ||\n actual.id ||\n ''\n);\n\nconst mediaType = limpiar(\n actual.media_type ||\n actual.tipo_media ||\n ''\n).toLowerCase();\n\nconst mediaIndex = toNumber(\n actual.media_index ||\n actual.index ||\n 1,\n 1\n);\n\nconst mediaTotal = toNumber(\n actual.media_total_esperado ||\n actual.media_total ||\n actual.total_media ||\n 1,\n 1\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nif (!mediaEventId) {\n throw new Error('No llegó media_event_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nconst redisCountKey = `fulgencio:analisis-count:${sessionId}`;\nconst redisLockKey = `fulgencio:analisis-final-lock:${sessionId}`;\n\nreturn {\n json: {\n ...actual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: mediaEventId,\n media_type: mediaType,\n\n media_index: mediaIndex,\n media_total: mediaTotal,\n media_total_esperado: mediaTotal,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n contador_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: mediaType,\n media_index: mediaIndex,\n media_total: mediaTotal,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey\n }\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 56448, 32368 ], "id": "e2e99035-966e-4ee5-85c7-650d7447a4b1", "name": "Code - Preparar contador análisis Redis TEST" }, { "parameters": { "operation": "incr", "key": "={{ $json.redis_count_key }}", "expire": true, "ttl": 86400 }, "type": "n8n-nodes-base.redis", "typeVersion": 1, "position": [ 56704, 32368 ], "id": "529ab021-bc1e-42e4-b76a-52e547b282f2", "name": "Redis - Contar análisis media TEST", "credentials": { "redis": { "id": "Fu2IevwVPaoyw9OS", "name": "Redis Fulgencio" } } }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const redisActual = $input.item.json || {};\nconst ctx = $('Code - Preparar contador análisis Redis TEST').item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(ctx.session_id);\nconst ejecucionId = limpiar(ctx.ejecucion_id || sessionId);\n\nconst mediaTotalEsperado = toNumber(\n ctx.media_total_esperado ||\n ctx.media_total ||\n 1,\n 1\n);\n\nconst redisCountKey = limpiar(\n ctx.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n ctx.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\n// El Redis Increment puede devolver:\n// { \"fulgencio:analisis-count:SESSION\": 2 }\n// o puede devolver otro nombre según n8n.\nlet redisConteoActual = 0;\n\nif (redisCountKey && redisActual[redisCountKey] !== undefined) {\n redisConteoActual = toNumber(redisActual[redisCountKey], 0);\n} else {\n const valoresNumericos = Object.values(redisActual)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n redisConteoActual = valoresNumericos.length ? valoresNumericos[0] : 0;\n}\n\nconst analisisCompleto =\n mediaTotalEsperado > 0 &&\n redisConteoActual >= mediaTotalEsperado;\n\nreturn {\n json: {\n ...ctx,\n\n redis_increment_response: redisActual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_contados_redis: redisConteoActual,\n\n analisis_debe_intentar_cierre: analisisCompleto,\n decision_cierre_analisis: analisisCompleto\n ? 'ANALISIS_COMPLETO_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_ESPERAR_OTRA_EJECUCION',\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n cierre_analisis_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n redisConteoActual,\n analisisCompleto,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n media_actual: {\n media_event_id: ctx.media_event_id,\n media_type: ctx.media_type,\n media_index: ctx.media_index,\n media_total: ctx.media_total\n },\n redis_increment_response: redisActual\n }\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 56944, 32368 ], "id": "b0e5135b-79b3-48ac-a7bc-1aafcc7f89e1", "name": "Code - Validar conteo análisis Redis TEST" }, { "parameters": { "amount": 4 }, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ 57600, 32448 ], "id": "6faad376-45c9-42b4-9042-73c4cb015344", "name": "Wait - Verificar cierre análisis Redis TEST", "webhookId": "b17e9d5b-ec57-49ea-a0a7-91bf65e5e41a" }, { "parameters": { "documentId": { "__rl": true, "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", "mode": "list", "cachedResultName": "Fulgencio - Propuestas Ejecutadas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": 1107394537, "mode": "list", "cachedResultName": "wa_ejecuciones_analisis_media", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" }, "filtersUI": { "values": [ { "lookupColumn": "session_id", "lookupValue": "={{ $json.session_id }}" } ] }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 57872, 32480 ], "id": "6dcc1ce9-30c0-4ae7-82fe-fe1522e88205", "name": "Sheets - Leer análisis media cierre Redis TEST", "alwaysOutputData": true, "executeOnce": true, "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const inputRows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst contextos = [\n getNodeJson('Code - Validar reintento cierre análisis TEST'),\n getNodeJson('Wait - Reintento cierre análisis Redis TEST'),\n getNodeJson('Wait - Verificar cierre análisis Redis TEST'),\n getNodeJson('Code - Validar conteo análisis Redis TEST'),\n getNodeJson('Code - Preparar contador análisis Redis TEST')\n];\n\nconst contexto =\n contextos.find(ctx => limpiar(ctx.session_id)) ||\n inputRows.find(row => limpiar(row.session_id)) ||\n {};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n inputRows[0]?.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n inputRows[0]?.ejecucion_id ||\n sessionId\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para decidir cierre de análisis por Sheet.');\n}\n\nconst redisCountKey = limpiar(\n contexto.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n contexto.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\nconst rowsSesion = inputRows.filter(row => {\n return limpiar(row.session_id) === sessionId;\n});\n\nconst rowsCompletados = rowsSesion.filter(row => {\n const estado = limpiar(row.estado).toUpperCase();\n\n return (\n estado.includes('COMPLETADO') &&\n (\n estado.includes('ANALISIS_AUDIO') ||\n estado.includes('ANALISIS_IMAGEN') ||\n estado.includes('ANALISIS_VIDEO')\n )\n );\n});\n\nconst mapaUnicos = new Map();\n\nfor (const row of rowsCompletados) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaIndex = limpiar(row.media_index);\n const mediaType = limpiar(row.media_type).toLowerCase();\n\n const key = mediaEventId || `${mediaType}_${mediaIndex}`;\n\n if (!key) continue;\n\n if (!mapaUnicos.has(key)) {\n mapaUnicos.set(key, row);\n }\n}\n\nconst analisisUnicos = Array.from(mapaUnicos.values()).sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst esperadoDesdeContexto = Number(\n contexto.media_total_esperado ||\n contexto.media_total ||\n 0\n);\n\nconst maxMediaTotalSheet = Math.max(\n 0,\n ...analisisUnicos\n .map(row => Number(row.media_total || row.media_total_esperado || 0))\n .filter(n => Number.isFinite(n))\n);\n\nconst mediaTotalEsperado = esperadoDesdeContexto || maxMediaTotalSheet || 0;\nconst totalAnalisisSheet = analisisUnicos.length;\n\nconst analisisCompletoPorSheet =\n mediaTotalEsperado > 0 &&\n totalAnalisisSheet >= mediaTotalEsperado;\n\nreturn [\n {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n media_total_esperado: mediaTotalEsperado,\n\n total_analisis_encontrados_sheet: totalAnalisisSheet,\n analisis_completo_por_sheet: analisisCompletoPorSheet,\n analisis_debe_intentar_cierre: analisisCompletoPorSheet,\n\n decision_cierre_analisis: analisisCompletoPorSheet\n ? 'ANALISIS_COMPLETO_POR_SHEET_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_POR_SHEET_ESPERAR_REINTENTO',\n\n analisis_rows_filtrados: analisisUnicos,\n\n cierre_analisis_sheet_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n totalAnalisisSheet,\n analisisCompletoPorSheet,\n rows_leidas_sheet: inputRows.length,\n rows_misma_sesion: rowsSesion.length,\n rows_completadas: rowsCompletados.length,\n analisis_unicos: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado\n }))\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 58080, 32480 ], "id": "7667cf5c-1cea-4083-b1a6-89b10788622c", "name": "Code - Decidir cierre análisis por Sheet Redis TEST" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "5a5b3564-6292-486b-8154-5df844083a7d", "leftValue": "={{ $json.analisis_debe_intentar_cierre }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 58288, 32480 ], "id": "d7f91117-8d97-462d-b3f1-359b117699c0", "name": "IF - Cierre análisis por Sheet Redis TEST" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const redisActual = $input.item.json || {};\n\nlet contexto = {};\ntry {\n contexto = $('IF - Cierre análisis por Sheet Redis TEST').item.json || {};\n} catch (error) {\n contexto = {};\n}\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n redisActual.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n redisActual.ejecucion_id ||\n sessionId\n);\n\nconst retryKey = limpiar(\n contexto.redis_retry_key ||\n redisActual.redis_retry_key ||\n `fulgencio:analisis-retry:${sessionId}`\n);\n\nconst extraerNumeroRedis = (obj) => {\n if (!obj || typeof obj !== 'object') return 0;\n\n if (retryKey && obj[retryKey] !== undefined) {\n return toNumber(obj[retryKey], 0);\n }\n\n const valoresNumericos = Object.values(obj)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n return valoresNumericos.length ? valoresNumericos[0] : 0;\n};\n\nconst retryCount = extraerNumeroRedis(redisActual);\n\nconst maxReintentos = 12;\nconst reintentar = retryCount <= maxReintentos;\n\nreturn {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_retry_key: retryKey,\n cierre_retry_count: retryCount,\n cierre_retry_max: maxReintentos,\n cierre_retry_permitido: reintentar,\n\n decision_reintento_cierre: reintentar\n ? 'REINTENTAR_LECTURA_ANALISIS'\n : 'REINTENTOS_AGOTADOS',\n\n cierre_retry_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n retry_key: retryKey,\n redis_respuesta: redisActual,\n retry_count: retryCount,\n max_reintentos: maxReintentos,\n reintentar\n }\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 58816, 32560 ], "id": "1f3e80b8-b608-4e50-aeb5-d3a3d9302111", "name": "Code - Validar reintento cierre análisis TEST" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "ad4f88ac-eda8-4cb1-bd08-34aca455e07c", "leftValue": "={{ $json.cierre_retry_permitido }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 59024, 32560 ], "id": "80040c3e-51a4-4d5d-b2e1-0daa92be6221", "name": "IF - Reintentar cierre análisis Redis TEST" }, { "parameters": { "amount": 4 }, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ 59392, 32544 ], "id": "4fcf9142-c63d-4a03-b2ea-5135f09bf15d", "name": "Wait - Reintento cierre análisis Redis TEST", "webhookId": "b54b49e7-ede0-48e7-8973-0d45d8482cc8" }, { "parameters": { "operation": "incr", "key": "={{ 'fulgencio:analisis-retry:' + $json.session_id }}", "expire": true, "ttl": 900 }, "type": "n8n-nodes-base.redis", "typeVersion": 1, "position": [ 58608, 32560 ], "id": "356e7264-cb9c-4a24-a2fe-298a62498dd7", "name": "Redis - Contar reintento cierre análisis TEST", "credentials": { "redis": { "id": "Fu2IevwVPaoyw9OS", "name": "Redis Fulgencio" } } }, { "parameters": { "url": "={{ \n 'https://slides.googleapis.com/v1/presentations/' + \n (\n $json.presentation_id ||\n $json.presentacion_id ||\n $json.slides_presentation_id ||\n $json.presentacion_ejecucion_id ||\n String($json.presentacion_ejecucion_url || $json.presentation_url || '').match(/\\/presentation\\/d\\/([^/]+)/)?.[1]\n )\n}}", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleOAuth2Api", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 66512, 32192 ], "id": "3859c57e-67e0-4d89-ac01-648b9f9c51dc", "name": "HTTP - Obtener presentación Slides limpiar TEST", "credentials": { "googleOAuth2Api": { "id": "eHseMeH39kRcXgOF", "name": "Google account 2" } } }, { "parameters": { "jsCode": "const inputItems = $input.all();\nconst presentation = inputItems[0]?.json || {};\n\n// Contexto anterior: aquí está el link, presentation_id, conteos, etc.\nconst contexto = $('Code - Normalizar respuesta Slides ejecución TEST').first().json || {};\n\nconst limpiar = (v) => String(v ?? '').trim();\n\nconst extraerIdPresentacion = (...valores) => {\n for (const valor of valores) {\n const txt = limpiar(valor);\n if (!txt) continue;\n\n const match = txt.match(/\\/presentation\\/d\\/([^/]+)/);\n if (match?.[1]) return match[1];\n\n // Si ya viene como ID limpio\n if (!txt.includes('/') && txt.length > 20) return txt;\n }\n return '';\n};\n\nconst presentationId = extraerIdPresentacion(\n contexto.presentation_id,\n contexto.presentacion_id,\n contexto.slides_presentation_id,\n contexto.presentacion_ejecucion_id,\n contexto.presentacion_ejecucion_url,\n contexto.presentation_url,\n presentation.presentationId\n);\n\nif (!presentationId) {\n throw new Error('No se pudo detectar presentation_id para limpiar slides vacíos.');\n}\n\n// Detectar cantidad de fotos reales.\n// Usamos varios posibles nombres porque el contexto puede variar según el nodo anterior.\nlet fotosCount = Number(\n contexto.fotos_count ??\n contexto.imagenes_count ??\n contexto.images_count ??\n contexto.media_fotos_count ??\n contexto.media_imagenes_count ??\n contexto.analisis_multimedia?.imagenes?.length ??\n contexto.analisis_multimedia?.fotos?.length ??\n 0\n);\n\n// Seguridad: si llega vacío, intenta contar imágenes insertadas desde posibles arrays.\nif (!fotosCount && Array.isArray(contexto.imagenes)) {\n fotosCount = contexto.imagenes.length;\n}\n\nif (!fotosCount && Array.isArray(contexto.fotos)) {\n fotosCount = contexto.fotos.length;\n}\n\n// Máximo de placeholders esperados en plantilla\nconst maxFotosPlantilla = 25;\n\n// Si por alguna razón no tenemos conteo, NO borramos nada.\nif (!fotosCount || fotosCount < 0) {\n fotosCount = 0;\n}\n\nfotosCount = Math.min(Number(fotosCount || 0), maxFotosPlantilla);\n\nconst extraerTextoSlide = (slide) => {\n const partes = [];\n\n const elementos = slide.pageElements || [];\n for (const el of elementos) {\n const textElements = el?.shape?.text?.textElements || [];\n for (const te of textElements) {\n const content = te?.textRun?.content;\n if (content) partes.push(content);\n }\n }\n\n return partes.join(' ').replace(/\\s+/g, ' ').trim();\n};\n\nconst slides = presentation.slides || [];\n\nconst slidesDetectados = [];\nconst slidesABorrar = [];\n\nfor (const slide of slides) {\n const objectId = slide.objectId;\n const textoSlide = extraerTextoSlide(slide);\n\n // Busca FOTO 1, FOTO 2, etc. Tolerante a espacios.\n const match = textoSlide.match(/\\bFOTO\\s*(\\d{1,2})\\b/i);\n\n if (!match) continue;\n\n const numeroFoto = Number(match[1]);\n if (numeroFoto < 1 || numeroFoto > maxFotosPlantilla) continue;\n\n slidesDetectados.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n\n if (numeroFoto > fotosCount) {\n slidesABorrar.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n }\n}\n\nconst deleteRequests = slidesABorrar.map((s) => ({\n deleteObject: {\n objectId: s.objectId,\n },\n}));\n\nreturn [\n {\n json: {\n ...contexto,\n\n presentation_id: presentationId,\n slides_cleanup_fotos_count: fotosCount,\n slides_cleanup_total_slides: slides.length,\n\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n slides_fotos_vacias_count: slidesABorrar.length,\n\n delete_slide_requests: deleteRequests,\n\n slides_cleanup_debug: {\n presentation_id: presentationId,\n fotos_count_detectado: fotosCount,\n total_slides_presentacion: slides.length,\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n delete_requests_count: deleteRequests.length,\n },\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 66720, 32192 ], "id": "077d6cd0-bab3-40a3-947c-66783d53d1ca", "name": "Code - Preparar borrado slides fotos vacías TEST" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "dca8dbb7-b14a-4def-9e43-27a3c1360d78", "leftValue": "={{ Number($json.slides_fotos_vacias_count || 0) > 0 }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 66928, 32192 ], "id": "afd81f91-c63a-4b6e-b454-471a5853c66e", "name": "IF - Hay slides fotos vacías para borrar TEST" }, { "parameters": { "method": "POST", "url": "={{ 'https://slides.googleapis.com/v1/presentations/' + $json.presentation_id + ':batchUpdate' }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleOAuth2Api", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ \n {\n requests: $json.delete_slide_requests || []\n }\n}}", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 67216, 32000 ], "id": "2131703b-fd8a-4216-bf1c-c310197046eb", "name": "HTTP - Borrar slides fotos vacías TEST", "credentials": { "googleOAuth2Api": { "id": "eHseMeH39kRcXgOF", "name": "Google account 2" } } }, { "parameters": { "jsCode": "const contexto = $('Code - Preparar borrado slides fotos vacías TEST').first().json || {};\nconst respuestaBorrado = $input.first()?.json || {};\n\nreturn [\n {\n json: {\n ...contexto,\n slides_cleanup_done: true,\n slides_delete_response: respuestaBorrado,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 67424, 32000 ], "id": "d23fa3aa-c79e-455c-940d-9584e8ec4731", "name": "Code - Restaurar contexto slides limpios TEST" }, { "parameters": { "content": "## 📥 ENTRADA WHATSAPP — CHAT PRIVADO\n\nEste bloque recibe mensajes desde WhatsApp Cloud API y normaliza el evento.\n\nResponsabilidades:\n- Identificar al usuario por su número.\n- Detectar texto, audio, imagen, video o documento.\n- Interpretar botones y comandos oficiales.\n- Detectar país e idioma por prefijo telefónico.\n- Rechazar grupos, reacciones, estados y eventos vacíos.\n\nRegla principal:\nesta versión procesa únicamente conversaciones privadas iniciadas por el usuario.", "height": 528, "width": 848 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 46464, 28720 ], "id": "4b32e84a-825a-4395-9176-f6dea26bb5c3", "name": "Sticky Note" }, { "parameters": { "content": "## 🧾 CONTROL DE SESIONES PRIVADAS\n\nCada usuario tiene una sesión independiente vinculada a su número.\n\nDecisiones:\n- CREAR_SESION: inicia con Hey.\n- AVISO_SESION_ACTIVA: ya existe un reporte abierto.\n- CONTINUAR_SESION: continúa el reporte.\n- CANCELAR_SESION: cancela la sesión.\n- SIN_SESION_ACTIVA: invita a iniciar.\n- MOSTRAR_LEADERBOARD: muestra el ranking.\n\nRegla:\nUna propuesta o reporte activo por número de teléfono.\n", "height": 624, "width": 1024, "color": 4 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 47584, 28624 ], "id": "db2a1857-3804-4ec3-9707-c00231fcfcce", "name": "Sticky Note1" }, { "parameters": { "content": "## 👋 CREACIÓN Y MENSAJES DE CONTROL\n\nEste bloque crea sesiones nuevas y responde a casos operativos básicos.\n\nIncluye:\n- Crear sesión en Google Sheets.\n- Enviar bienvenida y pedir nota de voz.\n- Avisar si ya existe una sesión activa.\n- Avisar si no hay sesión activa.\n- Cancelar sesión cuando el usuario escribe CANCELAR.\n\nLa bienvenida inicia el flujo guiado de 3 pasos:\n1. Audio obligatorio\n2. Imágenes obligatorias\n3. Videos opcionales", "height": 1904, "width": 1200, "color": 5 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 50640, 25136 ], "id": "ddbe02ee-a8a4-4184-8097-f1a36ef7e2c3", "name": "Sticky Note2" }, { "parameters": { "content": "## 🧭 ROUTER DEL REPORTE ACTIVO\n\nEste bloque decide qué hacer según la etapa actual de la sesión.\n\nEtapas:\n- ESPERANDO_AUDIO\n- ESPERANDO_IMAGENES\n- ESPERANDO_VIDEOS\n- PROCESANDO\n\nDecisiones principales:\n- Guardar audio\n- Pedir audio\n- Guardar imágenes\n- Validar FOTOS LISTAS\n- Guardar videos\n- Cerrar sin video\n- Cerrar con videos\n- Avisar procesamiento\n- Ignorar eventos no válidos\n\nRegla:\ncada mensaje se procesa según la etapa activa, no de forma aislada.", "height": 912, "width": 1024, "color": 2 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 48064, 29360 ], "id": "8e59ac91-36d8-4472-b455-2759fcdb33c9", "name": "Sticky Note3" }, { "parameters": { "content": "## 🎙️ PASO 1 — AUDIO OBLIGATORIO\n\nEl reporte no avanza sin nota de voz.\n\nCuando llega un audio:\n- Se guarda el evento en wa_ejecuciones_eventos.\n- Se actualiza la sesión.\n- Se incrementa audio_count.\n- La etapa cambia a ESPERANDO_IMAGENES.\n- El bot pide las imágenes obligatorias.\n\nLa nota de voz debe explicar:\n- Propuesta o referencia\n- Marca/cliente\n- País\n- Ubicación\n- Qué se ejecutó o reportó\n- Comentarios o resultados", "height": 640, "width": 1408, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 50416, 27232 ], "id": "c52a7cdc-e1cd-476f-8bb9-ad08ba2ec556", "name": "Sticky Note4" }, { "parameters": { "content": "## 🖼️ PASO 2 — IMÁGENES OBLIGATORIAS\n\nLas imágenes se acumulan dentro de la misma sesión privada, aunque lleguen en mensajes separados.\n\nRedis:\n- Agrupa imágenes recibidas muy próximas.\n- Evita procesar dos veces el mismo lote.\n- Expande y registra cada imagen individualmente.\n- Conserva todas las imágenes anteriores de la sesión.\n\nCuando termine:\nel usuario pulsa o escribe FOTOS LISTAS.", "height": 416, "width": 3744, "color": 6 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 50800, 28256 ], "id": "85d5ebfa-7822-45c5-b0e3-3a0ccac37656", "name": "Sticky Note5" }, { "parameters": { "content": "## 🎥 PASO 3 — VIDEOS OPCIONALES\n\nLos videos son opcionales.\n\nOpciones:\n- Si no hay videos: usuario escribe SIN VIDEO.\n- Si envió videos: usuario escribe LISTO.\n- Los videos deben enviarse uno por uno, en mensajes separados.\n\nAl cerrar:\n- La sesión pasa a PROCESANDO.\n- Se envía aviso de reporte recibido.\n- El flujo inicia recuperación, análisis y generación de presentación.\n\nRegla:\nlas imágenes sí pueden ir juntas; los videos deben ir separados.", "height": 768, "width": 1632, "color": "#1D6362" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 51360, 29408 ], "id": "459549a9-14bf-4d05-ab7e-7c93eb68936e", "name": "Sticky Note6" }, { "parameters": { "content": "## 📂 RECUPERACIÓN Y ORGANIZACIÓN DE EVIDENCIAS\n\nProceso:\n1. Lee todos los eventos multimedia de la sesión.\n2. Obtiene la URL temporal de cada media_id desde Meta.\n3. Descarga el archivo binario con Graph API.\n4. Sube audio, imágenes y videos a Google Drive.\n5. Guarda metadata en wa_ejecuciones_media.\n6. Consolida toda la evidencia de la sesión.\n\nCada archivo queda asociado a session_id y ejecucion_id.", "height": 1728, "width": 4400, "color": 4 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 49344, 30336 ], "id": "21ffe33b-7fbe-481c-9343-e22f18dd96a2", "name": "Sticky Note7" }, { "parameters": { "content": "## 🤖 ANÁLISIS IA Y CONTROL REDIS\n\nGemini analiza cada evidencia multimedia:\n- Audio\n- Imágenes\n- Videos opcionales\n\nCada análisis se guarda en:\nwa_ejecuciones_analisis_media\n\nRedis controla el cierre:\n- Cuenta análisis completados.\n- Verifica si ya están todos listos.\n- Usa lock para evitar cierre duplicado.\n- Si faltan análisis, espera y reintenta.\n\nRegla:\nel JSON final solo se genera cuando el análisis multimedia está completo.", "height": 1504, "width": 5712, "color": 3 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 54512, 31504 ], "id": "70c057eb-8b6f-49ed-a322-008a8e350807", "name": "Sticky Note8" }, { "parameters": { "content": "## ✅ CIERRE FINAL DEL REPORTE\n\nEste bloque genera el resultado final del reporte.\n\nProceso:\n1. Consolida análisis multimedia.\n2. Gemini genera JSON final estructurado.\n3. Se normaliza la respuesta.\n4. Se hace match contra el Banco de Propuestas.\n5. Se verifica duplicado.\n6. Se guarda en propuestas_ejecutadas.\n7. Se crea carpeta de ejecución en Drive.\n8. Se copia la plantilla de Slides.\n9. Se insertan imágenes y se borran slides vacíos.\n10. Se actualiza el link final.\n11. Si el match es alto, se actualiza el banco original.\n12. Se envía el link final por WhatsApp.\n\nRegla:\npropuestas externas o match bajo no actualizan el banco original.", "height": 1024, "width": 10912, "color": 6 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 60736, 31552 ], "id": "35fb204a-a809-44b9-8923-2881a4e919f7", "name": "Sticky Note9" }, { "parameters": { "content": "## 🔐 MODO PRIVADO TEMPORAL\n\n- Acceso por conversación individual.\n- El usuario debe escribir primero al número oficial.\n- No depende de grupos ni de wa_grupos_permitidos.\n- Respuestas normales dentro de la conversación activa.\n- Botones rápidos disponibles para avanzar, cancelar o consultar RANKING.", "height": 288, "width": 944, "color": 4 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 52240, 28800 ], "id": "216d12b6-78d5-4dc9-9b73-5f4ef7626ace", "name": "Sticky Note10" }, { "parameters": { "jsCode": "const metadataItems = $input.all();\nconst mediaItems = $('Code - Separar media en items WhatsApp TEST').all();\n\nconst clean = (value) => String(value ?? '').trim();\n\nreturn metadataItems.map((item, index) => {\n const metadata = item.json || {};\n const media = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n const downloadUrl = clean(metadata.url);\n if (!downloadUrl) {\n throw new Error(`Meta no devolvió URL para media_source_id: ${media.media_source_id || 'SIN_ID'}`);\n }\n\n return {\n json: {\n ...media,\n media_download_url: downloadUrl,\n media_mime_type: clean(metadata.mime_type) || media.media_mime_type || 'application/octet-stream',\n media_sha256: clean(metadata.sha256),\n media_file_size: Number(metadata.file_size || 0),\n estado_procesamiento: 'MEDIA_METADATA_META_RECUPERADA'\n }\n };\n});" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 51984, 31168 ], "id": "4aaec325-8929-4135-bb3d-c6a3fba614da", "name": "Code - Preparar descarga media Meta WhatsApp TEST" }, { "parameters": { "url": "={{ $json.media_download_url }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "whatsAppApi", "options": { "response": { "response": { "responseFormat": "file" } } } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [ 52176, 31168 ], "id": "7eb86614-aadf-40f2-ad70-6a15394b5678", "name": "HTTP - Descargar media Meta WhatsApp TEST", "credentials": { "whatsAppApi": { "id": "t14kVayc9FurLReq", "name": "WhatsApp API - GLM CDC" } } }, { "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": [ 49520, 28944 ], "id": "5e35ce41-52ac-4fd5-9cdc-5472444ec10e", "name": "Sheets - Leer propuestas ejecutadas para ranking", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const rows = $input.all().map(item => item.json || {});\nconst context = $('Code - Resolver sesión WhatsApp TEST').first().json || {};\n\nconst clean = (value) => String(value ?? '').trim();\nconst norm = (value) =>\n clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst detectCountryFromPhone = (value) => {\n const n = clean(value).replace(/\\D/g, '');\n\n if (n.startsWith('502')) return 'Guatemala';\n if (n.startsWith('503')) return 'El Salvador';\n if (n.startsWith('504')) return 'Honduras';\n if (n.startsWith('505')) return 'Nicaragua';\n if (n.startsWith('506')) return 'Costa Rica';\n if (n.startsWith('507')) return 'Panamá';\n if (n.startsWith('57')) return 'Colombia';\n if (n.startsWith('52')) return 'México';\n if (n.startsWith('58')) return 'Venezuela';\n if (n.startsWith('1809') || n.startsWith('1829') || n.startsWith('1849')) return 'República Dominicana';\n if (n.startsWith('1876')) return 'Jamaica';\n if (n.startsWith('1868')) return 'Trinidad y Tobago';\n if (n.startsWith('1787') || n.startsWith('1939')) return 'Puerto Rico';\n\n return 'No identificado';\n};\n\nconst isValidCountry = (value) => {\n const country = norm(value);\n\n if (!country) return false;\n\n const invalidValues = [\n 'NO IDENTIFICADO',\n 'PAIS NO IDENTIFICADO',\n 'PAIS +1 NO IDENTIFICADO',\n 'SIN PAIS',\n 'DESCONOCIDO',\n 'UNKNOWN',\n 'N/A',\n 'NO APLICA'\n ];\n\n return !invalidValues.some((invalid) =>\n country === invalid || country.includes(invalid)\n );\n};\n\nconst uniqueExecutions = new Map();\n\nfor (const row of rows) {\n const reportType = norm(\n row.tipo_reporte ||\n row.clasificacion ||\n row.tipo ||\n ''\n );\n\n // El leaderboard mide propuestas ejecutadas; no cuenta externas.\n if (reportType.includes('EXTERNA')) continue;\n\n const executionId = clean(\n row.ejecucion_id ||\n row.session_id ||\n row.presentacion_ejecucion_id ||\n row.presentation_id ||\n row.event_id\n );\n\n if (!executionId) continue;\n if (uniqueExecutions.has(executionId)) continue;\n\n let country = clean(\n row.pais ||\n row.PAIS ||\n row.pais_detectado ||\n row.country\n );\n\n if (!country) {\n country = detectCountryFromPhone(\n row.manager_telefono ||\n row.sender_phone ||\n row.telefono\n );\n }\n\n // El leaderboard debe mostrar exclusivamente países identificados.\n // Si el país no pudo determinarse, esta ejecución no participa en el ranking.\n if (!isValidCountry(country)) continue;\n\n uniqueExecutions.set(executionId, {\n execution_id: executionId,\n country\n });\n}\n\nconst counts = {};\n\nfor (const item of uniqueExecutions.values()) {\n counts[item.country] = (counts[item.country] || 0) + 1;\n}\n\nconst ranking = Object.entries(counts)\n .map(([pais, total]) => ({ pais, total }))\n .sort((a, b) => {\n if (b.total !== a.total) return b.total - a.total;\n return a.pais.localeCompare(b.pais, 'es');\n });\n\nconst medals = ['🥇', '🥈', '🥉'];\n\nconst lines = ranking.length\n ? ranking.map((item, index) =>\n `${medals[index] || `${index + 1}.`} *${item.pais}* — ${item.total}`\n )\n : ['Todavía no hay propuestas ejecutadas registradas.'];\n\nconst language = norm(context.idioma_flujo || 'ES');\nconst total = ranking.reduce((sum, item) => sum + item.total, 0);\nconst updatedAt = new Intl.DateTimeFormat(\n language === 'EN' ? 'en-US' : 'es-DO',\n {\n timeZone: 'America/Santo_Domingo',\n dateStyle: 'medium',\n timeStyle: 'short'\n }\n).format(new Date());\n\nconst message = language === 'EN'\n ? [\n '🏆 *Executed Proposals Leaderboard*',\n '',\n ...lines,\n '',\n `📊 *Total executed proposals:* ${total}`,\n `🕒 Updated: ${updatedAt}`,\n '',\n 'Keep documenting your executions so your country moves up the ranking. 🚀'\n ].join('\\n')\n : [\n '🏆 *Leaderboard de Propuestas Ejecutadas*',\n '',\n ...lines,\n '',\n `📊 *Total de propuestas ejecutadas:* ${total}`,\n `🕒 Actualizado: ${updatedAt}`,\n '',\n 'Sigan documentando sus ejecuciones para que su país suba en el ranking. 🚀'\n ].join('\\n');\n\nreturn [\n {\n json: {\n ...context,\n is_group: false,\n group_id: '',\n group_jid: '',\n leaderboard: ranking,\n leaderboard_total: total,\n whatsapp_to: clean(\n context.whatsapp_to ||\n context.sender_phone ||\n context.manager_telefono\n ).replace(/\\D/g, ''),\n whatsapp_recipient_type: 'individual',\n whatsapp_header: language === 'EN'\n ? 'Country leaderboard'\n : 'Leaderboard por país',\n whatsapp_text: message,\n whatsapp_buttons: [\n { id: 'HEY', title: language === 'EN' ? 'Start report' : 'Iniciar reporte' }\n ]\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 49760, 28944 ], "id": "d00cc55d-e928-47b6-aaf4-4ba54dab0a47", "name": "Code - Construir leaderboard de países" }, { "parameters": { "content": "## Preparación del Leaderboard de países", "height": 272, "width": 736, "color": 5 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 49312, 28864 ], "id": "cce1ce16-dce8-482a-a0ac-6c917c109978", "name": "Sticky Note11" } ], "pinData": {}, "connections": { "Code - Normalizar evento WhatsApp TEST": { "main": [ [ { "node": "Code - Validar chat privado WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Leer sesiones existentes": { "main": [ [ { "node": "Code - Resolver sesión WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Resolver sesión WhatsApp TEST": { "main": [ [ { "node": "Switch - Decisión sesión WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Switch - Decisión sesión WhatsApp TEST": { "main": [ [ { "node": "Sheets - Crear sesión WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar aviso sesión activa WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar sin sesión para cancelar WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar cancelación sesión WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar sin sesión para cancelar WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Resolver paso activo WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Sheets - Leer propuestas ejecutadas para ranking", "type": "main", "index": 0 } ] ] }, "Code - Preparar bienvenida WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Sheets - Crear sesión WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar bienvenida WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Resolver paso activo WhatsApp TEST": { "main": [ [ { "node": "Switch - Paso activo WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Switch - Paso activo WhatsApp TEST": { "main": [ [ { "node": "Sheets - Guardar evento audio WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar solicitud audio WhatsApp TEST", "type": "main", "index": 0 } ], [], [], [ { "node": "Code - Preparar buffer imagen Redis TEST", "type": "main", "index": 0 } ], [ { "node": "Sheets - Leer eventos sesión fotos WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar aviso falta imagen WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Sheets - Guardar evento video WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "CERRAR_SIN_VIDEO", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar cierre con videos WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar recordatorio videos WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar aviso procesando WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar solicitud audio WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Sheets - Guardar evento audio WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar actualización audio recibido WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar actualización audio recibido WhatsApp TEST": { "main": [ [ { "node": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar sesión audio recibido WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar solicitud imágenes WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar aviso sesión activa WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Code - Preparar solicitud imágenes WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Code - Preparar cancelación sesión WhatsApp TEST": { "main": [ [ { "node": "Sheets - Cancelar sesión WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Cancelar sesión WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar mensaje cancelación WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar mensaje cancelación WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Code - Preparar aviso falta imagen WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Preparar actualización fotos listas": { "main": [ [ { "node": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar sesión fotos listas WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar solicitud videos WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar solicitud videos WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Code - Preparar recordatorio videos WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "CERRAR_SIN_VIDEO": { "main": [ [ { "node": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar sesión cierre sin video WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar aviso procesando WhatsApp TEST1", "type": "main", "index": 0 } ] ] }, "Sheets - Guardar evento video WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar actualización video recibido WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar actualización video recibido WhatsApp TEST": { "main": [ [ { "node": "Sheets - Actualizar sesión video recibido WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar sesión video recibido WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar confirmación video WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar confirmación video WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Code - Preparar cierre con videos WhatsApp TEST": { "main": [ [ { "node": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar sesión cierre con videos WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar aviso procesando WhatsApp TEST1", "type": "main", "index": 0 } ] ] }, "Code - Preparar aviso procesando WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Code - Preparar búsqueda eventos media WhatsApp TEST": { "main": [ [ { "node": "Sheets - Leer eventos WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Leer eventos WhatsApp TEST": { "main": [ [ { "node": "Code - Filtrar eventos media de sesión WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Filtrar eventos media de sesión WhatsApp TEST": { "main": [ [ { "node": "Code - Separar media en items WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Separar media en items WhatsApp TEST": { "main": [ [ { "node": "HTTP - Obtener metadata media Meta WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Drive - Subir media WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar registro media Drive WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar registro media Drive WhatsApp TEST": { "main": [ [ { "node": "Sheets - Guardar media Drive WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Guardar media Drive WhatsApp TEST": { "main": [ [ { "node": "Code - Consolidar media recuperada WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Consolidar media recuperada WhatsApp TEST": { "main": [ [ { "node": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar sesión media recuperada WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar paquete análisis Gemini WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Switch - Tipo media para Gemini WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar Gemini audio WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar Gemini imagen WhatsApp TEST", "type": "main", "index": 0 } ], [ { "node": "Code - Preparar Gemini video WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar Gemini audio WhatsApp TEST": { "main": [ [ { "node": "Gemini - Analizar audio WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Gemini - Analizar audio WhatsApp TEST": { "main": [ [ { "node": "Code - Normalizar análisis audio WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar Gemini imagen WhatsApp TEST": { "main": [ [ { "node": "Gemini - Analizar imagen WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Gemini - Analizar imagen WhatsApp TEST": { "main": [ [ { "node": "Code - Normalizar análisis imagen WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar Gemini video WhatsApp TEST": { "main": [ [ { "node": "Analyze video", "type": "main", "index": 0 } ] ] }, "Analyze video": { "main": [ [ { "node": "Code - Normalizar análisis video WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Normalizar análisis video WhatsApp TEST": { "main": [ [ { "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Normalizar análisis imagen WhatsApp TEST": { "main": [ [ { "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Normalizar análisis audio WhatsApp TEST": { "main": [ [ { "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Guardar análisis media Gemini WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar contador análisis Redis TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Leer análisis media Gemini WhatsApp TEST": { "main": [ [ { "node": "Code - Consolidar análisis media Gemini WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Consolidar análisis media Gemini WhatsApp TEST": { "main": [ [ { "node": "Gemini - Generar JSON final propuesta ejecutada TEST", "type": "main", "index": 0 } ] ] }, "Gemini - Generar JSON final propuesta ejecutada TEST": { "main": [ [ { "node": "Code - Normalizar JSON final propuesta ejecutada TEST", "type": "main", "index": 0 } ] ] }, "Code - Normalizar JSON final propuesta ejecutada TEST": { "main": [ [ { "node": "Sheets - Leer banco propuestas Fulgencio TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Guardar propuesta ejecutada final TEST": { "main": [ [ { "node": "Code - Preparar copia presentación ejecución TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar sesión análisis completado WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar mensaje final WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar mensaje final WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Wait": { "main": [ [ { "node": "Sheets - Leer análisis media Gemini WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Leer propuestas ejecutadas final TEST": { "main": [ [ { "node": "Code - Verificar duplicado propuesta ejecutada TEST", "type": "main", "index": 0 } ] ] }, "Code - Verificar duplicado propuesta ejecutada TEST": { "main": [ [ { "node": "IF - Propuesta final ya existe TEST", "type": "main", "index": 0 } ] ] }, "IF - Propuesta final ya existe TEST": { "main": [ [], [ { "node": "Sheets - Guardar propuesta ejecutada final TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar copia presentación ejecución TEST": { "main": [ [ { "node": "Code - Preparar carpeta ejecución TEST", "type": "main", "index": 0 } ] ] }, "Drive - Copiar plantilla presentación ejecución TEST": { "main": [ [ { "node": "Code - Normalizar link presentación ejecución TEST", "type": "main", "index": 0 } ] ] }, "Code - Normalizar link presentación ejecución TEST": { "main": [ [ { "node": "Code - Preparar reemplazos Slides ejecución TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar link presentación ejecución TEST": { "main": [ [ { "node": "Code - Preparar actualización banco propuesta ejecutada TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar reemplazos Slides ejecución TEST": { "main": [ [ { "node": "HTTP Request - Reemplazar textos Slides ejecución TEST", "type": "main", "index": 0 } ] ] }, "HTTP Request - Reemplazar textos Slides ejecución TEST": { "main": [ [ { "node": "Code - Preparar payload insertar imágenes Slides TEST", "type": "main", "index": 0 } ] ] }, "Code - Normalizar respuesta Slides ejecución TEST": { "main": [ [ { "node": "HTTP - Obtener presentación Slides limpiar TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar sin sesión para cancelar WhatsApp TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "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": "WhatsApp - Enviar aviso procesando API Oficial", "type": "main", "index": 0 } ] ] }, "Code - Restaurar contexto aviso procesando TEST": { "main": [ [ { "node": "Code - Preparar búsqueda eventos media WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Leer banco propuestas Fulgencio TEST": { "main": [ [ { "node": "Code - Match propuesta banco Fulgencio TEST", "type": "main", "index": 0 } ] ] }, "Code - Match propuesta banco Fulgencio TEST": { "main": [ [ { "node": "Code - Enriquecer datos con match banco TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar actualización banco propuesta ejecutada TEST": { "main": [ [ { "node": "Sheets - Actualizar decisión automática banco TEST", "type": "main", "index": 0 } ] ] }, "IF - Actualizar banco match alta confianza TEST": { "main": [ [ { "node": "Sheets - Actualizar link ejecutada en banco TEST", "type": "main", "index": 0 } ], [ { "node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar link ejecutada en banco TEST": { "main": [ [ { "node": "Code - Restaurar contexto actualización banco TEST", "type": "main", "index": 0 } ] ] }, "Code - Restaurar contexto actualización banco TEST": { "main": [ [ { "node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Enriquecer datos con match banco TEST": { "main": [ [ { "node": "Sheets - Leer propuestas ejecutadas final TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar decisión automática banco TEST": { "main": [ [ { "node": "Code - Restaurar contexto decisión automática banco TEST", "type": "main", "index": 0 } ] ] }, "Code - Restaurar contexto decisión automática banco TEST": { "main": [ [ { "node": "IF - Actualizar banco match alta confianza TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar buffer imagen Redis TEST": { "main": [ [ { "node": "Redis - Push buffer imagen TEST", "type": "main", "index": 0 } ] ] }, "Redis - Push buffer imagen TEST": { "main": [ [ { "node": "Code - Restaurar contexto buffer imagen TEST", "type": "main", "index": 0 } ] ] }, "Code - Restaurar contexto buffer imagen TEST": { "main": [ [ { "node": "Wait - Esperar cierre buffer imagen TEST", "type": "main", "index": 0 } ] ] }, "Wait - Esperar cierre buffer imagen TEST": { "main": [ [ { "node": "Redis - Leer buffer imagen TEST", "type": "main", "index": 0 } ] ] }, "Redis - Leer buffer imagen TEST": { "main": [ [ { "node": "Code - Decidir procesar lote imagen Redis TEST", "type": "main", "index": 0 } ] ] }, "Code - Decidir procesar lote imagen Redis TEST": { "main": [ [ { "node": "IF - Procesar lote imagen Redis TEST", "type": "main", "index": 0 } ] ] }, "IF - Procesar lote imagen Redis TEST": { "main": [ [ { "node": "Redis - Borrar buffer imagen TEST", "type": "main", "index": 0 } ] ] }, "Redis - Borrar buffer imagen TEST": { "main": [ [ { "node": "Code - Expandir lote imagen Redis TEST", "type": "main", "index": 0 } ] ] }, "Code - Expandir lote imagen Redis TEST": { "main": [ [ { "node": "Sheets - Guardar eventos imagen lote Redis TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Guardar eventos imagen lote Redis TEST": { "main": [ [ { "node": "Code - Consolidar lote imagen guardado Redis TEST", "type": "main", "index": 0 } ] ] }, "Code - Consolidar lote imagen guardado Redis TEST": { "main": [ [ { "node": "Sheets - Actualizar sesión imágenes lote Redis TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Actualizar sesión imágenes lote Redis TEST": { "main": [ [ { "node": "Code - Preparar confirmación imágenes lote Redis TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar confirmación imágenes lote Redis TEST": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "IF - Análisis completo Redis TEST": { "main": [ [ { "node": "Redis - Lock análisis final TEST", "type": "main", "index": 0 } ], [ { "node": "Wait - Verificar cierre análisis Redis TEST", "type": "main", "index": 0 } ] ] }, "Redis - Lock análisis final TEST": { "main": [ [ { "node": "Code - Validar lock análisis final TEST", "type": "main", "index": 0 } ] ] }, "Code - Validar lock análisis final TEST": { "main": [ [ { "node": "IF - Lock análisis final adquirido TEST", "type": "main", "index": 0 } ] ] }, "IF - Lock análisis final adquirido TEST": { "main": [ [ { "node": "Wait", "type": "main", "index": 0 } ] ] }, "Code - Preparar contador análisis Redis TEST": { "main": [ [ { "node": "Redis - Contar análisis media TEST", "type": "main", "index": 0 } ] ] }, "Redis - Contar análisis media TEST": { "main": [ [ { "node": "Code - Validar conteo análisis Redis TEST", "type": "main", "index": 0 } ] ] }, "Code - Validar conteo análisis Redis TEST": { "main": [ [ { "node": "IF - Análisis completo Redis TEST", "type": "main", "index": 0 } ] ] }, "Wait - Verificar cierre análisis Redis TEST": { "main": [ [ { "node": "Sheets - Leer análisis media cierre Redis TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Leer análisis media cierre Redis TEST": { "main": [ [ { "node": "Code - Decidir cierre análisis por Sheet Redis TEST", "type": "main", "index": 0 } ] ] }, "Code - Decidir cierre análisis por Sheet Redis TEST": { "main": [ [ { "node": "IF - Cierre análisis por Sheet Redis TEST", "type": "main", "index": 0 } ] ] }, "IF - Cierre análisis por Sheet Redis TEST": { "main": [ [ { "node": "Redis - Lock análisis final TEST", "type": "main", "index": 0 } ], [ { "node": "Redis - Contar reintento cierre análisis TEST", "type": "main", "index": 0 } ] ] }, "Code - Validar reintento cierre análisis TEST": { "main": [ [ { "node": "IF - Reintentar cierre análisis Redis TEST", "type": "main", "index": 0 } ] ] }, "IF - Reintentar cierre análisis Redis TEST": { "main": [ [ { "node": "Wait - Reintento cierre análisis Redis TEST", "type": "main", "index": 0 } ] ] }, "Wait - Reintento cierre análisis Redis TEST": { "main": [ [ { "node": "Sheets - Leer análisis media cierre Redis TEST", "type": "main", "index": 0 } ] ] }, "Redis - Contar reintento cierre análisis TEST": { "main": [ [ { "node": "Code - Validar reintento cierre análisis TEST", "type": "main", "index": 0 } ] ] }, "HTTP - Obtener presentación Slides limpiar TEST": { "main": [ [ { "node": "Code - Preparar borrado slides fotos vacías TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar borrado slides fotos vacías TEST": { "main": [ [ { "node": "IF - Hay slides fotos vacías para borrar TEST", "type": "main", "index": 0 } ] ] }, "IF - Hay slides fotos vacías para borrar TEST": { "main": [ [ { "node": "HTTP - Borrar slides fotos vacías TEST", "type": "main", "index": 0 } ], [ { "node": "Sheets - Actualizar link presentación ejecución TEST", "type": "main", "index": 0 } ] ] }, "HTTP - Borrar slides fotos vacías TEST": { "main": [ [ { "node": "Code - Restaurar contexto slides limpios TEST", "type": "main", "index": 0 } ] ] }, "Code - Restaurar contexto slides limpios TEST": { "main": [ [ { "node": "Sheets - Actualizar link presentación ejecución TEST", "type": "main", "index": 0 } ] ] }, "WhatsApp Trigger - API Oficial": { "main": [ [ { "node": "Code - Normalizar evento WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "WhatsApp - Enviar aviso procesando API Oficial": { "main": [ [ { "node": "Code - Restaurar contexto aviso procesando TEST", "type": "main", "index": 0 } ] ] }, "HTTP - Obtener metadata media Meta WhatsApp TEST": { "main": [ [ { "node": "Code - Preparar descarga media Meta WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Restaurar contexto binario Meta WhatsApp TEST": { "main": [ [ { "node": "Drive - Subir media WhatsApp TEST", "type": "main", "index": 0 }, { "node": "Switch - Tipo media para Gemini WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Code - Preparar descarga media Meta WhatsApp TEST": { "main": [ [ { "node": "HTTP - Descargar media Meta WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "HTTP - Descargar media Meta WhatsApp TEST": { "main": [ [ { "node": "Code - Restaurar contexto binario Meta WhatsApp TEST", "type": "main", "index": 0 } ] ] }, "Sheets - Leer propuestas ejecutadas para ranking": { "main": [ [ { "node": "Code - Construir leaderboard de países", "type": "main", "index": 0 } ] ] }, "Code - Construir leaderboard de países": { "main": [ [ { "node": "WhatsApp - Enviar mensaje API Oficial", "type": "main", "index": 0 } ] ] }, "Code - Validar chat privado WhatsApp TEST": { "main": [ [ { "node": "IF - Procesar solo chat privado TEST", "type": "main", "index": 0 } ] ] }, "IF - Procesar solo chat privado TEST": { "main": [ [ { "node": "Sheets - Leer sesiones existentes", "type": "main", "index": 0 } ] ] } }, "active": true, "settings": { "executionOrder": "v1", "binaryMode": "separate" }, "versionId": "32e19443-2c54-4d00-a3d2-9298b17a4189", "meta": { "templateCredsSetupCompleted": true, "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" }, "id": "b9s7cKNrgrmBEcHC", "tags": [] }