diff --git a/chat-de-whatsapp-propuestas-ejecutadas-version-final.json b/chat-de-whatsapp-propuestas-ejecutadas-version-final.json new file mode 100644 index 0000000..9c1ce5a --- /dev/null +++ b/chat-de-whatsapp-propuestas-ejecutadas-version-final.json @@ -0,0 +1,20206 @@ +{ + "updatedAt": "2026-06-25T19:30:58.479Z", + "createdAt": "2026-06-24T14:10:15.435Z", + "id": "CdosDIWBgnDMnPdr", + "name": "Chat de WhatsApp - Propuestas Ejecutadas - Versión Final", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "method": "POST", + "url": "={{'https://wsp.gomezleemarketing.com'}}/message/sendText/{{'botsoporte'}}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "={{'C267126ABB45-4C12-B626-6BAB1833F5D7'}}" + } + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "number", + "value": "={{ (() => {\n const limpiar = (valor) => String(valor ?? '').trim();\n\n const candidatos = [\n $json.whatsapp_to,\n $json.group_jid,\n $json.whatsapp_remote_jid,\n $json.sesion_activa?.group_jid,\n $json.sesion_activa?.whatsapp_to,\n $json.sesion_activa?.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ];\n\n let raw = limpiar(candidatos.find(v => limpiar(v)));\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n // Si ya viene como grupo, conservarlo.\n if (raw.includes('@g.us')) {\n return raw;\n }\n\n // Si viene como usuario WhatsApp, validar si realmente era un grupo.\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n // Los grupos de WhatsApp suelen venir como 120363...\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n})() }}" + }, + { + "name": "delay", + "value": "={{ 1000 }}" + }, + { + "name": "text", + "value": "={{ $json.whatsapp_text || $json.telegram_text || $json.texto_respuesta || $json.mensaje || $json.text || 'Mensaje sin contenido' }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 15680, + 1632 + ], + "id": "dad797ac-f70a-4550-a1bb-297d2f296dc4", + "name": "WhatsApp - Enviar mensaje TEST" + }, + { + "parameters": { + "jsCode": "const input = $json || {};\nconst body = input.body || input;\nconst data = body.data || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst limpiarNumero = (valor) =>\n texto(valor)\n .replace('@s.whatsapp.net', '')\n .replace('@c.us', '')\n .replace('@g.us', '')\n .replace('@lid', '')\n .replace(/\\D/g, '');\n\nconst key = data.key || {};\nconst message = data.message || {};\n\n// --------------------------------------------------\n// 1. Identificar origen real del mensaje\n// --------------------------------------------------\n\nconst remoteJid = texto(\n key.remoteJid ||\n data.remoteJid ||\n body.remoteJid ||\n ''\n);\n\nconst isGroup = remoteJid.endsWith('@g.us');\n\nconst participantRaw = texto(\n key.participant ||\n data.participant ||\n body.participant ||\n data.sender ||\n data.participantJid ||\n ''\n);\n\nconst participantAlt = texto(\n key.participantAlt ||\n data.participantAlt ||\n body.participantAlt ||\n data.senderAlt ||\n ''\n);\n\n// En grupos, Evolution puede mandar:\n// participant = 1655...@lid\n// participantAlt = número real@s.whatsapp.net\n// Para sesiones y permisos necesitamos preferir el número real.\nconst participantJid = participantAlt || participantRaw;\n\n// En grupo, el sender real es participantAlt si existe.\n// En chat individual, el sender real es remoteJid.\nconst senderJid = isGroup\n ? participantJid\n : remoteJid;\n\nconst senderPhone = limpiarNumero(senderJid);\nconst groupJid = isGroup ? remoteJid : '';\n\nconst pushName =\n texto(data.pushName) ||\n texto(body.pushName) ||\n texto(data.senderName) ||\n texto(body.senderName) ||\n 'Usuario WhatsApp';\n\n// El bot debe responder al grupo si el mensaje vino de grupo.\n// Si vino de chat individual, responde al usuario.\nconst whatsappTo = isGroup ? groupJid : remoteJid;\n\n// --------------------------------------------------\n// 2. Extraer texto o payload de botones\n// --------------------------------------------------\n\nconst buttonText =\n texto(message.buttonsResponseMessage?.selectedButtonId) ||\n texto(message.buttonsResponseMessage?.selectedDisplayText) ||\n texto(message.listResponseMessage?.singleSelectReply?.selectedRowId) ||\n texto(message.listResponseMessage?.title) ||\n texto(message.templateButtonReplyMessage?.selectedId) ||\n texto(message.templateButtonReplyMessage?.selectedDisplayText) ||\n '';\n\nconst textoMensaje =\n buttonText ||\n texto(message.conversation) ||\n texto(message.extendedTextMessage?.text) ||\n texto(data.messageText) ||\n texto(data.text) ||\n texto(body.text) ||\n '';\n\n// --------------------------------------------------\n// 3. Detectar media\n// --------------------------------------------------\n\nconst audioMsg = message.audioMessage;\nconst imageMsg = message.imageMessage;\nconst videoMsg = message.videoMessage;\nconst documentMsg = message.documentMessage;\n\nconst documentMime = texto(documentMsg?.mimetype);\nconst documentFileName = texto(documentMsg?.fileName);\n\nlet messageType = 'unknown';\n\nif (audioMsg) {\n messageType = 'audio';\n} else if (imageMsg) {\n messageType = 'image';\n} else if (videoMsg) {\n messageType = 'video';\n} else if (documentMsg && documentMime.startsWith('video/')) {\n messageType = 'video';\n} else if (documentMsg && documentMime.startsWith('image/')) {\n messageType = 'image';\n} else if (documentMsg && documentMime.startsWith('audio/')) {\n messageType = 'audio';\n} else if (documentMsg) {\n messageType = 'document';\n} else if (textoMensaje) {\n messageType = 'text';\n}\n\nconst tieneMedia = ['audio', 'image', 'video', 'document'].includes(messageType);\n\nconst mediaMimeType =\n texto(audioMsg?.mimetype) ||\n texto(imageMsg?.mimetype) ||\n texto(videoMsg?.mimetype) ||\n documentMime ||\n '';\n\nconst mediaSourceId =\n texto(key.id) ||\n texto(data.id) ||\n texto(data.messageId) ||\n '';\n\n// --------------------------------------------------\n// 4. Normalizar comandos internos\n// --------------------------------------------------\n\nconst normalizarTexto = (valor) =>\n texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst normalizarComando = (valor) => {\n const t = normalizarTexto(valor);\n\n // Inicio oficial\n if (t === 'HEY') {\n return 'START';\n }\n\n // Cancelación oficial ES/EN\n if (\n t === 'CANCELAR' ||\n t === 'CANCEL'\n ) {\n return 'CANCELAR';\n }\n\n // Cierre de imágenes oficial ES/EN\n if (\n t === 'FOTOS LISTAS' ||\n t === 'FOTOS_LISTAS' ||\n t === 'PHOTOS READY' ||\n t === 'PHOTOS_READY'\n ) {\n return 'FOTOS_LISTAS';\n }\n\n // Cierre sin video oficial ES/EN\n if (\n t === 'SIN VIDEO' ||\n t === 'SIN_VIDEO' ||\n t === 'NO VIDEO' ||\n t === 'NO_VIDEO'\n ) {\n return 'SIN_VIDEO';\n }\n\n // Cierre con videos oficial ES/EN\n if (\n t === 'LISTO' ||\n t === 'DONE'\n ) {\n return 'LISTO';\n }\n\n return 'NORMAL';\n};\n\n// --------------------------------------------------\n// 5. Detectar país/idioma base por teléfono\n// --------------------------------------------------\n\nconst detectarIdiomaPorTelefono = (phone) => {\n const n = limpiarNumero(phone);\n\n // Países GLM principalmente hispanohablantes\n if (n.startsWith('502')) return { country_code: '502', pais_detectado: 'Guatemala', idioma_flujo: 'ES' };\n if (n.startsWith('503')) return { country_code: '503', pais_detectado: 'El Salvador', idioma_flujo: 'ES' };\n if (n.startsWith('504')) return { country_code: '504', pais_detectado: 'Honduras', idioma_flujo: 'ES' };\n if (n.startsWith('505')) return { country_code: '505', pais_detectado: 'Nicaragua', idioma_flujo: 'ES' };\n if (n.startsWith('506')) return { country_code: '506', pais_detectado: 'Costa Rica', idioma_flujo: 'ES' };\n if (n.startsWith('507')) return { country_code: '507', pais_detectado: 'Panamá', idioma_flujo: 'ES' };\n\n // República Dominicana: +1 809 / 829 / 849\n if (n.startsWith('1809') || n.startsWith('1829') || n.startsWith('1849')) {\n return { country_code: '1', pais_detectado: 'República Dominicana', idioma_flujo: 'ES' };\n }\n\n // Default para +1 no dominicano: inglés hasta que hagamos tabla formal\n if (n.startsWith('1')) {\n return { country_code: '1', pais_detectado: 'País +1 no identificado', idioma_flujo: 'EN' };\n }\n\n return { country_code: '', pais_detectado: 'No identificado', idioma_flujo: 'ES' };\n};\n\nconst idiomaInfo = detectarIdiomaPorTelefono(senderPhone);\n\n// --------------------------------------------------\n// 6. Resultado normalizado\n// --------------------------------------------------\n\nreturn [\n {\n json: {\n event_id: texto(key.id || data.id || Date.now()),\n fecha_recepcion: new Date().toISOString(),\n\n canal_origen: 'WHATSAPP',\n\n // Compatibilidad con el flujo actual:\n // seguimos llenando manager_telefono / manager_nombre,\n // pero ahora apuntan al sender real, no al grupo.\n manager_telefono: senderPhone,\n manager_nombre: pushName,\n usuario_id_origen: senderPhone,\n\n // Campos nuevos para grupo\n is_group: isGroup,\n group_jid: groupJid,\n group_name: texto(data.groupName || body.groupName || ''),\n sender_jid: senderJid,\n sender_phone: senderPhone,\n sender_name: pushName,\n\n whatsapp_remote_jid: remoteJid,\n whatsapp_to: whatsappTo,\n\n from_me: Boolean(key.fromMe || data.fromMe || false),\n\n message_type: messageType,\n texto: textoMensaje,\n accion_flujo: normalizarComando(textoMensaje),\n\n tiene_media: tieneMedia,\n media_count: tieneMedia ? 1 : 0,\n media_source_id: mediaSourceId,\n media_mime_type: mediaMimeType,\n media_file_name: documentFileName,\n\n country_code: idiomaInfo.country_code,\n pais_detectado: idiomaInfo.pais_detectado,\n idioma_flujo: idiomaInfo.idioma_flujo,\n\n raw_preview: JSON.stringify(body).slice(0, 4000),\n raw_event: body,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -9296, + -1024 + ], + "id": "5758e021-8dc1-4541-8cb9-be855b01bdbc", + "name": "Code - Normalizar evento WhatsApp TEST" + }, + { + "parameters": { + "httpMethod": "POST", + "path": "factura", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -9504, + -1024 + ], + "id": "8985b821-561b-4927-9528-1080bb264d83", + "name": "Webhook - Evolution WhatsApp TEST", + "webhookId": "6d9cdd90-a105-4e00-b42a-dcd7c4e5a7e1" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -8112, + -1072 + ], + "id": "2553dc5c-8ca5-416c-b08a-8e44f6e549a6", + "name": "Sheets - Leer sesiones existentes", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const evento = $('Code - Normalizar evento WhatsApp TEST').first().json || {};\nconst sesiones = $input.all().map((item) => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) =>\n texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst estadosAbiertos = new Set([\n 'ACTIVA',\n 'PENDIENTE',\n 'EN_PROCESO',\n 'PROCESANDO',\n 'LISTO_PARA_RECUPERAR_MEDIA',\n 'LISTO_PARA_ANALIZAR',\n]);\n\nconst etapasAbiertas = new Set([\n 'ESPERANDO_AUDIO',\n 'ESPERANDO_IMAGENES',\n 'ESPERANDO_VIDEOS',\n 'PROCESANDO',\n]);\n\nconst managerTelefono = texto(evento.manager_telefono || evento.sender_phone);\nconst senderPhone = texto(evento.sender_phone || managerTelefono);\nconst groupJid = texto(evento.group_jid);\nconst isGroup = Boolean(evento.is_group);\nconst accion = texto(evento.accion_flujo || 'NORMAL');\n\n// Idioma únicamente desde Code - Normalizar evento WhatsApp TEST.\n// Ese nodo lo calcula por número de teléfono.\nconst idiomaFlujo = normalizar(evento.idioma_flujo || 'ES') === 'EN'\n ? 'EN'\n : 'ES';\n\n// --------------------------------------------------\n// Buscar sesión activa\n// --------------------------------------------------\n// En grupo: sesión única por grupo + sender.\n// En chat individual: sesión por manager_telefono.\nconst sesionActiva = sesiones\n .filter((row) => {\n const rowManager = texto(row.manager_telefono);\n const rowSender = texto(row.sender_phone || row.manager_telefono);\n const rowGroup = texto(row.group_jid);\n\n if (isGroup) {\n return rowGroup === groupJid && rowSender === senderPhone;\n }\n\n return rowManager === managerTelefono;\n })\n .filter((row) => {\n const estado = normalizar(row.estado);\n const etapa = normalizar(row.etapa);\n\n return estadosAbiertos.has(estado) || etapasAbiertas.has(etapa);\n })\n .sort((a, b) => {\n return new Date(b.ultima_actividad || b.fecha_inicio || 0) - new Date(a.ultima_actividad || a.fecha_inicio || 0);\n })[0] || null;\n\n// --------------------------------------------------\n// Crear IDs\n// --------------------------------------------------\n\nconst suffix = Math.random().toString(36).slice(2, 6).toUpperCase();\nconst phoneSuffix = senderPhone.slice(-4) || managerTelefono.slice(-4) || '0000';\nconst sessionId = `WA_${Date.now()}_${phoneSuffix}_${suffix}`;\n\nlet decision = '';\n\nif (accion === 'START') {\n decision = sesionActiva ? 'AVISO_SESION_ACTIVA' : 'CREAR_SESION';\n} else if (accion === 'CANCELAR') {\n decision = sesionActiva ? 'CANCELAR_SESION' : 'SIN_SESION_PARA_CANCELAR';\n} else {\n decision = sesionActiva ? 'CONTINUAR_SESION' : 'SIN_SESION_ACTIVA';\n}\n\nreturn [\n {\n json: {\n ...evento,\n\n idioma_flujo: idiomaFlujo,\n\n sesion_activa_encontrada: Boolean(sesionActiva),\n sesion_activa: sesionActiva,\n\n session_id: sesionActiva ? texto(sesionActiva.session_id) : sessionId,\n ejecucion_id: sesionActiva ? texto(sesionActiva.ejecucion_id || sesionActiva.session_id) : sessionId,\n\n decision_flujo: decision,\n\n nueva_sesion: {\n session_id: sessionId,\n\n manager_telefono: managerTelefono,\n manager_nombre: texto(evento.manager_nombre),\n\n canal_origen: 'WHATSAPP',\n fecha_inicio: new Date().toISOString(),\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_AUDIO',\n\n audio_count: 0,\n imagenes_count: 0,\n videos_count: 0,\n\n estado: 'ACTIVA',\n ejecucion_id: sessionId,\n motivo_revision: '',\n\n is_group: isGroup,\n group_jid: groupJid,\n group_name: texto(evento.group_name),\n sender_jid: texto(evento.sender_jid),\n sender_phone: senderPhone,\n sender_name: texto(evento.sender_name),\n\n country_code: texto(evento.country_code),\n pais_detectado: texto(evento.pais_detectado),\n idioma_flujo: idiomaFlujo,\n\n tipo_reporte: ''\n },\n\n resolver_sesion_debug: {\n idioma_origen: 'Code - Normalizar evento WhatsApp TEST',\n idioma_final_usado: idiomaFlujo,\n pais_detectado: texto(evento.pais_detectado),\n country_code: texto(evento.country_code),\n manager_telefono: managerTelefono,\n sender_phone: senderPhone,\n group_jid: groupJid,\n is_group: isGroup,\n accion,\n decision_flujo: decision,\n total_sesiones_leidas: sesiones.length,\n sesion_activa_encontrada: Boolean(sesionActiva)\n }\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -7920, + -1072 + ], + "id": "062d75cf-28b9-44b1-b119-d619d83b5bfd", + "name": "Code - Resolver sesión WhatsApp TEST" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.decision_flujo === 'CREAR_SESION' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + }, + "id": "acec6506-977a-412d-b08a-fbcc8de4b0b8" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CREAR_SESION" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "5c9dcfa5-9b15-428c-ad80-c277b0671658", + "leftValue": "={{ $json.decision_flujo === 'AVISO_SESION_ACTIVA' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "AVISO_SESION_ACTIVA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "966402b5-4517-45c2-8a87-0a7aa4e5e682", + "leftValue": "={{ $json.decision_flujo === 'SIN_SESION_ACTIVA' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "SIN_SESION_ACTIVA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "698f3da3-29d2-472f-b625-af387891035e", + "leftValue": "={{ $json.decision_flujo === 'CANCELAR_SESION' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CANCELAR_SESION" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "d5feac06-4493-494e-ac1e-36c007f0de79", + "leftValue": "={{ $json.decision_flujo === 'SIN_SESION_PARA_CANCELAR' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "SIN_SESION_PARA_CANCELAR" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "950979b7-b930-4260-a392-52277a742e43", + "leftValue": "={{ $json.decision_flujo === 'CONTINUAR_SESION' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CONTINUAR_SESION" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + -7696, + -1136 + ], + "id": "cad5616f-22a5-4572-a816-253c2f48544c", + "name": "Switch - Decisión sesión WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $('Code - Resolver sesión WhatsApp TEST').first().json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.nueva_sesion?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst nombre =\n datos.manager_nombre ||\n datos.sender_name ||\n 'equipo';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '📸 *GLM Proposal Evidence Registry*',\n '',\n '*Step 1 of 3 — Voice note*',\n '',\n `Hello, ${nombre}.`,\n '',\n 'Use this flow to register evidence for Fulgencio Fumado.',\n '',\n '*Report Type:*',\n '1. *Executed Proposal*: Already implemented.',\n '2. *External Proposal*: Received outside the internal bank.',\n '',\n 'Send a voice note and clearly say if this is an *executed* or *external* proposal.',\n '',\n 'Include:',\n '• Proposal/reference',\n '• Brand/client',\n '• Country',\n '• Location',\n '• Date',\n '• What was implemented or reported',\n '• Results/comments',\n '',\n '*Important:* One proposal/report at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n '📸 *Registro de Evidencias GLM*',\n '',\n '*Paso 1 de 3 — Nota de voz*',\n '',\n `Hola, ${nombre}.`,\n '',\n 'Usa este flujo para registrar evidencias de Fulgencio Fumado.',\n '',\n '*Tipo de Reporte:*',\n '1. *Propuesta Ejecutada*: Ya implementada.',\n '2. *Propuesta Externa*: Recibida fuera del banco interno.',\n '',\n 'Envía una nota de voz e indica claramente si es *ejecutada* o *externa*.',\n '',\n 'Incluye:',\n '• Propuesta/referencia',\n '• Marca/cliente',\n '• País',\n '• Ubicación',\n '• Fecha',\n '• Qué se implementó o reportó',\n '• Resultados/comentarios',\n '',\n '*Importante:* Una sola propuesta o reporte a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid,\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4944, + -4608 + ], + "id": "0c8f0c1a-f57d-4b62-a5e7-91715c1dcaa6", + "name": "Code - Preparar bienvenida WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst etapa = String(datos.sesion_activa?.etapa || '').trim() || 'pendiente';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '⚠️ You already have an active proposal report in progress.',\n '',\n `Current step: ${etapa}`,\n '',\n 'Please complete that report before starting a new one.',\n '',\n 'To cancel it, use the cancel option.',\n ].join('\\n');\n} else {\n mensaje = [\n '⚠️ Ya tienes un reporte de propuesta en proceso.',\n '',\n `Etapa actual: ${etapa}`,\n '',\n 'Completa ese reporte antes de iniciar uno nuevo.',\n '',\n 'Para cancelarlo, usa la opción de cancelar.',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.whatsapp_remote_jid,\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4912, + -4160 + ], + "id": "6413892b-21b2-4d3a-be5e-b200b13c1de1", + "name": "Code - Preparar aviso sesión activa WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.nueva_sesion.session_id }}", + "manager_telefono": "={{ $json.nueva_sesion.manager_telefono }}", + "manager_nombre": "={{ $json.nueva_sesion.manager_nombre }}", + "canal_origen": "={{ $json.nueva_sesion.canal_origen }}", + "fecha_inicio": "={{ $json.nueva_sesion.fecha_inicio }}", + "ultima_actividad": "={{ $json.nueva_sesion.ultima_actividad }}", + "etapa": "={{ $json.nueva_sesion.etapa }}", + "audio_count": "={{ $json.nueva_sesion.audio_count }}", + "imagenes_count": "={{ $json.nueva_sesion.imagenes_count }}", + "videos_count": "={{ $json.nueva_sesion.videos_count }}", + "estado": "={{ $json.nueva_sesion.estado }}", + "ejecucion_id": "={{ $json.nueva_sesion.ejecucion_id }}", + "motivo_revision": "={{ $json.nueva_sesion.motivo_revision }}", + "is_group": "={{ $json.nueva_sesion.is_group }}", + "group_jid": "={{ $json.nueva_sesion.group_jid }}", + "group_name": "={{ $json.nueva_sesion.group_name }}", + "sender_jid": "={{ $json.nueva_sesion.sender_jid }}", + "sender_phone": "={{ $json.nueva_sesion.sender_phone }}", + "sender_name": "={{ $json.nueva_sesion.sender_name }}", + "country_code": "={{ $json.nueva_sesion.country_code }}", + "pais_detectado": "={{ $json.nueva_sesion.pais_detectado }}", + "idioma_flujo": "={{ $json.nueva_sesion.idioma_flujo }}", + "tipo_reporte": "={{ $json.nueva_sesion.tipo_reporte }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -5152, + -4608 + ], + "id": "66b65b89-24b9-484a-9693-a5acecdf77a4", + "name": "Sheets - Crear sesión WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst etapa = texto(datos.sesion_activa?.etapa).toUpperCase();\nconst tipoMensaje = texto(datos.message_type).toLowerCase();\nconst accion = texto(datos.accion_flujo).toUpperCase();\n\nconst imagenesCount = Number(datos.sesion_activa?.imagenes_count || 0);\nconst videosCount = Number(datos.sesion_activa?.videos_count || 0);\n\nlet paso_decision = 'NO_DEFINIDO';\n\n// --------------------------------------------------\n// 1. Cancelación global\n// --------------------------------------------------\nif (accion === 'CANCELAR') {\n paso_decision = 'CANCELAR_SESION';\n\n// --------------------------------------------------\n// 2. Paso 1: esperando nota de voz\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_AUDIO') {\n if (tipoMensaje === 'audio') {\n paso_decision = 'GUARDAR_AUDIO';\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_AUDIO';\n } else {\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 3. Paso 2: esperando imágenes obligatorias\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_IMAGENES') {\n if (tipoMensaje === 'image') {\n paso_decision = 'GUARDAR_IMAGEN';\n\n } else if (accion === 'FOTOS_LISTAS') {\n // La validación real se hace después leyendo wa_ejecuciones_eventos.\n paso_decision = 'FOTOS_LISTAS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_IMAGENES';\n\n } else {\n // Si WhatsApp manda eventos raros durante imágenes, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 4. Paso 3: esperando videos opcionales\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_VIDEOS') {\n if (tipoMensaje === 'video') {\n paso_decision = 'GUARDAR_VIDEO';\n\n } else if (accion === 'SIN_VIDEO') {\n paso_decision = 'CERRAR_SIN_VIDEO';\n\n } else if (accion === 'LISTO') {\n paso_decision = 'CERRAR_CON_VIDEOS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_VIDEOS';\n\n } else {\n // Si WhatsApp manda eventos raros durante videos, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 5. Sesión en procesamiento\n// --------------------------------------------------\n} else if (etapa === 'PROCESANDO') {\n paso_decision = 'AVISO_PROCESANDO';\n\n// --------------------------------------------------\n// 6. Etapa no reconocida\n// --------------------------------------------------\n} else {\n paso_decision = 'ETAPA_NO_RECONOCIDA';\n}\n\nreturn [\n {\n json: {\n ...datos,\n\n etapa_actual: etapa,\n tipo_mensaje_actual: tipoMensaje,\n accion_actual: accion,\n\n imagenes_count_actual: imagenesCount,\n videos_count_actual: videosCount,\n\n paso_decision,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -7408, + -656 + ], + "id": "99ec1dc9-7dde-42fa-8be2-9dc1b3986b12", + "name": "Code - Resolver paso activo WhatsApp TEST" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + }, + "id": "c7fef2d7-50ce-467a-b125-30ae7fba3a58" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "8da042cf-54e6-460f-b505-2b17c4f4ff3c", + "leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ecf52241-ef7e-4a9a-8823-b9e3b438da61", + "leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "04433553-d0ee-4391-a4f4-926197ac4c0c", + "leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "8637dc0f-59b6-40ac-8bc4-baf869829598", + "leftValue": "={{ $json.paso_decision === 'GUARDAR_IMAGEN' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_IMAGEN" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "4096e616-2517-48ab-b6c4-2712f763ec1d", + "leftValue": "={{ $json.paso_decision === 'FOTOS_LISTAS' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "FOTOS_LISTAS" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "d1dd1d5c-8e25-4147-876e-5e7227784f6f", + "leftValue": "={{ $json.paso_decision === 'PEDIR_IMAGENES' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_IMAGENES" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "bddbb16a-0463-47dd-9a6c-03c3635043f1", + "leftValue": "={{ $json.paso_decision === 'GUARDAR_VIDEO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_VIDEO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "fe05ba35-0ccc-417a-a4e8-b907611932e4", + "leftValue": "={{ $json.paso_decision === 'CERRAR_SIN_VIDEO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CERRAR_SIN_VIDEO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "94647ac0-ee80-4c96-9f25-8a847704b6a0", + "leftValue": "={{ $json.paso_decision === 'CERRAR_CON_VIDEOS' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CERRAR_CON_VIDEOS" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "3038a656-50ae-4738-b2ff-a6276bd06522", + "leftValue": "={{ $json.paso_decision === 'PEDIR_VIDEOS' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_VIDEOS" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "1b20a524-2289-4d37-8372-9a839dadc971", + "leftValue": "={{ $json.paso_decision === 'AVISO_PROCESANDO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "AVISO_PROCESANDO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "0943bbd0-f5b7-43c4-9b01-7f6bea244894", + "leftValue": "={{ $json.paso_decision === 'ETAPA_NO_RECONOCIDA' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ETAPA_NO_RECONOCIDA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "84c163a3-3c08-4636-9104-c0e4c55b11e6", + "leftValue": "={{ $json.paso_decision === 'IGNORAR_EVENTO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "IGNORAR_EVENTO" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + -7216, + -384 + ], + "id": "7599f552-a8d6-416c-9664-6acc127fef5b", + "name": "Switch - Paso activo WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '🎙️ *Step 1 of 3 — Voice note*',\n '',\n 'I still need the required voice note to start the report.',\n '',\n 'Please send one voice note and clearly say whether this is:',\n '',\n '1. *Executed Proposal*',\n '2. *External Proposal*',\n '',\n 'Include:',\n '• Proposal/reference',\n '• Brand/client',\n '• Country',\n '• Location',\n '• Date',\n '• What was implemented or reported',\n '• Results/comments',\n '',\n '*Important:* One proposal/report at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n '🎙️ *Paso 1 de 3 — Nota de voz*',\n '',\n 'Aún necesito la nota de voz obligatoria para iniciar el reporte.',\n '',\n 'Envía una sola nota de voz e indica claramente si es:',\n '',\n '1. *Propuesta Ejecutada*',\n '2. *Propuesta Externa*',\n '',\n 'Incluye:',\n '• Propuesta/referencia',\n '• Marca/cliente',\n '• País',\n '• Ubicación',\n '• Fecha',\n '• Qué se implementó o reportó',\n '• Resultados/comentarios',\n '',\n '*Importante:* Una sola propuesta o reporte a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid,\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4784, + -2368 + ], + "id": "ae1227b1-221a-459a-9dde-32d9d2881f00", + "name": "Code - Preparar solicitud audio WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "event_id": "={{ $json.event_id }}", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "message_type": "={{ $json.message_type }}", + "texto": "={{ $json.texto }}", + "comando": "={{ $json.accion_flujo }}", + "tiene_media": "={{ $json.tiene_media }}", + "media_count": "={{ $json.media_count }}", + "raw_preview": "={{ $json.raw_preview }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_procesado": "={{ $json.fecha_procesado }}", + "session_id": "={{ $json.session_id }}", + "canal_origen": "={{ $json.canal_origen }}", + "etapa_recibida": "={{ $json.etapa_recibida }}", + "media_source_id": "={{ $json.media_source_id }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "event_id", + "displayName": "event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "message_type", + "displayName": "message_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "texto", + "displayName": "texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comando", + "displayName": "comando", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tiene_media", + "displayName": "tiene_media", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_count", + "displayName": "media_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "raw_preview", + "displayName": "raw_preview", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_procesado", + "displayName": "fecha_procesado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa_recibida", + "displayName": "etapa_recibida", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_source_id", + "displayName": "media_source_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -5072, + -2752 + ], + "id": "4882de04-332a-4d43-be18-e00085f1144a", + "name": "Sheets - Guardar evento audio WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const datos = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = datos.sesion_activa || datos.nueva_sesion || {};\n\nreturn [\n {\n json: {\n ...datos,\n\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'ACTIVA',\n\n audio_count: Number(sesion.audio_count || 0) + 1,\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n manager_telefono: datos.manager_telefono || sesion.manager_telefono || datos.sender_phone || '',\n manager_nombre: datos.manager_nombre || sesion.manager_nombre || datos.sender_name || '',\n\n is_group: datos.is_group ?? sesion.is_group ?? false,\n group_jid: datos.group_jid || sesion.group_jid || '',\n group_name: datos.group_name || sesion.group_name || '',\n sender_jid: datos.sender_jid || sesion.sender_jid || '',\n sender_phone: datos.sender_phone || sesion.sender_phone || datos.manager_telefono || '',\n sender_name: datos.sender_name || sesion.sender_name || datos.manager_nombre || '',\n\n country_code: datos.country_code || sesion.country_code || '',\n pais_detectado: datos.pais_detectado || sesion.pais_detectado || '',\n idioma_flujo: datos.idioma_flujo || sesion.idioma_flujo || 'ES',\n\n tipo_reporte: sesion.tipo_reporte || datos.tipo_reporte || '',\n\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid || ''\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4864, + -2752 + ], + "id": "00052ad8-be3a-4164-a1ec-d583c244078d", + "name": "Code - Preparar actualización audio recibido WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "estado": "={{ $json.estado }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "is_group": "={{ $json.is_group }}", + "group_jid": "={{ $json.group_jid }}", + "group_name": "={{ $json.group_name }}", + "sender_jid": "={{ $json.sender_jid }}", + "sender_phone": "={{ $json.sender_phone }}", + "sender_name": "={{ $json.sender_name }}", + "country_code": "={{ $json.country_code }}", + "pais_detectado": "={{ $json.pais_detectado }}", + "idioma_flujo": "={{ $json.idioma_flujo }}", + "tipo_reporte": "={{ $json.tipo_reporte }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "ejecucion_id": "={{ $json.ejecucion_id }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -4656, + -2752 + ], + "id": "3fa46865-3a79-47f4-baa2-54338425a397", + "name": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const datos = $('Code - Preparar actualización audio recibido WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ Voice note received.',\n '',\n '*Step 2 of 3 — Required images*',\n '',\n 'Now send one or more photos related to the proposal or report.',\n '',\n 'You can send multiple images together or in separate messages.',\n '',\n 'When you finish sending images, write: PHOTOS READY'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ Nota de voz recibida.',\n '',\n '*Paso 2 de 3 — Imágenes obligatorias*',\n '',\n 'Ahora envía una o varias fotos relacionadas a la propuesta o reporte.',\n '',\n 'Puedes enviar varias imágenes juntas o en mensajes separados.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS'\n ].join('\\n');\n}\n\nconst whatsappTo = limpiar(\n datos.whatsapp_to ||\n datos.group_jid ||\n datos.whatsapp_remote_jid ||\n datos.sender_jid ||\n ''\n);\n\nif (!whatsappTo) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar solicitud de imágenes.');\n}\n\nreturn [\n {\n json: {\n ...datos,\n\n whatsapp_to: whatsappTo,\n\n // Campo principal usado por el nodo WhatsApp\n whatsapp_text: mensaje,\n\n // Campos compatibles por seguridad\n texto_respuesta: mensaje,\n mensaje,\n text: mensaje,\n message_text: mensaje,\n\n estado_mensaje: 'SOLICITUD_IMAGENES_PREPARADA',\n solicitud_imagenes_debug: {\n idioma,\n whatsapp_to: whatsappTo,\n permite_imagenes_juntas: true\n }\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4432, + -2752 + ], + "id": "f11e8263-5435-4b96-b0fa-ca669945c715", + "name": "Code - Preparar solicitud imágenes WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nreturn [\n {\n json: {\n ...datos,\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n etapa: 'CANCELADA',\n estado: 'CANCELADA',\n motivo_revision: 'CANCELADO_POR_USUARIO',\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5040, + -3344 + ], + "id": "0f480c5f-c26a-48ca-aaa9-05e89c54558a", + "name": "Code - Preparar cancelación sesión WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "estado": "={{ $json.estado }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -4832, + -3344 + ], + "id": "f1c30bbb-3fd0-4080-bf86-1c2f3d418b2a", + "name": "Sheets - Cancelar sesión WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver sesión WhatsApp TEST');\n\nconst sesion =\n base.sesion_activa ||\n actual.sesion_activa ||\n {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = limpiar(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n ''\n);\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar mensaje de cancelación.');\n}\n\nlet whatsappTo = destinoBase;\n\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ *Report cancelled successfully.*',\n '',\n 'The current report was closed and no additional evidence will be added to that session.',\n '',\n 'When you are ready, you can start a new registration by writing:',\n '',\n '*Hey*'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ *Reporte cancelado correctamente.*',\n '',\n 'El reporte actual fue cerrado y no se agregará más evidencia a esa sesión.',\n '',\n 'Cuando estés listo, puedes iniciar un nuevo registro escribiendo:',\n '',\n '*Hey*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4576, + -3408 + ], + "id": "c4735257-3f2c-49ff-ae03-0f9260e45fd7", + "name": "Code - Preparar mensaje cancelación WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '📸 I still need at least one image.',\n '',\n 'The voice note was already received, but one evidence photo is required to complete the report.',\n '',\n '⚠️ Please send at least one image, one per message.',\n 'Do not send multiple images together.',\n '',\n 'When you finish sending images, write: PHOTOS READY'\n ].join('\\n');\n} else {\n mensaje = [\n '📸 Necesito que me envíes al menos una imagen.',\n '',\n 'La nota de voz ya fue recibida, pero para completar el reporte hace falta una foto de evidencia.',\n '',\n '⚠️ Por favor envía al menos una imagen, una por mensaje.',\n 'No envíes varias imágenes juntas.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4064, + -320 + ], + "id": "99f86945-1098-4c41-a57f-da8ba11f28bb", + "name": "Code - Preparar aviso falta imagen WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || base.nueva_sesion || {};\n\nconst rows = $input.all().map(item => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizarNumero = (valor) =>\n texto(valor)\n .replace('@s.whatsapp.net', '')\n .replace('@c.us', '')\n .replace('@g.us', '')\n .replace('@lid', '')\n .replace(/\\D/g, '');\n\nconst sessionId = texto(base.session_id || sesion.session_id);\nconst ejecucionId = texto(sesion.ejecucion_id || base.ejecucion_id || sessionId);\n\nconst ahora = new Date().toISOString();\n\nconst fechaInicioSesion = texto(\n sesion.fecha_inicio ||\n base.fecha_inicio ||\n base.sesion_activa?.fecha_inicio ||\n ''\n);\n\nconst fechaInicioMs = fechaInicioSesion\n ? new Date(fechaInicioSesion).getTime()\n : 0;\n\nconst fechaAhoraMs = Date.now();\n\nconst groupJid = texto(\n base.group_jid ||\n sesion.group_jid ||\n base.whatsapp_remote_jid ||\n ''\n);\n\nconst managerTelefono = normalizarNumero(\n base.manager_telefono ||\n base.sender_phone ||\n sesion.manager_telefono ||\n sesion.sender_phone ||\n ''\n);\n\n// Ventana máxima de seguridad para esta sesión.\n// Así no mezclamos fotos viejas del mismo grupo/persona.\n// 45 minutos es suficiente para una ejecución normal.\nconst ventanaMaximaMs = 45 * 60 * 1000;\n\nconst imagenesUnicas = [];\nconst vistos = new Set();\n\nfor (const row of rows) {\n const tipo = texto(row.message_type).toLowerCase();\n\n if (tipo !== 'image') continue;\n\n const rowSessionId = texto(row.session_id);\n const rowGroupJid = texto(row.whatsapp_remote_jid);\n const rowManager = normalizarNumero(row.manager_telefono || row.sender_phone || '');\n\n const rowFecha = texto(row.fecha_recepcion || row.fecha_procesado || '');\n const rowFechaMs = rowFecha ? new Date(rowFecha).getTime() : 0;\n\n const mismaSesion = sessionId && rowSessionId === sessionId;\n\n const mismoGrupoYManager =\n groupJid &&\n rowGroupJid === groupJid &&\n managerTelefono &&\n rowManager === managerTelefono;\n\n const dentroDeVentana =\n rowFechaMs &&\n fechaInicioMs &&\n rowFechaMs >= fechaInicioMs - 15000 &&\n rowFechaMs <= fechaAhoraMs + 15000 &&\n rowFechaMs - fechaInicioMs <= ventanaMaximaMs;\n\n const perteneceALaEjecucion =\n mismaSesion ||\n (mismoGrupoYManager && dentroDeVentana);\n\n if (!perteneceALaEjecucion) continue;\n\n const mediaId = texto(row.media_source_id || row.event_id);\n\n if (!mediaId) continue;\n if (vistos.has(mediaId)) continue;\n\n vistos.add(mediaId);\n imagenesUnicas.push(row);\n}\n\nconst countSesion = Number(sesion.imagenes_count || base.imagenes_count_actual || 0);\n\nconst imagenesCountReal = imagenesUnicas.length > 0\n ? imagenesUnicas.length\n : countSesion;\n\nconst idsImagenes = imagenesUnicas\n .map(row => texto(row.media_source_id || row.event_id))\n .filter(Boolean);\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n ultima_actividad: ahora,\n\n etapa: 'ESPERANDO_VIDEOS',\n estado: 'ACTIVA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: imagenesCountReal,\n videos_count: Number(sesion.videos_count || 0),\n\n manager_telefono: base.manager_telefono || sesion.manager_telefono || base.sender_phone || '',\n manager_nombre: base.manager_nombre || sesion.manager_nombre || base.sender_name || '',\n\n is_group: base.is_group ?? sesion.is_group ?? false,\n group_jid: base.group_jid || sesion.group_jid || '',\n group_name: base.group_name || sesion.group_name || '',\n\n sender_jid: base.sender_jid || sesion.sender_jid || '',\n sender_phone: base.sender_phone || sesion.sender_phone || base.manager_telefono || '',\n sender_name: base.sender_name || sesion.sender_name || base.manager_nombre || '',\n\n country_code: base.country_code || sesion.country_code || '',\n pais_detectado: base.pais_detectado || sesion.pais_detectado || '',\n idioma_flujo: base.idioma_flujo || sesion.idioma_flujo || 'ES',\n\n tipo_reporte: sesion.tipo_reporte || base.tipo_reporte || '',\n\n whatsapp_to: base.whatsapp_to || base.group_jid || base.whatsapp_remote_jid || '',\n\n imagenes_eventos_detectados: imagenesUnicas.length,\n imagenes_media_source_ids: idsImagenes.join(','),\n\n debug_fotos_listas: {\n session_id_actual: sessionId,\n fecha_inicio_sesion: fechaInicioSesion,\n group_jid: groupJid,\n manager_telefono: managerTelefono,\n total_rows_leidas: rows.length,\n total_imagenes_detectadas: imagenesUnicas.length,\n ids_imagenes: idsImagenes\n }\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3440, + -1168 + ], + "id": "7a4c049a-def7-4dff-95aa-62e4399ebb32", + "name": "Preparar actualización fotos listas" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "is_group": "={{ $json.is_group }}", + "group_jid": "={{ $json.group_jid }}", + "group_name": "={{ $json.group_name }}", + "sender_jid": "={{ $json.sender_jid }}", + "sender_phone": "={{ $json.sender_phone }}", + "sender_name": "={{ $json.sender_name }}", + "country_code": "={{ $json.country_code }}", + "pais_detectado": "={{ $json.pais_detectado }}", + "idioma_flujo": "={{ $json.idioma_flujo }}", + "tipo_reporte": "={{ $json.tipo_reporte }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "canal_origen": "={{ $json.canal_origen }}", + "fecha_inicio": "={{ $json.fecha_inicio }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3248, + -1168 + ], + "id": "47d1bd24-d5d1-4004-b3a9-44f322412c11", + "name": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst idioma = String(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = String(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n ''\n).trim();\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar solicitud de videos.');\n}\n\nlet whatsappTo = destinoBase;\n\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '*Step 3 of 3 — Optional videos*',\n '',\n 'You can send one or more videos of the execution if you have them.',\n '',\n '⚠️ Important: send videos one by one, in separate messages.',\n 'Do not send multiple videos together.',\n '',\n 'If you do not have videos, write: NO VIDEO',\n '',\n 'If you already sent videos and finished, write: DONE',\n ].join('\\n');\n} else {\n mensaje = [\n '*Paso 3 de 3 — Videos opcionales*',\n '',\n 'Puedes enviar uno o varios videos de la ejecución si tienes.',\n '',\n '⚠️ Importante: envía los videos uno por uno, en mensajes separados.',\n 'No envíes varios videos juntos.',\n '',\n 'Si no tienes videos, escribe: SIN VIDEO',\n '',\n 'Si enviaste videos y ya terminaste, escribe: LISTO',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n session_id: actual.session_id || base.session_id || sesion.session_id || '',\n ejecucion_id: actual.ejecucion_id || base.ejecucion_id || sesion.ejecucion_id || '',\n\n manager_telefono: actual.manager_telefono || base.manager_telefono || sesion.manager_telefono || '',\n manager_nombre: actual.manager_nombre || base.manager_nombre || sesion.manager_nombre || '',\n\n is_group: actual.is_group ?? base.is_group ?? sesion.is_group ?? false,\n group_jid: actual.group_jid || base.group_jid || sesion.group_jid || '',\n sender_phone: actual.sender_phone || base.sender_phone || sesion.sender_phone || '',\n idioma_flujo: idioma,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3024, + -1168 + ], + "id": "b039c705-d2cf-4d73-bc03-06d93432cdb2", + "name": "Code - Preparar solicitud videos WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '🎥 We are already on the optional videos step.',\n '',\n 'You can send one or more videos of the execution.',\n '',\n '⚠️ Remember: send each video in a separate message.',\n 'Do not send multiple videos together.',\n '',\n 'If you do not have videos, write: *NO VIDEO*',\n '',\n 'If you already sent videos and finished, write: *DONE*'\n ].join('\\n');\n} else {\n mensaje = [\n '🎥 Ya estamos en el paso de videos opcionales.',\n '',\n 'Puedes enviar uno o varios videos de la ejecución.',\n '',\n '⚠️ Recuerda: envía cada video en un mensaje separado.',\n 'No envíes varios videos juntos.',\n '',\n 'Si no tienes videos, escribe: *SIN VIDEO*',\n '',\n 'Si ya enviaste videos y terminaste, escribe: *LISTO*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5936, + 1504 + ], + "id": "a82ee843-061b-4462-a4f0-14f8c6975463", + "name": "Code - Preparar recordatorio videos WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'SIN_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -6288, + 896 + ], + "id": "875805e7-2d5e-4113-8895-ae3704d90c1c", + "name": "CERRAR_SIN_VIDEO" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -6080, + 896 + ], + "id": "9eb38dee-9c6d-472c-9e39-8c9e943b3fc3", + "name": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "event_id": "={{ $json.event_id }}", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "message_type": "={{ $json.message_type }}", + "texto": "={{ $json.texto }}", + "comando": "={{ $json.accion_flujo }}", + "tiene_media": "={{ $json.tiene_media }}", + "media_count": "={{ $json.media_count }}", + "raw_preview": "={{ $json.raw_preview }}", + "estado": "=VIDEO_RECIBIDO", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "session_id": "={{ $json.session_id }}", + "canal_origen": "={{ $json.canal_origen }}", + "etapa_recibida": "={{ $json.etapa_actual }}", + "media_source_id": "={{ $json.media_source_id }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "event_id", + "displayName": "event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "message_type", + "displayName": "message_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "texto", + "displayName": "texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comando", + "displayName": "comando", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tiene_media", + "displayName": "tiene_media", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_count", + "displayName": "media_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "raw_preview", + "displayName": "raw_preview", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_procesado", + "displayName": "fecha_procesado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa_recibida", + "displayName": "etapa_recibida", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_source_id", + "displayName": "media_source_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3936, + -80 + ], + "id": "7e36d0b7-09ab-4744-aaf4-9d8f01ab17fe", + "name": "Sheets - Guardar evento video WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const evento = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst videosActuales = Number(sesion.videos_count || 0);\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n evento.whatsapp_to ||\n evento.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n ...evento,\n\n session_id: base.session_id || sesion.session_id || evento.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || evento.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || evento.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n // Seguimos en videos hasta que el usuario escriba LISTO\n etapa: 'ESPERANDO_VIDEOS',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: videosActuales + 1,\n\n estado: sesion.estado || 'ACTIVA',\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: sesion.motivo_revision || '',\n\n whatsapp_to: whatsappTo\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3712, + -144 + ], + "id": "807724d9-20f0-4285-96b7-104fdcc9b752", + "name": "Code - Preparar actualización video recibido WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "estado": "={{ $json.estado }}", + "videos_count": "={{ $json.videos_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "audio_count": "={{ $json.audio_count }}", + "etapa": "={{ $json.etapa }}", + "ultima_actividad": "={{ $json.ultima_actividad }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3504, + -144 + ], + "id": "23bf12aa-4a65-4fbb-8ff5-236ff367e50e", + "name": "Sheets - Actualizar sesión video recibido WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n actual.whatsapp_to ||\n actual.group_jid ||\n actual.whatsapp_remote_jid ||\n base.whatsapp_to ||\n base.group_jid ||\n base.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar confirmación de video.');\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ Video received.',\n '',\n 'You can send more videos if needed.',\n '',\n '⚠️ Remember: send each video in a separate message.',\n 'Do not send multiple videos together.',\n '',\n 'When you finish sending videos, write: DONE',\n '',\n 'If you do not have more videos, you can also write: DONE'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ Video recibido.',\n '',\n 'Puedes enviar más videos si hace falta.',\n '',\n '⚠️ Recuerda: envía cada video en un mensaje separado.',\n 'No envíes varios videos juntos.',\n '',\n 'Cuando termines de enviar los videos, escribe: LISTO',\n '',\n 'Si no tienes más videos, también puedes escribir: LISTO'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3296, + -144 + ], + "id": "20b53cc1-f5e4-4ecd-9886-20256440653e", + "name": "Code - Preparar confirmación video WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'CON_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -6368, + 1184 + ], + "id": "6ed742b7-db83-4cd6-8652-7efbd7f991ca", + "name": "Code - Preparar cierre con videos WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -6160, + 1184 + ], + "id": "8bfcdbc6-1cdb-4452-8bd2-16ac984f6a9d", + "name": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const data = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n data.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n ...data,\n\n whatsapp_to: whatsappTo,\n\n whatsapp_text: `⏳ Tu reporte ya fue recibido y está pendiente de procesamiento.\n\nPor favor espera mientras se prepara el análisis de la evidencia.`\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5872, + 1744 + ], + "id": "542f9202-df07-47a4-a4e2-707f24a2ffa3", + "name": "Code - Preparar aviso procesando WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nreturn [\n {\n json: {\n ...data,\n\n session_id_busqueda: data.session_id || '',\n estado_procesamiento: 'BUSCAR_EVENTOS_MEDIA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5104, + 1104 + ], + "id": "27e3a13d-e6e9-431d-a24f-43b08f572562", + "name": "Code - Preparar búsqueda eventos media WhatsApp TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -4848, + 1104 + ], + "id": "16b7850c-34c9-4d39-91fd-10e3fd3d8d97", + "name": "Sheets - Leer eventos WhatsApp TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const cierre = $('Code - Preparar búsqueda eventos media WhatsApp TEST').first().json || {};\nconst sessionId = String(cierre.session_id_busqueda || cierre.session_id || '').trim();\n\nconst eventos = $input.all().map(item => item.json || {});\n\nconst eventosSesion = eventos.filter(evento => {\n return String(evento.session_id || '').trim() === sessionId;\n});\n\nconst eventosMedia = eventosSesion.filter(evento => {\n const tipo = String(evento.message_type || '').toLowerCase();\n return ['audio', 'image', 'video'].includes(tipo);\n});\n\nconst audios = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'audio');\nconst imagenes = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'image');\nconst videos = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...cierre,\n\n total_eventos_sesion: eventosSesion.length,\n total_media: eventosMedia.length,\n\n audio_count_real: audios.length,\n imagenes_count_real: imagenes.length,\n videos_count_real: videos.length,\n\n eventos_audio: audios,\n eventos_imagenes: imagenes,\n eventos_videos: videos,\n\n media_lista: eventosMedia,\n\n estado_procesamiento: 'EVENTOS_MEDIA_FILTRADOS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4608, + 1104 + ], + "id": "48fffd2b-c91c-424f-823d-b11785b45673", + "name": "Code - Filtrar eventos media de sesión WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\nconst mediaLista = Array.isArray(data.media_lista) ? data.media_lista : [];\n\nconst sessionId = String(data.session_id || data.session_id_busqueda || '').trim();\nconst ejecucionId = String(data.ejecucion_id || sessionId).trim();\n\nconst items = mediaLista.map((media, index) => {\n const tipo = String(media.message_type || '').toLowerCase();\n const mime = String(media.media_mime_type || '').trim();\n\n let extension = 'bin';\n\n if (mime.includes('ogg')) extension = 'ogg';\n else if (mime.includes('mpeg')) extension = 'mp3';\n else if (mime.includes('mp4')) extension = 'mp4';\n else if (mime.includes('jpeg') || mime.includes('jpg')) extension = 'jpg';\n else if (mime.includes('png')) extension = 'png';\n else if (tipo === 'audio') extension = 'ogg';\n else if (tipo === 'image') extension = 'jpg';\n else if (tipo === 'video') extension = 'mp4';\n\n const mediaSourceId = String(\n media.media_source_id ||\n media.event_id ||\n ''\n ).trim();\n\n const remoteJid = String(\n media.whatsapp_remote_jid ||\n data.whatsapp_remote_jid ||\n data.whatsapp_to ||\n media.manager_telefono ||\n data.manager_telefono ||\n ''\n ).trim();\n\n return {\n json: {\n ...data,\n\n media_index: index + 1,\n media_total: mediaLista.length,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: media.event_id || '',\n media_source_id: mediaSourceId,\n media_type: tipo,\n media_mime_type: mime,\n media_extension: extension,\n\n whatsapp_remote_jid: remoteJid,\n\n media_file_name: `${ejecucionId}_${tipo}_${index + 1}.${extension}`,\n\n evento_original: media,\n\n estado_procesamiento: 'MEDIA_ITEM_PREPARADO'\n }\n };\n});\n\nreturn items;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4368, + 1104 + ], + "id": "4f0e23fe-0c3d-4cc1-8069-716b76ea3ce2", + "name": "Code - Separar media en items WhatsApp TEST" + }, + { + "parameters": { + "method": "POST", + "url": "https://wsp.gomezleemarketing.com/chat/getBase64FromMediaMessage/botsoporte", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "={{'C267126ABB45-4C12-B626-6BAB1833F5D7'}}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"message\": {\n \"key\": {\n \"id\": \"{{ $json.media_source_id }}\"\n }\n },\n \"convertToMp4\": true\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + -4160, + 1104 + ], + "id": "a7b7a1fa-3c27-49e2-9606-3b8148b1c443", + "name": "HTTP Request - Obtener media base64 Evolution TEST", + "alwaysOutputData": true + }, + { + "parameters": { + "jsCode": "const httpItems = $input.all();\nconst mediaItems = $('Code - Separar media en items WhatsApp TEST').all();\n\nconst buscarBase64 = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.base64 === 'string') return obj.base64;\n if (typeof obj.data === 'string') return obj.data;\n if (typeof obj.media === 'string') return obj.media;\n\n if (obj.message && typeof obj.message.base64 === 'string') return obj.message.base64;\n if (obj.response && typeof obj.response.base64 === 'string') return obj.response.base64;\n if (obj.data && typeof obj.data.base64 === 'string') return obj.data.base64;\n\n return '';\n};\n\nreturn httpItems.map((item, index) => {\n const respuesta = item.json || {};\n const mediaItem = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n let base64 = buscarBase64(respuesta);\n\n base64 = String(base64 || '').trim();\n base64 = base64.replace(/^data:.*?;base64,/, '');\n\n if (!base64) {\n throw new Error(`No se encontró base64 para media_source_id: ${mediaItem.media_source_id || 'SIN_ID'}`);\n }\n\n return {\n json: {\n ...mediaItem,\n\n base64_length: base64.length,\n estado_procesamiento: 'MEDIA_BASE64_RECUPERADA'\n },\n binary: {\n data: {\n data: base64,\n mimeType: mediaItem.media_mime_type || 'application/octet-stream',\n fileName: mediaItem.media_file_name || 'media_whatsapp.bin'\n }\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3952, + 1104 + ], + "id": "61204b9e-0419-444a-9e39-c87a56b88895", + "name": "Code - Convertir base64 a binario WhatsApp TEST" + }, + { + "parameters": { + "name": "={{ $json.media_file_name }}", + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "=1G11WZnOwPp7UC2moPE6rEvlPFl7_OoSB", + "mode": "id" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + -3712, + 1104 + ], + "id": "87632ce1-965c-4354-b06f-ac8ce766e7d4", + "name": "Drive - Subir media WhatsApp TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const driveItems = $input.all();\nconst mediaItems = $('Code - Convertir base64 a binario WhatsApp TEST').all();\n\nreturn driveItems.map((item, index) => {\n const drive = item.json || {};\n const media = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n const fileId =\n drive.id ||\n drive.fileId ||\n drive.file_id ||\n '';\n\n const webViewLink =\n drive.webViewLink ||\n drive.webContentLink ||\n (fileId ? `https://drive.google.com/file/d/${fileId}/view` : '');\n\n return {\n json: {\n ...media,\n\n drive_file_id: fileId,\n drive_file_name: media.media_file_name,\n drive_file_url: webViewLink,\n\n estado_procesamiento: 'MEDIA_GUARDADA_EN_DRIVE'\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3504, + 1104 + ], + "id": "ae4d3939-49c5-4794-bbbb-eda739313673", + "name": "Code - Preparar registro media Drive WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const mediaItems = $('Code - Preparar registro media Drive WhatsApp TEST').all();\n\nconst media = mediaItems.map(item => item.json || {});\n\nconst base = media[0] || {};\n\nconst audios = media.filter(m => String(m.media_type || '').toLowerCase() === 'audio');\nconst imagenes = media.filter(m => String(m.media_type || '').toLowerCase() === 'image');\nconst videos = media.filter(m => String(m.media_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || '',\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n\n audio_drive_urls: audios.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n imagenes_drive_urls: imagenes.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n videos_drive_urls: videos.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n\n total_media_drive: media.length,\n total_audio_drive: audios.length,\n total_imagenes_drive: imagenes.length,\n total_videos_drive: videos.length,\n\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_ANALIZAR',\n motivo_revision: 'MEDIA_RECUPERADA',\n\n estado_procesamiento: 'MEDIA_RECUPERADA_Y_REGISTRADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3088, + 1104 + ], + "id": "5e6b2d61-89b0-4d31-a371-51e38b01d88c", + "name": "Code - Consolidar media recuperada WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1962428033, + "mode": "list", + "cachedResultName": "wa_ejecuciones_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1962428033" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "media_event_id": "={{ $json.media_event_id }}", + "media_type": "={{ $json.media_type }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.drive_file_name }}", + "drive_file_id": "={{ $json.drive_file_id }}", + "drive_file_url": "={{ $json.drive_file_url }}", + "fecha_guardado": "={{ new Date().toISOString() }}", + "estado": "=MEDIA_GUARDADA_EN_DRIVE", + "media_index": "={{ $json.media_index }}", + "media_total": "={{ $json.media_total }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_event_id", + "displayName": "media_event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_type", + "displayName": "media_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "drive_file_id", + "displayName": "drive_file_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "drive_file_url", + "displayName": "drive_file_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_guardado", + "displayName": "fecha_guardado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_index", + "displayName": "media_index", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_total", + "displayName": "media_total", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3296, + 1104 + ], + "id": "71354a86-ea50-48df-8262-5882092dcb85", + "name": "Sheets - Guardar media Drive WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "estado": "={{ $json.estado }}", + "etapa": "={{ $json.etapa }}", + "ultima_actividad": "={{ $json.ultima_actividad }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -2880, + 1104 + ], + "id": "e23d8301-e104-4a02-9203-60d9f5af599b", + "name": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const data = $('Code - Consolidar media recuperada WhatsApp TEST').first().json || {};\n\nconst audioUrls = String(data.audio_drive_urls || '').trim();\nconst imagenesUrls = String(data.imagenes_drive_urls || '').trim();\nconst videosUrls = String(data.videos_drive_urls || '').trim();\n\nconst prompt = `\nAnaliza esta ejecución de propuesta usando la evidencia disponible.\n\nIMPORTANTE:\n- La nota de voz contiene la descripción principal de la ejecución.\n- Las imágenes muestran evidencia visual obligatoria.\n- Los videos son evidencia opcional adicional.\n- No inventes datos que no estén claros.\n- Si un dato no aparece, marca \"No identificado\".\n\nDebes devolver SOLO un JSON válido con esta estructura:\n\n{\n \"propuesta_referencia\": \"\",\n \"marca\": \"\",\n \"cliente\": \"\",\n \"pais\": \"\",\n \"ubicacion\": \"\",\n \"fecha_ejecucion\": \"\",\n \"que_se_implemento\": \"\",\n \"comentarios_resultados\": \"\",\n \"resumen_ejecucion\": \"\",\n \"nivel_confianza\": \"\",\n \"requiere_revision\": \"\",\n \"motivo_revision\": \"\"\n}\n\nDatos de control:\nsession_id: ${data.session_id}\nejecucion_id: ${data.ejecucion_id}\nmanager_nombre: ${data.manager_nombre}\nmanager_telefono: ${data.manager_telefono}\n\nArchivos de audio en Drive:\n${audioUrls || 'No hay audio registrado'}\n\nImágenes en Drive:\n${imagenesUrls || 'No hay imágenes registradas'}\n\nVideos en Drive:\n${videosUrls || 'No hay videos registrados'}\n`.trim();\n\nreturn [\n {\n json: {\n ...data,\n gemini_prompt: prompt,\n estado_procesamiento: 'PAQUETE_GEMINI_PREPARADO'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -2672, + 1104 + ], + "id": "28417401-35f1-4448-9305-34d82352836f", + "name": "Code - Preparar paquete análisis Gemini WhatsApp TEST" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", + "rightValue": "audio", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "ddf19bdc-2a61-4261-8950-02ea3026ba01" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "audio" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dcf1d733-7b80-4f67-b6f6-9e965a5bae24", + "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", + "rightValue": "image", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "=image" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "bb7e48e1-8383-463c-a43e-7e8e5d1ae21d", + "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", + "rightValue": "video", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "VIDEO" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + -1424, + 2224 + ], + "id": "5095c259-4595-4537-9913-9eeb1e4ce964", + "name": "Switch - Tipo media para Gemini WhatsApp TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const item = $input.item;\n\nconst data = item.json || {};\nconst binaryData = item.binary?.data;\n\nif (!binaryData) {\n throw new Error('No llegó binary.data al nodo de audio para Gemini');\n}\n\nconst geminiPromptAudio = `\nAnaliza esta nota de voz enviada por WhatsApp para el flujo de registro de propuestas de Fulgencio Fumado.\n\nObjetivo:\nExtraer información útil del reporte y clasificar correctamente el tipo de reporte.\n\nPrimero, identifica el tipo de reporte usando exactamente uno de estos valores:\n\n1. PROPUESTA_EJECUTADA\nCuando el manager reporta evidencia de una propuesta que ya fue implementada, instalada, ejecutada, realizada o activada.\n\n2. PROPUESTA_EXTERNA\nCuando el manager reporta una propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n\n3. NO_DETERMINADO\nCuando la nota de voz no permite saber claramente si es una propuesta ejecutada o una propuesta externa.\n\nReglas para clasificar:\n- Si el manager dice explícitamente \"propuesta ejecutada\", clasifica como PROPUESTA_EJECUTADA.\n- Si el manager dice que recibió una propuesta externa, referencia externa o algo fuera del banco, clasifica como PROPUESTA_EXTERNA.\n- Si solo hay una prueba de audio, saludo, ruido, conteo, información incompleta o no se entiende el objetivo, clasifica como NO_DETERMINADO.\n- No inventes el tipo de reporte.\n- Si hay duda, usa NO_DETERMINADO.\n\nAdemás, extrae la mayor cantidad posible de información útil:\n- Nombre o referencia de la propuesta\n- Marca o cliente\n- País\n- Ubicación\n- Fecha de ejecución o fecha del reporte\n- Qué se implementó o qué se está reportando\n- Comentarios o resultados observados\n- Si el manager menciona que es ejecutada, externa o no queda claro\n\nDevuelve el análisis en texto claro y estructurado, incluyendo obligatoriamente estas líneas:\n\nTipo de reporte detectado: PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\nConfianza del tipo de reporte: ALTA | MEDIA | BAJA\nMotivo del tipo de reporte: explicación breve\n\nNo inventes datos. Si algo no está claro, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_audio: geminiPromptAudio\n },\n binary: {\n data: binaryData\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -944, + 2048 + ], + "id": "7404bc52-fd09-40e9-94ab-27157f4be4ed", + "name": "Code - Preparar Gemini audio WhatsApp TEST" + }, + { + "parameters": { + "resource": "audio", + "operation": "analyze", + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "text": "={{ $json.gemini_prompt_audio }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -736, + 2048 + ], + "id": "353aba0e-8ed5-484a-b8fc-65ee7829a951", + "name": "Gemini - Analizar audio WhatsApp TEST", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini audio 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: 'audio',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_AUDIO_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": [ + -528, + 2048 + ], + "id": "fa99c19f-45ca-408d-883a-ffeb4d32ca9a", + "name": "Code - Normalizar análisis audio WhatsApp TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const input = $input.item;\nconst data = input.json || {};\nconst binaries = input.binary || {};\n\nconst binaryKeys = Object.keys(binaries);\nconst sourceBinaryKey = binaries.data ? 'data' : binaryKeys[0];\n\nif (!sourceBinaryKey || !binaries[sourceBinaryKey]) {\n throw new Error(\n `No llegó archivo binario para Gemini imagen. Binary keys recibidas: ${binaryKeys.join(', ') || 'NINGUNA'}`\n );\n}\n\nconst binaryData = {\n ...binaries[sourceBinaryKey],\n};\n\nbinaryData.fileName =\n binaryData.fileName ||\n data.media_file_name ||\n `imagen_${data.media_index || Date.now()}.jpg`;\n\nbinaryData.mimeType =\n binaryData.mimeType ||\n data.mime_type ||\n data.mimetype ||\n 'image/jpeg';\n\nconst geminiPromptImagen = `\nAnaliza esta imagen como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en la imagen\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Posible ubicación visible\n- Calidad de la implementación\n- Detalles relevantes para documentar la ejecución\n\nNo inventes datos que no sean visibles.\nSi algo no se puede identificar, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_imagen: geminiPromptImagen,\n binary_input_field: 'data',\n debug_binary_imagen: {\n binary_keys_recibidas: binaryKeys,\n source_binary_key: sourceBinaryKey,\n output_binary_key: 'data',\n file_name: binaryData.fileName,\n mime_type: binaryData.mimeType,\n },\n },\n binary: {\n data: binaryData,\n },\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -928, + 2272 + ], + "id": "3e673d5d-5aa4-4d74-8a09-eeac3fb67c97", + "name": "Code - Preparar Gemini imagen WhatsApp TEST" + }, + { + "parameters": { + "resource": "image", + "operation": "analyze", + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "text": "={{ $json.gemini_prompt_imagen }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -720, + 2272 + ], + "id": "212e7998-4787-445d-824e-5e7b3911e206", + "name": "Gemini - Analizar imagen WhatsApp TEST", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini imagen WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'image',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_IMAGEN_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -512, + 2272 + ], + "id": "7b2891dd-09dc-40e1-a470-486debfc6418", + "name": "Code - Normalizar análisis imagen WhatsApp TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const data = $json || {};\nconst binary = $binary || {};\n\nif (!binary.data) {\n throw new Error('No llegó binary.data al nodo Code - Preparar Gemini video.');\n}\n\nconst fileName =\n binary.data.fileName ||\n data.media_file_name ||\n data.file_name ||\n `video_${Date.now()}.mp4`;\n\nconst mimeType =\n binary.data.mimeType ||\n data.mime_type ||\n 'video/mp4';\n\n// Fuerza a n8n a cargar el contenido real del binario.\n// Esto evita el error de Gemini: \"Received undefined\".\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n\nif (!buffer || !Buffer.isBuffer(buffer) || buffer.length === 0) {\n throw new Error('El video llegó con metadata, pero sin contenido binario real.');\n}\n\nconst preparedBinary = await this.helpers.prepareBinaryData(\n buffer,\n fileName,\n mimeType\n);\n\nconst geminiPromptVideo = `\nAnaliza este video como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en el video\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Interacciones o movimientos relevantes\n- Posible ubicación visible\n- Calidad de la implementación\n- Resultados observables\n\nNo inventes datos que no sean visibles.\n`.trim();\n\nreturn {\n json: {\n ...data,\n media_type: 'video',\n media_file_name: fileName,\n mime_type: mimeType,\n gemini_prompt_video: geminiPromptVideo,\n },\n binary: {\n data: preparedBinary,\n },\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -1056, + 2512 + ], + "id": "7992ddf7-5394-4126-902c-0eec813ca6f3", + "name": "Code - Preparar Gemini video WhatsApp TEST" + }, + { + "parameters": { + "resource": "video", + "operation": "analyze", + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "text": "={{ $json.gemini_prompt_video }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -848, + 2512 + ], + "id": "4e0c63b2-429f-4908-8c57-69093c2db609", + "name": "Analyze video", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini video WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'video',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_VIDEO_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -640, + 2512 + ], + "id": "a05f39fa-f0df-4e8b-81f7-8ea4b9b5febc", + "name": "Code - Normalizar análisis video WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1107394537, + "mode": "list", + "cachedResultName": "wa_ejecuciones_analisis_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "media_event_id": "={{ $json.media_event_id }}", + "media_type": "={{ $json.media_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "analisis_texto": "={{ $json.analisis_texto }}", + "fecha_analisis": "={{ $json.fecha_analisis }}", + "estado": "={{ $json.estado }}", + "media_index": "={{ $json.media_index }}", + "media_total": "={{ $json.media_total }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_event_id", + "displayName": "media_event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_type", + "displayName": "media_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "analisis_texto", + "displayName": "analisis_texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_analisis", + "displayName": "fecha_analisis", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_index", + "displayName": "media_index", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_total", + "displayName": "media_total", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 144, + 2304 + ], + "id": "7a6a54c1-687f-479a-b512-8490a433b60e", + "name": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1107394537, + "mode": "list", + "cachedResultName": "wa_ejecuciones_analisis_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 3936, + 2112 + ], + "id": "73ae1420-d16f-425b-b6dd-ab4f92dd81ad", + "name": "Sheets - Leer análisis media Gemini WhatsApp TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst inputRows = $input.all().map(item => item.json || {});\nconst actual = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst extraerTextoGemini = (valor) => {\n if (valor === null || valor === undefined) return '';\n\n if (typeof valor === 'object') {\n const partes =\n valor?.content?.parts ||\n valor?.response?.content?.parts ||\n valor?.candidates?.[0]?.content?.parts ||\n valor?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(valor);\n }\n\n const raw = limpiar(valor);\n if (!raw) return '';\n\n try {\n const parsed = JSON.parse(raw);\n\n const partes =\n parsed?.content?.parts ||\n parsed?.response?.content?.parts ||\n parsed?.candidates?.[0]?.content?.parts ||\n parsed?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(parsed);\n } catch (error) {\n return raw;\n }\n};\n\nconst normalizarTipoMedia = (valor) => {\n const t = limpiar(valor).toLowerCase();\n\n if (t.includes('audio')) return 'audio';\n if (t.includes('image') || t.includes('imagen')) return 'image';\n if (t.includes('video')) return 'video';\n\n return t || 'unknown';\n};\n\nconst obtenerFecha = (row) => {\n const fecha = new Date(row.fecha_analisis || row.fecha_recepcion || row.ultima_actividad || 0);\n const time = fecha.getTime();\n return Number.isFinite(time) ? time : 0;\n};\n\n// Contextos posibles del cierre actual.\n// IMPORTANTE: NO usamos $json como prioridad, porque aquí $json puede ser la primera fila del Sheet.\nconst decisionPorSheet = getNodeJson('Code - Decidir cierre análisis por Sheet Redis TEST');\nconst validarLock = getNodeJson('Code - Validar lock análisis final TEST');\nconst validarConteo = getNodeJson('Code - Validar conteo análisis Redis TEST');\nconst cierreMedia = getNodeJson('Code - Consolidar media recuperada WhatsApp TEST');\nconst eventoPaso = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst eventoSesion = getNodeJson('Code - Resolver sesión WhatsApp TEST');\n\nlet sessionId = limpiar(\n decisionPorSheet.session_id ||\n validarLock.session_id ||\n validarConteo.session_id ||\n cierreMedia.session_id ||\n eventoPaso.session_id ||\n eventoSesion.session_id ||\n ''\n);\n\nlet ejecucionId = limpiar(\n decisionPorSheet.ejecucion_id ||\n validarLock.ejecucion_id ||\n validarConteo.ejecucion_id ||\n cierreMedia.ejecucion_id ||\n eventoPaso.ejecucion_id ||\n eventoSesion.ejecucion_id ||\n sessionId\n);\n\n// Si por cualquier razón no llegó session_id desde el contexto,\n// usamos la sesión más reciente del Sheet leído.\nif (!sessionId) {\n const grupos = {};\n\n for (const row of inputRows) {\n const sid = limpiar(row.session_id);\n if (!sid) continue;\n\n if (!grupos[sid]) {\n grupos[sid] = {\n session_id: sid,\n ejecucion_id: limpiar(row.ejecucion_id || sid),\n rows: [],\n ultima_fecha: 0\n };\n }\n\n grupos[sid].rows.push(row);\n grupos[sid].ultima_fecha = Math.max(grupos[sid].ultima_fecha, obtenerFecha(row));\n }\n\n const grupoMasReciente = Object.values(grupos)\n .sort((a, b) => b.ultima_fecha - a.ultima_fecha)[0];\n\n if (grupoMasReciente) {\n sessionId = grupoMasReciente.session_id;\n ejecucionId = grupoMasReciente.ejecucion_id || sessionId;\n }\n}\n\nif (!sessionId) {\n throw new Error('No llegó session_id para consolidar análisis de media.');\n}\n\n// Primero intentamos usar las filas filtradas por el nodo de cierre.\n// Si no vienen, usamos las filas leídas del Sheet.\nlet rows = [];\n\nif (\n Array.isArray(decisionPorSheet.analisis_rows_filtrados) &&\n decisionPorSheet.analisis_rows_filtrados.length > 0\n) {\n rows = decisionPorSheet.analisis_rows_filtrados;\n} else {\n rows = inputRows;\n}\n\n// Filtrar solo la sesión correcta.\nrows = rows.filter(row => limpiar(row.session_id) === sessionId);\n\n// Si todavía no encontró nada, usar fallback por ejecución.\nif (rows.length === 0 && ejecucionId) {\n rows = inputRows.filter(row => limpiar(row.ejecucion_id) === ejecucionId);\n}\n\n// Deduplicar por media_event_id + media_type.\nconst vistos = new Set();\nconst analisisUnicos = [];\n\nfor (const row of rows) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaType = normalizarTipoMedia(row.media_type);\n\n if (!mediaEventId || !mediaType) continue;\n\n const key = `${mediaEventId}_${mediaType}`;\n if (vistos.has(key)) continue;\n\n const textoAnalisis = extraerTextoGemini(row.analisis_texto);\n\n if (!textoAnalisis) continue;\n\n vistos.add(key);\n\n analisisUnicos.push({\n ...row,\n media_type: mediaType,\n media_index: Number(row.media_index || 0),\n media_total: Number(row.media_total || row.media_total_esperado || 0),\n analisis_texto_limpio: textoAnalisis\n });\n}\n\nanalisisUnicos.sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst audios = analisisUnicos.filter(row => row.media_type === 'audio');\nconst imagenes = analisisUnicos.filter(row => row.media_type === 'image');\nconst videos = analisisUnicos.filter(row => row.media_type === 'video');\n\nconst audiosCount = audios.length;\nconst fotosCount = imagenes.length;\nconst videosCount = videos.length;\n\nconst mediaTotalEsperado = Number(\n decisionPorSheet.media_total_esperado ||\n validarConteo.media_total_esperado ||\n cierreMedia.total_media_drive ||\n cierreMedia.total_media ||\n analisisUnicos[0]?.media_total ||\n analisisUnicos.length ||\n 0\n);\n\nconst analisisAudioTexto = audios\n .map(row => row.analisis_texto_limpio)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisImagenesTexto = imagenes\n .map((row, index) => `IMAGEN ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisVideosTexto = videos\n .map((row, index) => `VIDEO ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisConsolidado = [\n analisisAudioTexto ? `ANÁLISIS DE AUDIO:\\n${analisisAudioTexto}` : '',\n analisisImagenesTexto ? `ANÁLISIS DE IMÁGENES:\\n${analisisImagenesTexto}` : '',\n analisisVideosTexto ? `ANÁLISIS DE VIDEOS:\\n${analisisVideosTexto}` : ''\n].filter(Boolean).join('\\n\\n');\n\nif (!analisisConsolidado) {\n throw new Error(\n `No hay análisis multimedia consolidado para enviar a Gemini final. session_id=${sessionId}, ejecucion_id=${ejecucionId}, rows_filtradas=${rows.length}, input_rows=${inputRows.length}`\n );\n}\n\nconst transcripcionAudio = analisisAudioTexto || '';\n\nconst geminiPromptFinal = `\nEres un analista de evidencias de GomezLee Marketing / Fulgencio Fumado.\n\nTu tarea es consolidar el análisis de audio, imágenes y videos de un reporte recibido por WhatsApp.\n\nIMPORTANTE:\n- No inventes información.\n- Si un dato no aparece claramente, responde \"No identificado\".\n- El audio tiene prioridad para identificar propuesta, marca, país, ubicación, fecha y tipo de reporte.\n- Las imágenes y videos sirven como evidencia visual.\n- Clasifica el reporte usando exactamente uno de estos valores:\n - PROPUESTA_EJECUTADA\n - PROPUESTA_EXTERNA\n - NO_DETERMINADO\n\nDefiniciones:\n- PROPUESTA_EJECUTADA: evidencia de una propuesta ya implementada, instalada, ejecutada, realizada o activada.\n- PROPUESTA_EXTERNA: propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n- NO_DETERMINADO: la información no permite saber claramente si es ejecutada o externa.\n\nDATOS DEL REPORTE:\n- session_id: ${sessionId}\n- ejecucion_id: ${ejecucionId}\n- audios_count: ${audiosCount}\n- fotos_count: ${fotosCount}\n- videos_count: ${videosCount}\n\nANÁLISIS MULTIMEDIA CONSOLIDADO:\n${analisisConsolidado}\n\nDevuelve ÚNICAMENTE un JSON válido, sin markdown, sin explicación adicional y sin texto fuera del JSON.\n\nEl JSON debe tener exactamente esta estructura:\n\n{\n \"tipo_reporte\": \"PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\",\n \"tipo_reporte_confianza\": \"ALTA | MEDIA | BAJA\",\n \"motivo_tipo_reporte\": \"texto breve\",\n \"propuesta_referencia\": \"texto o No identificado\",\n \"marca\": \"texto o No identificado\",\n \"cliente\": \"texto o No identificado\",\n \"pais\": \"texto o No identificado\",\n \"ubicacion\": \"texto o No identificado\",\n \"fecha_ejecucion\": \"texto o No identificado\",\n \"descripcion_ejecucion\": \"texto claro y profesional\",\n \"elementos_detectados\": \"lista resumida en texto\",\n \"resumen_ia\": \"resumen ejecutivo del reporte\",\n \"comentarios_resultados\": \"texto o No identificado\",\n \"tags\": \"tags separados por coma\"\n}\n`.trim();\n\nreturn [\n {\n json: {\n ...cierreMedia,\n ...eventoSesion,\n ...eventoPaso,\n ...validarConteo,\n ...validarLock,\n ...decisionPorSheet,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_media: analisisUnicos.length,\n\n audio_count: audiosCount,\n audios_count: audiosCount,\n imagenes_count: fotosCount,\n fotos_count: fotosCount,\n videos_count: videosCount,\n\n transcripcion_audio: transcripcionAudio,\n\n analisis_audio: analisisAudioTexto,\n analisis_imagenes: analisisImagenesTexto,\n analisis_videos: analisisVideosTexto,\n\n analisis_audio_texto: analisisAudioTexto,\n analisis_imagenes_texto: analisisImagenesTexto,\n analisis_videos_texto: analisisVideosTexto,\n\n analisis_media_consolidado: analisisConsolidado,\n analisis_multimedia: analisisConsolidado,\n analisis_consolidado: analisisConsolidado,\n\n gemini_prompt_final: geminiPromptFinal,\n\n analisis_rows_filtrados: analisisUnicos,\n\n estado_analisis_media: 'ANALISIS_MEDIA_CONSOLIDADO',\n\n consolidacion_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n input_rows: inputRows.length,\n rows_filtradas: rows.length,\n total_analisis_unicos: analisisUnicos.length,\n media_total_esperado: mediaTotalEsperado,\n audios: audiosCount,\n imagenes: fotosCount,\n videos: videosCount,\n gemini_prompt_final_generado: Boolean(geminiPromptFinal),\n gemini_prompt_final_length: geminiPromptFinal.length,\n uso_decision_por_sheet: Object.keys(decisionPorSheet).length > 0,\n uso_validar_lock: Object.keys(validarLock).length > 0,\n uso_validar_conteo: Object.keys(validarConteo).length > 0,\n media: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado,\n media_file_name: row.media_file_name\n }))\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 4832, + 2144 + ], + "id": "43fee68c-e52d-4323-8175-fa95b0ed8531", + "name": "Code - Consolidar análisis media Gemini WhatsApp TEST" + }, + { + "parameters": { + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "messages": { + "values": [ + { + "content": "={{ $json.gemini_prompt_final }}" + } + ] + }, + "builtInTools": {}, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + 5024, + 2144 + ], + "id": "73c6e7dd-5ac4-4673-9780-7ab545a4e8c5", + "name": "Gemini - Generar JSON final propuesta ejecutada TEST", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "parameters": { + "jsCode": "const gemini = $json || {};\nconst base = $('Code - Consolidar análisis media Gemini WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst esNoIdentificado = (valor) => {\n const texto = normalizarPlano(valor);\n\n return (\n !texto ||\n texto === 'NO IDENTIFICADO' ||\n texto === 'NO DISPONIBLE' ||\n texto === 'N/A' ||\n texto === 'NA' ||\n texto === 'NULL' ||\n texto === 'UNDEFINED'\n );\n};\n\nconst valor = (campo, fallback = 'No identificado') => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return fallback;\n\n return v;\n};\n\nconst valorOpcional = (campo) => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return '';\n\n return v;\n};\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (Array.isArray(obj.content?.parts)) {\n const txt = obj.content.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.parts)) {\n const txt = obj.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.candidates)) {\n const txt = obj.candidates\n .flatMap(c => c.content?.parts || [])\n .map(p => p.text || '')\n .join('\\n')\n .trim();\n\n if (txt) return txt;\n }\n\n if (obj.message && typeof obj.message === 'object') {\n const txt = extraerTexto(obj.message);\n if (txt) return txt;\n }\n\n if (obj.data && typeof obj.data === 'object') {\n const txt = extraerTexto(obj.data);\n if (txt) return txt;\n }\n\n return JSON.stringify(obj);\n};\n\nlet texto = extraerTexto(gemini).trim();\n\ntexto = texto\n .replace(/^```json/i, '')\n .replace(/^```/i, '')\n .replace(/```$/i, '')\n .trim();\n\nlet parsed = {};\n\ntry {\n parsed = JSON.parse(texto);\n} catch (error) {\n parsed = {\n tipo_reporte: 'NO_DETERMINADO',\n tipo_reporte_confianza: 'BAJA',\n tipo_reporte_motivo: `No se pudo parsear el JSON final de Gemini: ${error.message}`,\n\n propuesta_referencia: 'No identificado',\n marca: 'No identificado',\n cliente: 'No identificado',\n pais: 'No identificado',\n ubicacion: 'No identificado',\n fecha_ejecucion: 'No identificado',\n que_se_implemento: 'No identificado',\n comentarios_resultados: '',\n resumen_ejecucion: texto || 'No se pudo parsear el JSON de Gemini.',\n evidencia_audio_resumen: base.texto_audio || '',\n evidencia_imagenes_resumen: base.texto_imagenes || '',\n evidencia_videos_resumen: base.texto_videos || '',\n nivel_confianza: 'BAJA',\n requiere_revision: 'SI',\n motivo_revision: `Error parseando JSON final: ${error.message}`\n };\n}\n\nconst normalizarTipoReporte = (valorTipo) => {\n const texto = String(valorTipo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, '_');\n\n if (\n texto === 'PROPUESTA_EJECUTADA' ||\n texto === 'EJECUTADA' ||\n texto === 'EJECUTADO'\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n if (\n texto === 'PROPUESTA_EXTERNA' ||\n texto === 'EXTERNA' ||\n texto === 'EXTERNO'\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n return 'NO_DETERMINADO';\n};\n\nconst normalizarConfianza = (valorConfianza) => {\n const texto = String(valorConfianza ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['ALTA', 'MEDIA', 'BAJA'].includes(texto)) {\n return texto;\n }\n\n return 'BAJA';\n};\n\nconst normalizarSiNo = (valorSiNo, fallback = 'SI') => {\n const texto = String(valorSiNo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['SI', 'SÍ', 'YES', 'TRUE'].includes(texto)) return 'SI';\n if (['NO', 'FALSE'].includes(texto)) return 'NO';\n\n return fallback;\n};\n\nconst limpiarReferenciaVisual = (valorRef) => {\n let textoRef = limpiar(valorRef);\n\n if (esNoIdentificado(textoRef)) return 'No identificado';\n\n textoRef = textoRef\n .replace(/_/g, ' ')\n .replace(/\\+/g, '+')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return textoRef || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valorUbicacion) => {\n const textoUbicacion = normalizarPlano(valorUbicacion);\n\n if (!textoUbicacion) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bVERSIÓN\\b/,\n /\\bCODIGO\\b/,\n /\\bCÓDIGO\\b/,\n /\\bGUATE WMC\\b/,\n /\\bPOLLO CAMPERO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(textoUbicacion));\n};\n\nconst normalizarUbicacion = (valorUbicacion) => {\n const textoUbicacion = limpiar(valorUbicacion);\n\n if (esNoIdentificado(textoUbicacion)) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(textoUbicacion)) {\n return 'No identificado';\n }\n\n return textoUbicacion;\n};\n\nconst normalizarFechaEjecucion = (valorFecha) => {\n const textoFecha = limpiar(valorFecha);\n\n if (esNoIdentificado(textoFecha)) return 'No identificado';\n\n // Si solo viene el año, no es una fecha de ejecución real.\n if (/^20\\d{2}$/.test(textoFecha)) {\n return 'No identificado';\n }\n\n return textoFecha;\n};\n\nconst limitarTexto = (valorTexto, max = 1200, fallback = '') => {\n const textoLimpio = valor(valorTexto, fallback);\n\n if (!textoLimpio) return '';\n\n return textoLimpio.length > max\n ? `${textoLimpio.slice(0, max - 3)}...`\n : textoLimpio;\n};\n\nconst ahora = new Date().toISOString();\n\nconst tipoReporte = normalizarTipoReporte(parsed.tipo_reporte);\nconst tipoReporteConfianza = normalizarConfianza(parsed.tipo_reporte_confianza);\nconst tipoReporteMotivo = valor(parsed.tipo_reporte_motivo, 'No identificado');\n\nconst requiereRevision = normalizarSiNo(parsed.requiere_revision, 'SI');\n\nconst estadoRevision = requiereRevision === 'SI'\n ? 'PENDIENTE_REVISION'\n : 'ANALISIS_COMPLETADO';\n\nconst propuestaReferenciaOriginal = valor(parsed.propuesta_referencia);\nconst propuestaReferenciaLimpia = limpiarReferenciaVisual(propuestaReferenciaOriginal);\n\nconst fechaEjecucionOriginal = valor(parsed.fecha_ejecucion);\nconst fechaEjecucionLimpia = normalizarFechaEjecucion(fechaEjecucionOriginal);\n\nconst ubicacionOriginal = valor(parsed.ubicacion);\nconst ubicacionLimpia = normalizarUbicacion(ubicacionOriginal);\n\nconst evidenciaAudio = valorOpcional(parsed.evidencia_audio_resumen || base.texto_audio);\nconst evidenciaImagenes = valorOpcional(parsed.evidencia_imagenes_resumen || base.texto_imagenes);\nconst evidenciaVideos = valorOpcional(parsed.evidencia_videos_resumen || base.texto_videos);\n\nconst elementosDetectados = [\n evidenciaImagenes,\n evidenciaVideos\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .join('\\n\\n');\n\nconst tagsSet = new Set();\n\n[\n parsed.marca,\n parsed.cliente,\n parsed.pais,\n ubicacionLimpia\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .forEach(v => tagsSet.add(v));\n\nconst tags = [...tagsSet].join(', ');\n\nconst resumenIa = limitarTexto(parsed.resumen_ejecucion, 1200, '');\nconst descripcionEjecucion = limitarTexto(parsed.que_se_implemento, 1200, 'No identificado');\nconst comentariosResultados = limitarTexto(parsed.comentarios_resultados, 900, '');\n\nreturn [\n {\n json: {\n ...base,\n\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n session_id: base.session_id || '',\n\n fecha_recepcion: base.fecha_recepcion || ahora,\n fecha_ejecucion: fechaEjecucionLimpia,\n fecha_ejecucion_original_ia: fechaEjecucionOriginal,\n\n manager_nombre: valor(base.manager_nombre),\n manager_telefono: valor(base.manager_telefono),\n canal_origen: base.canal_origen || 'WHATSAPP',\n\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza,\n tipo_reporte_motivo: tipoReporteMotivo,\n\n propuesta_referencia: propuestaReferenciaLimpia,\n propuesta_referencia_original_ia: propuestaReferenciaOriginal,\n\n propuesta_match_estado: 'PENDIENTE',\n propuesta_match_confianza: '',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_match_revision: 'PENDIENTE_MATCH_BANCO',\n\n marca: valor(parsed.marca),\n cliente: valor(parsed.cliente),\n pais: valor(parsed.pais),\n ubicacion: ubicacionLimpia,\n ubicacion_original_ia: ubicacionOriginal,\n\n comentario_original: evidenciaAudio || comentariosResultados || '',\n resumen_ia: resumenIa,\n descripcion_ejecucion: descripcionEjecucion,\n comentarios_resultados: comentariosResultados,\n elementos_detectados: elementosDetectados,\n tags,\n\n media_folder_url: base.media_folder_url || '',\n presentacion_ejecucion_url: '',\n\n fotos_count: base.total_analisis_imagenes || 0,\n videos_count: base.total_analisis_videos || 0,\n audios_count: base.total_analisis_audio || 0,\n\n estado_revision: estadoRevision,\n motivo_revision: valor(parsed.motivo_revision, ''),\n ultima_actualizacion: ahora,\n\n transcripcion_audio: evidenciaAudio,\n\n nivel_confianza: normalizarConfianza(parsed.nivel_confianza),\n requiere_revision: requiereRevision,\n\n gemini_json_raw: texto,\n\n normalizacion_json_final_debug: {\n propuesta_referencia_original: propuestaReferenciaOriginal,\n propuesta_referencia_limpia: propuestaReferenciaLimpia,\n fecha_ejecucion_original: fechaEjecucionOriginal,\n fecha_ejecucion_limpia: fechaEjecucionLimpia,\n ubicacion_original: ubicacionOriginal,\n ubicacion_limpia: ubicacionLimpia,\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 5376, + 2144 + ], + "id": "fb3640a6-3044-43a0-a3dc-21ea0221bc08", + "name": "Code - Normalizar JSON final propuesta ejecutada TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "fecha_ejecucion": "={{ $json.fecha_ejecucion }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "propuesta_referencia": "={{ $json.propuesta_referencia }}", + "propuesta_match_estado": "={{ $json.propuesta_match_estado }}", + "propuesta_match_confianza": "={{ $json.propuesta_match_confianza }}", + "propuesta_nombre_banco": "={{ $json.propuesta_nombre_banco }}", + "propuesta_link_banco": "={{ $json.propuesta_link_banco }}", + "marca": "={{ $json.marca }}", + "cliente": "={{ $json.cliente }}", + "pais": "={{ $json.pais }}", + "ubicacion": "={{ $json.ubicacion }}", + "comentario_original": "={{ $json.comentario_original }}", + "resumen_ia": "={{ $json.resumen_ia }}", + "descripcion_ejecucion": "={{ $json.descripcion_ejecucion }}", + "elementos_detectados": "={{ $json.elementos_detectados }}", + "tags": "={{ $json.tags }}", + "media_folder_url": "={{ $json.media_folder_url }}", + "presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}", + "fotos_count": "={{ $json.fotos_count }}", + "videos_count": "={{ $json.videos_count }}", + "audios_count": "={{ $json.audios_count }}", + "estado_revision": "={{ $json.estado_revision }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "ultima_actualizacion": "={{ $json.ultima_actualizacion }}", + "session_id": "={{ $json.session_id }}", + "canal_origen": "={{ $json.canal_origen }}", + "transcripcion_audio": "={{ $json.transcripcion_audio }}", + "propuesta_match_revision": "={{ $json.propuesta_match_revision }}", + "tipo_reporte": "={{ $json.tipo_reporte }}", + "tipo_reporte_confianza": "={{ $json.tipo_reporte_confianza }}", + "tipo_reporte_motivo": "={{ $json.tipo_reporte_motivo }}", + "decision_automatica_banco": "={{ $json.decision_automatica_banco }}", + "motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}", + "banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_ejecucion", + "displayName": "fecha_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_referencia", + "displayName": "propuesta_referencia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_estado", + "displayName": "propuesta_match_estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_confianza", + "displayName": "propuesta_match_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_nombre_banco", + "displayName": "propuesta_nombre_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_link_banco", + "displayName": "propuesta_link_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "marca", + "displayName": "marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "cliente", + "displayName": "cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "pais", + "displayName": "pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ubicacion", + "displayName": "ubicacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comentario_original", + "displayName": "comentario_original", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "resumen_ia", + "displayName": "resumen_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "descripcion_ejecucion", + "displayName": "descripcion_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "elementos_detectados", + "displayName": "elementos_detectados", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tags", + "displayName": "tags", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_folder_url", + "displayName": "media_folder_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "presentacion_ejecucion_url", + "displayName": "presentacion_ejecucion_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fotos_count", + "displayName": "fotos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audios_count", + "displayName": "audios_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado_revision", + "displayName": "estado_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "transcripcion_audio", + "displayName": "transcripcion_audio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_revision", + "displayName": "propuesta_match_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_confianza", + "displayName": "tipo_reporte_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_motivo", + "displayName": "tipo_reporte_motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "decision_automatica_banco", + "displayName": "decision_automatica_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo_decision_automatica", + "displayName": "motivo_decision_automatica", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "banco_actualizado_auto", + "displayName": "banco_actualizado_auto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 7120, + 2224 + ], + "id": "d69911ec-0d8c-478d-b88b-740b7d56c8d1", + "name": "Sheets - Guardar propuesta ejecutada final TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ new Date().toISOString() }}", + "etapa": "={{ 'COMPLETADO' }}", + "estado": "={{ 'ANALISIS_COMPLETADO' }}", + "motivo_revision": "={{ $json.motivo_revision || 'ANALISIS_COMPLETADO' }}", + "tipo_reporte": "={{ $json.tipo_reporte }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 14448, + 2112 + ], + "id": "f420de04-b294-4232-aebd-fd1ac4346058", + "name": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst normalizado = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst presentacion = getNodeJson('Code - Normalizar link presentación ejecución TEST');\nconst basePaso = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst actual = $json || {};\n\nlet data = {\n ...basePaso,\n ...normalizado,\n ...actual,\n ...presentacion,\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst destinoBase = limpiar(\n data.whatsapp_to ||\n data.group_jid ||\n data.whatsapp_remote_jid ||\n data.sesion_activa?.group_jid ||\n data.sesion_activa?.whatsapp_to ||\n data.sesion_activa?.whatsapp_remote_jid ||\n data.manager_telefono ||\n data.sender_phone ||\n ''\n);\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar mensaje final.');\n}\n\nlet whatsappTo = destinoBase;\n\n// IMPORTANTE:\n// Si es grupo, conservar @g.us.\n// No convertirlo a @s.whatsapp.net.\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nconst presentacionUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n ''\n);\n\nconst bloquePresentacion = presentacionUrl\n ? `\\n\\nPresentación:\\n${presentacionUrl}`\n : '';\n\nconst idioma = limpiar(\n data.idioma_flujo ||\n data.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet whatsappText = '';\n\nif (idioma === 'EN') {\n whatsappText = `✅ Report received and processed successfully.\n\nYour evidence was registered in the proposal evidence bank.${bloquePresentacion}\n\nThank you for completing the report.`;\n} else {\n whatsappText = `✅ Reporte recibido y procesado correctamente.\n\nTu evidencia fue registrada en el banco de propuestas.${bloquePresentacion}\n\nGracias por completar el reporte.`;\n}\n\nreturn [\n {\n json: {\n ...data,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: whatsappText,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 14736, + 2080 + ], + "id": "bcb3c93a-9807-450d-999d-a6cbccf497c7", + "name": "Code - Preparar mensaje final WhatsApp TEST" + }, + { + "parameters": { + "amount": 7 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 3744, + 2112 + ], + "id": "fa6815ea-190b-441f-99a3-ee23c379e9b9", + "name": "Wait", + "webhookId": "3d858751-a035-4915-8ee0-caf8f19c78a5" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 6336, + 2176 + ], + "id": "89509a04-841b-4300-a100-1fa9464f6017", + "name": "Sheets - Leer propuestas ejecutadas final TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const base = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\nconst rows = $input.all().map(item => item.json || {});\n\nconst sessionId = String(base.session_id || '').trim();\nconst ejecucionId = String(base.ejecucion_id || '').trim();\n\nconst existente = rows.find(row => {\n const rowSessionId = String(row.session_id || '').trim();\n const rowEjecucionId = String(row.ejecucion_id || '').trim();\n\n return (\n (sessionId && rowSessionId === sessionId) ||\n (ejecucionId && rowEjecucionId === ejecucionId)\n );\n});\n\nreturn [\n {\n json: {\n ...base,\n\n propuesta_final_ya_existe: Boolean(existente),\n propuesta_final_row_number: existente?.row_number || '',\n propuesta_final_existente: existente || null,\n\n estado_anti_duplicado: existente\n ? 'PROPUESTA_FINAL_YA_EXISTE'\n : 'PROPUESTA_FINAL_NUEVA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6544, + 2176 + ], + "id": "52a97f82-0554-4fae-b744-4588a07f7d60", + "name": "Code - Verificar duplicado propuesta ejecutada TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "80785332-9e50-44d5-9fd5-ecf9610876fd", + "leftValue": "={{ $json.propuesta_final_ya_existe === true }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 6752, + 2176 + ], + "id": "6afd6903-aac3-42bd-b0e4-583a43f92a68", + "name": "IF - Propuesta final ya existe TEST" + }, + { + "parameters": { + "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '10PZtoMFuiXczGM9Itbcjce4EKBUhiztFbXZiTWX2DKc';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7328, + 2192 + ], + "id": "1edd49c5-4078-4e39-b68e-56798507020d", + "name": "Code - Preparar copia presentación ejecución TEST" + }, + { + "parameters": { + "operation": "copy", + "fileId": { + "__rl": true, + "value": "={{ $json.slides_template_id }}", + "mode": "id" + }, + "name": "={{ $json.presentation_name }}", + "sameFolder": false, + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "={{ $json.execution_folder_id }}", + "mode": "id" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 8880, + 2192 + ], + "id": "d9274651-56f7-49d6-a4fe-ae4c05386bb9", + "name": "Drive - Copiar plantilla presentación ejecución TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const base = $('Code - Preparar copia presentación ejecución TEST').first().json || {};\nconst drive = $json || {};\n\nconst presentationId =\n drive.id ||\n drive.fileId ||\n drive.presentationId ||\n drive.data?.id ||\n '';\n\nif (!presentationId) {\n throw new Error('No se encontró el ID de la presentación copiada.');\n}\n\nconst presentationUrl =\n drive.webViewLink ||\n drive.webUrl ||\n `https://docs.google.com/presentation/d/${presentationId}/edit`;\n\nreturn [\n {\n json: {\n ...base,\n\n presentation_id: presentationId,\n presentacion_ejecucion_url: presentationUrl,\n\n estado_presentacion: 'PRESENTACION_CREADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 9152, + 2192 + ], + "id": "8195f1d0-b75f-4335-a6a4-84c66875c9d6", + "name": "Code - Normalizar link presentación ejecución TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}", + "ultima_actualizacion": "={{ new Date().toISOString() }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "session_id": "={{ $json.session_id }}", + "media_folder_url": "={{ $json.media_folder_url }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_ejecucion", + "displayName": "fecha_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_referencia", + "displayName": "propuesta_referencia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_estado", + "displayName": "propuesta_match_estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_confianza", + "displayName": "propuesta_match_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_nombre_banco", + "displayName": "propuesta_nombre_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_link_banco", + "displayName": "propuesta_link_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "marca", + "displayName": "marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "cliente", + "displayName": "cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "pais", + "displayName": "pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ubicacion", + "displayName": "ubicacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comentario_original", + "displayName": "comentario_original", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "resumen_ia", + "displayName": "resumen_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "descripcion_ejecucion", + "displayName": "descripcion_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "elementos_detectados", + "displayName": "elementos_detectados", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tags", + "displayName": "tags", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_folder_url", + "displayName": "media_folder_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "presentacion_ejecucion_url", + "displayName": "presentacion_ejecucion_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fotos_count", + "displayName": "fotos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audios_count", + "displayName": "audios_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado_revision", + "displayName": "estado_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "transcripcion_audio", + "displayName": "transcripcion_audio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_revision", + "displayName": "propuesta_match_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_confianza", + "displayName": "tipo_reporte_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_motivo", + "displayName": "tipo_reporte_motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "decision_automatica_banco", + "displayName": "decision_automatica_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo_decision_automatica", + "displayName": "motivo_decision_automatica", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "banco_actualizado_auto", + "displayName": "banco_actualizado_auto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 12368, + 2048 + ], + "id": "2bbb6c3f-2aa6-4093-9312-ac01e02dd67c", + "name": "Sheets - Actualizar link presentación ejecución TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst carpetaPreparada = getNodeJson('Code - Preparar carpeta ejecución TEST');\nconst carpetaDrive = getNodeJson('Drive - Crear carpeta ejecución TEST');\nconst carpetaNormalizada = getNodeJson('Code - Normalizar carpeta ejecución TEST');\n\nconst copiaPresentacion = getNodeJson('Drive - Copiar plantilla presentación ejecución TEST');\nconst linkPresentacionNormalizado = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...carpetaPreparada,\n ...carpetaDrive,\n ...carpetaNormalizada,\n ...copiaPresentacion,\n ...linkPresentacionNormalizado,\n ...actual,\n ...match,\n ...enriquecido\n};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = limpiar(valor);\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n if (texto.toLowerCase() === 'no disponible') return fallback;\n\n return texto;\n};\n\nconst cortar = (valor, max = 900) => {\n const texto = limpiarTexto(valor, 'No disponible');\n return texto.length > max ? texto.slice(0, max - 3) + '...' : texto;\n};\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n ''\n);\n\nconst matchConfianza = Number(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n 0\n);\n\nconst matchAlta =\n matchEstado === 'MATCH_ALTA_CONFIANZA' ||\n matchConfianza >= 85;\n\nconst propuestaNombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n ''\n);\n\nconst propuestaReferenciaEnriquecida = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n jsonFinal.propuesta_referencia ||\n ''\n);\n\nconst propuestaReferenciaSlides = limpiarReferenciaVisual(\n matchAlta && propuestaNombreBanco\n ? propuestaNombreBanco\n : propuestaReferenciaEnriquecida\n);\n\nconst ubicacionSlides = limpiarTexto(\n enriquecido.ubicacion ||\n data.ubicacion ||\n jsonFinal.ubicacion,\n 'No identificado'\n);\n\nconst fechaEjecucionSlides = limpiarTexto(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n jsonFinal.fecha_ejecucion,\n 'No identificado'\n);\n\n// --------------------------------------------------\n// Resolver carpeta de evidencias\n// --------------------------------------------------\n\nconst mediaFolderId = limpiar(\n data.media_folder_id ||\n data.execution_folder_id ||\n data.folder_id ||\n data.id ||\n carpetaNormalizada.media_folder_id ||\n carpetaNormalizada.execution_folder_id ||\n carpetaNormalizada.folder_id ||\n carpetaDrive.id ||\n ''\n);\n\nlet mediaFolderUrl = limpiar(\n data.media_folder_url ||\n data.execution_folder_url ||\n data.folder_url ||\n carpetaNormalizada.media_folder_url ||\n carpetaNormalizada.execution_folder_url ||\n carpetaNormalizada.folder_url ||\n carpetaDrive.webViewLink ||\n ''\n);\n\nif (!mediaFolderUrl && mediaFolderId) {\n mediaFolderUrl = `https://drive.google.com/drive/folders/${mediaFolderId}`;\n}\n\n// --------------------------------------------------\n// Resolver link de presentación\n// --------------------------------------------------\n\nconst presentationId = limpiar(\n data.presentation_id ||\n data.presentacion_id ||\n linkPresentacionNormalizado.presentation_id ||\n copiaPresentacion.id ||\n actual.presentation_id ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n data.presentationUrl ||\n data.link_presentacion ||\n linkPresentacionNormalizado.presentacion_ejecucion_url ||\n linkPresentacionNormalizado.presentation_url ||\n linkPresentacionNormalizado.webViewLink ||\n copiaPresentacion.webViewLink ||\n actual.presentacion_ejecucion_url ||\n actual.presentation_url ||\n actual.webViewLink ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\nif (!presentationId) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nconst 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\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 9360, + 2192 + ], + "id": "192b9018-dba7-4b23-9c7c-6503704186a6", + "name": "Code - Preparar reemplazos Slides ejecución TEST" + }, + { + "parameters": { + "method": "POST", + "url": "={{ 'https://slides.googleapis.com/v1/presentations/' + $json.presentation_id + ':batchUpdate' }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleOAuth2Api", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { requests: $json.slides_replace_requests } }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 9632, + 2128 + ], + "id": "3fd9c926-7369-4d05-a470-827e813a4d76", + "name": "HTTP Request - Reemplazar textos Slides ejecución TEST", + "credentials": { + "googleOAuth2Api": { + "id": "eHseMeH39kRcXgOF", + "name": "Google account 2" + } + } + }, + { + "parameters": { + "jsCode": "const base = $('Code - Preparar reemplazos Slides ejecución TEST').first().json || {};\nconst respuestaSlides = $json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n slides_batch_update_response: respuestaSlides,\n estado_presentacion: 'TEXTOS_SLIDES_REEMPLAZADOS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 10240, + 2128 + ], + "id": "47ecd981-3ce3-4462-87f5-fc4e3d5d628c", + "name": "Code - Normalizar respuesta Slides ejecución TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver sesión WhatsApp TEST');\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = limpiar(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n ''\n);\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar mensaje de cancelación sin sesión.');\n}\n\nlet whatsappTo = destinoBase;\n\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n 'ℹ️ *There is no active report to cancel.*',\n '',\n 'You can start a new evidence registration whenever you are ready.',\n '',\n 'Write:',\n '',\n '*Hey*'\n ].join('\\n');\n} else {\n mensaje = [\n 'ℹ️ *No hay un reporte activo para cancelar.*',\n '',\n 'Puedes iniciar un nuevo registro de evidencias cuando estés listo.',\n '',\n 'Escribe:',\n '',\n '*Hey*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4832, + -3792 + ], + "id": "3247cab6-8e96-4c06-a7f2-9f504d6e5005", + "name": "Code - Preparar sin sesión para cancelar WhatsApp TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3712, + -1168 + ], + "id": "41f82dcf-7d4b-4c40-b3a9-98a03a322dcf", + "name": "Sheets - Leer eventos sesión fotos WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = String(valor ?? '').trim();\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n\n return texto;\n};\n\nconst limpiarNombreArchivo = (valor) => {\n return limpiarTexto(valor)\n .replace(/[\\\\/:*?\"<>|#%{}~&]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 120);\n};\n\nconst fechaBase = limpiarTexto(\n data.fecha_ejecucion ||\n data.fecha_recepcion ||\n data.ultima_actualizacion ||\n new Date().toISOString()\n);\n\nconst fecha = fechaBase.slice(0, 10);\n\nconst marca = limpiarNombreArchivo(data.marca);\nconst cliente = limpiarNombreArchivo(data.cliente);\nconst sessionId = limpiarNombreArchivo(data.session_id || data.ejecucion_id);\n\nconst folderName = `${fecha} - ${marca} - ${cliente} - ${sessionId}`;\n\n// Carpeta base PROPUESTAS EJECUTADAS\nconst parentFolderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nreturn [\n {\n json: {\n ...data,\n\n execution_folder_name: folderName,\n execution_parent_folder_id: parentFolderId\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7536, + 2192 + ], + "id": "07051572-ab5b-4482-957d-da55178ec248", + "name": "Code - Preparar carpeta ejecución TEST" + }, + { + "parameters": { + "resource": "folder", + "name": "={{ $json.execution_folder_name }}", + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "={{ $json.execution_parent_folder_id }}", + "mode": "id" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 7744, + 2192 + ], + "id": "c46d6636-05b2-4cd2-9db0-4cc8270786ac", + "name": "Drive - Crear carpeta ejecución TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const carpeta = $json || {};\nconst base = $('Code - Preparar carpeta ejecución TEST').first().json || {};\n\nconst folderId =\n carpeta.id ||\n carpeta.fileId ||\n carpeta.folderId ||\n '';\n\nconst folderUrl = folderId\n ? `https://drive.google.com/drive/folders/${folderId}`\n : '';\n\nif (!folderId) {\n throw new Error('No se pudo obtener el ID de la carpeta de ejecución.');\n}\n\nreturn [\n {\n json: {\n ...base,\n\n execution_folder_id: folderId,\n execution_folder_url: folderUrl,\n\n media_folder_id: folderId,\n media_folder_url: folderUrl\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7952, + 2192 + ], + "id": "78fded8e-3963-458f-bafe-001eaf553022", + "name": "Code - Normalizar carpeta ejecución TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst folderId = texto(data.execution_folder_id || data.media_folder_id);\n\nif (!folderId) {\n throw new Error('No existe execution_folder_id/media_folder_id para mover la multimedia.');\n}\n\nconst archivos = [];\nconst vistos = new Set();\n\nconst extraerFileIdDesdeUrl = (valor) => {\n const url = texto(valor);\n if (!url) return '';\n\n // Formato: https://drive.google.com/file/d/FILE_ID/view\n const matchFile = url.match(/\\/file\\/d\\/([^/]+)/);\n if (matchFile?.[1]) return matchFile[1];\n\n // Formato: https://drive.google.com/open?id=FILE_ID\n const matchOpen = url.match(/[?&]id=([^&]+)/);\n if (matchOpen?.[1]) return matchOpen[1];\n\n // Si ya viene como ID limpio\n if (!url.includes('http') && url.length > 15) return url;\n\n return '';\n};\n\nconst separarLista = (valor) => {\n return texto(valor)\n .split(/[\\n,]+/)\n .map(v => texto(v))\n .filter(Boolean);\n};\n\nconst agregarArchivo = (fileIdOrUrl, tipo) => {\n const fileId = extraerFileIdDesdeUrl(fileIdOrUrl);\n\n if (!fileId) return;\n if (vistos.has(fileId)) return;\n\n vistos.add(fileId);\n\n archivos.push({\n json: {\n ...data,\n\n move_file_id: fileId,\n move_file_type: tipo,\n\n execution_folder_id: folderId,\n media_folder_id: folderId,\n media_folder_url: data.media_folder_url,\n execution_folder_url: data.execution_folder_url\n }\n });\n};\n\n// 1. Archivo actual del item, si existe\nagregarArchivo(data.drive_file_id, data.media_type || 'media');\n\n// 2. URLs acumuladas por tipo\nfor (const url of separarLista(data.audio_drive_urls)) {\n agregarArchivo(url, 'audio');\n}\n\nfor (const url of separarLista(data.imagenes_drive_urls)) {\n agregarArchivo(url, 'image');\n}\n\nfor (const url of separarLista(data.videos_drive_urls)) {\n agregarArchivo(url, 'video');\n}\n\n// 3. Si tienes otros campos futuros\nfor (const url of separarLista(data.media_drive_urls)) {\n agregarArchivo(url, 'media');\n}\n\nif (archivos.length === 0) {\n return [\n {\n json: {\n ...data,\n media_move_status: 'SIN_ARCHIVOS_PARA_MOVER',\n media_move_debug: {\n drive_file_id: data.drive_file_id || '',\n audio_drive_urls: data.audio_drive_urls || '',\n imagenes_drive_urls: data.imagenes_drive_urls || '',\n videos_drive_urls: data.videos_drive_urls || ''\n }\n }\n }\n ];\n}\n\nreturn archivos;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 8160, + 2192 + ], + "id": "a8b7b615-fb63-4e02-9b80-31efb5109ad8", + "name": "Code - Preparar media para mover carpeta TEST" + }, + { + "parameters": { + "operation": "move", + "fileId": { + "__rl": true, + "value": "={{ $json.move_file_id }}", + "mode": "id" + }, + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "={{ $json.execution_folder_id }}", + "mode": "id" + } + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 8368, + 2192 + ], + "id": "7797be31-dc35-4c58-b38e-224c0449c435", + "name": "Drive - Mover media a carpeta ejecución TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const movido = $json || {};\nconst base = $('Code - Preparar media para mover carpeta TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n media_move_status: 'MEDIA_MOVIDA_A_CARPETA',\n moved_file_id: base.move_file_id,\n moved_file_type: base.move_file_type,\n moved_file_url: base.move_file_url,\n\n drive_move_response_id: movido.id || movido.fileId || base.move_file_id\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 8576, + 2192 + ], + "id": "ff7562ea-6586-4dbf-955e-8b4ef4061a14", + "name": "Code - Confirmar media movida carpeta TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Preparar reemplazos Slides ejecución TEST');\n\nconst data = {\n ...base,\n ...actual,\n\n // Aseguramos estos campos desde el nodo base si el nodo Slides no los devuelve\n presentation_id: actual.presentation_id || base.presentation_id,\n imagenes_drive_urls: actual.imagenes_drive_urls || base.imagenes_drive_urls,\n imagen_drive_urls: actual.imagen_drive_urls || base.imagen_drive_urls,\n media_drive_urls: actual.media_drive_urls || base.media_drive_urls,\n};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst extraerFileIdDesdeUrl = (valor) => {\n const url = texto(valor);\n if (!url) return '';\n\n const matchFile = url.match(/\\/file\\/d\\/([^/]+)/);\n if (matchFile?.[1]) return matchFile[1];\n\n const matchOpen = url.match(/[?&]id=([^&]+)/);\n if (matchOpen?.[1]) return matchOpen[1];\n\n if (!url.includes('http') && url.length > 15) return url;\n\n return '';\n};\n\nconst separarLista = (valor) => {\n return texto(valor)\n .split(/[\\n,]+/)\n .map(v => texto(v))\n .filter(Boolean);\n};\n\nconst imagenesUrls = separarLista(\n data.imagenes_drive_urls ||\n data.imagen_drive_urls ||\n data.media_drive_urls ||\n ''\n);\n\nconst imageFileIds = imagenesUrls\n .map(extraerFileIdDesdeUrl)\n .filter(Boolean)\n .slice(0, 25)\n\nif (!data.presentation_id) {\n throw new Error('No llegó presentation_id para insertar imágenes en Slides.');\n}\n\nreturn [\n {\n json: {\n ...data,\n\n apps_script_insert_images_payload: {\n secret: 'glm_fulgencio_slides_2026_seguro',\n presentation_id: data.presentation_id,\n image_file_ids: imageFileIds\n },\n\n imagenes_para_slides_count: imageFileIds.length,\n imagenes_para_slides_ids: imageFileIds,\n\n estado_insertar_imagenes: 'PAYLOAD_INSERTAR_IMAGENES_PREPARADO'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 9840, + 2128 + ], + "id": "93846b66-2092-43ec-b57d-1347e3947c42", + "name": "Code - Preparar payload insertar imágenes Slides TEST" + }, + { + "parameters": { + "method": "POST", + "url": "https://script.google.com/macros/s/AKfycbyw76HYjYMvq0KXA9IN5S8nRWk0drPkjKSK2SAbCL9Ha8is3vFZMyR6Ldby4c3YVs7J/exec", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.apps_script_insert_images_payload }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 10048, + 2128 + ], + "id": "573fc25d-7dae-42e1-8025-1e38d3394c17", + "name": "HTTP - Insertar imágenes en Slides TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n actual.whatsapp_to ||\n actual.group_jid ||\n actual.whatsapp_remote_jid ||\n base.whatsapp_to ||\n base.group_jid ||\n base.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar aviso de procesamiento.');\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '⏳ *Report received.*',\n '',\n 'We are now processing your evidence.',\n '',\n 'This may take a moment while we analyze the media, organize the files and generate the presentation.',\n '',\n 'I will send you the final link when everything is ready.'\n ].join('\\n');\n} else {\n mensaje = [\n '⏳ *Reporte recibido.*',\n '',\n 'Estamos procesando tus evidencias.',\n '',\n 'Esto puede tomar un momento mientras analizamos los archivos, organizamos la carpeta y generamos la presentación.',\n '',\n 'Te enviaré el link final cuando todo esté listo.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje,\n\n aviso_procesando_preparado: true\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5824, + 1072 + ], + "id": "65715659-8334-4447-9e92-8aa372ed345b", + "name": "Code - Preparar aviso procesando WhatsApp TEST1" + }, + { + "parameters": { + "jsCode": "const respuesta = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nlet base =\n getNodeJson('Code - Preparar aviso procesando WhatsApp TEST') ||\n getNodeJson('Code - Preparar aviso procesando WhatsApp TEST1') ||\n {};\n\nif (!base || Object.keys(base).length === 0) {\n base = getNodeJson('Code - Resolver paso activo WhatsApp TEST') || {};\n}\n\nreturn [\n {\n json: {\n ...base,\n\n aviso_procesando_enviado: true,\n aviso_procesando_response: respuesta\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5376, + 1072 + ], + "id": "e9bac53c-7375-4375-830c-dee36d28b1e4", + "name": "Code - Restaurar contexto aviso procesando TEST" + }, + { + "parameters": { + "method": "POST", + "url": "={{'https://wsp.gomezleemarketing.com'}}/message/sendText/{{'botsoporte'}}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "={{'C267126ABB45-4C12-B626-6BAB1833F5D7'}}" + } + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "number", + "value": "={{ (() => {\n const limpiar = (valor) => String(valor ?? '').trim();\n\n const candidatos = [\n $json.whatsapp_to,\n $json.group_jid,\n $json.whatsapp_remote_jid,\n $json.sesion_activa?.group_jid,\n $json.sesion_activa?.whatsapp_to,\n $json.sesion_activa?.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ];\n\n let raw = limpiar(candidatos.find(v => limpiar(v)));\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n // Si ya viene como grupo, conservarlo.\n if (raw.includes('@g.us')) {\n return raw;\n }\n\n // Si viene como usuario WhatsApp, validar si realmente era un grupo.\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n // Los grupos de WhatsApp suelen venir como 120363...\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n})() }}" + }, + { + "name": "delay", + "value": "={{ 1000 }}" + }, + { + "name": "text", + "value": "={{ $json.whatsapp_text }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + -5600, + 1072 + ], + "id": "bafc62ac-98e2-476f-ac16-0aa906c03f33", + "name": "HTTP - Enviar aviso procesando WhatsApp TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "options": { + "dataLocationOnSheet": { + "values": { + "rangeDefinition": "detectAutomatically" + } + }, + "outputFormatting": { + "values": { + "general": "UNFORMATTED_VALUE", + "date": "FORMATTED_STRING" + } + }, + "returnFirstMatch": false + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 5584, + 2144 + ], + "id": "9bbb9e2c-1e27-4911-a836-e392c7e523b3", + "name": "Sheets - Leer banco propuestas Fulgencio TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst propuesta =\n getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST') ||\n $json ||\n {};\n\nconst bancoRows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) => {\n return limpiar(valor)\n .replace(/\\+/g, ' mas ')\n .replace(/_/g, ' ')\n .replace(/-/g, ' ')\n .replace(/\\//g, ' ')\n .replace(/\\./g, ' ')\n .replace(/\\bGUATE\\b/gi, ' Guatemala ')\n .replace(/\\bGUA\\b/gi, ' Guatemala ')\n .replace(/\\bGT\\b/gi, ' Guatemala ')\n .replace(/\\bRD\\b/gi, ' Republica Dominicana ')\n .replace(/\\bREP DOM\\b/gi, ' Republica Dominicana ')\n .replace(/\\bREP DOMINICANA\\b/gi, ' Republica Dominicana ')\n .replace(/\\bDOMINICANA\\b/gi, ' Republica Dominicana ')\n .replace(/\\bCR\\b/gi, ' Costa Rica ')\n .replace(/\\bPAN\\b/gi, ' Panama ')\n .replace(/\\bPTY\\b/gi, ' Panama ')\n .replace(/\\bSV\\b/gi, ' El Salvador ')\n .replace(/\\bSALVADOR\\b/gi, ' El Salvador ')\n .replace(/\\bHND\\b/gi, ' Honduras ')\n .replace(/\\bNIC\\b/gi, ' Nicaragua ')\n .replace(/\\bWMC\\b/gi, ' WMC ')\n .replace(/\\bRTM\\b/gi, ' RTM ')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst tokens = (valor) => {\n const stopwords = new Set([\n 'de', 'del', 'la', 'el', 'los', 'las', 'y', 'en', 'para', 'por', 'con',\n 'una', 'un', 'uno', 'dos', 'tres', 'the', 'of', 'and', 'for', 'to', 'a',\n 'propuesta', 'presentacion', 'presentación', 'ejecucion', 'ejecución',\n 'ejecutada', 'ejecutado', 'reporte', 'evidencia', 'version', 'versión',\n 'archivo', 'link', 'slide', 'slides', 'prueba'\n ]);\n\n return normalizar(valor)\n .split(' ')\n .map(t => t.trim())\n .filter(Boolean)\n .filter(t => t.length >= 3 || /^v\\d+$/i.test(t))\n .filter(t => !stopwords.has(t));\n};\n\nconst tokenSet = (valor) => new Set(tokens(valor));\n\nconst similitudTokens = (a, b) => {\n const ta = tokenSet(a);\n const tb = tokenSet(b);\n\n if (ta.size === 0 || tb.size === 0) return 0;\n\n let interseccion = 0;\n\n for (const token of ta) {\n if (tb.has(token)) interseccion++;\n }\n\n const union = new Set([...ta, ...tb]).size;\n\n return union === 0 ? 0 : interseccion / union;\n};\n\nconst coberturaTokens = (textoBase, textoContra) => {\n const base = [...tokenSet(textoBase)];\n const contra = tokenSet(textoContra);\n\n if (base.length === 0) return 0;\n\n const encontrados = base.filter(t => contra.has(t));\n\n return encontrados.length / base.length;\n};\n\nconst contarTokensComunes = (a, b) => {\n const ta = tokenSet(a);\n const tb = tokenSet(b);\n\n let comunes = 0;\n\n for (const token of ta) {\n if (tb.has(token)) comunes++;\n }\n\n return comunes;\n};\n\nconst obtenerCampo = (row, posiblesNombres) => {\n const keys = Object.keys(row || {});\n\n for (const nombre of posiblesNombres) {\n const exacto = keys.find(k => normalizar(k) === normalizar(nombre));\n if (exacto && limpiar(row[exacto])) return limpiar(row[exacto]);\n }\n\n for (const nombre of posiblesNombres) {\n const parcial = keys.find(k => normalizar(k).includes(normalizar(nombre)));\n if (parcial && limpiar(row[parcial])) return limpiar(row[parcial]);\n }\n\n return '';\n};\n\nconst extraerVersion = (valor) => {\n const texto = normalizar(valor);\n const match = texto.match(/\\bv\\d+\\b/);\n return match ? match[0].toUpperCase() : '';\n};\n\nconst extraerAnio = (valor) => {\n const texto = limpiar(valor);\n const match = texto.match(/\\b20\\d{2}\\b/);\n return match ? match[0] : '';\n};\n\nconst contienePais = (texto, pais) => {\n const t = normalizar(texto);\n const p = normalizar(pais);\n\n if (!p) return false;\n\n if (t.includes(p)) return true;\n\n if (p.includes('guatemala') && t.includes('guatemala')) return true;\n if (p.includes('republica dominicana') && t.includes('republica dominicana')) return true;\n if (p.includes('costa rica') && t.includes('costa rica')) return true;\n if (p.includes('panama') && t.includes('panama')) return true;\n if (p.includes('salvador') && t.includes('salvador')) return true;\n if (p.includes('honduras') && t.includes('honduras')) return true;\n if (p.includes('nicaragua') && t.includes('nicaragua')) return true;\n\n return false;\n};\n\nconst tipoReporte = limpiar(propuesta.tipo_reporte).toUpperCase();\n\nif (tipoReporte !== 'PROPUESTA_EJECUTADA') {\n return [\n {\n json: {\n ...propuesta,\n\n propuesta_match_estado:\n tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'NO_APLICA_PROPUESTA_EXTERNA'\n : 'NO_APLICA_NO_DETERMINADO',\n\n propuesta_match_confianza: '0',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_banco_row_number: '',\n propuesta_banco_enlaces_existentes: '',\n propuesta_banco_file_id: '',\n\n propuesta_match_revision:\n tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'NO_REQUIERE_MATCH_BANCO'\n : 'REQUIERE_REVISION_TIPO_NO_DETERMINADO',\n\n match_banco_debug: {\n motivo: 'No se ejecutó match porque el tipo_reporte no es PROPUESTA_EJECUTADA.',\n tipo_reporte: tipoReporte || 'NO_DETERMINADO'\n }\n }\n }\n ];\n}\n\nconst referencia = limpiar(propuesta.propuesta_referencia);\nconst marca = limpiar(propuesta.marca);\nconst cliente = limpiar(propuesta.cliente);\nconst pais = limpiar(propuesta.pais);\nconst ubicacion = limpiar(propuesta.ubicacion);\nconst descripcion = limpiar(propuesta.descripcion_ejecucion || propuesta.resumen_ia);\nconst elementosDetectados = limpiar(propuesta.elementos_detectados);\nconst comentarioOriginal = limpiar(propuesta.comentario_original);\nconst transcripcionAudio = limpiar(propuesta.transcripcion_audio);\nconst textoAudio = limpiar(propuesta.texto_audio);\nconst textoImagenes = limpiar(propuesta.texto_imagenes);\nconst tags = limpiar(propuesta.tags);\n\nconst textoPropuestaCompleto = [\n referencia,\n marca,\n cliente,\n pais,\n ubicacion,\n descripcion,\n elementosDetectados,\n comentarioOriginal,\n transcripcionAudio,\n textoAudio,\n textoImagenes,\n tags\n].filter(Boolean).join(' ');\n\nconst textoClavePropuesta = [\n referencia,\n marca,\n cliente,\n pais,\n elementosDetectados,\n textoImagenes,\n tags\n].filter(Boolean).join(' ');\n\nconst versionPropuesta = extraerVersion(textoPropuestaCompleto);\nconst anioPropuesta = extraerAnio(textoPropuestaCompleto);\n\nconst candidatos = [];\n\nfor (const [index, row] of bancoRows.entries()) {\n const bancoRowNumber = String(index + 2);\n\n const bancoNombre = obtenerCampo(row, [\n 'NOMBRE',\n 'propuesta',\n 'nombre propuesta',\n 'nombre de propuesta',\n 'titulo',\n 'título',\n 'nombre',\n 'referencia',\n 'brief',\n 'idea'\n ]);\n\n const bancoTipoAccion = obtenerCampo(row, [\n 'TIPO DE ACCION',\n 'TIPO DE ACCIÓN',\n 'tipo accion',\n 'tipo acción'\n ]);\n\n const bancoCliente = obtenerCampo(row, [\n 'CLIENTE',\n 'cliente',\n 'client'\n ]);\n\n const bancoMarca = obtenerCampo(row, [\n 'MARCA',\n 'marca',\n 'brand'\n ]);\n\n const bancoPais = obtenerCampo(row, [\n 'PAIS',\n 'PAÍS',\n 'pais',\n 'país',\n 'country'\n ]);\n\n const bancoCanal = obtenerCampo(row, [\n 'CANAL',\n 'canal'\n ]);\n\n const bancoAmbiente = obtenerCampo(row, [\n 'AMBIENTE DE COMPRA',\n 'ambiente de compra',\n 'ambiente'\n ]);\n\n const bancoDescripcion = obtenerCampo(row, [\n 'Descripcion',\n 'Descripción',\n 'descripcion',\n 'descripción',\n 'detalle',\n 'mecanica',\n 'mecánica',\n 'comentario',\n 'observacion',\n 'observación'\n ]);\n\n const bancoLink = obtenerCampo(row, [\n 'Enlace a la propuesta',\n 'link',\n 'enlace',\n 'url',\n 'brief link',\n 'link brief',\n 'presentacion',\n 'presentación',\n 'drive'\n ]);\n\n const bancoFileId = obtenerCampo(row, [\n 'file_id',\n 'File ID',\n 'archivo_id'\n ]);\n\n const bancoEnlacesEjecutadas = obtenerCampo(row, [\n 'Enlaces a propuestas ejecutadas',\n 'Enlace a propuestas ejecutadas',\n 'Links propuestas ejecutadas',\n 'Link propuestas ejecutadas',\n 'Ejecuciones',\n 'Propuestas ejecutadas'\n ]);\n\n if (!bancoNombre && !bancoMarca && !bancoCliente && !bancoDescripcion) {\n continue;\n }\n\n const textoBancoCompleto = [\n bancoNombre,\n bancoTipoAccion,\n bancoCliente,\n bancoMarca,\n bancoPais,\n bancoCanal,\n bancoAmbiente,\n bancoDescripcion\n ].filter(Boolean).join(' ');\n\n const textoClaveBanco = [\n bancoNombre,\n bancoCliente,\n bancoMarca,\n bancoPais\n ].filter(Boolean).join(' ');\n\n const scoreReferenciaNombre = similitudTokens(referencia, bancoNombre);\n const scoreEvidenciaNombre = similitudTokens(textoClavePropuesta, bancoNombre);\n const coberturaNombreEnEvidencia = coberturaTokens(bancoNombre, textoPropuestaCompleto);\n const coberturaReferenciaEnBanco = coberturaTokens(referencia, textoBancoCompleto);\n\n const scoreNombre = Math.max(\n scoreReferenciaNombre,\n scoreEvidenciaNombre,\n coberturaNombreEnEvidencia,\n coberturaReferenciaEnBanco\n );\n\n const scoreMarcaContraBanco = Math.max(\n similitudTokens(marca, bancoMarca),\n similitudTokens(marca, bancoNombre),\n similitudTokens(marca, textoBancoCompleto)\n );\n\n const scoreClienteContraBanco = Math.max(\n similitudTokens(cliente, bancoCliente),\n similitudTokens(cliente, bancoNombre),\n similitudTokens(cliente, textoBancoCompleto)\n );\n\n const scorePais = Math.max(\n similitudTokens(pais, bancoPais),\n contienePais(bancoNombre, pais) ? 1 : 0,\n contienePais(textoBancoCompleto, pais) ? 1 : 0\n );\n\n const scoreGeneral = similitudTokens(textoPropuestaCompleto, textoBancoCompleto);\n const scoreClave = similitudTokens(textoClavePropuesta, textoClaveBanco);\n const coberturaBancoEnPropuesta = coberturaTokens(bancoNombre, textoPropuestaCompleto);\n const comunesNombreEvidencia = contarTokensComunes(bancoNombre, textoPropuestaCompleto);\n\n const versionBanco = extraerVersion(textoBancoCompleto);\n const anioBanco = extraerAnio(textoBancoCompleto);\n\n const versionCoincide =\n versionPropuesta &&\n versionBanco &&\n versionPropuesta === versionBanco;\n\n const versionContradice =\n versionPropuesta &&\n versionBanco &&\n versionPropuesta !== versionBanco;\n\n const anioCoincide =\n anioPropuesta &&\n anioBanco &&\n anioPropuesta === anioBanco;\n\n const anioContradice =\n anioPropuesta &&\n anioBanco &&\n anioPropuesta !== anioBanco;\n\n const paisCoincide =\n scorePais >= 0.80 ||\n contienePais(bancoNombre, pais) ||\n contienePais(textoBancoCompleto, pais);\n\n const marcaCoincide =\n scoreMarcaContraBanco >= 0.45 ||\n normalizar(textoBancoCompleto).includes(normalizar(marca)) ||\n normalizar(textoPropuestaCompleto).includes(normalizar(bancoMarca));\n\n const scorePonderado =\n scoreNombre * 0.46 +\n scoreClave * 0.22 +\n scoreMarcaContraBanco * 0.08 +\n scoreClienteContraBanco * 0.04 +\n scorePais * 0.10 +\n scoreGeneral * 0.10;\n\n let scoreFinal = scorePonderado;\n let reglaMatch = 'PONDERADO';\n\n // Regla 1: el nombre/referencia de la propuesta está claramente contenido en la evidencia.\n if (coberturaBancoEnPropuesta >= 0.85 && (paisCoincide || anioCoincide || versionCoincide)) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.97 : 0.93);\n reglaMatch = versionCoincide\n ? 'NOMBRE_BANCO_EN_EVIDENCIA_CON_VERSION'\n : 'NOMBRE_BANCO_EN_EVIDENCIA';\n }\n\n // Regla 2: combinación fuerte de clave propuesta-banco.\n if (scoreClave >= 0.85 && (paisCoincide || anioCoincide || marcaCoincide)) {\n scoreFinal = Math.max(scoreFinal, 0.95);\n reglaMatch = 'CLAVE_COMPLETA_PROPUESTA_BANCO';\n } else if (scoreClave >= 0.75 && (paisCoincide || versionCoincide)) {\n scoreFinal = Math.max(scoreFinal, 0.90);\n reglaMatch = 'CLAVE_FUERTE_PROPUESTA_BANCO';\n }\n\n // Regla 3: referencia parcial + país + versión.\n if (scoreNombre >= 0.60 && paisCoincide && versionCoincide) {\n scoreFinal = Math.max(scoreFinal, 0.94);\n reglaMatch = 'REFERENCIA_PAIS_VERSION';\n }\n\n // Regla 4: muchos tokens comunes, aunque Gemini haya escrito el nombre distinto.\n if (comunesNombreEvidencia >= 5 && (paisCoincide || anioCoincide)) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.96 : 0.88);\n reglaMatch = versionCoincide\n ? 'TOKENS_COMUNES_PAIS_VERSION'\n : 'TOKENS_COMUNES_PAIS';\n }\n\n // Regla 5: para casos como RTM+ vs RTM más, GUATE vs Guatemala.\n if (\n normalizar(textoPropuestaCompleto).includes('rtm') &&\n normalizar(textoBancoCompleto).includes('rtm') &&\n normalizar(textoPropuestaCompleto).includes('wmc') &&\n normalizar(textoBancoCompleto).includes('wmc') &&\n (paisCoincide || normalizar(textoBancoCompleto).includes('guatemala')) &&\n (marcaCoincide || normalizar(textoBancoCompleto).includes('campero'))\n ) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.96 : 0.89);\n reglaMatch = versionCoincide\n ? 'RTM_WMC_MARCA_PAIS_VERSION'\n : 'RTM_WMC_MARCA_PAIS';\n }\n\n // Penalizaciones de seguridad.\n if (anioContradice) {\n scoreFinal = Math.min(scoreFinal, 0.59);\n reglaMatch = `${reglaMatch}_PENALIZADO_ANIO`;\n }\n\n if (versionContradice && scoreFinal >= 0.85) {\n scoreFinal = Math.min(scoreFinal, 0.79);\n reglaMatch = `${reglaMatch}_PENALIZADO_VERSION`;\n }\n\n // Si no coincide ni país, ni marca, ni año, no lo dejamos como alta confianza.\n if (\n scoreFinal >= 0.85 &&\n !paisCoincide &&\n !marcaCoincide &&\n !anioCoincide\n ) {\n scoreFinal = 0.74;\n reglaMatch = `${reglaMatch}_PENALIZADO_SIN_ANCLA`;\n }\n\n let bonusOrden = 0;\n\n if (versionCoincide) bonusOrden += 0.04;\n if (anioCoincide) bonusOrden += 0.02;\n if (paisCoincide) bonusOrden += 0.02;\n if (marcaCoincide) bonusOrden += 0.01;\n if (bancoFileId) bonusOrden += 0.005;\n\n candidatos.push({\n row,\n score: scoreFinal,\n score_orden: scoreFinal + bonusOrden,\n score_ponderado: scorePonderado,\n score_porcentaje: Math.round(scoreFinal * 100),\n\n score_nombre: Math.round(scoreNombre * 100),\n score_referencia_nombre: Math.round(scoreReferenciaNombre * 100),\n score_evidencia_nombre: Math.round(scoreEvidenciaNombre * 100),\n cobertura_nombre_en_evidencia: Math.round(coberturaNombreEnEvidencia * 100),\n cobertura_referencia_en_banco: Math.round(coberturaReferenciaEnBanco * 100),\n score_marca: Math.round(scoreMarcaContraBanco * 100),\n score_cliente: Math.round(scoreClienteContraBanco * 100),\n score_pais: Math.round(scorePais * 100),\n score_general: Math.round(scoreGeneral * 100),\n score_clave: Math.round(scoreClave * 100),\n cobertura_banco_en_propuesta: Math.round(coberturaBancoEnPropuesta * 100),\n comunes_nombre_evidencia: comunesNombreEvidencia,\n\n version_propuesta: versionPropuesta,\n version_banco: versionBanco,\n version_coincide: Boolean(versionCoincide),\n version_contradice: Boolean(versionContradice),\n\n anio_propuesta: anioPropuesta,\n anio_banco: anioBanco,\n anio_coincide: Boolean(anioCoincide),\n anio_contradice: Boolean(anioContradice),\n\n pais_coincide: Boolean(paisCoincide),\n marca_coincide: Boolean(marcaCoincide),\n\n regla_match: reglaMatch,\n\n banco_row_number: bancoRowNumber,\n banco_nombre: bancoNombre,\n banco_tipo_accion: bancoTipoAccion,\n banco_marca: bancoMarca,\n banco_cliente: bancoCliente,\n banco_pais: bancoPais,\n banco_canal: bancoCanal,\n banco_link: bancoLink,\n banco_file_id: bancoFileId,\n banco_enlaces_ejecutadas: bancoEnlacesEjecutadas\n });\n}\n\nconst candidatosValidos = candidatos\n .filter(c => c.score > 0)\n .sort((a, b) => {\n if (b.score_orden !== a.score_orden) return b.score_orden - a.score_orden;\n return Number(b.banco_row_number || 0) - Number(a.banco_row_number || 0);\n });\n\nconst mejor = candidatosValidos[0] || null;\nconst segundo = candidatosValidos[1] || null;\n\nlet propuestaMatchEstado = 'SIN_MATCH';\nlet propuestaMatchRevision = 'REQUIERE_REVISION_MANUAL';\nlet propuestaMatchConfianza = '0';\nlet propuestaNombreBanco = '';\nlet propuestaLinkBanco = '';\nlet propuestaBancoRowNumber = '';\nlet propuestaBancoEnlacesExistentes = '';\nlet propuestaBancoFileId = '';\n\nif (mejor) {\n let confianzaFinal = mejor.score_porcentaje;\n\n // Si el segundo está demasiado cerca, mantenemos match pero pedimos revisión.\n const segundoMuyCerca =\n segundo &&\n mejor.score_porcentaje >= 85 &&\n segundo.score_porcentaje >= 85 &&\n Math.abs(mejor.score_porcentaje - segundo.score_porcentaje) <= 2 &&\n normalizar(mejor.banco_nombre) !== normalizar(segundo.banco_nombre);\n\n propuestaNombreBanco = mejor.banco_nombre || 'No identificado';\n propuestaLinkBanco = mejor.banco_link || '';\n propuestaBancoRowNumber = mejor.banco_row_number || '';\n propuestaBancoEnlacesExistentes = mejor.banco_enlaces_ejecutadas || '';\n propuestaBancoFileId = mejor.banco_file_id || '';\n\n if (confianzaFinal >= 85 && !segundoMuyCerca) {\n propuestaMatchEstado = 'MATCH_ALTA_CONFIANZA';\n propuestaMatchRevision = 'NO_REQUIERE_REVISION';\n } else if (confianzaFinal >= 85 && segundoMuyCerca) {\n propuestaMatchEstado = 'MATCH_MEDIA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_VALIDACION_MANUAL';\n confianzaFinal = Math.min(confianzaFinal, 84);\n } else if (confianzaFinal >= 60) {\n propuestaMatchEstado = 'MATCH_MEDIA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_VALIDACION_MANUAL';\n } else {\n propuestaMatchEstado = 'MATCH_BAJA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_REVISION_MANUAL';\n }\n\n propuestaMatchConfianza = String(confianzaFinal);\n}\n\nreturn [\n {\n json: {\n ...propuesta,\n\n propuesta_match_estado: propuestaMatchEstado,\n propuesta_match_confianza: propuestaMatchConfianza,\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_link_banco: propuestaLinkBanco,\n propuesta_match_revision: propuestaMatchRevision,\n\n propuesta_banco_row_number: propuestaBancoRowNumber,\n propuesta_banco_enlaces_existentes: propuestaBancoEnlacesExistentes,\n propuesta_banco_file_id: propuestaBancoFileId,\n\n match_banco_debug: {\n total_filas_banco: bancoRows.length,\n total_candidatos: candidatosValidos.length,\n propuesta_normalizada: {\n referencia,\n marca,\n cliente,\n pais,\n version_propuesta: versionPropuesta,\n anio_propuesta: anioPropuesta,\n texto_clave: textoClavePropuesta\n },\n mejor_match: mejor\n ? {\n score: mejor.score_porcentaje,\n score_orden: Math.round(mejor.score_orden * 100),\n regla_match: mejor.regla_match,\n row_number: mejor.banco_row_number,\n nombre: mejor.banco_nombre,\n tipo_accion: mejor.banco_tipo_accion,\n marca: mejor.banco_marca,\n cliente: mejor.banco_cliente,\n pais: mejor.banco_pais,\n canal: mejor.banco_canal,\n link: mejor.banco_link,\n file_id: mejor.banco_file_id,\n enlaces_ejecutadas_existentes: mejor.banco_enlaces_ejecutadas,\n score_nombre: mejor.score_nombre,\n score_referencia_nombre: mejor.score_referencia_nombre,\n score_evidencia_nombre: mejor.score_evidencia_nombre,\n cobertura_nombre_en_evidencia: mejor.cobertura_nombre_en_evidencia,\n cobertura_referencia_en_banco: mejor.cobertura_referencia_en_banco,\n score_marca: mejor.score_marca,\n score_cliente: mejor.score_cliente,\n score_pais: mejor.score_pais,\n score_general: mejor.score_general,\n score_clave: mejor.score_clave,\n cobertura_banco_en_propuesta: mejor.cobertura_banco_en_propuesta,\n comunes_nombre_evidencia: mejor.comunes_nombre_evidencia,\n version_banco: mejor.version_banco,\n version_coincide: mejor.version_coincide,\n anio_banco: mejor.anio_banco,\n anio_coincide: mejor.anio_coincide,\n pais_coincide: mejor.pais_coincide,\n marca_coincide: mejor.marca_coincide\n }\n : null,\n segundo_match: segundo\n ? {\n score: segundo.score_porcentaje,\n regla_match: segundo.regla_match,\n row_number: segundo.banco_row_number,\n nombre: segundo.banco_nombre,\n marca: segundo.banco_marca,\n cliente: segundo.banco_cliente,\n pais: segundo.banco_pais,\n file_id: segundo.banco_file_id\n }\n : null,\n top_5: candidatosValidos.slice(0, 5).map(c => ({\n score: c.score_porcentaje,\n score_orden: Math.round(c.score_orden * 100),\n regla_match: c.regla_match,\n row_number: c.banco_row_number,\n nombre: c.banco_nombre,\n marca: c.banco_marca,\n cliente: c.banco_cliente,\n pais: c.banco_pais,\n file_id: c.banco_file_id,\n score_nombre: c.score_nombre,\n score_clave: c.score_clave,\n cobertura_banco_en_propuesta: c.cobertura_banco_en_propuesta,\n comunes_nombre_evidencia: c.comunes_nombre_evidencia,\n version_banco: c.version_banco,\n version_coincide: c.version_coincide,\n anio_banco: c.anio_banco,\n anio_coincide: c.anio_coincide\n }))\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 5792, + 2144 + ], + "id": "1e40c947-c41c-499f-9dd7-cd0ac4caa2d3", + "name": "Code - Match propuesta banco Fulgencio TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst toNumber = (valor) => {\n const limpio = limpiar(valor).replace(',', '.');\n const n = Number(limpio);\n return Number.isFinite(n) ? n : 0;\n};\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst slidesNormalizado1 = getNodeJson('Code - Normalizar respuesta Slides TEST');\nconst slidesNormalizado2 = getNodeJson('Code - Normalizar respuesta Slides ejecución TEST');\nconst slidesNormalizado3 = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...match,\n ...enriquecido,\n ...actual,\n ...slidesNormalizado1,\n ...slidesNormalizado2,\n ...slidesNormalizado3\n};\n\nconst matchDebug =\n match.match_banco_debug ||\n actual.match_banco_debug ||\n data.match_banco_debug ||\n {};\n\nconst mejorMatch = matchDebug.mejor_match || {};\n\n// --------------------------------------------------\n// 1. Resolver datos principales\n// --------------------------------------------------\n\nconst tipoReporte = normalizarPlano(\n jsonFinal.tipo_reporte ||\n data.tipo_reporte ||\n actual.tipo_reporte ||\n ''\n);\n\nlet matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n actual.propuesta_match_estado ||\n ''\n);\n\nlet matchConfianza = toNumber(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n actual.propuesta_match_confianza ||\n mejorMatch.score ||\n 0\n);\n\nlet nombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n actual.propuesta_nombre_banco ||\n mejorMatch.nombre ||\n ''\n);\n\nconst linkBancoOriginal = limpiar(\n match.propuesta_link_banco ||\n data.propuesta_link_banco ||\n actual.propuesta_link_banco ||\n mejorMatch.link ||\n ''\n);\n\nconst fileIdBanco = limpiar(\n match.propuesta_banco_file_id ||\n data.propuesta_banco_file_id ||\n actual.propuesta_banco_file_id ||\n mejorMatch.file_id ||\n ''\n);\n\nconst rowNumberBanco = limpiar(\n match.propuesta_banco_row_number ||\n data.propuesta_banco_row_number ||\n actual.propuesta_banco_row_number ||\n mejorMatch.row_number ||\n ''\n);\n\nconst enlacesExistentes = limpiar(\n match.propuesta_banco_enlaces_existentes ||\n data.propuesta_banco_enlaces_existentes ||\n actual.propuesta_banco_enlaces_existentes ||\n data.banco_enlaces_existentes ||\n actual.banco_enlaces_existentes ||\n mejorMatch.enlaces_ejecutadas_existentes ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n actual.presentacion_ejecucion_url ||\n data.presentation_url ||\n actual.presentation_url ||\n data.presentationUrl ||\n actual.presentationUrl ||\n data.webViewLink ||\n actual.webViewLink ||\n data.link_presentacion ||\n actual.link_presentacion ||\n slidesNormalizado3.presentacion_ejecucion_url ||\n slidesNormalizado3.presentation_url ||\n slidesNormalizado3.webViewLink ||\n ''\n);\n\nconst presentationId = limpiar(\n data.presentation_id ||\n actual.presentation_id ||\n slidesNormalizado3.presentation_id ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\n// --------------------------------------------------\n// 2. Normalizar estado de match de forma defensiva\n// --------------------------------------------------\n\nif (\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 85\n) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n}\n\nif (\n matchEstado !== 'MATCH_MEDIA_CONFIANZA' &&\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 60 &&\n matchConfianza < 85\n) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n}\n\nif (!matchEstado || matchEstado === 'PENDIENTE') {\n if (matchConfianza >= 85) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n } else if (matchConfianza >= 60) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n } else if (matchConfianza > 0) {\n matchEstado = 'MATCH_BAJA_CONFIANZA';\n } else {\n matchEstado = 'SIN_MATCH';\n }\n}\n\n// --------------------------------------------------\n// 3. Mantener valores enriquecidos\n// --------------------------------------------------\n\nconst propuestaReferenciaFinal = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n actual.propuesta_referencia ||\n ''\n);\n\nconst ubicacionFinal = limpiar(\n enriquecido.ubicacion ||\n data.ubicacion ||\n actual.ubicacion ||\n ''\n);\n\nconst fechaEjecucionFinal = limpiar(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n actual.fecha_ejecucion ||\n ''\n);\n\n// --------------------------------------------------\n// 4. Detectar duplicado de link de forma robusta\n// --------------------------------------------------\n\nconst limpiarUrl = (valor) => {\n return limpiar(valor)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '')\n .trim();\n};\n\nconst normalizarUrlComparacion = (valor) => {\n return limpiarUrl(valor)\n .replace(/\\/edit.*$/i, '')\n .replace(/\\/view.*$/i, '')\n .replace(/\\?.*$/i, '')\n .replace(/#.*$/i, '')\n .replace(/\\/$/, '')\n .toLowerCase();\n};\n\nconst extraerGoogleId = (valor) => {\n const url = limpiarUrl(valor);\n\n if (!url) return '';\n\n const patrones = [\n /\\/presentation\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/document\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/spreadsheets\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,\n /[?&]id=([a-zA-Z0-9_-]+)/\n ];\n\n for (const patron of patrones) {\n const match = url.match(patron);\n if (match?.[1]) return match[1];\n }\n\n return '';\n};\n\nconst extraerLinks = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return [];\n\n return texto\n .split(/[\\n\\r\\t;, ]+/)\n .map(limpiarUrl)\n .filter(Boolean)\n .filter(link => {\n return (\n link.startsWith('http://') ||\n link.startsWith('https://') ||\n link.includes('docs.google.com') ||\n link.includes('drive.google.com')\n );\n });\n};\n\nconst enlacesExistentesLista = extraerLinks(enlacesExistentes);\n\nconst presentationIdDetectado =\n presentationId ||\n extraerGoogleId(presentationUrl);\n\nconst presentationUrlNormalizada = normalizarUrlComparacion(presentationUrl);\n\nconst linkDuplicado = Boolean(\n presentationUrl &&\n enlacesExistentesLista.some((linkExistente) => {\n const idExistente = extraerGoogleId(linkExistente);\n\n if (\n presentationIdDetectado &&\n idExistente &&\n idExistente === presentationIdDetectado\n ) {\n return true;\n }\n\n return normalizarUrlComparacion(linkExistente) === presentationUrlNormalizada;\n })\n);\n\n// --------------------------------------------------\n// 5. Calcular enlaces propuestos\n// --------------------------------------------------\n\nlet enlacesPropuestos = enlacesExistentes;\n\nif (presentationUrl && !linkDuplicado) {\n enlacesPropuestos = enlacesExistentes\n ? `${enlacesExistentes}\\n${presentationUrl}`\n : presentationUrl;\n}\n\n// --------------------------------------------------\n// 6. Decisión automática final\n// --------------------------------------------------\n\nconst motivos = [];\n\nconst esPropuestaEjecutada = tipoReporte === 'PROPUESTA_EJECUTADA';\nconst esPropuestaExterna = tipoReporte === 'PROPUESTA_EXTERNA';\nconst matchAlta = matchEstado === 'MATCH_ALTA_CONFIANZA' && matchConfianza >= 85;\nconst hayMatchBanco = Boolean(rowNumberBanco && nombreBanco);\n\nlet decisionAutomaticaBanco = '';\nlet motivoDecisionAutomatica = '';\n\nif (!presentationUrl) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_ERROR_DATOS';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no existe link de presentación ejecutada.';\n motivos.push('no existe presentacion_ejecucion_url');\n\n} else if (esPropuestaExterna) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_PROPUESTA_EXTERNA';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el reporte fue clasificado como propuesta externa. Se genera presentación y se guarda el registro ejecutado.';\n\n} else if (!esPropuestaEjecutada) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_TIPO_NO_DETERMINADO';\n motivoDecisionAutomatica = `No se actualiza banco original porque tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}.`;\n motivos.push(`tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}`);\n\n} else if (linkDuplicado) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_LINK_DUPLICADO';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el link de presentación ejecutada ya existe en la columna de enlaces ejecutados.';\n motivos.push('el link ya existe en la columna de enlaces ejecutados');\n\n} else if (!hayMatchBanco) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_SIN_MATCH';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no se encontró una propuesta compatible con fila de banco válida.';\n if (!nombreBanco) motivos.push('no llegó propuesta_nombre_banco');\n if (!rowNumberBanco) motivos.push('no llegó propuesta_banco_row_number');\n\n} else if (!matchAlta) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_CONFIANZA_INSUFICIENTE';\n motivoDecisionAutomatica = `No se actualiza banco original porque el match no tiene confianza suficiente. Estado: ${matchEstado || 'SIN_MATCH'}, confianza: ${matchConfianza}.`;\n motivos.push(`propuesta_match_estado es ${matchEstado || 'SIN_MATCH'}`);\n motivos.push(`propuesta_match_confianza es ${matchConfianza}`);\n\n} else {\n decisionAutomaticaBanco = 'ACTUALIZADO_BANCO_AUTO';\n motivoDecisionAutomatica = 'Banco original actualizado automáticamente: propuesta ejecutada, match alto, confianza suficiente y link nuevo.';\n}\n\nconst actualizarBanco = decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO';\n\nconst bancoActualizadoAuto = actualizarBanco ? 'SI' : 'NO';\n\nconst motivoNoActualizaBanco = actualizarBanco\n ? ''\n : motivoDecisionAutomatica;\n\n// --------------------------------------------------\n// 7. Seguridad para evitar updates accidentales\n// --------------------------------------------------\n// Si NO se actualiza, dejamos row_number y banco_match_value vacíos.\n// Así, aunque alguien conecte mal el nodo de Google Sheets,\n// no debería encontrar fila válida para actualizar.\nconst rowNumberSeguro = actualizarBanco ? rowNumberBanco : '';\nconst bancoMatchValueSeguro = actualizarBanco ? rowNumberBanco : '';\nconst enlacesActualizadosSeguro = actualizarBanco ? enlacesPropuestos : enlacesExistentes;\n\n// Campo compatible con lógica anterior.\n// Ya no significa revisión humana; es una decisión automática del sistema.\nlet propuestaMatchRevision = '';\n\nif (decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_LINK_DUPLICADO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION_LINK_DUPLICADO';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_PROPUESTA_EXTERNA') {\n propuestaMatchRevision = 'NO_APLICA_PROPUESTA_EXTERNA';\n} else {\n propuestaMatchRevision = 'NO_ACTUALIZADO_AUTOMATICAMENTE';\n}\n\n// --------------------------------------------------\n// 8. Salida final\n// --------------------------------------------------\n\nreturn [\n {\n json: {\n ...data,\n\n // Mantener explícitamente los valores enriquecidos.\n propuesta_referencia: propuestaReferenciaFinal || data.propuesta_referencia || '',\n ubicacion: ubicacionFinal || data.ubicacion || '',\n fecha_ejecucion: fechaEjecucionFinal || data.fecha_ejecucion || '',\n\n // Mantener explícitamente el link final para nodos posteriores.\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n // Decisión automática nueva.\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n // Compatibilidad con flujo actual.\n actualizar_banco_original: actualizarBanco,\n motivo_no_actualiza_banco: motivoNoActualizaBanco,\n\n propuesta_match_estado: matchEstado || data.propuesta_match_estado || 'SIN_MATCH',\n propuesta_match_confianza: String(matchConfianza || 0),\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n propuesta_banco_enlaces_existentes: enlacesExistentes,\n propuesta_match_revision: propuestaMatchRevision,\n\n banco_match_column: 'row_number',\n banco_match_value: bancoMatchValueSeguro,\n\n row_number: rowNumberSeguro,\n 'Enlaces a propuestas ejecutadas': enlacesActualizadosSeguro,\n\n banco_enlaces_existentes: enlacesExistentes,\n banco_enlaces_propuestos: enlacesPropuestos,\n banco_enlaces_actualizados: enlacesActualizadosSeguro,\n banco_presentacion_url: presentationUrl,\n\n banco_link_duplicado_detectado: linkDuplicado,\n banco_presentacion_id_detectado: presentationIdDetectado,\n banco_enlaces_existentes_lista: enlacesExistentesLista,\n\n datos_enriquecidos_match_banco: Boolean(\n enriquecido.datos_enriquecidos_match_banco ||\n data.datos_enriquecidos_match_banco\n ),\n\n banco_update_debug: {\n tipo_reporte: tipoReporte,\n\n propuesta_match_estado: matchEstado,\n propuesta_match_confianza: matchConfianza,\n match_alta: matchAlta,\n hay_match_banco: hayMatchBanco,\n\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n propuesta_referencia_final: propuestaReferenciaFinal,\n ubicacion_final: ubicacionFinal,\n fecha_ejecucion_final: fechaEjecucionFinal,\n\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n\n presentationUrl,\n presentationId,\n presentationIdDetectado,\n\n enlacesExistentes,\n enlacesExistentesLista,\n enlacesPropuestos,\n enlacesActualizadosSeguro,\n\n linkDuplicado,\n\n rowNumberSeguro,\n bancoMatchValueSeguro,\n\n actualizarBanco,\n motivoNoActualizaBanco,\n\n motivos,\n\n mejor_match: mejorMatch,\n match_debug: matchDebug,\n\n enriquecido_debug: {\n propuesta_referencia: enriquecido.propuesta_referencia || '',\n ubicacion: enriquecido.ubicacion || '',\n fecha_ejecucion: enriquecido.fecha_ejecucion || '',\n datos_enriquecidos_match_banco: enriquecido.datos_enriquecidos_match_banco || false\n }\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 12688, + 1952 + ], + "id": "df339298-bbca-4b0e-9db7-41323b2c44c9", + "name": "Code - Preparar actualización banco propuesta ejecutada TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dccbea1d-e9cf-4f51-b94d-7b0162737548", + "leftValue": "={{ $json.actualizar_banco_original === true }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 13456, + 2000 + ], + "id": "da284028-5f31-4cda-b1b6-e27cf04b5c3b", + "name": "IF - Actualizar banco match alta confianza TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "Enlaces a propuestas ejecutadas": "={{ $json.banco_enlaces_actualizados }}", + "row_number": "={{ $json.propuesta_banco_row_number }}" + }, + "matchingColumns": [ + "row_number" + ], + "schema": [ + { + "id": "NOMBRE", + "displayName": "NOMBRE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "TIPO DE ACCION", + "displayName": "TIPO DE ACCION", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "CLIENTE", + "displayName": "CLIENTE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "MARCA", + "displayName": "MARCA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "PAIS", + "displayName": "PAIS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "CANAL", + "displayName": "CANAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "AMBIENTE DE COMPRA (RE)", + "displayName": "AMBIENTE DE COMPRA (RE)", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "TÁCTICA PROMOCIONAL", + "displayName": "TÁCTICA PROMOCIONAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "APROBADA", + "displayName": "APROBADA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ETIQUETAS", + "displayName": "ETIQUETAS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "AÑO", + "displayName": "AÑO", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Enlace a la propuesta", + "displayName": "Enlace a la propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Descripcion", + "displayName": "Descripcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "file_id", + "displayName": "file_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "nombre_archivo", + "displayName": "nombre_archivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "mime_type", + "displayName": "mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fuente_pais", + "displayName": "fuente_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "confianza_pais", + "displayName": "confianza_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "requiere_revision", + "displayName": "requiere_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "procesado_ia", + "displayName": "procesado_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivos_revision", + "displayName": "motivos_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Enlaces a propuestas ejecutadas", + "displayName": "Enlaces a propuestas ejecutadas", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tablero_sync_key", + "displayName": "tablero_sync_key", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tablero_project_id", + "displayName": "tablero_project_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tablero_origen", + "displayName": "tablero_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "drive_modified_time", + "displayName": "drive_modified_time", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultimo_refresco_ia", + "displayName": "ultimo_refresco_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "refresh_status", + "displayName": "refresh_status", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "refresh_attempts", + "displayName": "refresh_attempts", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "converted_google_slides_id", + "displayName": "converted_google_slides_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "converted_google_slides_link", + "displayName": "converted_google_slides_link", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 13888, + 2016 + ], + "id": "9932dc19-3202-4354-aae7-c33677259f41", + "name": "Sheets - Actualizar link ejecutada en banco TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const respuestaBanco = $json || {};\n\nconst base = $('Code - Preparar actualización banco propuesta ejecutada TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n banco_original_actualizado: true,\n banco_original_response: respuestaBanco\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 14096, + 2016 + ], + "id": "b2470e42-2786-491b-8f8d-84ac5cf7f31a", + "name": "Code - Restaurar contexto actualización banco TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valor) => {\n const texto = normalizar(valor);\n\n if (!texto) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bCODIGO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(texto));\n};\n\nconst normalizarUbicacion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst normalizarFechaEjecucion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (/^20\\d{2}$/.test(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst matchAltaOMedia = [\n 'MATCH_ALTA_CONFIANZA',\n 'MATCH_MEDIA_CONFIANZA'\n].includes(normalizar(data.propuesta_match_estado));\n\nconst nombreBanco = limpiar(data.propuesta_nombre_banco);\n\nconst referenciaOriginal = limpiar(data.propuesta_referencia);\n\nconst referenciaCanonica = matchAltaOMedia && nombreBanco\n ? nombreBanco\n : referenciaOriginal;\n\nconst ubicacionLimpia = normalizarUbicacion(data.ubicacion);\nconst fechaLimpia = normalizarFechaEjecucion(data.fecha_ejecucion);\n\nreturn [\n {\n json: {\n ...data,\n\n propuesta_referencia_original_ia: referenciaOriginal,\n ubicacion_original_ia: data.ubicacion || '',\n fecha_ejecucion_original_ia: data.fecha_ejecucion || '',\n\n propuesta_referencia: limpiarReferenciaVisual(referenciaCanonica),\n ubicacion: ubicacionLimpia,\n fecha_ejecucion: fechaLimpia,\n\n datos_enriquecidos_match_banco: true\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6000, + 2160 + ], + "id": "cb2a57aa-9f34-454e-a77f-8096bd49228b", + "name": "Code - Enriquecer datos con match banco TEST" + }, + { + "parameters": { + "jsCode": "const evento = $('Code - Normalizar evento WhatsApp TEST').first().json || {};\nconst grupos = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarTexto = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst normalizarJid = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return numero ? `${numero}@s.whatsapp.net` : '';\n};\n\nconst getCampo = (row, nombres) => {\n const keys = Object.keys(row || {});\n\n const normalizarKey = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '');\n };\n\n for (const nombre of nombres) {\n const nombreNorm = normalizarKey(nombre);\n\n const keyExacta = keys.find(k => normalizarKey(k) === nombreNorm);\n\n if (keyExacta !== undefined && limpiar(row[keyExacta]) !== '') {\n return limpiar(row[keyExacta]);\n }\n }\n\n return '';\n};\n\n// --------------------------------------------------\n// 1. Identificar origen del evento\n// --------------------------------------------------\n\nconst rawEvent = evento.raw_event || {};\nconst rawData = rawEvent.data || {};\nconst rawMessage = rawData.message || {};\n\nconst remoteJid = normalizarJid(\n evento.whatsapp_remote_jid ||\n evento.group_jid ||\n rawData.key?.remoteJid ||\n ''\n);\n\nconst groupJid = normalizarJid(\n evento.group_jid ||\n rawData.key?.remoteJid ||\n ''\n);\n\nconst isGroup = Boolean(evento.is_group || remoteJid.endsWith('@g.us'));\n\nconst messageType = limpiar(evento.message_type).toLowerCase();\nconst accion = limpiar(evento.accion_flujo).toUpperCase();\nconst texto = limpiar(evento.texto);\n\n// --------------------------------------------------\n// 2. Detectar eventos que nunca deben procesarse\n// --------------------------------------------------\n\nconst esReaccion = Boolean(\n rawMessage.reactionMessage ||\n rawMessage.protocolMessage?.type === 14 ||\n messageType === 'reaction' ||\n messageType === 'message_reaction' ||\n messageType === 'reactionmessage'\n);\n\nconst esMensajeDelBot = Boolean(\n evento.from_me ||\n rawData.key?.fromMe\n);\n\nconst esEventoSinContenido = !texto &&\n !evento.tiene_media &&\n !['audio', 'image', 'video', 'document'].includes(messageType);\n\n// --------------------------------------------------\n// 3. Leer grupos permitidos desde Google Sheets\n// --------------------------------------------------\n\nconst gruposNormalizados = grupos.map((row, index) => {\n const groupId = normalizarJid(\n getCampo(row, [\n 'group_jid',\n 'grupo_jid',\n 'jid',\n 'grupo',\n 'group',\n 'id_grupo'\n ])\n );\n\n const nombreGrupo = getCampo(row, [\n 'nombre_grupo',\n 'nombre',\n 'name'\n ]);\n\n const proyecto = getCampo(row, [\n 'proyecto',\n 'project'\n ]);\n\n const activo = getCampo(row, [\n 'activo',\n 'active',\n 'habilitado',\n 'permitido'\n ]);\n\n const activoNormalizado = normalizarTexto(activo);\n\n const estaActivo = [\n 'SI',\n 'SÍ',\n 'YES',\n 'TRUE',\n 'ACTIVO',\n '1'\n ].includes(activoNormalizado);\n\n return {\n index,\n row,\n group_jid: groupId,\n nombre_grupo: nombreGrupo,\n proyecto,\n activo,\n activo_normalizado: activoNormalizado,\n esta_activo: estaActivo\n };\n});\n\nconst grupoEncontrado = gruposNormalizados.find(grupo => {\n return grupo.group_jid && grupo.group_jid === (groupJid || remoteJid);\n}) || null;\n\nconst grupoPermitido = Boolean(\n isGroup &&\n grupoEncontrado &&\n grupoEncontrado.esta_activo\n);\n\n// Si en el futuro quieres permitir DM directo al bot, cambia esto a true.\nconst permitirChatIndividual = false;\nconst chatIndividualPermitido = !isGroup && permitirChatIndividual;\n\n// --------------------------------------------------\n// 4. Decidir si procesar o ignorar\n// --------------------------------------------------\n\nconst motivosIgnorar = [];\n\nif (esMensajeDelBot) {\n motivosIgnorar.push('mensaje enviado por el propio bot');\n}\n\nif (esReaccion) {\n motivosIgnorar.push('evento de reacción');\n}\n\nif (esEventoSinContenido) {\n motivosIgnorar.push('evento sin texto ni media útil');\n}\n\nif (isGroup && !grupoEncontrado) {\n motivosIgnorar.push(`grupo no encontrado en wa_grupos_permitidos: ${groupJid || remoteJid || 'SIN_GRUPO'}`);\n}\n\nif (isGroup && grupoEncontrado && !grupoEncontrado.esta_activo) {\n motivosIgnorar.push(`grupo encontrado pero inactivo: ${groupJid || remoteJid || 'SIN_GRUPO'}`);\n}\n\nif (!isGroup && !chatIndividualPermitido) {\n motivosIgnorar.push('chat individual no permitido');\n}\n\nconst procesar = motivosIgnorar.length === 0;\n\nreturn [\n {\n json: {\n ...evento,\n\n filtro_origen_procesar: procesar,\n filtro_origen_decision: procesar ? 'PROCESAR' : 'IGNORAR',\n filtro_origen_motivo: motivosIgnorar.join('; '),\n\n grupo_permitido: grupoPermitido,\n grupo_permiso_estado: grupoPermitido ? 'PERMITIDO' : 'DENEGADO',\n grupo_autorizado: grupoEncontrado?.row || null,\n grupo_nombre_autorizado: grupoEncontrado?.nombre_grupo || evento.group_name || '',\n grupo_proyecto_autorizado: grupoEncontrado?.proyecto || '',\n\n filtro_origen_debug: {\n remoteJid,\n groupJid,\n isGroup,\n messageType,\n accion,\n texto,\n\n esReaccion,\n esMensajeDelBot,\n esEventoSinContenido,\n\n total_grupos_leidos: grupos.length,\n grupo_encontrado: Boolean(grupoEncontrado),\n grupo_permitido: grupoPermitido,\n grupo_encontrado_debug: grupoEncontrado\n ? {\n group_jid: grupoEncontrado.group_jid,\n nombre_grupo: grupoEncontrado.nombre_grupo,\n proyecto: grupoEncontrado.proyecto,\n activo: grupoEncontrado.activo,\n activo_normalizado: grupoEncontrado.activo_normalizado,\n esta_activo: grupoEncontrado.esta_activo\n }\n : null,\n\n grupos_leidos_debug: gruposNormalizados.slice(0, 10).map(grupo => ({\n index: grupo.index,\n group_jid: grupo.group_jid,\n nombre_grupo: grupo.nombre_grupo,\n proyecto: grupo.proyecto,\n activo: grupo.activo,\n activo_normalizado: grupo.activo_normalizado,\n esta_activo: grupo.esta_activo\n })),\n\n chatIndividualPermitido\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -8880, + -1024 + ], + "id": "f472e804-82c8-443f-b847-e3adb44aafdf", + "name": "Code - Filtro origen WhatsApp TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "330d3843-6562-4ba6-afce-9a4bef9666b1", + "leftValue": "={{ $json.filtro_origen_procesar }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -8368, + -1056 + ], + "id": "146eed23-6f86-47a5-89fb-56b7ac54646f", + "name": "IF - Procesar solo grupo permitido TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 50533208, + "mode": "list", + "cachedResultName": "wa_grupos_permitidos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=50533208" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -9088, + -1024 + ], + "id": "542039fa-a89d-4655-b767-05dd55a0014b", + "name": "Sheets - Leer grupos permitidos WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "decision_automatica_banco": "={{ $json.decision_automatica_banco }}", + "banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}", + "propuesta_match_revision": "={{ $json.propuesta_match_revision }}", + "motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_ejecucion", + "displayName": "fecha_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_referencia", + "displayName": "propuesta_referencia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_match_estado", + "displayName": "propuesta_match_estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_match_confianza", + "displayName": "propuesta_match_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_nombre_banco", + "displayName": "propuesta_nombre_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_link_banco", + "displayName": "propuesta_link_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "marca", + "displayName": "marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "cliente", + "displayName": "cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "pais", + "displayName": "pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ubicacion", + "displayName": "ubicacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "comentario_original", + "displayName": "comentario_original", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "resumen_ia", + "displayName": "resumen_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "descripcion_ejecucion", + "displayName": "descripcion_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "elementos_detectados", + "displayName": "elementos_detectados", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tags", + "displayName": "tags", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "media_folder_url", + "displayName": "media_folder_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "presentacion_ejecucion_url", + "displayName": "presentacion_ejecucion_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fotos_count", + "displayName": "fotos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "audios_count", + "displayName": "audios_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "estado_revision", + "displayName": "estado_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "transcripcion_audio", + "displayName": "transcripcion_audio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_match_revision", + "displayName": "propuesta_match_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte_confianza", + "displayName": "tipo_reporte_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte_motivo", + "displayName": "tipo_reporte_motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "decision_automatica_banco", + "displayName": "decision_automatica_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_decision_automatica", + "displayName": "motivo_decision_automatica", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "banco_actualizado_auto", + "displayName": "banco_actualizado_auto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 12944, + 2032 + ], + "id": "cd1d8a04-7c10-400f-955d-05824392abc9", + "name": "Sheets - Actualizar decisión automática banco TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst decisionOriginal = getNodeJson('Code - Preparar actualización banco propuesta ejecutada TEST');\nconst updateDecisionSheet = $json || {};\n\nreturn [\n {\n json: {\n ...decisionOriginal,\n\n decision_sheet_actualizada: true,\n decision_sheet_update_debug: {\n row_number: updateDecisionSheet.row_number || '',\n session_id: updateDecisionSheet.session_id || decisionOriginal.session_id || '',\n decision_automatica_banco:\n updateDecisionSheet.decision_automatica_banco ||\n decisionOriginal.decision_automatica_banco ||\n '',\n banco_actualizado_auto:\n updateDecisionSheet.banco_actualizado_auto ||\n decisionOriginal.banco_actualizado_auto ||\n ''\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 13152, + 2032 + ], + "id": "2e297186-0656-4331-9df0-28a61b933861", + "name": "Code - Restaurar contexto decisión automática banco TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst sessionId = limpiar(data.session_id);\nconst eventId = limpiar(data.event_id || data.media_source_id || `${Date.now()}`);\nconst mediaSourceId = limpiar(data.media_source_id || data.event_id || eventId);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para crear buffer Redis de imagen.');\n}\n\nif (!mediaSourceId) {\n throw new Error('No llegó media_source_id/event_id para crear buffer Redis de imagen.');\n}\n\nconst redisKey = `fulgencio:media:${sessionId}:image`;\n\nconst bufferItem = {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n\n event_id: eventId,\n media_source_id: mediaSourceId,\n message_type: 'image',\n media_type: 'image',\n\n buffer_received_at: new Date().toISOString(),\n buffer_received_at_ms: Date.now()\n};\n\nreturn [\n {\n json: {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n redis_payload: JSON.stringify(bufferItem),\n\n estado_buffer_redis: 'IMAGEN_ENVIADA_A_BUFFER'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4576, + -1648 + ], + "id": "64b18b2d-f601-41c6-a041-4b9c8386581c", + "name": "Code - Preparar buffer imagen Redis TEST" + }, + { + "parameters": { + "operation": "push", + "list": "={{ $json.redis_buffer_key }}", + "messageData": "={{ $json.redis_payload }}", + "tail": true + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + -4368, + -1648 + ], + "id": "a23af96d-d5a1-4186-a9f4-0fd217ac6f5e", + "name": "Redis - Push buffer imagen TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "jsCode": "const original = $('Code - Preparar buffer imagen Redis TEST').first().json || {};\nconst redisPushResult = $json || {};\n\nreturn [\n {\n json: {\n ...original,\n redis_push_result: redisPushResult,\n redis_push_ok: true,\n estado_buffer_redis: 'IMAGEN_GUARDADA_EN_REDIS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4160, + -1648 + ], + "id": "5b888e51-25ea-4c3d-b1b3-a75053c95498", + "name": "Code - Restaurar contexto buffer imagen TEST" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + -3952, + -1648 + ], + "id": "d96a4efc-a786-451c-b1f4-d2e838b8e8be", + "name": "Wait - Esperar cierre buffer imagen TEST", + "webhookId": "5c73c08e-d77a-448d-a8ab-11080d08b476" + }, + { + "parameters": { + "operation": "get", + "propertyName": "redis_buffer_items", + "key": "={{ $('Code - Restaurar contexto buffer imagen TEST').first().json.redis_buffer_key }}", + "keyType": "list", + "options": {} + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + -3744, + -1648 + ], + "id": "a6cfe645-a160-4e56-9f9e-6ef5a8912bb7", + "name": "Redis - Leer buffer imagen TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Restaurar contexto buffer imagen TEST').first().json || {};\nconst redisOutput = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst currentBufferId = limpiar(contexto.redis_buffer_id);\nconst sessionId = limpiar(contexto.session_id);\nconst redisKey = limpiar(contexto.redis_buffer_key);\n\nlet rawItems = redisOutput.redis_buffer_items || redisOutput.message || [];\n\nif (!Array.isArray(rawItems)) {\n rawItems = rawItems ? [rawItems] : [];\n}\n\nconst parsed = [];\n\nfor (const raw of rawItems) {\n try {\n const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;\n if (obj && typeof obj === 'object') {\n parsed.push(obj);\n }\n } catch (error) {\n // Ignorar items corruptos del buffer\n }\n}\n\n// Deduplicar por media_source_id/event_id\nconst vistos = new Set();\nconst lote = [];\n\nfor (const item of parsed) {\n const id = limpiar(item.media_source_id || item.event_id || item.redis_buffer_id);\n if (!id) continue;\n if (vistos.has(id)) continue;\n\n vistos.add(id);\n\n lote.push({\n ...item,\n redis_buffer_key: redisKey,\n session_id: limpiar(item.session_id || sessionId),\n media_source_id: limpiar(item.media_source_id || id),\n event_id: limpiar(item.event_id || id),\n message_type: 'image',\n media_type: 'image'\n });\n}\n\nlote.sort((a, b) => {\n return Number(a.buffer_received_at_ms || 0) - Number(b.buffer_received_at_ms || 0);\n});\n\nconst ultimo = lote[lote.length - 1] || {};\nconst ultimoId = limpiar(ultimo.redis_buffer_id || ultimo.event_id || ultimo.media_source_id);\n\nconst debeProcesar =\n lote.length > 0 &&\n currentBufferId &&\n ultimoId &&\n currentBufferId === ultimoId;\n\nconst imagenesPrevias = Number(\n contexto.sesion_activa?.imagenes_count ??\n contexto.imagenes_count ??\n 0\n) || 0;\n\nconst imagenesNuevas = lote.length;\nconst imagenesTotal = imagenesPrevias + imagenesNuevas;\n\nreturn [\n {\n json: {\n ...contexto,\n\n redis_buffer_items_count: rawItems.length,\n redis_buffer_lote_count: lote.length,\n redis_buffer_lote: lote,\n\n redis_buffer_ultimo_id: ultimoId,\n redis_buffer_current_id: currentBufferId,\n\n redis_debe_procesar_lote: debeProcesar,\n redis_decision: debeProcesar ? 'PROCESAR_LOTE_IMAGEN' : 'IGNORAR_EJECUCION_INTERMEDIA',\n\n imagenes_previas: imagenesPrevias,\n imagenes_nuevas_lote: imagenesNuevas,\n imagenes_count: imagenesTotal,\n\n estado_buffer_redis: debeProcesar\n ? 'LOTE_IMAGEN_LISTO_PARA_PROCESAR'\n : 'IMAGEN_EN_BUFFER_ESPERANDO_ULTIMA_EJECUCION',\n\n redis_debug: {\n redisKey,\n rawItemsCount: rawItems.length,\n loteCount: lote.length,\n currentBufferId,\n ultimoId,\n debeProcesar\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3536, + -1648 + ], + "id": "3dd5e31b-2cee-4b91-9d21-091161591242", + "name": "Code - Decidir procesar lote imagen Redis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "f2394967-cd4b-4c6e-89be-7d9410d8a68b", + "leftValue": "={{ $json.redis_debe_procesar_lote }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -3312, + -1648 + ], + "id": "ad472bb4-83c4-4980-bdcc-ab1b185ffe71", + "name": "IF - Procesar lote imagen Redis TEST" + }, + { + "parameters": { + "operation": "delete", + "key": "={{ $json.redis_buffer_key }}" + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + -3040, + -1664 + ], + "id": "46b07f3c-4e6a-4c32-be24-a6bbfcf2ea69", + "name": "Redis - Borrar buffer imagen TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst lote = Array.isArray(contexto.redis_buffer_lote)\n ? contexto.redis_buffer_lote\n : [];\n\nif (!lote.length) {\n return [];\n}\n\nreturn lote.map((item, index) => {\n return {\n json: {\n ...contexto,\n ...item,\n\n media_index: index + 1,\n media_total: lote.length,\n\n estado: 'IMAGEN_RECIBIDA',\n fecha_procesado: new Date().toISOString(),\n\n lote_imagen_redis_total: lote.length,\n redis_lote_procesado: true\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -2832, + -1664 + ], + "id": "a28b6faa-d06a-45cb-9f43-7a159eb5d4e4", + "name": "Code - Expandir lote imagen Redis TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "event_id": "={{ $json.event_id }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "tiene_media": "={{ $json.tiene_media }}", + "estado": "=IMAGEN_RECIBIDA", + "session_id": "={{ $json.session_id }}", + "canal_origen": "=WHATSAPP", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "message_type": "={{ $json.message_type }}", + "texto": "={{ $json.texto }}", + "media_count": "={{ $json.media_count }}", + "raw_preview": "={{ $json.raw_preview }}", + "etapa_recibida": "={{ $json.etapa_actual }}", + "media_source_id": "={{ $json.media_source_id }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}", + "comando": "={{ $json.accion_flujo }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_procesado": "={{ new Date().toISOString() }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "event_id", + "displayName": "event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "message_type", + "displayName": "message_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "texto", + "displayName": "texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comando", + "displayName": "comando", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tiene_media", + "displayName": "tiene_media", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_count", + "displayName": "media_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "raw_preview", + "displayName": "raw_preview", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_procesado", + "displayName": "fecha_procesado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa_recibida", + "displayName": "etapa_recibida", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_source_id", + "displayName": "media_source_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -2592, + -1664 + ], + "id": "8b7ff874-bd05-499e-8655-381366cdba2e", + "name": "Sheets - Guardar eventos imagen lote Redis TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst guardados = $input.all().map(item => item.json || {});\n\nconst loteCount = Number(contexto.redis_buffer_lote_count || guardados.length || 0);\nconst imagenesPrevias = Number(contexto.imagenes_previas || 0) || 0;\nconst imagenesTotal = Number(contexto.imagenes_count || (imagenesPrevias + loteCount)) || loteCount;\n\nreturn [\n {\n json: {\n ...contexto,\n\n eventos_imagen_guardados: guardados.length,\n imagenes_nuevas_lote: loteCount,\n imagenes_count: imagenesTotal,\n\n ultima_actividad: new Date().toISOString(),\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'IMAGENES_RECIBIDAS',\n motivo_revision: `IMAGENES_RECIBIDAS_LOTE_${loteCount}`,\n\n redis_lote_imagen_guardado: true\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -2384, + -1664 + ], + "id": "b75fb900-5602-4d29-9a8e-9418a8262ee2", + "name": "Code - Consolidar lote imagen guardado Redis TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "estado": "={{ $json.estado }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "etapa": "={{ $json.etapa }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -2128, + -1664 + ], + "id": "fb406c50-cea0-46bd-8ddd-cdd0b6b796ee", + "name": "Sheets - Actualizar sesión imágenes lote Redis TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Consolidar lote imagen guardado Redis TEST').first().json || {};\nconst data = {\n ...contexto,\n ...($json || {})\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(data.idioma_flujo || 'ES').toUpperCase();\n\nconst to = limpiar(\n data.whatsapp_to ||\n data.group_jid ||\n data.whatsapp_remote_jid ||\n data.sesion_activa?.group_jid ||\n data.sesion_activa?.whatsapp_to ||\n data.sender_jid ||\n ''\n);\n\nconst count = Number(\n data.imagenes_nuevas_lote ||\n data.redis_buffer_lote_count ||\n data.eventos_imagen_guardados ||\n 1\n) || 1;\n\nif (!to) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar confirmación de imágenes.');\n}\n\nconst pluralEs = count === 1 ? 'imagen recibida' : 'imágenes recibidas';\nconst pluralEn = count === 1 ? 'image received' : 'images received';\n\nconst mensajeEs =\n`✅ ${count} ${pluralEs}.\n\nPuedes enviar más imágenes si hace falta.\n\nCuando termines de enviar las imágenes, escribe: FOTOS LISTAS`;\n\nconst mensajeEn =\n`✅ ${count} ${pluralEn}.\n\nYou can send more images if needed.\n\nWhen you finish sending images, write: PHOTOS READY`;\n\nconst mensajeFinal = idioma === 'EN' ? mensajeEn : mensajeEs;\n\nreturn [\n {\n json: {\n ...data,\n\n whatsapp_to: to,\n\n // Campos compatibles con tu nodo WhatsApp actual\n whatsapp_text: mensajeFinal,\n texto_respuesta: mensajeFinal,\n mensaje: mensajeFinal,\n text: mensajeFinal,\n\n // Campos debug\n mensaje_whatsapp: mensajeFinal,\n message_text: mensajeFinal,\n estado_mensaje: 'CONFIRMACION_IMAGENES_LOTE_PREPARADA',\n confirmacion_imagenes_lote_debug: {\n idioma,\n whatsapp_to: to,\n imagenes_nuevas_lote: count\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -1792, + -1648 + ], + "id": "96bf8413-9d42-4f7c-a3da-acb387f9caa5", + "name": "Code - Preparar confirmación imágenes lote Redis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "2cb38fef-879c-45f3-8886-b2f0a35d067b", + "leftValue": "={{ $json.analisis_debe_intentar_cierre }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 1280, + 2208 + ], + "id": "7995206c-6cce-441d-a03f-9ffb145d4449", + "name": "IF - Análisis completo Redis TEST" + }, + { + "parameters": { + "operation": "incr", + "key": "={{ $json.redis_lock_key }}", + "expire": true, + "ttl": 86400 + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 2880, + 2112 + ], + "id": "b6846811-0e07-472f-9680-7ce231a836a2", + "name": "Redis - Lock análisis final TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const actual = $input.item.json || {};\n\nconst extraerNumero = (valor) => {\n if (typeof valor === 'number') return valor;\n\n if (typeof valor === 'string') {\n const n = Number(valor);\n if (!Number.isNaN(n)) return n;\n }\n\n if (valor && typeof valor === 'object') {\n const valores = Object.values(valor);\n for (const v of valores) {\n const n = extraerNumero(v);\n if (n) return n;\n }\n }\n\n return 0;\n};\n\nconst redisActual = actual.redis_lock_response || actual;\n\nconst lockValor = extraerNumero(redisActual);\nconst lockAdquirido = lockValor === 1;\n\nreturn {\n json: {\n ...actual,\n\n redis_lock_response: redisActual,\n redis_lock_value: lockValor,\n\n lock_analisis_adquirido: lockAdquirido,\n decision_lock_analisis: lockAdquirido\n ? 'LOCK_ADQUIRIDO_GENERAR_REPORTE_FINAL'\n : 'LOCK_YA_EXISTIA_NO_GENERAR_DUPLICADO',\n\n lock_analisis_debug: {\n session_id: actual.session_id || '',\n redis_lock_key: actual.redis_lock_key || '',\n redis_lock_value: lockValor,\n lock_analisis_adquirido: lockAdquirido,\n redis_lock_response: redisActual,\n },\n },\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3088, + 2112 + ], + "id": "69448b97-ba47-40ec-b7f6-2e43f2fca593", + "name": "Code - Validar lock análisis final TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "432f82ae-e1bb-4ea7-a964-0cc4bebef5de", + "leftValue": "={{ $json.lock_analisis_adquirido }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 3296, + 2112 + ], + "id": "110f816e-13e6-42ef-b8b1-9396d0217f4c", + "name": "IF - Lock análisis final adquirido TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const actual = $input.item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n actual.session_id ||\n actual.sesion_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n actual.ejecucion_id ||\n sessionId\n);\n\nconst mediaEventId = limpiar(\n actual.media_event_id ||\n actual.message_id ||\n actual.id ||\n ''\n);\n\nconst mediaType = limpiar(\n actual.media_type ||\n actual.tipo_media ||\n ''\n).toLowerCase();\n\nconst mediaIndex = toNumber(\n actual.media_index ||\n actual.index ||\n 1,\n 1\n);\n\nconst mediaTotal = toNumber(\n actual.media_total_esperado ||\n actual.media_total ||\n actual.total_media ||\n 1,\n 1\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nif (!mediaEventId) {\n throw new Error('No llegó media_event_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nconst redisCountKey = `fulgencio:analisis-count:${sessionId}`;\nconst redisLockKey = `fulgencio:analisis-final-lock:${sessionId}`;\n\nreturn {\n json: {\n ...actual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: mediaEventId,\n media_type: mediaType,\n\n media_index: mediaIndex,\n media_total: mediaTotal,\n media_total_esperado: mediaTotal,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n contador_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: mediaType,\n media_index: mediaIndex,\n media_total: mediaTotal,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 416, + 2304 + ], + "id": "5e9bef81-fa4c-4c86-9a85-3f15927d32de", + "name": "Code - Preparar contador análisis Redis TEST" + }, + { + "parameters": { + "operation": "incr", + "key": "={{ $json.redis_count_key }}", + "expire": true, + "ttl": 86400 + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 672, + 2304 + ], + "id": "6fe212ba-75bb-4e41-b35e-9d7d222ad407", + "name": "Redis - Contar análisis media TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const redisActual = $input.item.json || {};\nconst ctx = $('Code - Preparar contador análisis Redis TEST').item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(ctx.session_id);\nconst ejecucionId = limpiar(ctx.ejecucion_id || sessionId);\n\nconst mediaTotalEsperado = toNumber(\n ctx.media_total_esperado ||\n ctx.media_total ||\n 1,\n 1\n);\n\nconst redisCountKey = limpiar(\n ctx.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n ctx.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\n// El Redis Increment puede devolver:\n// { \"fulgencio:analisis-count:SESSION\": 2 }\n// o puede devolver otro nombre según n8n.\nlet redisConteoActual = 0;\n\nif (redisCountKey && redisActual[redisCountKey] !== undefined) {\n redisConteoActual = toNumber(redisActual[redisCountKey], 0);\n} else {\n const valoresNumericos = Object.values(redisActual)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n redisConteoActual = valoresNumericos.length ? valoresNumericos[0] : 0;\n}\n\nconst analisisCompleto =\n mediaTotalEsperado > 0 &&\n redisConteoActual >= mediaTotalEsperado;\n\nreturn {\n json: {\n ...ctx,\n\n redis_increment_response: redisActual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_contados_redis: redisConteoActual,\n\n analisis_debe_intentar_cierre: analisisCompleto,\n decision_cierre_analisis: analisisCompleto\n ? 'ANALISIS_COMPLETO_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_ESPERAR_OTRA_EJECUCION',\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n cierre_analisis_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n redisConteoActual,\n analisisCompleto,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n media_actual: {\n media_event_id: ctx.media_event_id,\n media_type: ctx.media_type,\n media_index: ctx.media_index,\n media_total: ctx.media_total\n },\n redis_increment_response: redisActual\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 912, + 2304 + ], + "id": "f7041b6a-682a-4470-a76e-7dc7efd8586f", + "name": "Code - Validar conteo análisis Redis TEST" + }, + { + "parameters": { + "amount": 10 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 1568, + 2384 + ], + "id": "53135800-1202-4d11-b16e-71c7f8442f84", + "name": "Wait - Verificar cierre análisis Redis TEST", + "webhookId": "b17e9d5b-ec57-49ea-a0a7-91bf65e5e41a" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1107394537, + "mode": "list", + "cachedResultName": "wa_ejecuciones_analisis_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" + }, + "filtersUI": { + "values": [ + { + "lookupColumn": "session_id", + "lookupValue": "={{ $json.session_id }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 1840, + 2416 + ], + "id": "7d1d5352-258d-493e-a6e9-3579fbe54be0", + "name": "Sheets - Leer análisis media cierre Redis TEST", + "alwaysOutputData": true, + "executeOnce": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const inputRows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst contextos = [\n getNodeJson('Code - Validar reintento cierre análisis TEST'),\n getNodeJson('Wait - Reintento cierre análisis Redis TEST'),\n getNodeJson('Wait - Verificar cierre análisis Redis TEST'),\n getNodeJson('Code - Validar conteo análisis Redis TEST'),\n getNodeJson('Code - Preparar contador análisis Redis TEST')\n];\n\nconst contexto =\n contextos.find(ctx => limpiar(ctx.session_id)) ||\n inputRows.find(row => limpiar(row.session_id)) ||\n {};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n inputRows[0]?.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n inputRows[0]?.ejecucion_id ||\n sessionId\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para decidir cierre de análisis por Sheet.');\n}\n\nconst redisCountKey = limpiar(\n contexto.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n contexto.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\nconst rowsSesion = inputRows.filter(row => {\n return limpiar(row.session_id) === sessionId;\n});\n\nconst rowsCompletados = rowsSesion.filter(row => {\n const estado = limpiar(row.estado).toUpperCase();\n\n return (\n estado.includes('COMPLETADO') &&\n (\n estado.includes('ANALISIS_AUDIO') ||\n estado.includes('ANALISIS_IMAGEN') ||\n estado.includes('ANALISIS_VIDEO')\n )\n );\n});\n\nconst mapaUnicos = new Map();\n\nfor (const row of rowsCompletados) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaIndex = limpiar(row.media_index);\n const mediaType = limpiar(row.media_type).toLowerCase();\n\n const key = mediaEventId || `${mediaType}_${mediaIndex}`;\n\n if (!key) continue;\n\n if (!mapaUnicos.has(key)) {\n mapaUnicos.set(key, row);\n }\n}\n\nconst analisisUnicos = Array.from(mapaUnicos.values()).sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst esperadoDesdeContexto = Number(\n contexto.media_total_esperado ||\n contexto.media_total ||\n 0\n);\n\nconst maxMediaTotalSheet = Math.max(\n 0,\n ...analisisUnicos\n .map(row => Number(row.media_total || row.media_total_esperado || 0))\n .filter(n => Number.isFinite(n))\n);\n\nconst mediaTotalEsperado = esperadoDesdeContexto || maxMediaTotalSheet || 0;\nconst totalAnalisisSheet = analisisUnicos.length;\n\nconst analisisCompletoPorSheet =\n mediaTotalEsperado > 0 &&\n totalAnalisisSheet >= mediaTotalEsperado;\n\nreturn [\n {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n media_total_esperado: mediaTotalEsperado,\n\n total_analisis_encontrados_sheet: totalAnalisisSheet,\n analisis_completo_por_sheet: analisisCompletoPorSheet,\n analisis_debe_intentar_cierre: analisisCompletoPorSheet,\n\n decision_cierre_analisis: analisisCompletoPorSheet\n ? 'ANALISIS_COMPLETO_POR_SHEET_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_POR_SHEET_ESPERAR_REINTENTO',\n\n analisis_rows_filtrados: analisisUnicos,\n\n cierre_analisis_sheet_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n totalAnalisisSheet,\n analisisCompletoPorSheet,\n rows_leidas_sheet: inputRows.length,\n rows_misma_sesion: rowsSesion.length,\n rows_completadas: rowsCompletados.length,\n analisis_unicos: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado\n }))\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2048, + 2416 + ], + "id": "cf0e29f2-c2d4-4538-b4cd-2543d8361cf6", + "name": "Code - Decidir cierre análisis por Sheet Redis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "5a5b3564-6292-486b-8154-5df844083a7d", + "leftValue": "={{ $json.analisis_debe_intentar_cierre }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 2256, + 2416 + ], + "id": "4dad5977-e20f-48d6-b7f6-0ecdcb84ad88", + "name": "IF - Cierre análisis por Sheet Redis TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const redisActual = $input.item.json || {};\n\nlet contexto = {};\ntry {\n contexto = $('IF - Cierre análisis por Sheet Redis TEST').item.json || {};\n} catch (error) {\n contexto = {};\n}\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n redisActual.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n redisActual.ejecucion_id ||\n sessionId\n);\n\nconst retryKey = limpiar(\n contexto.redis_retry_key ||\n redisActual.redis_retry_key ||\n `fulgencio:analisis-retry:${sessionId}`\n);\n\nconst extraerNumeroRedis = (obj) => {\n if (!obj || typeof obj !== 'object') return 0;\n\n if (retryKey && obj[retryKey] !== undefined) {\n return toNumber(obj[retryKey], 0);\n }\n\n const valoresNumericos = Object.values(obj)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n return valoresNumericos.length ? valoresNumericos[0] : 0;\n};\n\nconst retryCount = extraerNumeroRedis(redisActual);\n\nconst maxReintentos = 12;\nconst reintentar = retryCount <= maxReintentos;\n\nreturn {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_retry_key: retryKey,\n cierre_retry_count: retryCount,\n cierre_retry_max: maxReintentos,\n cierre_retry_permitido: reintentar,\n\n decision_reintento_cierre: reintentar\n ? 'REINTENTAR_LECTURA_ANALISIS'\n : 'REINTENTOS_AGOTADOS',\n\n cierre_retry_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n retry_key: retryKey,\n redis_respuesta: redisActual,\n retry_count: retryCount,\n max_reintentos: maxReintentos,\n reintentar\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2784, + 2496 + ], + "id": "30c22af2-a6a4-4bd9-89fb-c5c3e8beb74e", + "name": "Code - Validar reintento cierre análisis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ad4f88ac-eda8-4cb1-bd08-34aca455e07c", + "leftValue": "={{ $json.cierre_retry_permitido }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 2992, + 2496 + ], + "id": "bd7ab438-286c-45e2-aff2-4ab7602bf6da", + "name": "IF - Reintentar cierre análisis Redis TEST" + }, + { + "parameters": { + "amount": 8 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 3360, + 2480 + ], + "id": "7c544ae1-5de8-4dcd-9591-35a9fc39420f", + "name": "Wait - Reintento cierre análisis Redis TEST", + "webhookId": "b54b49e7-ede0-48e7-8973-0d45d8482cc8" + }, + { + "parameters": { + "operation": "incr", + "key": "={{ 'fulgencio:analisis-retry:' + $json.session_id }}", + "expire": true, + "ttl": 900 + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 2576, + 2496 + ], + "id": "391a22f9-f721-4404-a042-086bcdd18062", + "name": "Redis - Contar reintento cierre análisis TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "url": "={{ \n 'https://slides.googleapis.com/v1/presentations/' + \n (\n $json.presentation_id ||\n $json.presentacion_id ||\n $json.slides_presentation_id ||\n $json.presentacion_ejecucion_id ||\n String($json.presentacion_ejecucion_url || $json.presentation_url || '').match(/\\/presentation\\/d\\/([^/]+)/)?.[1]\n )\n}}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleOAuth2Api", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 10480, + 2128 + ], + "id": "4df34af3-8e2a-4b7a-841c-675397a836cc", + "name": "HTTP - Obtener presentación Slides limpiar TEST", + "credentials": { + "googleOAuth2Api": { + "id": "eHseMeH39kRcXgOF", + "name": "Google account 2" + } + } + }, + { + "parameters": { + "jsCode": "const inputItems = $input.all();\nconst presentation = inputItems[0]?.json || {};\n\n// Contexto anterior: aquí está el link, presentation_id, conteos, etc.\nconst contexto = $('Code - Normalizar respuesta Slides ejecución TEST').first().json || {};\n\nconst limpiar = (v) => String(v ?? '').trim();\n\nconst extraerIdPresentacion = (...valores) => {\n for (const valor of valores) {\n const txt = limpiar(valor);\n if (!txt) continue;\n\n const match = txt.match(/\\/presentation\\/d\\/([^/]+)/);\n if (match?.[1]) return match[1];\n\n // Si ya viene como ID limpio\n if (!txt.includes('/') && txt.length > 20) return txt;\n }\n return '';\n};\n\nconst presentationId = extraerIdPresentacion(\n contexto.presentation_id,\n contexto.presentacion_id,\n contexto.slides_presentation_id,\n contexto.presentacion_ejecucion_id,\n contexto.presentacion_ejecucion_url,\n contexto.presentation_url,\n presentation.presentationId\n);\n\nif (!presentationId) {\n throw new Error('No se pudo detectar presentation_id para limpiar slides vacíos.');\n}\n\n// Detectar cantidad de fotos reales.\n// Usamos varios posibles nombres porque el contexto puede variar según el nodo anterior.\nlet fotosCount = Number(\n contexto.fotos_count ??\n contexto.imagenes_count ??\n contexto.images_count ??\n contexto.media_fotos_count ??\n contexto.media_imagenes_count ??\n contexto.analisis_multimedia?.imagenes?.length ??\n contexto.analisis_multimedia?.fotos?.length ??\n 0\n);\n\n// Seguridad: si llega vacío, intenta contar imágenes insertadas desde posibles arrays.\nif (!fotosCount && Array.isArray(contexto.imagenes)) {\n fotosCount = contexto.imagenes.length;\n}\n\nif (!fotosCount && Array.isArray(contexto.fotos)) {\n fotosCount = contexto.fotos.length;\n}\n\n// Máximo de placeholders esperados en plantilla\nconst maxFotosPlantilla = 25;\n\n// Si por alguna razón no tenemos conteo, NO borramos nada.\nif (!fotosCount || fotosCount < 0) {\n fotosCount = 0;\n}\n\nfotosCount = Math.min(Number(fotosCount || 0), maxFotosPlantilla);\n\nconst extraerTextoSlide = (slide) => {\n const partes = [];\n\n const elementos = slide.pageElements || [];\n for (const el of elementos) {\n const textElements = el?.shape?.text?.textElements || [];\n for (const te of textElements) {\n const content = te?.textRun?.content;\n if (content) partes.push(content);\n }\n }\n\n return partes.join(' ').replace(/\\s+/g, ' ').trim();\n};\n\nconst slides = presentation.slides || [];\n\nconst slidesDetectados = [];\nconst slidesABorrar = [];\n\nfor (const slide of slides) {\n const objectId = slide.objectId;\n const textoSlide = extraerTextoSlide(slide);\n\n // Busca FOTO 1, FOTO 2, etc. Tolerante a espacios.\n const match = textoSlide.match(/\\bFOTO\\s*(\\d{1,2})\\b/i);\n\n if (!match) continue;\n\n const numeroFoto = Number(match[1]);\n if (numeroFoto < 1 || numeroFoto > maxFotosPlantilla) continue;\n\n slidesDetectados.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n\n if (numeroFoto > fotosCount) {\n slidesABorrar.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n }\n}\n\nconst deleteRequests = slidesABorrar.map((s) => ({\n deleteObject: {\n objectId: s.objectId,\n },\n}));\n\nreturn [\n {\n json: {\n ...contexto,\n\n presentation_id: presentationId,\n slides_cleanup_fotos_count: fotosCount,\n slides_cleanup_total_slides: slides.length,\n\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n slides_fotos_vacias_count: slidesABorrar.length,\n\n delete_slide_requests: deleteRequests,\n\n slides_cleanup_debug: {\n presentation_id: presentationId,\n fotos_count_detectado: fotosCount,\n total_slides_presentacion: slides.length,\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n delete_requests_count: deleteRequests.length,\n },\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 10688, + 2128 + ], + "id": "278be3f3-bd01-4aac-b145-8b7a2a24c3f6", + "name": "Code - Preparar borrado slides fotos vacías TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dca8dbb7-b14a-4def-9e43-27a3c1360d78", + "leftValue": "={{ Number($json.slides_fotos_vacias_count || 0) > 0 }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 10896, + 2128 + ], + "id": "3fac8441-4cc4-4332-9a34-94c55a8d624a", + "name": "IF - Hay slides fotos vacías para borrar TEST" + }, + { + "parameters": { + "method": "POST", + "url": "={{ 'https://slides.googleapis.com/v1/presentations/' + $json.presentation_id + ':batchUpdate' }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleOAuth2Api", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ \n {\n requests: $json.delete_slide_requests || []\n }\n}}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 11184, + 1936 + ], + "id": "72d010e7-5aa8-4650-bdb4-36c3546c03c0", + "name": "HTTP - Borrar slides fotos vacías TEST", + "credentials": { + "googleOAuth2Api": { + "id": "eHseMeH39kRcXgOF", + "name": "Google account 2" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Preparar borrado slides fotos vacías TEST').first().json || {};\nconst respuestaBorrado = $input.first()?.json || {};\n\nreturn [\n {\n json: {\n ...contexto,\n slides_cleanup_done: true,\n slides_delete_response: respuestaBorrado,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 11392, + 1936 + ], + "id": "e845eb74-403a-4762-b81c-c0ff06db6e59", + "name": "Code - Restaurar contexto slides limpios TEST" + }, + { + "parameters": { + "content": "## 📥 ENTRADA WHATSAPP Y GRUPO OFICIAL\n\nEste bloque recibe eventos desde Evolution API y normaliza el mensaje recibido.\n\nResponsabilidades:\n- Detectar si viene de grupo o chat individual.\n- Identificar sender real en grupo.\n- Resolver whatsapp_to para responder al grupo correcto.\n- Detectar texto, audio, imagen, video o documento.\n- Normalizar comandos oficiales.\n- Validar que el mensaje venga de un grupo permitido.\n\nRegla principal:\nsolo se procesa si el grupo está autorizado.", + "height": 528, + "width": 832 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -9552, + -1344 + ], + "id": "d86e235c-45ef-48c5-a816-6c32f33fd9ae", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## 🧾 CONTROL DE SESIONES\n\nEste bloque controla si el usuario puede iniciar, continuar o cancelar un reporte.\n\nDecisiones:\n- CREAR_SESION: inicia nuevo reporte con Hey.\n- AVISO_SESION_ACTIVA: ya existe una sesión abierta.\n- CONTINUAR_SESION: sigue el reporte actual.\n- CANCELAR_SESION: cancela sesión activa.\n- SIN_SESION_ACTIVA: no hay reporte abierto.\n- SIN_SESION_PARA_CANCELAR: intento de cancelar sin sesión.\n\nRegla:\nuna propuesta/reporte activo por usuario dentro del grupo.", + "height": 672, + "width": 1024, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -8448, + -1440 + ], + "id": "4b1e674e-b24f-43b9-894c-14f6c018b516", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "## 👋 CREACIÓN Y MENSAJES DE CONTROL\n\nEste bloque crea sesiones nuevas y responde a casos operativos básicos.\n\nIncluye:\n- Crear sesión en Google Sheets.\n- Enviar bienvenida y pedir nota de voz.\n- Avisar si ya existe una sesión activa.\n- Avisar si no hay sesión activa.\n- Cancelar sesión cuando el usuario escribe CANCELAR.\n\nLa bienvenida inicia el flujo guiado de 3 pasos:\n1. Audio obligatorio\n2. Imágenes obligatorias\n3. Videos opcionales", + "height": 1904, + "width": 1200, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -5392, + -4928 + ], + "id": "17ae92c2-aaff-4121-9646-aef8939cfb48", + "name": "Sticky Note2" + }, + { + "parameters": { + "content": "## 🧭 ROUTER DEL REPORTE ACTIVO\n\nEste bloque decide qué hacer según la etapa actual de la sesión.\n\nEtapas:\n- ESPERANDO_AUDIO\n- ESPERANDO_IMAGENES\n- ESPERANDO_VIDEOS\n- PROCESANDO\n\nDecisiones principales:\n- Guardar audio\n- Pedir audio\n- Guardar imágenes\n- Validar FOTOS LISTAS\n- Guardar videos\n- Cerrar sin video\n- Cerrar con videos\n- Avisar procesamiento\n- Ignorar eventos no válidos\n\nRegla:\ncada mensaje se procesa según la etapa activa, no de forma aislada.", + "height": 944, + "width": 1024, + "color": 2 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -7968, + -704 + ], + "id": "09ccf504-4fb9-4a72-94fd-de1af944cbc0", + "name": "Sticky Note3" + }, + { + "parameters": { + "content": "## 🎙️ PASO 1 — AUDIO OBLIGATORIO\n\nEl reporte no avanza sin nota de voz.\n\nCuando llega un audio:\n- Se guarda el evento en wa_ejecuciones_eventos.\n- Se actualiza la sesión.\n- Se incrementa audio_count.\n- La etapa cambia a ESPERANDO_IMAGENES.\n- El bot pide las imágenes obligatorias.\n\nLa nota de voz debe explicar:\n- Propuesta o referencia\n- Marca/cliente\n- País\n- Ubicación\n- Qué se ejecutó o reportó\n- Comentarios o resultados", + "height": 640, + "width": 2448, + "color": 7 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -5952, + -2800 + ], + "id": "cf2e46cc-9bb7-469c-9335-a578a24fc23e", + "name": "Sticky Note4" + }, + { + "parameters": { + "content": "## 🖼️ PASO 2 — IMÁGENES OBLIGATORIAS\n\nEste bloque recibe imágenes y soporta varias imágenes en un mismo mensaje.\n\nUso de Redis:\n- Guarda temporalmente imágenes recibidas.\n- Espera un pequeño cierre de lote.\n- Expande el lote en eventos individuales.\n- Evita perder imágenes cuando WhatsApp las manda juntas.\n\nCuando el usuario termina:\ndebe escribir FOTOS LISTAS.\n\nRegla:\nel reporte no puede avanzar a videos si no hay imágenes guardadas.", + "height": 416, + "width": 3744, + "color": 6 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -5232, + -1808 + ], + "id": "89dc9ab6-f069-4c71-a4fa-db52c0dab336", + "name": "Sticky Note5" + }, + { + "parameters": { + "content": "## 🎥 PASO 3 — VIDEOS OPCIONALES\n\nLos videos son opcionales.\n\nOpciones:\n- Si no hay videos: usuario escribe SIN VIDEO.\n- Si envió videos: usuario escribe LISTO.\n- Los videos deben enviarse uno por uno, en mensajes separados.\n\nAl cerrar:\n- La sesión pasa a PROCESANDO.\n- Se envía aviso de reporte recibido.\n- El flujo inicia recuperación, análisis y generación de presentación.\n\nRegla:\nlas imágenes sí pueden ir juntas; los videos deben ir separados.", + "height": 768, + "width": 1632, + "color": "#1D6362" + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -4672, + -656 + ], + "id": "a74fc961-8836-4f46-ad9e-e943ca01f3a6", + "name": "Sticky Note6" + }, + { + "parameters": { + "content": "## 📂 RECUPERACIÓN Y ORGANIZACIÓN DE EVIDENCIAS\n\nEste bloque recupera todos los archivos de la sesión.\n\nProceso:\n1. Lee eventos multimedia de la sesión.\n2. Recupera media desde Evolution API.\n3. Convierte base64 a binario.\n4. Sube audio, imágenes y videos a Google Drive.\n5. Guarda metadata en wa_ejecuciones_media.\n6. Consolida la media recuperada.\n\nCada archivo queda asociado a:\n- session_id\n- ejecucion_id\n- media_event_id\n- tipo de media\n- link de Drive\n- nombre del archivo", + "height": 1728, + "width": 4400, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -6688, + 272 + ], + "id": "3908b74a-74bb-4333-9e16-7e90c5c74b3c", + "name": "Sticky Note7" + }, + { + "parameters": { + "content": "## 🤖 ANÁLISIS IA Y CONTROL REDIS\n\nGemini analiza cada evidencia multimedia:\n- Audio\n- Imágenes\n- Videos opcionales\n\nCada análisis se guarda en:\nwa_ejecuciones_analisis_media\n\nRedis controla el cierre:\n- Cuenta análisis completados.\n- Verifica si ya están todos listos.\n- Usa lock para evitar cierre duplicado.\n- Si faltan análisis, espera y reintenta.\n\nRegla:\nel JSON final solo se genera cuando el análisis multimedia está completo.", + "height": 1504, + "width": 6304, + "color": 3 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -2112, + 1440 + ], + "id": "4400f1ea-39b9-4302-86b4-a0c463f8e76c", + "name": "Sticky Note8" + }, + { + "parameters": { + "content": "## ✅ CIERRE FINAL DEL REPORTE\n\nEste bloque genera el resultado final del reporte.\n\nProceso:\n1. Consolida análisis multimedia.\n2. Gemini genera JSON final estructurado.\n3. Se normaliza la respuesta.\n4. Se hace match contra el Banco de Propuestas.\n5. Se verifica duplicado.\n6. Se guarda en propuestas_ejecutadas.\n7. Se crea carpeta de ejecución en Drive.\n8. Se copia la plantilla de Slides.\n9. Se insertan imágenes y se borran slides vacíos.\n10. Se actualiza el link final.\n11. Si el match es alto, se actualiza el banco original.\n12. Se envía el link final por WhatsApp.\n\nRegla:\npropuestas externas o match bajo no actualizan el banco original.", + "height": 1216, + "width": 11408, + "color": 6 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 4704, + 1488 + ], + "id": "a3245658-be74-4ea6-8a58-a104803896ac", + "name": "Sticky Note9" + } + ], + "connections": { + "Code - Normalizar evento WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Leer grupos permitidos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook - Evolution WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Normalizar evento WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "WhatsApp - Enviar mensaje TEST": { + "main": [ + [] + ] + }, + "Sheets - Leer sesiones existentes": { + "main": [ + [ + { + "node": "Code - Resolver sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Resolver sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Switch - Decisión sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch - Decisión sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Crear sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar aviso sesión activa WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar sin sesión para cancelar WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar cancelación sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [], + [ + { + "node": "Code - Resolver paso activo WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar bienvenida WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Crear sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar bienvenida WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Resolver paso activo WhatsApp TEST": { + "main": [ + [ + { + "node": "Switch - Paso activo WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch - Paso activo WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar evento audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar solicitud audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [], + [], + [ + { + "node": "Code - Preparar buffer imagen Redis TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Leer eventos sesión fotos WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar aviso falta imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Guardar evento video WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "CERRAR_SIN_VIDEO", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar cierre con videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar recordatorio videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar aviso procesando WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [], + [] + ] + }, + "Code - Preparar solicitud audio WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar evento audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar actualización audio recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización audio recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión audio recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar solicitud imágenes WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso sesión activa WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar solicitud imágenes WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar cancelación sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Cancelar sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Cancelar sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar mensaje cancelación WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar mensaje cancelación WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso falta imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar actualización fotos listas": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión fotos listas WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar solicitud videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar solicitud videos WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar recordatorio videos WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "CERRAR_SIN_VIDEO": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión cierre sin video WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar aviso procesando WhatsApp TEST1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar evento video WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar actualización video recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización video recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión video recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión video recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar confirmación video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar confirmación video WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar cierre con videos WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión cierre con videos WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar aviso procesando WhatsApp TEST1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso procesando WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar búsqueda eventos media WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Leer eventos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer eventos WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Filtrar eventos media de sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Filtrar eventos media de sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Separar media en items WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Separar media en items WhatsApp TEST": { + "main": [ + [ + { + "node": "HTTP Request - Obtener media base64 Evolution TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request - Obtener media base64 Evolution TEST": { + "main": [ + [ + { + "node": "Code - Convertir base64 a binario WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Convertir base64 a binario WhatsApp TEST": { + "main": [ + [ + { + "node": "Drive - Subir media WhatsApp TEST", + "type": "main", + "index": 0 + }, + { + "node": "Switch - Tipo media para Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Subir media WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar registro media Drive WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar registro media Drive WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar media Drive WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar media Drive WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Consolidar media recuperada WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Consolidar media recuperada WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión media recuperada WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar paquete análisis Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch - Tipo media para Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar Gemini audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar Gemini imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar Gemini video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Gemini audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Gemini - Analizar audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Analizar audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Normalizar análisis audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Gemini imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "Gemini - Analizar imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Analizar imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Normalizar análisis imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Gemini video WhatsApp TEST": { + "main": [ + [ + { + "node": "Analyze video", + "type": "main", + "index": 0 + } + ] + ] + }, + "Analyze video": { + "main": [ + [ + { + "node": "Code - Normalizar análisis video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar análisis video WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar análisis imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar análisis audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar contador análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Consolidar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Consolidar análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Gemini - Generar JSON final propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Generar JSON final propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "Code - Normalizar JSON final propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar JSON final propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "Sheets - Leer banco propuestas Fulgencio TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar propuesta ejecutada final TEST": { + "main": [ + [ + { + "node": "Code - Preparar copia presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión análisis completado WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar mensaje final WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar mensaje final WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait": { + "main": [ + [ + { + "node": "Sheets - Leer análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer propuestas ejecutadas final TEST": { + "main": [ + [ + { + "node": "Code - Verificar duplicado propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Verificar duplicado propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "IF - Propuesta final ya existe TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Propuesta final ya existe TEST": { + "main": [ + [], + [ + { + "node": "Sheets - Guardar propuesta ejecutada final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar copia presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Copiar plantilla presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Normalizar link presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar link presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar reemplazos Slides ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar link presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar actualización banco propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar reemplazos Slides ejecución TEST": { + "main": [ + [ + { + "node": "HTTP Request - Reemplazar textos Slides ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request - Reemplazar textos Slides ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar payload insertar imágenes Slides TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar respuesta Slides ejecución TEST": { + "main": [ + [ + { + "node": "HTTP - Obtener presentación Slides limpiar TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar sin sesión para cancelar WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer eventos sesión fotos WhatsApp TEST": { + "main": [ + [ + { + "node": "Preparar actualización fotos listas", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Drive - Crear carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Crear carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Code - Normalizar carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar media para mover carpeta TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar media para mover carpeta TEST": { + "main": [ + [ + { + "node": "Drive - Mover media a carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Mover media a carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Code - Confirmar media movida carpeta TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Confirmar media movida carpeta TEST": { + "main": [ + [ + { + "node": "Drive - Copiar plantilla presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar payload insertar imágenes Slides TEST": { + "main": [ + [ + { + "node": "HTTP - Insertar imágenes en Slides TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Insertar imágenes en Slides TEST": { + "main": [ + [ + { + "node": "Code - Normalizar respuesta Slides ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso procesando WhatsApp TEST1": { + "main": [ + [ + { + "node": "HTTP - Enviar aviso procesando WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto aviso procesando TEST": { + "main": [ + [ + { + "node": "Code - Preparar búsqueda eventos media WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Enviar aviso procesando WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto aviso procesando TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer banco propuestas Fulgencio TEST": { + "main": [ + [ + { + "node": "Code - Match propuesta banco Fulgencio TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Match propuesta banco Fulgencio TEST": { + "main": [ + [ + { + "node": "Code - Enriquecer datos con match banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización banco propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar decisión automática banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Actualizar banco match alta confianza TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar link ejecutada en banco TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar link ejecutada en banco TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto actualización banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto actualización banco TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Enriquecer datos con match banco TEST": { + "main": [ + [ + { + "node": "Sheets - Leer propuestas ejecutadas final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Filtro origen WhatsApp TEST": { + "main": [ + [ + { + "node": "IF - Procesar solo grupo permitido TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Procesar solo grupo permitido TEST": { + "main": [ + [ + { + "node": "Sheets - Leer sesiones existentes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer grupos permitidos WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Filtro origen WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar decisión automática banco TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto decisión automática banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto decisión automática banco TEST": { + "main": [ + [ + { + "node": "IF - Actualizar banco match alta confianza TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar buffer imagen Redis TEST": { + "main": [ + [ + { + "node": "Redis - Push buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Push buffer imagen TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto buffer imagen TEST": { + "main": [ + [ + { + "node": "Wait - Esperar cierre buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait - Esperar cierre buffer imagen TEST": { + "main": [ + [ + { + "node": "Redis - Leer buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Leer buffer imagen TEST": { + "main": [ + [ + { + "node": "Code - Decidir procesar lote imagen Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Decidir procesar lote imagen Redis TEST": { + "main": [ + [ + { + "node": "IF - Procesar lote imagen Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Procesar lote imagen Redis TEST": { + "main": [ + [ + { + "node": "Redis - Borrar buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Borrar buffer imagen TEST": { + "main": [ + [ + { + "node": "Code - Expandir lote imagen Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Expandir lote imagen Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar eventos imagen lote Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar eventos imagen lote Redis TEST": { + "main": [ + [ + { + "node": "Code - Consolidar lote imagen guardado Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Consolidar lote imagen guardado Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión imágenes lote Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión imágenes lote Redis TEST": { + "main": [ + [ + { + "node": "Code - Preparar confirmación imágenes lote Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar confirmación imágenes lote Redis TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Análisis completo Redis TEST": { + "main": [ + [ + { + "node": "Redis - Lock análisis final TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Wait - Verificar cierre análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Lock análisis final TEST": { + "main": [ + [ + { + "node": "Code - Validar lock análisis final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar lock análisis final TEST": { + "main": [ + [ + { + "node": "IF - Lock análisis final adquirido TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Lock análisis final adquirido TEST": { + "main": [ + [ + { + "node": "Wait", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar contador análisis Redis TEST": { + "main": [ + [ + { + "node": "Redis - Contar análisis media TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Contar análisis media TEST": { + "main": [ + [ + { + "node": "Code - Validar conteo análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar conteo análisis Redis TEST": { + "main": [ + [ + { + "node": "IF - Análisis completo Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait - Verificar cierre análisis Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Leer análisis media cierre Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer análisis media cierre Redis TEST": { + "main": [ + [ + { + "node": "Code - Decidir cierre análisis por Sheet Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Decidir cierre análisis por Sheet Redis TEST": { + "main": [ + [ + { + "node": "IF - Cierre análisis por Sheet Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Cierre análisis por Sheet Redis TEST": { + "main": [ + [ + { + "node": "Redis - Lock análisis final TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Redis - Contar reintento cierre análisis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar reintento cierre análisis TEST": { + "main": [ + [ + { + "node": "IF - Reintentar cierre análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Reintentar cierre análisis Redis TEST": { + "main": [ + [ + { + "node": "Wait - Reintento cierre análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait - Reintento cierre análisis Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Leer análisis media cierre Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Contar reintento cierre análisis TEST": { + "main": [ + [ + { + "node": "Code - Validar reintento cierre análisis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Obtener presentación Slides limpiar TEST": { + "main": [ + [ + { + "node": "Code - Preparar borrado slides fotos vacías TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar borrado slides fotos vacías TEST": { + "main": [ + [ + { + "node": "IF - Hay slides fotos vacías para borrar TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Hay slides fotos vacías para borrar TEST": { + "main": [ + [ + { + "node": "HTTP - Borrar slides fotos vacías TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Actualizar link presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Borrar slides fotos vacías TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto slides limpios TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto slides limpios TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar link presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": false + }, + "staticData": null, + "meta": { + "templateCredsSetupCompleted": true + }, + "versionId": "a72fa72c-6f91-4fca-9982-2b2438e339eb", + "activeVersionId": "a72fa72c-6f91-4fca-9982-2b2438e339eb", + "versionCounter": 175, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-06-24T14:10:15.444Z", + "createdAt": "2026-06-24T14:10:15.444Z", + "role": "workflow:owner", + "workflowId": "CdosDIWBgnDMnPdr", + "projectId": "PJpTANzTXIFibWsW", + "project": { + "updatedAt": "2026-04-22T14:25:09.686Z", + "createdAt": "2026-04-22T14:22:54.790Z", + "id": "PJpTANzTXIFibWsW", + "name": "Isaac Aracena ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-06-25T19:31:03.000Z", + "createdAt": "2026-06-25T19:30:58.490Z", + "versionId": "a72fa72c-6f91-4fca-9982-2b2438e339eb", + "workflowId": "CdosDIWBgnDMnPdr", + "nodes": [ + { + "parameters": { + "method": "POST", + "url": "={{'https://wsp.gomezleemarketing.com'}}/message/sendText/{{'botsoporte'}}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "={{'C267126ABB45-4C12-B626-6BAB1833F5D7'}}" + } + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "number", + "value": "={{ (() => {\n const limpiar = (valor) => String(valor ?? '').trim();\n\n const candidatos = [\n $json.whatsapp_to,\n $json.group_jid,\n $json.whatsapp_remote_jid,\n $json.sesion_activa?.group_jid,\n $json.sesion_activa?.whatsapp_to,\n $json.sesion_activa?.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ];\n\n let raw = limpiar(candidatos.find(v => limpiar(v)));\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n // Si ya viene como grupo, conservarlo.\n if (raw.includes('@g.us')) {\n return raw;\n }\n\n // Si viene como usuario WhatsApp, validar si realmente era un grupo.\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n // Los grupos de WhatsApp suelen venir como 120363...\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n})() }}" + }, + { + "name": "delay", + "value": "={{ 1000 }}" + }, + { + "name": "text", + "value": "={{ $json.whatsapp_text || $json.telegram_text || $json.texto_respuesta || $json.mensaje || $json.text || 'Mensaje sin contenido' }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 15680, + 1632 + ], + "id": "dad797ac-f70a-4550-a1bb-297d2f296dc4", + "name": "WhatsApp - Enviar mensaje TEST" + }, + { + "parameters": { + "jsCode": "const input = $json || {};\nconst body = input.body || input;\nconst data = body.data || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst limpiarNumero = (valor) =>\n texto(valor)\n .replace('@s.whatsapp.net', '')\n .replace('@c.us', '')\n .replace('@g.us', '')\n .replace('@lid', '')\n .replace(/\\D/g, '');\n\nconst key = data.key || {};\nconst message = data.message || {};\n\n// --------------------------------------------------\n// 1. Identificar origen real del mensaje\n// --------------------------------------------------\n\nconst remoteJid = texto(\n key.remoteJid ||\n data.remoteJid ||\n body.remoteJid ||\n ''\n);\n\nconst isGroup = remoteJid.endsWith('@g.us');\n\nconst participantRaw = texto(\n key.participant ||\n data.participant ||\n body.participant ||\n data.sender ||\n data.participantJid ||\n ''\n);\n\nconst participantAlt = texto(\n key.participantAlt ||\n data.participantAlt ||\n body.participantAlt ||\n data.senderAlt ||\n ''\n);\n\n// En grupos, Evolution puede mandar:\n// participant = 1655...@lid\n// participantAlt = número real@s.whatsapp.net\n// Para sesiones y permisos necesitamos preferir el número real.\nconst participantJid = participantAlt || participantRaw;\n\n// En grupo, el sender real es participantAlt si existe.\n// En chat individual, el sender real es remoteJid.\nconst senderJid = isGroup\n ? participantJid\n : remoteJid;\n\nconst senderPhone = limpiarNumero(senderJid);\nconst groupJid = isGroup ? remoteJid : '';\n\nconst pushName =\n texto(data.pushName) ||\n texto(body.pushName) ||\n texto(data.senderName) ||\n texto(body.senderName) ||\n 'Usuario WhatsApp';\n\n// El bot debe responder al grupo si el mensaje vino de grupo.\n// Si vino de chat individual, responde al usuario.\nconst whatsappTo = isGroup ? groupJid : remoteJid;\n\n// --------------------------------------------------\n// 2. Extraer texto o payload de botones\n// --------------------------------------------------\n\nconst buttonText =\n texto(message.buttonsResponseMessage?.selectedButtonId) ||\n texto(message.buttonsResponseMessage?.selectedDisplayText) ||\n texto(message.listResponseMessage?.singleSelectReply?.selectedRowId) ||\n texto(message.listResponseMessage?.title) ||\n texto(message.templateButtonReplyMessage?.selectedId) ||\n texto(message.templateButtonReplyMessage?.selectedDisplayText) ||\n '';\n\nconst textoMensaje =\n buttonText ||\n texto(message.conversation) ||\n texto(message.extendedTextMessage?.text) ||\n texto(data.messageText) ||\n texto(data.text) ||\n texto(body.text) ||\n '';\n\n// --------------------------------------------------\n// 3. Detectar media\n// --------------------------------------------------\n\nconst audioMsg = message.audioMessage;\nconst imageMsg = message.imageMessage;\nconst videoMsg = message.videoMessage;\nconst documentMsg = message.documentMessage;\n\nconst documentMime = texto(documentMsg?.mimetype);\nconst documentFileName = texto(documentMsg?.fileName);\n\nlet messageType = 'unknown';\n\nif (audioMsg) {\n messageType = 'audio';\n} else if (imageMsg) {\n messageType = 'image';\n} else if (videoMsg) {\n messageType = 'video';\n} else if (documentMsg && documentMime.startsWith('video/')) {\n messageType = 'video';\n} else if (documentMsg && documentMime.startsWith('image/')) {\n messageType = 'image';\n} else if (documentMsg && documentMime.startsWith('audio/')) {\n messageType = 'audio';\n} else if (documentMsg) {\n messageType = 'document';\n} else if (textoMensaje) {\n messageType = 'text';\n}\n\nconst tieneMedia = ['audio', 'image', 'video', 'document'].includes(messageType);\n\nconst mediaMimeType =\n texto(audioMsg?.mimetype) ||\n texto(imageMsg?.mimetype) ||\n texto(videoMsg?.mimetype) ||\n documentMime ||\n '';\n\nconst mediaSourceId =\n texto(key.id) ||\n texto(data.id) ||\n texto(data.messageId) ||\n '';\n\n// --------------------------------------------------\n// 4. Normalizar comandos internos\n// --------------------------------------------------\n\nconst normalizarTexto = (valor) =>\n texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst normalizarComando = (valor) => {\n const t = normalizarTexto(valor);\n\n // Inicio oficial\n if (t === 'HEY') {\n return 'START';\n }\n\n // Cancelación oficial ES/EN\n if (\n t === 'CANCELAR' ||\n t === 'CANCEL'\n ) {\n return 'CANCELAR';\n }\n\n // Cierre de imágenes oficial ES/EN\n if (\n t === 'FOTOS LISTAS' ||\n t === 'FOTOS_LISTAS' ||\n t === 'PHOTOS READY' ||\n t === 'PHOTOS_READY'\n ) {\n return 'FOTOS_LISTAS';\n }\n\n // Cierre sin video oficial ES/EN\n if (\n t === 'SIN VIDEO' ||\n t === 'SIN_VIDEO' ||\n t === 'NO VIDEO' ||\n t === 'NO_VIDEO'\n ) {\n return 'SIN_VIDEO';\n }\n\n // Cierre con videos oficial ES/EN\n if (\n t === 'LISTO' ||\n t === 'DONE'\n ) {\n return 'LISTO';\n }\n\n return 'NORMAL';\n};\n\n// --------------------------------------------------\n// 5. Detectar país/idioma base por teléfono\n// --------------------------------------------------\n\nconst detectarIdiomaPorTelefono = (phone) => {\n const n = limpiarNumero(phone);\n\n // Países GLM principalmente hispanohablantes\n if (n.startsWith('502')) return { country_code: '502', pais_detectado: 'Guatemala', idioma_flujo: 'ES' };\n if (n.startsWith('503')) return { country_code: '503', pais_detectado: 'El Salvador', idioma_flujo: 'ES' };\n if (n.startsWith('504')) return { country_code: '504', pais_detectado: 'Honduras', idioma_flujo: 'ES' };\n if (n.startsWith('505')) return { country_code: '505', pais_detectado: 'Nicaragua', idioma_flujo: 'ES' };\n if (n.startsWith('506')) return { country_code: '506', pais_detectado: 'Costa Rica', idioma_flujo: 'ES' };\n if (n.startsWith('507')) return { country_code: '507', pais_detectado: 'Panamá', idioma_flujo: 'ES' };\n\n // República Dominicana: +1 809 / 829 / 849\n if (n.startsWith('1809') || n.startsWith('1829') || n.startsWith('1849')) {\n return { country_code: '1', pais_detectado: 'República Dominicana', idioma_flujo: 'ES' };\n }\n\n // Default para +1 no dominicano: inglés hasta que hagamos tabla formal\n if (n.startsWith('1')) {\n return { country_code: '1', pais_detectado: 'País +1 no identificado', idioma_flujo: 'EN' };\n }\n\n return { country_code: '', pais_detectado: 'No identificado', idioma_flujo: 'ES' };\n};\n\nconst idiomaInfo = detectarIdiomaPorTelefono(senderPhone);\n\n// --------------------------------------------------\n// 6. Resultado normalizado\n// --------------------------------------------------\n\nreturn [\n {\n json: {\n event_id: texto(key.id || data.id || Date.now()),\n fecha_recepcion: new Date().toISOString(),\n\n canal_origen: 'WHATSAPP',\n\n // Compatibilidad con el flujo actual:\n // seguimos llenando manager_telefono / manager_nombre,\n // pero ahora apuntan al sender real, no al grupo.\n manager_telefono: senderPhone,\n manager_nombre: pushName,\n usuario_id_origen: senderPhone,\n\n // Campos nuevos para grupo\n is_group: isGroup,\n group_jid: groupJid,\n group_name: texto(data.groupName || body.groupName || ''),\n sender_jid: senderJid,\n sender_phone: senderPhone,\n sender_name: pushName,\n\n whatsapp_remote_jid: remoteJid,\n whatsapp_to: whatsappTo,\n\n from_me: Boolean(key.fromMe || data.fromMe || false),\n\n message_type: messageType,\n texto: textoMensaje,\n accion_flujo: normalizarComando(textoMensaje),\n\n tiene_media: tieneMedia,\n media_count: tieneMedia ? 1 : 0,\n media_source_id: mediaSourceId,\n media_mime_type: mediaMimeType,\n media_file_name: documentFileName,\n\n country_code: idiomaInfo.country_code,\n pais_detectado: idiomaInfo.pais_detectado,\n idioma_flujo: idiomaInfo.idioma_flujo,\n\n raw_preview: JSON.stringify(body).slice(0, 4000),\n raw_event: body,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -9296, + -1024 + ], + "id": "5758e021-8dc1-4541-8cb9-be855b01bdbc", + "name": "Code - Normalizar evento WhatsApp TEST" + }, + { + "parameters": { + "httpMethod": "POST", + "path": "factura", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -9504, + -1024 + ], + "id": "8985b821-561b-4927-9528-1080bb264d83", + "name": "Webhook - Evolution WhatsApp TEST", + "webhookId": "6d9cdd90-a105-4e00-b42a-dcd7c4e5a7e1" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -8112, + -1072 + ], + "id": "2553dc5c-8ca5-416c-b08a-8e44f6e549a6", + "name": "Sheets - Leer sesiones existentes", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const evento = $('Code - Normalizar evento WhatsApp TEST').first().json || {};\nconst sesiones = $input.all().map((item) => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) =>\n texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst estadosAbiertos = new Set([\n 'ACTIVA',\n 'PENDIENTE',\n 'EN_PROCESO',\n 'PROCESANDO',\n 'LISTO_PARA_RECUPERAR_MEDIA',\n 'LISTO_PARA_ANALIZAR',\n]);\n\nconst etapasAbiertas = new Set([\n 'ESPERANDO_AUDIO',\n 'ESPERANDO_IMAGENES',\n 'ESPERANDO_VIDEOS',\n 'PROCESANDO',\n]);\n\nconst managerTelefono = texto(evento.manager_telefono || evento.sender_phone);\nconst senderPhone = texto(evento.sender_phone || managerTelefono);\nconst groupJid = texto(evento.group_jid);\nconst isGroup = Boolean(evento.is_group);\nconst accion = texto(evento.accion_flujo || 'NORMAL');\n\n// Idioma únicamente desde Code - Normalizar evento WhatsApp TEST.\n// Ese nodo lo calcula por número de teléfono.\nconst idiomaFlujo = normalizar(evento.idioma_flujo || 'ES') === 'EN'\n ? 'EN'\n : 'ES';\n\n// --------------------------------------------------\n// Buscar sesión activa\n// --------------------------------------------------\n// En grupo: sesión única por grupo + sender.\n// En chat individual: sesión por manager_telefono.\nconst sesionActiva = sesiones\n .filter((row) => {\n const rowManager = texto(row.manager_telefono);\n const rowSender = texto(row.sender_phone || row.manager_telefono);\n const rowGroup = texto(row.group_jid);\n\n if (isGroup) {\n return rowGroup === groupJid && rowSender === senderPhone;\n }\n\n return rowManager === managerTelefono;\n })\n .filter((row) => {\n const estado = normalizar(row.estado);\n const etapa = normalizar(row.etapa);\n\n return estadosAbiertos.has(estado) || etapasAbiertas.has(etapa);\n })\n .sort((a, b) => {\n return new Date(b.ultima_actividad || b.fecha_inicio || 0) - new Date(a.ultima_actividad || a.fecha_inicio || 0);\n })[0] || null;\n\n// --------------------------------------------------\n// Crear IDs\n// --------------------------------------------------\n\nconst suffix = Math.random().toString(36).slice(2, 6).toUpperCase();\nconst phoneSuffix = senderPhone.slice(-4) || managerTelefono.slice(-4) || '0000';\nconst sessionId = `WA_${Date.now()}_${phoneSuffix}_${suffix}`;\n\nlet decision = '';\n\nif (accion === 'START') {\n decision = sesionActiva ? 'AVISO_SESION_ACTIVA' : 'CREAR_SESION';\n} else if (accion === 'CANCELAR') {\n decision = sesionActiva ? 'CANCELAR_SESION' : 'SIN_SESION_PARA_CANCELAR';\n} else {\n decision = sesionActiva ? 'CONTINUAR_SESION' : 'SIN_SESION_ACTIVA';\n}\n\nreturn [\n {\n json: {\n ...evento,\n\n idioma_flujo: idiomaFlujo,\n\n sesion_activa_encontrada: Boolean(sesionActiva),\n sesion_activa: sesionActiva,\n\n session_id: sesionActiva ? texto(sesionActiva.session_id) : sessionId,\n ejecucion_id: sesionActiva ? texto(sesionActiva.ejecucion_id || sesionActiva.session_id) : sessionId,\n\n decision_flujo: decision,\n\n nueva_sesion: {\n session_id: sessionId,\n\n manager_telefono: managerTelefono,\n manager_nombre: texto(evento.manager_nombre),\n\n canal_origen: 'WHATSAPP',\n fecha_inicio: new Date().toISOString(),\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_AUDIO',\n\n audio_count: 0,\n imagenes_count: 0,\n videos_count: 0,\n\n estado: 'ACTIVA',\n ejecucion_id: sessionId,\n motivo_revision: '',\n\n is_group: isGroup,\n group_jid: groupJid,\n group_name: texto(evento.group_name),\n sender_jid: texto(evento.sender_jid),\n sender_phone: senderPhone,\n sender_name: texto(evento.sender_name),\n\n country_code: texto(evento.country_code),\n pais_detectado: texto(evento.pais_detectado),\n idioma_flujo: idiomaFlujo,\n\n tipo_reporte: ''\n },\n\n resolver_sesion_debug: {\n idioma_origen: 'Code - Normalizar evento WhatsApp TEST',\n idioma_final_usado: idiomaFlujo,\n pais_detectado: texto(evento.pais_detectado),\n country_code: texto(evento.country_code),\n manager_telefono: managerTelefono,\n sender_phone: senderPhone,\n group_jid: groupJid,\n is_group: isGroup,\n accion,\n decision_flujo: decision,\n total_sesiones_leidas: sesiones.length,\n sesion_activa_encontrada: Boolean(sesionActiva)\n }\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -7920, + -1072 + ], + "id": "062d75cf-28b9-44b1-b119-d619d83b5bfd", + "name": "Code - Resolver sesión WhatsApp TEST" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.decision_flujo === 'CREAR_SESION' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + }, + "id": "acec6506-977a-412d-b08a-fbcc8de4b0b8" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CREAR_SESION" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "5c9dcfa5-9b15-428c-ad80-c277b0671658", + "leftValue": "={{ $json.decision_flujo === 'AVISO_SESION_ACTIVA' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "AVISO_SESION_ACTIVA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "966402b5-4517-45c2-8a87-0a7aa4e5e682", + "leftValue": "={{ $json.decision_flujo === 'SIN_SESION_ACTIVA' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "SIN_SESION_ACTIVA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "698f3da3-29d2-472f-b625-af387891035e", + "leftValue": "={{ $json.decision_flujo === 'CANCELAR_SESION' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CANCELAR_SESION" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "d5feac06-4493-494e-ac1e-36c007f0de79", + "leftValue": "={{ $json.decision_flujo === 'SIN_SESION_PARA_CANCELAR' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "SIN_SESION_PARA_CANCELAR" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "950979b7-b930-4260-a392-52277a742e43", + "leftValue": "={{ $json.decision_flujo === 'CONTINUAR_SESION' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CONTINUAR_SESION" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + -7696, + -1136 + ], + "id": "cad5616f-22a5-4572-a816-253c2f48544c", + "name": "Switch - Decisión sesión WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $('Code - Resolver sesión WhatsApp TEST').first().json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.nueva_sesion?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst nombre =\n datos.manager_nombre ||\n datos.sender_name ||\n 'equipo';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '📸 *GLM Proposal Evidence Registry*',\n '',\n '*Step 1 of 3 — Voice note*',\n '',\n `Hello, ${nombre}.`,\n '',\n 'Use this flow to register evidence for Fulgencio Fumado.',\n '',\n '*Report Type:*',\n '1. *Executed Proposal*: Already implemented.',\n '2. *External Proposal*: Received outside the internal bank.',\n '',\n 'Send a voice note and clearly say if this is an *executed* or *external* proposal.',\n '',\n 'Include:',\n '• Proposal/reference',\n '• Brand/client',\n '• Country',\n '• Location',\n '• Date',\n '• What was implemented or reported',\n '• Results/comments',\n '',\n '*Important:* One proposal/report at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n '📸 *Registro de Evidencias GLM*',\n '',\n '*Paso 1 de 3 — Nota de voz*',\n '',\n `Hola, ${nombre}.`,\n '',\n 'Usa este flujo para registrar evidencias de Fulgencio Fumado.',\n '',\n '*Tipo de Reporte:*',\n '1. *Propuesta Ejecutada*: Ya implementada.',\n '2. *Propuesta Externa*: Recibida fuera del banco interno.',\n '',\n 'Envía una nota de voz e indica claramente si es *ejecutada* o *externa*.',\n '',\n 'Incluye:',\n '• Propuesta/referencia',\n '• Marca/cliente',\n '• País',\n '• Ubicación',\n '• Fecha',\n '• Qué se implementó o reportó',\n '• Resultados/comentarios',\n '',\n '*Importante:* Una sola propuesta o reporte a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid,\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4944, + -4608 + ], + "id": "0c8f0c1a-f57d-4b62-a5e7-91715c1dcaa6", + "name": "Code - Preparar bienvenida WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst etapa = String(datos.sesion_activa?.etapa || '').trim() || 'pendiente';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '⚠️ You already have an active proposal report in progress.',\n '',\n `Current step: ${etapa}`,\n '',\n 'Please complete that report before starting a new one.',\n '',\n 'To cancel it, use the cancel option.',\n ].join('\\n');\n} else {\n mensaje = [\n '⚠️ Ya tienes un reporte de propuesta en proceso.',\n '',\n `Etapa actual: ${etapa}`,\n '',\n 'Completa ese reporte antes de iniciar uno nuevo.',\n '',\n 'Para cancelarlo, usa la opción de cancelar.',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.whatsapp_remote_jid,\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4912, + -4160 + ], + "id": "6413892b-21b2-4d3a-be5e-b200b13c1de1", + "name": "Code - Preparar aviso sesión activa WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.nueva_sesion.session_id }}", + "manager_telefono": "={{ $json.nueva_sesion.manager_telefono }}", + "manager_nombre": "={{ $json.nueva_sesion.manager_nombre }}", + "canal_origen": "={{ $json.nueva_sesion.canal_origen }}", + "fecha_inicio": "={{ $json.nueva_sesion.fecha_inicio }}", + "ultima_actividad": "={{ $json.nueva_sesion.ultima_actividad }}", + "etapa": "={{ $json.nueva_sesion.etapa }}", + "audio_count": "={{ $json.nueva_sesion.audio_count }}", + "imagenes_count": "={{ $json.nueva_sesion.imagenes_count }}", + "videos_count": "={{ $json.nueva_sesion.videos_count }}", + "estado": "={{ $json.nueva_sesion.estado }}", + "ejecucion_id": "={{ $json.nueva_sesion.ejecucion_id }}", + "motivo_revision": "={{ $json.nueva_sesion.motivo_revision }}", + "is_group": "={{ $json.nueva_sesion.is_group }}", + "group_jid": "={{ $json.nueva_sesion.group_jid }}", + "group_name": "={{ $json.nueva_sesion.group_name }}", + "sender_jid": "={{ $json.nueva_sesion.sender_jid }}", + "sender_phone": "={{ $json.nueva_sesion.sender_phone }}", + "sender_name": "={{ $json.nueva_sesion.sender_name }}", + "country_code": "={{ $json.nueva_sesion.country_code }}", + "pais_detectado": "={{ $json.nueva_sesion.pais_detectado }}", + "idioma_flujo": "={{ $json.nueva_sesion.idioma_flujo }}", + "tipo_reporte": "={{ $json.nueva_sesion.tipo_reporte }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -5152, + -4608 + ], + "id": "66b65b89-24b9-484a-9693-a5acecdf77a4", + "name": "Sheets - Crear sesión WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst etapa = texto(datos.sesion_activa?.etapa).toUpperCase();\nconst tipoMensaje = texto(datos.message_type).toLowerCase();\nconst accion = texto(datos.accion_flujo).toUpperCase();\n\nconst imagenesCount = Number(datos.sesion_activa?.imagenes_count || 0);\nconst videosCount = Number(datos.sesion_activa?.videos_count || 0);\n\nlet paso_decision = 'NO_DEFINIDO';\n\n// --------------------------------------------------\n// 1. Cancelación global\n// --------------------------------------------------\nif (accion === 'CANCELAR') {\n paso_decision = 'CANCELAR_SESION';\n\n// --------------------------------------------------\n// 2. Paso 1: esperando nota de voz\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_AUDIO') {\n if (tipoMensaje === 'audio') {\n paso_decision = 'GUARDAR_AUDIO';\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_AUDIO';\n } else {\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 3. Paso 2: esperando imágenes obligatorias\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_IMAGENES') {\n if (tipoMensaje === 'image') {\n paso_decision = 'GUARDAR_IMAGEN';\n\n } else if (accion === 'FOTOS_LISTAS') {\n // La validación real se hace después leyendo wa_ejecuciones_eventos.\n paso_decision = 'FOTOS_LISTAS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_IMAGENES';\n\n } else {\n // Si WhatsApp manda eventos raros durante imágenes, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 4. Paso 3: esperando videos opcionales\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_VIDEOS') {\n if (tipoMensaje === 'video') {\n paso_decision = 'GUARDAR_VIDEO';\n\n } else if (accion === 'SIN_VIDEO') {\n paso_decision = 'CERRAR_SIN_VIDEO';\n\n } else if (accion === 'LISTO') {\n paso_decision = 'CERRAR_CON_VIDEOS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_VIDEOS';\n\n } else {\n // Si WhatsApp manda eventos raros durante videos, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 5. Sesión en procesamiento\n// --------------------------------------------------\n} else if (etapa === 'PROCESANDO') {\n paso_decision = 'AVISO_PROCESANDO';\n\n// --------------------------------------------------\n// 6. Etapa no reconocida\n// --------------------------------------------------\n} else {\n paso_decision = 'ETAPA_NO_RECONOCIDA';\n}\n\nreturn [\n {\n json: {\n ...datos,\n\n etapa_actual: etapa,\n tipo_mensaje_actual: tipoMensaje,\n accion_actual: accion,\n\n imagenes_count_actual: imagenesCount,\n videos_count_actual: videosCount,\n\n paso_decision,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -7408, + -656 + ], + "id": "99ec1dc9-7dde-42fa-8be2-9dc1b3986b12", + "name": "Code - Resolver paso activo WhatsApp TEST" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + }, + "id": "c7fef2d7-50ce-467a-b125-30ae7fba3a58" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "8da042cf-54e6-460f-b505-2b17c4f4ff3c", + "leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ecf52241-ef7e-4a9a-8823-b9e3b438da61", + "leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "04433553-d0ee-4391-a4f4-926197ac4c0c", + "leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "8637dc0f-59b6-40ac-8bc4-baf869829598", + "leftValue": "={{ $json.paso_decision === 'GUARDAR_IMAGEN' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_IMAGEN" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "4096e616-2517-48ab-b6c4-2712f763ec1d", + "leftValue": "={{ $json.paso_decision === 'FOTOS_LISTAS' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "FOTOS_LISTAS" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "d1dd1d5c-8e25-4147-876e-5e7227784f6f", + "leftValue": "={{ $json.paso_decision === 'PEDIR_IMAGENES' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_IMAGENES" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "bddbb16a-0463-47dd-9a6c-03c3635043f1", + "leftValue": "={{ $json.paso_decision === 'GUARDAR_VIDEO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_VIDEO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "fe05ba35-0ccc-417a-a4e8-b907611932e4", + "leftValue": "={{ $json.paso_decision === 'CERRAR_SIN_VIDEO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CERRAR_SIN_VIDEO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "94647ac0-ee80-4c96-9f25-8a847704b6a0", + "leftValue": "={{ $json.paso_decision === 'CERRAR_CON_VIDEOS' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CERRAR_CON_VIDEOS" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "3038a656-50ae-4738-b2ff-a6276bd06522", + "leftValue": "={{ $json.paso_decision === 'PEDIR_VIDEOS' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_VIDEOS" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "1b20a524-2289-4d37-8372-9a839dadc971", + "leftValue": "={{ $json.paso_decision === 'AVISO_PROCESANDO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "AVISO_PROCESANDO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "0943bbd0-f5b7-43c4-9b01-7f6bea244894", + "leftValue": "={{ $json.paso_decision === 'ETAPA_NO_RECONOCIDA' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ETAPA_NO_RECONOCIDA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "84c163a3-3c08-4636-9104-c0e4c55b11e6", + "leftValue": "={{ $json.paso_decision === 'IGNORAR_EVENTO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "IGNORAR_EVENTO" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + -7216, + -384 + ], + "id": "7599f552-a8d6-416c-9664-6acc127fef5b", + "name": "Switch - Paso activo WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '🎙️ *Step 1 of 3 — Voice note*',\n '',\n 'I still need the required voice note to start the report.',\n '',\n 'Please send one voice note and clearly say whether this is:',\n '',\n '1. *Executed Proposal*',\n '2. *External Proposal*',\n '',\n 'Include:',\n '• Proposal/reference',\n '• Brand/client',\n '• Country',\n '• Location',\n '• Date',\n '• What was implemented or reported',\n '• Results/comments',\n '',\n '*Important:* One proposal/report at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n '🎙️ *Paso 1 de 3 — Nota de voz*',\n '',\n 'Aún necesito la nota de voz obligatoria para iniciar el reporte.',\n '',\n 'Envía una sola nota de voz e indica claramente si es:',\n '',\n '1. *Propuesta Ejecutada*',\n '2. *Propuesta Externa*',\n '',\n 'Incluye:',\n '• Propuesta/referencia',\n '• Marca/cliente',\n '• País',\n '• Ubicación',\n '• Fecha',\n '• Qué se implementó o reportó',\n '• Resultados/comentarios',\n '',\n '*Importante:* Una sola propuesta o reporte a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid,\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4784, + -2368 + ], + "id": "ae1227b1-221a-459a-9dde-32d9d2881f00", + "name": "Code - Preparar solicitud audio WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "event_id": "={{ $json.event_id }}", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "message_type": "={{ $json.message_type }}", + "texto": "={{ $json.texto }}", + "comando": "={{ $json.accion_flujo }}", + "tiene_media": "={{ $json.tiene_media }}", + "media_count": "={{ $json.media_count }}", + "raw_preview": "={{ $json.raw_preview }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_procesado": "={{ $json.fecha_procesado }}", + "session_id": "={{ $json.session_id }}", + "canal_origen": "={{ $json.canal_origen }}", + "etapa_recibida": "={{ $json.etapa_recibida }}", + "media_source_id": "={{ $json.media_source_id }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "event_id", + "displayName": "event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "message_type", + "displayName": "message_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "texto", + "displayName": "texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comando", + "displayName": "comando", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tiene_media", + "displayName": "tiene_media", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_count", + "displayName": "media_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "raw_preview", + "displayName": "raw_preview", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_procesado", + "displayName": "fecha_procesado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa_recibida", + "displayName": "etapa_recibida", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_source_id", + "displayName": "media_source_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -5072, + -2752 + ], + "id": "4882de04-332a-4d43-be18-e00085f1144a", + "name": "Sheets - Guardar evento audio WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const datos = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = datos.sesion_activa || datos.nueva_sesion || {};\n\nreturn [\n {\n json: {\n ...datos,\n\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'ACTIVA',\n\n audio_count: Number(sesion.audio_count || 0) + 1,\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n manager_telefono: datos.manager_telefono || sesion.manager_telefono || datos.sender_phone || '',\n manager_nombre: datos.manager_nombre || sesion.manager_nombre || datos.sender_name || '',\n\n is_group: datos.is_group ?? sesion.is_group ?? false,\n group_jid: datos.group_jid || sesion.group_jid || '',\n group_name: datos.group_name || sesion.group_name || '',\n sender_jid: datos.sender_jid || sesion.sender_jid || '',\n sender_phone: datos.sender_phone || sesion.sender_phone || datos.manager_telefono || '',\n sender_name: datos.sender_name || sesion.sender_name || datos.manager_nombre || '',\n\n country_code: datos.country_code || sesion.country_code || '',\n pais_detectado: datos.pais_detectado || sesion.pais_detectado || '',\n idioma_flujo: datos.idioma_flujo || sesion.idioma_flujo || 'ES',\n\n tipo_reporte: sesion.tipo_reporte || datos.tipo_reporte || '',\n\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid || ''\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4864, + -2752 + ], + "id": "00052ad8-be3a-4164-a1ec-d583c244078d", + "name": "Code - Preparar actualización audio recibido WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "estado": "={{ $json.estado }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "is_group": "={{ $json.is_group }}", + "group_jid": "={{ $json.group_jid }}", + "group_name": "={{ $json.group_name }}", + "sender_jid": "={{ $json.sender_jid }}", + "sender_phone": "={{ $json.sender_phone }}", + "sender_name": "={{ $json.sender_name }}", + "country_code": "={{ $json.country_code }}", + "pais_detectado": "={{ $json.pais_detectado }}", + "idioma_flujo": "={{ $json.idioma_flujo }}", + "tipo_reporte": "={{ $json.tipo_reporte }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "ejecucion_id": "={{ $json.ejecucion_id }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -4656, + -2752 + ], + "id": "3fa46865-3a79-47f4-baa2-54338425a397", + "name": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const datos = $('Code - Preparar actualización audio recibido WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ Voice note received.',\n '',\n '*Step 2 of 3 — Required images*',\n '',\n 'Now send one or more photos related to the proposal or report.',\n '',\n 'You can send multiple images together or in separate messages.',\n '',\n 'When you finish sending images, write: PHOTOS READY'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ Nota de voz recibida.',\n '',\n '*Paso 2 de 3 — Imágenes obligatorias*',\n '',\n 'Ahora envía una o varias fotos relacionadas a la propuesta o reporte.',\n '',\n 'Puedes enviar varias imágenes juntas o en mensajes separados.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS'\n ].join('\\n');\n}\n\nconst whatsappTo = limpiar(\n datos.whatsapp_to ||\n datos.group_jid ||\n datos.whatsapp_remote_jid ||\n datos.sender_jid ||\n ''\n);\n\nif (!whatsappTo) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar solicitud de imágenes.');\n}\n\nreturn [\n {\n json: {\n ...datos,\n\n whatsapp_to: whatsappTo,\n\n // Campo principal usado por el nodo WhatsApp\n whatsapp_text: mensaje,\n\n // Campos compatibles por seguridad\n texto_respuesta: mensaje,\n mensaje,\n text: mensaje,\n message_text: mensaje,\n\n estado_mensaje: 'SOLICITUD_IMAGENES_PREPARADA',\n solicitud_imagenes_debug: {\n idioma,\n whatsapp_to: whatsappTo,\n permite_imagenes_juntas: true\n }\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4432, + -2752 + ], + "id": "f11e8263-5435-4b96-b0fa-ca669945c715", + "name": "Code - Preparar solicitud imágenes WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nreturn [\n {\n json: {\n ...datos,\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n etapa: 'CANCELADA',\n estado: 'CANCELADA',\n motivo_revision: 'CANCELADO_POR_USUARIO',\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5040, + -3344 + ], + "id": "0f480c5f-c26a-48ca-aaa9-05e89c54558a", + "name": "Code - Preparar cancelación sesión WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "estado": "={{ $json.estado }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -4832, + -3344 + ], + "id": "f1c30bbb-3fd0-4080-bf86-1c2f3d418b2a", + "name": "Sheets - Cancelar sesión WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver sesión WhatsApp TEST');\n\nconst sesion =\n base.sesion_activa ||\n actual.sesion_activa ||\n {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = limpiar(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n ''\n);\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar mensaje de cancelación.');\n}\n\nlet whatsappTo = destinoBase;\n\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ *Report cancelled successfully.*',\n '',\n 'The current report was closed and no additional evidence will be added to that session.',\n '',\n 'When you are ready, you can start a new registration by writing:',\n '',\n '*Hey*'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ *Reporte cancelado correctamente.*',\n '',\n 'El reporte actual fue cerrado y no se agregará más evidencia a esa sesión.',\n '',\n 'Cuando estés listo, puedes iniciar un nuevo registro escribiendo:',\n '',\n '*Hey*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4576, + -3408 + ], + "id": "c4735257-3f2c-49ff-ae03-0f9260e45fd7", + "name": "Code - Preparar mensaje cancelación WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '📸 I still need at least one image.',\n '',\n 'The voice note was already received, but one evidence photo is required to complete the report.',\n '',\n '⚠️ Please send at least one image, one per message.',\n 'Do not send multiple images together.',\n '',\n 'When you finish sending images, write: PHOTOS READY'\n ].join('\\n');\n} else {\n mensaje = [\n '📸 Necesito que me envíes al menos una imagen.',\n '',\n 'La nota de voz ya fue recibida, pero para completar el reporte hace falta una foto de evidencia.',\n '',\n '⚠️ Por favor envía al menos una imagen, una por mensaje.',\n 'No envíes varias imágenes juntas.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4064, + -320 + ], + "id": "99f86945-1098-4c41-a57f-da8ba11f28bb", + "name": "Code - Preparar aviso falta imagen WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || base.nueva_sesion || {};\n\nconst rows = $input.all().map(item => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizarNumero = (valor) =>\n texto(valor)\n .replace('@s.whatsapp.net', '')\n .replace('@c.us', '')\n .replace('@g.us', '')\n .replace('@lid', '')\n .replace(/\\D/g, '');\n\nconst sessionId = texto(base.session_id || sesion.session_id);\nconst ejecucionId = texto(sesion.ejecucion_id || base.ejecucion_id || sessionId);\n\nconst ahora = new Date().toISOString();\n\nconst fechaInicioSesion = texto(\n sesion.fecha_inicio ||\n base.fecha_inicio ||\n base.sesion_activa?.fecha_inicio ||\n ''\n);\n\nconst fechaInicioMs = fechaInicioSesion\n ? new Date(fechaInicioSesion).getTime()\n : 0;\n\nconst fechaAhoraMs = Date.now();\n\nconst groupJid = texto(\n base.group_jid ||\n sesion.group_jid ||\n base.whatsapp_remote_jid ||\n ''\n);\n\nconst managerTelefono = normalizarNumero(\n base.manager_telefono ||\n base.sender_phone ||\n sesion.manager_telefono ||\n sesion.sender_phone ||\n ''\n);\n\n// Ventana máxima de seguridad para esta sesión.\n// Así no mezclamos fotos viejas del mismo grupo/persona.\n// 45 minutos es suficiente para una ejecución normal.\nconst ventanaMaximaMs = 45 * 60 * 1000;\n\nconst imagenesUnicas = [];\nconst vistos = new Set();\n\nfor (const row of rows) {\n const tipo = texto(row.message_type).toLowerCase();\n\n if (tipo !== 'image') continue;\n\n const rowSessionId = texto(row.session_id);\n const rowGroupJid = texto(row.whatsapp_remote_jid);\n const rowManager = normalizarNumero(row.manager_telefono || row.sender_phone || '');\n\n const rowFecha = texto(row.fecha_recepcion || row.fecha_procesado || '');\n const rowFechaMs = rowFecha ? new Date(rowFecha).getTime() : 0;\n\n const mismaSesion = sessionId && rowSessionId === sessionId;\n\n const mismoGrupoYManager =\n groupJid &&\n rowGroupJid === groupJid &&\n managerTelefono &&\n rowManager === managerTelefono;\n\n const dentroDeVentana =\n rowFechaMs &&\n fechaInicioMs &&\n rowFechaMs >= fechaInicioMs - 15000 &&\n rowFechaMs <= fechaAhoraMs + 15000 &&\n rowFechaMs - fechaInicioMs <= ventanaMaximaMs;\n\n const perteneceALaEjecucion =\n mismaSesion ||\n (mismoGrupoYManager && dentroDeVentana);\n\n if (!perteneceALaEjecucion) continue;\n\n const mediaId = texto(row.media_source_id || row.event_id);\n\n if (!mediaId) continue;\n if (vistos.has(mediaId)) continue;\n\n vistos.add(mediaId);\n imagenesUnicas.push(row);\n}\n\nconst countSesion = Number(sesion.imagenes_count || base.imagenes_count_actual || 0);\n\nconst imagenesCountReal = imagenesUnicas.length > 0\n ? imagenesUnicas.length\n : countSesion;\n\nconst idsImagenes = imagenesUnicas\n .map(row => texto(row.media_source_id || row.event_id))\n .filter(Boolean);\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n ultima_actividad: ahora,\n\n etapa: 'ESPERANDO_VIDEOS',\n estado: 'ACTIVA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: imagenesCountReal,\n videos_count: Number(sesion.videos_count || 0),\n\n manager_telefono: base.manager_telefono || sesion.manager_telefono || base.sender_phone || '',\n manager_nombre: base.manager_nombre || sesion.manager_nombre || base.sender_name || '',\n\n is_group: base.is_group ?? sesion.is_group ?? false,\n group_jid: base.group_jid || sesion.group_jid || '',\n group_name: base.group_name || sesion.group_name || '',\n\n sender_jid: base.sender_jid || sesion.sender_jid || '',\n sender_phone: base.sender_phone || sesion.sender_phone || base.manager_telefono || '',\n sender_name: base.sender_name || sesion.sender_name || base.manager_nombre || '',\n\n country_code: base.country_code || sesion.country_code || '',\n pais_detectado: base.pais_detectado || sesion.pais_detectado || '',\n idioma_flujo: base.idioma_flujo || sesion.idioma_flujo || 'ES',\n\n tipo_reporte: sesion.tipo_reporte || base.tipo_reporte || '',\n\n whatsapp_to: base.whatsapp_to || base.group_jid || base.whatsapp_remote_jid || '',\n\n imagenes_eventos_detectados: imagenesUnicas.length,\n imagenes_media_source_ids: idsImagenes.join(','),\n\n debug_fotos_listas: {\n session_id_actual: sessionId,\n fecha_inicio_sesion: fechaInicioSesion,\n group_jid: groupJid,\n manager_telefono: managerTelefono,\n total_rows_leidas: rows.length,\n total_imagenes_detectadas: imagenesUnicas.length,\n ids_imagenes: idsImagenes\n }\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3440, + -1168 + ], + "id": "7a4c049a-def7-4dff-95aa-62e4399ebb32", + "name": "Preparar actualización fotos listas" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "is_group": "={{ $json.is_group }}", + "group_jid": "={{ $json.group_jid }}", + "group_name": "={{ $json.group_name }}", + "sender_jid": "={{ $json.sender_jid }}", + "sender_phone": "={{ $json.sender_phone }}", + "sender_name": "={{ $json.sender_name }}", + "country_code": "={{ $json.country_code }}", + "pais_detectado": "={{ $json.pais_detectado }}", + "idioma_flujo": "={{ $json.idioma_flujo }}", + "tipo_reporte": "={{ $json.tipo_reporte }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "canal_origen": "={{ $json.canal_origen }}", + "fecha_inicio": "={{ $json.fecha_inicio }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3248, + -1168 + ], + "id": "47d1bd24-d5d1-4004-b3a9-44f322412c11", + "name": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst idioma = String(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = String(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n ''\n).trim();\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar solicitud de videos.');\n}\n\nlet whatsappTo = destinoBase;\n\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '*Step 3 of 3 — Optional videos*',\n '',\n 'You can send one or more videos of the execution if you have them.',\n '',\n '⚠️ Important: send videos one by one, in separate messages.',\n 'Do not send multiple videos together.',\n '',\n 'If you do not have videos, write: NO VIDEO',\n '',\n 'If you already sent videos and finished, write: DONE',\n ].join('\\n');\n} else {\n mensaje = [\n '*Paso 3 de 3 — Videos opcionales*',\n '',\n 'Puedes enviar uno o varios videos de la ejecución si tienes.',\n '',\n '⚠️ Importante: envía los videos uno por uno, en mensajes separados.',\n 'No envíes varios videos juntos.',\n '',\n 'Si no tienes videos, escribe: SIN VIDEO',\n '',\n 'Si enviaste videos y ya terminaste, escribe: LISTO',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n session_id: actual.session_id || base.session_id || sesion.session_id || '',\n ejecucion_id: actual.ejecucion_id || base.ejecucion_id || sesion.ejecucion_id || '',\n\n manager_telefono: actual.manager_telefono || base.manager_telefono || sesion.manager_telefono || '',\n manager_nombre: actual.manager_nombre || base.manager_nombre || sesion.manager_nombre || '',\n\n is_group: actual.is_group ?? base.is_group ?? sesion.is_group ?? false,\n group_jid: actual.group_jid || base.group_jid || sesion.group_jid || '',\n sender_phone: actual.sender_phone || base.sender_phone || sesion.sender_phone || '',\n idioma_flujo: idioma,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3024, + -1168 + ], + "id": "b039c705-d2cf-4d73-bc03-06d93432cdb2", + "name": "Code - Preparar solicitud videos WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '🎥 We are already on the optional videos step.',\n '',\n 'You can send one or more videos of the execution.',\n '',\n '⚠️ Remember: send each video in a separate message.',\n 'Do not send multiple videos together.',\n '',\n 'If you do not have videos, write: *NO VIDEO*',\n '',\n 'If you already sent videos and finished, write: *DONE*'\n ].join('\\n');\n} else {\n mensaje = [\n '🎥 Ya estamos en el paso de videos opcionales.',\n '',\n 'Puedes enviar uno o varios videos de la ejecución.',\n '',\n '⚠️ Recuerda: envía cada video en un mensaje separado.',\n 'No envíes varios videos juntos.',\n '',\n 'Si no tienes videos, escribe: *SIN VIDEO*',\n '',\n 'Si ya enviaste videos y terminaste, escribe: *LISTO*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5936, + 1504 + ], + "id": "a82ee843-061b-4462-a4f0-14f8c6975463", + "name": "Code - Preparar recordatorio videos WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'SIN_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -6288, + 896 + ], + "id": "875805e7-2d5e-4113-8895-ae3704d90c1c", + "name": "CERRAR_SIN_VIDEO" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -6080, + 896 + ], + "id": "9eb38dee-9c6d-472c-9e39-8c9e943b3fc3", + "name": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "event_id": "={{ $json.event_id }}", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "message_type": "={{ $json.message_type }}", + "texto": "={{ $json.texto }}", + "comando": "={{ $json.accion_flujo }}", + "tiene_media": "={{ $json.tiene_media }}", + "media_count": "={{ $json.media_count }}", + "raw_preview": "={{ $json.raw_preview }}", + "estado": "=VIDEO_RECIBIDO", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "session_id": "={{ $json.session_id }}", + "canal_origen": "={{ $json.canal_origen }}", + "etapa_recibida": "={{ $json.etapa_actual }}", + "media_source_id": "={{ $json.media_source_id }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "event_id", + "displayName": "event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "message_type", + "displayName": "message_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "texto", + "displayName": "texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comando", + "displayName": "comando", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tiene_media", + "displayName": "tiene_media", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_count", + "displayName": "media_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "raw_preview", + "displayName": "raw_preview", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_procesado", + "displayName": "fecha_procesado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa_recibida", + "displayName": "etapa_recibida", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_source_id", + "displayName": "media_source_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3936, + -80 + ], + "id": "7e36d0b7-09ab-4744-aaf4-9d8f01ab17fe", + "name": "Sheets - Guardar evento video WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const evento = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst videosActuales = Number(sesion.videos_count || 0);\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n evento.whatsapp_to ||\n evento.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n ...evento,\n\n session_id: base.session_id || sesion.session_id || evento.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || evento.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || evento.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n // Seguimos en videos hasta que el usuario escriba LISTO\n etapa: 'ESPERANDO_VIDEOS',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: videosActuales + 1,\n\n estado: sesion.estado || 'ACTIVA',\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: sesion.motivo_revision || '',\n\n whatsapp_to: whatsappTo\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3712, + -144 + ], + "id": "807724d9-20f0-4285-96b7-104fdcc9b752", + "name": "Code - Preparar actualización video recibido WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "estado": "={{ $json.estado }}", + "videos_count": "={{ $json.videos_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "audio_count": "={{ $json.audio_count }}", + "etapa": "={{ $json.etapa }}", + "ultima_actividad": "={{ $json.ultima_actividad }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3504, + -144 + ], + "id": "23bf12aa-4a65-4fbb-8ff5-236ff367e50e", + "name": "Sheets - Actualizar sesión video recibido WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n actual.whatsapp_to ||\n actual.group_jid ||\n actual.whatsapp_remote_jid ||\n base.whatsapp_to ||\n base.group_jid ||\n base.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar confirmación de video.');\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ Video received.',\n '',\n 'You can send more videos if needed.',\n '',\n '⚠️ Remember: send each video in a separate message.',\n 'Do not send multiple videos together.',\n '',\n 'When you finish sending videos, write: DONE',\n '',\n 'If you do not have more videos, you can also write: DONE'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ Video recibido.',\n '',\n 'Puedes enviar más videos si hace falta.',\n '',\n '⚠️ Recuerda: envía cada video en un mensaje separado.',\n 'No envíes varios videos juntos.',\n '',\n 'Cuando termines de enviar los videos, escribe: LISTO',\n '',\n 'Si no tienes más videos, también puedes escribir: LISTO'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3296, + -144 + ], + "id": "20b53cc1-f5e4-4ecd-9886-20256440653e", + "name": "Code - Preparar confirmación video WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'CON_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -6368, + 1184 + ], + "id": "6ed742b7-db83-4cd6-8652-7efbd7f991ca", + "name": "Code - Preparar cierre con videos WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -6160, + 1184 + ], + "id": "8bfcdbc6-1cdb-4452-8bd2-16ac984f6a9d", + "name": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const data = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n data.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst whatsappTo = telefonoBase.includes('@s.whatsapp.net')\n ? telefonoBase\n : `${telefonoBase.replace(/\\D/g, '')}@s.whatsapp.net`;\n\nreturn [\n {\n json: {\n ...base,\n ...data,\n\n whatsapp_to: whatsappTo,\n\n whatsapp_text: `⏳ Tu reporte ya fue recibido y está pendiente de procesamiento.\n\nPor favor espera mientras se prepara el análisis de la evidencia.`\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5872, + 1744 + ], + "id": "542f9202-df07-47a4-a4e2-707f24a2ffa3", + "name": "Code - Preparar aviso procesando WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nreturn [\n {\n json: {\n ...data,\n\n session_id_busqueda: data.session_id || '',\n estado_procesamiento: 'BUSCAR_EVENTOS_MEDIA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5104, + 1104 + ], + "id": "27e3a13d-e6e9-431d-a24f-43b08f572562", + "name": "Code - Preparar búsqueda eventos media WhatsApp TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -4848, + 1104 + ], + "id": "16b7850c-34c9-4d39-91fd-10e3fd3d8d97", + "name": "Sheets - Leer eventos WhatsApp TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const cierre = $('Code - Preparar búsqueda eventos media WhatsApp TEST').first().json || {};\nconst sessionId = String(cierre.session_id_busqueda || cierre.session_id || '').trim();\n\nconst eventos = $input.all().map(item => item.json || {});\n\nconst eventosSesion = eventos.filter(evento => {\n return String(evento.session_id || '').trim() === sessionId;\n});\n\nconst eventosMedia = eventosSesion.filter(evento => {\n const tipo = String(evento.message_type || '').toLowerCase();\n return ['audio', 'image', 'video'].includes(tipo);\n});\n\nconst audios = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'audio');\nconst imagenes = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'image');\nconst videos = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...cierre,\n\n total_eventos_sesion: eventosSesion.length,\n total_media: eventosMedia.length,\n\n audio_count_real: audios.length,\n imagenes_count_real: imagenes.length,\n videos_count_real: videos.length,\n\n eventos_audio: audios,\n eventos_imagenes: imagenes,\n eventos_videos: videos,\n\n media_lista: eventosMedia,\n\n estado_procesamiento: 'EVENTOS_MEDIA_FILTRADOS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4608, + 1104 + ], + "id": "48fffd2b-c91c-424f-823d-b11785b45673", + "name": "Code - Filtrar eventos media de sesión WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\nconst mediaLista = Array.isArray(data.media_lista) ? data.media_lista : [];\n\nconst sessionId = String(data.session_id || data.session_id_busqueda || '').trim();\nconst ejecucionId = String(data.ejecucion_id || sessionId).trim();\n\nconst items = mediaLista.map((media, index) => {\n const tipo = String(media.message_type || '').toLowerCase();\n const mime = String(media.media_mime_type || '').trim();\n\n let extension = 'bin';\n\n if (mime.includes('ogg')) extension = 'ogg';\n else if (mime.includes('mpeg')) extension = 'mp3';\n else if (mime.includes('mp4')) extension = 'mp4';\n else if (mime.includes('jpeg') || mime.includes('jpg')) extension = 'jpg';\n else if (mime.includes('png')) extension = 'png';\n else if (tipo === 'audio') extension = 'ogg';\n else if (tipo === 'image') extension = 'jpg';\n else if (tipo === 'video') extension = 'mp4';\n\n const mediaSourceId = String(\n media.media_source_id ||\n media.event_id ||\n ''\n ).trim();\n\n const remoteJid = String(\n media.whatsapp_remote_jid ||\n data.whatsapp_remote_jid ||\n data.whatsapp_to ||\n media.manager_telefono ||\n data.manager_telefono ||\n ''\n ).trim();\n\n return {\n json: {\n ...data,\n\n media_index: index + 1,\n media_total: mediaLista.length,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: media.event_id || '',\n media_source_id: mediaSourceId,\n media_type: tipo,\n media_mime_type: mime,\n media_extension: extension,\n\n whatsapp_remote_jid: remoteJid,\n\n media_file_name: `${ejecucionId}_${tipo}_${index + 1}.${extension}`,\n\n evento_original: media,\n\n estado_procesamiento: 'MEDIA_ITEM_PREPARADO'\n }\n };\n});\n\nreturn items;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4368, + 1104 + ], + "id": "4f0e23fe-0c3d-4cc1-8069-716b76ea3ce2", + "name": "Code - Separar media en items WhatsApp TEST" + }, + { + "parameters": { + "method": "POST", + "url": "https://wsp.gomezleemarketing.com/chat/getBase64FromMediaMessage/botsoporte", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "={{'C267126ABB45-4C12-B626-6BAB1833F5D7'}}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"message\": {\n \"key\": {\n \"id\": \"{{ $json.media_source_id }}\"\n }\n },\n \"convertToMp4\": true\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + -4160, + 1104 + ], + "id": "a7b7a1fa-3c27-49e2-9606-3b8148b1c443", + "name": "HTTP Request - Obtener media base64 Evolution TEST", + "alwaysOutputData": true + }, + { + "parameters": { + "jsCode": "const httpItems = $input.all();\nconst mediaItems = $('Code - Separar media en items WhatsApp TEST').all();\n\nconst buscarBase64 = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.base64 === 'string') return obj.base64;\n if (typeof obj.data === 'string') return obj.data;\n if (typeof obj.media === 'string') return obj.media;\n\n if (obj.message && typeof obj.message.base64 === 'string') return obj.message.base64;\n if (obj.response && typeof obj.response.base64 === 'string') return obj.response.base64;\n if (obj.data && typeof obj.data.base64 === 'string') return obj.data.base64;\n\n return '';\n};\n\nreturn httpItems.map((item, index) => {\n const respuesta = item.json || {};\n const mediaItem = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n let base64 = buscarBase64(respuesta);\n\n base64 = String(base64 || '').trim();\n base64 = base64.replace(/^data:.*?;base64,/, '');\n\n if (!base64) {\n throw new Error(`No se encontró base64 para media_source_id: ${mediaItem.media_source_id || 'SIN_ID'}`);\n }\n\n return {\n json: {\n ...mediaItem,\n\n base64_length: base64.length,\n estado_procesamiento: 'MEDIA_BASE64_RECUPERADA'\n },\n binary: {\n data: {\n data: base64,\n mimeType: mediaItem.media_mime_type || 'application/octet-stream',\n fileName: mediaItem.media_file_name || 'media_whatsapp.bin'\n }\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3952, + 1104 + ], + "id": "61204b9e-0419-444a-9e39-c87a56b88895", + "name": "Code - Convertir base64 a binario WhatsApp TEST" + }, + { + "parameters": { + "name": "={{ $json.media_file_name }}", + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "=1G11WZnOwPp7UC2moPE6rEvlPFl7_OoSB", + "mode": "id" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + -3712, + 1104 + ], + "id": "87632ce1-965c-4354-b06f-ac8ce766e7d4", + "name": "Drive - Subir media WhatsApp TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const driveItems = $input.all();\nconst mediaItems = $('Code - Convertir base64 a binario WhatsApp TEST').all();\n\nreturn driveItems.map((item, index) => {\n const drive = item.json || {};\n const media = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n const fileId =\n drive.id ||\n drive.fileId ||\n drive.file_id ||\n '';\n\n const webViewLink =\n drive.webViewLink ||\n drive.webContentLink ||\n (fileId ? `https://drive.google.com/file/d/${fileId}/view` : '');\n\n return {\n json: {\n ...media,\n\n drive_file_id: fileId,\n drive_file_name: media.media_file_name,\n drive_file_url: webViewLink,\n\n estado_procesamiento: 'MEDIA_GUARDADA_EN_DRIVE'\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3504, + 1104 + ], + "id": "ae4d3939-49c5-4794-bbbb-eda739313673", + "name": "Code - Preparar registro media Drive WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const mediaItems = $('Code - Preparar registro media Drive WhatsApp TEST').all();\n\nconst media = mediaItems.map(item => item.json || {});\n\nconst base = media[0] || {};\n\nconst audios = media.filter(m => String(m.media_type || '').toLowerCase() === 'audio');\nconst imagenes = media.filter(m => String(m.media_type || '').toLowerCase() === 'image');\nconst videos = media.filter(m => String(m.media_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || '',\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n\n audio_drive_urls: audios.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n imagenes_drive_urls: imagenes.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n videos_drive_urls: videos.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n\n total_media_drive: media.length,\n total_audio_drive: audios.length,\n total_imagenes_drive: imagenes.length,\n total_videos_drive: videos.length,\n\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_ANALIZAR',\n motivo_revision: 'MEDIA_RECUPERADA',\n\n estado_procesamiento: 'MEDIA_RECUPERADA_Y_REGISTRADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3088, + 1104 + ], + "id": "5e6b2d61-89b0-4d31-a371-51e38b01d88c", + "name": "Code - Consolidar media recuperada WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1962428033, + "mode": "list", + "cachedResultName": "wa_ejecuciones_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1962428033" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "media_event_id": "={{ $json.media_event_id }}", + "media_type": "={{ $json.media_type }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.drive_file_name }}", + "drive_file_id": "={{ $json.drive_file_id }}", + "drive_file_url": "={{ $json.drive_file_url }}", + "fecha_guardado": "={{ new Date().toISOString() }}", + "estado": "=MEDIA_GUARDADA_EN_DRIVE", + "media_index": "={{ $json.media_index }}", + "media_total": "={{ $json.media_total }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_event_id", + "displayName": "media_event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_type", + "displayName": "media_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "drive_file_id", + "displayName": "drive_file_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "drive_file_url", + "displayName": "drive_file_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_guardado", + "displayName": "fecha_guardado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_index", + "displayName": "media_index", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_total", + "displayName": "media_total", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3296, + 1104 + ], + "id": "71354a86-ea50-48df-8262-5882092dcb85", + "name": "Sheets - Guardar media Drive WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "estado": "={{ $json.estado }}", + "etapa": "={{ $json.etapa }}", + "ultima_actividad": "={{ $json.ultima_actividad }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -2880, + 1104 + ], + "id": "e23d8301-e104-4a02-9203-60d9f5af599b", + "name": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const data = $('Code - Consolidar media recuperada WhatsApp TEST').first().json || {};\n\nconst audioUrls = String(data.audio_drive_urls || '').trim();\nconst imagenesUrls = String(data.imagenes_drive_urls || '').trim();\nconst videosUrls = String(data.videos_drive_urls || '').trim();\n\nconst prompt = `\nAnaliza esta ejecución de propuesta usando la evidencia disponible.\n\nIMPORTANTE:\n- La nota de voz contiene la descripción principal de la ejecución.\n- Las imágenes muestran evidencia visual obligatoria.\n- Los videos son evidencia opcional adicional.\n- No inventes datos que no estén claros.\n- Si un dato no aparece, marca \"No identificado\".\n\nDebes devolver SOLO un JSON válido con esta estructura:\n\n{\n \"propuesta_referencia\": \"\",\n \"marca\": \"\",\n \"cliente\": \"\",\n \"pais\": \"\",\n \"ubicacion\": \"\",\n \"fecha_ejecucion\": \"\",\n \"que_se_implemento\": \"\",\n \"comentarios_resultados\": \"\",\n \"resumen_ejecucion\": \"\",\n \"nivel_confianza\": \"\",\n \"requiere_revision\": \"\",\n \"motivo_revision\": \"\"\n}\n\nDatos de control:\nsession_id: ${data.session_id}\nejecucion_id: ${data.ejecucion_id}\nmanager_nombre: ${data.manager_nombre}\nmanager_telefono: ${data.manager_telefono}\n\nArchivos de audio en Drive:\n${audioUrls || 'No hay audio registrado'}\n\nImágenes en Drive:\n${imagenesUrls || 'No hay imágenes registradas'}\n\nVideos en Drive:\n${videosUrls || 'No hay videos registrados'}\n`.trim();\n\nreturn [\n {\n json: {\n ...data,\n gemini_prompt: prompt,\n estado_procesamiento: 'PAQUETE_GEMINI_PREPARADO'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -2672, + 1104 + ], + "id": "28417401-35f1-4448-9305-34d82352836f", + "name": "Code - Preparar paquete análisis Gemini WhatsApp TEST" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", + "rightValue": "audio", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "ddf19bdc-2a61-4261-8950-02ea3026ba01" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "audio" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dcf1d733-7b80-4f67-b6f6-9e965a5bae24", + "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", + "rightValue": "image", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "=image" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "bb7e48e1-8383-463c-a43e-7e8e5d1ae21d", + "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", + "rightValue": "video", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "VIDEO" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + -1424, + 2224 + ], + "id": "5095c259-4595-4537-9913-9eeb1e4ce964", + "name": "Switch - Tipo media para Gemini WhatsApp TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const item = $input.item;\n\nconst data = item.json || {};\nconst binaryData = item.binary?.data;\n\nif (!binaryData) {\n throw new Error('No llegó binary.data al nodo de audio para Gemini');\n}\n\nconst geminiPromptAudio = `\nAnaliza esta nota de voz enviada por WhatsApp para el flujo de registro de propuestas de Fulgencio Fumado.\n\nObjetivo:\nExtraer información útil del reporte y clasificar correctamente el tipo de reporte.\n\nPrimero, identifica el tipo de reporte usando exactamente uno de estos valores:\n\n1. PROPUESTA_EJECUTADA\nCuando el manager reporta evidencia de una propuesta que ya fue implementada, instalada, ejecutada, realizada o activada.\n\n2. PROPUESTA_EXTERNA\nCuando el manager reporta una propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n\n3. NO_DETERMINADO\nCuando la nota de voz no permite saber claramente si es una propuesta ejecutada o una propuesta externa.\n\nReglas para clasificar:\n- Si el manager dice explícitamente \"propuesta ejecutada\", clasifica como PROPUESTA_EJECUTADA.\n- Si el manager dice que recibió una propuesta externa, referencia externa o algo fuera del banco, clasifica como PROPUESTA_EXTERNA.\n- Si solo hay una prueba de audio, saludo, ruido, conteo, información incompleta o no se entiende el objetivo, clasifica como NO_DETERMINADO.\n- No inventes el tipo de reporte.\n- Si hay duda, usa NO_DETERMINADO.\n\nAdemás, extrae la mayor cantidad posible de información útil:\n- Nombre o referencia de la propuesta\n- Marca o cliente\n- País\n- Ubicación\n- Fecha de ejecución o fecha del reporte\n- Qué se implementó o qué se está reportando\n- Comentarios o resultados observados\n- Si el manager menciona que es ejecutada, externa o no queda claro\n\nDevuelve el análisis en texto claro y estructurado, incluyendo obligatoriamente estas líneas:\n\nTipo de reporte detectado: PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\nConfianza del tipo de reporte: ALTA | MEDIA | BAJA\nMotivo del tipo de reporte: explicación breve\n\nNo inventes datos. Si algo no está claro, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_audio: geminiPromptAudio\n },\n binary: {\n data: binaryData\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -944, + 2048 + ], + "id": "7404bc52-fd09-40e9-94ab-27157f4be4ed", + "name": "Code - Preparar Gemini audio WhatsApp TEST" + }, + { + "parameters": { + "resource": "audio", + "operation": "analyze", + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "text": "={{ $json.gemini_prompt_audio }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -736, + 2048 + ], + "id": "353aba0e-8ed5-484a-b8fc-65ee7829a951", + "name": "Gemini - Analizar audio WhatsApp TEST", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini audio 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: 'audio',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_AUDIO_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": [ + -528, + 2048 + ], + "id": "fa99c19f-45ca-408d-883a-ffeb4d32ca9a", + "name": "Code - Normalizar análisis audio WhatsApp TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const input = $input.item;\nconst data = input.json || {};\nconst binaries = input.binary || {};\n\nconst binaryKeys = Object.keys(binaries);\nconst sourceBinaryKey = binaries.data ? 'data' : binaryKeys[0];\n\nif (!sourceBinaryKey || !binaries[sourceBinaryKey]) {\n throw new Error(\n `No llegó archivo binario para Gemini imagen. Binary keys recibidas: ${binaryKeys.join(', ') || 'NINGUNA'}`\n );\n}\n\nconst binaryData = {\n ...binaries[sourceBinaryKey],\n};\n\nbinaryData.fileName =\n binaryData.fileName ||\n data.media_file_name ||\n `imagen_${data.media_index || Date.now()}.jpg`;\n\nbinaryData.mimeType =\n binaryData.mimeType ||\n data.mime_type ||\n data.mimetype ||\n 'image/jpeg';\n\nconst geminiPromptImagen = `\nAnaliza esta imagen como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en la imagen\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Posible ubicación visible\n- Calidad de la implementación\n- Detalles relevantes para documentar la ejecución\n\nNo inventes datos que no sean visibles.\nSi algo no se puede identificar, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_imagen: geminiPromptImagen,\n binary_input_field: 'data',\n debug_binary_imagen: {\n binary_keys_recibidas: binaryKeys,\n source_binary_key: sourceBinaryKey,\n output_binary_key: 'data',\n file_name: binaryData.fileName,\n mime_type: binaryData.mimeType,\n },\n },\n binary: {\n data: binaryData,\n },\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -928, + 2272 + ], + "id": "3e673d5d-5aa4-4d74-8a09-eeac3fb67c97", + "name": "Code - Preparar Gemini imagen WhatsApp TEST" + }, + { + "parameters": { + "resource": "image", + "operation": "analyze", + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "text": "={{ $json.gemini_prompt_imagen }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -720, + 2272 + ], + "id": "212e7998-4787-445d-824e-5e7b3911e206", + "name": "Gemini - Analizar imagen WhatsApp TEST", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini imagen WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'image',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_IMAGEN_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -512, + 2272 + ], + "id": "7b2891dd-09dc-40e1-a470-486debfc6418", + "name": "Code - Normalizar análisis imagen WhatsApp TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const data = $json || {};\nconst binary = $binary || {};\n\nif (!binary.data) {\n throw new Error('No llegó binary.data al nodo Code - Preparar Gemini video.');\n}\n\nconst fileName =\n binary.data.fileName ||\n data.media_file_name ||\n data.file_name ||\n `video_${Date.now()}.mp4`;\n\nconst mimeType =\n binary.data.mimeType ||\n data.mime_type ||\n 'video/mp4';\n\n// Fuerza a n8n a cargar el contenido real del binario.\n// Esto evita el error de Gemini: \"Received undefined\".\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n\nif (!buffer || !Buffer.isBuffer(buffer) || buffer.length === 0) {\n throw new Error('El video llegó con metadata, pero sin contenido binario real.');\n}\n\nconst preparedBinary = await this.helpers.prepareBinaryData(\n buffer,\n fileName,\n mimeType\n);\n\nconst geminiPromptVideo = `\nAnaliza este video como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en el video\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Interacciones o movimientos relevantes\n- Posible ubicación visible\n- Calidad de la implementación\n- Resultados observables\n\nNo inventes datos que no sean visibles.\n`.trim();\n\nreturn {\n json: {\n ...data,\n media_type: 'video',\n media_file_name: fileName,\n mime_type: mimeType,\n gemini_prompt_video: geminiPromptVideo,\n },\n binary: {\n data: preparedBinary,\n },\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -1056, + 2512 + ], + "id": "7992ddf7-5394-4126-902c-0eec813ca6f3", + "name": "Code - Preparar Gemini video WhatsApp TEST" + }, + { + "parameters": { + "resource": "video", + "operation": "analyze", + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "text": "={{ $json.gemini_prompt_video }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -848, + 2512 + ], + "id": "4e0c63b2-429f-4908-8c57-69093c2db609", + "name": "Analyze video", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini video WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'video',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_VIDEO_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -640, + 2512 + ], + "id": "a05f39fa-f0df-4e8b-81f7-8ea4b9b5febc", + "name": "Code - Normalizar análisis video WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1107394537, + "mode": "list", + "cachedResultName": "wa_ejecuciones_analisis_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "media_event_id": "={{ $json.media_event_id }}", + "media_type": "={{ $json.media_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "analisis_texto": "={{ $json.analisis_texto }}", + "fecha_analisis": "={{ $json.fecha_analisis }}", + "estado": "={{ $json.estado }}", + "media_index": "={{ $json.media_index }}", + "media_total": "={{ $json.media_total }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_event_id", + "displayName": "media_event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_type", + "displayName": "media_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "analisis_texto", + "displayName": "analisis_texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_analisis", + "displayName": "fecha_analisis", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_index", + "displayName": "media_index", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_total", + "displayName": "media_total", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 144, + 2304 + ], + "id": "7a6a54c1-687f-479a-b512-8490a433b60e", + "name": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1107394537, + "mode": "list", + "cachedResultName": "wa_ejecuciones_analisis_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 3936, + 2112 + ], + "id": "73ae1420-d16f-425b-b6dd-ab4f92dd81ad", + "name": "Sheets - Leer análisis media Gemini WhatsApp TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst inputRows = $input.all().map(item => item.json || {});\nconst actual = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst extraerTextoGemini = (valor) => {\n if (valor === null || valor === undefined) return '';\n\n if (typeof valor === 'object') {\n const partes =\n valor?.content?.parts ||\n valor?.response?.content?.parts ||\n valor?.candidates?.[0]?.content?.parts ||\n valor?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(valor);\n }\n\n const raw = limpiar(valor);\n if (!raw) return '';\n\n try {\n const parsed = JSON.parse(raw);\n\n const partes =\n parsed?.content?.parts ||\n parsed?.response?.content?.parts ||\n parsed?.candidates?.[0]?.content?.parts ||\n parsed?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(parsed);\n } catch (error) {\n return raw;\n }\n};\n\nconst normalizarTipoMedia = (valor) => {\n const t = limpiar(valor).toLowerCase();\n\n if (t.includes('audio')) return 'audio';\n if (t.includes('image') || t.includes('imagen')) return 'image';\n if (t.includes('video')) return 'video';\n\n return t || 'unknown';\n};\n\nconst obtenerFecha = (row) => {\n const fecha = new Date(row.fecha_analisis || row.fecha_recepcion || row.ultima_actividad || 0);\n const time = fecha.getTime();\n return Number.isFinite(time) ? time : 0;\n};\n\n// Contextos posibles del cierre actual.\n// IMPORTANTE: NO usamos $json como prioridad, porque aquí $json puede ser la primera fila del Sheet.\nconst decisionPorSheet = getNodeJson('Code - Decidir cierre análisis por Sheet Redis TEST');\nconst validarLock = getNodeJson('Code - Validar lock análisis final TEST');\nconst validarConteo = getNodeJson('Code - Validar conteo análisis Redis TEST');\nconst cierreMedia = getNodeJson('Code - Consolidar media recuperada WhatsApp TEST');\nconst eventoPaso = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst eventoSesion = getNodeJson('Code - Resolver sesión WhatsApp TEST');\n\nlet sessionId = limpiar(\n decisionPorSheet.session_id ||\n validarLock.session_id ||\n validarConteo.session_id ||\n cierreMedia.session_id ||\n eventoPaso.session_id ||\n eventoSesion.session_id ||\n ''\n);\n\nlet ejecucionId = limpiar(\n decisionPorSheet.ejecucion_id ||\n validarLock.ejecucion_id ||\n validarConteo.ejecucion_id ||\n cierreMedia.ejecucion_id ||\n eventoPaso.ejecucion_id ||\n eventoSesion.ejecucion_id ||\n sessionId\n);\n\n// Si por cualquier razón no llegó session_id desde el contexto,\n// usamos la sesión más reciente del Sheet leído.\nif (!sessionId) {\n const grupos = {};\n\n for (const row of inputRows) {\n const sid = limpiar(row.session_id);\n if (!sid) continue;\n\n if (!grupos[sid]) {\n grupos[sid] = {\n session_id: sid,\n ejecucion_id: limpiar(row.ejecucion_id || sid),\n rows: [],\n ultima_fecha: 0\n };\n }\n\n grupos[sid].rows.push(row);\n grupos[sid].ultima_fecha = Math.max(grupos[sid].ultima_fecha, obtenerFecha(row));\n }\n\n const grupoMasReciente = Object.values(grupos)\n .sort((a, b) => b.ultima_fecha - a.ultima_fecha)[0];\n\n if (grupoMasReciente) {\n sessionId = grupoMasReciente.session_id;\n ejecucionId = grupoMasReciente.ejecucion_id || sessionId;\n }\n}\n\nif (!sessionId) {\n throw new Error('No llegó session_id para consolidar análisis de media.');\n}\n\n// Primero intentamos usar las filas filtradas por el nodo de cierre.\n// Si no vienen, usamos las filas leídas del Sheet.\nlet rows = [];\n\nif (\n Array.isArray(decisionPorSheet.analisis_rows_filtrados) &&\n decisionPorSheet.analisis_rows_filtrados.length > 0\n) {\n rows = decisionPorSheet.analisis_rows_filtrados;\n} else {\n rows = inputRows;\n}\n\n// Filtrar solo la sesión correcta.\nrows = rows.filter(row => limpiar(row.session_id) === sessionId);\n\n// Si todavía no encontró nada, usar fallback por ejecución.\nif (rows.length === 0 && ejecucionId) {\n rows = inputRows.filter(row => limpiar(row.ejecucion_id) === ejecucionId);\n}\n\n// Deduplicar por media_event_id + media_type.\nconst vistos = new Set();\nconst analisisUnicos = [];\n\nfor (const row of rows) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaType = normalizarTipoMedia(row.media_type);\n\n if (!mediaEventId || !mediaType) continue;\n\n const key = `${mediaEventId}_${mediaType}`;\n if (vistos.has(key)) continue;\n\n const textoAnalisis = extraerTextoGemini(row.analisis_texto);\n\n if (!textoAnalisis) continue;\n\n vistos.add(key);\n\n analisisUnicos.push({\n ...row,\n media_type: mediaType,\n media_index: Number(row.media_index || 0),\n media_total: Number(row.media_total || row.media_total_esperado || 0),\n analisis_texto_limpio: textoAnalisis\n });\n}\n\nanalisisUnicos.sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst audios = analisisUnicos.filter(row => row.media_type === 'audio');\nconst imagenes = analisisUnicos.filter(row => row.media_type === 'image');\nconst videos = analisisUnicos.filter(row => row.media_type === 'video');\n\nconst audiosCount = audios.length;\nconst fotosCount = imagenes.length;\nconst videosCount = videos.length;\n\nconst mediaTotalEsperado = Number(\n decisionPorSheet.media_total_esperado ||\n validarConteo.media_total_esperado ||\n cierreMedia.total_media_drive ||\n cierreMedia.total_media ||\n analisisUnicos[0]?.media_total ||\n analisisUnicos.length ||\n 0\n);\n\nconst analisisAudioTexto = audios\n .map(row => row.analisis_texto_limpio)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisImagenesTexto = imagenes\n .map((row, index) => `IMAGEN ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisVideosTexto = videos\n .map((row, index) => `VIDEO ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisConsolidado = [\n analisisAudioTexto ? `ANÁLISIS DE AUDIO:\\n${analisisAudioTexto}` : '',\n analisisImagenesTexto ? `ANÁLISIS DE IMÁGENES:\\n${analisisImagenesTexto}` : '',\n analisisVideosTexto ? `ANÁLISIS DE VIDEOS:\\n${analisisVideosTexto}` : ''\n].filter(Boolean).join('\\n\\n');\n\nif (!analisisConsolidado) {\n throw new Error(\n `No hay análisis multimedia consolidado para enviar a Gemini final. session_id=${sessionId}, ejecucion_id=${ejecucionId}, rows_filtradas=${rows.length}, input_rows=${inputRows.length}`\n );\n}\n\nconst transcripcionAudio = analisisAudioTexto || '';\n\nconst geminiPromptFinal = `\nEres un analista de evidencias de GomezLee Marketing / Fulgencio Fumado.\n\nTu tarea es consolidar el análisis de audio, imágenes y videos de un reporte recibido por WhatsApp.\n\nIMPORTANTE:\n- No inventes información.\n- Si un dato no aparece claramente, responde \"No identificado\".\n- El audio tiene prioridad para identificar propuesta, marca, país, ubicación, fecha y tipo de reporte.\n- Las imágenes y videos sirven como evidencia visual.\n- Clasifica el reporte usando exactamente uno de estos valores:\n - PROPUESTA_EJECUTADA\n - PROPUESTA_EXTERNA\n - NO_DETERMINADO\n\nDefiniciones:\n- PROPUESTA_EJECUTADA: evidencia de una propuesta ya implementada, instalada, ejecutada, realizada o activada.\n- PROPUESTA_EXTERNA: propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n- NO_DETERMINADO: la información no permite saber claramente si es ejecutada o externa.\n\nDATOS DEL REPORTE:\n- session_id: ${sessionId}\n- ejecucion_id: ${ejecucionId}\n- audios_count: ${audiosCount}\n- fotos_count: ${fotosCount}\n- videos_count: ${videosCount}\n\nANÁLISIS MULTIMEDIA CONSOLIDADO:\n${analisisConsolidado}\n\nDevuelve ÚNICAMENTE un JSON válido, sin markdown, sin explicación adicional y sin texto fuera del JSON.\n\nEl JSON debe tener exactamente esta estructura:\n\n{\n \"tipo_reporte\": \"PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\",\n \"tipo_reporte_confianza\": \"ALTA | MEDIA | BAJA\",\n \"motivo_tipo_reporte\": \"texto breve\",\n \"propuesta_referencia\": \"texto o No identificado\",\n \"marca\": \"texto o No identificado\",\n \"cliente\": \"texto o No identificado\",\n \"pais\": \"texto o No identificado\",\n \"ubicacion\": \"texto o No identificado\",\n \"fecha_ejecucion\": \"texto o No identificado\",\n \"descripcion_ejecucion\": \"texto claro y profesional\",\n \"elementos_detectados\": \"lista resumida en texto\",\n \"resumen_ia\": \"resumen ejecutivo del reporte\",\n \"comentarios_resultados\": \"texto o No identificado\",\n \"tags\": \"tags separados por coma\"\n}\n`.trim();\n\nreturn [\n {\n json: {\n ...cierreMedia,\n ...eventoSesion,\n ...eventoPaso,\n ...validarConteo,\n ...validarLock,\n ...decisionPorSheet,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_media: analisisUnicos.length,\n\n audio_count: audiosCount,\n audios_count: audiosCount,\n imagenes_count: fotosCount,\n fotos_count: fotosCount,\n videos_count: videosCount,\n\n transcripcion_audio: transcripcionAudio,\n\n analisis_audio: analisisAudioTexto,\n analisis_imagenes: analisisImagenesTexto,\n analisis_videos: analisisVideosTexto,\n\n analisis_audio_texto: analisisAudioTexto,\n analisis_imagenes_texto: analisisImagenesTexto,\n analisis_videos_texto: analisisVideosTexto,\n\n analisis_media_consolidado: analisisConsolidado,\n analisis_multimedia: analisisConsolidado,\n analisis_consolidado: analisisConsolidado,\n\n gemini_prompt_final: geminiPromptFinal,\n\n analisis_rows_filtrados: analisisUnicos,\n\n estado_analisis_media: 'ANALISIS_MEDIA_CONSOLIDADO',\n\n consolidacion_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n input_rows: inputRows.length,\n rows_filtradas: rows.length,\n total_analisis_unicos: analisisUnicos.length,\n media_total_esperado: mediaTotalEsperado,\n audios: audiosCount,\n imagenes: fotosCount,\n videos: videosCount,\n gemini_prompt_final_generado: Boolean(geminiPromptFinal),\n gemini_prompt_final_length: geminiPromptFinal.length,\n uso_decision_por_sheet: Object.keys(decisionPorSheet).length > 0,\n uso_validar_lock: Object.keys(validarLock).length > 0,\n uso_validar_conteo: Object.keys(validarConteo).length > 0,\n media: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado,\n media_file_name: row.media_file_name\n }))\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 4832, + 2144 + ], + "id": "43fee68c-e52d-4323-8175-fa95b0ed8531", + "name": "Code - Consolidar análisis media Gemini WhatsApp TEST" + }, + { + "parameters": { + "modelId": { + "__rl": true, + "value": "models/gemini-3.1-flash-lite", + "mode": "list", + "cachedResultName": "models/gemini-3.1-flash-lite" + }, + "messages": { + "values": [ + { + "content": "={{ $json.gemini_prompt_final }}" + } + ] + }, + "builtInTools": {}, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + 5024, + 2144 + ], + "id": "73c6e7dd-5ac4-4673-9780-7ab545a4e8c5", + "name": "Gemini - Generar JSON final propuesta ejecutada TEST", + "credentials": { + "googlePalmApi": { + "id": "6Jdcd9FONxG35YuJ", + "name": "Isaac - Gemini Api Gratis" + } + } + }, + { + "parameters": { + "jsCode": "const gemini = $json || {};\nconst base = $('Code - Consolidar análisis media Gemini WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst esNoIdentificado = (valor) => {\n const texto = normalizarPlano(valor);\n\n return (\n !texto ||\n texto === 'NO IDENTIFICADO' ||\n texto === 'NO DISPONIBLE' ||\n texto === 'N/A' ||\n texto === 'NA' ||\n texto === 'NULL' ||\n texto === 'UNDEFINED'\n );\n};\n\nconst valor = (campo, fallback = 'No identificado') => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return fallback;\n\n return v;\n};\n\nconst valorOpcional = (campo) => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return '';\n\n return v;\n};\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (Array.isArray(obj.content?.parts)) {\n const txt = obj.content.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.parts)) {\n const txt = obj.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.candidates)) {\n const txt = obj.candidates\n .flatMap(c => c.content?.parts || [])\n .map(p => p.text || '')\n .join('\\n')\n .trim();\n\n if (txt) return txt;\n }\n\n if (obj.message && typeof obj.message === 'object') {\n const txt = extraerTexto(obj.message);\n if (txt) return txt;\n }\n\n if (obj.data && typeof obj.data === 'object') {\n const txt = extraerTexto(obj.data);\n if (txt) return txt;\n }\n\n return JSON.stringify(obj);\n};\n\nlet texto = extraerTexto(gemini).trim();\n\ntexto = texto\n .replace(/^```json/i, '')\n .replace(/^```/i, '')\n .replace(/```$/i, '')\n .trim();\n\nlet parsed = {};\n\ntry {\n parsed = JSON.parse(texto);\n} catch (error) {\n parsed = {\n tipo_reporte: 'NO_DETERMINADO',\n tipo_reporte_confianza: 'BAJA',\n tipo_reporte_motivo: `No se pudo parsear el JSON final de Gemini: ${error.message}`,\n\n propuesta_referencia: 'No identificado',\n marca: 'No identificado',\n cliente: 'No identificado',\n pais: 'No identificado',\n ubicacion: 'No identificado',\n fecha_ejecucion: 'No identificado',\n que_se_implemento: 'No identificado',\n comentarios_resultados: '',\n resumen_ejecucion: texto || 'No se pudo parsear el JSON de Gemini.',\n evidencia_audio_resumen: base.texto_audio || '',\n evidencia_imagenes_resumen: base.texto_imagenes || '',\n evidencia_videos_resumen: base.texto_videos || '',\n nivel_confianza: 'BAJA',\n requiere_revision: 'SI',\n motivo_revision: `Error parseando JSON final: ${error.message}`\n };\n}\n\nconst normalizarTipoReporte = (valorTipo) => {\n const texto = String(valorTipo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, '_');\n\n if (\n texto === 'PROPUESTA_EJECUTADA' ||\n texto === 'EJECUTADA' ||\n texto === 'EJECUTADO'\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n if (\n texto === 'PROPUESTA_EXTERNA' ||\n texto === 'EXTERNA' ||\n texto === 'EXTERNO'\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n return 'NO_DETERMINADO';\n};\n\nconst normalizarConfianza = (valorConfianza) => {\n const texto = String(valorConfianza ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['ALTA', 'MEDIA', 'BAJA'].includes(texto)) {\n return texto;\n }\n\n return 'BAJA';\n};\n\nconst normalizarSiNo = (valorSiNo, fallback = 'SI') => {\n const texto = String(valorSiNo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['SI', 'SÍ', 'YES', 'TRUE'].includes(texto)) return 'SI';\n if (['NO', 'FALSE'].includes(texto)) return 'NO';\n\n return fallback;\n};\n\nconst limpiarReferenciaVisual = (valorRef) => {\n let textoRef = limpiar(valorRef);\n\n if (esNoIdentificado(textoRef)) return 'No identificado';\n\n textoRef = textoRef\n .replace(/_/g, ' ')\n .replace(/\\+/g, '+')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return textoRef || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valorUbicacion) => {\n const textoUbicacion = normalizarPlano(valorUbicacion);\n\n if (!textoUbicacion) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bVERSIÓN\\b/,\n /\\bCODIGO\\b/,\n /\\bCÓDIGO\\b/,\n /\\bGUATE WMC\\b/,\n /\\bPOLLO CAMPERO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(textoUbicacion));\n};\n\nconst normalizarUbicacion = (valorUbicacion) => {\n const textoUbicacion = limpiar(valorUbicacion);\n\n if (esNoIdentificado(textoUbicacion)) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(textoUbicacion)) {\n return 'No identificado';\n }\n\n return textoUbicacion;\n};\n\nconst normalizarFechaEjecucion = (valorFecha) => {\n const textoFecha = limpiar(valorFecha);\n\n if (esNoIdentificado(textoFecha)) return 'No identificado';\n\n // Si solo viene el año, no es una fecha de ejecución real.\n if (/^20\\d{2}$/.test(textoFecha)) {\n return 'No identificado';\n }\n\n return textoFecha;\n};\n\nconst limitarTexto = (valorTexto, max = 1200, fallback = '') => {\n const textoLimpio = valor(valorTexto, fallback);\n\n if (!textoLimpio) return '';\n\n return textoLimpio.length > max\n ? `${textoLimpio.slice(0, max - 3)}...`\n : textoLimpio;\n};\n\nconst ahora = new Date().toISOString();\n\nconst tipoReporte = normalizarTipoReporte(parsed.tipo_reporte);\nconst tipoReporteConfianza = normalizarConfianza(parsed.tipo_reporte_confianza);\nconst tipoReporteMotivo = valor(parsed.tipo_reporte_motivo, 'No identificado');\n\nconst requiereRevision = normalizarSiNo(parsed.requiere_revision, 'SI');\n\nconst estadoRevision = requiereRevision === 'SI'\n ? 'PENDIENTE_REVISION'\n : 'ANALISIS_COMPLETADO';\n\nconst propuestaReferenciaOriginal = valor(parsed.propuesta_referencia);\nconst propuestaReferenciaLimpia = limpiarReferenciaVisual(propuestaReferenciaOriginal);\n\nconst fechaEjecucionOriginal = valor(parsed.fecha_ejecucion);\nconst fechaEjecucionLimpia = normalizarFechaEjecucion(fechaEjecucionOriginal);\n\nconst ubicacionOriginal = valor(parsed.ubicacion);\nconst ubicacionLimpia = normalizarUbicacion(ubicacionOriginal);\n\nconst evidenciaAudio = valorOpcional(parsed.evidencia_audio_resumen || base.texto_audio);\nconst evidenciaImagenes = valorOpcional(parsed.evidencia_imagenes_resumen || base.texto_imagenes);\nconst evidenciaVideos = valorOpcional(parsed.evidencia_videos_resumen || base.texto_videos);\n\nconst elementosDetectados = [\n evidenciaImagenes,\n evidenciaVideos\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .join('\\n\\n');\n\nconst tagsSet = new Set();\n\n[\n parsed.marca,\n parsed.cliente,\n parsed.pais,\n ubicacionLimpia\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .forEach(v => tagsSet.add(v));\n\nconst tags = [...tagsSet].join(', ');\n\nconst resumenIa = limitarTexto(parsed.resumen_ejecucion, 1200, '');\nconst descripcionEjecucion = limitarTexto(parsed.que_se_implemento, 1200, 'No identificado');\nconst comentariosResultados = limitarTexto(parsed.comentarios_resultados, 900, '');\n\nreturn [\n {\n json: {\n ...base,\n\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n session_id: base.session_id || '',\n\n fecha_recepcion: base.fecha_recepcion || ahora,\n fecha_ejecucion: fechaEjecucionLimpia,\n fecha_ejecucion_original_ia: fechaEjecucionOriginal,\n\n manager_nombre: valor(base.manager_nombre),\n manager_telefono: valor(base.manager_telefono),\n canal_origen: base.canal_origen || 'WHATSAPP',\n\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza,\n tipo_reporte_motivo: tipoReporteMotivo,\n\n propuesta_referencia: propuestaReferenciaLimpia,\n propuesta_referencia_original_ia: propuestaReferenciaOriginal,\n\n propuesta_match_estado: 'PENDIENTE',\n propuesta_match_confianza: '',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_match_revision: 'PENDIENTE_MATCH_BANCO',\n\n marca: valor(parsed.marca),\n cliente: valor(parsed.cliente),\n pais: valor(parsed.pais),\n ubicacion: ubicacionLimpia,\n ubicacion_original_ia: ubicacionOriginal,\n\n comentario_original: evidenciaAudio || comentariosResultados || '',\n resumen_ia: resumenIa,\n descripcion_ejecucion: descripcionEjecucion,\n comentarios_resultados: comentariosResultados,\n elementos_detectados: elementosDetectados,\n tags,\n\n media_folder_url: base.media_folder_url || '',\n presentacion_ejecucion_url: '',\n\n fotos_count: base.total_analisis_imagenes || 0,\n videos_count: base.total_analisis_videos || 0,\n audios_count: base.total_analisis_audio || 0,\n\n estado_revision: estadoRevision,\n motivo_revision: valor(parsed.motivo_revision, ''),\n ultima_actualizacion: ahora,\n\n transcripcion_audio: evidenciaAudio,\n\n nivel_confianza: normalizarConfianza(parsed.nivel_confianza),\n requiere_revision: requiereRevision,\n\n gemini_json_raw: texto,\n\n normalizacion_json_final_debug: {\n propuesta_referencia_original: propuestaReferenciaOriginal,\n propuesta_referencia_limpia: propuestaReferenciaLimpia,\n fecha_ejecucion_original: fechaEjecucionOriginal,\n fecha_ejecucion_limpia: fechaEjecucionLimpia,\n ubicacion_original: ubicacionOriginal,\n ubicacion_limpia: ubicacionLimpia,\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 5376, + 2144 + ], + "id": "fb3640a6-3044-43a0-a3dc-21ea0221bc08", + "name": "Code - Normalizar JSON final propuesta ejecutada TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "fecha_ejecucion": "={{ $json.fecha_ejecucion }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "propuesta_referencia": "={{ $json.propuesta_referencia }}", + "propuesta_match_estado": "={{ $json.propuesta_match_estado }}", + "propuesta_match_confianza": "={{ $json.propuesta_match_confianza }}", + "propuesta_nombre_banco": "={{ $json.propuesta_nombre_banco }}", + "propuesta_link_banco": "={{ $json.propuesta_link_banco }}", + "marca": "={{ $json.marca }}", + "cliente": "={{ $json.cliente }}", + "pais": "={{ $json.pais }}", + "ubicacion": "={{ $json.ubicacion }}", + "comentario_original": "={{ $json.comentario_original }}", + "resumen_ia": "={{ $json.resumen_ia }}", + "descripcion_ejecucion": "={{ $json.descripcion_ejecucion }}", + "elementos_detectados": "={{ $json.elementos_detectados }}", + "tags": "={{ $json.tags }}", + "media_folder_url": "={{ $json.media_folder_url }}", + "presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}", + "fotos_count": "={{ $json.fotos_count }}", + "videos_count": "={{ $json.videos_count }}", + "audios_count": "={{ $json.audios_count }}", + "estado_revision": "={{ $json.estado_revision }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "ultima_actualizacion": "={{ $json.ultima_actualizacion }}", + "session_id": "={{ $json.session_id }}", + "canal_origen": "={{ $json.canal_origen }}", + "transcripcion_audio": "={{ $json.transcripcion_audio }}", + "propuesta_match_revision": "={{ $json.propuesta_match_revision }}", + "tipo_reporte": "={{ $json.tipo_reporte }}", + "tipo_reporte_confianza": "={{ $json.tipo_reporte_confianza }}", + "tipo_reporte_motivo": "={{ $json.tipo_reporte_motivo }}", + "decision_automatica_banco": "={{ $json.decision_automatica_banco }}", + "motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}", + "banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_ejecucion", + "displayName": "fecha_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_referencia", + "displayName": "propuesta_referencia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_estado", + "displayName": "propuesta_match_estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_confianza", + "displayName": "propuesta_match_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_nombre_banco", + "displayName": "propuesta_nombre_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_link_banco", + "displayName": "propuesta_link_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "marca", + "displayName": "marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "cliente", + "displayName": "cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "pais", + "displayName": "pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ubicacion", + "displayName": "ubicacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comentario_original", + "displayName": "comentario_original", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "resumen_ia", + "displayName": "resumen_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "descripcion_ejecucion", + "displayName": "descripcion_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "elementos_detectados", + "displayName": "elementos_detectados", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tags", + "displayName": "tags", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_folder_url", + "displayName": "media_folder_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "presentacion_ejecucion_url", + "displayName": "presentacion_ejecucion_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fotos_count", + "displayName": "fotos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audios_count", + "displayName": "audios_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado_revision", + "displayName": "estado_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "transcripcion_audio", + "displayName": "transcripcion_audio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_revision", + "displayName": "propuesta_match_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_confianza", + "displayName": "tipo_reporte_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_motivo", + "displayName": "tipo_reporte_motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "decision_automatica_banco", + "displayName": "decision_automatica_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo_decision_automatica", + "displayName": "motivo_decision_automatica", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "banco_actualizado_auto", + "displayName": "banco_actualizado_auto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 7120, + 2224 + ], + "id": "d69911ec-0d8c-478d-b88b-740b7d56c8d1", + "name": "Sheets - Guardar propuesta ejecutada final TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ new Date().toISOString() }}", + "etapa": "={{ 'COMPLETADO' }}", + "estado": "={{ 'ANALISIS_COMPLETADO' }}", + "motivo_revision": "={{ $json.motivo_revision || 'ANALISIS_COMPLETADO' }}", + "tipo_reporte": "={{ $json.tipo_reporte }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 14448, + 2112 + ], + "id": "f420de04-b294-4232-aebd-fd1ac4346058", + "name": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst normalizado = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst presentacion = getNodeJson('Code - Normalizar link presentación ejecución TEST');\nconst basePaso = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst actual = $json || {};\n\nlet data = {\n ...basePaso,\n ...normalizado,\n ...actual,\n ...presentacion,\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst destinoBase = limpiar(\n data.whatsapp_to ||\n data.group_jid ||\n data.whatsapp_remote_jid ||\n data.sesion_activa?.group_jid ||\n data.sesion_activa?.whatsapp_to ||\n data.sesion_activa?.whatsapp_remote_jid ||\n data.manager_telefono ||\n data.sender_phone ||\n ''\n);\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar mensaje final.');\n}\n\nlet whatsappTo = destinoBase;\n\n// IMPORTANTE:\n// Si es grupo, conservar @g.us.\n// No convertirlo a @s.whatsapp.net.\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nconst presentacionUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n ''\n);\n\nconst bloquePresentacion = presentacionUrl\n ? `\\n\\nPresentación:\\n${presentacionUrl}`\n : '';\n\nconst idioma = limpiar(\n data.idioma_flujo ||\n data.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet whatsappText = '';\n\nif (idioma === 'EN') {\n whatsappText = `✅ Report received and processed successfully.\n\nYour evidence was registered in the proposal evidence bank.${bloquePresentacion}\n\nThank you for completing the report.`;\n} else {\n whatsappText = `✅ Reporte recibido y procesado correctamente.\n\nTu evidencia fue registrada en el banco de propuestas.${bloquePresentacion}\n\nGracias por completar el reporte.`;\n}\n\nreturn [\n {\n json: {\n ...data,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: whatsappText,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 14736, + 2080 + ], + "id": "bcb3c93a-9807-450d-999d-a6cbccf497c7", + "name": "Code - Preparar mensaje final WhatsApp TEST" + }, + { + "parameters": { + "amount": 7 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 3744, + 2112 + ], + "id": "fa6815ea-190b-441f-99a3-ee23c379e9b9", + "name": "Wait", + "webhookId": "3d858751-a035-4915-8ee0-caf8f19c78a5" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 6336, + 2176 + ], + "id": "89509a04-841b-4300-a100-1fa9464f6017", + "name": "Sheets - Leer propuestas ejecutadas final TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const base = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\nconst rows = $input.all().map(item => item.json || {});\n\nconst sessionId = String(base.session_id || '').trim();\nconst ejecucionId = String(base.ejecucion_id || '').trim();\n\nconst existente = rows.find(row => {\n const rowSessionId = String(row.session_id || '').trim();\n const rowEjecucionId = String(row.ejecucion_id || '').trim();\n\n return (\n (sessionId && rowSessionId === sessionId) ||\n (ejecucionId && rowEjecucionId === ejecucionId)\n );\n});\n\nreturn [\n {\n json: {\n ...base,\n\n propuesta_final_ya_existe: Boolean(existente),\n propuesta_final_row_number: existente?.row_number || '',\n propuesta_final_existente: existente || null,\n\n estado_anti_duplicado: existente\n ? 'PROPUESTA_FINAL_YA_EXISTE'\n : 'PROPUESTA_FINAL_NUEVA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6544, + 2176 + ], + "id": "52a97f82-0554-4fae-b744-4588a07f7d60", + "name": "Code - Verificar duplicado propuesta ejecutada TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "80785332-9e50-44d5-9fd5-ecf9610876fd", + "leftValue": "={{ $json.propuesta_final_ya_existe === true }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 6752, + 2176 + ], + "id": "6afd6903-aac3-42bd-b0e4-583a43f92a68", + "name": "IF - Propuesta final ya existe TEST" + }, + { + "parameters": { + "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '10PZtoMFuiXczGM9Itbcjce4EKBUhiztFbXZiTWX2DKc';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7328, + 2192 + ], + "id": "1edd49c5-4078-4e39-b68e-56798507020d", + "name": "Code - Preparar copia presentación ejecución TEST" + }, + { + "parameters": { + "operation": "copy", + "fileId": { + "__rl": true, + "value": "={{ $json.slides_template_id }}", + "mode": "id" + }, + "name": "={{ $json.presentation_name }}", + "sameFolder": false, + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "={{ $json.execution_folder_id }}", + "mode": "id" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 8880, + 2192 + ], + "id": "d9274651-56f7-49d6-a4fe-ae4c05386bb9", + "name": "Drive - Copiar plantilla presentación ejecución TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const base = $('Code - Preparar copia presentación ejecución TEST').first().json || {};\nconst drive = $json || {};\n\nconst presentationId =\n drive.id ||\n drive.fileId ||\n drive.presentationId ||\n drive.data?.id ||\n '';\n\nif (!presentationId) {\n throw new Error('No se encontró el ID de la presentación copiada.');\n}\n\nconst presentationUrl =\n drive.webViewLink ||\n drive.webUrl ||\n `https://docs.google.com/presentation/d/${presentationId}/edit`;\n\nreturn [\n {\n json: {\n ...base,\n\n presentation_id: presentationId,\n presentacion_ejecucion_url: presentationUrl,\n\n estado_presentacion: 'PRESENTACION_CREADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 9152, + 2192 + ], + "id": "8195f1d0-b75f-4335-a6a4-84c66875c9d6", + "name": "Code - Normalizar link presentación ejecución TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}", + "ultima_actualizacion": "={{ new Date().toISOString() }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "session_id": "={{ $json.session_id }}", + "media_folder_url": "={{ $json.media_folder_url }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_ejecucion", + "displayName": "fecha_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_referencia", + "displayName": "propuesta_referencia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_estado", + "displayName": "propuesta_match_estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_confianza", + "displayName": "propuesta_match_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_nombre_banco", + "displayName": "propuesta_nombre_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_link_banco", + "displayName": "propuesta_link_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "marca", + "displayName": "marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "cliente", + "displayName": "cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "pais", + "displayName": "pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ubicacion", + "displayName": "ubicacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comentario_original", + "displayName": "comentario_original", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "resumen_ia", + "displayName": "resumen_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "descripcion_ejecucion", + "displayName": "descripcion_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "elementos_detectados", + "displayName": "elementos_detectados", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tags", + "displayName": "tags", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_folder_url", + "displayName": "media_folder_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "presentacion_ejecucion_url", + "displayName": "presentacion_ejecucion_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fotos_count", + "displayName": "fotos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audios_count", + "displayName": "audios_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado_revision", + "displayName": "estado_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "transcripcion_audio", + "displayName": "transcripcion_audio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_revision", + "displayName": "propuesta_match_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_confianza", + "displayName": "tipo_reporte_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_motivo", + "displayName": "tipo_reporte_motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "decision_automatica_banco", + "displayName": "decision_automatica_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo_decision_automatica", + "displayName": "motivo_decision_automatica", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "banco_actualizado_auto", + "displayName": "banco_actualizado_auto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 12368, + 2048 + ], + "id": "2bbb6c3f-2aa6-4093-9312-ac01e02dd67c", + "name": "Sheets - Actualizar link presentación ejecución TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst carpetaPreparada = getNodeJson('Code - Preparar carpeta ejecución TEST');\nconst carpetaDrive = getNodeJson('Drive - Crear carpeta ejecución TEST');\nconst carpetaNormalizada = getNodeJson('Code - Normalizar carpeta ejecución TEST');\n\nconst copiaPresentacion = getNodeJson('Drive - Copiar plantilla presentación ejecución TEST');\nconst linkPresentacionNormalizado = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...carpetaPreparada,\n ...carpetaDrive,\n ...carpetaNormalizada,\n ...copiaPresentacion,\n ...linkPresentacionNormalizado,\n ...actual,\n ...match,\n ...enriquecido\n};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = limpiar(valor);\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n if (texto.toLowerCase() === 'no disponible') return fallback;\n\n return texto;\n};\n\nconst cortar = (valor, max = 900) => {\n const texto = limpiarTexto(valor, 'No disponible');\n return texto.length > max ? texto.slice(0, max - 3) + '...' : texto;\n};\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n ''\n);\n\nconst matchConfianza = Number(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n 0\n);\n\nconst matchAlta =\n matchEstado === 'MATCH_ALTA_CONFIANZA' ||\n matchConfianza >= 85;\n\nconst propuestaNombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n ''\n);\n\nconst propuestaReferenciaEnriquecida = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n jsonFinal.propuesta_referencia ||\n ''\n);\n\nconst propuestaReferenciaSlides = limpiarReferenciaVisual(\n matchAlta && propuestaNombreBanco\n ? propuestaNombreBanco\n : propuestaReferenciaEnriquecida\n);\n\nconst ubicacionSlides = limpiarTexto(\n enriquecido.ubicacion ||\n data.ubicacion ||\n jsonFinal.ubicacion,\n 'No identificado'\n);\n\nconst fechaEjecucionSlides = limpiarTexto(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n jsonFinal.fecha_ejecucion,\n 'No identificado'\n);\n\n// --------------------------------------------------\n// Resolver carpeta de evidencias\n// --------------------------------------------------\n\nconst mediaFolderId = limpiar(\n data.media_folder_id ||\n data.execution_folder_id ||\n data.folder_id ||\n data.id ||\n carpetaNormalizada.media_folder_id ||\n carpetaNormalizada.execution_folder_id ||\n carpetaNormalizada.folder_id ||\n carpetaDrive.id ||\n ''\n);\n\nlet mediaFolderUrl = limpiar(\n data.media_folder_url ||\n data.execution_folder_url ||\n data.folder_url ||\n carpetaNormalizada.media_folder_url ||\n carpetaNormalizada.execution_folder_url ||\n carpetaNormalizada.folder_url ||\n carpetaDrive.webViewLink ||\n ''\n);\n\nif (!mediaFolderUrl && mediaFolderId) {\n mediaFolderUrl = `https://drive.google.com/drive/folders/${mediaFolderId}`;\n}\n\n// --------------------------------------------------\n// Resolver link de presentación\n// --------------------------------------------------\n\nconst presentationId = limpiar(\n data.presentation_id ||\n data.presentacion_id ||\n linkPresentacionNormalizado.presentation_id ||\n copiaPresentacion.id ||\n actual.presentation_id ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n data.presentationUrl ||\n data.link_presentacion ||\n linkPresentacionNormalizado.presentacion_ejecucion_url ||\n linkPresentacionNormalizado.presentation_url ||\n linkPresentacionNormalizado.webViewLink ||\n copiaPresentacion.webViewLink ||\n actual.presentacion_ejecucion_url ||\n actual.presentation_url ||\n actual.webViewLink ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\nif (!presentationId) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nconst 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\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 9360, + 2192 + ], + "id": "192b9018-dba7-4b23-9c7c-6503704186a6", + "name": "Code - Preparar reemplazos Slides ejecución TEST" + }, + { + "parameters": { + "method": "POST", + "url": "={{ 'https://slides.googleapis.com/v1/presentations/' + $json.presentation_id + ':batchUpdate' }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleOAuth2Api", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { requests: $json.slides_replace_requests } }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 9632, + 2128 + ], + "id": "3fd9c926-7369-4d05-a470-827e813a4d76", + "name": "HTTP Request - Reemplazar textos Slides ejecución TEST", + "credentials": { + "googleOAuth2Api": { + "id": "eHseMeH39kRcXgOF", + "name": "Google account 2" + } + } + }, + { + "parameters": { + "jsCode": "const base = $('Code - Preparar reemplazos Slides ejecución TEST').first().json || {};\nconst respuestaSlides = $json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n slides_batch_update_response: respuestaSlides,\n estado_presentacion: 'TEXTOS_SLIDES_REEMPLAZADOS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 10240, + 2128 + ], + "id": "47ecd981-3ce3-4462-87f5-fc4e3d5d628c", + "name": "Code - Normalizar respuesta Slides ejecución TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver sesión WhatsApp TEST');\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = limpiar(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n ''\n);\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar mensaje de cancelación sin sesión.');\n}\n\nlet whatsappTo = destinoBase;\n\nif (whatsappTo.includes('@g.us')) {\n whatsappTo = whatsappTo;\n} else if (whatsappTo.includes('@s.whatsapp.net')) {\n whatsappTo = whatsappTo;\n} else {\n whatsappTo = `${whatsappTo.replace(/\\D/g, '')}@s.whatsapp.net`;\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n 'ℹ️ *There is no active report to cancel.*',\n '',\n 'You can start a new evidence registration whenever you are ready.',\n '',\n 'Write:',\n '',\n '*Hey*'\n ].join('\\n');\n} else {\n mensaje = [\n 'ℹ️ *No hay un reporte activo para cancelar.*',\n '',\n 'Puedes iniciar un nuevo registro de evidencias cuando estés listo.',\n '',\n 'Escribe:',\n '',\n '*Hey*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: whatsappTo,\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4832, + -3792 + ], + "id": "3247cab6-8e96-4c06-a7f2-9f504d6e5005", + "name": "Code - Preparar sin sesión para cancelar WhatsApp TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -3712, + -1168 + ], + "id": "41f82dcf-7d4b-4c40-b3a9-98a03a322dcf", + "name": "Sheets - Leer eventos sesión fotos WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = String(valor ?? '').trim();\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n\n return texto;\n};\n\nconst limpiarNombreArchivo = (valor) => {\n return limpiarTexto(valor)\n .replace(/[\\\\/:*?\"<>|#%{}~&]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 120);\n};\n\nconst fechaBase = limpiarTexto(\n data.fecha_ejecucion ||\n data.fecha_recepcion ||\n data.ultima_actualizacion ||\n new Date().toISOString()\n);\n\nconst fecha = fechaBase.slice(0, 10);\n\nconst marca = limpiarNombreArchivo(data.marca);\nconst cliente = limpiarNombreArchivo(data.cliente);\nconst sessionId = limpiarNombreArchivo(data.session_id || data.ejecucion_id);\n\nconst folderName = `${fecha} - ${marca} - ${cliente} - ${sessionId}`;\n\n// Carpeta base PROPUESTAS EJECUTADAS\nconst parentFolderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nreturn [\n {\n json: {\n ...data,\n\n execution_folder_name: folderName,\n execution_parent_folder_id: parentFolderId\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7536, + 2192 + ], + "id": "07051572-ab5b-4482-957d-da55178ec248", + "name": "Code - Preparar carpeta ejecución TEST" + }, + { + "parameters": { + "resource": "folder", + "name": "={{ $json.execution_folder_name }}", + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "={{ $json.execution_parent_folder_id }}", + "mode": "id" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 7744, + 2192 + ], + "id": "c46d6636-05b2-4cd2-9db0-4cc8270786ac", + "name": "Drive - Crear carpeta ejecución TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const carpeta = $json || {};\nconst base = $('Code - Preparar carpeta ejecución TEST').first().json || {};\n\nconst folderId =\n carpeta.id ||\n carpeta.fileId ||\n carpeta.folderId ||\n '';\n\nconst folderUrl = folderId\n ? `https://drive.google.com/drive/folders/${folderId}`\n : '';\n\nif (!folderId) {\n throw new Error('No se pudo obtener el ID de la carpeta de ejecución.');\n}\n\nreturn [\n {\n json: {\n ...base,\n\n execution_folder_id: folderId,\n execution_folder_url: folderUrl,\n\n media_folder_id: folderId,\n media_folder_url: folderUrl\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7952, + 2192 + ], + "id": "78fded8e-3963-458f-bafe-001eaf553022", + "name": "Code - Normalizar carpeta ejecución TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst folderId = texto(data.execution_folder_id || data.media_folder_id);\n\nif (!folderId) {\n throw new Error('No existe execution_folder_id/media_folder_id para mover la multimedia.');\n}\n\nconst archivos = [];\nconst vistos = new Set();\n\nconst extraerFileIdDesdeUrl = (valor) => {\n const url = texto(valor);\n if (!url) return '';\n\n // Formato: https://drive.google.com/file/d/FILE_ID/view\n const matchFile = url.match(/\\/file\\/d\\/([^/]+)/);\n if (matchFile?.[1]) return matchFile[1];\n\n // Formato: https://drive.google.com/open?id=FILE_ID\n const matchOpen = url.match(/[?&]id=([^&]+)/);\n if (matchOpen?.[1]) return matchOpen[1];\n\n // Si ya viene como ID limpio\n if (!url.includes('http') && url.length > 15) return url;\n\n return '';\n};\n\nconst separarLista = (valor) => {\n return texto(valor)\n .split(/[\\n,]+/)\n .map(v => texto(v))\n .filter(Boolean);\n};\n\nconst agregarArchivo = (fileIdOrUrl, tipo) => {\n const fileId = extraerFileIdDesdeUrl(fileIdOrUrl);\n\n if (!fileId) return;\n if (vistos.has(fileId)) return;\n\n vistos.add(fileId);\n\n archivos.push({\n json: {\n ...data,\n\n move_file_id: fileId,\n move_file_type: tipo,\n\n execution_folder_id: folderId,\n media_folder_id: folderId,\n media_folder_url: data.media_folder_url,\n execution_folder_url: data.execution_folder_url\n }\n });\n};\n\n// 1. Archivo actual del item, si existe\nagregarArchivo(data.drive_file_id, data.media_type || 'media');\n\n// 2. URLs acumuladas por tipo\nfor (const url of separarLista(data.audio_drive_urls)) {\n agregarArchivo(url, 'audio');\n}\n\nfor (const url of separarLista(data.imagenes_drive_urls)) {\n agregarArchivo(url, 'image');\n}\n\nfor (const url of separarLista(data.videos_drive_urls)) {\n agregarArchivo(url, 'video');\n}\n\n// 3. Si tienes otros campos futuros\nfor (const url of separarLista(data.media_drive_urls)) {\n agregarArchivo(url, 'media');\n}\n\nif (archivos.length === 0) {\n return [\n {\n json: {\n ...data,\n media_move_status: 'SIN_ARCHIVOS_PARA_MOVER',\n media_move_debug: {\n drive_file_id: data.drive_file_id || '',\n audio_drive_urls: data.audio_drive_urls || '',\n imagenes_drive_urls: data.imagenes_drive_urls || '',\n videos_drive_urls: data.videos_drive_urls || ''\n }\n }\n }\n ];\n}\n\nreturn archivos;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 8160, + 2192 + ], + "id": "a8b7b615-fb63-4e02-9b80-31efb5109ad8", + "name": "Code - Preparar media para mover carpeta TEST" + }, + { + "parameters": { + "operation": "move", + "fileId": { + "__rl": true, + "value": "={{ $json.move_file_id }}", + "mode": "id" + }, + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "={{ $json.execution_folder_id }}", + "mode": "id" + } + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 8368, + 2192 + ], + "id": "7797be31-dc35-4c58-b38e-224c0449c435", + "name": "Drive - Mover media a carpeta ejecución TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const movido = $json || {};\nconst base = $('Code - Preparar media para mover carpeta TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n media_move_status: 'MEDIA_MOVIDA_A_CARPETA',\n moved_file_id: base.move_file_id,\n moved_file_type: base.move_file_type,\n moved_file_url: base.move_file_url,\n\n drive_move_response_id: movido.id || movido.fileId || base.move_file_id\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 8576, + 2192 + ], + "id": "ff7562ea-6586-4dbf-955e-8b4ef4061a14", + "name": "Code - Confirmar media movida carpeta TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Preparar reemplazos Slides ejecución TEST');\n\nconst data = {\n ...base,\n ...actual,\n\n // Aseguramos estos campos desde el nodo base si el nodo Slides no los devuelve\n presentation_id: actual.presentation_id || base.presentation_id,\n imagenes_drive_urls: actual.imagenes_drive_urls || base.imagenes_drive_urls,\n imagen_drive_urls: actual.imagen_drive_urls || base.imagen_drive_urls,\n media_drive_urls: actual.media_drive_urls || base.media_drive_urls,\n};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst extraerFileIdDesdeUrl = (valor) => {\n const url = texto(valor);\n if (!url) return '';\n\n const matchFile = url.match(/\\/file\\/d\\/([^/]+)/);\n if (matchFile?.[1]) return matchFile[1];\n\n const matchOpen = url.match(/[?&]id=([^&]+)/);\n if (matchOpen?.[1]) return matchOpen[1];\n\n if (!url.includes('http') && url.length > 15) return url;\n\n return '';\n};\n\nconst separarLista = (valor) => {\n return texto(valor)\n .split(/[\\n,]+/)\n .map(v => texto(v))\n .filter(Boolean);\n};\n\nconst imagenesUrls = separarLista(\n data.imagenes_drive_urls ||\n data.imagen_drive_urls ||\n data.media_drive_urls ||\n ''\n);\n\nconst imageFileIds = imagenesUrls\n .map(extraerFileIdDesdeUrl)\n .filter(Boolean)\n .slice(0, 25)\n\nif (!data.presentation_id) {\n throw new Error('No llegó presentation_id para insertar imágenes en Slides.');\n}\n\nreturn [\n {\n json: {\n ...data,\n\n apps_script_insert_images_payload: {\n secret: 'glm_fulgencio_slides_2026_seguro',\n presentation_id: data.presentation_id,\n image_file_ids: imageFileIds\n },\n\n imagenes_para_slides_count: imageFileIds.length,\n imagenes_para_slides_ids: imageFileIds,\n\n estado_insertar_imagenes: 'PAYLOAD_INSERTAR_IMAGENES_PREPARADO'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 9840, + 2128 + ], + "id": "93846b66-2092-43ec-b57d-1347e3947c42", + "name": "Code - Preparar payload insertar imágenes Slides TEST" + }, + { + "parameters": { + "method": "POST", + "url": "https://script.google.com/macros/s/AKfycbyw76HYjYMvq0KXA9IN5S8nRWk0drPkjKSK2SAbCL9Ha8is3vFZMyR6Ldby4c3YVs7J/exec", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.apps_script_insert_images_payload }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 10048, + 2128 + ], + "id": "573fc25d-7dae-42e1-8025-1e38d3394c17", + "name": "HTTP - Insertar imágenes en Slides TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n actual.whatsapp_to ||\n actual.group_jid ||\n actual.whatsapp_remote_jid ||\n base.whatsapp_to ||\n base.group_jid ||\n base.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar aviso de procesamiento.');\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '⏳ *Report received.*',\n '',\n 'We are now processing your evidence.',\n '',\n 'This may take a moment while we analyze the media, organize the files and generate the presentation.',\n '',\n 'I will send you the final link when everything is ready.'\n ].join('\\n');\n} else {\n mensaje = [\n '⏳ *Reporte recibido.*',\n '',\n 'Estamos procesando tus evidencias.',\n '',\n 'Esto puede tomar un momento mientras analizamos los archivos, organizamos la carpeta y generamos la presentación.',\n '',\n 'Te enviaré el link final cuando todo esté listo.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_text: mensaje,\n\n aviso_procesando_preparado: true\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5824, + 1072 + ], + "id": "65715659-8334-4447-9e92-8aa372ed345b", + "name": "Code - Preparar aviso procesando WhatsApp TEST1" + }, + { + "parameters": { + "jsCode": "const respuesta = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nlet base =\n getNodeJson('Code - Preparar aviso procesando WhatsApp TEST') ||\n getNodeJson('Code - Preparar aviso procesando WhatsApp TEST1') ||\n {};\n\nif (!base || Object.keys(base).length === 0) {\n base = getNodeJson('Code - Resolver paso activo WhatsApp TEST') || {};\n}\n\nreturn [\n {\n json: {\n ...base,\n\n aviso_procesando_enviado: true,\n aviso_procesando_response: respuesta\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5376, + 1072 + ], + "id": "e9bac53c-7375-4375-830c-dee36d28b1e4", + "name": "Code - Restaurar contexto aviso procesando TEST" + }, + { + "parameters": { + "method": "POST", + "url": "={{'https://wsp.gomezleemarketing.com'}}/message/sendText/{{'botsoporte'}}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "={{'C267126ABB45-4C12-B626-6BAB1833F5D7'}}" + } + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "number", + "value": "={{ (() => {\n const limpiar = (valor) => String(valor ?? '').trim();\n\n const candidatos = [\n $json.whatsapp_to,\n $json.group_jid,\n $json.whatsapp_remote_jid,\n $json.sesion_activa?.group_jid,\n $json.sesion_activa?.whatsapp_to,\n $json.sesion_activa?.whatsapp_remote_jid,\n $json.manager_telefono,\n $json.sender_phone\n ];\n\n let raw = limpiar(candidatos.find(v => limpiar(v)));\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n // Si ya viene como grupo, conservarlo.\n if (raw.includes('@g.us')) {\n return raw;\n }\n\n // Si viene como usuario WhatsApp, validar si realmente era un grupo.\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n // Los grupos de WhatsApp suelen venir como 120363...\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n})() }}" + }, + { + "name": "delay", + "value": "={{ 1000 }}" + }, + { + "name": "text", + "value": "={{ $json.whatsapp_text }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + -5600, + 1072 + ], + "id": "bafc62ac-98e2-476f-ac16-0aa906c03f33", + "name": "HTTP - Enviar aviso procesando WhatsApp TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "options": { + "dataLocationOnSheet": { + "values": { + "rangeDefinition": "detectAutomatically" + } + }, + "outputFormatting": { + "values": { + "general": "UNFORMATTED_VALUE", + "date": "FORMATTED_STRING" + } + }, + "returnFirstMatch": false + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 5584, + 2144 + ], + "id": "9bbb9e2c-1e27-4911-a836-e392c7e523b3", + "name": "Sheets - Leer banco propuestas Fulgencio TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst propuesta =\n getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST') ||\n $json ||\n {};\n\nconst bancoRows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) => {\n return limpiar(valor)\n .replace(/\\+/g, ' mas ')\n .replace(/_/g, ' ')\n .replace(/-/g, ' ')\n .replace(/\\//g, ' ')\n .replace(/\\./g, ' ')\n .replace(/\\bGUATE\\b/gi, ' Guatemala ')\n .replace(/\\bGUA\\b/gi, ' Guatemala ')\n .replace(/\\bGT\\b/gi, ' Guatemala ')\n .replace(/\\bRD\\b/gi, ' Republica Dominicana ')\n .replace(/\\bREP DOM\\b/gi, ' Republica Dominicana ')\n .replace(/\\bREP DOMINICANA\\b/gi, ' Republica Dominicana ')\n .replace(/\\bDOMINICANA\\b/gi, ' Republica Dominicana ')\n .replace(/\\bCR\\b/gi, ' Costa Rica ')\n .replace(/\\bPAN\\b/gi, ' Panama ')\n .replace(/\\bPTY\\b/gi, ' Panama ')\n .replace(/\\bSV\\b/gi, ' El Salvador ')\n .replace(/\\bSALVADOR\\b/gi, ' El Salvador ')\n .replace(/\\bHND\\b/gi, ' Honduras ')\n .replace(/\\bNIC\\b/gi, ' Nicaragua ')\n .replace(/\\bWMC\\b/gi, ' WMC ')\n .replace(/\\bRTM\\b/gi, ' RTM ')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst tokens = (valor) => {\n const stopwords = new Set([\n 'de', 'del', 'la', 'el', 'los', 'las', 'y', 'en', 'para', 'por', 'con',\n 'una', 'un', 'uno', 'dos', 'tres', 'the', 'of', 'and', 'for', 'to', 'a',\n 'propuesta', 'presentacion', 'presentación', 'ejecucion', 'ejecución',\n 'ejecutada', 'ejecutado', 'reporte', 'evidencia', 'version', 'versión',\n 'archivo', 'link', 'slide', 'slides', 'prueba'\n ]);\n\n return normalizar(valor)\n .split(' ')\n .map(t => t.trim())\n .filter(Boolean)\n .filter(t => t.length >= 3 || /^v\\d+$/i.test(t))\n .filter(t => !stopwords.has(t));\n};\n\nconst tokenSet = (valor) => new Set(tokens(valor));\n\nconst similitudTokens = (a, b) => {\n const ta = tokenSet(a);\n const tb = tokenSet(b);\n\n if (ta.size === 0 || tb.size === 0) return 0;\n\n let interseccion = 0;\n\n for (const token of ta) {\n if (tb.has(token)) interseccion++;\n }\n\n const union = new Set([...ta, ...tb]).size;\n\n return union === 0 ? 0 : interseccion / union;\n};\n\nconst coberturaTokens = (textoBase, textoContra) => {\n const base = [...tokenSet(textoBase)];\n const contra = tokenSet(textoContra);\n\n if (base.length === 0) return 0;\n\n const encontrados = base.filter(t => contra.has(t));\n\n return encontrados.length / base.length;\n};\n\nconst contarTokensComunes = (a, b) => {\n const ta = tokenSet(a);\n const tb = tokenSet(b);\n\n let comunes = 0;\n\n for (const token of ta) {\n if (tb.has(token)) comunes++;\n }\n\n return comunes;\n};\n\nconst obtenerCampo = (row, posiblesNombres) => {\n const keys = Object.keys(row || {});\n\n for (const nombre of posiblesNombres) {\n const exacto = keys.find(k => normalizar(k) === normalizar(nombre));\n if (exacto && limpiar(row[exacto])) return limpiar(row[exacto]);\n }\n\n for (const nombre of posiblesNombres) {\n const parcial = keys.find(k => normalizar(k).includes(normalizar(nombre)));\n if (parcial && limpiar(row[parcial])) return limpiar(row[parcial]);\n }\n\n return '';\n};\n\nconst extraerVersion = (valor) => {\n const texto = normalizar(valor);\n const match = texto.match(/\\bv\\d+\\b/);\n return match ? match[0].toUpperCase() : '';\n};\n\nconst extraerAnio = (valor) => {\n const texto = limpiar(valor);\n const match = texto.match(/\\b20\\d{2}\\b/);\n return match ? match[0] : '';\n};\n\nconst contienePais = (texto, pais) => {\n const t = normalizar(texto);\n const p = normalizar(pais);\n\n if (!p) return false;\n\n if (t.includes(p)) return true;\n\n if (p.includes('guatemala') && t.includes('guatemala')) return true;\n if (p.includes('republica dominicana') && t.includes('republica dominicana')) return true;\n if (p.includes('costa rica') && t.includes('costa rica')) return true;\n if (p.includes('panama') && t.includes('panama')) return true;\n if (p.includes('salvador') && t.includes('salvador')) return true;\n if (p.includes('honduras') && t.includes('honduras')) return true;\n if (p.includes('nicaragua') && t.includes('nicaragua')) return true;\n\n return false;\n};\n\nconst tipoReporte = limpiar(propuesta.tipo_reporte).toUpperCase();\n\nif (tipoReporte !== 'PROPUESTA_EJECUTADA') {\n return [\n {\n json: {\n ...propuesta,\n\n propuesta_match_estado:\n tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'NO_APLICA_PROPUESTA_EXTERNA'\n : 'NO_APLICA_NO_DETERMINADO',\n\n propuesta_match_confianza: '0',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_banco_row_number: '',\n propuesta_banco_enlaces_existentes: '',\n propuesta_banco_file_id: '',\n\n propuesta_match_revision:\n tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'NO_REQUIERE_MATCH_BANCO'\n : 'REQUIERE_REVISION_TIPO_NO_DETERMINADO',\n\n match_banco_debug: {\n motivo: 'No se ejecutó match porque el tipo_reporte no es PROPUESTA_EJECUTADA.',\n tipo_reporte: tipoReporte || 'NO_DETERMINADO'\n }\n }\n }\n ];\n}\n\nconst referencia = limpiar(propuesta.propuesta_referencia);\nconst marca = limpiar(propuesta.marca);\nconst cliente = limpiar(propuesta.cliente);\nconst pais = limpiar(propuesta.pais);\nconst ubicacion = limpiar(propuesta.ubicacion);\nconst descripcion = limpiar(propuesta.descripcion_ejecucion || propuesta.resumen_ia);\nconst elementosDetectados = limpiar(propuesta.elementos_detectados);\nconst comentarioOriginal = limpiar(propuesta.comentario_original);\nconst transcripcionAudio = limpiar(propuesta.transcripcion_audio);\nconst textoAudio = limpiar(propuesta.texto_audio);\nconst textoImagenes = limpiar(propuesta.texto_imagenes);\nconst tags = limpiar(propuesta.tags);\n\nconst textoPropuestaCompleto = [\n referencia,\n marca,\n cliente,\n pais,\n ubicacion,\n descripcion,\n elementosDetectados,\n comentarioOriginal,\n transcripcionAudio,\n textoAudio,\n textoImagenes,\n tags\n].filter(Boolean).join(' ');\n\nconst textoClavePropuesta = [\n referencia,\n marca,\n cliente,\n pais,\n elementosDetectados,\n textoImagenes,\n tags\n].filter(Boolean).join(' ');\n\nconst versionPropuesta = extraerVersion(textoPropuestaCompleto);\nconst anioPropuesta = extraerAnio(textoPropuestaCompleto);\n\nconst candidatos = [];\n\nfor (const [index, row] of bancoRows.entries()) {\n const bancoRowNumber = String(index + 2);\n\n const bancoNombre = obtenerCampo(row, [\n 'NOMBRE',\n 'propuesta',\n 'nombre propuesta',\n 'nombre de propuesta',\n 'titulo',\n 'título',\n 'nombre',\n 'referencia',\n 'brief',\n 'idea'\n ]);\n\n const bancoTipoAccion = obtenerCampo(row, [\n 'TIPO DE ACCION',\n 'TIPO DE ACCIÓN',\n 'tipo accion',\n 'tipo acción'\n ]);\n\n const bancoCliente = obtenerCampo(row, [\n 'CLIENTE',\n 'cliente',\n 'client'\n ]);\n\n const bancoMarca = obtenerCampo(row, [\n 'MARCA',\n 'marca',\n 'brand'\n ]);\n\n const bancoPais = obtenerCampo(row, [\n 'PAIS',\n 'PAÍS',\n 'pais',\n 'país',\n 'country'\n ]);\n\n const bancoCanal = obtenerCampo(row, [\n 'CANAL',\n 'canal'\n ]);\n\n const bancoAmbiente = obtenerCampo(row, [\n 'AMBIENTE DE COMPRA',\n 'ambiente de compra',\n 'ambiente'\n ]);\n\n const bancoDescripcion = obtenerCampo(row, [\n 'Descripcion',\n 'Descripción',\n 'descripcion',\n 'descripción',\n 'detalle',\n 'mecanica',\n 'mecánica',\n 'comentario',\n 'observacion',\n 'observación'\n ]);\n\n const bancoLink = obtenerCampo(row, [\n 'Enlace a la propuesta',\n 'link',\n 'enlace',\n 'url',\n 'brief link',\n 'link brief',\n 'presentacion',\n 'presentación',\n 'drive'\n ]);\n\n const bancoFileId = obtenerCampo(row, [\n 'file_id',\n 'File ID',\n 'archivo_id'\n ]);\n\n const bancoEnlacesEjecutadas = obtenerCampo(row, [\n 'Enlaces a propuestas ejecutadas',\n 'Enlace a propuestas ejecutadas',\n 'Links propuestas ejecutadas',\n 'Link propuestas ejecutadas',\n 'Ejecuciones',\n 'Propuestas ejecutadas'\n ]);\n\n if (!bancoNombre && !bancoMarca && !bancoCliente && !bancoDescripcion) {\n continue;\n }\n\n const textoBancoCompleto = [\n bancoNombre,\n bancoTipoAccion,\n bancoCliente,\n bancoMarca,\n bancoPais,\n bancoCanal,\n bancoAmbiente,\n bancoDescripcion\n ].filter(Boolean).join(' ');\n\n const textoClaveBanco = [\n bancoNombre,\n bancoCliente,\n bancoMarca,\n bancoPais\n ].filter(Boolean).join(' ');\n\n const scoreReferenciaNombre = similitudTokens(referencia, bancoNombre);\n const scoreEvidenciaNombre = similitudTokens(textoClavePropuesta, bancoNombre);\n const coberturaNombreEnEvidencia = coberturaTokens(bancoNombre, textoPropuestaCompleto);\n const coberturaReferenciaEnBanco = coberturaTokens(referencia, textoBancoCompleto);\n\n const scoreNombre = Math.max(\n scoreReferenciaNombre,\n scoreEvidenciaNombre,\n coberturaNombreEnEvidencia,\n coberturaReferenciaEnBanco\n );\n\n const scoreMarcaContraBanco = Math.max(\n similitudTokens(marca, bancoMarca),\n similitudTokens(marca, bancoNombre),\n similitudTokens(marca, textoBancoCompleto)\n );\n\n const scoreClienteContraBanco = Math.max(\n similitudTokens(cliente, bancoCliente),\n similitudTokens(cliente, bancoNombre),\n similitudTokens(cliente, textoBancoCompleto)\n );\n\n const scorePais = Math.max(\n similitudTokens(pais, bancoPais),\n contienePais(bancoNombre, pais) ? 1 : 0,\n contienePais(textoBancoCompleto, pais) ? 1 : 0\n );\n\n const scoreGeneral = similitudTokens(textoPropuestaCompleto, textoBancoCompleto);\n const scoreClave = similitudTokens(textoClavePropuesta, textoClaveBanco);\n const coberturaBancoEnPropuesta = coberturaTokens(bancoNombre, textoPropuestaCompleto);\n const comunesNombreEvidencia = contarTokensComunes(bancoNombre, textoPropuestaCompleto);\n\n const versionBanco = extraerVersion(textoBancoCompleto);\n const anioBanco = extraerAnio(textoBancoCompleto);\n\n const versionCoincide =\n versionPropuesta &&\n versionBanco &&\n versionPropuesta === versionBanco;\n\n const versionContradice =\n versionPropuesta &&\n versionBanco &&\n versionPropuesta !== versionBanco;\n\n const anioCoincide =\n anioPropuesta &&\n anioBanco &&\n anioPropuesta === anioBanco;\n\n const anioContradice =\n anioPropuesta &&\n anioBanco &&\n anioPropuesta !== anioBanco;\n\n const paisCoincide =\n scorePais >= 0.80 ||\n contienePais(bancoNombre, pais) ||\n contienePais(textoBancoCompleto, pais);\n\n const marcaCoincide =\n scoreMarcaContraBanco >= 0.45 ||\n normalizar(textoBancoCompleto).includes(normalizar(marca)) ||\n normalizar(textoPropuestaCompleto).includes(normalizar(bancoMarca));\n\n const scorePonderado =\n scoreNombre * 0.46 +\n scoreClave * 0.22 +\n scoreMarcaContraBanco * 0.08 +\n scoreClienteContraBanco * 0.04 +\n scorePais * 0.10 +\n scoreGeneral * 0.10;\n\n let scoreFinal = scorePonderado;\n let reglaMatch = 'PONDERADO';\n\n // Regla 1: el nombre/referencia de la propuesta está claramente contenido en la evidencia.\n if (coberturaBancoEnPropuesta >= 0.85 && (paisCoincide || anioCoincide || versionCoincide)) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.97 : 0.93);\n reglaMatch = versionCoincide\n ? 'NOMBRE_BANCO_EN_EVIDENCIA_CON_VERSION'\n : 'NOMBRE_BANCO_EN_EVIDENCIA';\n }\n\n // Regla 2: combinación fuerte de clave propuesta-banco.\n if (scoreClave >= 0.85 && (paisCoincide || anioCoincide || marcaCoincide)) {\n scoreFinal = Math.max(scoreFinal, 0.95);\n reglaMatch = 'CLAVE_COMPLETA_PROPUESTA_BANCO';\n } else if (scoreClave >= 0.75 && (paisCoincide || versionCoincide)) {\n scoreFinal = Math.max(scoreFinal, 0.90);\n reglaMatch = 'CLAVE_FUERTE_PROPUESTA_BANCO';\n }\n\n // Regla 3: referencia parcial + país + versión.\n if (scoreNombre >= 0.60 && paisCoincide && versionCoincide) {\n scoreFinal = Math.max(scoreFinal, 0.94);\n reglaMatch = 'REFERENCIA_PAIS_VERSION';\n }\n\n // Regla 4: muchos tokens comunes, aunque Gemini haya escrito el nombre distinto.\n if (comunesNombreEvidencia >= 5 && (paisCoincide || anioCoincide)) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.96 : 0.88);\n reglaMatch = versionCoincide\n ? 'TOKENS_COMUNES_PAIS_VERSION'\n : 'TOKENS_COMUNES_PAIS';\n }\n\n // Regla 5: para casos como RTM+ vs RTM más, GUATE vs Guatemala.\n if (\n normalizar(textoPropuestaCompleto).includes('rtm') &&\n normalizar(textoBancoCompleto).includes('rtm') &&\n normalizar(textoPropuestaCompleto).includes('wmc') &&\n normalizar(textoBancoCompleto).includes('wmc') &&\n (paisCoincide || normalizar(textoBancoCompleto).includes('guatemala')) &&\n (marcaCoincide || normalizar(textoBancoCompleto).includes('campero'))\n ) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.96 : 0.89);\n reglaMatch = versionCoincide\n ? 'RTM_WMC_MARCA_PAIS_VERSION'\n : 'RTM_WMC_MARCA_PAIS';\n }\n\n // Penalizaciones de seguridad.\n if (anioContradice) {\n scoreFinal = Math.min(scoreFinal, 0.59);\n reglaMatch = `${reglaMatch}_PENALIZADO_ANIO`;\n }\n\n if (versionContradice && scoreFinal >= 0.85) {\n scoreFinal = Math.min(scoreFinal, 0.79);\n reglaMatch = `${reglaMatch}_PENALIZADO_VERSION`;\n }\n\n // Si no coincide ni país, ni marca, ni año, no lo dejamos como alta confianza.\n if (\n scoreFinal >= 0.85 &&\n !paisCoincide &&\n !marcaCoincide &&\n !anioCoincide\n ) {\n scoreFinal = 0.74;\n reglaMatch = `${reglaMatch}_PENALIZADO_SIN_ANCLA`;\n }\n\n let bonusOrden = 0;\n\n if (versionCoincide) bonusOrden += 0.04;\n if (anioCoincide) bonusOrden += 0.02;\n if (paisCoincide) bonusOrden += 0.02;\n if (marcaCoincide) bonusOrden += 0.01;\n if (bancoFileId) bonusOrden += 0.005;\n\n candidatos.push({\n row,\n score: scoreFinal,\n score_orden: scoreFinal + bonusOrden,\n score_ponderado: scorePonderado,\n score_porcentaje: Math.round(scoreFinal * 100),\n\n score_nombre: Math.round(scoreNombre * 100),\n score_referencia_nombre: Math.round(scoreReferenciaNombre * 100),\n score_evidencia_nombre: Math.round(scoreEvidenciaNombre * 100),\n cobertura_nombre_en_evidencia: Math.round(coberturaNombreEnEvidencia * 100),\n cobertura_referencia_en_banco: Math.round(coberturaReferenciaEnBanco * 100),\n score_marca: Math.round(scoreMarcaContraBanco * 100),\n score_cliente: Math.round(scoreClienteContraBanco * 100),\n score_pais: Math.round(scorePais * 100),\n score_general: Math.round(scoreGeneral * 100),\n score_clave: Math.round(scoreClave * 100),\n cobertura_banco_en_propuesta: Math.round(coberturaBancoEnPropuesta * 100),\n comunes_nombre_evidencia: comunesNombreEvidencia,\n\n version_propuesta: versionPropuesta,\n version_banco: versionBanco,\n version_coincide: Boolean(versionCoincide),\n version_contradice: Boolean(versionContradice),\n\n anio_propuesta: anioPropuesta,\n anio_banco: anioBanco,\n anio_coincide: Boolean(anioCoincide),\n anio_contradice: Boolean(anioContradice),\n\n pais_coincide: Boolean(paisCoincide),\n marca_coincide: Boolean(marcaCoincide),\n\n regla_match: reglaMatch,\n\n banco_row_number: bancoRowNumber,\n banco_nombre: bancoNombre,\n banco_tipo_accion: bancoTipoAccion,\n banco_marca: bancoMarca,\n banco_cliente: bancoCliente,\n banco_pais: bancoPais,\n banco_canal: bancoCanal,\n banco_link: bancoLink,\n banco_file_id: bancoFileId,\n banco_enlaces_ejecutadas: bancoEnlacesEjecutadas\n });\n}\n\nconst candidatosValidos = candidatos\n .filter(c => c.score > 0)\n .sort((a, b) => {\n if (b.score_orden !== a.score_orden) return b.score_orden - a.score_orden;\n return Number(b.banco_row_number || 0) - Number(a.banco_row_number || 0);\n });\n\nconst mejor = candidatosValidos[0] || null;\nconst segundo = candidatosValidos[1] || null;\n\nlet propuestaMatchEstado = 'SIN_MATCH';\nlet propuestaMatchRevision = 'REQUIERE_REVISION_MANUAL';\nlet propuestaMatchConfianza = '0';\nlet propuestaNombreBanco = '';\nlet propuestaLinkBanco = '';\nlet propuestaBancoRowNumber = '';\nlet propuestaBancoEnlacesExistentes = '';\nlet propuestaBancoFileId = '';\n\nif (mejor) {\n let confianzaFinal = mejor.score_porcentaje;\n\n // Si el segundo está demasiado cerca, mantenemos match pero pedimos revisión.\n const segundoMuyCerca =\n segundo &&\n mejor.score_porcentaje >= 85 &&\n segundo.score_porcentaje >= 85 &&\n Math.abs(mejor.score_porcentaje - segundo.score_porcentaje) <= 2 &&\n normalizar(mejor.banco_nombre) !== normalizar(segundo.banco_nombre);\n\n propuestaNombreBanco = mejor.banco_nombre || 'No identificado';\n propuestaLinkBanco = mejor.banco_link || '';\n propuestaBancoRowNumber = mejor.banco_row_number || '';\n propuestaBancoEnlacesExistentes = mejor.banco_enlaces_ejecutadas || '';\n propuestaBancoFileId = mejor.banco_file_id || '';\n\n if (confianzaFinal >= 85 && !segundoMuyCerca) {\n propuestaMatchEstado = 'MATCH_ALTA_CONFIANZA';\n propuestaMatchRevision = 'NO_REQUIERE_REVISION';\n } else if (confianzaFinal >= 85 && segundoMuyCerca) {\n propuestaMatchEstado = 'MATCH_MEDIA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_VALIDACION_MANUAL';\n confianzaFinal = Math.min(confianzaFinal, 84);\n } else if (confianzaFinal >= 60) {\n propuestaMatchEstado = 'MATCH_MEDIA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_VALIDACION_MANUAL';\n } else {\n propuestaMatchEstado = 'MATCH_BAJA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_REVISION_MANUAL';\n }\n\n propuestaMatchConfianza = String(confianzaFinal);\n}\n\nreturn [\n {\n json: {\n ...propuesta,\n\n propuesta_match_estado: propuestaMatchEstado,\n propuesta_match_confianza: propuestaMatchConfianza,\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_link_banco: propuestaLinkBanco,\n propuesta_match_revision: propuestaMatchRevision,\n\n propuesta_banco_row_number: propuestaBancoRowNumber,\n propuesta_banco_enlaces_existentes: propuestaBancoEnlacesExistentes,\n propuesta_banco_file_id: propuestaBancoFileId,\n\n match_banco_debug: {\n total_filas_banco: bancoRows.length,\n total_candidatos: candidatosValidos.length,\n propuesta_normalizada: {\n referencia,\n marca,\n cliente,\n pais,\n version_propuesta: versionPropuesta,\n anio_propuesta: anioPropuesta,\n texto_clave: textoClavePropuesta\n },\n mejor_match: mejor\n ? {\n score: mejor.score_porcentaje,\n score_orden: Math.round(mejor.score_orden * 100),\n regla_match: mejor.regla_match,\n row_number: mejor.banco_row_number,\n nombre: mejor.banco_nombre,\n tipo_accion: mejor.banco_tipo_accion,\n marca: mejor.banco_marca,\n cliente: mejor.banco_cliente,\n pais: mejor.banco_pais,\n canal: mejor.banco_canal,\n link: mejor.banco_link,\n file_id: mejor.banco_file_id,\n enlaces_ejecutadas_existentes: mejor.banco_enlaces_ejecutadas,\n score_nombre: mejor.score_nombre,\n score_referencia_nombre: mejor.score_referencia_nombre,\n score_evidencia_nombre: mejor.score_evidencia_nombre,\n cobertura_nombre_en_evidencia: mejor.cobertura_nombre_en_evidencia,\n cobertura_referencia_en_banco: mejor.cobertura_referencia_en_banco,\n score_marca: mejor.score_marca,\n score_cliente: mejor.score_cliente,\n score_pais: mejor.score_pais,\n score_general: mejor.score_general,\n score_clave: mejor.score_clave,\n cobertura_banco_en_propuesta: mejor.cobertura_banco_en_propuesta,\n comunes_nombre_evidencia: mejor.comunes_nombre_evidencia,\n version_banco: mejor.version_banco,\n version_coincide: mejor.version_coincide,\n anio_banco: mejor.anio_banco,\n anio_coincide: mejor.anio_coincide,\n pais_coincide: mejor.pais_coincide,\n marca_coincide: mejor.marca_coincide\n }\n : null,\n segundo_match: segundo\n ? {\n score: segundo.score_porcentaje,\n regla_match: segundo.regla_match,\n row_number: segundo.banco_row_number,\n nombre: segundo.banco_nombre,\n marca: segundo.banco_marca,\n cliente: segundo.banco_cliente,\n pais: segundo.banco_pais,\n file_id: segundo.banco_file_id\n }\n : null,\n top_5: candidatosValidos.slice(0, 5).map(c => ({\n score: c.score_porcentaje,\n score_orden: Math.round(c.score_orden * 100),\n regla_match: c.regla_match,\n row_number: c.banco_row_number,\n nombre: c.banco_nombre,\n marca: c.banco_marca,\n cliente: c.banco_cliente,\n pais: c.banco_pais,\n file_id: c.banco_file_id,\n score_nombre: c.score_nombre,\n score_clave: c.score_clave,\n cobertura_banco_en_propuesta: c.cobertura_banco_en_propuesta,\n comunes_nombre_evidencia: c.comunes_nombre_evidencia,\n version_banco: c.version_banco,\n version_coincide: c.version_coincide,\n anio_banco: c.anio_banco,\n anio_coincide: c.anio_coincide\n }))\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 5792, + 2144 + ], + "id": "1e40c947-c41c-499f-9dd7-cd0ac4caa2d3", + "name": "Code - Match propuesta banco Fulgencio TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst toNumber = (valor) => {\n const limpio = limpiar(valor).replace(',', '.');\n const n = Number(limpio);\n return Number.isFinite(n) ? n : 0;\n};\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst slidesNormalizado1 = getNodeJson('Code - Normalizar respuesta Slides TEST');\nconst slidesNormalizado2 = getNodeJson('Code - Normalizar respuesta Slides ejecución TEST');\nconst slidesNormalizado3 = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...match,\n ...enriquecido,\n ...actual,\n ...slidesNormalizado1,\n ...slidesNormalizado2,\n ...slidesNormalizado3\n};\n\nconst matchDebug =\n match.match_banco_debug ||\n actual.match_banco_debug ||\n data.match_banco_debug ||\n {};\n\nconst mejorMatch = matchDebug.mejor_match || {};\n\n// --------------------------------------------------\n// 1. Resolver datos principales\n// --------------------------------------------------\n\nconst tipoReporte = normalizarPlano(\n jsonFinal.tipo_reporte ||\n data.tipo_reporte ||\n actual.tipo_reporte ||\n ''\n);\n\nlet matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n actual.propuesta_match_estado ||\n ''\n);\n\nlet matchConfianza = toNumber(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n actual.propuesta_match_confianza ||\n mejorMatch.score ||\n 0\n);\n\nlet nombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n actual.propuesta_nombre_banco ||\n mejorMatch.nombre ||\n ''\n);\n\nconst linkBancoOriginal = limpiar(\n match.propuesta_link_banco ||\n data.propuesta_link_banco ||\n actual.propuesta_link_banco ||\n mejorMatch.link ||\n ''\n);\n\nconst fileIdBanco = limpiar(\n match.propuesta_banco_file_id ||\n data.propuesta_banco_file_id ||\n actual.propuesta_banco_file_id ||\n mejorMatch.file_id ||\n ''\n);\n\nconst rowNumberBanco = limpiar(\n match.propuesta_banco_row_number ||\n data.propuesta_banco_row_number ||\n actual.propuesta_banco_row_number ||\n mejorMatch.row_number ||\n ''\n);\n\nconst enlacesExistentes = limpiar(\n match.propuesta_banco_enlaces_existentes ||\n data.propuesta_banco_enlaces_existentes ||\n actual.propuesta_banco_enlaces_existentes ||\n data.banco_enlaces_existentes ||\n actual.banco_enlaces_existentes ||\n mejorMatch.enlaces_ejecutadas_existentes ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n actual.presentacion_ejecucion_url ||\n data.presentation_url ||\n actual.presentation_url ||\n data.presentationUrl ||\n actual.presentationUrl ||\n data.webViewLink ||\n actual.webViewLink ||\n data.link_presentacion ||\n actual.link_presentacion ||\n slidesNormalizado3.presentacion_ejecucion_url ||\n slidesNormalizado3.presentation_url ||\n slidesNormalizado3.webViewLink ||\n ''\n);\n\nconst presentationId = limpiar(\n data.presentation_id ||\n actual.presentation_id ||\n slidesNormalizado3.presentation_id ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\n// --------------------------------------------------\n// 2. Normalizar estado de match de forma defensiva\n// --------------------------------------------------\n\nif (\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 85\n) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n}\n\nif (\n matchEstado !== 'MATCH_MEDIA_CONFIANZA' &&\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 60 &&\n matchConfianza < 85\n) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n}\n\nif (!matchEstado || matchEstado === 'PENDIENTE') {\n if (matchConfianza >= 85) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n } else if (matchConfianza >= 60) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n } else if (matchConfianza > 0) {\n matchEstado = 'MATCH_BAJA_CONFIANZA';\n } else {\n matchEstado = 'SIN_MATCH';\n }\n}\n\n// --------------------------------------------------\n// 3. Mantener valores enriquecidos\n// --------------------------------------------------\n\nconst propuestaReferenciaFinal = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n actual.propuesta_referencia ||\n ''\n);\n\nconst ubicacionFinal = limpiar(\n enriquecido.ubicacion ||\n data.ubicacion ||\n actual.ubicacion ||\n ''\n);\n\nconst fechaEjecucionFinal = limpiar(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n actual.fecha_ejecucion ||\n ''\n);\n\n// --------------------------------------------------\n// 4. Detectar duplicado de link de forma robusta\n// --------------------------------------------------\n\nconst limpiarUrl = (valor) => {\n return limpiar(valor)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '')\n .trim();\n};\n\nconst normalizarUrlComparacion = (valor) => {\n return limpiarUrl(valor)\n .replace(/\\/edit.*$/i, '')\n .replace(/\\/view.*$/i, '')\n .replace(/\\?.*$/i, '')\n .replace(/#.*$/i, '')\n .replace(/\\/$/, '')\n .toLowerCase();\n};\n\nconst extraerGoogleId = (valor) => {\n const url = limpiarUrl(valor);\n\n if (!url) return '';\n\n const patrones = [\n /\\/presentation\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/document\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/spreadsheets\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,\n /[?&]id=([a-zA-Z0-9_-]+)/\n ];\n\n for (const patron of patrones) {\n const match = url.match(patron);\n if (match?.[1]) return match[1];\n }\n\n return '';\n};\n\nconst extraerLinks = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return [];\n\n return texto\n .split(/[\\n\\r\\t;, ]+/)\n .map(limpiarUrl)\n .filter(Boolean)\n .filter(link => {\n return (\n link.startsWith('http://') ||\n link.startsWith('https://') ||\n link.includes('docs.google.com') ||\n link.includes('drive.google.com')\n );\n });\n};\n\nconst enlacesExistentesLista = extraerLinks(enlacesExistentes);\n\nconst presentationIdDetectado =\n presentationId ||\n extraerGoogleId(presentationUrl);\n\nconst presentationUrlNormalizada = normalizarUrlComparacion(presentationUrl);\n\nconst linkDuplicado = Boolean(\n presentationUrl &&\n enlacesExistentesLista.some((linkExistente) => {\n const idExistente = extraerGoogleId(linkExistente);\n\n if (\n presentationIdDetectado &&\n idExistente &&\n idExistente === presentationIdDetectado\n ) {\n return true;\n }\n\n return normalizarUrlComparacion(linkExistente) === presentationUrlNormalizada;\n })\n);\n\n// --------------------------------------------------\n// 5. Calcular enlaces propuestos\n// --------------------------------------------------\n\nlet enlacesPropuestos = enlacesExistentes;\n\nif (presentationUrl && !linkDuplicado) {\n enlacesPropuestos = enlacesExistentes\n ? `${enlacesExistentes}\\n${presentationUrl}`\n : presentationUrl;\n}\n\n// --------------------------------------------------\n// 6. Decisión automática final\n// --------------------------------------------------\n\nconst motivos = [];\n\nconst esPropuestaEjecutada = tipoReporte === 'PROPUESTA_EJECUTADA';\nconst esPropuestaExterna = tipoReporte === 'PROPUESTA_EXTERNA';\nconst matchAlta = matchEstado === 'MATCH_ALTA_CONFIANZA' && matchConfianza >= 85;\nconst hayMatchBanco = Boolean(rowNumberBanco && nombreBanco);\n\nlet decisionAutomaticaBanco = '';\nlet motivoDecisionAutomatica = '';\n\nif (!presentationUrl) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_ERROR_DATOS';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no existe link de presentación ejecutada.';\n motivos.push('no existe presentacion_ejecucion_url');\n\n} else if (esPropuestaExterna) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_PROPUESTA_EXTERNA';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el reporte fue clasificado como propuesta externa. Se genera presentación y se guarda el registro ejecutado.';\n\n} else if (!esPropuestaEjecutada) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_TIPO_NO_DETERMINADO';\n motivoDecisionAutomatica = `No se actualiza banco original porque tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}.`;\n motivos.push(`tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}`);\n\n} else if (linkDuplicado) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_LINK_DUPLICADO';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el link de presentación ejecutada ya existe en la columna de enlaces ejecutados.';\n motivos.push('el link ya existe en la columna de enlaces ejecutados');\n\n} else if (!hayMatchBanco) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_SIN_MATCH';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no se encontró una propuesta compatible con fila de banco válida.';\n if (!nombreBanco) motivos.push('no llegó propuesta_nombre_banco');\n if (!rowNumberBanco) motivos.push('no llegó propuesta_banco_row_number');\n\n} else if (!matchAlta) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_CONFIANZA_INSUFICIENTE';\n motivoDecisionAutomatica = `No se actualiza banco original porque el match no tiene confianza suficiente. Estado: ${matchEstado || 'SIN_MATCH'}, confianza: ${matchConfianza}.`;\n motivos.push(`propuesta_match_estado es ${matchEstado || 'SIN_MATCH'}`);\n motivos.push(`propuesta_match_confianza es ${matchConfianza}`);\n\n} else {\n decisionAutomaticaBanco = 'ACTUALIZADO_BANCO_AUTO';\n motivoDecisionAutomatica = 'Banco original actualizado automáticamente: propuesta ejecutada, match alto, confianza suficiente y link nuevo.';\n}\n\nconst actualizarBanco = decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO';\n\nconst bancoActualizadoAuto = actualizarBanco ? 'SI' : 'NO';\n\nconst motivoNoActualizaBanco = actualizarBanco\n ? ''\n : motivoDecisionAutomatica;\n\n// --------------------------------------------------\n// 7. Seguridad para evitar updates accidentales\n// --------------------------------------------------\n// Si NO se actualiza, dejamos row_number y banco_match_value vacíos.\n// Así, aunque alguien conecte mal el nodo de Google Sheets,\n// no debería encontrar fila válida para actualizar.\nconst rowNumberSeguro = actualizarBanco ? rowNumberBanco : '';\nconst bancoMatchValueSeguro = actualizarBanco ? rowNumberBanco : '';\nconst enlacesActualizadosSeguro = actualizarBanco ? enlacesPropuestos : enlacesExistentes;\n\n// Campo compatible con lógica anterior.\n// Ya no significa revisión humana; es una decisión automática del sistema.\nlet propuestaMatchRevision = '';\n\nif (decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_LINK_DUPLICADO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION_LINK_DUPLICADO';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_PROPUESTA_EXTERNA') {\n propuestaMatchRevision = 'NO_APLICA_PROPUESTA_EXTERNA';\n} else {\n propuestaMatchRevision = 'NO_ACTUALIZADO_AUTOMATICAMENTE';\n}\n\n// --------------------------------------------------\n// 8. Salida final\n// --------------------------------------------------\n\nreturn [\n {\n json: {\n ...data,\n\n // Mantener explícitamente los valores enriquecidos.\n propuesta_referencia: propuestaReferenciaFinal || data.propuesta_referencia || '',\n ubicacion: ubicacionFinal || data.ubicacion || '',\n fecha_ejecucion: fechaEjecucionFinal || data.fecha_ejecucion || '',\n\n // Mantener explícitamente el link final para nodos posteriores.\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n // Decisión automática nueva.\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n // Compatibilidad con flujo actual.\n actualizar_banco_original: actualizarBanco,\n motivo_no_actualiza_banco: motivoNoActualizaBanco,\n\n propuesta_match_estado: matchEstado || data.propuesta_match_estado || 'SIN_MATCH',\n propuesta_match_confianza: String(matchConfianza || 0),\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n propuesta_banco_enlaces_existentes: enlacesExistentes,\n propuesta_match_revision: propuestaMatchRevision,\n\n banco_match_column: 'row_number',\n banco_match_value: bancoMatchValueSeguro,\n\n row_number: rowNumberSeguro,\n 'Enlaces a propuestas ejecutadas': enlacesActualizadosSeguro,\n\n banco_enlaces_existentes: enlacesExistentes,\n banco_enlaces_propuestos: enlacesPropuestos,\n banco_enlaces_actualizados: enlacesActualizadosSeguro,\n banco_presentacion_url: presentationUrl,\n\n banco_link_duplicado_detectado: linkDuplicado,\n banco_presentacion_id_detectado: presentationIdDetectado,\n banco_enlaces_existentes_lista: enlacesExistentesLista,\n\n datos_enriquecidos_match_banco: Boolean(\n enriquecido.datos_enriquecidos_match_banco ||\n data.datos_enriquecidos_match_banco\n ),\n\n banco_update_debug: {\n tipo_reporte: tipoReporte,\n\n propuesta_match_estado: matchEstado,\n propuesta_match_confianza: matchConfianza,\n match_alta: matchAlta,\n hay_match_banco: hayMatchBanco,\n\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n propuesta_referencia_final: propuestaReferenciaFinal,\n ubicacion_final: ubicacionFinal,\n fecha_ejecucion_final: fechaEjecucionFinal,\n\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n\n presentationUrl,\n presentationId,\n presentationIdDetectado,\n\n enlacesExistentes,\n enlacesExistentesLista,\n enlacesPropuestos,\n enlacesActualizadosSeguro,\n\n linkDuplicado,\n\n rowNumberSeguro,\n bancoMatchValueSeguro,\n\n actualizarBanco,\n motivoNoActualizaBanco,\n\n motivos,\n\n mejor_match: mejorMatch,\n match_debug: matchDebug,\n\n enriquecido_debug: {\n propuesta_referencia: enriquecido.propuesta_referencia || '',\n ubicacion: enriquecido.ubicacion || '',\n fecha_ejecucion: enriquecido.fecha_ejecucion || '',\n datos_enriquecidos_match_banco: enriquecido.datos_enriquecidos_match_banco || false\n }\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 12688, + 1952 + ], + "id": "df339298-bbca-4b0e-9db7-41323b2c44c9", + "name": "Code - Preparar actualización banco propuesta ejecutada TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dccbea1d-e9cf-4f51-b94d-7b0162737548", + "leftValue": "={{ $json.actualizar_banco_original === true }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 13456, + 2000 + ], + "id": "da284028-5f31-4cda-b1b6-e27cf04b5c3b", + "name": "IF - Actualizar banco match alta confianza TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "Enlaces a propuestas ejecutadas": "={{ $json.banco_enlaces_actualizados }}", + "row_number": "={{ $json.propuesta_banco_row_number }}" + }, + "matchingColumns": [ + "row_number" + ], + "schema": [ + { + "id": "NOMBRE", + "displayName": "NOMBRE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "TIPO DE ACCION", + "displayName": "TIPO DE ACCION", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "CLIENTE", + "displayName": "CLIENTE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "MARCA", + "displayName": "MARCA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "PAIS", + "displayName": "PAIS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "CANAL", + "displayName": "CANAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "AMBIENTE DE COMPRA (RE)", + "displayName": "AMBIENTE DE COMPRA (RE)", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "TÁCTICA PROMOCIONAL", + "displayName": "TÁCTICA PROMOCIONAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "APROBADA", + "displayName": "APROBADA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ETIQUETAS", + "displayName": "ETIQUETAS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "AÑO", + "displayName": "AÑO", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Enlace a la propuesta", + "displayName": "Enlace a la propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Descripcion", + "displayName": "Descripcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "file_id", + "displayName": "file_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "nombre_archivo", + "displayName": "nombre_archivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "mime_type", + "displayName": "mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fuente_pais", + "displayName": "fuente_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "confianza_pais", + "displayName": "confianza_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "requiere_revision", + "displayName": "requiere_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "procesado_ia", + "displayName": "procesado_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivos_revision", + "displayName": "motivos_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Enlaces a propuestas ejecutadas", + "displayName": "Enlaces a propuestas ejecutadas", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tablero_sync_key", + "displayName": "tablero_sync_key", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tablero_project_id", + "displayName": "tablero_project_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tablero_origen", + "displayName": "tablero_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "drive_modified_time", + "displayName": "drive_modified_time", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultimo_refresco_ia", + "displayName": "ultimo_refresco_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "refresh_status", + "displayName": "refresh_status", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "refresh_attempts", + "displayName": "refresh_attempts", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "converted_google_slides_id", + "displayName": "converted_google_slides_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "converted_google_slides_link", + "displayName": "converted_google_slides_link", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 13888, + 2016 + ], + "id": "9932dc19-3202-4354-aae7-c33677259f41", + "name": "Sheets - Actualizar link ejecutada en banco TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const respuestaBanco = $json || {};\n\nconst base = $('Code - Preparar actualización banco propuesta ejecutada TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n banco_original_actualizado: true,\n banco_original_response: respuestaBanco\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 14096, + 2016 + ], + "id": "b2470e42-2786-491b-8f8d-84ac5cf7f31a", + "name": "Code - Restaurar contexto actualización banco TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valor) => {\n const texto = normalizar(valor);\n\n if (!texto) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bCODIGO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(texto));\n};\n\nconst normalizarUbicacion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst normalizarFechaEjecucion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (/^20\\d{2}$/.test(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst matchAltaOMedia = [\n 'MATCH_ALTA_CONFIANZA',\n 'MATCH_MEDIA_CONFIANZA'\n].includes(normalizar(data.propuesta_match_estado));\n\nconst nombreBanco = limpiar(data.propuesta_nombre_banco);\n\nconst referenciaOriginal = limpiar(data.propuesta_referencia);\n\nconst referenciaCanonica = matchAltaOMedia && nombreBanco\n ? nombreBanco\n : referenciaOriginal;\n\nconst ubicacionLimpia = normalizarUbicacion(data.ubicacion);\nconst fechaLimpia = normalizarFechaEjecucion(data.fecha_ejecucion);\n\nreturn [\n {\n json: {\n ...data,\n\n propuesta_referencia_original_ia: referenciaOriginal,\n ubicacion_original_ia: data.ubicacion || '',\n fecha_ejecucion_original_ia: data.fecha_ejecucion || '',\n\n propuesta_referencia: limpiarReferenciaVisual(referenciaCanonica),\n ubicacion: ubicacionLimpia,\n fecha_ejecucion: fechaLimpia,\n\n datos_enriquecidos_match_banco: true\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6000, + 2160 + ], + "id": "cb2a57aa-9f34-454e-a77f-8096bd49228b", + "name": "Code - Enriquecer datos con match banco TEST" + }, + { + "parameters": { + "jsCode": "const evento = $('Code - Normalizar evento WhatsApp TEST').first().json || {};\nconst grupos = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarTexto = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst normalizarJid = (valor) => {\n let raw = limpiar(valor);\n\n if (!raw) return '';\n\n raw = raw.replace('@c.us', '@s.whatsapp.net');\n\n if (raw.includes('@g.us')) return raw;\n\n if (raw.includes('@s.whatsapp.net')) {\n const numero = raw.replace('@s.whatsapp.net', '').replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return `${numero}@s.whatsapp.net`;\n }\n\n const numero = raw.replace(/\\D/g, '');\n\n if (numero.startsWith('120363')) {\n return `${numero}@g.us`;\n }\n\n return numero ? `${numero}@s.whatsapp.net` : '';\n};\n\nconst getCampo = (row, nombres) => {\n const keys = Object.keys(row || {});\n\n const normalizarKey = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '');\n };\n\n for (const nombre of nombres) {\n const nombreNorm = normalizarKey(nombre);\n\n const keyExacta = keys.find(k => normalizarKey(k) === nombreNorm);\n\n if (keyExacta !== undefined && limpiar(row[keyExacta]) !== '') {\n return limpiar(row[keyExacta]);\n }\n }\n\n return '';\n};\n\n// --------------------------------------------------\n// 1. Identificar origen del evento\n// --------------------------------------------------\n\nconst rawEvent = evento.raw_event || {};\nconst rawData = rawEvent.data || {};\nconst rawMessage = rawData.message || {};\n\nconst remoteJid = normalizarJid(\n evento.whatsapp_remote_jid ||\n evento.group_jid ||\n rawData.key?.remoteJid ||\n ''\n);\n\nconst groupJid = normalizarJid(\n evento.group_jid ||\n rawData.key?.remoteJid ||\n ''\n);\n\nconst isGroup = Boolean(evento.is_group || remoteJid.endsWith('@g.us'));\n\nconst messageType = limpiar(evento.message_type).toLowerCase();\nconst accion = limpiar(evento.accion_flujo).toUpperCase();\nconst texto = limpiar(evento.texto);\n\n// --------------------------------------------------\n// 2. Detectar eventos que nunca deben procesarse\n// --------------------------------------------------\n\nconst esReaccion = Boolean(\n rawMessage.reactionMessage ||\n rawMessage.protocolMessage?.type === 14 ||\n messageType === 'reaction' ||\n messageType === 'message_reaction' ||\n messageType === 'reactionmessage'\n);\n\nconst esMensajeDelBot = Boolean(\n evento.from_me ||\n rawData.key?.fromMe\n);\n\nconst esEventoSinContenido = !texto &&\n !evento.tiene_media &&\n !['audio', 'image', 'video', 'document'].includes(messageType);\n\n// --------------------------------------------------\n// 3. Leer grupos permitidos desde Google Sheets\n// --------------------------------------------------\n\nconst gruposNormalizados = grupos.map((row, index) => {\n const groupId = normalizarJid(\n getCampo(row, [\n 'group_jid',\n 'grupo_jid',\n 'jid',\n 'grupo',\n 'group',\n 'id_grupo'\n ])\n );\n\n const nombreGrupo = getCampo(row, [\n 'nombre_grupo',\n 'nombre',\n 'name'\n ]);\n\n const proyecto = getCampo(row, [\n 'proyecto',\n 'project'\n ]);\n\n const activo = getCampo(row, [\n 'activo',\n 'active',\n 'habilitado',\n 'permitido'\n ]);\n\n const activoNormalizado = normalizarTexto(activo);\n\n const estaActivo = [\n 'SI',\n 'SÍ',\n 'YES',\n 'TRUE',\n 'ACTIVO',\n '1'\n ].includes(activoNormalizado);\n\n return {\n index,\n row,\n group_jid: groupId,\n nombre_grupo: nombreGrupo,\n proyecto,\n activo,\n activo_normalizado: activoNormalizado,\n esta_activo: estaActivo\n };\n});\n\nconst grupoEncontrado = gruposNormalizados.find(grupo => {\n return grupo.group_jid && grupo.group_jid === (groupJid || remoteJid);\n}) || null;\n\nconst grupoPermitido = Boolean(\n isGroup &&\n grupoEncontrado &&\n grupoEncontrado.esta_activo\n);\n\n// Si en el futuro quieres permitir DM directo al bot, cambia esto a true.\nconst permitirChatIndividual = false;\nconst chatIndividualPermitido = !isGroup && permitirChatIndividual;\n\n// --------------------------------------------------\n// 4. Decidir si procesar o ignorar\n// --------------------------------------------------\n\nconst motivosIgnorar = [];\n\nif (esMensajeDelBot) {\n motivosIgnorar.push('mensaje enviado por el propio bot');\n}\n\nif (esReaccion) {\n motivosIgnorar.push('evento de reacción');\n}\n\nif (esEventoSinContenido) {\n motivosIgnorar.push('evento sin texto ni media útil');\n}\n\nif (isGroup && !grupoEncontrado) {\n motivosIgnorar.push(`grupo no encontrado en wa_grupos_permitidos: ${groupJid || remoteJid || 'SIN_GRUPO'}`);\n}\n\nif (isGroup && grupoEncontrado && !grupoEncontrado.esta_activo) {\n motivosIgnorar.push(`grupo encontrado pero inactivo: ${groupJid || remoteJid || 'SIN_GRUPO'}`);\n}\n\nif (!isGroup && !chatIndividualPermitido) {\n motivosIgnorar.push('chat individual no permitido');\n}\n\nconst procesar = motivosIgnorar.length === 0;\n\nreturn [\n {\n json: {\n ...evento,\n\n filtro_origen_procesar: procesar,\n filtro_origen_decision: procesar ? 'PROCESAR' : 'IGNORAR',\n filtro_origen_motivo: motivosIgnorar.join('; '),\n\n grupo_permitido: grupoPermitido,\n grupo_permiso_estado: grupoPermitido ? 'PERMITIDO' : 'DENEGADO',\n grupo_autorizado: grupoEncontrado?.row || null,\n grupo_nombre_autorizado: grupoEncontrado?.nombre_grupo || evento.group_name || '',\n grupo_proyecto_autorizado: grupoEncontrado?.proyecto || '',\n\n filtro_origen_debug: {\n remoteJid,\n groupJid,\n isGroup,\n messageType,\n accion,\n texto,\n\n esReaccion,\n esMensajeDelBot,\n esEventoSinContenido,\n\n total_grupos_leidos: grupos.length,\n grupo_encontrado: Boolean(grupoEncontrado),\n grupo_permitido: grupoPermitido,\n grupo_encontrado_debug: grupoEncontrado\n ? {\n group_jid: grupoEncontrado.group_jid,\n nombre_grupo: grupoEncontrado.nombre_grupo,\n proyecto: grupoEncontrado.proyecto,\n activo: grupoEncontrado.activo,\n activo_normalizado: grupoEncontrado.activo_normalizado,\n esta_activo: grupoEncontrado.esta_activo\n }\n : null,\n\n grupos_leidos_debug: gruposNormalizados.slice(0, 10).map(grupo => ({\n index: grupo.index,\n group_jid: grupo.group_jid,\n nombre_grupo: grupo.nombre_grupo,\n proyecto: grupo.proyecto,\n activo: grupo.activo,\n activo_normalizado: grupo.activo_normalizado,\n esta_activo: grupo.esta_activo\n })),\n\n chatIndividualPermitido\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -8880, + -1024 + ], + "id": "f472e804-82c8-443f-b847-e3adb44aafdf", + "name": "Code - Filtro origen WhatsApp TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "330d3843-6562-4ba6-afce-9a4bef9666b1", + "leftValue": "={{ $json.filtro_origen_procesar }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -8368, + -1056 + ], + "id": "146eed23-6f86-47a5-89fb-56b7ac54646f", + "name": "IF - Procesar solo grupo permitido TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 50533208, + "mode": "list", + "cachedResultName": "wa_grupos_permitidos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=50533208" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -9088, + -1024 + ], + "id": "542039fa-a89d-4655-b767-05dd55a0014b", + "name": "Sheets - Leer grupos permitidos WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "decision_automatica_banco": "={{ $json.decision_automatica_banco }}", + "banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}", + "propuesta_match_revision": "={{ $json.propuesta_match_revision }}", + "motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_ejecucion", + "displayName": "fecha_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_referencia", + "displayName": "propuesta_referencia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_match_estado", + "displayName": "propuesta_match_estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_match_confianza", + "displayName": "propuesta_match_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_nombre_banco", + "displayName": "propuesta_nombre_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_link_banco", + "displayName": "propuesta_link_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "marca", + "displayName": "marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "cliente", + "displayName": "cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "pais", + "displayName": "pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ubicacion", + "displayName": "ubicacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "comentario_original", + "displayName": "comentario_original", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "resumen_ia", + "displayName": "resumen_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "descripcion_ejecucion", + "displayName": "descripcion_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "elementos_detectados", + "displayName": "elementos_detectados", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tags", + "displayName": "tags", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "media_folder_url", + "displayName": "media_folder_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "presentacion_ejecucion_url", + "displayName": "presentacion_ejecucion_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fotos_count", + "displayName": "fotos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "audios_count", + "displayName": "audios_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "estado_revision", + "displayName": "estado_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "transcripcion_audio", + "displayName": "transcripcion_audio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_match_revision", + "displayName": "propuesta_match_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte_confianza", + "displayName": "tipo_reporte_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte_motivo", + "displayName": "tipo_reporte_motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "decision_automatica_banco", + "displayName": "decision_automatica_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_decision_automatica", + "displayName": "motivo_decision_automatica", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "banco_actualizado_auto", + "displayName": "banco_actualizado_auto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 12944, + 2032 + ], + "id": "cd1d8a04-7c10-400f-955d-05824392abc9", + "name": "Sheets - Actualizar decisión automática banco TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst decisionOriginal = getNodeJson('Code - Preparar actualización banco propuesta ejecutada TEST');\nconst updateDecisionSheet = $json || {};\n\nreturn [\n {\n json: {\n ...decisionOriginal,\n\n decision_sheet_actualizada: true,\n decision_sheet_update_debug: {\n row_number: updateDecisionSheet.row_number || '',\n session_id: updateDecisionSheet.session_id || decisionOriginal.session_id || '',\n decision_automatica_banco:\n updateDecisionSheet.decision_automatica_banco ||\n decisionOriginal.decision_automatica_banco ||\n '',\n banco_actualizado_auto:\n updateDecisionSheet.banco_actualizado_auto ||\n decisionOriginal.banco_actualizado_auto ||\n ''\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 13152, + 2032 + ], + "id": "2e297186-0656-4331-9df0-28a61b933861", + "name": "Code - Restaurar contexto decisión automática banco TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst sessionId = limpiar(data.session_id);\nconst eventId = limpiar(data.event_id || data.media_source_id || `${Date.now()}`);\nconst mediaSourceId = limpiar(data.media_source_id || data.event_id || eventId);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para crear buffer Redis de imagen.');\n}\n\nif (!mediaSourceId) {\n throw new Error('No llegó media_source_id/event_id para crear buffer Redis de imagen.');\n}\n\nconst redisKey = `fulgencio:media:${sessionId}:image`;\n\nconst bufferItem = {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n\n event_id: eventId,\n media_source_id: mediaSourceId,\n message_type: 'image',\n media_type: 'image',\n\n buffer_received_at: new Date().toISOString(),\n buffer_received_at_ms: Date.now()\n};\n\nreturn [\n {\n json: {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n redis_payload: JSON.stringify(bufferItem),\n\n estado_buffer_redis: 'IMAGEN_ENVIADA_A_BUFFER'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4576, + -1648 + ], + "id": "64b18b2d-f601-41c6-a041-4b9c8386581c", + "name": "Code - Preparar buffer imagen Redis TEST" + }, + { + "parameters": { + "operation": "push", + "list": "={{ $json.redis_buffer_key }}", + "messageData": "={{ $json.redis_payload }}", + "tail": true + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + -4368, + -1648 + ], + "id": "a23af96d-d5a1-4186-a9f4-0fd217ac6f5e", + "name": "Redis - Push buffer imagen TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "jsCode": "const original = $('Code - Preparar buffer imagen Redis TEST').first().json || {};\nconst redisPushResult = $json || {};\n\nreturn [\n {\n json: {\n ...original,\n redis_push_result: redisPushResult,\n redis_push_ok: true,\n estado_buffer_redis: 'IMAGEN_GUARDADA_EN_REDIS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -4160, + -1648 + ], + "id": "5b888e51-25ea-4c3d-b1b3-a75053c95498", + "name": "Code - Restaurar contexto buffer imagen TEST" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + -3952, + -1648 + ], + "id": "d96a4efc-a786-451c-b1f4-d2e838b8e8be", + "name": "Wait - Esperar cierre buffer imagen TEST", + "webhookId": "5c73c08e-d77a-448d-a8ab-11080d08b476" + }, + { + "parameters": { + "operation": "get", + "propertyName": "redis_buffer_items", + "key": "={{ $('Code - Restaurar contexto buffer imagen TEST').first().json.redis_buffer_key }}", + "keyType": "list", + "options": {} + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + -3744, + -1648 + ], + "id": "a6cfe645-a160-4e56-9f9e-6ef5a8912bb7", + "name": "Redis - Leer buffer imagen TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Restaurar contexto buffer imagen TEST').first().json || {};\nconst redisOutput = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst currentBufferId = limpiar(contexto.redis_buffer_id);\nconst sessionId = limpiar(contexto.session_id);\nconst redisKey = limpiar(contexto.redis_buffer_key);\n\nlet rawItems = redisOutput.redis_buffer_items || redisOutput.message || [];\n\nif (!Array.isArray(rawItems)) {\n rawItems = rawItems ? [rawItems] : [];\n}\n\nconst parsed = [];\n\nfor (const raw of rawItems) {\n try {\n const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;\n if (obj && typeof obj === 'object') {\n parsed.push(obj);\n }\n } catch (error) {\n // Ignorar items corruptos del buffer\n }\n}\n\n// Deduplicar por media_source_id/event_id\nconst vistos = new Set();\nconst lote = [];\n\nfor (const item of parsed) {\n const id = limpiar(item.media_source_id || item.event_id || item.redis_buffer_id);\n if (!id) continue;\n if (vistos.has(id)) continue;\n\n vistos.add(id);\n\n lote.push({\n ...item,\n redis_buffer_key: redisKey,\n session_id: limpiar(item.session_id || sessionId),\n media_source_id: limpiar(item.media_source_id || id),\n event_id: limpiar(item.event_id || id),\n message_type: 'image',\n media_type: 'image'\n });\n}\n\nlote.sort((a, b) => {\n return Number(a.buffer_received_at_ms || 0) - Number(b.buffer_received_at_ms || 0);\n});\n\nconst ultimo = lote[lote.length - 1] || {};\nconst ultimoId = limpiar(ultimo.redis_buffer_id || ultimo.event_id || ultimo.media_source_id);\n\nconst debeProcesar =\n lote.length > 0 &&\n currentBufferId &&\n ultimoId &&\n currentBufferId === ultimoId;\n\nconst imagenesPrevias = Number(\n contexto.sesion_activa?.imagenes_count ??\n contexto.imagenes_count ??\n 0\n) || 0;\n\nconst imagenesNuevas = lote.length;\nconst imagenesTotal = imagenesPrevias + imagenesNuevas;\n\nreturn [\n {\n json: {\n ...contexto,\n\n redis_buffer_items_count: rawItems.length,\n redis_buffer_lote_count: lote.length,\n redis_buffer_lote: lote,\n\n redis_buffer_ultimo_id: ultimoId,\n redis_buffer_current_id: currentBufferId,\n\n redis_debe_procesar_lote: debeProcesar,\n redis_decision: debeProcesar ? 'PROCESAR_LOTE_IMAGEN' : 'IGNORAR_EJECUCION_INTERMEDIA',\n\n imagenes_previas: imagenesPrevias,\n imagenes_nuevas_lote: imagenesNuevas,\n imagenes_count: imagenesTotal,\n\n estado_buffer_redis: debeProcesar\n ? 'LOTE_IMAGEN_LISTO_PARA_PROCESAR'\n : 'IMAGEN_EN_BUFFER_ESPERANDO_ULTIMA_EJECUCION',\n\n redis_debug: {\n redisKey,\n rawItemsCount: rawItems.length,\n loteCount: lote.length,\n currentBufferId,\n ultimoId,\n debeProcesar\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -3536, + -1648 + ], + "id": "3dd5e31b-2cee-4b91-9d21-091161591242", + "name": "Code - Decidir procesar lote imagen Redis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "f2394967-cd4b-4c6e-89be-7d9410d8a68b", + "leftValue": "={{ $json.redis_debe_procesar_lote }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -3312, + -1648 + ], + "id": "ad472bb4-83c4-4980-bdcc-ab1b185ffe71", + "name": "IF - Procesar lote imagen Redis TEST" + }, + { + "parameters": { + "operation": "delete", + "key": "={{ $json.redis_buffer_key }}" + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + -3040, + -1664 + ], + "id": "46b07f3c-4e6a-4c32-be24-a6bbfcf2ea69", + "name": "Redis - Borrar buffer imagen TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst lote = Array.isArray(contexto.redis_buffer_lote)\n ? contexto.redis_buffer_lote\n : [];\n\nif (!lote.length) {\n return [];\n}\n\nreturn lote.map((item, index) => {\n return {\n json: {\n ...contexto,\n ...item,\n\n media_index: index + 1,\n media_total: lote.length,\n\n estado: 'IMAGEN_RECIBIDA',\n fecha_procesado: new Date().toISOString(),\n\n lote_imagen_redis_total: lote.length,\n redis_lote_procesado: true\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -2832, + -1664 + ], + "id": "a28b6faa-d06a-45cb-9f43-7a159eb5d4e4", + "name": "Code - Expandir lote imagen Redis TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "event_id": "={{ $json.event_id }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "tiene_media": "={{ $json.tiene_media }}", + "estado": "=IMAGEN_RECIBIDA", + "session_id": "={{ $json.session_id }}", + "canal_origen": "=WHATSAPP", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "message_type": "={{ $json.message_type }}", + "texto": "={{ $json.texto }}", + "media_count": "={{ $json.media_count }}", + "raw_preview": "={{ $json.raw_preview }}", + "etapa_recibida": "={{ $json.etapa_actual }}", + "media_source_id": "={{ $json.media_source_id }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}", + "comando": "={{ $json.accion_flujo }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_procesado": "={{ new Date().toISOString() }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "event_id", + "displayName": "event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "message_type", + "displayName": "message_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "texto", + "displayName": "texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comando", + "displayName": "comando", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tiene_media", + "displayName": "tiene_media", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_count", + "displayName": "media_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "raw_preview", + "displayName": "raw_preview", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_procesado", + "displayName": "fecha_procesado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa_recibida", + "displayName": "etapa_recibida", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_source_id", + "displayName": "media_source_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -2592, + -1664 + ], + "id": "8b7ff874-bd05-499e-8655-381366cdba2e", + "name": "Sheets - Guardar eventos imagen lote Redis TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst guardados = $input.all().map(item => item.json || {});\n\nconst loteCount = Number(contexto.redis_buffer_lote_count || guardados.length || 0);\nconst imagenesPrevias = Number(contexto.imagenes_previas || 0) || 0;\nconst imagenesTotal = Number(contexto.imagenes_count || (imagenesPrevias + loteCount)) || loteCount;\n\nreturn [\n {\n json: {\n ...contexto,\n\n eventos_imagen_guardados: guardados.length,\n imagenes_nuevas_lote: loteCount,\n imagenes_count: imagenesTotal,\n\n ultima_actividad: new Date().toISOString(),\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'IMAGENES_RECIBIDAS',\n motivo_revision: `IMAGENES_RECIBIDAS_LOTE_${loteCount}`,\n\n redis_lote_imagen_guardado: true\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -2384, + -1664 + ], + "id": "b75fb900-5602-4d29-9a8e-9418a8262ee2", + "name": "Code - Consolidar lote imagen guardado Redis TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "estado": "={{ $json.estado }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "etapa": "={{ $json.etapa }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -2128, + -1664 + ], + "id": "fb406c50-cea0-46bd-8ddd-cdd0b6b796ee", + "name": "Sheets - Actualizar sesión imágenes lote Redis TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Consolidar lote imagen guardado Redis TEST').first().json || {};\nconst data = {\n ...contexto,\n ...($json || {})\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(data.idioma_flujo || 'ES').toUpperCase();\n\nconst to = limpiar(\n data.whatsapp_to ||\n data.group_jid ||\n data.whatsapp_remote_jid ||\n data.sesion_activa?.group_jid ||\n data.sesion_activa?.whatsapp_to ||\n data.sender_jid ||\n ''\n);\n\nconst count = Number(\n data.imagenes_nuevas_lote ||\n data.redis_buffer_lote_count ||\n data.eventos_imagen_guardados ||\n 1\n) || 1;\n\nif (!to) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar confirmación de imágenes.');\n}\n\nconst pluralEs = count === 1 ? 'imagen recibida' : 'imágenes recibidas';\nconst pluralEn = count === 1 ? 'image received' : 'images received';\n\nconst mensajeEs =\n`✅ ${count} ${pluralEs}.\n\nPuedes enviar más imágenes si hace falta.\n\nCuando termines de enviar las imágenes, escribe: FOTOS LISTAS`;\n\nconst mensajeEn =\n`✅ ${count} ${pluralEn}.\n\nYou can send more images if needed.\n\nWhen you finish sending images, write: PHOTOS READY`;\n\nconst mensajeFinal = idioma === 'EN' ? mensajeEn : mensajeEs;\n\nreturn [\n {\n json: {\n ...data,\n\n whatsapp_to: to,\n\n // Campos compatibles con tu nodo WhatsApp actual\n whatsapp_text: mensajeFinal,\n texto_respuesta: mensajeFinal,\n mensaje: mensajeFinal,\n text: mensajeFinal,\n\n // Campos debug\n mensaje_whatsapp: mensajeFinal,\n message_text: mensajeFinal,\n estado_mensaje: 'CONFIRMACION_IMAGENES_LOTE_PREPARADA',\n confirmacion_imagenes_lote_debug: {\n idioma,\n whatsapp_to: to,\n imagenes_nuevas_lote: count\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -1792, + -1648 + ], + "id": "96bf8413-9d42-4f7c-a3da-acb387f9caa5", + "name": "Code - Preparar confirmación imágenes lote Redis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "2cb38fef-879c-45f3-8886-b2f0a35d067b", + "leftValue": "={{ $json.analisis_debe_intentar_cierre }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 1280, + 2208 + ], + "id": "7995206c-6cce-441d-a03f-9ffb145d4449", + "name": "IF - Análisis completo Redis TEST" + }, + { + "parameters": { + "operation": "incr", + "key": "={{ $json.redis_lock_key }}", + "expire": true, + "ttl": 86400 + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 2880, + 2112 + ], + "id": "b6846811-0e07-472f-9680-7ce231a836a2", + "name": "Redis - Lock análisis final TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const actual = $input.item.json || {};\n\nconst extraerNumero = (valor) => {\n if (typeof valor === 'number') return valor;\n\n if (typeof valor === 'string') {\n const n = Number(valor);\n if (!Number.isNaN(n)) return n;\n }\n\n if (valor && typeof valor === 'object') {\n const valores = Object.values(valor);\n for (const v of valores) {\n const n = extraerNumero(v);\n if (n) return n;\n }\n }\n\n return 0;\n};\n\nconst redisActual = actual.redis_lock_response || actual;\n\nconst lockValor = extraerNumero(redisActual);\nconst lockAdquirido = lockValor === 1;\n\nreturn {\n json: {\n ...actual,\n\n redis_lock_response: redisActual,\n redis_lock_value: lockValor,\n\n lock_analisis_adquirido: lockAdquirido,\n decision_lock_analisis: lockAdquirido\n ? 'LOCK_ADQUIRIDO_GENERAR_REPORTE_FINAL'\n : 'LOCK_YA_EXISTIA_NO_GENERAR_DUPLICADO',\n\n lock_analisis_debug: {\n session_id: actual.session_id || '',\n redis_lock_key: actual.redis_lock_key || '',\n redis_lock_value: lockValor,\n lock_analisis_adquirido: lockAdquirido,\n redis_lock_response: redisActual,\n },\n },\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3088, + 2112 + ], + "id": "69448b97-ba47-40ec-b7f6-2e43f2fca593", + "name": "Code - Validar lock análisis final TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "432f82ae-e1bb-4ea7-a964-0cc4bebef5de", + "leftValue": "={{ $json.lock_analisis_adquirido }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 3296, + 2112 + ], + "id": "110f816e-13e6-42ef-b8b1-9396d0217f4c", + "name": "IF - Lock análisis final adquirido TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const actual = $input.item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n actual.session_id ||\n actual.sesion_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n actual.ejecucion_id ||\n sessionId\n);\n\nconst mediaEventId = limpiar(\n actual.media_event_id ||\n actual.message_id ||\n actual.id ||\n ''\n);\n\nconst mediaType = limpiar(\n actual.media_type ||\n actual.tipo_media ||\n ''\n).toLowerCase();\n\nconst mediaIndex = toNumber(\n actual.media_index ||\n actual.index ||\n 1,\n 1\n);\n\nconst mediaTotal = toNumber(\n actual.media_total_esperado ||\n actual.media_total ||\n actual.total_media ||\n 1,\n 1\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nif (!mediaEventId) {\n throw new Error('No llegó media_event_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nconst redisCountKey = `fulgencio:analisis-count:${sessionId}`;\nconst redisLockKey = `fulgencio:analisis-final-lock:${sessionId}`;\n\nreturn {\n json: {\n ...actual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: mediaEventId,\n media_type: mediaType,\n\n media_index: mediaIndex,\n media_total: mediaTotal,\n media_total_esperado: mediaTotal,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n contador_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: mediaType,\n media_index: mediaIndex,\n media_total: mediaTotal,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 416, + 2304 + ], + "id": "5e9bef81-fa4c-4c86-9a85-3f15927d32de", + "name": "Code - Preparar contador análisis Redis TEST" + }, + { + "parameters": { + "operation": "incr", + "key": "={{ $json.redis_count_key }}", + "expire": true, + "ttl": 86400 + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 672, + 2304 + ], + "id": "6fe212ba-75bb-4e41-b35e-9d7d222ad407", + "name": "Redis - Contar análisis media TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const redisActual = $input.item.json || {};\nconst ctx = $('Code - Preparar contador análisis Redis TEST').item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(ctx.session_id);\nconst ejecucionId = limpiar(ctx.ejecucion_id || sessionId);\n\nconst mediaTotalEsperado = toNumber(\n ctx.media_total_esperado ||\n ctx.media_total ||\n 1,\n 1\n);\n\nconst redisCountKey = limpiar(\n ctx.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n ctx.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\n// El Redis Increment puede devolver:\n// { \"fulgencio:analisis-count:SESSION\": 2 }\n// o puede devolver otro nombre según n8n.\nlet redisConteoActual = 0;\n\nif (redisCountKey && redisActual[redisCountKey] !== undefined) {\n redisConteoActual = toNumber(redisActual[redisCountKey], 0);\n} else {\n const valoresNumericos = Object.values(redisActual)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n redisConteoActual = valoresNumericos.length ? valoresNumericos[0] : 0;\n}\n\nconst analisisCompleto =\n mediaTotalEsperado > 0 &&\n redisConteoActual >= mediaTotalEsperado;\n\nreturn {\n json: {\n ...ctx,\n\n redis_increment_response: redisActual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_contados_redis: redisConteoActual,\n\n analisis_debe_intentar_cierre: analisisCompleto,\n decision_cierre_analisis: analisisCompleto\n ? 'ANALISIS_COMPLETO_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_ESPERAR_OTRA_EJECUCION',\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n cierre_analisis_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n redisConteoActual,\n analisisCompleto,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n media_actual: {\n media_event_id: ctx.media_event_id,\n media_type: ctx.media_type,\n media_index: ctx.media_index,\n media_total: ctx.media_total\n },\n redis_increment_response: redisActual\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 912, + 2304 + ], + "id": "f7041b6a-682a-4470-a76e-7dc7efd8586f", + "name": "Code - Validar conteo análisis Redis TEST" + }, + { + "parameters": { + "amount": 10 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 1568, + 2384 + ], + "id": "53135800-1202-4d11-b16e-71c7f8442f84", + "name": "Wait - Verificar cierre análisis Redis TEST", + "webhookId": "b17e9d5b-ec57-49ea-a0a7-91bf65e5e41a" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1107394537, + "mode": "list", + "cachedResultName": "wa_ejecuciones_analisis_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" + }, + "filtersUI": { + "values": [ + { + "lookupColumn": "session_id", + "lookupValue": "={{ $json.session_id }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 1840, + 2416 + ], + "id": "7d1d5352-258d-493e-a6e9-3579fbe54be0", + "name": "Sheets - Leer análisis media cierre Redis TEST", + "alwaysOutputData": true, + "executeOnce": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const inputRows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst contextos = [\n getNodeJson('Code - Validar reintento cierre análisis TEST'),\n getNodeJson('Wait - Reintento cierre análisis Redis TEST'),\n getNodeJson('Wait - Verificar cierre análisis Redis TEST'),\n getNodeJson('Code - Validar conteo análisis Redis TEST'),\n getNodeJson('Code - Preparar contador análisis Redis TEST')\n];\n\nconst contexto =\n contextos.find(ctx => limpiar(ctx.session_id)) ||\n inputRows.find(row => limpiar(row.session_id)) ||\n {};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n inputRows[0]?.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n inputRows[0]?.ejecucion_id ||\n sessionId\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para decidir cierre de análisis por Sheet.');\n}\n\nconst redisCountKey = limpiar(\n contexto.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n contexto.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\nconst rowsSesion = inputRows.filter(row => {\n return limpiar(row.session_id) === sessionId;\n});\n\nconst rowsCompletados = rowsSesion.filter(row => {\n const estado = limpiar(row.estado).toUpperCase();\n\n return (\n estado.includes('COMPLETADO') &&\n (\n estado.includes('ANALISIS_AUDIO') ||\n estado.includes('ANALISIS_IMAGEN') ||\n estado.includes('ANALISIS_VIDEO')\n )\n );\n});\n\nconst mapaUnicos = new Map();\n\nfor (const row of rowsCompletados) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaIndex = limpiar(row.media_index);\n const mediaType = limpiar(row.media_type).toLowerCase();\n\n const key = mediaEventId || `${mediaType}_${mediaIndex}`;\n\n if (!key) continue;\n\n if (!mapaUnicos.has(key)) {\n mapaUnicos.set(key, row);\n }\n}\n\nconst analisisUnicos = Array.from(mapaUnicos.values()).sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst esperadoDesdeContexto = Number(\n contexto.media_total_esperado ||\n contexto.media_total ||\n 0\n);\n\nconst maxMediaTotalSheet = Math.max(\n 0,\n ...analisisUnicos\n .map(row => Number(row.media_total || row.media_total_esperado || 0))\n .filter(n => Number.isFinite(n))\n);\n\nconst mediaTotalEsperado = esperadoDesdeContexto || maxMediaTotalSheet || 0;\nconst totalAnalisisSheet = analisisUnicos.length;\n\nconst analisisCompletoPorSheet =\n mediaTotalEsperado > 0 &&\n totalAnalisisSheet >= mediaTotalEsperado;\n\nreturn [\n {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n media_total_esperado: mediaTotalEsperado,\n\n total_analisis_encontrados_sheet: totalAnalisisSheet,\n analisis_completo_por_sheet: analisisCompletoPorSheet,\n analisis_debe_intentar_cierre: analisisCompletoPorSheet,\n\n decision_cierre_analisis: analisisCompletoPorSheet\n ? 'ANALISIS_COMPLETO_POR_SHEET_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_POR_SHEET_ESPERAR_REINTENTO',\n\n analisis_rows_filtrados: analisisUnicos,\n\n cierre_analisis_sheet_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n totalAnalisisSheet,\n analisisCompletoPorSheet,\n rows_leidas_sheet: inputRows.length,\n rows_misma_sesion: rowsSesion.length,\n rows_completadas: rowsCompletados.length,\n analisis_unicos: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado\n }))\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2048, + 2416 + ], + "id": "cf0e29f2-c2d4-4538-b4cd-2543d8361cf6", + "name": "Code - Decidir cierre análisis por Sheet Redis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "5a5b3564-6292-486b-8154-5df844083a7d", + "leftValue": "={{ $json.analisis_debe_intentar_cierre }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 2256, + 2416 + ], + "id": "4dad5977-e20f-48d6-b7f6-0ecdcb84ad88", + "name": "IF - Cierre análisis por Sheet Redis TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const redisActual = $input.item.json || {};\n\nlet contexto = {};\ntry {\n contexto = $('IF - Cierre análisis por Sheet Redis TEST').item.json || {};\n} catch (error) {\n contexto = {};\n}\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n redisActual.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n redisActual.ejecucion_id ||\n sessionId\n);\n\nconst retryKey = limpiar(\n contexto.redis_retry_key ||\n redisActual.redis_retry_key ||\n `fulgencio:analisis-retry:${sessionId}`\n);\n\nconst extraerNumeroRedis = (obj) => {\n if (!obj || typeof obj !== 'object') return 0;\n\n if (retryKey && obj[retryKey] !== undefined) {\n return toNumber(obj[retryKey], 0);\n }\n\n const valoresNumericos = Object.values(obj)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n return valoresNumericos.length ? valoresNumericos[0] : 0;\n};\n\nconst retryCount = extraerNumeroRedis(redisActual);\n\nconst maxReintentos = 12;\nconst reintentar = retryCount <= maxReintentos;\n\nreturn {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_retry_key: retryKey,\n cierre_retry_count: retryCount,\n cierre_retry_max: maxReintentos,\n cierre_retry_permitido: reintentar,\n\n decision_reintento_cierre: reintentar\n ? 'REINTENTAR_LECTURA_ANALISIS'\n : 'REINTENTOS_AGOTADOS',\n\n cierre_retry_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n retry_key: retryKey,\n redis_respuesta: redisActual,\n retry_count: retryCount,\n max_reintentos: maxReintentos,\n reintentar\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2784, + 2496 + ], + "id": "30c22af2-a6a4-4bd9-89fb-c5c3e8beb74e", + "name": "Code - Validar reintento cierre análisis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ad4f88ac-eda8-4cb1-bd08-34aca455e07c", + "leftValue": "={{ $json.cierre_retry_permitido }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 2992, + 2496 + ], + "id": "bd7ab438-286c-45e2-aff2-4ab7602bf6da", + "name": "IF - Reintentar cierre análisis Redis TEST" + }, + { + "parameters": { + "amount": 8 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 3360, + 2480 + ], + "id": "7c544ae1-5de8-4dcd-9591-35a9fc39420f", + "name": "Wait - Reintento cierre análisis Redis TEST", + "webhookId": "b54b49e7-ede0-48e7-8973-0d45d8482cc8" + }, + { + "parameters": { + "operation": "incr", + "key": "={{ 'fulgencio:analisis-retry:' + $json.session_id }}", + "expire": true, + "ttl": 900 + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 2576, + 2496 + ], + "id": "391a22f9-f721-4404-a042-086bcdd18062", + "name": "Redis - Contar reintento cierre análisis TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "url": "={{ \n 'https://slides.googleapis.com/v1/presentations/' + \n (\n $json.presentation_id ||\n $json.presentacion_id ||\n $json.slides_presentation_id ||\n $json.presentacion_ejecucion_id ||\n String($json.presentacion_ejecucion_url || $json.presentation_url || '').match(/\\/presentation\\/d\\/([^/]+)/)?.[1]\n )\n}}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleOAuth2Api", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 10480, + 2128 + ], + "id": "4df34af3-8e2a-4b7a-841c-675397a836cc", + "name": "HTTP - Obtener presentación Slides limpiar TEST", + "credentials": { + "googleOAuth2Api": { + "id": "eHseMeH39kRcXgOF", + "name": "Google account 2" + } + } + }, + { + "parameters": { + "jsCode": "const inputItems = $input.all();\nconst presentation = inputItems[0]?.json || {};\n\n// Contexto anterior: aquí está el link, presentation_id, conteos, etc.\nconst contexto = $('Code - Normalizar respuesta Slides ejecución TEST').first().json || {};\n\nconst limpiar = (v) => String(v ?? '').trim();\n\nconst extraerIdPresentacion = (...valores) => {\n for (const valor of valores) {\n const txt = limpiar(valor);\n if (!txt) continue;\n\n const match = txt.match(/\\/presentation\\/d\\/([^/]+)/);\n if (match?.[1]) return match[1];\n\n // Si ya viene como ID limpio\n if (!txt.includes('/') && txt.length > 20) return txt;\n }\n return '';\n};\n\nconst presentationId = extraerIdPresentacion(\n contexto.presentation_id,\n contexto.presentacion_id,\n contexto.slides_presentation_id,\n contexto.presentacion_ejecucion_id,\n contexto.presentacion_ejecucion_url,\n contexto.presentation_url,\n presentation.presentationId\n);\n\nif (!presentationId) {\n throw new Error('No se pudo detectar presentation_id para limpiar slides vacíos.');\n}\n\n// Detectar cantidad de fotos reales.\n// Usamos varios posibles nombres porque el contexto puede variar según el nodo anterior.\nlet fotosCount = Number(\n contexto.fotos_count ??\n contexto.imagenes_count ??\n contexto.images_count ??\n contexto.media_fotos_count ??\n contexto.media_imagenes_count ??\n contexto.analisis_multimedia?.imagenes?.length ??\n contexto.analisis_multimedia?.fotos?.length ??\n 0\n);\n\n// Seguridad: si llega vacío, intenta contar imágenes insertadas desde posibles arrays.\nif (!fotosCount && Array.isArray(contexto.imagenes)) {\n fotosCount = contexto.imagenes.length;\n}\n\nif (!fotosCount && Array.isArray(contexto.fotos)) {\n fotosCount = contexto.fotos.length;\n}\n\n// Máximo de placeholders esperados en plantilla\nconst maxFotosPlantilla = 25;\n\n// Si por alguna razón no tenemos conteo, NO borramos nada.\nif (!fotosCount || fotosCount < 0) {\n fotosCount = 0;\n}\n\nfotosCount = Math.min(Number(fotosCount || 0), maxFotosPlantilla);\n\nconst extraerTextoSlide = (slide) => {\n const partes = [];\n\n const elementos = slide.pageElements || [];\n for (const el of elementos) {\n const textElements = el?.shape?.text?.textElements || [];\n for (const te of textElements) {\n const content = te?.textRun?.content;\n if (content) partes.push(content);\n }\n }\n\n return partes.join(' ').replace(/\\s+/g, ' ').trim();\n};\n\nconst slides = presentation.slides || [];\n\nconst slidesDetectados = [];\nconst slidesABorrar = [];\n\nfor (const slide of slides) {\n const objectId = slide.objectId;\n const textoSlide = extraerTextoSlide(slide);\n\n // Busca FOTO 1, FOTO 2, etc. Tolerante a espacios.\n const match = textoSlide.match(/\\bFOTO\\s*(\\d{1,2})\\b/i);\n\n if (!match) continue;\n\n const numeroFoto = Number(match[1]);\n if (numeroFoto < 1 || numeroFoto > maxFotosPlantilla) continue;\n\n slidesDetectados.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n\n if (numeroFoto > fotosCount) {\n slidesABorrar.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n }\n}\n\nconst deleteRequests = slidesABorrar.map((s) => ({\n deleteObject: {\n objectId: s.objectId,\n },\n}));\n\nreturn [\n {\n json: {\n ...contexto,\n\n presentation_id: presentationId,\n slides_cleanup_fotos_count: fotosCount,\n slides_cleanup_total_slides: slides.length,\n\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n slides_fotos_vacias_count: slidesABorrar.length,\n\n delete_slide_requests: deleteRequests,\n\n slides_cleanup_debug: {\n presentation_id: presentationId,\n fotos_count_detectado: fotosCount,\n total_slides_presentacion: slides.length,\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n delete_requests_count: deleteRequests.length,\n },\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 10688, + 2128 + ], + "id": "278be3f3-bd01-4aac-b145-8b7a2a24c3f6", + "name": "Code - Preparar borrado slides fotos vacías TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dca8dbb7-b14a-4def-9e43-27a3c1360d78", + "leftValue": "={{ Number($json.slides_fotos_vacias_count || 0) > 0 }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 10896, + 2128 + ], + "id": "3fac8441-4cc4-4332-9a34-94c55a8d624a", + "name": "IF - Hay slides fotos vacías para borrar TEST" + }, + { + "parameters": { + "method": "POST", + "url": "={{ 'https://slides.googleapis.com/v1/presentations/' + $json.presentation_id + ':batchUpdate' }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleOAuth2Api", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ \n {\n requests: $json.delete_slide_requests || []\n }\n}}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 11184, + 1936 + ], + "id": "72d010e7-5aa8-4650-bdb4-36c3546c03c0", + "name": "HTTP - Borrar slides fotos vacías TEST", + "credentials": { + "googleOAuth2Api": { + "id": "eHseMeH39kRcXgOF", + "name": "Google account 2" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Preparar borrado slides fotos vacías TEST').first().json || {};\nconst respuestaBorrado = $input.first()?.json || {};\n\nreturn [\n {\n json: {\n ...contexto,\n slides_cleanup_done: true,\n slides_delete_response: respuestaBorrado,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 11392, + 1936 + ], + "id": "e845eb74-403a-4762-b81c-c0ff06db6e59", + "name": "Code - Restaurar contexto slides limpios TEST" + }, + { + "parameters": { + "content": "## 📥 ENTRADA WHATSAPP Y GRUPO OFICIAL\n\nEste bloque recibe eventos desde Evolution API y normaliza el mensaje recibido.\n\nResponsabilidades:\n- Detectar si viene de grupo o chat individual.\n- Identificar sender real en grupo.\n- Resolver whatsapp_to para responder al grupo correcto.\n- Detectar texto, audio, imagen, video o documento.\n- Normalizar comandos oficiales.\n- Validar que el mensaje venga de un grupo permitido.\n\nRegla principal:\nsolo se procesa si el grupo está autorizado.", + "height": 528, + "width": 832 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -9552, + -1344 + ], + "id": "d86e235c-45ef-48c5-a816-6c32f33fd9ae", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## 🧾 CONTROL DE SESIONES\n\nEste bloque controla si el usuario puede iniciar, continuar o cancelar un reporte.\n\nDecisiones:\n- CREAR_SESION: inicia nuevo reporte con Hey.\n- AVISO_SESION_ACTIVA: ya existe una sesión abierta.\n- CONTINUAR_SESION: sigue el reporte actual.\n- CANCELAR_SESION: cancela sesión activa.\n- SIN_SESION_ACTIVA: no hay reporte abierto.\n- SIN_SESION_PARA_CANCELAR: intento de cancelar sin sesión.\n\nRegla:\nuna propuesta/reporte activo por usuario dentro del grupo.", + "height": 672, + "width": 1024, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -8448, + -1440 + ], + "id": "4b1e674e-b24f-43b9-894c-14f6c018b516", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "## 👋 CREACIÓN Y MENSAJES DE CONTROL\n\nEste bloque crea sesiones nuevas y responde a casos operativos básicos.\n\nIncluye:\n- Crear sesión en Google Sheets.\n- Enviar bienvenida y pedir nota de voz.\n- Avisar si ya existe una sesión activa.\n- Avisar si no hay sesión activa.\n- Cancelar sesión cuando el usuario escribe CANCELAR.\n\nLa bienvenida inicia el flujo guiado de 3 pasos:\n1. Audio obligatorio\n2. Imágenes obligatorias\n3. Videos opcionales", + "height": 1904, + "width": 1200, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -5392, + -4928 + ], + "id": "17ae92c2-aaff-4121-9646-aef8939cfb48", + "name": "Sticky Note2" + }, + { + "parameters": { + "content": "## 🧭 ROUTER DEL REPORTE ACTIVO\n\nEste bloque decide qué hacer según la etapa actual de la sesión.\n\nEtapas:\n- ESPERANDO_AUDIO\n- ESPERANDO_IMAGENES\n- ESPERANDO_VIDEOS\n- PROCESANDO\n\nDecisiones principales:\n- Guardar audio\n- Pedir audio\n- Guardar imágenes\n- Validar FOTOS LISTAS\n- Guardar videos\n- Cerrar sin video\n- Cerrar con videos\n- Avisar procesamiento\n- Ignorar eventos no válidos\n\nRegla:\ncada mensaje se procesa según la etapa activa, no de forma aislada.", + "height": 944, + "width": 1024, + "color": 2 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -7968, + -704 + ], + "id": "09ccf504-4fb9-4a72-94fd-de1af944cbc0", + "name": "Sticky Note3" + }, + { + "parameters": { + "content": "## 🎙️ PASO 1 — AUDIO OBLIGATORIO\n\nEl reporte no avanza sin nota de voz.\n\nCuando llega un audio:\n- Se guarda el evento en wa_ejecuciones_eventos.\n- Se actualiza la sesión.\n- Se incrementa audio_count.\n- La etapa cambia a ESPERANDO_IMAGENES.\n- El bot pide las imágenes obligatorias.\n\nLa nota de voz debe explicar:\n- Propuesta o referencia\n- Marca/cliente\n- País\n- Ubicación\n- Qué se ejecutó o reportó\n- Comentarios o resultados", + "height": 640, + "width": 2448, + "color": 7 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -5952, + -2800 + ], + "id": "cf2e46cc-9bb7-469c-9335-a578a24fc23e", + "name": "Sticky Note4" + }, + { + "parameters": { + "content": "## 🖼️ PASO 2 — IMÁGENES OBLIGATORIAS\n\nEste bloque recibe imágenes y soporta varias imágenes en un mismo mensaje.\n\nUso de Redis:\n- Guarda temporalmente imágenes recibidas.\n- Espera un pequeño cierre de lote.\n- Expande el lote en eventos individuales.\n- Evita perder imágenes cuando WhatsApp las manda juntas.\n\nCuando el usuario termina:\ndebe escribir FOTOS LISTAS.\n\nRegla:\nel reporte no puede avanzar a videos si no hay imágenes guardadas.", + "height": 416, + "width": 3744, + "color": 6 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -5232, + -1808 + ], + "id": "89dc9ab6-f069-4c71-a4fa-db52c0dab336", + "name": "Sticky Note5" + }, + { + "parameters": { + "content": "## 🎥 PASO 3 — VIDEOS OPCIONALES\n\nLos videos son opcionales.\n\nOpciones:\n- Si no hay videos: usuario escribe SIN VIDEO.\n- Si envió videos: usuario escribe LISTO.\n- Los videos deben enviarse uno por uno, en mensajes separados.\n\nAl cerrar:\n- La sesión pasa a PROCESANDO.\n- Se envía aviso de reporte recibido.\n- El flujo inicia recuperación, análisis y generación de presentación.\n\nRegla:\nlas imágenes sí pueden ir juntas; los videos deben ir separados.", + "height": 768, + "width": 1632, + "color": "#1D6362" + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -4672, + -656 + ], + "id": "a74fc961-8836-4f46-ad9e-e943ca01f3a6", + "name": "Sticky Note6" + }, + { + "parameters": { + "content": "## 📂 RECUPERACIÓN Y ORGANIZACIÓN DE EVIDENCIAS\n\nEste bloque recupera todos los archivos de la sesión.\n\nProceso:\n1. Lee eventos multimedia de la sesión.\n2. Recupera media desde Evolution API.\n3. Convierte base64 a binario.\n4. Sube audio, imágenes y videos a Google Drive.\n5. Guarda metadata en wa_ejecuciones_media.\n6. Consolida la media recuperada.\n\nCada archivo queda asociado a:\n- session_id\n- ejecucion_id\n- media_event_id\n- tipo de media\n- link de Drive\n- nombre del archivo", + "height": 1728, + "width": 4400, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -6688, + 272 + ], + "id": "3908b74a-74bb-4333-9e16-7e90c5c74b3c", + "name": "Sticky Note7" + }, + { + "parameters": { + "content": "## 🤖 ANÁLISIS IA Y CONTROL REDIS\n\nGemini analiza cada evidencia multimedia:\n- Audio\n- Imágenes\n- Videos opcionales\n\nCada análisis se guarda en:\nwa_ejecuciones_analisis_media\n\nRedis controla el cierre:\n- Cuenta análisis completados.\n- Verifica si ya están todos listos.\n- Usa lock para evitar cierre duplicado.\n- Si faltan análisis, espera y reintenta.\n\nRegla:\nel JSON final solo se genera cuando el análisis multimedia está completo.", + "height": 1504, + "width": 6304, + "color": 3 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -2112, + 1440 + ], + "id": "4400f1ea-39b9-4302-86b4-a0c463f8e76c", + "name": "Sticky Note8" + }, + { + "parameters": { + "content": "## ✅ CIERRE FINAL DEL REPORTE\n\nEste bloque genera el resultado final del reporte.\n\nProceso:\n1. Consolida análisis multimedia.\n2. Gemini genera JSON final estructurado.\n3. Se normaliza la respuesta.\n4. Se hace match contra el Banco de Propuestas.\n5. Se verifica duplicado.\n6. Se guarda en propuestas_ejecutadas.\n7. Se crea carpeta de ejecución en Drive.\n8. Se copia la plantilla de Slides.\n9. Se insertan imágenes y se borran slides vacíos.\n10. Se actualiza el link final.\n11. Si el match es alto, se actualiza el banco original.\n12. Se envía el link final por WhatsApp.\n\nRegla:\npropuestas externas o match bajo no actualizan el banco original.", + "height": 1216, + "width": 11408, + "color": 6 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 4704, + 1488 + ], + "id": "a3245658-be74-4ea6-8a58-a104803896ac", + "name": "Sticky Note9" + } + ], + "connections": { + "Code - Normalizar evento WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Leer grupos permitidos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook - Evolution WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Normalizar evento WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "WhatsApp - Enviar mensaje TEST": { + "main": [ + [] + ] + }, + "Sheets - Leer sesiones existentes": { + "main": [ + [ + { + "node": "Code - Resolver sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Resolver sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Switch - Decisión sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch - Decisión sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Crear sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar aviso sesión activa WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar sin sesión para cancelar WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar cancelación sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [], + [ + { + "node": "Code - Resolver paso activo WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar bienvenida WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Crear sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar bienvenida WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Resolver paso activo WhatsApp TEST": { + "main": [ + [ + { + "node": "Switch - Paso activo WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch - Paso activo WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar evento audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar solicitud audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [], + [], + [ + { + "node": "Code - Preparar buffer imagen Redis TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Leer eventos sesión fotos WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar aviso falta imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Guardar evento video WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "CERRAR_SIN_VIDEO", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar cierre con videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar recordatorio videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar aviso procesando WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [], + [] + ] + }, + "Code - Preparar solicitud audio WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar evento audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar actualización audio recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización audio recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión audio recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar solicitud imágenes WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso sesión activa WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar solicitud imágenes WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar cancelación sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Cancelar sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Cancelar sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar mensaje cancelación WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar mensaje cancelación WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso falta imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar actualización fotos listas": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión fotos listas WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar solicitud videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar solicitud videos WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar recordatorio videos WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "CERRAR_SIN_VIDEO": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión cierre sin video WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar aviso procesando WhatsApp TEST1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar evento video WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar actualización video recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización video recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión video recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión video recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar confirmación video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar confirmación video WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar cierre con videos WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión cierre con videos WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar aviso procesando WhatsApp TEST1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso procesando WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar búsqueda eventos media WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Leer eventos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer eventos WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Filtrar eventos media de sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Filtrar eventos media de sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Separar media en items WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Separar media en items WhatsApp TEST": { + "main": [ + [ + { + "node": "HTTP Request - Obtener media base64 Evolution TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request - Obtener media base64 Evolution TEST": { + "main": [ + [ + { + "node": "Code - Convertir base64 a binario WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Convertir base64 a binario WhatsApp TEST": { + "main": [ + [ + { + "node": "Drive - Subir media WhatsApp TEST", + "type": "main", + "index": 0 + }, + { + "node": "Switch - Tipo media para Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Subir media WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar registro media Drive WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar registro media Drive WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar media Drive WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar media Drive WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Consolidar media recuperada WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Consolidar media recuperada WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión media recuperada WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar paquete análisis Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch - Tipo media para Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar Gemini audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar Gemini imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar Gemini video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Gemini audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Gemini - Analizar audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Analizar audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Normalizar análisis audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Gemini imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "Gemini - Analizar imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Analizar imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Normalizar análisis imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Gemini video WhatsApp TEST": { + "main": [ + [ + { + "node": "Analyze video", + "type": "main", + "index": 0 + } + ] + ] + }, + "Analyze video": { + "main": [ + [ + { + "node": "Code - Normalizar análisis video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar análisis video WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar análisis imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar análisis audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar contador análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Consolidar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Consolidar análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Gemini - Generar JSON final propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Generar JSON final propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "Code - Normalizar JSON final propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar JSON final propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "Sheets - Leer banco propuestas Fulgencio TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar propuesta ejecutada final TEST": { + "main": [ + [ + { + "node": "Code - Preparar copia presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión análisis completado WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar mensaje final WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar mensaje final WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait": { + "main": [ + [ + { + "node": "Sheets - Leer análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer propuestas ejecutadas final TEST": { + "main": [ + [ + { + "node": "Code - Verificar duplicado propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Verificar duplicado propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "IF - Propuesta final ya existe TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Propuesta final ya existe TEST": { + "main": [ + [], + [ + { + "node": "Sheets - Guardar propuesta ejecutada final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar copia presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Copiar plantilla presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Normalizar link presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar link presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar reemplazos Slides ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar link presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar actualización banco propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar reemplazos Slides ejecución TEST": { + "main": [ + [ + { + "node": "HTTP Request - Reemplazar textos Slides ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request - Reemplazar textos Slides ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar payload insertar imágenes Slides TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar respuesta Slides ejecución TEST": { + "main": [ + [ + { + "node": "HTTP - Obtener presentación Slides limpiar TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar sin sesión para cancelar WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer eventos sesión fotos WhatsApp TEST": { + "main": [ + [ + { + "node": "Preparar actualización fotos listas", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Drive - Crear carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Crear carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Code - Normalizar carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar media para mover carpeta TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar media para mover carpeta TEST": { + "main": [ + [ + { + "node": "Drive - Mover media a carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Mover media a carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Code - Confirmar media movida carpeta TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Confirmar media movida carpeta TEST": { + "main": [ + [ + { + "node": "Drive - Copiar plantilla presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar payload insertar imágenes Slides TEST": { + "main": [ + [ + { + "node": "HTTP - Insertar imágenes en Slides TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Insertar imágenes en Slides TEST": { + "main": [ + [ + { + "node": "Code - Normalizar respuesta Slides ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso procesando WhatsApp TEST1": { + "main": [ + [ + { + "node": "HTTP - Enviar aviso procesando WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto aviso procesando TEST": { + "main": [ + [ + { + "node": "Code - Preparar búsqueda eventos media WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Enviar aviso procesando WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto aviso procesando TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer banco propuestas Fulgencio TEST": { + "main": [ + [ + { + "node": "Code - Match propuesta banco Fulgencio TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Match propuesta banco Fulgencio TEST": { + "main": [ + [ + { + "node": "Code - Enriquecer datos con match banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización banco propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar decisión automática banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Actualizar banco match alta confianza TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar link ejecutada en banco TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar link ejecutada en banco TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto actualización banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto actualización banco TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Enriquecer datos con match banco TEST": { + "main": [ + [ + { + "node": "Sheets - Leer propuestas ejecutadas final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Filtro origen WhatsApp TEST": { + "main": [ + [ + { + "node": "IF - Procesar solo grupo permitido TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Procesar solo grupo permitido TEST": { + "main": [ + [ + { + "node": "Sheets - Leer sesiones existentes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer grupos permitidos WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Filtro origen WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar decisión automática banco TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto decisión automática banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto decisión automática banco TEST": { + "main": [ + [ + { + "node": "IF - Actualizar banco match alta confianza TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar buffer imagen Redis TEST": { + "main": [ + [ + { + "node": "Redis - Push buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Push buffer imagen TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto buffer imagen TEST": { + "main": [ + [ + { + "node": "Wait - Esperar cierre buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait - Esperar cierre buffer imagen TEST": { + "main": [ + [ + { + "node": "Redis - Leer buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Leer buffer imagen TEST": { + "main": [ + [ + { + "node": "Code - Decidir procesar lote imagen Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Decidir procesar lote imagen Redis TEST": { + "main": [ + [ + { + "node": "IF - Procesar lote imagen Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Procesar lote imagen Redis TEST": { + "main": [ + [ + { + "node": "Redis - Borrar buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Borrar buffer imagen TEST": { + "main": [ + [ + { + "node": "Code - Expandir lote imagen Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Expandir lote imagen Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar eventos imagen lote Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar eventos imagen lote Redis TEST": { + "main": [ + [ + { + "node": "Code - Consolidar lote imagen guardado Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Consolidar lote imagen guardado Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión imágenes lote Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión imágenes lote Redis TEST": { + "main": [ + [ + { + "node": "Code - Preparar confirmación imágenes lote Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar confirmación imágenes lote Redis TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Análisis completo Redis TEST": { + "main": [ + [ + { + "node": "Redis - Lock análisis final TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Wait - Verificar cierre análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Lock análisis final TEST": { + "main": [ + [ + { + "node": "Code - Validar lock análisis final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar lock análisis final TEST": { + "main": [ + [ + { + "node": "IF - Lock análisis final adquirido TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Lock análisis final adquirido TEST": { + "main": [ + [ + { + "node": "Wait", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar contador análisis Redis TEST": { + "main": [ + [ + { + "node": "Redis - Contar análisis media TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Contar análisis media TEST": { + "main": [ + [ + { + "node": "Code - Validar conteo análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar conteo análisis Redis TEST": { + "main": [ + [ + { + "node": "IF - Análisis completo Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait - Verificar cierre análisis Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Leer análisis media cierre Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer análisis media cierre Redis TEST": { + "main": [ + [ + { + "node": "Code - Decidir cierre análisis por Sheet Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Decidir cierre análisis por Sheet Redis TEST": { + "main": [ + [ + { + "node": "IF - Cierre análisis por Sheet Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Cierre análisis por Sheet Redis TEST": { + "main": [ + [ + { + "node": "Redis - Lock análisis final TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Redis - Contar reintento cierre análisis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar reintento cierre análisis TEST": { + "main": [ + [ + { + "node": "IF - Reintentar cierre análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Reintentar cierre análisis Redis TEST": { + "main": [ + [ + { + "node": "Wait - Reintento cierre análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait - Reintento cierre análisis Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Leer análisis media cierre Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Contar reintento cierre análisis TEST": { + "main": [ + [ + { + "node": "Code - Validar reintento cierre análisis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Obtener presentación Slides limpiar TEST": { + "main": [ + [ + { + "node": "Code - Preparar borrado slides fotos vacías TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar borrado slides fotos vacías TEST": { + "main": [ + [ + { + "node": "IF - Hay slides fotos vacías para borrar TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Hay slides fotos vacías para borrar TEST": { + "main": [ + [ + { + "node": "HTTP - Borrar slides fotos vacías TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Actualizar link presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Borrar slides fotos vacías TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto slides limpios TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto slides limpios TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar link presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "Isaac Aracena", + "name": "Version a72fa72c", + "description": "", + "autosaved": true, + "workflowPublishHistory": [ + { + "createdAt": "2026-06-25T19:31:03.336Z", + "id": 1885, + "workflowId": "CdosDIWBgnDMnPdr", + "versionId": "a72fa72c-6f91-4fca-9982-2b2438e339eb", + "event": "activated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + ] + } +} \ No newline at end of file