Files
chat-de-whatsapp-de-propues…/chat-de-whatsapp-de-propuestas-ejecutadas-api-oficial.json

20489 lines
1.0 MiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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 <iaracena@gomezleemarketing.com>",
"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"
}
]
}
}