From 157136057ba3c1f207c43507deeb24a5fcee8e95 Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Sun, 9 Aug 2026 04:01:43 +0000 Subject: [PATCH] Create backup: Chat de WhatsApp de Propuestas Ejecutadas - API Oficial --- ...-de-propuestas-ejecutadas-api-oficial.json | 20489 ++++++++++++++++ 1 file changed, 20489 insertions(+) create mode 100644 chat-de-whatsapp-de-propuestas-ejecutadas-api-oficial.json diff --git a/chat-de-whatsapp-de-propuestas-ejecutadas-api-oficial.json b/chat-de-whatsapp-de-propuestas-ejecutadas-api-oficial.json new file mode 100644 index 0000000..d8a3513 --- /dev/null +++ b/chat-de-whatsapp-de-propuestas-ejecutadas-api-oficial.json @@ -0,0 +1,20489 @@ +{ + "updatedAt": "2026-08-08T23:23:57.401Z", + "createdAt": "2026-08-05T13:01:53.232Z", + "id": "b9s7cKNrgrmBEcHC", + "name": "Chat de WhatsApp de Propuestas Ejecutadas - API Oficial", + "description": null, + "active": true, + "isArchived": false, + "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/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 normalizePhone = (value) => {\n const raw = cleanSingleLine(value).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const digits = raw.replace(/\\D/g, '');\n return digits.length >= 7 ? `+${digits}` : '';\n };\n\n const normalizeBsuid = (value) => {\n const raw = cleanSingleLine(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n const isBsuid =\n /^[A-Za-z]{2}\\.[A-Za-z0-9]+$/.test(raw) ||\n /^[A-Za-z]{2}\\.ENT\\.[A-Za-z0-9]+$/i.test(raw) ||\n /^user\\.[A-Za-z0-9]+$/i.test(raw);\n return isBsuid ? raw : '';\n };\n\n const rawEvent = $json.raw_event || {};\n const rawMessage = rawEvent.messages?.[0] || {};\n const rawContact = rawEvent.contacts?.[0] || {};\n\n const phone = [\n $json.whatsapp_phone,\n $json.sender_phone_real,\n rawMessage.from,\n rawContact.wa_id,\n $json.whatsapp_to,\n $json.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ]\n .map(normalizePhone)\n .find(Boolean) || '';\n\n const bsuid = [\n $json.whatsapp_user_id,\n $json.whatsapp_parent_user_id,\n rawMessage.from_user_id,\n rawMessage.from_parent_user_id,\n rawContact.user_id,\n rawContact.parent_user_id,\n $json.usuario_id_origen,\n $json.sender_jid,\n $json.whatsapp_to,\n $json.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ]\n .map(normalizeBsuid)\n .find(Boolean) || '';\n\n if (!phone && !bsuid) {\n throw new Error('No se encontró un teléfono ni BSUID válido para responder por 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 const recipientFields = {};\n if (phone) recipientFields.to = phone;\n if (bsuid) recipientFields.recipient = bsuid;\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 ...recipientFields,\n type: 'interactive',\n interactive\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n ...recipientFields,\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": [ + 109872, + 44400 + ], + "id": "ab62d4b4-b438-498d-8f21-76b565e6ab50", + "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] || {};\nconst contact = contacts[0] || {};\n\nconst clean = (value) => String(value ?? '').trim();\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 normalizePhone = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const onlyDigits = raw.replace(/\\D/g, '');\n return onlyDigits.length >= 7 ? onlyDigits : '';\n};\n\nconst normalizeBsuid = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n const isBsuid =\n /^[A-Za-z]{2}\\.[A-Za-z0-9]+$/.test(raw) ||\n /^[A-Za-z]{2}\\.ENT\\.[A-Za-z0-9]+$/i.test(raw) ||\n /^user\\.[A-Za-z0-9]+$/i.test(raw);\n return isBsuid ? raw : '';\n};\n\n// --------------------------------------------------\n// 2. Identidad del remitente: teléfono + BSUID\n// --------------------------------------------------\nconst profileName =\n clean(contact.profile?.name) ||\n clean(message.profile?.name) ||\n 'Usuario WhatsApp';\n\nconst username =\n clean(contact.profile?.username) ||\n clean(contact.username) ||\n clean(message.username) ||\n '';\n\nconst senderPhone =\n normalizePhone(message.from) ||\n normalizePhone(contact.wa_id) ||\n normalizePhone(entryValue.from) ||\n normalizePhone(input.from) ||\n '';\n\nconst senderUserId =\n normalizeBsuid(message.from_user_id) ||\n normalizeBsuid(message.user_id) ||\n normalizeBsuid(contact.user_id) ||\n normalizeBsuid(entryValue.from_user_id) ||\n normalizeBsuid(entryValue.user_id) ||\n normalizeBsuid(input.from_user_id) ||\n normalizeBsuid(input.user_id) ||\n normalizeBsuid(message.from) ||\n normalizeBsuid(entryValue.from) ||\n normalizeBsuid(input.from) ||\n '';\n\nconst senderParentUserId =\n normalizeBsuid(message.from_parent_user_id) ||\n normalizeBsuid(message.parent_user_id) ||\n normalizeBsuid(contact.parent_user_id) ||\n normalizeBsuid(entryValue.from_parent_user_id) ||\n normalizeBsuid(entryValue.parent_user_id) ||\n normalizeBsuid(input.from_parent_user_id) ||\n normalizeBsuid(input.parent_user_id) ||\n '';\n\nconst senderIdentity =\n senderUserId ||\n senderParentUserId ||\n senderPhone;\n\nconst privateDestination =\n senderPhone ||\n senderUserId ||\n senderParentUserId;\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 : privateDestination;\n\n// --------------------------------------------------\n// 3. 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// 4. 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// 5. 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 if (t === 'RANKING') return 'LEADERBOARD';\n\n return 'NORMAL';\n};\n\n// --------------------------------------------------\n// 6. País e idioma: teléfono, country_code o prefijo del BSUID\n// --------------------------------------------------\nconst countryByIso = {\n GT: { country_code: '502', pais_detectado: 'Guatemala', idioma_flujo: 'ES' },\n SV: { country_code: '503', pais_detectado: 'El Salvador', idioma_flujo: 'ES' },\n HN: { country_code: '504', pais_detectado: 'Honduras', idioma_flujo: 'ES' },\n NI: { country_code: '505', pais_detectado: 'Nicaragua', idioma_flujo: 'ES' },\n CR: { country_code: '506', pais_detectado: 'Costa Rica', idioma_flujo: 'ES' },\n PA: { country_code: '507', pais_detectado: 'Panamá', idioma_flujo: 'ES' },\n CO: { country_code: '57', pais_detectado: 'Colombia', idioma_flujo: 'ES' },\n MX: { country_code: '52', pais_detectado: 'México', idioma_flujo: 'ES' },\n VE: { country_code: '58', pais_detectado: 'Venezuela', idioma_flujo: 'ES' },\n DO: { country_code: '1', pais_detectado: 'República Dominicana', idioma_flujo: 'ES' },\n JM: { country_code: '1', pais_detectado: 'Jamaica', idioma_flujo: 'EN' },\n TT: { country_code: '1', pais_detectado: 'Trinidad y Tobago', idioma_flujo: 'EN' },\n PR: { country_code: '1', pais_detectado: 'Puerto Rico', idioma_flujo: 'ES' }\n};\n\nconst detectCountryFromPhone = (phone) => {\n const n = normalizePhone(phone);\n\n if (n.startsWith('502')) return countryByIso.GT;\n if (n.startsWith('503')) return countryByIso.SV;\n if (n.startsWith('504')) return countryByIso.HN;\n if (n.startsWith('505')) return countryByIso.NI;\n if (n.startsWith('506')) return countryByIso.CR;\n if (n.startsWith('507')) return countryByIso.PA;\n if (n.startsWith('57')) return countryByIso.CO;\n if (n.startsWith('52')) return countryByIso.MX;\n if (n.startsWith('58')) return countryByIso.VE;\n\n if (n.startsWith('1809') || n.startsWith('1829') || n.startsWith('1849')) return countryByIso.DO;\n if (n.startsWith('1876')) return countryByIso.JM;\n if (n.startsWith('1868')) return countryByIso.TT;\n if (n.startsWith('1787') || n.startsWith('1939')) return countryByIso.PR;\n\n return null;\n};\n\nconst rawCountryCode = normalizeText(\n contact.profile?.country_code ||\n contact.country_code ||\n message.country_code ||\n entryValue.country_code ||\n input.country_code ||\n ''\n);\n\nconst bsuidForCountry = senderUserId || senderParentUserId;\nconst bsuidCountryCode = bsuidForCountry.includes('.')\n ? normalizeText(bsuidForCountry.split('.')[0])\n : '';\n\nconst countryInfo =\n detectCountryFromPhone(senderPhone) ||\n countryByIso[rawCountryCode] ||\n countryByIso[bsuidCountryCode] ||\n { country_code: rawCountryCode || '', pais_detectado: 'No identificado', idioma_flujo: 'ES' };\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 || senderIdentity,\n manager_nombre: profileName,\n usuario_id_origen: senderIdentity,\n\n whatsapp_phone: senderPhone ? `+${senderPhone}` : '',\n whatsapp_user_id: senderUserId,\n whatsapp_parent_user_id: senderParentUserId,\n whatsapp_username: username,\n sender_phone_real: senderPhone,\n identidad_tipo:\n senderPhone && senderUserId\n ? 'PHONE_AND_BSUID'\n : senderPhone\n ? 'PHONE'\n : senderUserId || senderParentUserId\n ? 'BSUID'\n : 'UNKNOWN',\n\n is_group: isGroup,\n group_jid: groupId,\n group_id: groupId,\n group_name: clean(entryValue.group_name || input.group_name || ''),\n\n sender_jid: senderIdentity,\n sender_phone: senderPhone || senderIdentity,\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": [ + 85328, + 41680 + ], + "id": "37f48caa-4518-4edb-861b-6468d9c341ec", + "name": "Code - Normalizar evento WhatsApp TEST" + }, + { + "parameters": { + "updates": [ + "messages" + ], + "options": {} + }, + "type": "n8n-nodes-base.whatsAppTrigger", + "typeVersion": 1, + "position": [ + 85120, + 41680 + ], + "id": "d7bba797-6298-47b6-8e07-fa7e97758eaf", + "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": [ + 86512, + 41632 + ], + "id": "f28feebb-406d-41e0-b7de-525db4105327", + "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 normalizarIdentidad = (valor) => {\n const raw = texto(valor).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw.toUpperCase();\n return raw.replace(/\\D/g, '');\n};\n\nconst telefonoReal = (valor) => {\n const raw = texto(valor).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const n = raw.replace(/\\D/g, '');\n return n.length >= 7 ? n : '';\n};\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 currentKeys = [\n evento.whatsapp_user_id,\n evento.whatsapp_parent_user_id,\n evento.usuario_id_origen,\n evento.sender_jid,\n evento.sender_phone_real,\n evento.sender_phone,\n evento.manager_telefono,\n evento.whatsapp_to,\n evento.whatsapp_remote_jid\n]\n .map(normalizarIdentidad)\n .filter(Boolean);\n\nconst uniqueCurrentKeys = [...new Set(currentKeys)];\n\nconst primaryIdentity = normalizarIdentidad(\n evento.whatsapp_user_id ||\n evento.whatsapp_parent_user_id ||\n evento.usuario_id_origen ||\n evento.sender_jid ||\n evento.sender_phone_real ||\n evento.sender_phone ||\n evento.manager_telefono ||\n evento.whatsapp_to\n);\n\nconst senderPhoneReal =\n telefonoReal(evento.sender_phone_real) ||\n telefonoReal(evento.whatsapp_phone) ||\n telefonoReal(evento.sender_phone) ||\n telefonoReal(evento.manager_telefono) ||\n telefonoReal(evento.whatsapp_to);\n\nconst whatsappDestination = texto(\n senderPhoneReal ||\n evento.whatsapp_to ||\n evento.whatsapp_remote_jid ||\n evento.whatsapp_user_id ||\n evento.whatsapp_parent_user_id ||\n evento.usuario_id_origen ||\n evento.sender_jid\n);\n\nconst managerContacto = texto(\n senderPhoneReal ||\n evento.manager_telefono ||\n evento.sender_phone ||\n primaryIdentity\n);\n\nconst accion = texto(evento.accion_flujo || 'NORMAL');\n\nconst idiomaFlujo =\n normalizar(evento.idioma_flujo || 'ES') === 'EN'\n ? 'EN'\n : 'ES';\n\nif (!primaryIdentity || !whatsappDestination) {\n throw new Error('No llegó un identificador válido del usuario (teléfono o BSUID) para resolver la sesión privada.');\n}\n\n// Reutiliza sesiones privadas por cualquiera de las dos identidades:\n// BSUID (preferido) o teléfono histórico.\nconst sesionActiva = sesiones\n .filter((row) => {\n const rowKeys = [\n row.sender_jid,\n row.sender_phone,\n row.manager_telefono\n ]\n .map(normalizarIdentidad)\n .filter(Boolean);\n\n const rowGroup = texto(row.group_jid || row.group_id);\n const rowIsGroup = esVerdadero(row.is_group) || Boolean(rowGroup);\n\n const sameIdentity = rowKeys.some((key) => uniqueCurrentKeys.includes(key));\n\n return sameIdentity && !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 identitySuffix = primaryIdentity.replace(/[^A-Z0-9]/gi, '').slice(-4) || '0000';\nconst sessionId = `WA_${Date.now()}_${identitySuffix}_${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\n whatsapp_phone: senderPhoneReal ? `+${senderPhoneReal}` : '',\n whatsapp_user_id: texto(evento.whatsapp_user_id),\n whatsapp_parent_user_id: texto(evento.whatsapp_parent_user_id),\n\n whatsapp_to: whatsappDestination,\n whatsapp_remote_jid: whatsappDestination,\n whatsapp_recipient_type: 'individual',\n\n manager_telefono: managerContacto,\n sender_phone: senderPhoneReal || texto(evento.sender_phone) || primaryIdentity,\n sender_jid: texto(evento.whatsapp_user_id || evento.sender_jid || primaryIdentity),\n usuario_id_origen: texto(evento.whatsapp_user_id || evento.usuario_id_origen || primaryIdentity),\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: managerContacto,\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\n // sender_jid guarda el BSUID cuando existe; esto permite sesiones\n // estables incluso si el usuario oculta su número.\n sender_jid: texto(evento.whatsapp_user_id || evento.sender_jid || primaryIdentity),\n sender_phone: senderPhoneReal || texto(evento.sender_phone) || primaryIdentity,\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 identidad_primaria: primaryIdentity,\n identidades_candidatas: uniqueCurrentKeys,\n whatsapp_to: whatsappDestination,\n whatsapp_phone: senderPhoneReal ? `+${senderPhoneReal}` : '',\n sender_phone_real: senderPhoneReal,\n whatsapp_user_id: texto(evento.whatsapp_user_id),\n whatsapp_parent_user_id: texto(evento.whatsapp_parent_user_id),\n idioma_final_usado: idiomaFlujo,\n pais_detectado: texto(evento.pais_detectado),\n country_code: texto(evento.country_code),\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": [ + 86704, + 41632 + ], + "id": "879d137f-7cd2-43af-aede-b3b451ef4420", + "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": [ + 86928, + 41568 + ], + "id": "eb2e3996-7dd1-414b-9d2c-287d9019145d", + "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": [ + 89680, + 38096 + ], + "id": "e50206e3-fdee-4a65-a289-04685ecd7c0a", + "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": [ + 89712, + 38544 + ], + "id": "5085b9a5-8267-4a8d-8cf4-e124d04a9258", + "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": [ + 89472, + 38096 + ], + "id": "a6d457e7-c4fe-45a2-9a8f-688061aa60d7", + "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": [ + 87232, + 42128 + ], + "id": "dd8b0d2a-1672-4c87-a2c3-e404dd900cde", + "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": [ + 87408, + 42320 + ], + "id": "f609051a-a5c5-41a9-90bb-4913f6951155", + "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": [ + 89840, + 40336 + ], + "id": "7c772d23-313b-46d0-8fa0-e9340b4d86d9", + "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": [ + 89552, + 39952 + ], + "id": "53096943-afa2-4938-af55-d0a499a0b1d9", + "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": [ + 89760, + 39952 + ], + "id": "cf67f9b0-5456-48df-87c8-866eaa22f71b", + "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": [ + 89968, + 39952 + ], + "id": "a8603316-1728-49b1-a344-e62b4b724544", + "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": [ + 90192, + 39952 + ], + "id": "03a1f157-144b-4380-92a3-765e91b84c4f", + "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": [ + 89584, + 39360 + ], + "id": "f21d99bd-2fee-4136-87f9-c0f7f7b69fe0", + "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": [ + 89792, + 39360 + ], + "id": "5ec33212-e750-4c78-b0d0-9e4e9290a29a", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\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 whatsappTo = normalizarDestinoPrivado(\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 destino 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": [ + 90048, + 39296 + ], + "id": "295224e8-9051-4eba-b4ef-08fa4e25dc51", + "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 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\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\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 if (/[A-Za-z]/.test(raw)) return raw;\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": [ + 90560, + 42384 + ], + "id": "8ac878b1-bc57-4f9d-a05d-2fd9214fac36", + "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 normalizarIdentidad = (valor) => {\n const raw = texto(valor).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw.toUpperCase();\n return raw.replace(/\\D/g, '');\n};\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 = normalizarIdentidad(\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 = normalizarIdentidad(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": [ + 91168, + 41728 + ], + "id": "41fc12ef-11be-4490-aef5-d609572fb466", + "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": [ + 91360, + 41728 + ], + "id": "29c338cd-e5ee-457c-a752-ea8132acc1a8", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\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 : normalizarDestinoPrivado(destinoBase);\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": [ + 91584, + 41728 + ], + "id": "58c58dde-d095-483e-a15a-54c4ea8f3815", + "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 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\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\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 if (/[A-Za-z]/.test(raw)) return raw;\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": [ + 88688, + 44208 + ], + "id": "6eda0309-9e97-4a21-aa8b-ed78acd0f20e", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 : normalizarDestinoPrivado(telefonoBase);\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": [ + 88336, + 43600 + ], + "id": "bcb9b541-e658-44e3-9f4f-f2d96fa56a73", + "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": [ + 88544, + 43600 + ], + "id": "458feb7d-73f1-46d4-bebc-c469d90d756b", + "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": [ + 90688, + 42624 + ], + "id": "d11b97c0-12ba-4e3b-b4d5-9afdbe7575c1", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 : normalizarDestinoPrivado(telefonoBase);\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": [ + 90912, + 42560 + ], + "id": "3881a504-161e-4d18-8733-1bc0b4f25a1b", + "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": [ + 91120, + 42560 + ], + "id": "2582861e-e0be-4ed8-94f5-5c0fffacf014", + "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 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\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\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 if (/[A-Za-z]/.test(raw)) return raw;\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": [ + 91328, + 42560 + ], + "id": "40012d12-3692-47be-a99e-5307d1c0f351", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 : normalizarDestinoPrivado(telefonoBase);\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": [ + 88256, + 43888 + ], + "id": "f3b56567-2cdf-4ba1-840d-675529ea3af3", + "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": [ + 88464, + 43888 + ], + "id": "3757fce4-499a-4426-a088-65ee039e62a8", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 : normalizarDestinoPrivado(telefonoBase);\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": [ + 88752, + 44448 + ], + "id": "be7a39cc-8a32-4127-8968-8b24788b58fd", + "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": [ + 89520, + 43808 + ], + "id": "2ea4ddbf-9081-4247-8bd6-fd82c3367f76", + "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": [ + 89776, + 43808 + ], + "id": "74d214ac-eff5-413d-8002-a38efbab55ac", + "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": [ + 90016, + 43808 + ], + "id": "a7133cc5-00dc-4238-a81d-e359f6383edf", + "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": [ + 90256, + 43808 + ], + "id": "43df5376-faf9-468a-bde7-85394abcd2e9", + "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": [ + 90464, + 43808 + ], + "id": "7056eaf3-79d9-4458-9b3c-a68ab878be29", + "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": [ + 90976, + 43808 + ], + "id": "40609efe-d3e7-4434-a4d9-743ae29468cc", + "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": [ + 90912, + 43808 + ], + "id": "474ec6ba-41d9-4e6c-bb18-9200b5bcdd83", + "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": [ + 91120, + 43808 + ], + "id": "c297b709-82f9-4656-afc6-f76c50b76a76", + "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": [ + 91536, + 43808 + ], + "id": "90fda4f9-fd16-408b-bfda-38cd3447423b", + "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": [ + 91328, + 43808 + ], + "id": "fcdeeca9-b585-4426-a7ce-111dc82e8d27", + "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": [ + 91744, + 43808 + ], + "id": "4ee87837-de9d-46f3-a34c-08745d7f51c2", + "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": [ + 91952, + 43808 + ], + "id": "c6e698dd-4f4a-457a-8222-7cb307a35a46", + "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": [ + 93200, + 44928 + ], + "id": "f5b00af6-f2ff-4ff6-a8d4-0f71cc95e4bb", + "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": [ + 93680, + 44752 + ], + "id": "6a482b01-d853-494f-a1ac-dd4e7655aea1", + "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": [ + 93888, + 44752 + ], + "id": "2476d566-d204-45ec-8dd8-516d9facd93a", + "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": [ + 94096, + 44752 + ], + "id": "c95190d8-e607-45b5-b6c1-b0045ea5d333", + "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": [ + 93696, + 44976 + ], + "id": "8c06f2a2-7357-49f4-b474-d901bf0f7622", + "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": [ + 93904, + 44976 + ], + "id": "18e8af54-48a9-4717-aa82-7b4943bc8593", + "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": [ + 94112, + 44976 + ], + "id": "6b8c4d6b-ec9b-4548-87aa-fe8875f7a859", + "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": [ + 93568, + 45216 + ], + "id": "dca2f800-90b1-470e-a1cc-e80b70eeabfc", + "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": [ + 93776, + 45216 + ], + "id": "962e3c0a-d7e3-4f86-8c59-19c458969e45", + "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": [ + 93984, + 45216 + ], + "id": "3104a7ce-8e65-4a14-b223-dd26ad0f5911", + "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": [ + 94768, + 45008 + ], + "id": "e4f045a7-acd5-425b-84f5-dd6ff54834e4", + "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": [ + 98560, + 44816 + ], + "id": "ee179ac3-10ed-447f-969e-7bddd3832954", + "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": [ + 99456, + 44848 + ], + "id": "315f4c21-3e3d-4dea-ab56-ff0e965ca5fe", + "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": [ + 99648, + 44848 + ], + "id": "fbc5bf77-0db6-4e36-8481-ffdc75d930cc", + "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": [ + 100000, + 44848 + ], + "id": "48ea284e-29f4-4b94-9761-1f95899e1822", + "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": [ + 101744, + 44928 + ], + "id": "200760fa-96eb-4715-b1d3-d9fbab011cf9", + "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": [ + 109072, + 44816 + ], + "id": "50f1b769-a4f5-4958-9f27-856d64e80bd0", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\n\nconst whatsappTo = normalizarDestinoPrivado(\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 destino 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": [ + 109360, + 44784 + ], + "id": "4a8a167e-e72f-4d96-b43a-c8b4920709c5", + "name": "Code - Preparar mensaje final WhatsApp TEST" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 98368, + 44816 + ], + "id": "7bae126c-564e-4d97-bde1-43c75aca0fce", + "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": [ + 100960, + 44880 + ], + "id": "ac6d7e32-7515-4409-84af-8e38b9a02f10", + "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": [ + 101168, + 44880 + ], + "id": "2e22f8b2-b103-4609-a982-a7fea4ac576f", + "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": [ + 101376, + 44880 + ], + "id": "638639c2-0d8b-4840-ac2a-f451875419ea", + "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": [ + 101952, + 44896 + ], + "id": "3f6f9b38-a637-4518-9355-2cd406094690", + "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": [ + 103504, + 44896 + ], + "id": "3553b5b0-3c5b-4a96-a689-ac5703945595", + "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": [ + 103776, + 44896 + ], + "id": "ac52fc21-68f3-42c4-86dd-bbdce3e2b62e", + "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": [ + 106992, + 44752 + ], + "id": "53a5ffbc-e656-4242-96a4-f3365fc5e739", + "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": [ + 103984, + 44896 + ], + "id": "33ccdc99-104a-405b-828a-7ceb3df41cc4", + "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": [ + 104256, + 44832 + ], + "id": "73a65962-2de1-480a-be25-449ac76b193b", + "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": [ + 104864, + 44832 + ], + "id": "9562e363-a097-4f27-adc9-7b5c71e26ade", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 = normalizarDestinoPrivado(\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 destino 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": [ + 89792, + 38912 + ], + "id": "32630225-3feb-4909-a913-b9a93d585ea3", + "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": [ + 90896, + 41728 + ], + "id": "364723b0-a653-4e75-a081-f1eeedba1f36", + "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": [ + 102160, + 44896 + ], + "id": "0b1bc36f-d6d4-40bc-85d7-cdc3dff701c2", + "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": [ + 102368, + 44896 + ], + "id": "9dc198ee-0812-4d36-896f-ae3b30cfd825", + "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": [ + 102576, + 44896 + ], + "id": "8fe63f67-6fba-43aa-afaf-1633175c0c5e", + "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": [ + 102784, + 44896 + ], + "id": "5f954231-348e-4add-bd6e-8df85abc8318", + "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": [ + 102992, + 44896 + ], + "id": "385d2bc1-6984-4705-b78b-01ab594e517e", + "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": [ + 103200, + 44896 + ], + "id": "476a36fb-5972-4682-a78c-b43a9fd4bfe9", + "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": [ + 104464, + 44832 + ], + "id": "caaf2436-7d47-41d2-ba7c-1d81c400fbe4", + "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": [ + 104672, + 44832 + ], + "id": "7db90e15-30b8-4e1d-9065-761919c64c3b", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\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 whatsappTo = normalizarDestinoPrivado(\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 destino 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": [ + 88800, + 43776 + ], + "id": "ab7c8c19-a16e-4b2e-ba87-ca210678fb48", + "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": [ + 89248, + 43776 + ], + "id": "85e64dea-a170-4494-a330-a5ff1db6506a", + "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/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 normalizePhone = (value) => {\n const raw = cleanSingleLine(value).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const digits = raw.replace(/\\D/g, '');\n return digits.length >= 7 ? `+${digits}` : '';\n };\n\n const normalizeBsuid = (value) => {\n const raw = cleanSingleLine(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n const isBsuid =\n /^[A-Za-z]{2}\\.[A-Za-z0-9]+$/.test(raw) ||\n /^[A-Za-z]{2}\\.ENT\\.[A-Za-z0-9]+$/i.test(raw) ||\n /^user\\.[A-Za-z0-9]+$/i.test(raw);\n return isBsuid ? raw : '';\n };\n\n const rawEvent = $json.raw_event || {};\n const rawMessage = rawEvent.messages?.[0] || {};\n const rawContact = rawEvent.contacts?.[0] || {};\n\n const phone = [\n $json.whatsapp_phone,\n $json.sender_phone_real,\n rawMessage.from,\n rawContact.wa_id,\n $json.whatsapp_to,\n $json.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ]\n .map(normalizePhone)\n .find(Boolean) || '';\n\n const bsuid = [\n $json.whatsapp_user_id,\n $json.whatsapp_parent_user_id,\n rawMessage.from_user_id,\n rawMessage.from_parent_user_id,\n rawContact.user_id,\n rawContact.parent_user_id,\n $json.usuario_id_origen,\n $json.sender_jid,\n $json.whatsapp_to,\n $json.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ]\n .map(normalizeBsuid)\n .find(Boolean) || '';\n\n if (!phone && !bsuid) {\n throw new Error('No se encontró un teléfono ni BSUID válido para responder por 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 const recipientFields = {};\n if (phone) recipientFields.to = phone;\n if (bsuid) recipientFields.recipient = bsuid;\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 ...recipientFields,\n type: 'interactive',\n interactive\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n ...recipientFields,\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": [ + 89024, + 43776 + ], + "id": "8dfb73e8-43df-42b9-88f0-f09852ffdff9", + "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": [ + 100208, + 44848 + ], + "id": "0959b924-ee0c-4129-8eb5-07fe31b43613", + "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": [ + 100416, + 44848 + ], + "id": "0e066ac2-e1cc-43e3-871a-c4a509987b9b", + "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": [ + 107312, + 44656 + ], + "id": "e2f5a6d0-8ffb-4109-9544-e0b824f0c055", + "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": [ + 108080, + 44704 + ], + "id": "b4cab921-67f7-492e-8781-b9415517f85b", + "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": [ + 108432, + 44576 + ], + "id": "8182db08-1e0b-41fd-8031-92dedaa288cb", + "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": [ + 108720, + 44576 + ], + "id": "5048f6f2-cba3-443d-8522-5b4412365484", + "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": [ + 100624, + 44864 + ], + "id": "6aea7ea7-d889-4a87-8751-630a97485b7a", + "name": "Code - Enriquecer datos con match banco TEST" + }, + { + "parameters": { + "jsCode": "const items = $input.all();\n\nconst clean = (value) => String(value ?? '').trim();\n\nconst normalizePhone = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const digits = raw.replace(/\\D/g, '');\n return digits.length >= 7 ? digits : '';\n};\n\nconst normalizeBsuid = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n const isBsuid =\n /^[A-Za-z]{2}\\.[A-Za-z0-9]+$/.test(raw) ||\n /^[A-Za-z]{2}\\.ENT\\.[A-Za-z0-9]+$/i.test(raw) ||\n /^user\\.[A-Za-z0-9]+$/i.test(raw);\n return isBsuid ? raw : '';\n};\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\n const rawMessage =\n evento.raw_event?.messages?.[0] ||\n evento.raw_event ||\n {};\n\n const rawContact =\n evento.raw_event?.contacts?.[0] ||\n {};\n\n const senderPhone =\n normalizePhone(evento.sender_phone_real) ||\n normalizePhone(evento.whatsapp_phone) ||\n normalizePhone(rawMessage.from) ||\n normalizePhone(rawContact.wa_id) ||\n normalizePhone(evento.sender_phone) ||\n normalizePhone(evento.manager_telefono) ||\n normalizePhone(evento.whatsapp_to) ||\n '';\n\n const senderUserId =\n normalizeBsuid(evento.whatsapp_user_id) ||\n normalizeBsuid(rawMessage.from_user_id) ||\n normalizeBsuid(rawMessage.user_id) ||\n normalizeBsuid(rawContact.user_id) ||\n normalizeBsuid(evento.usuario_id_origen) ||\n normalizeBsuid(evento.sender_jid) ||\n normalizeBsuid(evento.whatsapp_to) ||\n '';\n\n const senderParentUserId =\n normalizeBsuid(evento.whatsapp_parent_user_id) ||\n normalizeBsuid(rawMessage.from_parent_user_id) ||\n normalizeBsuid(rawMessage.parent_user_id) ||\n normalizeBsuid(rawContact.parent_user_id) ||\n '';\n\n const senderIdentity =\n senderUserId ||\n senderParentUserId ||\n senderPhone;\n\n const privateDestination =\n senderPhone ||\n senderUserId ||\n senderParentUserId;\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 (!senderIdentity) reasons.push('remitente sin identificador 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 is_group: false,\n group_id: '',\n group_jid: '',\n group_name: '',\n\n whatsapp_phone: senderPhone ? `+${senderPhone}` : '',\n whatsapp_user_id: senderUserId,\n whatsapp_parent_user_id: senderParentUserId,\n\n whatsapp_to: privateDestination,\n whatsapp_remote_jid: privateDestination,\n whatsapp_recipient_type: 'individual',\n\n manager_telefono:\n senderPhone ||\n clean(evento.manager_telefono) ||\n senderIdentity,\n\n sender_phone_real: senderPhone,\n\n sender_phone:\n senderPhone ||\n clean(evento.sender_phone) ||\n senderIdentity,\n\n sender_jid:\n senderUserId ||\n senderParentUserId ||\n clean(evento.sender_jid) ||\n senderIdentity,\n\n usuario_id_origen:\n senderUserId ||\n senderParentUserId ||\n clean(evento.usuario_id_origen) ||\n senderIdentity,\n\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_identity: senderIdentity,\n whatsapp_phone: senderPhone ? `+${senderPhone}` : '',\n whatsapp_user_id: senderUserId,\n whatsapp_parent_user_id: senderParentUserId,\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": [ + 85744, + 41680 + ], + "id": "2f466b2b-533a-4067-a9fe-f83abb92d437", + "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": [ + 86256, + 41648 + ], + "id": "a9ea43dd-28a3-4a11-a2b7-a5c5981755a2", + "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": [ + 107568, + 44736 + ], + "id": "b928462c-5b55-46b6-9c5e-c4171536590d", + "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": [ + 107776, + 44736 + ], + "id": "84ee49ac-b282-4a26-b105-352fda82b68b", + "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": [ + 90048, + 41056 + ], + "id": "8264eb15-3b1d-4260-9289-81bb74607276", + "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": [ + 90256, + 41056 + ], + "id": "d6f735d7-f3f0-41d7-8981-a42017e3b509", + "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": [ + 90464, + 41056 + ], + "id": "a87aa2a7-9e3e-4b54-bf21-cccf1ef47164", + "name": "Code - Restaurar contexto buffer imagen TEST" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 90672, + 41056 + ], + "id": "6d44254c-0892-4f02-8c5f-31a25830f8cb", + "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": [ + 90880, + 41056 + ], + "id": "24bc0ad4-854a-43eb-a93e-69b270db18e2", + "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": [ + 91088, + 41056 + ], + "id": "d7cd56cc-deec-4eec-bce3-c2398b613caf", + "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": [ + 91312, + 41056 + ], + "id": "13e416fe-d784-4b6a-ae10-1b7919389d5e", + "name": "IF - Procesar lote imagen Redis TEST" + }, + { + "parameters": { + "operation": "delete", + "key": "={{ $json.redis_buffer_key }}" + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 91584, + 41040 + ], + "id": "3a85c9b8-4702-4655-963b-164db5c41067", + "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": [ + 91792, + 41040 + ], + "id": "84886e9d-12ad-4c3b-b943-325d353c58f6", + "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": [ + 92032, + 41040 + ], + "id": "fa33b076-6923-4117-9128-740751ee2ac5", + "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": [ + 92240, + 41040 + ], + "id": "5622ed59-220e-4873-adba-90743918b36b", + "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": [ + 92496, + 41040 + ], + "id": "2a781320-45d6-4c70-a31e-8cef201d197a", + "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": [ + 92832, + 41056 + ], + "id": "9345d57a-5d40-4c20-8129-36d3571375b0", + "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": [ + 95904, + 44912 + ], + "id": "c0100e53-2ef6-40d8-b8c4-9a9b2bc791d9", + "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": [ + 97504, + 44816 + ], + "id": "8f02db71-9ad8-448d-9bae-eea72d39b4de", + "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": [ + 97712, + 44816 + ], + "id": "22020a22-4b21-40ab-a6d9-87818b1cca21", + "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": [ + 97920, + 44816 + ], + "id": "943f1a58-eed2-4417-ac6d-770a49933975", + "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": [ + 95040, + 45008 + ], + "id": "0f839019-6d9e-4f25-afa9-82ac0ae94106", + "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": [ + 95296, + 45008 + ], + "id": "5fc18bdf-24e6-4b8c-91d1-cf32197c4bc3", + "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": [ + 95536, + 45008 + ], + "id": "f9928fbd-ce44-4993-ae91-e17ac433e7ed", + "name": "Code - Validar conteo análisis Redis TEST" + }, + { + "parameters": { + "amount": 4 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 96192, + 45088 + ], + "id": "293d89f9-b0cb-4fac-930e-ef7d4f59b45e", + "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": [ + 96464, + 45120 + ], + "id": "e6f3d6a9-b7c4-4983-b1af-837ebc0e0793", + "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": [ + 96672, + 45120 + ], + "id": "8506e34e-dd72-47a0-b29c-bdaf24358201", + "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": [ + 96880, + 45120 + ], + "id": "59a9b0b4-c9a9-4f7f-8ab2-7b610f770531", + "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": [ + 97408, + 45200 + ], + "id": "b353cc36-a0a2-4f45-9860-43ca617e3eb5", + "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": [ + 97616, + 45200 + ], + "id": "255420b3-1a74-43c2-810a-829b49c02c1a", + "name": "IF - Reintentar cierre análisis Redis TEST" + }, + { + "parameters": { + "amount": 4 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 97984, + 45184 + ], + "id": "61b2acca-38f7-49ec-930f-a8c4a79681b4", + "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": [ + 97200, + 45200 + ], + "id": "b1555213-786d-4348-b4cc-2505a6be4512", + "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": [ + 105104, + 44832 + ], + "id": "6684f890-5efb-47a8-adb3-9d462c11a2d1", + "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": [ + 105312, + 44832 + ], + "id": "12a0813a-3e8b-42f8-a576-999a81f9b529", + "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": [ + 105520, + 44832 + ], + "id": "410ea695-fc28-46f3-b050-da814bfc7dd8", + "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": [ + 105808, + 44640 + ], + "id": "8128ae32-ad57-4bf8-b2bc-838b3ea7625c", + "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": [ + 106016, + 44640 + ], + "id": "40bddd46-84a2-4f9d-8e49-5fce710ed718", + "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": [ + 85056, + 41360 + ], + "id": "55f1bcf6-cf32-4c29-9476-bcc127edb65b", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## 🧾 CONTROL DE SESIONES PRIVADAS\n\nCada usuario tiene una sesión independiente vinculada a su identidad de WhatsApp (teléfono o BSUID).\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 identidad de WhatsApp.\n", + "height": 624, + "width": 1024, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 86176, + 41264 + ], + "id": "5f23b767-64ac-40c6-a10c-8cd5fa2840ff", + "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": [ + 89232, + 37776 + ], + "id": "d90d17c6-1b01-4f2b-9e03-fe61bbc0cd8f", + "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": [ + 86656, + 42000 + ], + "id": "facfe92a-f0a8-4696-af38-521c1557148b", + "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": [ + 89008, + 39872 + ], + "id": "a4397846-4283-4413-a69a-fa1aa88dbc64", + "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": [ + 89392, + 40896 + ], + "id": "21994911-bd6b-4ade-9a03-5fcc009a3296", + "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": [ + 89952, + 42048 + ], + "id": "363ed4b8-100e-4b32-9f99-4ab192024538", + "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": [ + 87936, + 42976 + ], + "id": "1acf1b1b-d9e7-4feb-bdf1-2b4dd533b84b", + "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": [ + 93104, + 44144 + ], + "id": "3779a3e9-669c-4435-b3cc-2320261bba10", + "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": [ + 99328, + 44192 + ], + "id": "57f88125-cd94-4d6a-ab8d-e97575924999", + "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": 448, + "width": 944, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 90832, + 41520 + ], + "id": "1e2c35bd-7c43-4fc4-b533-da0231609d7a", + "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": [ + 90576, + 43808 + ], + "id": "4b8da1f5-28c9-43b5-ba86-d4ad4dd9292f", + "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": [ + 90768, + 43808 + ], + "id": "54100d3e-5dd8-48f3-9c0b-bc4d9483a4ef", + "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": [ + 88112, + 41584 + ], + "id": "99483c85-8991-4273-a955-e0236f626608", + "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();\n\nconst norm = (value) =>\n clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst normalizeRecipient = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\n\nconst countryFromIso = (code) => {\n const c = norm(code);\n const map = {\n GT: 'Guatemala',\n SV: 'El Salvador',\n HN: 'Honduras',\n NI: 'Nicaragua',\n CR: 'Costa Rica',\n PA: 'Panamá',\n CO: 'Colombia',\n MX: 'México',\n VE: 'Venezuela',\n DO: 'República Dominicana',\n JM: 'Jamaica',\n TT: 'Trinidad y Tobago',\n PR: 'Puerto Rico'\n };\n return map[c] || '';\n};\n\nconst detectCountryFromIdentity = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n\n // BSUID: por ejemplo DO.xxxxx / JM.xxxxx / TT.xxxxx.\n if (/^[A-Za-z]{2}\\./.test(raw)) {\n const byBsuid = countryFromIso(raw.split('.')[0]);\n if (byBsuid) return byBsuid;\n }\n\n // Teléfono E.164 tradicional.\n if (/[A-Za-z]/.test(raw)) return '';\n const n = raw.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 '';\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 (!isValidCountry(country)) {\n country = detectCountryFromIdentity(\n row.sender_jid ||\n row.manager_telefono ||\n row.sender_phone ||\n row.telefono ||\n ''\n );\n }\n\n // El leaderboard muestra exclusivamente países identificados.\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);\n\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\nconst whatsappTo = normalizeRecipient(\n context.whatsapp_to ||\n context.whatsapp_user_id ||\n context.usuario_id_origen ||\n context.sender_jid ||\n context.sender_phone ||\n context.manager_telefono\n);\n\nif (!whatsappTo) {\n throw new Error('No se encontró un destino privado válido para enviar el leaderboard.');\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: whatsappTo,\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": [ + 88352, + 41584 + ], + "id": "70edf189-2d81-4351-af1d-bb2a6a7a2949", + "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": [ + 87904, + 41504 + ], + "id": "27b2eee9-da0f-4081-b823-4d3abe07de1b", + "name": "Sticky Note11" + } + ], + "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 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "staticData": null, + "meta": { + "templateCredsSetupCompleted": true + }, + "versionId": "45d7c314-9749-4cde-bb1d-37e817bfa8e2", + "activeVersionId": "45d7c314-9749-4cde-bb1d-37e817bfa8e2", + "versionCounter": 137, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-08-05T13:01:53.244Z", + "createdAt": "2026-08-05T13:01:53.244Z", + "role": "workflow:owner", + "workflowId": "b9s7cKNrgrmBEcHC", + "projectId": "PJpTANzTXIFibWsW", + "project": { + "updatedAt": "2026-04-22T14:25:09.686Z", + "createdAt": "2026-04-22T14:22:54.790Z", + "id": "PJpTANzTXIFibWsW", + "name": "Isaac Aracena ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-08-08T23:24:02.000Z", + "createdAt": "2026-08-08T23:23:57.415Z", + "versionId": "45d7c314-9749-4cde-bb1d-37e817bfa8e2", + "workflowId": "b9s7cKNrgrmBEcHC", + "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/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 normalizePhone = (value) => {\n const raw = cleanSingleLine(value).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const digits = raw.replace(/\\D/g, '');\n return digits.length >= 7 ? `+${digits}` : '';\n };\n\n const normalizeBsuid = (value) => {\n const raw = cleanSingleLine(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n const isBsuid =\n /^[A-Za-z]{2}\\.[A-Za-z0-9]+$/.test(raw) ||\n /^[A-Za-z]{2}\\.ENT\\.[A-Za-z0-9]+$/i.test(raw) ||\n /^user\\.[A-Za-z0-9]+$/i.test(raw);\n return isBsuid ? raw : '';\n };\n\n const rawEvent = $json.raw_event || {};\n const rawMessage = rawEvent.messages?.[0] || {};\n const rawContact = rawEvent.contacts?.[0] || {};\n\n const phone = [\n $json.whatsapp_phone,\n $json.sender_phone_real,\n rawMessage.from,\n rawContact.wa_id,\n $json.whatsapp_to,\n $json.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ]\n .map(normalizePhone)\n .find(Boolean) || '';\n\n const bsuid = [\n $json.whatsapp_user_id,\n $json.whatsapp_parent_user_id,\n rawMessage.from_user_id,\n rawMessage.from_parent_user_id,\n rawContact.user_id,\n rawContact.parent_user_id,\n $json.usuario_id_origen,\n $json.sender_jid,\n $json.whatsapp_to,\n $json.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ]\n .map(normalizeBsuid)\n .find(Boolean) || '';\n\n if (!phone && !bsuid) {\n throw new Error('No se encontró un teléfono ni BSUID válido para responder por 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 const recipientFields = {};\n if (phone) recipientFields.to = phone;\n if (bsuid) recipientFields.recipient = bsuid;\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 ...recipientFields,\n type: 'interactive',\n interactive\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n ...recipientFields,\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": [ + 109872, + 44400 + ], + "id": "ab62d4b4-b438-498d-8f21-76b565e6ab50", + "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] || {};\nconst contact = contacts[0] || {};\n\nconst clean = (value) => String(value ?? '').trim();\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 normalizePhone = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const onlyDigits = raw.replace(/\\D/g, '');\n return onlyDigits.length >= 7 ? onlyDigits : '';\n};\n\nconst normalizeBsuid = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n const isBsuid =\n /^[A-Za-z]{2}\\.[A-Za-z0-9]+$/.test(raw) ||\n /^[A-Za-z]{2}\\.ENT\\.[A-Za-z0-9]+$/i.test(raw) ||\n /^user\\.[A-Za-z0-9]+$/i.test(raw);\n return isBsuid ? raw : '';\n};\n\n// --------------------------------------------------\n// 2. Identidad del remitente: teléfono + BSUID\n// --------------------------------------------------\nconst profileName =\n clean(contact.profile?.name) ||\n clean(message.profile?.name) ||\n 'Usuario WhatsApp';\n\nconst username =\n clean(contact.profile?.username) ||\n clean(contact.username) ||\n clean(message.username) ||\n '';\n\nconst senderPhone =\n normalizePhone(message.from) ||\n normalizePhone(contact.wa_id) ||\n normalizePhone(entryValue.from) ||\n normalizePhone(input.from) ||\n '';\n\nconst senderUserId =\n normalizeBsuid(message.from_user_id) ||\n normalizeBsuid(message.user_id) ||\n normalizeBsuid(contact.user_id) ||\n normalizeBsuid(entryValue.from_user_id) ||\n normalizeBsuid(entryValue.user_id) ||\n normalizeBsuid(input.from_user_id) ||\n normalizeBsuid(input.user_id) ||\n normalizeBsuid(message.from) ||\n normalizeBsuid(entryValue.from) ||\n normalizeBsuid(input.from) ||\n '';\n\nconst senderParentUserId =\n normalizeBsuid(message.from_parent_user_id) ||\n normalizeBsuid(message.parent_user_id) ||\n normalizeBsuid(contact.parent_user_id) ||\n normalizeBsuid(entryValue.from_parent_user_id) ||\n normalizeBsuid(entryValue.parent_user_id) ||\n normalizeBsuid(input.from_parent_user_id) ||\n normalizeBsuid(input.parent_user_id) ||\n '';\n\nconst senderIdentity =\n senderUserId ||\n senderParentUserId ||\n senderPhone;\n\nconst privateDestination =\n senderPhone ||\n senderUserId ||\n senderParentUserId;\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 : privateDestination;\n\n// --------------------------------------------------\n// 3. 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// 4. 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// 5. 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 if (t === 'RANKING') return 'LEADERBOARD';\n\n return 'NORMAL';\n};\n\n// --------------------------------------------------\n// 6. País e idioma: teléfono, country_code o prefijo del BSUID\n// --------------------------------------------------\nconst countryByIso = {\n GT: { country_code: '502', pais_detectado: 'Guatemala', idioma_flujo: 'ES' },\n SV: { country_code: '503', pais_detectado: 'El Salvador', idioma_flujo: 'ES' },\n HN: { country_code: '504', pais_detectado: 'Honduras', idioma_flujo: 'ES' },\n NI: { country_code: '505', pais_detectado: 'Nicaragua', idioma_flujo: 'ES' },\n CR: { country_code: '506', pais_detectado: 'Costa Rica', idioma_flujo: 'ES' },\n PA: { country_code: '507', pais_detectado: 'Panamá', idioma_flujo: 'ES' },\n CO: { country_code: '57', pais_detectado: 'Colombia', idioma_flujo: 'ES' },\n MX: { country_code: '52', pais_detectado: 'México', idioma_flujo: 'ES' },\n VE: { country_code: '58', pais_detectado: 'Venezuela', idioma_flujo: 'ES' },\n DO: { country_code: '1', pais_detectado: 'República Dominicana', idioma_flujo: 'ES' },\n JM: { country_code: '1', pais_detectado: 'Jamaica', idioma_flujo: 'EN' },\n TT: { country_code: '1', pais_detectado: 'Trinidad y Tobago', idioma_flujo: 'EN' },\n PR: { country_code: '1', pais_detectado: 'Puerto Rico', idioma_flujo: 'ES' }\n};\n\nconst detectCountryFromPhone = (phone) => {\n const n = normalizePhone(phone);\n\n if (n.startsWith('502')) return countryByIso.GT;\n if (n.startsWith('503')) return countryByIso.SV;\n if (n.startsWith('504')) return countryByIso.HN;\n if (n.startsWith('505')) return countryByIso.NI;\n if (n.startsWith('506')) return countryByIso.CR;\n if (n.startsWith('507')) return countryByIso.PA;\n if (n.startsWith('57')) return countryByIso.CO;\n if (n.startsWith('52')) return countryByIso.MX;\n if (n.startsWith('58')) return countryByIso.VE;\n\n if (n.startsWith('1809') || n.startsWith('1829') || n.startsWith('1849')) return countryByIso.DO;\n if (n.startsWith('1876')) return countryByIso.JM;\n if (n.startsWith('1868')) return countryByIso.TT;\n if (n.startsWith('1787') || n.startsWith('1939')) return countryByIso.PR;\n\n return null;\n};\n\nconst rawCountryCode = normalizeText(\n contact.profile?.country_code ||\n contact.country_code ||\n message.country_code ||\n entryValue.country_code ||\n input.country_code ||\n ''\n);\n\nconst bsuidForCountry = senderUserId || senderParentUserId;\nconst bsuidCountryCode = bsuidForCountry.includes('.')\n ? normalizeText(bsuidForCountry.split('.')[0])\n : '';\n\nconst countryInfo =\n detectCountryFromPhone(senderPhone) ||\n countryByIso[rawCountryCode] ||\n countryByIso[bsuidCountryCode] ||\n { country_code: rawCountryCode || '', pais_detectado: 'No identificado', idioma_flujo: 'ES' };\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 || senderIdentity,\n manager_nombre: profileName,\n usuario_id_origen: senderIdentity,\n\n whatsapp_phone: senderPhone ? `+${senderPhone}` : '',\n whatsapp_user_id: senderUserId,\n whatsapp_parent_user_id: senderParentUserId,\n whatsapp_username: username,\n sender_phone_real: senderPhone,\n identidad_tipo:\n senderPhone && senderUserId\n ? 'PHONE_AND_BSUID'\n : senderPhone\n ? 'PHONE'\n : senderUserId || senderParentUserId\n ? 'BSUID'\n : 'UNKNOWN',\n\n is_group: isGroup,\n group_jid: groupId,\n group_id: groupId,\n group_name: clean(entryValue.group_name || input.group_name || ''),\n\n sender_jid: senderIdentity,\n sender_phone: senderPhone || senderIdentity,\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": [ + 85328, + 41680 + ], + "id": "37f48caa-4518-4edb-861b-6468d9c341ec", + "name": "Code - Normalizar evento WhatsApp TEST" + }, + { + "parameters": { + "updates": [ + "messages" + ], + "options": {} + }, + "type": "n8n-nodes-base.whatsAppTrigger", + "typeVersion": 1, + "position": [ + 85120, + 41680 + ], + "id": "d7bba797-6298-47b6-8e07-fa7e97758eaf", + "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": [ + 86512, + 41632 + ], + "id": "f28feebb-406d-41e0-b7de-525db4105327", + "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 normalizarIdentidad = (valor) => {\n const raw = texto(valor).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw.toUpperCase();\n return raw.replace(/\\D/g, '');\n};\n\nconst telefonoReal = (valor) => {\n const raw = texto(valor).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const n = raw.replace(/\\D/g, '');\n return n.length >= 7 ? n : '';\n};\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 currentKeys = [\n evento.whatsapp_user_id,\n evento.whatsapp_parent_user_id,\n evento.usuario_id_origen,\n evento.sender_jid,\n evento.sender_phone_real,\n evento.sender_phone,\n evento.manager_telefono,\n evento.whatsapp_to,\n evento.whatsapp_remote_jid\n]\n .map(normalizarIdentidad)\n .filter(Boolean);\n\nconst uniqueCurrentKeys = [...new Set(currentKeys)];\n\nconst primaryIdentity = normalizarIdentidad(\n evento.whatsapp_user_id ||\n evento.whatsapp_parent_user_id ||\n evento.usuario_id_origen ||\n evento.sender_jid ||\n evento.sender_phone_real ||\n evento.sender_phone ||\n evento.manager_telefono ||\n evento.whatsapp_to\n);\n\nconst senderPhoneReal =\n telefonoReal(evento.sender_phone_real) ||\n telefonoReal(evento.whatsapp_phone) ||\n telefonoReal(evento.sender_phone) ||\n telefonoReal(evento.manager_telefono) ||\n telefonoReal(evento.whatsapp_to);\n\nconst whatsappDestination = texto(\n senderPhoneReal ||\n evento.whatsapp_to ||\n evento.whatsapp_remote_jid ||\n evento.whatsapp_user_id ||\n evento.whatsapp_parent_user_id ||\n evento.usuario_id_origen ||\n evento.sender_jid\n);\n\nconst managerContacto = texto(\n senderPhoneReal ||\n evento.manager_telefono ||\n evento.sender_phone ||\n primaryIdentity\n);\n\nconst accion = texto(evento.accion_flujo || 'NORMAL');\n\nconst idiomaFlujo =\n normalizar(evento.idioma_flujo || 'ES') === 'EN'\n ? 'EN'\n : 'ES';\n\nif (!primaryIdentity || !whatsappDestination) {\n throw new Error('No llegó un identificador válido del usuario (teléfono o BSUID) para resolver la sesión privada.');\n}\n\n// Reutiliza sesiones privadas por cualquiera de las dos identidades:\n// BSUID (preferido) o teléfono histórico.\nconst sesionActiva = sesiones\n .filter((row) => {\n const rowKeys = [\n row.sender_jid,\n row.sender_phone,\n row.manager_telefono\n ]\n .map(normalizarIdentidad)\n .filter(Boolean);\n\n const rowGroup = texto(row.group_jid || row.group_id);\n const rowIsGroup = esVerdadero(row.is_group) || Boolean(rowGroup);\n\n const sameIdentity = rowKeys.some((key) => uniqueCurrentKeys.includes(key));\n\n return sameIdentity && !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 identitySuffix = primaryIdentity.replace(/[^A-Z0-9]/gi, '').slice(-4) || '0000';\nconst sessionId = `WA_${Date.now()}_${identitySuffix}_${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\n whatsapp_phone: senderPhoneReal ? `+${senderPhoneReal}` : '',\n whatsapp_user_id: texto(evento.whatsapp_user_id),\n whatsapp_parent_user_id: texto(evento.whatsapp_parent_user_id),\n\n whatsapp_to: whatsappDestination,\n whatsapp_remote_jid: whatsappDestination,\n whatsapp_recipient_type: 'individual',\n\n manager_telefono: managerContacto,\n sender_phone: senderPhoneReal || texto(evento.sender_phone) || primaryIdentity,\n sender_jid: texto(evento.whatsapp_user_id || evento.sender_jid || primaryIdentity),\n usuario_id_origen: texto(evento.whatsapp_user_id || evento.usuario_id_origen || primaryIdentity),\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: managerContacto,\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\n // sender_jid guarda el BSUID cuando existe; esto permite sesiones\n // estables incluso si el usuario oculta su número.\n sender_jid: texto(evento.whatsapp_user_id || evento.sender_jid || primaryIdentity),\n sender_phone: senderPhoneReal || texto(evento.sender_phone) || primaryIdentity,\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 identidad_primaria: primaryIdentity,\n identidades_candidatas: uniqueCurrentKeys,\n whatsapp_to: whatsappDestination,\n whatsapp_phone: senderPhoneReal ? `+${senderPhoneReal}` : '',\n sender_phone_real: senderPhoneReal,\n whatsapp_user_id: texto(evento.whatsapp_user_id),\n whatsapp_parent_user_id: texto(evento.whatsapp_parent_user_id),\n idioma_final_usado: idiomaFlujo,\n pais_detectado: texto(evento.pais_detectado),\n country_code: texto(evento.country_code),\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": [ + 86704, + 41632 + ], + "id": "879d137f-7cd2-43af-aede-b3b451ef4420", + "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": [ + 86928, + 41568 + ], + "id": "eb2e3996-7dd1-414b-9d2c-287d9019145d", + "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": [ + 89680, + 38096 + ], + "id": "e50206e3-fdee-4a65-a289-04685ecd7c0a", + "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": [ + 89712, + 38544 + ], + "id": "5085b9a5-8267-4a8d-8cf4-e124d04a9258", + "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": [ + 89472, + 38096 + ], + "id": "a6d457e7-c4fe-45a2-9a8f-688061aa60d7", + "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": [ + 87232, + 42128 + ], + "id": "dd8b0d2a-1672-4c87-a2c3-e404dd900cde", + "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": [ + 87408, + 42320 + ], + "id": "f609051a-a5c5-41a9-90bb-4913f6951155", + "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": [ + 89840, + 40336 + ], + "id": "7c772d23-313b-46d0-8fa0-e9340b4d86d9", + "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": [ + 89552, + 39952 + ], + "id": "53096943-afa2-4938-af55-d0a499a0b1d9", + "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": [ + 89760, + 39952 + ], + "id": "cf67f9b0-5456-48df-87c8-866eaa22f71b", + "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": [ + 89968, + 39952 + ], + "id": "a8603316-1728-49b1-a344-e62b4b724544", + "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": [ + 90192, + 39952 + ], + "id": "03a1f157-144b-4380-92a3-765e91b84c4f", + "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": [ + 89584, + 39360 + ], + "id": "f21d99bd-2fee-4136-87f9-c0f7f7b69fe0", + "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": [ + 89792, + 39360 + ], + "id": "5ec33212-e750-4c78-b0d0-9e4e9290a29a", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\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 whatsappTo = normalizarDestinoPrivado(\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 destino 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": [ + 90048, + 39296 + ], + "id": "295224e8-9051-4eba-b4ef-08fa4e25dc51", + "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 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\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\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 if (/[A-Za-z]/.test(raw)) return raw;\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": [ + 90560, + 42384 + ], + "id": "8ac878b1-bc57-4f9d-a05d-2fd9214fac36", + "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 normalizarIdentidad = (valor) => {\n const raw = texto(valor).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw.toUpperCase();\n return raw.replace(/\\D/g, '');\n};\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 = normalizarIdentidad(\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 = normalizarIdentidad(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": [ + 91168, + 41728 + ], + "id": "41fc12ef-11be-4490-aef5-d609572fb466", + "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": [ + 91360, + 41728 + ], + "id": "29c338cd-e5ee-457c-a752-ea8132acc1a8", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\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 : normalizarDestinoPrivado(destinoBase);\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": [ + 91584, + 41728 + ], + "id": "58c58dde-d095-483e-a15a-54c4ea8f3815", + "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 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\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\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 if (/[A-Za-z]/.test(raw)) return raw;\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": [ + 88688, + 44208 + ], + "id": "6eda0309-9e97-4a21-aa8b-ed78acd0f20e", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 : normalizarDestinoPrivado(telefonoBase);\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": [ + 88336, + 43600 + ], + "id": "bcb9b541-e658-44e3-9f4f-f2d96fa56a73", + "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": [ + 88544, + 43600 + ], + "id": "458feb7d-73f1-46d4-bebc-c469d90d756b", + "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": [ + 90688, + 42624 + ], + "id": "d11b97c0-12ba-4e3b-b4d5-9afdbe7575c1", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 : normalizarDestinoPrivado(telefonoBase);\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": [ + 90912, + 42560 + ], + "id": "3881a504-161e-4d18-8733-1bc0b4f25a1b", + "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": [ + 91120, + 42560 + ], + "id": "2582861e-e0be-4ed8-94f5-5c0fffacf014", + "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 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\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\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 if (/[A-Za-z]/.test(raw)) return raw;\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": [ + 91328, + 42560 + ], + "id": "40012d12-3692-47be-a99e-5307d1c0f351", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 : normalizarDestinoPrivado(telefonoBase);\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": [ + 88256, + 43888 + ], + "id": "f3b56567-2cdf-4ba1-840d-675529ea3af3", + "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": [ + 88464, + 43888 + ], + "id": "3757fce4-499a-4426-a088-65ee039e62a8", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 : normalizarDestinoPrivado(telefonoBase);\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": [ + 88752, + 44448 + ], + "id": "be7a39cc-8a32-4127-8968-8b24788b58fd", + "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": [ + 89520, + 43808 + ], + "id": "2ea4ddbf-9081-4247-8bd6-fd82c3367f76", + "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": [ + 89776, + 43808 + ], + "id": "74d214ac-eff5-413d-8002-a38efbab55ac", + "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": [ + 90016, + 43808 + ], + "id": "a7133cc5-00dc-4238-a81d-e359f6383edf", + "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": [ + 90256, + 43808 + ], + "id": "43df5376-faf9-468a-bde7-85394abcd2e9", + "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": [ + 90464, + 43808 + ], + "id": "7056eaf3-79d9-4458-9b3c-a68ab878be29", + "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": [ + 90976, + 43808 + ], + "id": "40609efe-d3e7-4434-a4d9-743ae29468cc", + "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": [ + 90912, + 43808 + ], + "id": "474ec6ba-41d9-4e6c-bb18-9200b5bcdd83", + "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": [ + 91120, + 43808 + ], + "id": "c297b709-82f9-4656-afc6-f76c50b76a76", + "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": [ + 91536, + 43808 + ], + "id": "90fda4f9-fd16-408b-bfda-38cd3447423b", + "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": [ + 91328, + 43808 + ], + "id": "fcdeeca9-b585-4426-a7ce-111dc82e8d27", + "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": [ + 91744, + 43808 + ], + "id": "4ee87837-de9d-46f3-a34c-08745d7f51c2", + "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": [ + 91952, + 43808 + ], + "id": "c6e698dd-4f4a-457a-8222-7cb307a35a46", + "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": [ + 93200, + 44928 + ], + "id": "f5b00af6-f2ff-4ff6-a8d4-0f71cc95e4bb", + "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": [ + 93680, + 44752 + ], + "id": "6a482b01-d853-494f-a1ac-dd4e7655aea1", + "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": [ + 93888, + 44752 + ], + "id": "2476d566-d204-45ec-8dd8-516d9facd93a", + "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": [ + 94096, + 44752 + ], + "id": "c95190d8-e607-45b5-b6c1-b0045ea5d333", + "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": [ + 93696, + 44976 + ], + "id": "8c06f2a2-7357-49f4-b474-d901bf0f7622", + "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": [ + 93904, + 44976 + ], + "id": "18e8af54-48a9-4717-aa82-7b4943bc8593", + "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": [ + 94112, + 44976 + ], + "id": "6b8c4d6b-ec9b-4548-87aa-fe8875f7a859", + "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": [ + 93568, + 45216 + ], + "id": "dca2f800-90b1-470e-a1cc-e80b70eeabfc", + "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": [ + 93776, + 45216 + ], + "id": "962e3c0a-d7e3-4f86-8c59-19c458969e45", + "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": [ + 93984, + 45216 + ], + "id": "3104a7ce-8e65-4a14-b223-dd26ad0f5911", + "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": [ + 94768, + 45008 + ], + "id": "e4f045a7-acd5-425b-84f5-dd6ff54834e4", + "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": [ + 98560, + 44816 + ], + "id": "ee179ac3-10ed-447f-969e-7bddd3832954", + "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": [ + 99456, + 44848 + ], + "id": "315f4c21-3e3d-4dea-ab56-ff0e965ca5fe", + "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": [ + 99648, + 44848 + ], + "id": "fbc5bf77-0db6-4e36-8481-ffdc75d930cc", + "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": [ + 100000, + 44848 + ], + "id": "48ea284e-29f4-4b94-9761-1f95899e1822", + "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": [ + 101744, + 44928 + ], + "id": "200760fa-96eb-4715-b1d3-d9fbab011cf9", + "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": [ + 109072, + 44816 + ], + "id": "50f1b769-a4f5-4958-9f27-856d64e80bd0", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\n\nconst whatsappTo = normalizarDestinoPrivado(\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 destino 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": [ + 109360, + 44784 + ], + "id": "4a8a167e-e72f-4d96-b43a-c8b4920709c5", + "name": "Code - Preparar mensaje final WhatsApp TEST" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 98368, + 44816 + ], + "id": "7bae126c-564e-4d97-bde1-43c75aca0fce", + "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": [ + 100960, + 44880 + ], + "id": "ac6d7e32-7515-4409-84af-8e38b9a02f10", + "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": [ + 101168, + 44880 + ], + "id": "2e22f8b2-b103-4609-a982-a7fea4ac576f", + "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": [ + 101376, + 44880 + ], + "id": "638639c2-0d8b-4840-ac2a-f451875419ea", + "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": [ + 101952, + 44896 + ], + "id": "3f6f9b38-a637-4518-9355-2cd406094690", + "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": [ + 103504, + 44896 + ], + "id": "3553b5b0-3c5b-4a96-a689-ac5703945595", + "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": [ + 103776, + 44896 + ], + "id": "ac52fc21-68f3-42c4-86dd-bbdce3e2b62e", + "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": [ + 106992, + 44752 + ], + "id": "53a5ffbc-e656-4242-96a4-f3365fc5e739", + "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": [ + 103984, + 44896 + ], + "id": "33ccdc99-104a-405b-828a-7ceb3df41cc4", + "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": [ + 104256, + 44832 + ], + "id": "73a65962-2de1-480a-be25-449ac76b193b", + "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": [ + 104864, + 44832 + ], + "id": "9562e363-a097-4f27-adc9-7b5c71e26ade", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\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 = normalizarDestinoPrivado(\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 destino 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": [ + 89792, + 38912 + ], + "id": "32630225-3feb-4909-a913-b9a93d585ea3", + "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": [ + 90896, + 41728 + ], + "id": "364723b0-a653-4e75-a081-f1eeedba1f36", + "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": [ + 102160, + 44896 + ], + "id": "0b1bc36f-d6d4-40bc-85d7-cdc3dff701c2", + "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": [ + 102368, + 44896 + ], + "id": "9dc198ee-0812-4d36-896f-ae3b30cfd825", + "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": [ + 102576, + 44896 + ], + "id": "8fe63f67-6fba-43aa-afaf-1633175c0c5e", + "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": [ + 102784, + 44896 + ], + "id": "5f954231-348e-4add-bd6e-8df85abc8318", + "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": [ + 102992, + 44896 + ], + "id": "385d2bc1-6984-4705-b78b-01ab594e517e", + "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": [ + 103200, + 44896 + ], + "id": "476a36fb-5972-4682-a78c-b43a9fd4bfe9", + "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": [ + 104464, + 44832 + ], + "id": "caaf2436-7d47-41d2-ba7c-1d81c400fbe4", + "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": [ + 104672, + 44832 + ], + "id": "7db90e15-30b8-4e1d-9065-761919c64c3b", + "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 normalizarDestinoPrivado = (valor) => {\n const raw = String(valor ?? '').trim().replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\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 whatsappTo = normalizarDestinoPrivado(\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 destino 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": [ + 88800, + 43776 + ], + "id": "ab7c8c19-a16e-4b2e-ba87-ca210678fb48", + "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": [ + 89248, + 43776 + ], + "id": "85e64dea-a170-4494-a330-a5ff1db6506a", + "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/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 normalizePhone = (value) => {\n const raw = cleanSingleLine(value).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const digits = raw.replace(/\\D/g, '');\n return digits.length >= 7 ? `+${digits}` : '';\n };\n\n const normalizeBsuid = (value) => {\n const raw = cleanSingleLine(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n const isBsuid =\n /^[A-Za-z]{2}\\.[A-Za-z0-9]+$/.test(raw) ||\n /^[A-Za-z]{2}\\.ENT\\.[A-Za-z0-9]+$/i.test(raw) ||\n /^user\\.[A-Za-z0-9]+$/i.test(raw);\n return isBsuid ? raw : '';\n };\n\n const rawEvent = $json.raw_event || {};\n const rawMessage = rawEvent.messages?.[0] || {};\n const rawContact = rawEvent.contacts?.[0] || {};\n\n const phone = [\n $json.whatsapp_phone,\n $json.sender_phone_real,\n rawMessage.from,\n rawContact.wa_id,\n $json.whatsapp_to,\n $json.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ]\n .map(normalizePhone)\n .find(Boolean) || '';\n\n const bsuid = [\n $json.whatsapp_user_id,\n $json.whatsapp_parent_user_id,\n rawMessage.from_user_id,\n rawMessage.from_parent_user_id,\n rawContact.user_id,\n rawContact.parent_user_id,\n $json.usuario_id_origen,\n $json.sender_jid,\n $json.whatsapp_to,\n $json.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ]\n .map(normalizeBsuid)\n .find(Boolean) || '';\n\n if (!phone && !bsuid) {\n throw new Error('No se encontró un teléfono ni BSUID válido para responder por 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 const recipientFields = {};\n if (phone) recipientFields.to = phone;\n if (bsuid) recipientFields.recipient = bsuid;\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 ...recipientFields,\n type: 'interactive',\n interactive\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n ...recipientFields,\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": [ + 89024, + 43776 + ], + "id": "8dfb73e8-43df-42b9-88f0-f09852ffdff9", + "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": [ + 100208, + 44848 + ], + "id": "0959b924-ee0c-4129-8eb5-07fe31b43613", + "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": [ + 100416, + 44848 + ], + "id": "0e066ac2-e1cc-43e3-871a-c4a509987b9b", + "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": [ + 107312, + 44656 + ], + "id": "e2f5a6d0-8ffb-4109-9544-e0b824f0c055", + "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": [ + 108080, + 44704 + ], + "id": "b4cab921-67f7-492e-8781-b9415517f85b", + "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": [ + 108432, + 44576 + ], + "id": "8182db08-1e0b-41fd-8031-92dedaa288cb", + "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": [ + 108720, + 44576 + ], + "id": "5048f6f2-cba3-443d-8522-5b4412365484", + "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": [ + 100624, + 44864 + ], + "id": "6aea7ea7-d889-4a87-8751-630a97485b7a", + "name": "Code - Enriquecer datos con match banco TEST" + }, + { + "parameters": { + "jsCode": "const items = $input.all();\n\nconst clean = (value) => String(value ?? '').trim();\n\nconst normalizePhone = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw || /[A-Za-z]/.test(raw)) return '';\n const digits = raw.replace(/\\D/g, '');\n return digits.length >= 7 ? digits : '';\n};\n\nconst normalizeBsuid = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n const isBsuid =\n /^[A-Za-z]{2}\\.[A-Za-z0-9]+$/.test(raw) ||\n /^[A-Za-z]{2}\\.ENT\\.[A-Za-z0-9]+$/i.test(raw) ||\n /^user\\.[A-Za-z0-9]+$/i.test(raw);\n return isBsuid ? raw : '';\n};\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\n const rawMessage =\n evento.raw_event?.messages?.[0] ||\n evento.raw_event ||\n {};\n\n const rawContact =\n evento.raw_event?.contacts?.[0] ||\n {};\n\n const senderPhone =\n normalizePhone(evento.sender_phone_real) ||\n normalizePhone(evento.whatsapp_phone) ||\n normalizePhone(rawMessage.from) ||\n normalizePhone(rawContact.wa_id) ||\n normalizePhone(evento.sender_phone) ||\n normalizePhone(evento.manager_telefono) ||\n normalizePhone(evento.whatsapp_to) ||\n '';\n\n const senderUserId =\n normalizeBsuid(evento.whatsapp_user_id) ||\n normalizeBsuid(rawMessage.from_user_id) ||\n normalizeBsuid(rawMessage.user_id) ||\n normalizeBsuid(rawContact.user_id) ||\n normalizeBsuid(evento.usuario_id_origen) ||\n normalizeBsuid(evento.sender_jid) ||\n normalizeBsuid(evento.whatsapp_to) ||\n '';\n\n const senderParentUserId =\n normalizeBsuid(evento.whatsapp_parent_user_id) ||\n normalizeBsuid(rawMessage.from_parent_user_id) ||\n normalizeBsuid(rawMessage.parent_user_id) ||\n normalizeBsuid(rawContact.parent_user_id) ||\n '';\n\n const senderIdentity =\n senderUserId ||\n senderParentUserId ||\n senderPhone;\n\n const privateDestination =\n senderPhone ||\n senderUserId ||\n senderParentUserId;\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 (!senderIdentity) reasons.push('remitente sin identificador 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 is_group: false,\n group_id: '',\n group_jid: '',\n group_name: '',\n\n whatsapp_phone: senderPhone ? `+${senderPhone}` : '',\n whatsapp_user_id: senderUserId,\n whatsapp_parent_user_id: senderParentUserId,\n\n whatsapp_to: privateDestination,\n whatsapp_remote_jid: privateDestination,\n whatsapp_recipient_type: 'individual',\n\n manager_telefono:\n senderPhone ||\n clean(evento.manager_telefono) ||\n senderIdentity,\n\n sender_phone_real: senderPhone,\n\n sender_phone:\n senderPhone ||\n clean(evento.sender_phone) ||\n senderIdentity,\n\n sender_jid:\n senderUserId ||\n senderParentUserId ||\n clean(evento.sender_jid) ||\n senderIdentity,\n\n usuario_id_origen:\n senderUserId ||\n senderParentUserId ||\n clean(evento.usuario_id_origen) ||\n senderIdentity,\n\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_identity: senderIdentity,\n whatsapp_phone: senderPhone ? `+${senderPhone}` : '',\n whatsapp_user_id: senderUserId,\n whatsapp_parent_user_id: senderParentUserId,\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": [ + 85744, + 41680 + ], + "id": "2f466b2b-533a-4067-a9fe-f83abb92d437", + "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": [ + 86256, + 41648 + ], + "id": "a9ea43dd-28a3-4a11-a2b7-a5c5981755a2", + "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": [ + 107568, + 44736 + ], + "id": "b928462c-5b55-46b6-9c5e-c4171536590d", + "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": [ + 107776, + 44736 + ], + "id": "84ee49ac-b282-4a26-b105-352fda82b68b", + "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": [ + 90048, + 41056 + ], + "id": "8264eb15-3b1d-4260-9289-81bb74607276", + "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": [ + 90256, + 41056 + ], + "id": "d6f735d7-f3f0-41d7-8981-a42017e3b509", + "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": [ + 90464, + 41056 + ], + "id": "a87aa2a7-9e3e-4b54-bf21-cccf1ef47164", + "name": "Code - Restaurar contexto buffer imagen TEST" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 90672, + 41056 + ], + "id": "6d44254c-0892-4f02-8c5f-31a25830f8cb", + "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": [ + 90880, + 41056 + ], + "id": "24bc0ad4-854a-43eb-a93e-69b270db18e2", + "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": [ + 91088, + 41056 + ], + "id": "d7cd56cc-deec-4eec-bce3-c2398b613caf", + "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": [ + 91312, + 41056 + ], + "id": "13e416fe-d784-4b6a-ae10-1b7919389d5e", + "name": "IF - Procesar lote imagen Redis TEST" + }, + { + "parameters": { + "operation": "delete", + "key": "={{ $json.redis_buffer_key }}" + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 91584, + 41040 + ], + "id": "3a85c9b8-4702-4655-963b-164db5c41067", + "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": [ + 91792, + 41040 + ], + "id": "84886e9d-12ad-4c3b-b943-325d353c58f6", + "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": [ + 92032, + 41040 + ], + "id": "fa33b076-6923-4117-9128-740751ee2ac5", + "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": [ + 92240, + 41040 + ], + "id": "5622ed59-220e-4873-adba-90743918b36b", + "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": [ + 92496, + 41040 + ], + "id": "2a781320-45d6-4c70-a31e-8cef201d197a", + "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": [ + 92832, + 41056 + ], + "id": "9345d57a-5d40-4c20-8129-36d3571375b0", + "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": [ + 95904, + 44912 + ], + "id": "c0100e53-2ef6-40d8-b8c4-9a9b2bc791d9", + "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": [ + 97504, + 44816 + ], + "id": "8f02db71-9ad8-448d-9bae-eea72d39b4de", + "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": [ + 97712, + 44816 + ], + "id": "22020a22-4b21-40ab-a6d9-87818b1cca21", + "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": [ + 97920, + 44816 + ], + "id": "943f1a58-eed2-4417-ac6d-770a49933975", + "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": [ + 95040, + 45008 + ], + "id": "0f839019-6d9e-4f25-afa9-82ac0ae94106", + "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": [ + 95296, + 45008 + ], + "id": "5fc18bdf-24e6-4b8c-91d1-cf32197c4bc3", + "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": [ + 95536, + 45008 + ], + "id": "f9928fbd-ce44-4993-ae91-e17ac433e7ed", + "name": "Code - Validar conteo análisis Redis TEST" + }, + { + "parameters": { + "amount": 4 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 96192, + 45088 + ], + "id": "293d89f9-b0cb-4fac-930e-ef7d4f59b45e", + "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": [ + 96464, + 45120 + ], + "id": "e6f3d6a9-b7c4-4983-b1af-837ebc0e0793", + "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": [ + 96672, + 45120 + ], + "id": "8506e34e-dd72-47a0-b29c-bdaf24358201", + "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": [ + 96880, + 45120 + ], + "id": "59a9b0b4-c9a9-4f7f-8ab2-7b610f770531", + "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": [ + 97408, + 45200 + ], + "id": "b353cc36-a0a2-4f45-9860-43ca617e3eb5", + "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": [ + 97616, + 45200 + ], + "id": "255420b3-1a74-43c2-810a-829b49c02c1a", + "name": "IF - Reintentar cierre análisis Redis TEST" + }, + { + "parameters": { + "amount": 4 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 97984, + 45184 + ], + "id": "61b2acca-38f7-49ec-930f-a8c4a79681b4", + "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": [ + 97200, + 45200 + ], + "id": "b1555213-786d-4348-b4cc-2505a6be4512", + "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": [ + 105104, + 44832 + ], + "id": "6684f890-5efb-47a8-adb3-9d462c11a2d1", + "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": [ + 105312, + 44832 + ], + "id": "12a0813a-3e8b-42f8-a576-999a81f9b529", + "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": [ + 105520, + 44832 + ], + "id": "410ea695-fc28-46f3-b050-da814bfc7dd8", + "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": [ + 105808, + 44640 + ], + "id": "8128ae32-ad57-4bf8-b2bc-838b3ea7625c", + "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": [ + 106016, + 44640 + ], + "id": "40bddd46-84a2-4f9d-8e49-5fce710ed718", + "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": [ + 85056, + 41360 + ], + "id": "55f1bcf6-cf32-4c29-9476-bcc127edb65b", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## 🧾 CONTROL DE SESIONES PRIVADAS\n\nCada usuario tiene una sesión independiente vinculada a su identidad de WhatsApp (teléfono o BSUID).\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 identidad de WhatsApp.\n", + "height": 624, + "width": 1024, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 86176, + 41264 + ], + "id": "5f23b767-64ac-40c6-a10c-8cd5fa2840ff", + "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": [ + 89232, + 37776 + ], + "id": "d90d17c6-1b01-4f2b-9e03-fe61bbc0cd8f", + "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": [ + 86656, + 42000 + ], + "id": "facfe92a-f0a8-4696-af38-521c1557148b", + "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": [ + 89008, + 39872 + ], + "id": "a4397846-4283-4413-a69a-fa1aa88dbc64", + "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": [ + 89392, + 40896 + ], + "id": "21994911-bd6b-4ade-9a03-5fcc009a3296", + "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": [ + 89952, + 42048 + ], + "id": "363ed4b8-100e-4b32-9f99-4ab192024538", + "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": [ + 87936, + 42976 + ], + "id": "1acf1b1b-d9e7-4feb-bdf1-2b4dd533b84b", + "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": [ + 93104, + 44144 + ], + "id": "3779a3e9-669c-4435-b3cc-2320261bba10", + "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": [ + 99328, + 44192 + ], + "id": "57f88125-cd94-4d6a-ab8d-e97575924999", + "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": 448, + "width": 944, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 90832, + 41520 + ], + "id": "1e2c35bd-7c43-4fc4-b533-da0231609d7a", + "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": [ + 90576, + 43808 + ], + "id": "4b8da1f5-28c9-43b5-ba86-d4ad4dd9292f", + "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": [ + 90768, + 43808 + ], + "id": "54100d3e-5dd8-48f3-9c0b-bc4d9483a4ef", + "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": [ + 88112, + 41584 + ], + "id": "99483c85-8991-4273-a955-e0236f626608", + "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();\n\nconst norm = (value) =>\n clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst normalizeRecipient = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n if (!raw) return '';\n if (/[A-Za-z]/.test(raw)) return raw;\n return raw.replace(/\\D/g, '');\n};\n\nconst countryFromIso = (code) => {\n const c = norm(code);\n const map = {\n GT: 'Guatemala',\n SV: 'El Salvador',\n HN: 'Honduras',\n NI: 'Nicaragua',\n CR: 'Costa Rica',\n PA: 'Panamá',\n CO: 'Colombia',\n MX: 'México',\n VE: 'Venezuela',\n DO: 'República Dominicana',\n JM: 'Jamaica',\n TT: 'Trinidad y Tobago',\n PR: 'Puerto Rico'\n };\n return map[c] || '';\n};\n\nconst detectCountryFromIdentity = (value) => {\n const raw = clean(value).replace(/^whatsapp:/i, '');\n\n // BSUID: por ejemplo DO.xxxxx / JM.xxxxx / TT.xxxxx.\n if (/^[A-Za-z]{2}\\./.test(raw)) {\n const byBsuid = countryFromIso(raw.split('.')[0]);\n if (byBsuid) return byBsuid;\n }\n\n // Teléfono E.164 tradicional.\n if (/[A-Za-z]/.test(raw)) return '';\n const n = raw.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 '';\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 (!isValidCountry(country)) {\n country = detectCountryFromIdentity(\n row.sender_jid ||\n row.manager_telefono ||\n row.sender_phone ||\n row.telefono ||\n ''\n );\n }\n\n // El leaderboard muestra exclusivamente países identificados.\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);\n\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\nconst whatsappTo = normalizeRecipient(\n context.whatsapp_to ||\n context.whatsapp_user_id ||\n context.usuario_id_origen ||\n context.sender_jid ||\n context.sender_phone ||\n context.manager_telefono\n);\n\nif (!whatsappTo) {\n throw new Error('No se encontró un destino privado válido para enviar el leaderboard.');\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: whatsappTo,\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": [ + 88352, + 41584 + ], + "id": "70edf189-2d81-4351-af1d-bb2a6a7a2949", + "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": [ + 87904, + 41504 + ], + "id": "27b2eee9-da0f-4081-b823-4d3abe07de1b", + "name": "Sticky Note11" + } + ], + "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 + } + ] + ] + } + }, + "authors": "Isaac Aracena", + "name": "Version 45d7c314", + "description": "", + "autosaved": true, + "workflowPublishHistory": [ + { + "createdAt": "2026-08-08T23:24:02.377Z", + "id": 4357, + "workflowId": "b9s7cKNrgrmBEcHC", + "versionId": "45d7c314-9749-4cde-bb1d-37e817bfa8e2", + "event": "activated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + ] + } +} \ No newline at end of file