From 6e975fb1102fc5d659d2138729d5f2c62a232a1e Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Fri, 7 Aug 2026 14:19:13 +0000 Subject: [PATCH] Subir archivos a "/" --- Chat de Fulgencio.json | 556 + ...Propuestas Ejecutadas - Evolution API.json | 10112 +++++++++++++++ ...e Propuestas Ejecutadas - API Oficial.json | 10230 ++++++++++++++++ Fulgencio Alertas Sheets - GLM.json | 402 + ...io Procesar Propuestas en Bruto - GLM.json | 668 + 5 files changed, 21968 insertions(+) create mode 100644 Chat de Fulgencio.json create mode 100644 Chat de WhatsApp - Propuestas Ejecutadas - Evolution API.json create mode 100644 Chat de WhatsApp de Propuestas Ejecutadas - API Oficial.json create mode 100644 Fulgencio Alertas Sheets - GLM.json create mode 100644 Fulgencio Procesar Propuestas en Bruto - GLM.json diff --git a/Chat de Fulgencio.json b/Chat de Fulgencio.json new file mode 100644 index 0000000..b7aa98b --- /dev/null +++ b/Chat de Fulgencio.json @@ -0,0 +1,556 @@ +{ + "name": "Chat de Fulgencio", + "nodes": [ + { + "parameters": { + "respondWith": "json", + "responseBody": "{\n \"text\": \"⏳ Procesando tu solicitud, dame un momento...\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + -512, + 864 + ], + "id": "0f2904c5-1488-4c75-97b2-64b4cb2bd911", + "name": "Respond to Webhook" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const webhookData = $json || {};\n\n// En algunos casos Google Chat llega dentro de body.\n// En otros, puede venir directo en el objeto principal.\nconst body = webhookData.body || webhookData;\n\nconst message = body.message || {};\nconst user = body.user || message.sender || {};\nconst space = body.space || message.space || {};\nconst thread = body.thread || message.thread || {};\n\nconst texto =\n message.argumentText ||\n message.text ||\n body.text ||\n '';\n\nconst pregunta = String(texto || '')\n .replace(/^@\\S+\\s*/, '')\n .trim();\n\nreturn {\n json: {\n pregunta,\n usuario_nombre: user.displayName || message.sender?.displayName || '',\n usuario_email: user.email || message.sender?.email || '',\n space_name: space.name || message.space?.name || '',\n thread_name: thread.name || message.thread?.name || '',\n message_name: message.name || '',\n event_type: body.type || '',\n fecha_evento: body.eventTime || new Date().toISOString()\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -320, + 864 + ], + "id": "85040629-41da-4ccb-b4cc-f4d7b15d9e7d", + "name": "Code - Preparar mensaje" + }, + { + "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" + }, + "filtersUI": { + "values": [] + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 80, + 864 + ], + "id": "f7cea213-1a5a-476a-8f87-3bc3b04ab1b2", + "name": "Sheets - Leer propuestas", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const rows = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst MAX_CANDIDATOS_DEFAULT = 25;\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nfunction getFirstValue(row, keys = []) {\n for (const key of keys) {\n const value = clean(row?.[key]);\n if (value) return value;\n }\n return '';\n}\n\nfunction getEnlacesEjecutadas(row) {\n return getFirstValue(row, [\n 'Enlaces a propuestas ejecutadas',\n 'Enlaces propuestas ejecutadas',\n 'Enlace a propuestas ejecutadas',\n 'Enlace propuesta ejecutada',\n 'Propuestas ejecutadas',\n 'Enlaces a ejecuciones',\n 'Ejecuciones',\n ]);\n}\n\nconst preguntaNorm = norm(pregunta);\n\nfunction detectarIntencionEspecial(texto) {\n const t = norm(texto);\n\n const esSaludoSimple = [\n // Español\n 'HOLA',\n 'BUENAS',\n 'BUENOS DIAS',\n 'BUENOS DÍAS',\n 'BUEN DIA',\n 'BUEN DÍA',\n 'BUENAS TARDES',\n 'BUENAS NOCHES',\n 'SALUDOS',\n 'HEY',\n\n // Inglés\n 'HI',\n 'HELLO',\n 'HEY THERE',\n 'GOOD MORNING',\n 'GOOD AFTERNOON',\n 'GOOD EVENING',\n\n // Genéricos / cortesía\n 'OK',\n 'OKAY',\n 'OKEY',\n 'LISTO',\n 'DALE',\n 'PERFECTO',\n 'GRACIAS',\n 'THANKS',\n 'THANK YOU',\n 'ENTENDIDO',\n 'VALE'\n ].includes(t);\n\n if (esSaludoSimple) {\n return {\n tipo: 'saludo_ayuda',\n prompt: `\nEl usuario solo saludó o envió un mensaje muy corto.\n\nResponde de forma breve, cercana y útil. No busques propuestas, briefs ni ejecuciones.\n\nDebes decir que eres Fulgencio Fumado, asistente interno para consultar propuestas, briefs, propuestas ejecutadas, propuestas externas y recursos creativos de GomezLee Marketing.\n\nIncluye ejemplos cortos de lo que puede preguntar:\n1. Dame las 5 últimas propuestas de Motorola.\n2. Busca propuestas de Nestlé en República Dominicana.\n3. Dame propuestas aprobadas de activación en PDV.\n4. Dame los últimos briefs creados.\n5. Busca briefs relacionados con Motorola.\n6. Envíame la presentación de la propuesta ejecutada de Pollo Campero.\n7. Dame propuestas externas registradas.\n8. Necesito crear un brief.\n9. Necesito acceder al Tablero CDC.\n\nNo incluyas propuestas específicas, briefs específicos, ejecuciones específicas ni enlaces de archivos.\nSolo incluye estos enlaces generales si aplica:\n- Crear brief: https://digitalcompass.agency/brief\n- Tablero CDC: https://digitalcompass.agency/tablero-cdc/\n`\n };\n }\n\n // IMPORTANTE:\n // No tratamos cualquier mención de \"brief\" como intención de crear un brief,\n // porque Fulgencio también puede buscar briefs existentes en el Sheet de CDC Briefs.\n const pideCrearBrief =\n t.includes('CREAR BRIEF') ||\n t.includes('CREAR UN BRIEF') ||\n t.includes('CREAR UNA SOLICITUD') ||\n t.includes('CREAR SOLICITUD') ||\n t.includes('LLENAR BRIEF') ||\n t.includes('LLENAR UN BRIEF') ||\n t.includes('HACER BRIEF') ||\n t.includes('HACER UN BRIEF') ||\n t.includes('NUEVO BRIEF') ||\n t.includes('NUEVA SOLICITUD') ||\n t.includes('FORMULARIO DE BRIEF') ||\n t.includes('FORMULARIO PARA BRIEF') ||\n t.includes('SOLICITUD CREATIVA');\n\n const pideImagen =\n t.includes('CREAR IMAGEN') ||\n t.includes('GENERAR IMAGEN') ||\n t.includes('HAZME UNA IMAGEN') ||\n t.includes('DISEÑA UNA IMAGEN') ||\n t.includes('DISENA UNA IMAGEN') ||\n t.includes('IMAGEN NUEVA') ||\n t.includes('ARTE NUEVO');\n\n const pideAyuda =\n t.includes('QUE PUEDES HACER') ||\n t.includes('QUÉ PUEDES HACER') ||\n t.includes('AYUDA') ||\n t.includes('COMO TE USO') ||\n t.includes('CÓMO TE USO') ||\n t.includes('COMO FUNCIONAS') ||\n t.includes('CÓMO FUNCIONAS') ||\n t.includes('COMO FUNCIONA') ||\n t.includes('CÓMO FUNCIONA');\n\n if (pideCrearBrief) {\n return {\n tipo: 'brief',\n prompt: `\nEl usuario necesita crear o llenar un brief.\n\nResponde de forma breve indicando que puede crear la solicitud en este enlace:\nhttps://digitalcompass.agency/brief\n\nNo busques propuestas. No inventes información adicional.\n`\n };\n }\n\n if (pideImagen) {\n return {\n tipo: 'imagen_no_disponible',\n prompt: `\nEl usuario está pidiendo crear o generar una imagen.\n\nResponde claramente que la creación de imágenes no está disponible actualmente en Fulgencio.\nLuego ofrece ayudarle a buscar referencias, propuestas, campañas, briefs, propuestas ejecutadas, propuestas externas o ideas dentro de las fuentes disponibles.\n`\n };\n }\n\n if (pideAyuda) {\n return {\n tipo: 'ayuda',\n prompt: `\nEl usuario está pidiendo ayuda sobre cómo usar Fulgencio.\n\nExplica brevemente que Fulgencio puede ayudar a buscar información en:\n- banco de propuestas\n- briefs o solicitudes creativas de CDC Brief\n- propuestas ejecutadas\n- propuestas externas\n- evidencias y presentaciones de ejecución\n\nPuede buscar por:\n- cliente\n- marca\n- país\n- año\n- tipo de acción\n- estado de aprobación\n- canal\n- etiquetas o temas\n- brief ID\n- solicitante\n- tema del brief\n- nombre de propuesta ejecutada\n- presentación de ejecución\n- propuesta externa\n\nDa 8 ejemplos de preguntas útiles:\n1. Dame las 5 últimas propuestas de Motorola.\n2. Busca propuestas de Nestlé en República Dominicana.\n3. Dame propuestas aprobadas de activación en PDV.\n4. Tienes propuestas de Kit Kat en Costa Rica?\n5. Dame los últimos briefs creados.\n6. Busca briefs relacionados con Motorola.\n7. Envíame la presentación de la propuesta ejecutada de Pollo Campero Guatemala.\n8. Dame propuestas externas registradas.\n`\n };\n }\n\n return null;\n}\n\nfunction extraerCantidad(texto) {\n const t = norm(texto);\n\n const numero = t.match(/\\b(\\d{1,2})\\b/);\n if (numero) return Number(numero[1]);\n\n const palabras = {\n UNA: 1,\n UNO: 1,\n DOS: 2,\n TRES: 3,\n CUATRO: 4,\n CINCO: 5,\n SEIS: 6,\n SIETE: 7,\n OCHO: 8,\n NUEVE: 9,\n DIEZ: 10,\n QUINCE: 15,\n VEINTE: 20,\n };\n\n for (const [palabra, valor] of Object.entries(palabras)) {\n if (t.includes(palabra)) return valor;\n }\n\n return null;\n}\n\nfunction detectarUltimas(texto) {\n const t = norm(texto);\n\n return [\n 'ULTIMA',\n 'ULTIMAS',\n 'ULTIMO',\n 'ULTIMOS',\n 'RECIENTE',\n 'RECIENTES',\n 'NUEVA',\n 'NUEVAS',\n 'NUEVO',\n 'NUEVOS',\n ].some(x => t.includes(x));\n}\n\nfunction detectarSeguimiento(texto) {\n const t = norm(texto);\n\n return [\n 'DE ESAS',\n 'DE ESTAS',\n 'DE LOS ANTERIORES',\n 'DE LAS ANTERIORES',\n 'LA ANTERIOR',\n 'EL ANTERIOR',\n 'LA PRIMERA',\n 'LA SEGUNDA',\n 'LA TERCERA',\n 'ESAS',\n 'ESTAS',\n 'ESOS',\n 'ESTOS',\n 'LOS QUE ME MOSTRASTE',\n 'LAS QUE ME MOSTRASTE',\n ].some(x => t.includes(x));\n}\n\nfunction detectarEstado(texto) {\n const t = norm(texto);\n\n if (t.includes('NO APROBADA') || t.includes('NO APROBADAS')) {\n return 'NO APROBADA';\n }\n\n if (t.includes('APROBADA') || t.includes('APROBADAS')) {\n return 'APROBADA';\n }\n\n if (\n t.includes('PENDIENTE') ||\n t.includes('PENDIENTES') ||\n t.includes('PENDIENTE DE APROBACION') ||\n t.includes('PENDIENTE DE APROBACIÓN')\n ) {\n return 'PENDIENTE DE APROBACION';\n }\n\n return '';\n}\n\nfunction detectarAnios(texto) {\n const matches = norm(texto).match(/\\b(20\\d{2}|19\\d{2})\\b/g);\n return matches ? [...new Set(matches)] : [];\n}\n\nfunction detectarPaises(texto) {\n const t = norm(texto);\n\n const paises = [\n 'EL SALVADOR',\n 'PANAMA',\n 'REPUBLICA DOMINICANA',\n 'COLOMBIA',\n 'PUERTO RICO',\n 'HONDURAS',\n 'MEXICO',\n 'VENEZUELA',\n 'JAMAICA',\n 'TRINIDAD Y TOBAGO',\n 'COSTA RICA',\n 'NICARAGUA',\n 'GUATEMALA',\n ];\n\n const encontrados = [];\n\n for (const pais of paises) {\n if (t.includes(norm(pais))) {\n encontrados.push(pais);\n }\n }\n\n if (/\\bRD\\b/.test(t) || t.includes('REP DOM') || t.includes('DOMINICANA')) {\n if (!encontrados.includes('REPUBLICA DOMINICANA')) {\n encontrados.push('REPUBLICA DOMINICANA');\n }\n }\n\n return encontrados;\n}\n\nfunction detectarCanales(texto) {\n const t = norm(texto);\n\n const canales = [];\n\n if (t.includes('MODERNO')) canales.push('MODERNO');\n if (t.includes('TRADICIONAL')) canales.push('TRADICIONAL');\n if (t.includes('ONLINE') || t.includes('ON LINE')) canales.push('ONLINE');\n if (t.includes('PDV') || t.includes('PUNTO DE VENTA')) canales.push('PDV');\n if (t.includes('NO APLICA')) canales.push('NO APLICA');\n\n return canales;\n}\n\nfunction detectarInteresPropuestas(texto) {\n const t = norm(texto);\n\n return [\n 'PROPUESTA',\n 'PROPUESTAS',\n 'BANCO DE PROPUESTAS',\n 'CASO',\n 'CASOS',\n 'ACTIVACION',\n 'ACTIVACIONES',\n 'ACTIVACIÓN',\n 'CAMPAÑA',\n 'CAMPAÑAS',\n 'CAMPANA',\n 'CAMPANAS',\n 'PRESENTACION',\n 'PRESENTACIONES',\n 'PRESENTACIÓN',\n ].some(x => t.includes(norm(x)));\n}\n\nconst intencionEspecial = detectarIntencionEspecial(pregunta);\n\nif (intencionEspecial) {\n return [\n {\n json: {\n source_type: 'propuestas',\n pregunta,\n intencion_especial: intencionEspecial.tipo,\n tokens: [],\n cantidad_solicitada: null,\n quiere_ultimas: false,\n es_seguimiento: false,\n estado_solicitado: '',\n paises_solicitados: [],\n anios_solicitados: [],\n canales_solicitados: [],\n total_propuestas: rows.length,\n total_candidatos: 0,\n sin_resultados: false,\n candidatos: [],\n contexto_propuestas: '',\n prompt_usuario: intencionEspecial.prompt\n }\n }\n ];\n}\n\nconst cantidadSolicitada = extraerCantidad(pregunta);\nconst quiereUltimas = detectarUltimas(pregunta);\nconst esSeguimiento = detectarSeguimiento(pregunta);\nconst estadoSolicitado = detectarEstado(pregunta);\nconst aniosSolicitados = detectarAnios(pregunta);\nconst paisesSolicitados = detectarPaises(pregunta);\nconst canalesSolicitados = detectarCanales(pregunta);\nconst mencionaPropuestas = detectarInteresPropuestas(pregunta);\n\nconst stopwords = new Set([\n 'DAME', 'DARME', 'MUESTRA', 'MUESTRAME', 'BUSCA', 'BUSCAME',\n 'ENVIAME', 'ENVÍAME', 'QUIERO', 'NECESITO',\n 'PROPUESTA', 'PROPUESTAS', 'IDEA', 'IDEAS', 'DOCUMENTO', 'DOCUMENTOS',\n 'ULTIMA', 'ULTIMAS', 'ULTIMO', 'ULTIMOS', 'RECIENTE', 'RECIENTES',\n 'NUEVA', 'NUEVAS', 'NUEVO', 'NUEVOS',\n 'APROBADA', 'APROBADAS', 'PENDIENTE', 'PENDIENTES',\n 'PRESENTACION', 'PRESENTACIONES', 'PRESENTACIÓN',\n 'EJECUTADA', 'EJECUTADAS', 'EJECUCION', 'EJECUCIONES',\n 'EVIDENCIA', 'EVIDENCIAS',\n 'LINK', 'ENLACE',\n 'DE', 'DEL', 'LA', 'LAS', 'EL', 'LOS', 'PARA', 'POR', 'CON',\n 'EN', 'UN', 'UNA', 'Y', 'O', 'QUE', 'ME', 'TENGO', 'TIENES',\n 'ALGUNA', 'ALGUNAS', 'ALGUN', 'SOBRE', 'RELACIONADA', 'RELACIONADAS',\n 'ESAS', 'ESTAS', 'ESOS', 'ESTOS',\n]);\n\nconst tokens = preguntaNorm\n .split(/[^A-Z0-9Ñ]+/)\n .map(t => t.trim())\n .filter(t => t.length >= 3 && !stopwords.has(t))\n .filter(t => !aniosSolicitados.includes(t));\n\nfunction getYear(value) {\n const year = clean(value).match(/\\b(20\\d{2}|19\\d{2})\\b/);\n return year ? Number(year[1]) : 0;\n}\n\nfunction fieldContains(value, expected) {\n return norm(value).includes(norm(expected));\n}\n\nfunction matchesAny(value, expectedList) {\n if (!expectedList.length) return true;\n return expectedList.some(expected => fieldContains(value, expected));\n}\n\nfunction hardFilterPass(p) {\n if (paisesSolicitados.length && !matchesAny(p.PAIS, paisesSolicitados)) {\n return false;\n }\n\n if (aniosSolicitados.length && !aniosSolicitados.includes(String(p.ANIO))) {\n return false;\n }\n\n if (estadoSolicitado && norm(p.APROBADA) !== norm(estadoSolicitado)) {\n return false;\n }\n\n if (canalesSolicitados.length && !matchesAny(p.CANAL, canalesSolicitados)) {\n return false;\n }\n\n return true;\n}\n\nfunction scoreRow(row) {\n const enlacesEjecutadas = getEnlacesEjecutadas(row);\n\n const camposGenerales = [\n row.NOMBRE,\n row.CLIENTE,\n row.MARCA,\n row.PAIS,\n row['TIPO DE ACCION'],\n row.CANAL,\n row['AMBIENTE DE COMPRA (RE)'],\n row['TÁCTICA PROMOCIONAL'],\n row.APROBADA,\n row.ETIQUETAS,\n row['AÑO'],\n row.Descripcion,\n row.nombre_archivo,\n row['Enlace a la propuesta'],\n enlacesEjecutadas,\n ];\n\n const camposFuertes = [\n row.NOMBRE,\n row.CLIENTE,\n row.MARCA,\n row.PAIS,\n row['TIPO DE ACCION'],\n row.ETIQUETAS,\n row['AÑO'],\n ];\n\n const textoGeneral = norm(camposGenerales.join(' '));\n const textoFuerte = norm(camposFuertes.join(' '));\n\n let score = 0;\n\n for (const token of tokens) {\n if (textoGeneral.includes(token)) score += 1;\n if (textoFuerte.includes(token)) score += 2;\n }\n\n if (paisesSolicitados.length && matchesAny(row.PAIS, paisesSolicitados)) {\n score += 8;\n }\n\n if (aniosSolicitados.length && aniosSolicitados.includes(clean(row['AÑO']))) {\n score += 8;\n }\n\n if (estadoSolicitado && norm(row.APROBADA) === norm(estadoSolicitado)) {\n score += 8;\n }\n\n if (canalesSolicitados.length && matchesAny(row.CANAL, canalesSolicitados)) {\n score += 5;\n }\n\n if (enlacesEjecutadas) {\n score += 3;\n }\n\n return score;\n}\n\nconst propuestas = rows\n .map((item, index) => {\n const row = item.json || {};\n\n const fila =\n row.row_number ||\n row.__row_number ||\n row.__rowNumber ||\n row.rowNumber ||\n index + 2;\n\n const anio = clean(row['AÑO']);\n const enlacesEjecutadas = getEnlacesEjecutadas(row);\n\n return {\n fila,\n score: scoreRow(row),\n\n NOMBRE: clean(row.NOMBRE),\n CLIENTE: clean(row.CLIENTE),\n MARCA: clean(row.MARCA),\n PAIS: clean(row.PAIS),\n TIPO_DE_ACCION: clean(row['TIPO DE ACCION']),\n CANAL: clean(row.CANAL),\n AMBIENTE_DE_COMPRA: clean(row['AMBIENTE DE COMPRA (RE)']),\n TACTICA_PROMOCIONAL: clean(row['TÁCTICA PROMOCIONAL']),\n APROBADA: clean(row.APROBADA),\n ETIQUETAS: clean(row.ETIQUETAS),\n ANIO: anio,\n ANIO_NUM: getYear(anio),\n ENLACE: clean(row['Enlace a la propuesta']),\n ENLACES_EJECUTADAS: enlacesEjecutadas,\n DESCRIPCION: clean(row.Descripcion),\n };\n })\n .filter(p => p.NOMBRE || p.DESCRIPCION || p.ENLACE || p.ENLACES_EJECUTADAS);\n\nlet candidatos = propuestas\n .filter(p => hardFilterPass(p))\n .filter(p => {\n if (tokens.length > 0) return p.score > 0;\n\n if (\n paisesSolicitados.length ||\n aniosSolicitados.length ||\n estadoSolicitado ||\n canalesSolicitados.length\n ) {\n return true;\n }\n\n if (mencionaPropuestas) {\n return true;\n }\n\n return false;\n });\n\nif (candidatos.length === 0 && tokens.length === 0 && !esSeguimiento) {\n candidatos = [];\n}\n\nif (quiereUltimas) {\n candidatos = candidatos.sort((a, b) => b.fila - a.fila);\n} else {\n candidatos = candidatos.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n return b.fila - a.fila;\n });\n}\n\nconst limiteCandidatos = cantidadSolicitada\n ? Math.min(Math.max(cantidadSolicitada, 1), 25)\n : MAX_CANDIDATOS_DEFAULT;\n\ncandidatos = candidatos.slice(0, limiteCandidatos);\n\nconst contexto = candidatos.map((p, index) => {\n return [\n `RESULTADO ${index + 1}`,\n `Fila: ${p.fila}`,\n `Nombre: ${p.NOMBRE}`,\n `Cliente: ${p.CLIENTE}`,\n `Marca: ${p.MARCA}`,\n `País: ${p.PAIS}`,\n `Tipo de acción: ${p.TIPO_DE_ACCION}`,\n `Canal: ${p.CANAL}`,\n `Ambiente de compra: ${p.AMBIENTE_DE_COMPRA}`,\n `Táctica promocional: ${p.TACTICA_PROMOCIONAL}`,\n `Estado aprobación: ${p.APROBADA}`,\n `Etiquetas: ${p.ETIQUETAS}`,\n `Año: ${p.ANIO}`,\n `Descripción: ${p.DESCRIPCION}`,\n `Enlace: ${p.ENLACE}`,\n `Enlaces propuestas ejecutadas: ${p.ENLACES_EJECUTADAS}`,\n ].join('\\n');\n}).join('\\n\\n---\\n\\n');\n\nconst sinResultados = candidatos.length === 0;\n\nconst promptUsuario = `\nPregunta del usuario:\n${pregunta}\n\nInstrucción de búsqueda detectada:\n- Cantidad solicitada: ${cantidadSolicitada || 'No especificada'}\n- Quiere últimas/recientes: ${quiereUltimas ? 'Sí' : 'No'}\n- Es pregunta de seguimiento: ${esSeguimiento ? 'Sí' : 'No'}\n- Estado solicitado: ${estadoSolicitado || 'No especificado'}\n- Países solicitados: ${paisesSolicitados.length ? paisesSolicitados.join(', ') : 'No especificado'}\n- Años solicitados: ${aniosSolicitados.length ? aniosSolicitados.join(', ') : 'No especificado'}\n- Canales solicitados: ${canalesSolicitados.length ? canalesSolicitados.join(', ') : 'No especificado'}\n\nResultado de búsqueda:\n${sinResultados ? 'No se encontraron propuestas candidatas con los filtros detectados.' : 'Se encontraron propuestas candidatas.'}\n\nContexto encontrado en el banco de propuestas:\n${contexto || 'No se encontraron propuestas candidatas con la búsqueda.'}\n\nInstrucción para responder:\n${sinResultados\n ? 'Indica que no encontraste coincidencias. No inventes propuestas. Sugiere probar con otro cliente, marca, país, año, canal o tipo de acción.'\n : 'Responde usando únicamente las propuestas del contexto.'}\n`;\n\nreturn [\n {\n json: {\n source_type: 'propuestas',\n pregunta,\n tokens,\n cantidad_solicitada: cantidadSolicitada,\n quiere_ultimas: quiereUltimas,\n es_seguimiento: esSeguimiento,\n estado_solicitado: estadoSolicitado,\n paises_solicitados: paisesSolicitados,\n anios_solicitados: aniosSolicitados,\n canales_solicitados: canalesSolicitados,\n total_propuestas: propuestas.length,\n total_candidatos: candidatos.length,\n sin_resultados: sinResultados,\n intencion_especial: '',\n candidatos,\n contexto_propuestas: contexto,\n prompt_usuario: promptUsuario,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 304, + 864 + ], + "id": "cd454ec5-035b-43ce-bb92-9a9e135ffbd3", + "name": "Code - Filtrar propuestas relevantes" + }, + { + "parameters": { + "promptType": "define", + "text": "={{ $json.prompt_usuario }}", + "options": { + "systemMessage": "=Eres Fulgencio Fumado, asistente interno de creatividad, briefs y propuestas de GomezLee Marketing.\n\nTu función es responder preguntas usando únicamente el contexto entregado por el sistema.\n\nActualmente puedes recibir contexto de tres fuentes:\n\n1. Banco de propuestas:\nContiene propuestas originales, presentaciones base, activaciones, casos, campañas y referencias internas de GomezLee Marketing.\n\n2. CDC Briefs:\nContiene briefs, solicitudes creativas y requerimientos generados desde la app CDC Brief.\n\n3. Propuestas Ejecutadas / Externas:\nContiene reportes reales de ejecución, evidencias fotográficas, videos opcionales, presentaciones de ejecución generadas, propuestas externas recibidas y relaciones con propuestas del banco original cuando exista match.\n\nNo confundas ambas fuentes:\n- Una propuesta es un caso, presentación, activación o referencia interna del banco de propuestas.\n- Un brief es una solicitud o requerimiento creativo.\n- Si el usuario pregunta por propuestas, casos, activaciones, presentaciones o campañas, prioriza el banco de propuestas.\n- Si el usuario pregunta por briefs, solicitudes, requerimientos creativos, pedidos o temas registrados en CDC Brief, prioriza CDC Briefs.\n- Si hay información útil en ambas fuentes, separa la respuesta por secciones:\n Banco de propuestas\n CDC Briefs\n\nReglas obligatorias:\n1. No inventes propuestas, briefs, enlaces, países, clientes, marcas, años, estados, fechas ni solicitantes.\n2. Si no hay información suficiente en el contexto, dilo claramente.\n3. No digas que puedes crear imágenes. La creación de imágenes no está disponible actualmente en Fulgencio.\n4. No menciones n8n, workflows, Google Sheets, nodos, prompts, memoria técnica ni procesos internos.\n5. Responde en español, con tono profesional, cercano, directo y útil. Evita sonar demasiado formal o robótico.\n6. Cuando incluyas enlaces, ponlos completos.\n7. Si algún dato aparece como PENDIENTE, vacío o no disponible, no lo inventes; indica que ese dato está pendiente o no disponible.\n8. Usa únicamente la información entregada en el contexto o en la memoria reciente cuando sea una pregunta de seguimiento.\n9. No prometas enviar archivos, crear documentos, generar imágenes, modificar propuestas, actualizar información o hacer cambios en sistemas. Solo puedes consultar, resumir, comparar y sugerir usando el contexto disponible.\n10. Si el usuario combina varios filtros y hay pocos o ningún resultado, explica qué filtros se aplicaron y sugiere quitar o cambiar algún filtro.\n\nReglas para propuestas:\n- Si el usuario pide propuestas, entrega una lista clara con:\n - Nombre\n - Cliente/Marca\n - País\n - Año si está disponible\n - Tipo de acción si está disponible\n - Breve descripción\n - Enlace\n- Si una propuesta tiene enlace disponible, inclúyelo siempre.\n- Si no tiene enlace, indica “Enlace no disponible”.\n- Si el usuario pide una cantidad específica, respeta esa cantidad cuando el contexto la tenga disponible.\n- Si el usuario pide las últimas o más recientes, respeta el orden entregado en el contexto.\n- Cuando la instrucción de búsqueda detectada indique país, año, canal, estado o cantidad solicitada, respeta esos filtros estrictamente.\n- Si no hay candidatos de propuestas en el contexto, no inventes resultados. Indica que no encontraste coincidencias y sugiere ajustar la búsqueda por cliente, marca, país, año, canal, estado o tipo de acción.\n\nReglas para CDC Briefs:\n- Si el usuario pide briefs, solicitudes, requerimientos o pedidos creativos existentes, responde usando la sección CDC Briefs del contexto.\n- Cuando respondas sobre briefs, incluye cuando esté disponible:\n - Brief ID\n - Fecha de creación\n - Solicitante\n - Correo\n - Estado\n - Fecha de entrega\n - Resumen\n - Link del documento brief\n - Link de carpeta Drive\n- No trates un brief como si fuera una propuesta.\n- No trates una propuesta como si fuera un brief.\n- Si el usuario pide los últimos briefs o briefs recientes, respeta el orden entregado en el contexto.\n- Si no hay briefs candidatos en el contexto, indica que no encontraste briefs relacionados y sugiere buscar por brief ID, solicitante, tema, estado o fecha.\n- Si el usuario pide crear un brief, llenar un brief, hacer una nueva solicitud o abrir el formulario de brief, comparte este enlace:\nhttps://digitalcompass.agency/brief\n- Si el usuario solo pregunta por briefs existentes, no respondas únicamente con el enlace del formulario. Busca y responde con los briefs del contexto.\n\nReglas para Propuestas Ejecutadas / Externas:\n- Si el usuario pregunta por propuesta ejecutada, ejecución, evidencias, presentación ejecutada, presentación de ejecución, fotos, videos, implementación o propuesta externa, prioriza la fuente Propuestas Ejecutadas / Externas.\n- Si el usuario pide “Envíame la presentación de la propuesta ejecutada de X”, entrega el link de Presentación ejecución cuando esté disponible.\n- Si existe Propuesta original link, inclúyelo como referencia adicional.\n- Si existe Carpeta evidencias, inclúyela cuando el usuario pida evidencias, fotos, videos o soporte de la ejecución.\n- Si la ejecución tiene Resumen IA o Descripción ejecución, úsalo para dar un resumen claro.\n- Si hay varias ejecuciones relacionadas, muéstralas en lista y no elijas una sola sin aclararlo.\n- Si el registro es PROPUESTA_EXTERNA, aclara que es una propuesta externa registrada, no una propuesta original del banco.\n- Si el match es bajo, manual, pendiente o no determinado, no afirmes que corresponde definitivamente a una propuesta original.\n- No inventes presentaciones ejecutadas ni enlaces de evidencia.\n\nFormato recomendado:\n- Resumen breve al inicio.\n- Lista numerada cuando aplique.\n- Si usas ambas fuentes, separa en secciones:\n Banco de propuestas\n CDC Briefs\n- Cierre breve ofreciendo filtrar por país, marca, cliente, año, estado, tipo de acción, brief ID, solicitante o tema.\n\nFormato especial:\n- Si el usuario pide “tabla”, “formato tabla” o “en una tabla”, responde directamente en tabla Markdown.\n- Para propuestas en tabla, usa columnas útiles como:\n Nombre, Cliente/Marca, País, Año, Tipo de acción y Enlace.\n- Para briefs en tabla, usa columnas útiles como:\n Brief ID, Fecha, Solicitante, Estado, Fecha de entrega, Resumen y Enlace.\n- Si el usuario pide bullets, responde en bullets.\n\nAnálisis, patrones e ideas:\n- Si el usuario pide análisis, patrones, insights o ideas creativas, analiza únicamente la información entregada en el contexto.\n- Resume patrones repetidos, tipos de activación, mecánicas, formatos, canales, oportunidades y temas relevantes sin inventar datos fuera del contexto.\n- Si el usuario pide ideas nuevas, usa el contexto como referencia.\n- Aclara que son ideas inspiradas en propuestas o briefs existentes y no propuestas ya aprobadas, salvo que el contexto indique lo contrario.\n\nComparaciones:\n- Si el usuario pide comparar propuestas, organiza la respuesta por diferencias y similitudes:\n - Tipo de acción\n - País\n - Marca\n - Canal\n - Objetivo\n - Elementos creativos\n- Si el usuario pide comparar briefs, organiza la respuesta por:\n - Solicitante\n - Estado\n - Fecha de entrega\n - Objetivo o tema\n - Materiales o insumos disponibles\n - Diferencias principales\n- Usa solo el contexto disponible.\n\nSeguimiento y memoria:\n- Si el usuario dice “de esas”, “la segunda”, “la anterior”, “dame los enlaces”, “resume esas”, “de esos briefs” o “las que me mostraste”, usa la memoria reciente para entender la referencia.\n- Responde solo con información que aparezca en el contexto o memoria entregada.\n- No mezcles resultados nuevos con resultados de memoria si el usuario claramente se refiere a resultados ya mostrados.\n- Si no puedes identificar a qué se refiere, pide una aclaración breve.\n\nMensajes ambiguos:\n\n* Si el usuario envía un saludo, cortesía, confirmación o mensaje muy corto sin una intención clara de búsqueda, no muestres propuestas ni briefs específicos aunque exista contexto disponible.\n* Esto aplica a mensajes como “hola”, “hi”, “hello”, “buenas”, “hey”, “ok”, “gracias”, “thanks”, “listo”, “perfecto”, “dale”, “saludos” o mensajes similares.\n* En esos casos, responde con una bienvenida breve, explica cómo puede usar Fulgencio y da ejemplos de preguntas útiles.\n* No digas que revisaste el banco de propuestas ni CDC Briefs si el usuario solo saludó o envió una cortesía.\n* No incluyas propuestas, briefs, tablas, enlaces de archivos ni resultados específicos en respuestas a saludos o mensajes genéricos.\n\nEjemplos de preguntas útiles:\n\n1. Dame las 5 últimas propuestas de Motorola.\n2. Busca propuestas de Nestlé en República Dominicana.\n3. Dame propuestas aprobadas de activación en PDV.\n4. Tienes propuestas de Kit Kat en Costa Rica?\n5. Dame los últimos briefs creados.\n6. Busca briefs relacionados con Motorola.\n7. Necesito crear un brief.\n8. Necesito acceder al Tablero CDC.\n9. ¿Dónde registro un proyecto creativo?\n\n\nImágenes:\n- Si el usuario solicita crear o generar imágenes, indica que la creación de imágenes no está disponible actualmente en Fulgencio.\n- Ofrece buscar referencias, propuestas, briefs o ideas existentes dentro del contexto disponible.\n\nBrief nuevo:\n- Si el usuario pide crear un brief, llenar un brief, hacer una solicitud creativa o abrir el formulario, comparte este enlace:\nhttps://digitalcompass.agency/brief\n\nTablero CDC / Aprobaciones Proyectos:\n- Si el usuario pide acceder al Tablero CDC, abrir Aprobaciones Proyectos, entrar al tablero de proyectos o registrar un proyecto creativo, comparte este enlace:\nhttps://digitalcompass.agency/tablero-cdc/\n- Indica brevemente que corresponde al Tablero CDC / Aprobaciones Proyectos.\n- No afirmes que puedes crear, modificar, aprobar, eliminar o consultar proyectos dentro del Tablero CDC desde el chat.\n- No confundas el Tablero CDC con CDC Brief ni con el Banco de Propuestas.\n\nRestricción final:\nSi el contexto indica que no se encontraron candidatos, no inventes resultados. Responde que no encontraste coincidencias y sugiere ajustar la búsqueda según la fuente:\n- Para propuestas: cliente, marca, país, año, canal, estado o tipo de acción.\n- Para briefs: brief ID, solicitante, estado, fecha, tema o palabras clave del brief." + } + }, + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 2, + "position": [ + 1104, + 848 + ], + "id": "e8fa7895-75d2-4f0b-b0bb-7214ff424c9e", + "name": "AI Agent - Gemini Fulgencio" + }, + { + "parameters": { + "modelName": "models/gemini-2.5-pro", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", + "typeVersion": 1, + "position": [ + 1104, + 1072 + ], + "id": "af32c118-9f2f-473b-89a6-cd4a3b653be6", + "name": "Google Gemini Chat Model", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const data = $json || {};\n\nlet respuesta =\n data.output ||\n data.text ||\n data.response ||\n data.content ||\n data.message ||\n '';\n\nrespuesta = String(respuesta || '').trim();\n\nif (!respuesta) {\n respuesta = 'No pude generar una respuesta con la información disponible.';\n}\n\nfunction limpiarMarkdownParaGoogleChat(texto) {\n return String(texto || '')\n // Links Markdown: [texto](url) -> texto: url\n .replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, '$1: $2')\n\n // Negritas Markdown: **texto** -> texto\n .replace(/\\*\\*([^*]+)\\*\\*/g, '$1')\n\n // Negritas con __texto__ -> texto\n .replace(/__([^_]+)__/g, '$1')\n\n // Cursivas simples: *texto* -> texto\n .replace(/\\*([^*\\n]+)\\*/g, '$1')\n\n // Títulos Markdown: ### Título -> Título\n .replace(/^#{1,6}\\s+/gm, '')\n\n // Código inline: `texto` -> texto\n .replace(/`([^`]+)`/g, '$1')\n\n // Separadores Markdown\n .replace(/^\\s*---+\\s*$/gm, '')\n\n // Convertir bullets markdown a bullets limpios\n .replace(/^\\s*[-*]\\s+/gm, '• ')\n\n // Limpiar espacios antes de saltos\n .replace(/[ \\t]+\\n/g, '\\n')\n\n // Evitar demasiadas líneas vacías seguidas\n .replace(/\\n{4,}/g, '\\n\\n\\n')\n\n .trim();\n}\n\nrespuesta = limpiarMarkdownParaGoogleChat(respuesta);\n\nconst MAX_CHARS = 7000;\n\nif (respuesta.length > MAX_CHARS) {\n respuesta =\n respuesta.slice(0, MAX_CHARS) +\n '\\n\\nRespuesta recortada por longitud. Puedes pedirme un filtro más específico por cliente, marca, país, año, estado o tipo de acción.';\n}\n\nreturn {\n json: {\n respuesta\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1456, + 848 + ], + "id": "18c1db5a-5290-4901-8a56-100bf7ef9da8", + "name": "Code - Formatear respuesta Chat" + }, + { + "parameters": { + "spaceId": "={{ $('Code - Preparar mensaje').first().json.space_name }}", + "messageUi": { + "text": "={{ $json.respuesta }}" + }, + "additionalFields": {} + }, + "type": "n8n-nodes-base.googleChat", + "typeVersion": 1, + "position": [ + 1664, + 848 + ], + "id": "8f68c6cb-507b-4ecb-b767-5071ee81a6c9", + "name": "Chat - Enviar respuesta", + "webhookId": "0147a967-96b1-4410-8fc8-b15ccbce7995", + "credentials": { + "googleApi": { + "id": "u5FLl9Ysd4oNofFB", + "name": "Google Chat - Fulgencio Service Account" + } + } + }, + { + "parameters": { + "sessionIdType": "customKey", + "sessionKey": "={{ $('Code - Preparar mensaje').first().json.thread_name || ($('Code - Preparar mensaje').first().json.space_name + '::' + $('Code - Preparar mensaje').first().json.usuario_email) }}", + "contextWindowLength": 6 + }, + "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", + "typeVersion": 1.3, + "position": [ + 1248, + 1072 + ], + "id": "e6091e31-bcee-4afe-aad4-37fc8d3d0b18", + "name": "Simple Memory" + }, + { + "parameters": { + "content": "## 💬 Entrada Google Chat\n\nRecibe el mensaje del usuario desde Google Chat, responde rápido con “procesando” y prepara los datos de la conversación: pregunta, usuario, espacio, hilo y session key.", + "height": 448, + "width": 608, + "color": 2 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -768, + 688 + ], + "id": "72856778-43d5-4cf3-b1d1-3423f8dde3ba", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## 🔎 Búsqueda y contexto de Fulgencio\n\nLee el banco de propuestas y el Sheet de CDC Briefs.\n\nFiltra candidatos relevantes antes de enviarlos a Gemini:\n- propuestas, casos, activaciones y campañas\n- briefs, solicitudes creativas y requerimientos\n\nLuego une ambas fuentes en un solo contexto, manteniéndolas separadas para no mezclar propuestas con briefs.", + "height": 944, + "width": 960, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -32, + 624 + ], + "id": "0490b0b2-83de-4962-b8eb-c832c97285f8", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "## 🤖 Respuesta con Gemini\n\nGemini responde usando solo el contexto filtrado y combinado.\n\nPuede responder sobre propuestas y CDC Briefs, manteniendo cada fuente separada cuando aplique.\n\nNo crea imágenes, no inventa datos y conserva enlaces reales cuando estén disponibles.", + "height": 544, + "width": 864, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 1040, + 704 + ], + "id": "be8a8afc-da65-468f-a1fc-9002974d07f9", + "name": "Sticky Note2" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1h_d4l4FYkLvyy_Zp7bEQFKpy5hx6pVj9xZ98mjMHHSU", + "mode": "list", + "cachedResultName": "CDC Brief - Base para Fulgencio", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1h_d4l4FYkLvyy_Zp7bEQFKpy5hx6pVj9xZ98mjMHHSU/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "Briefs", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1h_d4l4FYkLvyy_Zp7bEQFKpy5hx6pVj9xZ98mjMHHSU/edit#gid=0" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 48, + 1056 + ], + "id": "04aac58a-ef25-4df0-ac38-46d279db1ff1", + "name": "Sheets - Leer briefs CDC", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const rows = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst MAX_BRIEFS_DEFAULT = 10;\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nconst preguntaNorm = norm(pregunta);\n\nfunction extraerCantidad(texto) {\n const t = norm(texto);\n\n const numero = t.match(/\\b(\\d{1,2})\\b/);\n if (numero) return Number(numero[1]);\n\n const palabras = {\n UNA: 1,\n UNO: 1,\n DOS: 2,\n TRES: 3,\n CUATRO: 4,\n CINCO: 5,\n SEIS: 6,\n SIETE: 7,\n OCHO: 8,\n NUEVE: 9,\n DIEZ: 10,\n };\n\n for (const [palabra, valor] of Object.entries(palabras)) {\n if (t.includes(palabra)) return valor;\n }\n\n return null;\n}\n\nfunction detectarUltimas(texto) {\n const t = norm(texto);\n\n return [\n 'ULTIMA',\n 'ULTIMAS',\n 'ULTIMO',\n 'ULTIMOS',\n 'RECIENTE',\n 'RECIENTES',\n 'NUEVA',\n 'NUEVAS',\n 'NUEVO',\n 'NUEVOS',\n ].some(x => t.includes(x));\n}\n\nfunction detectarInteresBriefs(texto) {\n const t = norm(texto);\n\n return [\n 'BRIEF',\n 'BRIEFS',\n 'SOLICITUD',\n 'SOLICITUDES',\n 'REQUERIMIENTO',\n 'REQUERIMIENTOS',\n 'PEDIDO CREATIVO',\n 'CDC',\n 'FORMULARIO',\n ].some(x => t.includes(x));\n}\n\nfunction detectarInteresPropuestas(texto) {\n const t = norm(texto);\n\n return [\n 'PROPUESTA',\n 'PROPUESTAS',\n 'BANCO DE PROPUESTAS',\n 'CASO',\n 'CASOS',\n 'ACTIVACION',\n 'ACTIVACIONES',\n ].some(x => t.includes(x));\n}\n\nconst cantidadSolicitada = extraerCantidad(pregunta);\nconst quiereUltimas = detectarUltimas(pregunta);\nconst mencionaBriefs = detectarInteresBriefs(pregunta);\nconst mencionaPropuestas = detectarInteresPropuestas(pregunta);\n\nconst stopwords = new Set([\n 'DAME', 'DARME', 'MUESTRA', 'MUESTRAME', 'BUSCA', 'BUSCAME',\n 'BRIEF', 'BRIEFS', 'SOLICITUD', 'SOLICITUDES', 'REQUERIMIENTO', 'REQUERIMIENTOS',\n 'PROPUESTA', 'PROPUESTAS', 'DOCUMENTO', 'DOCUMENTOS',\n 'ULTIMA', 'ULTIMAS', 'ULTIMO', 'ULTIMOS', 'RECIENTE', 'RECIENTES',\n 'NUEVA', 'NUEVAS', 'NUEVO', 'NUEVOS',\n 'DE', 'DEL', 'LA', 'LAS', 'EL', 'LOS', 'PARA', 'POR', 'CON',\n 'EN', 'UN', 'UNA', 'Y', 'O', 'QUE', 'ME', 'TENGO', 'TIENES',\n 'ALGUNA', 'ALGUNAS', 'ALGUN', 'SOBRE', 'RELACIONADA', 'RELACIONADAS',\n 'CDC', 'FORMULARIO',\n]);\n\nconst tokens = preguntaNorm\n .split(/[^A-Z0-9Ñ]+/)\n .map(t => t.trim())\n .filter(t => t.length >= 3 && !stopwords.has(t));\n\nfunction getDateValue(value) {\n const raw = clean(value);\n const d = new Date(raw);\n return Number.isNaN(d.getTime()) ? 0 : d.getTime();\n}\n\nfunction scoreBrief(row) {\n const camposFuertes = [\n row.brief_id,\n row.nombre_carpeta,\n row.enviado_por,\n row.correo,\n row.estado,\n ];\n\n const camposGenerales = [\n row.brief_id,\n row.nombre_carpeta,\n row.enviado_por,\n row.correo,\n row.estado,\n row.resumen_chatbot,\n row.texto_brief,\n row.texto_busqueda,\n row.fecha_entrega,\n ];\n\n const textoFuerte = norm(camposFuertes.join(' '));\n const textoGeneral = norm(camposGenerales.join(' '));\n\n let score = 0;\n\n for (const token of tokens) {\n if (textoGeneral.includes(token)) score += 1;\n if (textoFuerte.includes(token)) score += 2;\n }\n\n if (mencionaBriefs) score += 3;\n\n return score;\n}\n\nconst debeBuscarBriefs = mencionaBriefs || (!mencionaPropuestas && tokens.length > 0);\n\nlet briefs = rows\n .map((item, index) => {\n const row = item.json || {};\n\n return {\n fila: row.row_number || row.__row_number || row.__rowNumber || row.rowNumber || index + 2,\n score: scoreBrief(row),\n\n fecha_creacion: clean(row.fecha_creacion),\n brief_id: clean(row.brief_id),\n nombre_carpeta: clean(row.nombre_carpeta),\n enviado_por: clean(row.enviado_por),\n correo: clean(row.correo),\n link_carpeta_drive: clean(row.link_carpeta_drive),\n link_documento_brief: clean(row.link_documento_brief),\n estado: clean(row.estado),\n cantidad_audios: clean(row.cantidad_audios),\n cantidad_imagenes: clean(row.cantidad_imagenes),\n cantidad_documentos: clean(row.cantidad_documentos),\n cantidad_links: clean(row.cantidad_links),\n resumen_chatbot: clean(row.resumen_chatbot),\n texto_brief: clean(row.texto_brief),\n texto_busqueda: clean(row.texto_busqueda),\n fecha_entrega: clean(row.fecha_entrega),\n\n fecha_sort: getDateValue(row.fecha_creacion),\n };\n })\n .filter(b => b.brief_id || b.nombre_carpeta || b.texto_brief || b.resumen_chatbot);\n\nlet candidatos = [];\n\nif (debeBuscarBriefs) {\n candidatos = briefs.filter(b => {\n if (tokens.length > 0) return b.score > 0;\n if (mencionaBriefs) return true;\n return false;\n });\n}\n\nif (quiereUltimas) {\n candidatos = candidatos.sort((a, b) => {\n if (b.fecha_sort !== a.fecha_sort) return b.fecha_sort - a.fecha_sort;\n return b.fila - a.fila;\n });\n} else {\n candidatos = candidatos.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n if (b.fecha_sort !== a.fecha_sort) return b.fecha_sort - a.fecha_sort;\n return b.fila - a.fila;\n });\n}\n\nconst limite = cantidadSolicitada\n ? Math.min(Math.max(cantidadSolicitada, 1), 25)\n : MAX_BRIEFS_DEFAULT;\n\ncandidatos = candidatos.slice(0, limite);\n\nconst contextoBriefs = candidatos.map((b, index) => {\n return [\n `BRIEF ${index + 1}`,\n `Fila: ${b.fila}`,\n `Fecha creación: ${b.fecha_creacion}`,\n `Brief ID: ${b.brief_id}`,\n `Nombre carpeta: ${b.nombre_carpeta}`,\n `Enviado por: ${b.enviado_por}`,\n `Correo: ${b.correo}`,\n `Estado: ${b.estado}`,\n `Fecha entrega: ${b.fecha_entrega}`,\n `Cantidad audios: ${b.cantidad_audios}`,\n `Cantidad imágenes: ${b.cantidad_imagenes}`,\n `Cantidad documentos: ${b.cantidad_documentos}`,\n `Cantidad links: ${b.cantidad_links}`,\n `Resumen chatbot: ${b.resumen_chatbot}`,\n `Texto brief: ${b.texto_brief}`,\n `Texto búsqueda: ${b.texto_busqueda}`,\n `Link carpeta Drive: ${b.link_carpeta_drive}`,\n `Link documento brief: ${b.link_documento_brief}`,\n ].join('\\n');\n}).join('\\n\\n---\\n\\n');\n\nreturn [\n {\n json: {\n source_type: 'briefs_cdc',\n pregunta,\n tokens,\n menciona_briefs: mencionaBriefs,\n menciona_propuestas: mencionaPropuestas,\n cantidad_solicitada: cantidadSolicitada,\n quiere_ultimas: quiereUltimas,\n total_briefs: briefs.length,\n total_briefs_candidatos: candidatos.length,\n briefs_candidatos: candidatos,\n contexto_briefs: contextoBriefs,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 256, + 1056 + ], + "id": "d44ec6a2-e62a-4a0d-a976-2df5408d2621", + "name": "Code - Filtrar briefs CDC" + }, + { + "parameters": { + "jsCode": "const items = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst propuestasItem =\n items.find(i => i.json.source_type === 'propuestas')?.json || {};\n\nconst briefsItem =\n items.find(i => i.json.source_type === 'briefs_cdc')?.json || {};\n\nconst ejecucionesItem =\n items.find(i => i.json.source_type === 'propuestas_ejecutadas')?.json || {};\n\nif (propuestasItem.intencion_especial) {\n return [\n {\n json: {\n ...propuestasItem,\n prompt_usuario: propuestasItem.prompt_usuario,\n total_fuentes: 1,\n fuentes_usadas: ['intencion_especial'],\n },\n },\n ];\n}\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction construirContextoPropuestas(candidatos = []) {\n if (!Array.isArray(candidatos) || candidatos.length === 0) return '';\n\n return candidatos.map((p, index) => {\n return [\n `PROPUESTA ${index + 1}`,\n `Fila: ${clean(p.fila)}`,\n `Nombre: ${clean(p.NOMBRE)}`,\n `Cliente: ${clean(p.CLIENTE)}`,\n `Marca: ${clean(p.MARCA)}`,\n `País: ${clean(p.PAIS)}`,\n `Tipo de acción: ${clean(p.TIPO_DE_ACCION)}`,\n `Canal: ${clean(p.CANAL)}`,\n `Ambiente de compra: ${clean(p.AMBIENTE_DE_COMPRA)}`,\n `Táctica promocional: ${clean(p.TACTICA_PROMOCIONAL)}`,\n `Estado aprobación: ${clean(p.APROBADA)}`,\n `Etiquetas: ${clean(p.ETIQUETAS)}`,\n `Año: ${clean(p.ANIO)}`,\n `Descripción: ${clean(p.DESCRIPCION)}`,\n `Enlace: ${clean(p.ENLACE)}`,\n `Enlaces propuestas ejecutadas: ${clean(p.ENLACES_EJECUTADAS)}`,\n ].join('\\n');\n }).join('\\n\\n---\\n\\n');\n}\n\nfunction construirContextoEjecuciones(candidatos = []) {\n if (!Array.isArray(candidatos) || candidatos.length === 0) return '';\n\n return candidatos.map((e, index) => {\n return [\n `EJECUCION ${index + 1}`,\n `Fila: ${clean(e.fila)}`,\n `Ejecución ID: ${clean(e.ejecucion_id)}`,\n `Tipo reporte: ${clean(e.tipo_reporte || e.tipo_normalizado)}`,\n `Fecha recepción: ${clean(e.fecha_recepcion)}`,\n `Fecha ejecución: ${clean(e.fecha_ejecucion)}`,\n `Manager: ${clean(e.manager_nombre)}`,\n `Propuesta referencia: ${clean(e.propuesta_referencia)}`,\n `Match estado: ${clean(e.propuesta_match_estado)}`,\n `Match confianza: ${clean(e.propuesta_match_confianza)}`,\n `Propuesta nombre banco: ${clean(e.propuesta_nombre_banco)}`,\n `Propuesta original link: ${clean(e.propuesta_link_banco)}`,\n `Marca: ${clean(e.marca)}`,\n `Cliente: ${clean(e.cliente)}`,\n `País: ${clean(e.pais)}`,\n `Ubicación: ${clean(e.ubicacion)}`,\n `Resumen IA: ${clean(e.resumen_ia)}`,\n `Descripción ejecución: ${clean(e.descripcion_ejecucion)}`,\n `Elementos detectados: ${clean(e.elementos_detectados)}`,\n `Tags: ${clean(e.tags)}`,\n `Fotos: ${clean(e.fotos_count)}`,\n `Videos: ${clean(e.videos_count)}`,\n `Audios: ${clean(e.audios_count)}`,\n `Carpeta evidencias: ${clean(e.media_folder_url)}`,\n `Presentación ejecución: ${clean(e.presentacion_ejecucion_url)}`,\n `Estado revisión: ${clean(e.estado_revision)}`,\n `Motivo revisión: ${clean(e.motivo_revision)}`,\n ].join('\\n');\n }).join('\\n\\n---\\n\\n');\n}\n\nconst contextoPropuestas =\n clean(propuestasItem.contexto_propuestas) ||\n construirContextoPropuestas(propuestasItem.candidatos || []);\n\nconst contextoBriefs = clean(briefsItem.contexto_briefs);\n\nconst contextoEjecuciones =\n clean(ejecucionesItem.contexto_ejecuciones) ||\n construirContextoEjecuciones(ejecucionesItem.ejecuciones_candidatas || []);\n\nconst totalPropuestas = Number(propuestasItem.total_candidatos || 0);\nconst totalBriefs = Number(briefsItem.total_briefs_candidatos || 0);\nconst totalEjecuciones = Number(ejecucionesItem.total_ejecuciones_candidatas || 0);\n\nconst sinResultados =\n totalPropuestas === 0 &&\n totalBriefs === 0 &&\n totalEjecuciones === 0;\n\nconst promptUsuario = `\nPregunta del usuario:\n${pregunta}\n\nFuentes disponibles para responder:\n\n1. Banco de propuestas:\n - Contiene propuestas originales, casos, presentaciones, activaciones y referencias internas de GomezLee Marketing.\n - Total de candidatos encontrados: ${totalPropuestas}\n\n2. CDC Briefs:\n - Contiene briefs y solicitudes creativas generadas desde la app CDC Brief.\n - Total de candidatos encontrados: ${totalBriefs}\n\n3. Propuestas Ejecutadas / Externas:\n - Contiene reportes reales de ejecución, evidencias, propuestas externas recibidas y presentaciones generadas desde WhatsApp.\n - Total de candidatos encontrados: ${totalEjecuciones}\n\nReglas para elegir fuente:\n- Si el usuario pregunta por propuestas originales, casos, activaciones o presentaciones base, prioriza el Banco de propuestas.\n- Si el usuario pregunta por briefs, solicitudes, requerimientos creativos, pedidos o formularios CDC, prioriza CDC Briefs.\n- Si el usuario pregunta por propuesta ejecutada, ejecución, evidencias, presentación de ejecución, fotos, videos, propuesta externa o link de ejecución, prioriza Propuestas Ejecutadas / Externas.\n- Si el usuario pide “la presentación de la propuesta ejecutada de X”, busca primero en Propuestas Ejecutadas / Externas.\n- Si una ejecución está relacionada con una propuesta original, puedes mostrar ambos enlaces: presentación ejecutada y propuesta original.\n- Si hay información útil en varias fuentes, separa la respuesta por secciones.\n- No mezcles un brief con una propuesta.\n- No mezcles una propuesta original con una propuesta ejecutada como si fueran lo mismo.\n- Usa únicamente la información entregada abajo.\n- No inventes enlaces ni datos.\n\nResultado de búsqueda:\n${sinResultados ? 'No se encontraron candidatos en ninguna fuente.' : 'Se encontraron candidatos en una o más fuentes.'}\n\nContexto del Banco de propuestas:\n${contextoPropuestas || 'No se encontraron propuestas candidatas.'}\n\nContexto de CDC Briefs:\n${contextoBriefs || 'No se encontraron briefs candidatos.'}\n\nContexto de Propuestas Ejecutadas / Externas:\n${contextoEjecuciones || 'No se encontraron propuestas ejecutadas o externas candidatas.'}\n\nInstrucción para responder:\n${sinResultados\n ? 'Indica que no encontraste coincidencias. Sugiere buscar por cliente, marca, país, año, tipo de acción, estado, brief ID, solicitante, tema del brief, nombre de propuesta ejecutada o propuesta externa.'\n : 'Responde usando únicamente el contexto anterior. Si usas varias fuentes, separa la respuesta por secciones.'}\n`;\n\nreturn [\n {\n json: {\n pregunta,\n\n source_type: 'contexto_fulgencio_combinado',\n\n total_propuestas_candidatas: totalPropuestas,\n total_briefs_candidatos: totalBriefs,\n total_ejecuciones_candidatas: totalEjecuciones,\n sin_resultados: sinResultados,\n\n propuestas: propuestasItem.candidatos || [],\n briefs: briefsItem.briefs_candidatos || [],\n ejecuciones: ejecucionesItem.ejecuciones_candidatas || [],\n\n contexto_propuestas: contextoPropuestas,\n contexto_briefs: contextoBriefs,\n contexto_ejecuciones: contextoEjecuciones,\n\n prompt_usuario: promptUsuario,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 768, + 896 + ], + "id": "12528b2b-1d6e-43f6-9573-5b7dccd8cab6", + "name": "Code - Construir contexto Fulgencio" + }, + { + "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": [ + 64, + 1280 + ], + "id": "96b7e960-b6b2-4a19-8707-31ee42c6cfcd", + "name": "Sheets - Leer propuestas ejecutadas", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const rows = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst MAX_EJECUCIONES_DEFAULT = 15;\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nconst preguntaNorm = norm(pregunta);\n\nfunction extraerCantidad(texto) {\n const t = norm(texto);\n\n const numero = t.match(/\\b(\\d{1,2})\\b/);\n if (numero) return Number(numero[1]);\n\n const palabras = {\n UNA: 1,\n UNO: 1,\n DOS: 2,\n TRES: 3,\n CUATRO: 4,\n CINCO: 5,\n SEIS: 6,\n SIETE: 7,\n OCHO: 8,\n NUEVE: 9,\n DIEZ: 10,\n QUINCE: 15,\n VEINTE: 20,\n };\n\n for (const [palabra, valor] of Object.entries(palabras)) {\n if (t.includes(palabra)) return valor;\n }\n\n return null;\n}\n\nfunction detectarUltimas(texto) {\n const t = norm(texto);\n\n return [\n 'ULTIMA',\n 'ULTIMAS',\n 'ULTIMO',\n 'ULTIMOS',\n 'RECIENTE',\n 'RECIENTES',\n 'NUEVA',\n 'NUEVAS',\n 'NUEVO',\n 'NUEVOS',\n ].some(x => t.includes(x));\n}\n\nfunction detectarInteresEjecuciones(texto) {\n const t = norm(texto);\n\n return [\n 'EJECUTADA',\n 'EJECUTADAS',\n 'EJECUCION',\n 'EJECUCIONES',\n 'EVIDENCIA',\n 'EVIDENCIAS',\n 'PROPUESTA EJECUTADA',\n 'PROPUESTAS EJECUTADAS',\n 'PRESENTACION EJECUTADA',\n 'PRESENTACION DE EJECUCION',\n 'PRESENTACION DE LA EJECUCION',\n 'MUESTRAME LA EJECUCION',\n 'ENVIAME LA PRESENTACION',\n 'LINK DE LA EJECUCION',\n 'PROPUESTA EXTERNA',\n 'PROPUESTAS EXTERNAS',\n 'EXTERNA',\n 'EXTERNAS',\n 'FOTOS',\n 'VIDEOS',\n 'IMPLEMENTACION',\n 'IMPLEMENTADA',\n 'IMPLEMENTADAS',\n ].some(x => t.includes(norm(x)));\n}\n\nfunction detectarTipoSolicitado(texto) {\n const t = norm(texto);\n\n if (\n t.includes('PROPUESTA EXTERNA') ||\n t.includes('PROPUESTAS EXTERNAS') ||\n t.includes(' EXTERNA') ||\n t.includes(' EXTERNAS')\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n if (\n t.includes('PROPUESTA EJECUTADA') ||\n t.includes('PROPUESTAS EJECUTADAS') ||\n t.includes('EJECUCION') ||\n t.includes('EJECUCIONES') ||\n t.includes('EJECUTADA') ||\n t.includes('EJECUTADAS')\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n return '';\n}\n\nfunction detectarPaises(texto) {\n const t = norm(texto);\n\n const paises = [\n 'EL SALVADOR',\n 'PANAMA',\n 'REPUBLICA DOMINICANA',\n 'COLOMBIA',\n 'PUERTO RICO',\n 'HONDURAS',\n 'MEXICO',\n 'VENEZUELA',\n 'JAMAICA',\n 'TRINIDAD Y TOBAGO',\n 'COSTA RICA',\n 'NICARAGUA',\n 'GUATEMALA',\n ];\n\n const encontrados = [];\n\n for (const pais of paises) {\n if (t.includes(norm(pais))) {\n encontrados.push(pais);\n }\n }\n\n if (/\\bRD\\b/.test(t) || t.includes('REP DOM') || t.includes('DOMINICANA')) {\n if (!encontrados.includes('REPUBLICA DOMINICANA')) {\n encontrados.push('REPUBLICA DOMINICANA');\n }\n }\n\n return encontrados;\n}\n\nfunction detectarAnios(texto) {\n const matches = norm(texto).match(/\\b(20\\d{2}|19\\d{2})\\b/g);\n return matches ? [...new Set(matches)] : [];\n}\n\nconst cantidadSolicitada = extraerCantidad(pregunta);\nconst quiereUltimas = detectarUltimas(pregunta);\nconst mencionaEjecuciones = detectarInteresEjecuciones(pregunta);\nconst tipoSolicitado = detectarTipoSolicitado(pregunta);\nconst paisesSolicitados = detectarPaises(pregunta);\nconst aniosSolicitados = detectarAnios(pregunta);\n\nconst stopwords = new Set([\n 'DAME', 'DARME', 'MUESTRA', 'MUESTRAME', 'BUSCA', 'BUSCAME',\n 'ENVIAME', 'ENVIAR', 'QUIERO', 'NECESITO',\n 'PROPUESTA', 'PROPUESTAS', 'EJECUTADA', 'EJECUTADAS',\n 'EJECUCION', 'EJECUCIONES', 'PRESENTACION', 'PRESENTACIONES',\n 'EXTERNA', 'EXTERNAS', 'EVIDENCIA', 'EVIDENCIAS',\n 'LINK', 'ENLACE', 'RESUMEN',\n 'ULTIMA', 'ULTIMAS', 'ULTIMO', 'ULTIMOS', 'RECIENTE', 'RECIENTES',\n 'NUEVA', 'NUEVAS', 'NUEVO', 'NUEVOS',\n 'DE', 'DEL', 'LA', 'LAS', 'EL', 'LOS', 'PARA', 'POR', 'CON',\n 'EN', 'UN', 'UNA', 'Y', 'O', 'QUE', 'ME', 'TIENES', 'TENGO',\n 'ALGUNA', 'ALGUNAS', 'ALGUN', 'SOBRE', 'RELACIONADA', 'RELACIONADAS',\n]);\n\nconst tokens = preguntaNorm\n .split(/[^A-Z0-9Ñ]+/)\n .map(t => t.trim())\n .filter(t => t.length >= 3 && !stopwords.has(t))\n .filter(t => !aniosSolicitados.includes(t));\n\nfunction getDateValue(...values) {\n for (const value of values) {\n const raw = clean(value);\n if (!raw) continue;\n\n const d = new Date(raw);\n if (!Number.isNaN(d.getTime())) return d.getTime();\n }\n\n return 0;\n}\n\nfunction fieldContains(value, expected) {\n return norm(value).includes(norm(expected));\n}\n\nfunction matchesAny(value, expectedList) {\n if (!expectedList.length) return true;\n return expectedList.some(expected => fieldContains(value, expected));\n}\n\nfunction normalizarTipo(row) {\n const campos = [\n row.tipo_reporte,\n row.clasificacion_reporte,\n row.propuesta_match_estado,\n row.estado_revision,\n row.motivo_revision,\n row.propuesta_match_revision,\n ].map(norm).join(' ');\n\n if (\n campos.includes('PROPUESTA_EXTERNA') ||\n campos.includes('EXTERNA') ||\n campos.includes('SIN MATCH') ||\n campos.includes('MATCH BAJO')\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n if (\n campos.includes('PROPUESTA_EJECUTADA') ||\n campos.includes('MATCH ALTO') ||\n campos.includes('MATCH_ALTO') ||\n campos.includes('EJECUTADA')\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n return clean(row.tipo_reporte || row.clasificacion_reporte || row.propuesta_match_estado || '');\n}\n\nfunction hardFilterPass(e) {\n if (tipoSolicitado === 'PROPUESTA_EXTERNA' && e.tipo_normalizado !== 'PROPUESTA_EXTERNA') {\n return false;\n }\n\n if (tipoSolicitado === 'PROPUESTA_EJECUTADA' && e.tipo_normalizado === 'PROPUESTA_EXTERNA') {\n return false;\n }\n\n if (paisesSolicitados.length && !matchesAny(e.pais, paisesSolicitados)) {\n return false;\n }\n\n if (aniosSolicitados.length) {\n const textoFechas = norm(`${e.fecha_recepcion} ${e.fecha_ejecucion} ${e.fecha_sort_raw}`);\n const tieneAnio = aniosSolicitados.some(anio => textoFechas.includes(anio));\n if (!tieneAnio) return false;\n }\n\n return true;\n}\n\nfunction scoreRow(row) {\n const camposFuertes = [\n row.propuesta_nombre_banco,\n row.propuesta_referencia,\n row.propuesta_nombre,\n row.nombre_propuesta,\n row.marca,\n row.cliente,\n row.pais,\n row.ubicacion,\n ];\n\n const camposGenerales = [\n row.propuesta_nombre_banco,\n row.propuesta_referencia,\n row.propuesta_nombre,\n row.nombre_propuesta,\n row.propuesta_link_banco,\n row.marca,\n row.cliente,\n row.pais,\n row.ubicacion,\n row.comentario_original,\n row.resumen_ia,\n row.descripcion_ejecucion,\n row.elementos_detectados,\n row.tags,\n row.transcripcion_audio,\n row.tipo_reporte,\n row.propuesta_match_estado,\n row.propuesta_match_revision,\n row.estado_revision,\n row.motivo_revision,\n ];\n\n const textoFuerte = norm(camposFuertes.join(' '));\n const textoGeneral = norm(camposGenerales.join(' '));\n\n let score = 0;\n\n for (const token of tokens) {\n if (textoGeneral.includes(token)) score += 1;\n if (textoFuerte.includes(token)) score += 3;\n }\n\n if (mencionaEjecuciones) score += 4;\n\n if (tipoSolicitado) {\n score += 5;\n }\n\n if (paisesSolicitados.length && matchesAny(row.pais, paisesSolicitados)) {\n score += 8;\n }\n\n if (clean(row.presentacion_ejecucion_url)) {\n score += 4;\n }\n\n if (clean(row.propuesta_nombre_banco)) {\n score += 3;\n }\n\n return score;\n}\n\nconst ejecuciones = rows\n .map((item, index) => {\n const row = item.json || {};\n\n const fila =\n row.row_number ||\n row.__row_number ||\n row.__rowNumber ||\n row.rowNumber ||\n index + 2;\n\n const tipoNormalizado = normalizarTipo(row);\n\n const fechaSort = getDateValue(\n row.fecha_ejecucion,\n row.fecha_recepcion,\n row.ultima_actualizacion\n );\n\n return {\n fila,\n score: scoreRow(row),\n\n ejecucion_id: clean(row.ejecucion_id),\n session_id: clean(row.session_id),\n\n tipo_reporte: clean(row.tipo_reporte || row.clasificacion_reporte),\n tipo_normalizado: tipoNormalizado,\n\n fecha_recepcion: clean(row.fecha_recepcion),\n fecha_ejecucion: clean(row.fecha_ejecucion),\n fecha_sort: fechaSort,\n fecha_sort_raw: clean(row.fecha_ejecucion || row.fecha_recepcion || row.ultima_actualizacion),\n\n manager_nombre: clean(row.manager_nombre),\n manager_telefono: clean(row.manager_telefono),\n\n propuesta_referencia: clean(row.propuesta_referencia),\n propuesta_match_estado: clean(row.propuesta_match_estado),\n propuesta_match_confianza: clean(row.propuesta_match_confianza),\n propuesta_nombre_banco: clean(row.propuesta_nombre_banco),\n propuesta_link_banco: clean(row.propuesta_link_banco),\n propuesta_match_revision: clean(row.propuesta_match_revision),\n\n marca: clean(row.marca),\n cliente: clean(row.cliente),\n pais: clean(row.pais),\n ubicacion: clean(row.ubicacion),\n\n comentario_original: clean(row.comentario_original),\n resumen_ia: clean(row.resumen_ia),\n descripcion_ejecucion: clean(row.descripcion_ejecucion),\n elementos_detectados: clean(row.elementos_detectados),\n tags: clean(row.tags),\n transcripcion_audio: clean(row.transcripcion_audio),\n\n media_folder_url: clean(row.media_folder_url),\n presentacion_ejecucion_url: clean(row.presentacion_ejecucion_url),\n\n fotos_count: clean(row.fotos_count),\n videos_count: clean(row.videos_count),\n audios_count: clean(row.audios_count),\n\n estado_revision: clean(row.estado_revision),\n motivo_revision: clean(row.motivo_revision),\n canal_origen: clean(row.canal_origen),\n };\n })\n .filter(e =>\n e.ejecucion_id ||\n e.presentacion_ejecucion_url ||\n e.resumen_ia ||\n e.descripcion_ejecucion ||\n e.propuesta_nombre_banco\n );\n\nlet candidatos = ejecuciones\n .filter(hardFilterPass)\n .filter(e => {\n if (tokens.length > 0) return e.score > 0;\n if (mencionaEjecuciones) return true;\n if (tipoSolicitado) return true;\n return false;\n });\n\nif (quiereUltimas) {\n candidatos = candidatos.sort((a, b) => {\n if (b.fecha_sort !== a.fecha_sort) return b.fecha_sort - a.fecha_sort;\n return b.fila - a.fila;\n });\n} else {\n candidatos = candidatos.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n if (b.fecha_sort !== a.fecha_sort) return b.fecha_sort - a.fecha_sort;\n return b.fila - a.fila;\n });\n}\n\nconst limite = cantidadSolicitada\n ? Math.min(Math.max(cantidadSolicitada, 1), 25)\n : MAX_EJECUCIONES_DEFAULT;\n\ncandidatos = candidatos.slice(0, limite);\n\nconst contextoEjecuciones = candidatos.map((e, index) => {\n return [\n `EJECUCION ${index + 1}`,\n `Fila: ${e.fila}`,\n `Ejecución ID: ${e.ejecucion_id}`,\n `Tipo reporte: ${e.tipo_reporte || e.tipo_normalizado}`,\n `Fecha recepción: ${e.fecha_recepcion}`,\n `Fecha ejecución: ${e.fecha_ejecucion}`,\n `Manager: ${e.manager_nombre}`,\n `Propuesta referencia: ${e.propuesta_referencia}`,\n `Match estado: ${e.propuesta_match_estado}`,\n `Match confianza: ${e.propuesta_match_confianza}`,\n `Propuesta nombre banco: ${e.propuesta_nombre_banco}`,\n `Propuesta original link: ${e.propuesta_link_banco}`,\n `Marca: ${e.marca}`,\n `Cliente: ${e.cliente}`,\n `País: ${e.pais}`,\n `Ubicación: ${e.ubicacion}`,\n `Resumen IA: ${e.resumen_ia}`,\n `Descripción ejecución: ${e.descripcion_ejecucion}`,\n `Elementos detectados: ${e.elementos_detectados}`,\n `Tags: ${e.tags}`,\n `Fotos: ${e.fotos_count}`,\n `Videos: ${e.videos_count}`,\n `Audios: ${e.audios_count}`,\n `Carpeta evidencias: ${e.media_folder_url}`,\n `Presentación ejecución: ${e.presentacion_ejecucion_url}`,\n `Estado revisión: ${e.estado_revision}`,\n `Motivo revisión: ${e.motivo_revision}`,\n ].join('\\n');\n}).join('\\n\\n---\\n\\n');\n\nreturn [\n {\n json: {\n source_type: 'propuestas_ejecutadas',\n pregunta,\n tokens,\n menciona_ejecuciones: mencionaEjecuciones,\n tipo_solicitado: tipoSolicitado,\n cantidad_solicitada: cantidadSolicitada,\n quiere_ultimas: quiereUltimas,\n paises_solicitados: paisesSolicitados,\n anios_solicitados: aniosSolicitados,\n total_ejecuciones: ejecuciones.length,\n total_ejecuciones_candidatas: candidatos.length,\n ejecuciones_candidatas: candidatos,\n contexto_ejecuciones: contextoEjecuciones,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 272, + 1280 + ], + "id": "79aa045d-603c-44b3-abcc-f58471fac1a4", + "name": "Code - Filtrar propuestas ejecutadas relevantes" + }, + { + "parameters": { + "numberInputs": 3 + }, + "type": "n8n-nodes-base.merge", + "typeVersion": 3.2, + "position": [ + 560, + 896 + ], + "id": "722b0de9-c0ad-4ee9-981c-390df1c9b6c9", + "name": "Merge - Unir propuestas y briefs Fulgencio" + }, + { + "parameters": { + "httpMethod": "POST", + "path": "googlechat", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + -720, + 864 + ], + "id": "511df9a0-6f14-4141-b277-bfbf04ac8967", + "name": "Webhook1", + "webhookId": "85f57925-7a74-4686-9b6b-1d022ec8bc37" + } + ], + "pinData": {}, + "connections": { + "Respond to Webhook": { + "main": [ + [ + { + "node": "Code - Preparar mensaje", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar mensaje": { + "main": [ + [ + { + "node": "Sheets - Leer propuestas", + "type": "main", + "index": 0 + }, + { + "node": "Sheets - Leer briefs CDC", + "type": "main", + "index": 0 + }, + { + "node": "Sheets - Leer propuestas ejecutadas", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer propuestas": { + "main": [ + [ + { + "node": "Code - Filtrar propuestas relevantes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Filtrar propuestas relevantes": { + "main": [ + [ + { + "node": "Merge - Unir propuestas y briefs Fulgencio", + "type": "main", + "index": 0 + } + ] + ] + }, + "AI Agent - Gemini Fulgencio": { + "main": [ + [ + { + "node": "Code - Formatear respuesta Chat", + "type": "main", + "index": 0 + } + ] + ] + }, + "Google Gemini Chat Model": { + "ai_languageModel": [ + [ + { + "node": "AI Agent - Gemini Fulgencio", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Code - Formatear respuesta Chat": { + "main": [ + [ + { + "node": "Chat - Enviar respuesta", + "type": "main", + "index": 0 + } + ] + ] + }, + "Chat - Enviar respuesta": { + "main": [ + [] + ] + }, + "Simple Memory": { + "ai_memory": [ + [ + { + "node": "AI Agent - Gemini Fulgencio", + "type": "ai_memory", + "index": 0 + } + ] + ] + }, + "Sheets - Leer briefs CDC": { + "main": [ + [ + { + "node": "Code - Filtrar briefs CDC", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Filtrar briefs CDC": { + "main": [ + [ + { + "node": "Merge - Unir propuestas y briefs Fulgencio", + "type": "main", + "index": 1 + } + ] + ] + }, + "Code - Construir contexto Fulgencio": { + "main": [ + [ + { + "node": "AI Agent - Gemini Fulgencio", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer propuestas ejecutadas": { + "main": [ + [ + { + "node": "Code - Filtrar propuestas ejecutadas relevantes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge - Unir propuestas y briefs Fulgencio": { + "main": [ + [ + { + "node": "Code - Construir contexto Fulgencio", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Filtrar propuestas ejecutadas relevantes": { + "main": [ + [ + { + "node": "Merge - Unir propuestas y briefs Fulgencio", + "type": "main", + "index": 2 + } + ] + ] + }, + "Webhook1": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": true, + "timeSavedMode": "fixed", + "errorWorkflow": "puF4LUczoSz3hcek", + "timezone": "America/Santo_Domingo", + "callerPolicy": "workflowsFromSameOwner" + }, + "versionId": "803bfe09-4729-49a6-aa61-3e9046b4e198", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "KRF6cPYGgmYT68rF", + "tags": [] +} \ No newline at end of file diff --git a/Chat de WhatsApp - Propuestas Ejecutadas - Evolution API.json b/Chat de WhatsApp - Propuestas Ejecutadas - Evolution API.json new file mode 100644 index 0000000..7fc4603 --- /dev/null +++ b/Chat de WhatsApp - Propuestas Ejecutadas - Evolution API.json @@ -0,0 +1,10112 @@ +{ + "name": "Chat de WhatsApp - Propuestas Ejecutadas - Evolution API", + "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": [ + 15248, + 1696 + ], + "id": "dad797ac-f70a-4550-a1bb-297d2f296dc4", + "name": "WhatsApp - Enviar mensaje TEST", + "disabled": true + }, + { + "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", + "disabled": true + }, + { + "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": [ + -7392, + -576 + ], + "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, + "disabled": 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-2.5-pro", + "mode": "list", + "cachedResultName": "models/gemini-2.5-pro" + }, + "text": "={{ $json.gemini_prompt_audio }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -736, + 2048 + ], + "id": "353aba0e-8ed5-484a-b8fc-65ee7829a951", + "name": "Gemini - Analizar audio WhatsApp TEST", + "retryOnFail": true, + "waitBetweenTries": 5000, + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + }, + "onError": "continueRegularOutput" + }, + { + "parameters": { + "jsCode": "const geminiItems = $input.all();\nconst prepItems = $('Code - Preparar Gemini audio WhatsApp TEST').all();\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction safeJson(value) {\n try {\n return JSON.stringify(value);\n } catch (e) {\n return String(value ?? '');\n }\n}\n\nfunction extractTextFromGemini(obj) {\n if (!obj) return '';\n\n if (typeof obj === 'string') {\n return clean(obj);\n }\n\n if (typeof obj.text === 'string') return clean(obj.text);\n if (typeof obj.output === 'string') return clean(obj.output);\n if (typeof obj.response === 'string') return clean(obj.response);\n if (typeof obj.content === 'string') return clean(obj.content);\n\n const possibleParts =\n obj?.content?.parts ||\n obj?.candidates?.[0]?.content?.parts ||\n obj?.response?.candidates?.[0]?.content?.parts ||\n obj?.parts ||\n [];\n\n if (Array.isArray(possibleParts)) {\n const text = possibleParts\n .map(part => clean(part?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (text) return text;\n }\n\n const jsonText = safeJson(obj);\n return jsonText && jsonText !== '{}' ? jsonText : '';\n}\n\nfunction detectGeminiError(obj) {\n if (!obj || typeof obj !== 'object') return '';\n\n const candidates = [\n obj.error,\n obj.message,\n obj.description,\n obj.fullMessage,\n obj.full_message,\n obj.errorMessage,\n obj.error_message,\n obj?.error?.message,\n obj?.error?.description,\n obj?.error?.cause,\n obj?.json?.error,\n obj?.json?.message,\n ];\n\n const found = candidates\n .map(clean)\n .filter(Boolean)\n .join(' | ');\n\n const serialized = safeJson(obj);\n\n if (\n found ||\n serialized.includes('Internal error') ||\n serialized.includes('INTERNAL') ||\n serialized.includes('500') ||\n serialized.includes('service was not able to process')\n ) {\n return found || serialized;\n }\n\n return '';\n}\n\nconst output = [];\n\nconst total = Math.max(geminiItems.length, prepItems.length);\n\nfor (let i = 0; i < total; i++) {\n const geminiItem = geminiItems[i] || {};\n const prepItem = prepItems[i] || {};\n\n const geminiJson = geminiItem.json || {};\n const mediaJson = prepItem.json || geminiJson || {};\n\n const sessionId = clean(mediaJson.session_id);\n const ejecucionId = clean(mediaJson.ejecucion_id || sessionId);\n const mediaEventId = clean(mediaJson.media_event_id || mediaJson.event_id);\n const mediaFileName = clean(mediaJson.media_file_name || mediaJson.file_name || mediaJson.nombre_archivo);\n const driveFileId = clean(mediaJson.drive_file_id || mediaJson.file_id);\n const driveFileUrl = clean(mediaJson.drive_file_url || mediaJson.media_drive_url || mediaJson.webViewLink);\n\n const errorGemini = detectGeminiError(geminiJson);\n\n let analisisTexto = '';\n let estado = '';\n let requiereRevision = false;\n\n if (errorGemini) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_ERROR_GEMINI';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini no pudo procesar esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar propuesta, cliente, marca, país, ubicación o contexto.',\n '',\n `Detalle técnico: ${errorGemini}`,\n ].join('\\n');\n } else {\n analisisTexto = extractTextFromGemini(geminiJson);\n\n if (!analisisTexto) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_VACIO';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini respondió, pero no devolvió texto útil para esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar la propuesta.',\n ].join('\\n');\n } else {\n estado = 'ANALISIS_AUDIO_COMPLETADO';\n }\n }\n\n if (!sessionId) {\n throw new Error('No llegó session_id al normalizar análisis de audio.');\n }\n\n if (!mediaEventId) {\n throw new Error('No llegó media_event_id al normalizar análisis de audio.');\n }\n\n output.push({\n json: {\n ...mediaJson,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: 'audio',\n media_file_name: mediaFileName,\n drive_file_id: driveFileId,\n drive_file_url: driveFileUrl,\n\n analisis_texto: analisisTexto,\n fecha_analisis: nowIso(),\n estado,\n\n gemini_audio_error: errorGemini || '',\n audio_requiere_revision: requiereRevision,\n\n media_index: Number(mediaJson.media_index || 0),\n media_total: Number(mediaJson.media_total || mediaJson.media_total_esperado || 0),\n },\n });\n}\n\nreturn output;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -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-2.5-pro", + "mode": "list", + "cachedResultName": "models/gemini-2.5-pro" + }, + "text": "={{ $json.gemini_prompt_imagen }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -720, + 2272 + ], + "id": "212e7998-4787-445d-824e-5e7b3911e206", + "name": "Gemini - Analizar imagen WhatsApp TEST", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini imagen WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'image',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_IMAGEN_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -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-2.5-pro", + "mode": "list", + "cachedResultName": "models/gemini-2.5-pro" + }, + "text": "={{ $json.gemini_prompt_video }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + -848, + 2512 + ], + "id": "4e0c63b2-429f-4908-8c57-69093c2db609", + "name": "Analyze video", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini video WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'video',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_VIDEO_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -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-2.5-pro", + "mode": "list", + "cachedResultName": "models/gemini-2.5-pro" + }, + "messages": { + "values": [ + { + "content": "={{ $json.gemini_prompt_final }}" + } + ] + }, + "builtInTools": {}, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + 5024, + 2144 + ], + "id": "73c6e7dd-5ac4-4673-9780-7ab545a4e8c5", + "name": "Gemini - Generar JSON final propuesta ejecutada TEST", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + }, + { + "parameters": { + "jsCode": "const gemini = $json || {};\nconst base = $('Code - Consolidar análisis media Gemini WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst esNoIdentificado = (valor) => {\n const texto = normalizarPlano(valor);\n\n return (\n !texto ||\n texto === 'NO IDENTIFICADO' ||\n texto === 'NO DISPONIBLE' ||\n texto === 'N/A' ||\n texto === 'NA' ||\n texto === 'NULL' ||\n texto === 'UNDEFINED'\n );\n};\n\nconst valor = (campo, fallback = 'No identificado') => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return fallback;\n\n return v;\n};\n\nconst valorOpcional = (campo) => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return '';\n\n return v;\n};\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (Array.isArray(obj.content?.parts)) {\n const txt = obj.content.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.parts)) {\n const txt = obj.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.candidates)) {\n const txt = obj.candidates\n .flatMap(c => c.content?.parts || [])\n .map(p => p.text || '')\n .join('\\n')\n .trim();\n\n if (txt) return txt;\n }\n\n if (obj.message && typeof obj.message === 'object') {\n const txt = extraerTexto(obj.message);\n if (txt) return txt;\n }\n\n if (obj.data && typeof obj.data === 'object') {\n const txt = extraerTexto(obj.data);\n if (txt) return txt;\n }\n\n return JSON.stringify(obj);\n};\n\nlet texto = extraerTexto(gemini).trim();\n\ntexto = texto\n .replace(/^```json/i, '')\n .replace(/^```/i, '')\n .replace(/```$/i, '')\n .trim();\n\nlet parsed = {};\n\ntry {\n parsed = JSON.parse(texto);\n} catch (error) {\n parsed = {\n tipo_reporte: 'NO_DETERMINADO',\n tipo_reporte_confianza: 'BAJA',\n tipo_reporte_motivo: `No se pudo parsear el JSON final de Gemini: ${error.message}`,\n\n propuesta_referencia: 'No identificado',\n marca: 'No identificado',\n cliente: 'No identificado',\n pais: 'No identificado',\n ubicacion: 'No identificado',\n fecha_ejecucion: 'No identificado',\n que_se_implemento: 'No identificado',\n comentarios_resultados: '',\n resumen_ejecucion: texto || 'No se pudo parsear el JSON de Gemini.',\n evidencia_audio_resumen: base.texto_audio || '',\n evidencia_imagenes_resumen: base.texto_imagenes || '',\n evidencia_videos_resumen: base.texto_videos || '',\n nivel_confianza: 'BAJA',\n requiere_revision: 'SI',\n motivo_revision: `Error parseando JSON final: ${error.message}`\n };\n}\n\nconst normalizarTipoReporte = (valorTipo) => {\n const texto = String(valorTipo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, '_');\n\n if (\n texto === 'PROPUESTA_EJECUTADA' ||\n texto === 'EJECUTADA' ||\n texto === 'EJECUTADO'\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n if (\n texto === 'PROPUESTA_EXTERNA' ||\n texto === 'EXTERNA' ||\n texto === 'EXTERNO'\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n return 'NO_DETERMINADO';\n};\n\nconst normalizarConfianza = (valorConfianza) => {\n const texto = String(valorConfianza ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['ALTA', 'MEDIA', 'BAJA'].includes(texto)) {\n return texto;\n }\n\n return 'BAJA';\n};\n\nconst normalizarSiNo = (valorSiNo, fallback = 'SI') => {\n const texto = String(valorSiNo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['SI', 'SÍ', 'YES', 'TRUE'].includes(texto)) return 'SI';\n if (['NO', 'FALSE'].includes(texto)) return 'NO';\n\n return fallback;\n};\n\nconst limpiarReferenciaVisual = (valorRef) => {\n let textoRef = limpiar(valorRef);\n\n if (esNoIdentificado(textoRef)) return 'No identificado';\n\n textoRef = textoRef\n .replace(/_/g, ' ')\n .replace(/\\+/g, '+')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return textoRef || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valorUbicacion) => {\n const textoUbicacion = normalizarPlano(valorUbicacion);\n\n if (!textoUbicacion) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bVERSIÓN\\b/,\n /\\bCODIGO\\b/,\n /\\bCÓDIGO\\b/,\n /\\bGUATE WMC\\b/,\n /\\bPOLLO CAMPERO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(textoUbicacion));\n};\n\nconst normalizarUbicacion = (valorUbicacion) => {\n const textoUbicacion = limpiar(valorUbicacion);\n\n if (esNoIdentificado(textoUbicacion)) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(textoUbicacion)) {\n return 'No identificado';\n }\n\n return textoUbicacion;\n};\n\nconst normalizarFechaEjecucion = (valorFecha) => {\n const textoFecha = limpiar(valorFecha);\n\n if (esNoIdentificado(textoFecha)) return 'No identificado';\n\n // Si solo viene el año, no es una fecha de ejecución real.\n if (/^20\\d{2}$/.test(textoFecha)) {\n return 'No identificado';\n }\n\n return textoFecha;\n};\n\nconst limitarTexto = (valorTexto, max = 1200, fallback = '') => {\n const textoLimpio = valor(valorTexto, fallback);\n\n if (!textoLimpio) return '';\n\n return textoLimpio.length > max\n ? `${textoLimpio.slice(0, max - 3)}...`\n : textoLimpio;\n};\n\nconst ahora = new Date().toISOString();\n\nconst tipoReporte = normalizarTipoReporte(parsed.tipo_reporte);\nconst tipoReporteConfianza = normalizarConfianza(parsed.tipo_reporte_confianza);\nconst tipoReporteMotivo = valor(parsed.tipo_reporte_motivo, 'No identificado');\n\nconst requiereRevision = normalizarSiNo(parsed.requiere_revision, 'SI');\n\nconst estadoRevision = requiereRevision === 'SI'\n ? 'PENDIENTE_REVISION'\n : 'ANALISIS_COMPLETADO';\n\nconst propuestaReferenciaOriginal = valor(parsed.propuesta_referencia);\nconst propuestaReferenciaLimpia = limpiarReferenciaVisual(propuestaReferenciaOriginal);\n\nconst fechaEjecucionOriginal = valor(parsed.fecha_ejecucion);\nconst fechaEjecucionLimpia = normalizarFechaEjecucion(fechaEjecucionOriginal);\n\nconst ubicacionOriginal = valor(parsed.ubicacion);\nconst ubicacionLimpia = normalizarUbicacion(ubicacionOriginal);\n\nconst evidenciaAudio = valorOpcional(parsed.evidencia_audio_resumen || base.texto_audio);\nconst evidenciaImagenes = valorOpcional(parsed.evidencia_imagenes_resumen || base.texto_imagenes);\nconst evidenciaVideos = valorOpcional(parsed.evidencia_videos_resumen || base.texto_videos);\n\nconst elementosDetectados = [\n evidenciaImagenes,\n evidenciaVideos\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .join('\\n\\n');\n\nconst tagsSet = new Set();\n\n[\n parsed.marca,\n parsed.cliente,\n parsed.pais,\n ubicacionLimpia\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .forEach(v => tagsSet.add(v));\n\nconst tags = [...tagsSet].join(', ');\n\nconst resumenIa = limitarTexto(parsed.resumen_ejecucion, 1200, '');\nconst descripcionEjecucion = limitarTexto(parsed.que_se_implemento, 1200, 'No identificado');\nconst comentariosResultados = limitarTexto(parsed.comentarios_resultados, 900, '');\n\nreturn [\n {\n json: {\n ...base,\n\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n session_id: base.session_id || '',\n\n fecha_recepcion: base.fecha_recepcion || ahora,\n fecha_ejecucion: fechaEjecucionLimpia,\n fecha_ejecucion_original_ia: fechaEjecucionOriginal,\n\n manager_nombre: valor(base.manager_nombre),\n manager_telefono: valor(base.manager_telefono),\n canal_origen: base.canal_origen || 'WHATSAPP',\n\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza,\n tipo_reporte_motivo: tipoReporteMotivo,\n\n propuesta_referencia: propuestaReferenciaLimpia,\n propuesta_referencia_original_ia: propuestaReferenciaOriginal,\n\n propuesta_match_estado: 'PENDIENTE',\n propuesta_match_confianza: '',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_match_revision: 'PENDIENTE_MATCH_BANCO',\n\n marca: valor(parsed.marca),\n cliente: valor(parsed.cliente),\n pais: valor(parsed.pais),\n ubicacion: ubicacionLimpia,\n ubicacion_original_ia: ubicacionOriginal,\n\n comentario_original: evidenciaAudio || comentariosResultados || '',\n resumen_ia: resumenIa,\n descripcion_ejecucion: descripcionEjecucion,\n comentarios_resultados: comentariosResultados,\n elementos_detectados: elementosDetectados,\n tags,\n\n media_folder_url: base.media_folder_url || '',\n presentacion_ejecucion_url: '',\n\n fotos_count: base.total_analisis_imagenes || 0,\n videos_count: base.total_analisis_videos || 0,\n audios_count: base.total_analisis_audio || 0,\n\n estado_revision: estadoRevision,\n motivo_revision: valor(parsed.motivo_revision, ''),\n ultima_actualizacion: ahora,\n\n transcripcion_audio: evidenciaAudio,\n\n nivel_confianza: normalizarConfianza(parsed.nivel_confianza),\n requiere_revision: requiereRevision,\n\n gemini_json_raw: texto,\n\n normalizacion_json_final_debug: {\n propuesta_referencia_original: propuestaReferenciaOriginal,\n propuesta_referencia_limpia: propuestaReferenciaLimpia,\n fecha_ejecucion_original: fechaEjecucionOriginal,\n fecha_ejecucion_limpia: fechaEjecucionLimpia,\n ubicacion_original: ubicacionOriginal,\n ubicacion_limpia: ubicacionLimpia,\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 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": {}, + "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 = '1gHgQBDL2uFaa7yNwcyWrtE24B_UrCU1Dy-n63TPurPE';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 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 objetivoSlides = cortar(\n data.objetivo ||\n jsonFinal.objetivo ||\n data.descripcion_ejecucion ||\n jsonFinal.descripcion_ejecucion ||\n data.resumen_ia ||\n jsonFinal.resumen_ia ||\n 'No disponible',\n 900\n);\n\nconst replacements = {\n '{{MARCA}}': limpiarTexto(data.marca || jsonFinal.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente || jsonFinal.cliente),\n '{{PAIS}}': limpiarTexto(data.pais || jsonFinal.pais),\n '{{UBICACION}}': ubicacionSlides,\n\n '{{FECHA_EJECUCION}}': fechaEjecucionSlides,\n '{{PROPUESTA_REFERENCIA}}': propuestaReferenciaSlides,\n '{{OBJETIVO}}': objetivoSlides,\n\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 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", + "disabled": true + }, + { + "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": [ + 13808, + 1872 + ], + "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, + 1872 + ], + "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": 4 + }, + "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": 4 + }, + "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": 848 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -9568, + -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": 624, + "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": 912, + "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": 1408, + "color": 7 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -5616, + -2832 + ], + "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": 5712, + "color": 3 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -1520, + 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": 1024, + "width": 10912, + "color": 6 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 4704, + 1488 + ], + "id": "a3245658-be74-4ea6-8a58-a104803896ac", + "name": "Sticky Note9" + }, + { + "parameters": { + "content": "## Fotos Listas\n\nActualizar a Fotos listas y preparar solicitud de videos", + "height": 288, + "width": 944, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -3792, + -1264 + ], + "id": "0ce97fde-e030-4175-baa9-b804cdc18803", + "name": "Sticky Note10" + } + ], + "pinData": {}, + "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 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": true, + "timeSavedMode": "fixed", + "callerPolicy": "workflowsFromSameOwner", + "timezone": "America/Santo_Domingo" + }, + "versionId": "14dfae53-7771-4e60-945c-f5707b162c00", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "CdosDIWBgnDMnPdr", + "tags": [] +} \ No newline at end of file diff --git a/Chat de WhatsApp de Propuestas Ejecutadas - API Oficial.json b/Chat de WhatsApp de Propuestas Ejecutadas - API Oficial.json new file mode 100644 index 0000000..5935876 --- /dev/null +++ b/Chat de WhatsApp de Propuestas Ejecutadas - API Oficial.json @@ -0,0 +1,10230 @@ +{ + "name": "Chat de WhatsApp de Propuestas Ejecutadas - API Oficial", + "nodes": [ + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v26.0/1151359118067218/messages", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "whatsAppApi", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ (() => {\n const text = (value) => String(value ?? '');\n\n const cleanMultiline = (value) =>\n text(value)\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\\\r\\\\n/g, '\\n')\n .replace(/\\\\n/g, '\\n')\n .replace(/[ \\t]+\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n\n const cleanSingleLine = (value) =>\n cleanMultiline(value)\n .replace(/\\s*\\n\\s*/g, ' ')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n\n const digits = (value) => text(value).replace(/\\D/g, '');\n\n const destination = digits(\n $json.whatsapp_to ||\n $json.sender_phone ||\n $json.manager_telefono ||\n $json.whatsapp_remote_jid ||\n ''\n );\n\n if (!destination) {\n throw new Error('No se encontró el número privado de destino de WhatsApp.');\n }\n\n const bodyText = cleanMultiline(\n $json.whatsapp_text ||\n $json.texto_respuesta ||\n $json.mensaje ||\n $json.text ||\n $json.message_text ||\n ''\n );\n\n const headerText = cleanSingleLine(\n $json.whatsapp_header || ''\n ).slice(0, 60);\n\n const footerText = cleanSingleLine(\n $json.whatsapp_footer ||\n 'GomezLee Marketing · Fulgencio'\n ).slice(0, 60);\n\n const buttons = Array.isArray($json.whatsapp_buttons)\n ? $json.whatsapp_buttons\n .map((button, index) => ({\n id: cleanSingleLine(\n button?.id || `OPTION_${index + 1}`\n ).slice(0, 256),\n title: cleanSingleLine(\n button?.title || `Opción ${index + 1}`\n ).slice(0, 20)\n }))\n .filter((button) => button.id && button.title)\n .slice(0, 3)\n : [];\n\n if (buttons.length > 0) {\n const interactive = {\n type: 'button',\n body: {\n text: (bodyText || 'Selecciona una opción:').slice(0, 1024)\n },\n action: {\n buttons: buttons.map((button) => ({\n type: 'reply',\n reply: {\n id: button.id,\n title: button.title\n }\n }))\n }\n };\n\n if (headerText) {\n interactive.header = {\n type: 'text',\n text: headerText\n };\n }\n\n if (footerText) {\n interactive.footer = {\n text: footerText\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: destination,\n type: 'interactive',\n interactive\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: destination,\n type: 'text',\n text: {\n preview_url: true,\n body: (bodyText || 'Mensaje sin contenido').slice(0, 4096)\n }\n };\n})() }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 71280, + 31760 + ], + "id": "0d1e3e42-3f9a-4e16-b847-faa6aacbb503", + "name": "WhatsApp - Enviar mensaje API Oficial", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 5000, + "credentials": { + "whatsAppApi": { + "id": "t14kVayc9FurLReq", + "name": "WhatsApp API - GLM CDC" + } + } + }, + { + "parameters": { + "jsCode": "const input = $json || {};\n\n// --------------------------------------------------\n// 1. Resolver payload de WhatsApp Cloud API\n// --------------------------------------------------\nconst entryValue =\n input.entry?.[0]?.changes?.[0]?.value ||\n input.value ||\n input.body?.entry?.[0]?.changes?.[0]?.value ||\n input.body?.value ||\n input;\n\nconst metadata = entryValue.metadata || input.metadata || {};\nconst contacts = entryValue.contacts || input.contacts || [];\nconst messages = entryValue.messages || input.messages || [];\nconst message = messages[0] || {};\n\nconst clean = (value) => String(value ?? '').trim();\nconst digits = (value) => clean(value).replace(/\\D/g, '');\n\nconst normalizeText = (value) =>\n clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst contact = contacts[0] || {};\nconst profileName =\n clean(contact.profile?.name) ||\n clean(message.profile?.name) ||\n 'Usuario WhatsApp';\n\n// Groups API entrega group_id en el mensaje.\n// En conversación individual, \"from\" es el número del usuario.\nconst senderPhone = digits(\n message.from ||\n entryValue.from ||\n input.from ||\n contact.wa_id ||\n ''\n);\n\nconst groupId = clean(\n message.group_id ||\n message.groupId ||\n entryValue.group_id ||\n entryValue.groupId ||\n input.group_id ||\n input.groupId ||\n ''\n);\n\nconst isGroup = Boolean(groupId);\nconst recipientType = isGroup ? 'group' : 'individual';\nconst whatsappTo = isGroup ? groupId : senderPhone;\n\n// --------------------------------------------------\n// 2. Extraer texto, botones y listas\n// --------------------------------------------------\nconst interactiveText =\n clean(message.interactive?.button_reply?.id) ||\n clean(message.interactive?.button_reply?.title) ||\n clean(message.interactive?.list_reply?.id) ||\n clean(message.interactive?.list_reply?.title) ||\n clean(message.button?.payload) ||\n clean(message.button?.text) ||\n '';\n\nconst textMessage =\n interactiveText ||\n clean(message.text?.body) ||\n clean(message.caption) ||\n clean(entryValue.text?.body) ||\n clean(input.text?.body) ||\n clean(input.text) ||\n '';\n\n// --------------------------------------------------\n// 3. Detectar media oficial\n// --------------------------------------------------\nconst audio = message.audio || null;\nconst image = message.image || null;\nconst video = message.video || null;\nconst document = message.document || null;\nconst sticker = message.sticker || null;\n\nlet messageType = clean(message.type).toLowerCase();\n\nif (!messageType) {\n if (audio) messageType = 'audio';\n else if (image) messageType = 'image';\n else if (video) messageType = 'video';\n else if (document) messageType = 'document';\n else if (sticker) messageType = 'sticker';\n else if (textMessage) messageType = 'text';\n else messageType = 'unknown';\n}\n\nconst mediaObject =\n audio ||\n image ||\n video ||\n document ||\n sticker ||\n null;\n\nconst hasMedia = Boolean(mediaObject?.id);\nconst mediaSourceId = clean(mediaObject?.id);\nconst mediaMimeType = clean(mediaObject?.mime_type);\nconst mediaFileName =\n clean(document?.filename) ||\n `${messageType}_${mediaSourceId || Date.now()}`;\n\n// --------------------------------------------------\n// 4. Comandos oficiales\n// --------------------------------------------------\nconst normalizeCommand = (value) => {\n const t = normalizeText(value);\n\n if (t === 'HEY') return 'START';\n if (t === 'CANCELAR' || t === 'CANCEL') return 'CANCELAR';\n\n if (\n t === 'FOTOS LISTAS' ||\n t === 'FOTOS_LISTAS' ||\n t === 'PHOTOS READY' ||\n t === 'PHOTOS_READY'\n ) return 'FOTOS_LISTAS';\n\n if (\n t === 'SIN VIDEO' ||\n t === 'SIN_VIDEO' ||\n t === 'NO VIDEO' ||\n t === 'NO_VIDEO'\n ) return 'SIN_VIDEO';\n\n if (t === 'LISTO' || t === 'DONE') return 'LISTO';\n\n // Único comando oficial del leaderboard.\n if (t === 'RANKING') return 'LEADERBOARD';\n\n return 'NORMAL';\n};\n\n// --------------------------------------------------\n// 5. País e idioma por teléfono\n// --------------------------------------------------\nconst detectCountry = (phone) => {\n const n = digits(phone);\n\n if (n.startsWith('502')) return { country_code: '502', pais_detectado: 'Guatemala', idioma_flujo: 'ES' };\n if (n.startsWith('503')) return { country_code: '503', pais_detectado: 'El Salvador', idioma_flujo: 'ES' };\n if (n.startsWith('504')) return { country_code: '504', pais_detectado: 'Honduras', idioma_flujo: 'ES' };\n if (n.startsWith('505')) return { country_code: '505', pais_detectado: 'Nicaragua', idioma_flujo: 'ES' };\n if (n.startsWith('506')) return { country_code: '506', pais_detectado: 'Costa Rica', idioma_flujo: 'ES' };\n if (n.startsWith('507')) return { country_code: '507', pais_detectado: 'Panamá', idioma_flujo: 'ES' };\n if (n.startsWith('57')) return { country_code: '57', pais_detectado: 'Colombia', idioma_flujo: 'ES' };\n if (n.startsWith('52')) return { country_code: '52', pais_detectado: 'México', idioma_flujo: 'ES' };\n if (n.startsWith('58')) return { country_code: '58', pais_detectado: 'Venezuela', idioma_flujo: 'ES' };\n\n if (n.startsWith('1809') || n.startsWith('1829') || n.startsWith('1849')) {\n return { country_code: '1', pais_detectado: 'República Dominicana', idioma_flujo: 'ES' };\n }\n\n if (n.startsWith('1876')) {\n return { country_code: '1', pais_detectado: 'Jamaica', idioma_flujo: 'EN' };\n }\n\n if (n.startsWith('1868')) {\n return { country_code: '1', pais_detectado: 'Trinidad y Tobago', idioma_flujo: 'EN' };\n }\n\n if (n.startsWith('1787') || n.startsWith('1939')) {\n return { country_code: '1', pais_detectado: 'Puerto Rico', idioma_flujo: 'ES' };\n }\n\n if (n.startsWith('1')) {\n return { country_code: '1', pais_detectado: 'País +1 no identificado', idioma_flujo: 'EN' };\n }\n\n return { country_code: '', pais_detectado: 'No identificado', idioma_flujo: 'ES' };\n};\n\nconst countryInfo = detectCountry(senderPhone);\n\nreturn [\n {\n json: {\n event_id: clean(message.id || input.id || Date.now()),\n fecha_recepcion: new Date(\n Number(message.timestamp || 0) > 0\n ? Number(message.timestamp) * 1000\n : Date.now()\n ).toISOString(),\n\n canal_origen: isGroup ? 'WHATSAPP_OFICIAL_GRUPO' : 'WHATSAPP_OFICIAL_PRIVADO',\n api_origen: 'META_WHATSAPP_CLOUD_API',\n\n manager_telefono: senderPhone,\n manager_nombre: profileName,\n usuario_id_origen: senderPhone,\n\n is_group: isGroup,\n group_jid: groupId,\n group_id: groupId,\n group_name: clean(entryValue.group_name || input.group_name || ''),\n sender_jid: senderPhone,\n sender_phone: senderPhone,\n sender_name: profileName,\n\n whatsapp_remote_jid: whatsappTo,\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: recipientType,\n\n from_me: false,\n phone_number_id: clean(metadata.phone_number_id),\n display_phone_number: clean(metadata.display_phone_number),\n\n message_type: messageType,\n texto: textMessage,\n accion_flujo: normalizeCommand(textMessage),\n\n tiene_media: hasMedia,\n media_count: hasMedia ? 1 : 0,\n media_source_id: mediaSourceId,\n media_mime_type: mediaMimeType,\n media_file_name: mediaFileName,\n\n country_code: countryInfo.country_code,\n pais_detectado: countryInfo.pais_detectado,\n idioma_flujo: countryInfo.idioma_flujo,\n\n raw_preview: JSON.stringify(entryValue).slice(0, 4000),\n raw_event: entryValue,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 46736, + 29040 + ], + "id": "1d30f3a0-94f3-4510-a16e-94faa8109103", + "name": "Code - Normalizar evento WhatsApp TEST" + }, + { + "parameters": { + "updates": [ + "messages" + ], + "options": {} + }, + "type": "n8n-nodes-base.whatsAppTrigger", + "typeVersion": 1, + "position": [ + 46528, + 29040 + ], + "id": "c052dcd0-876a-4ea0-b1d5-fa6b50b36e4a", + "name": "WhatsApp Trigger - API Oficial", + "webhookId": "f7699eff-45d9-4f7d-831a-78280c1b6ac3", + "credentials": { + "whatsAppTriggerApi": { + "id": "gTh5IGhZ1rhxTgAY", + "name": "WhatsApp API - GLM CDC" + } + } + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 47920, + 28992 + ], + "id": "d3b8ca0e-a551-4596-a92b-0ef06097cac9", + "name": "Sheets - Leer sesiones existentes", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const evento = $('Code - Validar chat privado WhatsApp TEST').first().json || {};\nconst sesiones = $input.all().map((item) => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) =>\n texto(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst esVerdadero = (valor) =>\n ['TRUE', 'SI', 'SÍ', 'YES', '1'].includes(normalizar(valor));\n\nconst estadosAbiertos = new Set([\n 'ACTIVA',\n 'PENDIENTE',\n 'EN_PROCESO',\n 'PROCESANDO',\n 'LISTO_PARA_RECUPERAR_MEDIA',\n 'LISTO_PARA_ANALIZAR',\n]);\n\nconst etapasAbiertas = new Set([\n 'ESPERANDO_AUDIO',\n 'ESPERANDO_IMAGENES',\n 'ESPERANDO_VIDEOS',\n 'PROCESANDO',\n]);\n\nconst managerTelefono = texto(\n evento.manager_telefono ||\n evento.sender_phone ||\n evento.whatsapp_to\n).replace(/\\D/g, '');\n\nconst senderPhone = managerTelefono;\nconst accion = texto(evento.accion_flujo || 'NORMAL');\n\nconst idiomaFlujo =\n normalizar(evento.idioma_flujo || 'ES') === 'EN'\n ? 'EN'\n : 'ES';\n\nif (!managerTelefono) {\n throw new Error('No llegó el número del usuario para resolver la sesión privada.');\n}\n\n// Solo reutiliza sesiones privadas. Esto evita tomar una sesión histórica\n// creada cuando el workflow operaba dentro de un grupo.\nconst sesionActiva = sesiones\n .filter((row) => {\n const rowManager = texto(\n row.manager_telefono ||\n row.sender_phone\n ).replace(/\\D/g, '');\n\n const rowGroup = texto(row.group_jid || row.group_id);\n const rowIsGroup = esVerdadero(row.is_group) || Boolean(rowGroup);\n\n return rowManager === managerTelefono && !rowIsGroup;\n })\n .filter((row) => {\n const estado = normalizar(row.estado);\n const etapa = normalizar(row.etapa);\n\n return estadosAbiertos.has(estado) || etapasAbiertas.has(etapa);\n })\n .sort((a, b) => {\n return (\n new Date(b.ultima_actividad || b.fecha_inicio || 0) -\n new Date(a.ultima_actividad || a.fecha_inicio || 0)\n );\n })[0] || null;\n\nconst suffix = Math.random().toString(36).slice(2, 6).toUpperCase();\nconst phoneSuffix = senderPhone.slice(-4) || '0000';\nconst sessionId = `WA_${Date.now()}_${phoneSuffix}_${suffix}`;\n\nlet decision = '';\n\nif (accion === 'LEADERBOARD') {\n decision = 'MOSTRAR_LEADERBOARD';\n} else if (accion === 'START') {\n decision = sesionActiva ? 'AVISO_SESION_ACTIVA' : 'CREAR_SESION';\n} else if (accion === 'CANCELAR') {\n decision = sesionActiva ? 'CANCELAR_SESION' : 'SIN_SESION_PARA_CANCELAR';\n} else {\n decision = sesionActiva ? 'CONTINUAR_SESION' : 'SIN_SESION_ACTIVA';\n}\n\nreturn [\n {\n json: {\n ...evento,\n\n is_group: false,\n group_id: '',\n group_jid: '',\n group_name: '',\n whatsapp_to: managerTelefono,\n whatsapp_remote_jid: managerTelefono,\n whatsapp_recipient_type: 'individual',\n\n idioma_flujo: idiomaFlujo,\n\n sesion_activa_encontrada: Boolean(sesionActiva),\n sesion_activa: sesionActiva,\n\n session_id: sesionActiva\n ? texto(sesionActiva.session_id)\n : sessionId,\n\n ejecucion_id: sesionActiva\n ? texto(sesionActiva.ejecucion_id || sesionActiva.session_id)\n : sessionId,\n\n decision_flujo: decision,\n\n nueva_sesion: {\n session_id: sessionId,\n\n manager_telefono: managerTelefono,\n manager_nombre: texto(evento.manager_nombre),\n\n canal_origen: 'WHATSAPP_OFICIAL_PRIVADO',\n fecha_inicio: new Date().toISOString(),\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_AUDIO',\n\n audio_count: 0,\n imagenes_count: 0,\n videos_count: 0,\n\n estado: 'ACTIVA',\n ejecucion_id: sessionId,\n motivo_revision: '',\n\n is_group: false,\n group_jid: '',\n group_name: '',\n sender_jid: senderPhone,\n sender_phone: senderPhone,\n sender_name: texto(evento.sender_name),\n\n country_code: texto(evento.country_code),\n pais_detectado: texto(evento.pais_detectado),\n idioma_flujo: idiomaFlujo,\n\n tipo_reporte: ''\n },\n\n resolver_sesion_debug: {\n modo: 'CHAT_PRIVADO_API_OFICIAL',\n idioma_final_usado: idiomaFlujo,\n pais_detectado: texto(evento.pais_detectado),\n country_code: texto(evento.country_code),\n manager_telefono: managerTelefono,\n accion,\n decision_flujo: decision,\n total_sesiones_leidas: sesiones.length,\n sesion_privada_activa_encontrada: Boolean(sesionActiva)\n }\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 48112, + 28992 + ], + "id": "445c260e-28e4-4df1-878e-3e3389d272e2", + "name": "Code - Resolver sesión WhatsApp TEST" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.decision_flujo === 'CREAR_SESION' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + }, + "id": "acec6506-977a-412d-b08a-fbcc8de4b0b8" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CREAR_SESION" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "5c9dcfa5-9b15-428c-ad80-c277b0671658", + "leftValue": "={{ $json.decision_flujo === 'AVISO_SESION_ACTIVA' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "AVISO_SESION_ACTIVA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "966402b5-4517-45c2-8a87-0a7aa4e5e682", + "leftValue": "={{ $json.decision_flujo === 'SIN_SESION_ACTIVA' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "SIN_SESION_ACTIVA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "698f3da3-29d2-472f-b625-af387891035e", + "leftValue": "={{ $json.decision_flujo === 'CANCELAR_SESION' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CANCELAR_SESION" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "d5feac06-4493-494e-ac1e-36c007f0de79", + "leftValue": "={{ $json.decision_flujo === 'SIN_SESION_PARA_CANCELAR' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "SIN_SESION_PARA_CANCELAR" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "950979b7-b930-4260-a392-52277a742e43", + "leftValue": "={{ $json.decision_flujo === 'CONTINUAR_SESION' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CONTINUAR_SESION" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ad9d6c34-b481-4d43-b680-26cbd0579601", + "leftValue": "={{ $json.decision_flujo === 'MOSTRAR_LEADERBOARD' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "MOSTRAR_LEADERBOARD" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 48336, + 28928 + ], + "id": "6d6ba7a9-452f-4ec0-8f23-cf5fadebfa71", + "name": "Switch - Decisión sesión WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $('Code - Resolver sesión WhatsApp TEST').first().json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.nueva_sesion?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst nombre =\n datos.manager_nombre ||\n datos.sender_name ||\n 'equipo';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n `Hello, *${nombre}*.`,\n '',\n '🎙️ *Current step: 1 of 3 — Voice note*',\n 'Send one voice note and clearly state whether the proposal is:',\n '',\n '1️⃣ *Executed Proposal* — already implemented.',\n '2️⃣ *External Proposal* — received outside the internal bank.',\n '',\n 'Include proposal/reference, brand/client, country, location, date, implementation and results.',\n '',\n '🖼️ Step 2: required images',\n '🎥 Step 3: optional videos',\n '',\n '🏆 Select *View ranking* to see the country leaderboard.',\n '',\n '⚠️ One proposal at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n `Hola, *${nombre}*.`,\n '',\n '🎙️ *Paso actual: 1 de 3 — Nota de voz*',\n 'Envía una nota de voz e indica claramente si la propuesta es:',\n '',\n '1️⃣ *Propuesta Ejecutada* — propuesta interna que ya fue implementada.',\n '2️⃣ *Propuesta Externa* — propuesta que no pertenece a la empresa pero que puede servir de inspiración en futuros proyectos.',\n '',\n '*Debes mencionar en la nota de voz:*',\n'',\n'- Nombre de la propuesta',\n'- Cliente',\n'- Marca',\n'- País',\n'- Lugar',\n'- Fecha de la ejecución',\n'- Detalles adicionales',\n\n '',\n '🖼️ Paso 2: imágenes obligatorias',\n '🎥 Paso 3: videos opcionales',\n '',\n '🏆 Selecciona *Ver ranking* si desea ver el leaderboard por país.',\n '',\n '⚠️ Una sola propuesta a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to:\n datos.whatsapp_to ||\n datos.group_id ||\n datos.group_jid ||\n datos.whatsapp_remote_jid,\n whatsapp_recipient_type:\n datos.whatsapp_recipient_type ||\n (datos.is_group ? 'group' : 'individual'),\n whatsapp_header: idioma === 'EN' ? 'GLM Evidence Registry' : 'Registro de Evidencias GLM',\n whatsapp_footer: 'GomezLee Marketing · Fulgencio',\n whatsapp_text: mensaje,\n whatsapp_buttons: [\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' }\n ]\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51088, + 25456 + ], + "id": "77a5874c-054e-4262-aea0-d634d5bfc2c7", + "name": "Code - Preparar bienvenida WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst etapa = String(datos.sesion_activa?.etapa || '').trim() || 'pendiente';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '⚠️ You already have an active proposal report in progress.',\n '',\n `Current step: ${etapa}`,\n '',\n 'Please complete that report before starting a new one.',\n '',\n 'To cancel it, use the cancel option.',\n ].join('\\n');\n} else {\n mensaje = [\n '⚠️ Ya tienes un reporte de propuesta en proceso.',\n '',\n `Etapa actual: ${etapa}`,\n '',\n 'Completa ese reporte antes de iniciar uno nuevo.',\n '',\n 'Para cancelarlo, usa la opción de cancelar.',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.whatsapp_remote_jid,\n\n whatsapp_header: idioma === 'EN' ? \"Active report\" : \"Reporte activo\",\n whatsapp_buttons: [\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51120, + 25904 + ], + "id": "9c674091-bd91-47f6-9da9-e394b3d8e728", + "name": "Code - Preparar aviso sesión activa WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.nueva_sesion.session_id }}", + "manager_telefono": "={{ $json.nueva_sesion.manager_telefono }}", + "manager_nombre": "={{ $json.nueva_sesion.manager_nombre }}", + "canal_origen": "={{ $json.nueva_sesion.canal_origen }}", + "fecha_inicio": "={{ $json.nueva_sesion.fecha_inicio }}", + "ultima_actividad": "={{ $json.nueva_sesion.ultima_actividad }}", + "etapa": "={{ $json.nueva_sesion.etapa }}", + "audio_count": "={{ $json.nueva_sesion.audio_count }}", + "imagenes_count": "={{ $json.nueva_sesion.imagenes_count }}", + "videos_count": "={{ $json.nueva_sesion.videos_count }}", + "estado": "={{ $json.nueva_sesion.estado }}", + "ejecucion_id": "={{ $json.nueva_sesion.ejecucion_id }}", + "motivo_revision": "={{ $json.nueva_sesion.motivo_revision }}", + "is_group": "={{ $json.nueva_sesion.is_group }}", + "group_jid": "={{ $json.nueva_sesion.group_jid }}", + "group_name": "={{ $json.nueva_sesion.group_name }}", + "sender_jid": "={{ $json.nueva_sesion.sender_jid }}", + "sender_phone": "={{ $json.nueva_sesion.sender_phone }}", + "sender_name": "={{ $json.nueva_sesion.sender_name }}", + "country_code": "={{ $json.nueva_sesion.country_code }}", + "pais_detectado": "={{ $json.nueva_sesion.pais_detectado }}", + "idioma_flujo": "={{ $json.nueva_sesion.idioma_flujo }}", + "tipo_reporte": "={{ $json.nueva_sesion.tipo_reporte }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 50880, + 25456 + ], + "id": "e1fda6f9-7fcc-478c-bd94-e56aa5253562", + "name": "Sheets - Crear sesión WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst etapa = texto(datos.sesion_activa?.etapa).toUpperCase();\nconst tipoMensaje = texto(datos.message_type).toLowerCase();\nconst accion = texto(datos.accion_flujo).toUpperCase();\n\nconst imagenesCount = Number(datos.sesion_activa?.imagenes_count || 0);\nconst videosCount = Number(datos.sesion_activa?.videos_count || 0);\n\nlet paso_decision = 'NO_DEFINIDO';\n\n// --------------------------------------------------\n// 1. Cancelación global\n// --------------------------------------------------\nif (accion === 'CANCELAR') {\n paso_decision = 'CANCELAR_SESION';\n\n// --------------------------------------------------\n// 2. Paso 1: esperando nota de voz\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_AUDIO') {\n if (tipoMensaje === 'audio') {\n paso_decision = 'GUARDAR_AUDIO';\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_AUDIO';\n } else {\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 3. Paso 2: esperando imágenes obligatorias\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_IMAGENES') {\n if (tipoMensaje === 'image') {\n paso_decision = 'GUARDAR_IMAGEN';\n\n } else if (accion === 'FOTOS_LISTAS') {\n // La validación real se hace después leyendo wa_ejecuciones_eventos.\n paso_decision = 'FOTOS_LISTAS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_IMAGENES';\n\n } else {\n // Si WhatsApp manda eventos raros durante imágenes, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 4. Paso 3: esperando videos opcionales\n// --------------------------------------------------\n} else if (etapa === 'ESPERANDO_VIDEOS') {\n if (tipoMensaje === 'video') {\n paso_decision = 'GUARDAR_VIDEO';\n\n } else if (accion === 'SIN_VIDEO') {\n paso_decision = 'CERRAR_SIN_VIDEO';\n\n } else if (accion === 'LISTO') {\n paso_decision = 'CERRAR_CON_VIDEOS';\n\n } else if (tipoMensaje === 'text') {\n paso_decision = 'PEDIR_VIDEOS';\n\n } else {\n // Si WhatsApp manda eventos raros durante videos, no responder.\n paso_decision = 'IGNORAR_EVENTO';\n }\n\n// --------------------------------------------------\n// 5. Sesión en procesamiento\n// --------------------------------------------------\n} else if (etapa === 'PROCESANDO') {\n paso_decision = 'AVISO_PROCESANDO';\n\n// --------------------------------------------------\n// 6. Etapa no reconocida\n// --------------------------------------------------\n} else {\n paso_decision = 'ETAPA_NO_RECONOCIDA';\n}\n\nreturn [\n {\n json: {\n ...datos,\n\n etapa_actual: etapa,\n tipo_mensaje_actual: tipoMensaje,\n accion_actual: accion,\n\n imagenes_count_actual: imagenesCount,\n videos_count_actual: videosCount,\n\n paso_decision,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 48640, + 29488 + ], + "id": "534d0221-5bf9-42fc-85ce-ccf79c03cd8c", + "name": "Code - Resolver paso activo WhatsApp TEST" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + }, + "id": "c7fef2d7-50ce-467a-b125-30ae7fba3a58" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "8da042cf-54e6-460f-b505-2b17c4f4ff3c", + "leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ecf52241-ef7e-4a9a-8823-b9e3b438da61", + "leftValue": "={{ $json.paso_decision === 'GUARDAR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "04433553-d0ee-4391-a4f4-926197ac4c0c", + "leftValue": "={{ $json.paso_decision === 'PEDIR_AUDIO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_AUDIO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "8637dc0f-59b6-40ac-8bc4-baf869829598", + "leftValue": "={{ $json.paso_decision === 'GUARDAR_IMAGEN' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_IMAGEN" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "4096e616-2517-48ab-b6c4-2712f763ec1d", + "leftValue": "={{ $json.paso_decision === 'FOTOS_LISTAS' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "FOTOS_LISTAS" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "d1dd1d5c-8e25-4147-876e-5e7227784f6f", + "leftValue": "={{ $json.paso_decision === 'PEDIR_IMAGENES' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_IMAGENES" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "bddbb16a-0463-47dd-9a6c-03c3635043f1", + "leftValue": "={{ $json.paso_decision === 'GUARDAR_VIDEO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "GUARDAR_VIDEO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "fe05ba35-0ccc-417a-a4e8-b907611932e4", + "leftValue": "={{ $json.paso_decision === 'CERRAR_SIN_VIDEO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CERRAR_SIN_VIDEO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "94647ac0-ee80-4c96-9f25-8a847704b6a0", + "leftValue": "={{ $json.paso_decision === 'CERRAR_CON_VIDEOS' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CERRAR_CON_VIDEOS" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "3038a656-50ae-4738-b2ff-a6276bd06522", + "leftValue": "={{ $json.paso_decision === 'PEDIR_VIDEOS' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "PEDIR_VIDEOS" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "1b20a524-2289-4d37-8372-9a839dadc971", + "leftValue": "={{ $json.paso_decision === 'AVISO_PROCESANDO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "AVISO_PROCESANDO" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "0943bbd0-f5b7-43c4-9b01-7f6bea244894", + "leftValue": "={{ $json.paso_decision === 'ETAPA_NO_RECONOCIDA' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ETAPA_NO_RECONOCIDA" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "84c163a3-3c08-4636-9104-c0e4c55b11e6", + "leftValue": "={{ $json.paso_decision === 'IGNORAR_EVENTO' }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "IGNORAR_EVENTO" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 48816, + 29680 + ], + "id": "f46f27d2-5404-4b35-a19a-ccb9981217a5", + "name": "Switch - Paso activo WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nconst idioma = String(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '🎙️ *Step 1 of 3 — Voice note*',\n '',\n 'I still need the required voice note to start the report.',\n '',\n 'Please send one voice note and clearly say whether this is:',\n '',\n '1. *Executed Proposal*',\n '2. *External Proposal*',\n '',\n 'Include:',\n '• Proposal/reference',\n '• Brand/client',\n '• Country',\n '• Location',\n '• Date',\n '• What was implemented or reported',\n '• Results/comments',\n '',\n '*Important:* One proposal/report at a time.'\n ].join('\\n');\n} else {\n mensaje = [\n '🎙️ *Paso 1 de 3 — Nota de voz*',\n '',\n 'Aún necesito la nota de voz obligatoria para iniciar el reporte.',\n '',\n 'Envía una sola nota de voz e indica claramente si es:',\n '',\n '1. *Propuesta Ejecutada*',\n '2. *Propuesta Externa*',\n '',\n 'Incluye:',\n '• Propuesta/referencia',\n '• Marca/cliente',\n '• País',\n '• Ubicación',\n '• Fecha',\n '• Qué se implementó o reportó',\n '• Resultados/comentarios',\n '',\n '*Importante:* Una sola propuesta o reporte a la vez.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid,\n\n whatsapp_header: idioma === 'EN' ? \"Step 1 of 3\" : \"Paso 1 de 3\",\n whatsapp_buttons: [\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51248, + 27696 + ], + "id": "79b0aeae-4f64-417a-a880-85a090efa4c4", + "name": "Code - Preparar solicitud audio WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "event_id": "={{ $json.event_id }}", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "message_type": "={{ $json.message_type }}", + "texto": "={{ $json.texto }}", + "comando": "={{ $json.accion_flujo }}", + "tiene_media": "={{ $json.tiene_media }}", + "media_count": "={{ $json.media_count }}", + "raw_preview": "={{ $json.raw_preview }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_procesado": "={{ $json.fecha_procesado }}", + "session_id": "={{ $json.session_id }}", + "canal_origen": "={{ $json.canal_origen }}", + "etapa_recibida": "={{ $json.etapa_recibida }}", + "media_source_id": "={{ $json.media_source_id }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "event_id", + "displayName": "event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "message_type", + "displayName": "message_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "texto", + "displayName": "texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comando", + "displayName": "comando", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tiene_media", + "displayName": "tiene_media", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_count", + "displayName": "media_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "raw_preview", + "displayName": "raw_preview", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_procesado", + "displayName": "fecha_procesado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa_recibida", + "displayName": "etapa_recibida", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_source_id", + "displayName": "media_source_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 50960, + 27312 + ], + "id": "21e1f693-fbdf-4068-9bfe-22d52f28a62b", + "name": "Sheets - Guardar evento audio WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const datos = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = datos.sesion_activa || datos.nueva_sesion || {};\n\nreturn [\n {\n json: {\n ...datos,\n\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'ACTIVA',\n\n audio_count: Number(sesion.audio_count || 0) + 1,\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n manager_telefono: datos.manager_telefono || sesion.manager_telefono || datos.sender_phone || '',\n manager_nombre: datos.manager_nombre || sesion.manager_nombre || datos.sender_name || '',\n\n is_group: datos.is_group ?? sesion.is_group ?? false,\n group_jid: datos.group_jid || sesion.group_jid || '',\n group_name: datos.group_name || sesion.group_name || '',\n sender_jid: datos.sender_jid || sesion.sender_jid || '',\n sender_phone: datos.sender_phone || sesion.sender_phone || datos.manager_telefono || '',\n sender_name: datos.sender_name || sesion.sender_name || datos.manager_nombre || '',\n\n country_code: datos.country_code || sesion.country_code || '',\n pais_detectado: datos.pais_detectado || sesion.pais_detectado || '',\n idioma_flujo: datos.idioma_flujo || sesion.idioma_flujo || 'ES',\n\n tipo_reporte: sesion.tipo_reporte || datos.tipo_reporte || '',\n\n whatsapp_to: datos.whatsapp_to || datos.group_jid || datos.whatsapp_remote_jid || ''\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51168, + 27312 + ], + "id": "9e17aaf9-24d6-44bd-9bff-42cb077d393f", + "name": "Code - Preparar actualización audio recibido WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "estado": "={{ $json.estado }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "is_group": "={{ $json.is_group }}", + "group_jid": "={{ $json.group_jid }}", + "group_name": "={{ $json.group_name }}", + "sender_jid": "={{ $json.sender_jid }}", + "sender_phone": "={{ $json.sender_phone }}", + "sender_name": "={{ $json.sender_name }}", + "country_code": "={{ $json.country_code }}", + "pais_detectado": "={{ $json.pais_detectado }}", + "idioma_flujo": "={{ $json.idioma_flujo }}", + "tipo_reporte": "={{ $json.tipo_reporte }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "ejecucion_id": "={{ $json.ejecucion_id }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 51376, + 27312 + ], + "id": "89a6f88f-f8c9-4530-afd0-2fa1fbf2d7e6", + "name": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const datos = $('Code - Preparar actualización audio recibido WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(\n datos.idioma_flujo ||\n datos.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ Voice note received.',\n '',\n '*Step 2 of 3 — Required images*',\n '',\n 'Now send one or more photos related to the proposal.',\n '',\n 'You can send multiple images together or in separate messages.',\n '',\n 'When you finish sending images, select: Photos ready.'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ Nota de voz recibida.',\n '',\n '*Paso 2 de 3 — Imágenes obligatorias*',\n '',\n 'Ahora envía una o varias fotos relacionadas a la propuesta.',\n '',\n 'Puedes enviar varias imágenes juntas o en mensajes separados.',\n '',\n 'Cuando termines de enviar las imágenes, selecciona: Fotos listas.'\n ].join('\\n');\n}\n\nconst whatsappTo = limpiar(\n datos.whatsapp_to ||\n datos.group_jid ||\n datos.whatsapp_remote_jid ||\n datos.sender_jid ||\n ''\n);\n\nif (!whatsappTo) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar solicitud de imágenes.');\n}\n\nreturn [\n {\n json: {\n ...datos,\n whatsapp_to: whatsappTo,\n whatsapp_header: '',\n whatsapp_buttons: [\n { id: 'FOTOS_LISTAS', title: idioma === 'EN' ? 'Photos ready' : 'Fotos listas' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje,\n texto_respuesta: mensaje,\n mensaje,\n text: mensaje,\n message_text: mensaje,\n estado_mensaje: 'SOLICITUD_IMAGENES_PREPARADA',\n solicitud_imagenes_debug: {\n idioma,\n whatsapp_to: whatsappTo,\n permite_imagenes_juntas: true\n }\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51600, + 27312 + ], + "id": "8ee26380-25a4-40f4-9024-026be1751a0b", + "name": "Code - Preparar solicitud imágenes WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const datos = $json || {};\n\nreturn [\n {\n json: {\n ...datos,\n session_id: datos.session_id,\n ultima_actividad: new Date().toISOString(),\n etapa: 'CANCELADA',\n estado: 'CANCELADA',\n motivo_revision: 'CANCELADO_POR_USUARIO',\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 50992, + 26720 + ], + "id": "c657b6f2-9946-46aa-92b9-bd2f4b3b9127", + "name": "Code - Preparar cancelación sesión WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "estado": "={{ $json.estado }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 51200, + 26720 + ], + "id": "cd2e53ef-91a9-404c-802e-e122421868bf", + "name": "Sheets - Cancelar sesión WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver sesión WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\nconst digits = (valor) => limpiar(valor).replace(/\\D/g, '');\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst whatsappTo = digits(\n base.whatsapp_to ||\n actual.whatsapp_to ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone\n);\n\nif (!whatsappTo) {\n throw new Error('No se encontró el número privado para confirmar la cancelación.');\n}\n\nconst mensaje = idioma === 'EN'\n ? [\n '✅ *Report cancelled successfully.*',\n '',\n 'The current report was closed and no additional evidence will be added to that session.',\n '',\n 'You can start another report whenever you are ready.'\n ].join('\\n')\n : [\n '✅ *Reporte cancelado correctamente.*',\n '',\n 'El reporte actual fue cerrado y no se agregará más evidencia a esa sesión.',\n '',\n 'Puedes iniciar otro reporte cuando estés listo.'\n ].join('\\n');\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n is_group: false,\n group_id: '',\n group_jid: '',\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: 'individual',\n whatsapp_header: idioma === 'EN' ? 'Report cancelled' : 'Reporte cancelado',\n whatsapp_text: mensaje,\n whatsapp_buttons: [\n { id: 'HEY', title: idioma === 'EN' ? 'Start report' : 'Iniciar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ]\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51456, + 26656 + ], + "id": "a3c37ea4-2d7a-4530-9e6d-13d309a6f039", + "name": "Code - Preparar mensaje cancelación WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\n\n const esGrupo = Boolean(\n actual.is_group ||\n base.is_group ||\n sesion.is_group ||\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid\n );\n\n if (esGrupo) {\n return limpiar(\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n raw\n );\n }\n\n return raw.replace(/\\D/g, '');\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '📸 I still need at least one image.',\n '',\n 'The voice note was already received, but one evidence photo is required to complete the report.',\n '',\n '⚠️ Send at least one image.',\n 'You may send images together or in separate messages.',\n '',\n 'When you finish sending images, write: PHOTOS READY'\n ].join('\\n');\n} else {\n mensaje = [\n '📸 Necesito que me envíes al menos una imagen.',\n '',\n 'La nota de voz ya fue recibida, pero para completar el reporte hace falta una foto de evidencia.',\n '',\n '⚠️ Por favor envía al menos una imagen.',\n 'Puedes enviarlas juntas o en mensajes separados.',\n '',\n 'Cuando termines de enviar las imágenes, escribe: FOTOS LISTAS'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n whatsapp_header: idioma === 'EN' ? 'Image required' : 'Imagen requerida',\n whatsapp_buttons: [\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51968, + 29744 + ], + "id": "6bdca504-2ddf-4c68-972b-c59e6fd06d70", + "name": "Code - Preparar aviso falta imagen WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || base.nueva_sesion || {};\n\nconst rows = $input.all().map(item => item.json || {});\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst normalizarNumero = (valor) =>\n texto(valor).replace(/\\D/g, '');\n\nconst sessionId = texto(base.session_id || sesion.session_id);\nconst ejecucionId = texto(sesion.ejecucion_id || base.ejecucion_id || sessionId);\n\nconst ahora = new Date().toISOString();\n\nconst fechaInicioSesion = texto(\n sesion.fecha_inicio ||\n base.fecha_inicio ||\n base.sesion_activa?.fecha_inicio ||\n ''\n);\n\nconst fechaInicioMs = fechaInicioSesion\n ? new Date(fechaInicioSesion).getTime()\n : 0;\n\nconst fechaAhoraMs = Date.now();\n\nconst groupJid = texto(\n base.group_jid ||\n sesion.group_jid ||\n base.whatsapp_remote_jid ||\n ''\n);\n\nconst managerTelefono = normalizarNumero(\n base.manager_telefono ||\n base.sender_phone ||\n sesion.manager_telefono ||\n sesion.sender_phone ||\n ''\n);\n\n// Ventana máxima de seguridad para esta sesión.\n// Así no mezclamos fotos viejas del mismo grupo/persona.\n// 45 minutos es suficiente para una ejecución normal.\nconst ventanaMaximaMs = 45 * 60 * 1000;\n\nconst imagenesUnicas = [];\nconst vistos = new Set();\n\nfor (const row of rows) {\n const tipo = texto(row.message_type).toLowerCase();\n\n if (tipo !== 'image') continue;\n\n const rowSessionId = texto(row.session_id);\n const rowGroupJid = texto(row.whatsapp_remote_jid);\n const rowManager = normalizarNumero(row.manager_telefono || row.sender_phone || '');\n\n const rowFecha = texto(row.fecha_recepcion || row.fecha_procesado || '');\n const rowFechaMs = rowFecha ? new Date(rowFecha).getTime() : 0;\n\n const mismaSesion = sessionId && rowSessionId === sessionId;\n\n const mismoGrupoYManager =\n groupJid &&\n rowGroupJid === groupJid &&\n managerTelefono &&\n rowManager === managerTelefono;\n\n const dentroDeVentana =\n rowFechaMs &&\n fechaInicioMs &&\n rowFechaMs >= fechaInicioMs - 15000 &&\n rowFechaMs <= fechaAhoraMs + 15000 &&\n rowFechaMs - fechaInicioMs <= ventanaMaximaMs;\n\n const perteneceALaEjecucion =\n mismaSesion ||\n (mismoGrupoYManager && dentroDeVentana);\n\n if (!perteneceALaEjecucion) continue;\n\n const mediaId = texto(row.media_source_id || row.event_id);\n\n if (!mediaId) continue;\n if (vistos.has(mediaId)) continue;\n\n vistos.add(mediaId);\n imagenesUnicas.push(row);\n}\n\nconst countSesion = Number(sesion.imagenes_count || base.imagenes_count_actual || 0);\n\nconst imagenesCountReal = imagenesUnicas.length > 0\n ? imagenesUnicas.length\n : countSesion;\n\nconst idsImagenes = imagenesUnicas\n .map(row => texto(row.media_source_id || row.event_id))\n .filter(Boolean);\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n ultima_actividad: ahora,\n\n etapa: 'ESPERANDO_VIDEOS',\n estado: 'ACTIVA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: imagenesCountReal,\n videos_count: Number(sesion.videos_count || 0),\n\n manager_telefono: base.manager_telefono || sesion.manager_telefono || base.sender_phone || '',\n manager_nombre: base.manager_nombre || sesion.manager_nombre || base.sender_name || '',\n\n is_group: base.is_group ?? sesion.is_group ?? false,\n group_jid: base.group_jid || sesion.group_jid || '',\n group_name: base.group_name || sesion.group_name || '',\n\n sender_jid: base.sender_jid || sesion.sender_jid || '',\n sender_phone: base.sender_phone || sesion.sender_phone || base.manager_telefono || '',\n sender_name: base.sender_name || sesion.sender_name || base.manager_nombre || '',\n\n country_code: base.country_code || sesion.country_code || '',\n pais_detectado: base.pais_detectado || sesion.pais_detectado || '',\n idioma_flujo: base.idioma_flujo || sesion.idioma_flujo || 'ES',\n\n tipo_reporte: sesion.tipo_reporte || base.tipo_reporte || '',\n\n whatsapp_to: base.whatsapp_to || base.group_jid || base.whatsapp_remote_jid || '',\n\n imagenes_eventos_detectados: imagenesUnicas.length,\n imagenes_media_source_ids: idsImagenes.join(','),\n\n debug_fotos_listas: {\n session_id_actual: sessionId,\n fecha_inicio_sesion: fechaInicioSesion,\n group_jid: groupJid,\n manager_telefono: managerTelefono,\n total_rows_leidas: rows.length,\n total_imagenes_detectadas: imagenesUnicas.length,\n ids_imagenes: idsImagenes\n }\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 52592, + 28896 + ], + "id": "fb8ca667-903b-4384-9cf3-d5ef5e82216a", + "name": "Preparar actualización fotos listas" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "is_group": "={{ $json.is_group }}", + "group_jid": "={{ $json.group_jid }}", + "group_name": "={{ $json.group_name }}", + "sender_jid": "={{ $json.sender_jid }}", + "sender_phone": "={{ $json.sender_phone }}", + "sender_name": "={{ $json.sender_name }}", + "country_code": "={{ $json.country_code }}", + "pais_detectado": "={{ $json.pais_detectado }}", + "idioma_flujo": "={{ $json.idioma_flujo }}", + "tipo_reporte": "={{ $json.tipo_reporte }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "canal_origen": "={{ $json.canal_origen }}", + "fecha_inicio": "={{ $json.fecha_inicio }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 52784, + 28896 + ], + "id": "081d8dfd-967d-42aa-ba40-f09423d57285", + "name": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst idioma = String(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase = String(\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n ''\n).trim();\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar solicitud de videos.');\n}\n\nconst esGrupo = Boolean(\n actual.is_group ||\n base.is_group ||\n sesion.is_group ||\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n destinoBase\n ).trim()\n : destinoBase.replace(/\\D/g, '');\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '*Step 3 of 3 — Optional videos*',\n '',\n 'You can send one or more videos of the proposal if you have them.',\n '',\n '⚠️ Important: send videos one by one, in separate messages.',\n '',\n 'If you do not have videos, select: No video',\n '',\n 'If you already sent videos and finished, select: Done',\n ].join('\\n');\n} else {\n mensaje = [\n '*Paso 3 de 3 — Videos opcionales*',\n '',\n 'Puedes enviar uno o varios videos de la propuesta si tienes.',\n '',\n '⚠️ Importante: envía los videos uno por uno, en mensajes separados.',\n '',\n 'Si no tienes videos, selecciona: Sin video',\n '',\n 'Si enviaste videos y ya terminaste, selecciona: Listo',\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n session_id: actual.session_id || base.session_id || sesion.session_id || '',\n ejecucion_id: actual.ejecucion_id || base.ejecucion_id || sesion.ejecucion_id || '',\n manager_telefono: actual.manager_telefono || base.manager_telefono || sesion.manager_telefono || '',\n manager_nombre: actual.manager_nombre || base.manager_nombre || sesion.manager_nombre || '',\n is_group: actual.is_group ?? base.is_group ?? sesion.is_group ?? false,\n group_jid: actual.group_jid || base.group_jid || sesion.group_jid || '',\n sender_phone: actual.sender_phone || base.sender_phone || sesion.sender_phone || '',\n idioma_flujo: idioma,\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n whatsapp_header: '',\n whatsapp_buttons: [\n { id: 'SIN_VIDEO', title: idioma === 'EN' ? 'No video' : 'Sin video' },\n { id: 'LISTO', title: idioma === 'EN' ? 'Done' : 'Listo' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' }\n ],\n whatsapp_text: mensaje,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 53008, + 28896 + ], + "id": "829fa486-382e-4859-8b2d-cabbbeb78618", + "name": "Code - Preparar solicitud videos WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\n\n const esGrupo = Boolean(\n actual.is_group ||\n base.is_group ||\n sesion.is_group ||\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid\n );\n\n if (esGrupo) {\n return limpiar(\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n raw\n );\n }\n\n return raw.replace(/\\D/g, '');\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n base.whatsapp_to ||\n base.group_jid ||\n actual.whatsapp_to ||\n actual.group_jid ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '🎥 We are already on the optional videos step.',\n '',\n 'You can send one or more videos of the execution.',\n '',\n '⚠️ Remember: send each video in a separate message.',\n 'Do not send multiple videos together.',\n '',\n 'If you do not have videos, write: *NO VIDEO*',\n '',\n 'If you already sent videos and finished, write: *DONE*'\n ].join('\\n');\n} else {\n mensaje = [\n '🎥 Ya estamos en el paso de videos opcionales.',\n '',\n 'Puedes enviar uno o varios videos de la ejecución.',\n '',\n '⚠️ Recuerda: envía cada video en un mensaje separado.',\n 'No envíes varios videos juntos.',\n '',\n 'Si no tienes videos, escribe: *SIN VIDEO*',\n '',\n 'Si ya enviaste videos y terminaste, escribe: *LISTO*'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n whatsapp_header: idioma === 'EN' ? 'Optional videos' : 'Videos opcionales',\n whatsapp_buttons: [\n { id: 'SIN_VIDEO', title: idioma === 'EN' ? 'No video' : 'Sin video' },\n { id: 'LISTO', title: idioma === 'EN' ? 'Done' : 'Listo' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' }\n ],\n whatsapp_text: mensaje\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 50096, + 31568 + ], + "id": "f1c0b52a-70c5-4e4c-87fe-63e4313d51fa", + "name": "Code - Preparar recordatorio videos WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst esGrupo = Boolean(\n base.is_group ||\n sesion.is_group ||\n base.group_id ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n base.group_id ||\n base.group_jid ||\n sesion.group_jid ||\n telefonoBase\n ).trim()\n : telefonoBase.replace(/\\D/g, '');\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'SIN_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 49744, + 30960 + ], + "id": "7ef84b4c-feda-4e5b-beea-3db1e93a961c", + "name": "CERRAR_SIN_VIDEO" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 49952, + 30960 + ], + "id": "24ffbf6a-2bce-49fb-99da-14f9d66379b0", + "name": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "event_id": "={{ $json.event_id }}", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "message_type": "={{ $json.message_type }}", + "texto": "={{ $json.texto }}", + "comando": "={{ $json.accion_flujo }}", + "tiene_media": "={{ $json.tiene_media }}", + "media_count": "={{ $json.media_count }}", + "raw_preview": "={{ $json.raw_preview }}", + "estado": "=VIDEO_RECIBIDO", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "session_id": "={{ $json.session_id }}", + "canal_origen": "={{ $json.canal_origen }}", + "etapa_recibida": "={{ $json.etapa_actual }}", + "media_source_id": "={{ $json.media_source_id }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "event_id", + "displayName": "event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "message_type", + "displayName": "message_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "texto", + "displayName": "texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comando", + "displayName": "comando", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tiene_media", + "displayName": "tiene_media", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_count", + "displayName": "media_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "raw_preview", + "displayName": "raw_preview", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_procesado", + "displayName": "fecha_procesado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa_recibida", + "displayName": "etapa_recibida", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_source_id", + "displayName": "media_source_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 52096, + 29984 + ], + "id": "24e27426-8bff-4d9a-8326-37c0300abd49", + "name": "Sheets - Guardar evento video WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const evento = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst videosActuales = Number(sesion.videos_count || 0);\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n evento.whatsapp_to ||\n evento.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst esGrupo = Boolean(\n evento.is_group ||\n base.is_group ||\n sesion.is_group ||\n evento.group_id ||\n base.group_id ||\n evento.group_jid ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n evento.group_id ||\n base.group_id ||\n evento.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n telefonoBase\n ).trim()\n : telefonoBase.replace(/\\D/g, '');\n\nreturn [\n {\n json: {\n ...base,\n ...evento,\n\n session_id: base.session_id || sesion.session_id || evento.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || evento.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || evento.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n // Seguimos en videos hasta que el usuario escriba LISTO\n etapa: 'ESPERANDO_VIDEOS',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: videosActuales + 1,\n\n estado: sesion.estado || 'ACTIVA',\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: sesion.motivo_revision || '',\n\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 52320, + 29920 + ], + "id": "389d9bf0-02a8-45ba-9658-7f2ad1da3bd1", + "name": "Code - Preparar actualización video recibido WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "estado": "={{ $json.estado }}", + "videos_count": "={{ $json.videos_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "audio_count": "={{ $json.audio_count }}", + "etapa": "={{ $json.etapa }}", + "ultima_actividad": "={{ $json.ultima_actividad }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 52528, + 29920 + ], + "id": "7c94f5bf-fc11-495f-a4a0-684a79c3d7f3", + "name": "Sheets - Actualizar sesión video recibido WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarDestino = (valor) => {\n const raw = limpiar(valor);\n if (!raw) return '';\n\n const esGrupo = Boolean(\n actual.is_group ||\n base.is_group ||\n sesion.is_group ||\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid\n );\n\n if (esGrupo) {\n return limpiar(\n actual.group_id ||\n base.group_id ||\n actual.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n raw\n );\n }\n\n return raw.replace(/\\D/g, '');\n};\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst destinoBase =\n actual.whatsapp_to ||\n actual.group_jid ||\n actual.whatsapp_remote_jid ||\n base.whatsapp_to ||\n base.group_jid ||\n base.whatsapp_remote_jid ||\n sesion.group_jid ||\n sesion.whatsapp_to ||\n sesion.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone ||\n '';\n\nif (!destinoBase) {\n throw new Error('No se encontró destino WhatsApp para enviar confirmación de video.');\n}\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = [\n '✅ Video received.',\n '',\n 'You can send more videos if needed.',\n '',\n '⚠️ Remember: send each video in a separate message.',\n 'Do not send multiple videos together.',\n '',\n 'When you finish sending videos, write: DONE',\n '',\n 'If you do not have more videos, you can also write: DONE'\n ].join('\\n');\n} else {\n mensaje = [\n '✅ Video recibido.',\n '',\n 'Puedes enviar más videos si hace falta.',\n '',\n '⚠️ Recuerda: envía cada video en un mensaje separado.',\n 'No envíes varios videos juntos.',\n '',\n 'Cuando termines de enviar los videos, escribe: LISTO',\n '',\n 'Si no tienes más videos, también puedes escribir: LISTO'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n whatsapp_to: normalizarDestino(destinoBase),\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n whatsapp_header: idioma === 'EN' ? 'Video received' : 'Video recibido',\n whatsapp_buttons: [\n { id: 'LISTO', title: idioma === 'EN' ? 'Done' : 'Listo' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensaje\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 52736, + 29920 + ], + "id": "e8d56ac9-d966-42a9-82bf-4c30edc263c7", + "name": "Code - Preparar confirmación video WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst ahora = new Date().toISOString();\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst esGrupo = Boolean(\n base.is_group ||\n sesion.is_group ||\n base.group_id ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n base.group_id ||\n base.group_jid ||\n sesion.group_jid ||\n telefonoBase\n ).trim()\n : telefonoBase.replace(/\\D/g, '');\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || sesion.session_id || '',\n manager_telefono: sesion.manager_telefono || base.manager_telefono || '',\n manager_nombre: sesion.manager_nombre || base.manager_nombre || '',\n canal_origen: sesion.canal_origen || base.canal_origen || 'WHATSAPP',\n\n fecha_inicio: sesion.fecha_inicio || '',\n ultima_actividad: ahora,\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_RECUPERAR_MEDIA',\n\n audio_count: Number(sesion.audio_count || 0),\n imagenes_count: Number(sesion.imagenes_count || 0),\n videos_count: Number(sesion.videos_count || 0),\n\n ejecucion_id: sesion.ejecucion_id || base.ejecucion_id || base.session_id || '',\n motivo_revision: 'CON_VIDEO_REPORTADO',\n\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 49664, + 31248 + ], + "id": "acece944-d7e8-432b-b614-6b0603fe703a", + "name": "Code - Preparar cierre con videos WhatsApp TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "etapa": "={{ $json.etapa }}", + "audio_count": "={{ $json.audio_count }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "videos_count": "={{ $json.videos_count }}", + "estado": "={{ $json.estado }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 49872, + 31248 + ], + "id": "993b75c7-f768-4761-b63e-b08694d2ab21", + "name": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const data = $json || {};\nconst base = $('Code - Resolver paso activo WhatsApp TEST').first().json || {};\nconst sesion = base.sesion_activa || {};\n\nconst telefonoBase = String(\n base.whatsapp_to ||\n base.whatsapp_remote_jid ||\n data.whatsapp_to ||\n data.whatsapp_remote_jid ||\n sesion.manager_telefono ||\n data.manager_telefono ||\n base.manager_telefono ||\n ''\n).trim();\n\nconst esGrupo = Boolean(\n data.is_group ||\n base.is_group ||\n sesion.is_group ||\n data.group_id ||\n base.group_id ||\n data.group_jid ||\n base.group_jid ||\n sesion.group_jid\n);\n\nconst whatsappTo = esGrupo\n ? String(\n data.group_id ||\n base.group_id ||\n data.group_jid ||\n base.group_jid ||\n sesion.group_jid ||\n telefonoBase\n ).trim()\n : telefonoBase.replace(/\\D/g, '');\n\nreturn [\n {\n json: {\n ...base,\n ...data,\n\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: esGrupo ? 'group' : 'individual',\n\n whatsapp_text: `⏳ Tu reporte ya fue recibido y está pendiente de procesamiento.\n\nPor favor espera mientras se prepara el análisis de la evidencia.`\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 50160, + 31808 + ], + "id": "d3384354-e562-4f3d-9510-6fe0c226a503", + "name": "Code - Preparar aviso procesando WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nreturn [\n {\n json: {\n ...data,\n\n session_id_busqueda: data.session_id || '',\n estado_procesamiento: 'BUSCAR_EVENTOS_MEDIA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 50928, + 31168 + ], + "id": "c6c15e2a-25ee-4fb7-93f5-b1b675cca80a", + "name": "Code - Preparar búsqueda eventos media WhatsApp TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 51184, + 31168 + ], + "id": "f8a25b7b-025d-4b53-bab6-ec5eeb8d47df", + "name": "Sheets - Leer eventos WhatsApp TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const cierre = $('Code - Preparar búsqueda eventos media WhatsApp TEST').first().json || {};\nconst sessionId = String(cierre.session_id_busqueda || cierre.session_id || '').trim();\n\nconst eventos = $input.all().map(item => item.json || {});\n\nconst eventosSesion = eventos.filter(evento => {\n return String(evento.session_id || '').trim() === sessionId;\n});\n\nconst eventosMedia = eventosSesion.filter(evento => {\n const tipo = String(evento.message_type || '').toLowerCase();\n return ['audio', 'image', 'video'].includes(tipo);\n});\n\nconst audios = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'audio');\nconst imagenes = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'image');\nconst videos = eventosMedia.filter(e => String(e.message_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...cierre,\n\n total_eventos_sesion: eventosSesion.length,\n total_media: eventosMedia.length,\n\n audio_count_real: audios.length,\n imagenes_count_real: imagenes.length,\n videos_count_real: videos.length,\n\n eventos_audio: audios,\n eventos_imagenes: imagenes,\n eventos_videos: videos,\n\n media_lista: eventosMedia,\n\n estado_procesamiento: 'EVENTOS_MEDIA_FILTRADOS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51424, + 31168 + ], + "id": "07afda02-eaa5-4117-ae00-f4a5babfe503", + "name": "Code - Filtrar eventos media de sesión WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\nconst mediaLista = Array.isArray(data.media_lista) ? data.media_lista : [];\n\nconst sessionId = String(data.session_id || data.session_id_busqueda || '').trim();\nconst ejecucionId = String(data.ejecucion_id || sessionId).trim();\n\nconst items = mediaLista.map((media, index) => {\n const tipo = String(media.message_type || '').toLowerCase();\n const mime = String(media.media_mime_type || '').trim();\n\n let extension = 'bin';\n\n if (mime.includes('ogg')) extension = 'ogg';\n else if (mime.includes('mpeg')) extension = 'mp3';\n else if (mime.includes('mp4')) extension = 'mp4';\n else if (mime.includes('jpeg') || mime.includes('jpg')) extension = 'jpg';\n else if (mime.includes('png')) extension = 'png';\n else if (tipo === 'audio') extension = 'ogg';\n else if (tipo === 'image') extension = 'jpg';\n else if (tipo === 'video') extension = 'mp4';\n\n const mediaSourceId = String(\n media.media_source_id ||\n media.event_id ||\n ''\n ).trim();\n\n const remoteJid = String(\n media.whatsapp_remote_jid ||\n data.whatsapp_remote_jid ||\n data.whatsapp_to ||\n media.manager_telefono ||\n data.manager_telefono ||\n ''\n ).trim();\n\n return {\n json: {\n ...data,\n\n media_index: index + 1,\n media_total: mediaLista.length,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: media.event_id || '',\n media_source_id: mediaSourceId,\n media_type: tipo,\n media_mime_type: mime,\n media_extension: extension,\n\n whatsapp_remote_jid: remoteJid,\n\n media_file_name: `${ejecucionId}_${tipo}_${index + 1}.${extension}`,\n\n evento_original: media,\n\n estado_procesamiento: 'MEDIA_ITEM_PREPARADO'\n }\n };\n});\n\nreturn items;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51664, + 31168 + ], + "id": "39734cec-0ee3-4874-a668-274d0c8fb38f", + "name": "Code - Separar media en items WhatsApp TEST" + }, + { + "parameters": { + "resource": "media", + "operation": "mediaUrlGet", + "mediaGetId": "={{ $json.media_source_id }}" + }, + "type": "n8n-nodes-base.whatsApp", + "typeVersion": 1.1, + "position": [ + 51872, + 31168 + ], + "id": "84e2ae15-dd4b-4487-abc7-3c500bc2f1fa", + "name": "HTTP - Obtener metadata media Meta WhatsApp TEST", + "alwaysOutputData": true, + "webhookId": "1e8de0c8-0d81-4233-9eb1-61449b259ce5", + "credentials": { + "whatsAppApi": { + "id": "t14kVayc9FurLReq", + "name": "WhatsApp API - GLM CDC" + } + } + }, + { + "parameters": { + "jsCode": "const downloadedItems = $input.all();\nconst mediaItems = $('Code - Preparar descarga media Meta WhatsApp TEST').all();\n\nreturn downloadedItems.map((item, index) => {\n const media = mediaItems[index]?.json || mediaItems[0]?.json || {};\n const binary = item.binary || {};\n\n if (!binary.data) {\n throw new Error(`No llegó binary.data para media_source_id: ${media.media_source_id || 'SIN_ID'}`);\n }\n\n binary.data.mimeType =\n media.media_mime_type ||\n binary.data.mimeType ||\n 'application/octet-stream';\n\n binary.data.fileName =\n media.media_file_name ||\n binary.data.fileName ||\n `media_${media.media_source_id || Date.now()}`;\n\n return {\n json: {\n ...media,\n estado_procesamiento: 'MEDIA_OFICIAL_DESCARGADA'\n },\n binary\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 52384, + 31168 + ], + "id": "7f59f571-ae44-4646-b635-4e1d19ac24a3", + "name": "Code - Restaurar contexto binario Meta WhatsApp TEST" + }, + { + "parameters": { + "name": "={{ $json.media_file_name }}", + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "=1G11WZnOwPp7UC2moPE6rEvlPFl7_OoSB", + "mode": "id" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 52320, + 31168 + ], + "id": "253c7221-838d-41ea-b1d0-b2c4bec6fdab", + "name": "Drive - Subir media WhatsApp TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const driveItems = $input.all();\nconst mediaItems = $('Code - Restaurar contexto binario Meta WhatsApp TEST').all();\n\nreturn driveItems.map((item, index) => {\n const drive = item.json || {};\n const media = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n const fileId =\n drive.id ||\n drive.fileId ||\n drive.file_id ||\n '';\n\n const webViewLink =\n drive.webViewLink ||\n drive.webContentLink ||\n (fileId ? `https://drive.google.com/file/d/${fileId}/view` : '');\n\n return {\n json: {\n ...media,\n\n drive_file_id: fileId,\n drive_file_name: media.media_file_name,\n drive_file_url: webViewLink,\n\n estado_procesamiento: 'MEDIA_GUARDADA_EN_DRIVE'\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 52528, + 31168 + ], + "id": "ffd194c9-020d-4911-8927-4c4061dddfd1", + "name": "Code - Preparar registro media Drive WhatsApp TEST" + }, + { + "parameters": { + "jsCode": "const mediaItems = $('Code - Preparar registro media Drive WhatsApp TEST').all();\n\nconst media = mediaItems.map(item => item.json || {});\n\nconst base = media[0] || {};\n\nconst audios = media.filter(m => String(m.media_type || '').toLowerCase() === 'audio');\nconst imagenes = media.filter(m => String(m.media_type || '').toLowerCase() === 'image');\nconst videos = media.filter(m => String(m.media_type || '').toLowerCase() === 'video');\n\nreturn [\n {\n json: {\n ...base,\n\n session_id: base.session_id || '',\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n\n audio_drive_urls: audios.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n imagenes_drive_urls: imagenes.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n videos_drive_urls: videos.map(m => m.drive_file_url).filter(Boolean).join('\\n'),\n\n total_media_drive: media.length,\n total_audio_drive: audios.length,\n total_imagenes_drive: imagenes.length,\n total_videos_drive: videos.length,\n\n ultima_actividad: new Date().toISOString(),\n\n etapa: 'PROCESANDO',\n estado: 'LISTO_PARA_ANALIZAR',\n motivo_revision: 'MEDIA_RECUPERADA',\n\n estado_procesamiento: 'MEDIA_RECUPERADA_Y_REGISTRADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 52944, + 31168 + ], + "id": "50b617f4-9f9c-4790-8621-3c12588d1ea8", + "name": "Code - Consolidar media recuperada WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1962428033, + "mode": "list", + "cachedResultName": "wa_ejecuciones_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1962428033" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "media_event_id": "={{ $json.media_event_id }}", + "media_type": "={{ $json.media_type }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.drive_file_name }}", + "drive_file_id": "={{ $json.drive_file_id }}", + "drive_file_url": "={{ $json.drive_file_url }}", + "fecha_guardado": "={{ new Date().toISOString() }}", + "estado": "=MEDIA_GUARDADA_EN_DRIVE", + "media_index": "={{ $json.media_index }}", + "media_total": "={{ $json.media_total }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_event_id", + "displayName": "media_event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_type", + "displayName": "media_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "drive_file_id", + "displayName": "drive_file_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "drive_file_url", + "displayName": "drive_file_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_guardado", + "displayName": "fecha_guardado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_index", + "displayName": "media_index", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_total", + "displayName": "media_total", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 52736, + 31168 + ], + "id": "61b15082-007c-44df-8dde-d720a19c4167", + "name": "Sheets - Guardar media Drive WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "estado": "={{ $json.estado }}", + "etapa": "={{ $json.etapa }}", + "ultima_actividad": "={{ $json.ultima_actividad }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 53152, + 31168 + ], + "id": "38218f3d-b442-4b98-8832-bb77213fc943", + "name": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const data = $('Code - Consolidar media recuperada WhatsApp TEST').first().json || {};\n\nconst audioUrls = String(data.audio_drive_urls || '').trim();\nconst imagenesUrls = String(data.imagenes_drive_urls || '').trim();\nconst videosUrls = String(data.videos_drive_urls || '').trim();\n\nconst prompt = `\nAnaliza esta ejecución de propuesta usando la evidencia disponible.\n\nIMPORTANTE:\n- La nota de voz contiene la descripción principal de la ejecución.\n- Las imágenes muestran evidencia visual obligatoria.\n- Los videos son evidencia opcional adicional.\n- No inventes datos que no estén claros.\n- Si un dato no aparece, marca \"No identificado\".\n\nDebes devolver SOLO un JSON válido con esta estructura:\n\n{\n \"propuesta_referencia\": \"\",\n \"marca\": \"\",\n \"cliente\": \"\",\n \"pais\": \"\",\n \"ubicacion\": \"\",\n \"fecha_ejecucion\": \"\",\n \"que_se_implemento\": \"\",\n \"comentarios_resultados\": \"\",\n \"resumen_ejecucion\": \"\",\n \"nivel_confianza\": \"\",\n \"requiere_revision\": \"\",\n \"motivo_revision\": \"\"\n}\n\nDatos de control:\nsession_id: ${data.session_id}\nejecucion_id: ${data.ejecucion_id}\nmanager_nombre: ${data.manager_nombre}\nmanager_telefono: ${data.manager_telefono}\n\nArchivos de audio en Drive:\n${audioUrls || 'No hay audio registrado'}\n\nImágenes en Drive:\n${imagenesUrls || 'No hay imágenes registradas'}\n\nVideos en Drive:\n${videosUrls || 'No hay videos registrados'}\n`.trim();\n\nreturn [\n {\n json: {\n ...data,\n gemini_prompt: prompt,\n estado_procesamiento: 'PAQUETE_GEMINI_PREPARADO'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 53360, + 31168 + ], + "id": "b86ab1e9-c2cc-48d7-a69c-de7eca4f1fc8", + "name": "Code - Preparar paquete análisis Gemini WhatsApp TEST" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", + "rightValue": "audio", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "ddf19bdc-2a61-4261-8950-02ea3026ba01" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "audio" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dcf1d733-7b80-4f67-b6f6-9e965a5bae24", + "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", + "rightValue": "image", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "=image" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "bb7e48e1-8383-463c-a43e-7e8e5d1ae21d", + "leftValue": "={{ String($json.media_type || '').toLowerCase() }}", + "rightValue": "video", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "VIDEO" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 54608, + 32288 + ], + "id": "656a829f-b21a-4568-8614-eb60318288b5", + "name": "Switch - Tipo media para Gemini WhatsApp TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const item = $input.item;\n\nconst data = item.json || {};\nconst binaryData = item.binary?.data;\n\nif (!binaryData) {\n throw new Error('No llegó binary.data al nodo de audio para Gemini');\n}\n\nconst geminiPromptAudio = `\nAnaliza esta nota de voz enviada por WhatsApp para el flujo de registro de propuestas de Fulgencio Fumado.\n\nObjetivo:\nExtraer información útil del reporte y clasificar correctamente el tipo de reporte.\n\nPrimero, identifica el tipo de reporte usando exactamente uno de estos valores:\n\n1. PROPUESTA_EJECUTADA\nCuando el manager reporta evidencia de una propuesta que ya fue implementada, instalada, ejecutada, realizada o activada.\n\n2. PROPUESTA_EXTERNA\nCuando el manager reporta una propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n\n3. NO_DETERMINADO\nCuando la nota de voz no permite saber claramente si es una propuesta ejecutada o una propuesta externa.\n\nReglas para clasificar:\n- Si el manager dice explícitamente \"propuesta ejecutada\", clasifica como PROPUESTA_EJECUTADA.\n- Si el manager dice que recibió una propuesta externa, referencia externa o algo fuera del banco, clasifica como PROPUESTA_EXTERNA.\n- Si solo hay una prueba de audio, saludo, ruido, conteo, información incompleta o no se entiende el objetivo, clasifica como NO_DETERMINADO.\n- No inventes el tipo de reporte.\n- Si hay duda, usa NO_DETERMINADO.\n\nAdemás, extrae la mayor cantidad posible de información útil:\n- Nombre o referencia de la propuesta\n- Marca o cliente\n- País\n- Ubicación\n- Fecha de ejecución o fecha del reporte\n- Qué se implementó o qué se está reportando\n- Comentarios o resultados observados\n- Si el manager menciona que es ejecutada, externa o no queda claro\n\nDevuelve el análisis en texto claro y estructurado, incluyendo obligatoriamente estas líneas:\n\nTipo de reporte detectado: PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\nConfianza del tipo de reporte: ALTA | MEDIA | BAJA\nMotivo del tipo de reporte: explicación breve\n\nNo inventes datos. Si algo no está claro, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_audio: geminiPromptAudio\n },\n binary: {\n data: binaryData\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 55088, + 32112 + ], + "id": "4686e032-679b-4a2a-a472-471aeef83b3a", + "name": "Code - Preparar Gemini audio WhatsApp TEST" + }, + { + "parameters": { + "resource": "audio", + "operation": "analyze", + "modelId": { + "__rl": true, + "value": "models/gemini-2.5-pro", + "mode": "list", + "cachedResultName": "models/gemini-2.5-pro" + }, + "text": "={{ $json.gemini_prompt_audio }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + 55296, + 32112 + ], + "id": "849b5cb4-ab22-402c-baa1-f8686c700dbd", + "name": "Gemini - Analizar audio WhatsApp TEST", + "retryOnFail": true, + "waitBetweenTries": 5000, + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + }, + "onError": "continueRegularOutput" + }, + { + "parameters": { + "jsCode": "const geminiItems = $input.all();\nconst prepItems = $('Code - Preparar Gemini audio WhatsApp TEST').all();\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction safeJson(value) {\n try {\n return JSON.stringify(value);\n } catch (e) {\n return String(value ?? '');\n }\n}\n\nfunction extractTextFromGemini(obj) {\n if (!obj) return '';\n\n if (typeof obj === 'string') {\n return clean(obj);\n }\n\n if (typeof obj.text === 'string') return clean(obj.text);\n if (typeof obj.output === 'string') return clean(obj.output);\n if (typeof obj.response === 'string') return clean(obj.response);\n if (typeof obj.content === 'string') return clean(obj.content);\n\n const possibleParts =\n obj?.content?.parts ||\n obj?.candidates?.[0]?.content?.parts ||\n obj?.response?.candidates?.[0]?.content?.parts ||\n obj?.parts ||\n [];\n\n if (Array.isArray(possibleParts)) {\n const text = possibleParts\n .map(part => clean(part?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (text) return text;\n }\n\n const jsonText = safeJson(obj);\n return jsonText && jsonText !== '{}' ? jsonText : '';\n}\n\nfunction detectGeminiError(obj) {\n if (!obj || typeof obj !== 'object') return '';\n\n const candidates = [\n obj.error,\n obj.message,\n obj.description,\n obj.fullMessage,\n obj.full_message,\n obj.errorMessage,\n obj.error_message,\n obj?.error?.message,\n obj?.error?.description,\n obj?.error?.cause,\n obj?.json?.error,\n obj?.json?.message,\n ];\n\n const found = candidates\n .map(clean)\n .filter(Boolean)\n .join(' | ');\n\n const serialized = safeJson(obj);\n\n if (\n found ||\n serialized.includes('Internal error') ||\n serialized.includes('INTERNAL') ||\n serialized.includes('500') ||\n serialized.includes('service was not able to process')\n ) {\n return found || serialized;\n }\n\n return '';\n}\n\nconst output = [];\n\nconst total = Math.max(geminiItems.length, prepItems.length);\n\nfor (let i = 0; i < total; i++) {\n const geminiItem = geminiItems[i] || {};\n const prepItem = prepItems[i] || {};\n\n const geminiJson = geminiItem.json || {};\n const mediaJson = prepItem.json || geminiJson || {};\n\n const sessionId = clean(mediaJson.session_id);\n const ejecucionId = clean(mediaJson.ejecucion_id || sessionId);\n const mediaEventId = clean(mediaJson.media_event_id || mediaJson.event_id);\n const mediaFileName = clean(mediaJson.media_file_name || mediaJson.file_name || mediaJson.nombre_archivo);\n const driveFileId = clean(mediaJson.drive_file_id || mediaJson.file_id);\n const driveFileUrl = clean(mediaJson.drive_file_url || mediaJson.media_drive_url || mediaJson.webViewLink);\n\n const errorGemini = detectGeminiError(geminiJson);\n\n let analisisTexto = '';\n let estado = '';\n let requiereRevision = false;\n\n if (errorGemini) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_ERROR_GEMINI';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini no pudo procesar esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar propuesta, cliente, marca, país, ubicación o contexto.',\n '',\n `Detalle técnico: ${errorGemini}`,\n ].join('\\n');\n } else {\n analisisTexto = extractTextFromGemini(geminiJson);\n\n if (!analisisTexto) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_VACIO';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini respondió, pero no devolvió texto útil para esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar la propuesta.',\n ].join('\\n');\n } else {\n estado = 'ANALISIS_AUDIO_COMPLETADO';\n }\n }\n\n if (!sessionId) {\n throw new Error('No llegó session_id al normalizar análisis de audio.');\n }\n\n if (!mediaEventId) {\n throw new Error('No llegó media_event_id al normalizar análisis de audio.');\n }\n\n output.push({\n json: {\n ...mediaJson,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: 'audio',\n media_file_name: mediaFileName,\n drive_file_id: driveFileId,\n drive_file_url: driveFileUrl,\n\n analisis_texto: analisisTexto,\n fecha_analisis: nowIso(),\n estado,\n\n gemini_audio_error: errorGemini || '',\n audio_requiere_revision: requiereRevision,\n\n media_index: Number(mediaJson.media_index || 0),\n media_total: Number(mediaJson.media_total || mediaJson.media_total_esperado || 0),\n },\n });\n}\n\nreturn output;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 55504, + 32112 + ], + "id": "ffdd7a46-0fa2-41e2-afb7-49b49765e52e", + "name": "Code - Normalizar análisis audio WhatsApp TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const input = $input.item;\nconst data = input.json || {};\nconst binaries = input.binary || {};\n\nconst binaryKeys = Object.keys(binaries);\nconst sourceBinaryKey = binaries.data ? 'data' : binaryKeys[0];\n\nif (!sourceBinaryKey || !binaries[sourceBinaryKey]) {\n throw new Error(\n `No llegó archivo binario para Gemini imagen. Binary keys recibidas: ${binaryKeys.join(', ') || 'NINGUNA'}`\n );\n}\n\nconst binaryData = {\n ...binaries[sourceBinaryKey],\n};\n\nbinaryData.fileName =\n binaryData.fileName ||\n data.media_file_name ||\n `imagen_${data.media_index || Date.now()}.jpg`;\n\nbinaryData.mimeType =\n binaryData.mimeType ||\n data.mime_type ||\n data.mimetype ||\n 'image/jpeg';\n\nconst geminiPromptImagen = `\nAnaliza esta imagen como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en la imagen\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Posible ubicación visible\n- Calidad de la implementación\n- Detalles relevantes para documentar la ejecución\n\nNo inventes datos que no sean visibles.\nSi algo no se puede identificar, indica \"No identificado\".\n`.trim();\n\nreturn {\n json: {\n ...data,\n gemini_prompt_imagen: geminiPromptImagen,\n binary_input_field: 'data',\n debug_binary_imagen: {\n binary_keys_recibidas: binaryKeys,\n source_binary_key: sourceBinaryKey,\n output_binary_key: 'data',\n file_name: binaryData.fileName,\n mime_type: binaryData.mimeType,\n },\n },\n binary: {\n data: binaryData,\n },\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 55104, + 32336 + ], + "id": "d2eaab10-ae91-4748-99c0-dd63e8323495", + "name": "Code - Preparar Gemini imagen WhatsApp TEST" + }, + { + "parameters": { + "resource": "image", + "operation": "analyze", + "modelId": { + "__rl": true, + "value": "models/gemini-2.5-pro", + "mode": "list", + "cachedResultName": "models/gemini-2.5-pro" + }, + "text": "={{ $json.gemini_prompt_imagen }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + 55312, + 32336 + ], + "id": "19f27963-b01f-4ac2-a133-c109e84b8aa2", + "name": "Gemini - Analizar imagen WhatsApp TEST", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini imagen WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'image',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_IMAGEN_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 55520, + 32336 + ], + "id": "8ca940c9-108d-4676-acc3-29822f52a162", + "name": "Code - Normalizar análisis imagen WhatsApp TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const data = $json || {};\nconst binary = $binary || {};\n\nif (!binary.data) {\n throw new Error('No llegó binary.data al nodo Code - Preparar Gemini video.');\n}\n\nconst fileName =\n binary.data.fileName ||\n data.media_file_name ||\n data.file_name ||\n `video_${Date.now()}.mp4`;\n\nconst mimeType =\n binary.data.mimeType ||\n data.mime_type ||\n 'video/mp4';\n\n// Fuerza a n8n a cargar el contenido real del binario.\n// Esto evita el error de Gemini: \"Received undefined\".\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n\nif (!buffer || !Buffer.isBuffer(buffer) || buffer.length === 0) {\n throw new Error('El video llegó con metadata, pero sin contenido binario real.');\n}\n\nconst preparedBinary = await this.helpers.prepareBinaryData(\n buffer,\n fileName,\n mimeType\n);\n\nconst geminiPromptVideo = `\nAnaliza este video como evidencia de una propuesta ejecutada o reporte de Fulgencio Fumado.\n\nDescribe:\n- Qué se observa en el video\n- Elementos de marca visibles\n- Materiales instalados o implementados\n- Interacciones o movimientos relevantes\n- Posible ubicación visible\n- Calidad de la implementación\n- Resultados observables\n\nNo inventes datos que no sean visibles.\n`.trim();\n\nreturn {\n json: {\n ...data,\n media_type: 'video',\n media_file_name: fileName,\n mime_type: mimeType,\n gemini_prompt_video: geminiPromptVideo,\n },\n binary: {\n data: preparedBinary,\n },\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 54976, + 32576 + ], + "id": "6cc2ea4e-0fb2-4eaa-a0f5-0e72a51d39d5", + "name": "Code - Preparar Gemini video WhatsApp TEST" + }, + { + "parameters": { + "resource": "video", + "operation": "analyze", + "modelId": { + "__rl": true, + "value": "models/gemini-2.5-pro", + "mode": "list", + "cachedResultName": "models/gemini-2.5-pro" + }, + "text": "={{ $json.gemini_prompt_video }}", + "inputType": "binary", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + 55184, + 32576 + ], + "id": "e3aa489a-510e-4536-af34-4687ecc5e13b", + "name": "Analyze video", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini video WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'video',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_VIDEO_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 55392, + 32576 + ], + "id": "bc43c81d-c79f-4318-80c0-4991c4e28c8c", + "name": "Code - Normalizar análisis video WhatsApp TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1107394537, + "mode": "list", + "cachedResultName": "wa_ejecuciones_analisis_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "media_event_id": "={{ $json.media_event_id }}", + "media_type": "={{ $json.media_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "analisis_texto": "={{ $json.analisis_texto }}", + "fecha_analisis": "={{ $json.fecha_analisis }}", + "estado": "={{ $json.estado }}", + "media_index": "={{ $json.media_index }}", + "media_total": "={{ $json.media_total }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_event_id", + "displayName": "media_event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_type", + "displayName": "media_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "analisis_texto", + "displayName": "analisis_texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_analisis", + "displayName": "fecha_analisis", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_index", + "displayName": "media_index", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_total", + "displayName": "media_total", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 56176, + 32368 + ], + "id": "9d11dc64-15b2-4e39-8e5d-5c662748f1dc", + "name": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1107394537, + "mode": "list", + "cachedResultName": "wa_ejecuciones_analisis_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 59968, + 32176 + ], + "id": "612ac4ae-154b-4c7b-b51a-ad952fe2564b", + "name": "Sheets - Leer análisis media Gemini WhatsApp TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst inputRows = $input.all().map(item => item.json || {});\nconst actual = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst extraerTextoGemini = (valor) => {\n if (valor === null || valor === undefined) return '';\n\n if (typeof valor === 'object') {\n const partes =\n valor?.content?.parts ||\n valor?.response?.content?.parts ||\n valor?.candidates?.[0]?.content?.parts ||\n valor?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(valor);\n }\n\n const raw = limpiar(valor);\n if (!raw) return '';\n\n try {\n const parsed = JSON.parse(raw);\n\n const partes =\n parsed?.content?.parts ||\n parsed?.response?.content?.parts ||\n parsed?.candidates?.[0]?.content?.parts ||\n parsed?.parts ||\n [];\n\n if (Array.isArray(partes)) {\n const textoPartes = partes\n .map(p => limpiar(p?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (textoPartes) return textoPartes;\n }\n\n return JSON.stringify(parsed);\n } catch (error) {\n return raw;\n }\n};\n\nconst normalizarTipoMedia = (valor) => {\n const t = limpiar(valor).toLowerCase();\n\n if (t.includes('audio')) return 'audio';\n if (t.includes('image') || t.includes('imagen')) return 'image';\n if (t.includes('video')) return 'video';\n\n return t || 'unknown';\n};\n\nconst obtenerFecha = (row) => {\n const fecha = new Date(row.fecha_analisis || row.fecha_recepcion || row.ultima_actividad || 0);\n const time = fecha.getTime();\n return Number.isFinite(time) ? time : 0;\n};\n\n// Contextos posibles del cierre actual.\n// IMPORTANTE: NO usamos $json como prioridad, porque aquí $json puede ser la primera fila del Sheet.\nconst decisionPorSheet = getNodeJson('Code - Decidir cierre análisis por Sheet Redis TEST');\nconst validarLock = getNodeJson('Code - Validar lock análisis final TEST');\nconst validarConteo = getNodeJson('Code - Validar conteo análisis Redis TEST');\nconst cierreMedia = getNodeJson('Code - Consolidar media recuperada WhatsApp TEST');\nconst eventoPaso = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst eventoSesion = getNodeJson('Code - Resolver sesión WhatsApp TEST');\n\nlet sessionId = limpiar(\n decisionPorSheet.session_id ||\n validarLock.session_id ||\n validarConteo.session_id ||\n cierreMedia.session_id ||\n eventoPaso.session_id ||\n eventoSesion.session_id ||\n ''\n);\n\nlet ejecucionId = limpiar(\n decisionPorSheet.ejecucion_id ||\n validarLock.ejecucion_id ||\n validarConteo.ejecucion_id ||\n cierreMedia.ejecucion_id ||\n eventoPaso.ejecucion_id ||\n eventoSesion.ejecucion_id ||\n sessionId\n);\n\n// Si por cualquier razón no llegó session_id desde el contexto,\n// usamos la sesión más reciente del Sheet leído.\nif (!sessionId) {\n const grupos = {};\n\n for (const row of inputRows) {\n const sid = limpiar(row.session_id);\n if (!sid) continue;\n\n if (!grupos[sid]) {\n grupos[sid] = {\n session_id: sid,\n ejecucion_id: limpiar(row.ejecucion_id || sid),\n rows: [],\n ultima_fecha: 0\n };\n }\n\n grupos[sid].rows.push(row);\n grupos[sid].ultima_fecha = Math.max(grupos[sid].ultima_fecha, obtenerFecha(row));\n }\n\n const grupoMasReciente = Object.values(grupos)\n .sort((a, b) => b.ultima_fecha - a.ultima_fecha)[0];\n\n if (grupoMasReciente) {\n sessionId = grupoMasReciente.session_id;\n ejecucionId = grupoMasReciente.ejecucion_id || sessionId;\n }\n}\n\nif (!sessionId) {\n throw new Error('No llegó session_id para consolidar análisis de media.');\n}\n\n// Primero intentamos usar las filas filtradas por el nodo de cierre.\n// Si no vienen, usamos las filas leídas del Sheet.\nlet rows = [];\n\nif (\n Array.isArray(decisionPorSheet.analisis_rows_filtrados) &&\n decisionPorSheet.analisis_rows_filtrados.length > 0\n) {\n rows = decisionPorSheet.analisis_rows_filtrados;\n} else {\n rows = inputRows;\n}\n\n// Filtrar solo la sesión correcta.\nrows = rows.filter(row => limpiar(row.session_id) === sessionId);\n\n// Si todavía no encontró nada, usar fallback por ejecución.\nif (rows.length === 0 && ejecucionId) {\n rows = inputRows.filter(row => limpiar(row.ejecucion_id) === ejecucionId);\n}\n\n// Deduplicar por media_event_id + media_type.\nconst vistos = new Set();\nconst analisisUnicos = [];\n\nfor (const row of rows) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaType = normalizarTipoMedia(row.media_type);\n\n if (!mediaEventId || !mediaType) continue;\n\n const key = `${mediaEventId}_${mediaType}`;\n if (vistos.has(key)) continue;\n\n const textoAnalisis = extraerTextoGemini(row.analisis_texto);\n\n if (!textoAnalisis) continue;\n\n vistos.add(key);\n\n analisisUnicos.push({\n ...row,\n media_type: mediaType,\n media_index: Number(row.media_index || 0),\n media_total: Number(row.media_total || row.media_total_esperado || 0),\n analisis_texto_limpio: textoAnalisis\n });\n}\n\nanalisisUnicos.sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst audios = analisisUnicos.filter(row => row.media_type === 'audio');\nconst imagenes = analisisUnicos.filter(row => row.media_type === 'image');\nconst videos = analisisUnicos.filter(row => row.media_type === 'video');\n\nconst audiosCount = audios.length;\nconst fotosCount = imagenes.length;\nconst videosCount = videos.length;\n\nconst mediaTotalEsperado = Number(\n decisionPorSheet.media_total_esperado ||\n validarConteo.media_total_esperado ||\n cierreMedia.total_media_drive ||\n cierreMedia.total_media ||\n analisisUnicos[0]?.media_total ||\n analisisUnicos.length ||\n 0\n);\n\nconst analisisAudioTexto = audios\n .map(row => row.analisis_texto_limpio)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisImagenesTexto = imagenes\n .map((row, index) => `IMAGEN ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisVideosTexto = videos\n .map((row, index) => `VIDEO ${index + 1}:\\n${row.analisis_texto_limpio}`)\n .filter(Boolean)\n .join('\\n\\n');\n\nconst analisisConsolidado = [\n analisisAudioTexto ? `ANÁLISIS DE AUDIO:\\n${analisisAudioTexto}` : '',\n analisisImagenesTexto ? `ANÁLISIS DE IMÁGENES:\\n${analisisImagenesTexto}` : '',\n analisisVideosTexto ? `ANÁLISIS DE VIDEOS:\\n${analisisVideosTexto}` : ''\n].filter(Boolean).join('\\n\\n');\n\nif (!analisisConsolidado) {\n throw new Error(\n `No hay análisis multimedia consolidado para enviar a Gemini final. session_id=${sessionId}, ejecucion_id=${ejecucionId}, rows_filtradas=${rows.length}, input_rows=${inputRows.length}`\n );\n}\n\nconst transcripcionAudio = analisisAudioTexto || '';\n\nconst geminiPromptFinal = `\nEres un analista de evidencias de GomezLee Marketing / Fulgencio Fumado.\n\nTu tarea es consolidar el análisis de audio, imágenes y videos de un reporte recibido por WhatsApp.\n\nIMPORTANTE:\n- No inventes información.\n- Si un dato no aparece claramente, responde \"No identificado\".\n- El audio tiene prioridad para identificar propuesta, marca, país, ubicación, fecha y tipo de reporte.\n- Las imágenes y videos sirven como evidencia visual.\n- Clasifica el reporte usando exactamente uno de estos valores:\n - PROPUESTA_EJECUTADA\n - PROPUESTA_EXTERNA\n - NO_DETERMINADO\n\nDefiniciones:\n- PROPUESTA_EJECUTADA: evidencia de una propuesta ya implementada, instalada, ejecutada, realizada o activada.\n- PROPUESTA_EXTERNA: propuesta, actividad, idea, ejecución, referencia o material recibido fuera del banco interno de propuestas y que debe documentarse.\n- NO_DETERMINADO: la información no permite saber claramente si es ejecutada o externa.\n\nDATOS DEL REPORTE:\n- session_id: ${sessionId}\n- ejecucion_id: ${ejecucionId}\n- audios_count: ${audiosCount}\n- fotos_count: ${fotosCount}\n- videos_count: ${videosCount}\n\nANÁLISIS MULTIMEDIA CONSOLIDADO:\n${analisisConsolidado}\n\nDevuelve ÚNICAMENTE un JSON válido, sin markdown, sin explicación adicional y sin texto fuera del JSON.\n\nEl JSON debe tener exactamente esta estructura:\n\n{\n \"tipo_reporte\": \"PROPUESTA_EJECUTADA | PROPUESTA_EXTERNA | NO_DETERMINADO\",\n \"tipo_reporte_confianza\": \"ALTA | MEDIA | BAJA\",\n \"motivo_tipo_reporte\": \"texto breve\",\n \"propuesta_referencia\": \"texto o No identificado\",\n \"marca\": \"texto o No identificado\",\n \"cliente\": \"texto o No identificado\",\n \"pais\": \"texto o No identificado\",\n \"ubicacion\": \"texto o No identificado\",\n \"fecha_ejecucion\": \"texto o No identificado\",\n \"descripcion_ejecucion\": \"texto claro y profesional\",\n \"elementos_detectados\": \"lista resumida en texto\",\n \"resumen_ia\": \"resumen ejecutivo del reporte\",\n \"comentarios_resultados\": \"texto o No identificado\",\n \"tags\": \"tags separados por coma\"\n}\n`.trim();\n\nreturn [\n {\n json: {\n ...cierreMedia,\n ...eventoSesion,\n ...eventoPaso,\n ...validarConteo,\n ...validarLock,\n ...decisionPorSheet,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_media: analisisUnicos.length,\n\n audio_count: audiosCount,\n audios_count: audiosCount,\n imagenes_count: fotosCount,\n fotos_count: fotosCount,\n videos_count: videosCount,\n\n transcripcion_audio: transcripcionAudio,\n\n analisis_audio: analisisAudioTexto,\n analisis_imagenes: analisisImagenesTexto,\n analisis_videos: analisisVideosTexto,\n\n analisis_audio_texto: analisisAudioTexto,\n analisis_imagenes_texto: analisisImagenesTexto,\n analisis_videos_texto: analisisVideosTexto,\n\n analisis_media_consolidado: analisisConsolidado,\n analisis_multimedia: analisisConsolidado,\n analisis_consolidado: analisisConsolidado,\n\n gemini_prompt_final: geminiPromptFinal,\n\n analisis_rows_filtrados: analisisUnicos,\n\n estado_analisis_media: 'ANALISIS_MEDIA_CONSOLIDADO',\n\n consolidacion_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n input_rows: inputRows.length,\n rows_filtradas: rows.length,\n total_analisis_unicos: analisisUnicos.length,\n media_total_esperado: mediaTotalEsperado,\n audios: audiosCount,\n imagenes: fotosCount,\n videos: videosCount,\n gemini_prompt_final_generado: Boolean(geminiPromptFinal),\n gemini_prompt_final_length: geminiPromptFinal.length,\n uso_decision_por_sheet: Object.keys(decisionPorSheet).length > 0,\n uso_validar_lock: Object.keys(validarLock).length > 0,\n uso_validar_conteo: Object.keys(validarConteo).length > 0,\n media: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado,\n media_file_name: row.media_file_name\n }))\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 60864, + 32208 + ], + "id": "4f8a76a2-9311-4337-972a-8fc4378cb5dc", + "name": "Code - Consolidar análisis media Gemini WhatsApp TEST" + }, + { + "parameters": { + "modelId": { + "__rl": true, + "value": "models/gemini-2.5-pro", + "mode": "list", + "cachedResultName": "models/gemini-2.5-pro" + }, + "messages": { + "values": [ + { + "content": "={{ $json.gemini_prompt_final }}" + } + ] + }, + "builtInTools": {}, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.googleGemini", + "typeVersion": 1.2, + "position": [ + 61056, + 32208 + ], + "id": "4e5a1a2d-961f-4947-98ab-813a710e12a8", + "name": "Gemini - Generar JSON final propuesta ejecutada TEST", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + }, + { + "parameters": { + "jsCode": "const gemini = $json || {};\nconst base = $('Code - Consolidar análisis media Gemini WhatsApp TEST').first().json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst esNoIdentificado = (valor) => {\n const texto = normalizarPlano(valor);\n\n return (\n !texto ||\n texto === 'NO IDENTIFICADO' ||\n texto === 'NO DISPONIBLE' ||\n texto === 'N/A' ||\n texto === 'NA' ||\n texto === 'NULL' ||\n texto === 'UNDEFINED'\n );\n};\n\nconst valor = (campo, fallback = 'No identificado') => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return fallback;\n\n return v;\n};\n\nconst valorOpcional = (campo) => {\n const v = limpiar(campo);\n\n if (esNoIdentificado(v)) return '';\n\n return v;\n};\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (Array.isArray(obj.content?.parts)) {\n const txt = obj.content.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.parts)) {\n const txt = obj.parts.map(p => p.text || '').join('\\n').trim();\n if (txt) return txt;\n }\n\n if (Array.isArray(obj.candidates)) {\n const txt = obj.candidates\n .flatMap(c => c.content?.parts || [])\n .map(p => p.text || '')\n .join('\\n')\n .trim();\n\n if (txt) return txt;\n }\n\n if (obj.message && typeof obj.message === 'object') {\n const txt = extraerTexto(obj.message);\n if (txt) return txt;\n }\n\n if (obj.data && typeof obj.data === 'object') {\n const txt = extraerTexto(obj.data);\n if (txt) return txt;\n }\n\n return JSON.stringify(obj);\n};\n\nlet texto = extraerTexto(gemini).trim();\n\ntexto = texto\n .replace(/^```json/i, '')\n .replace(/^```/i, '')\n .replace(/```$/i, '')\n .trim();\n\nlet parsed = {};\n\ntry {\n parsed = JSON.parse(texto);\n} catch (error) {\n parsed = {\n tipo_reporte: 'NO_DETERMINADO',\n tipo_reporte_confianza: 'BAJA',\n tipo_reporte_motivo: `No se pudo parsear el JSON final de Gemini: ${error.message}`,\n\n propuesta_referencia: 'No identificado',\n marca: 'No identificado',\n cliente: 'No identificado',\n pais: 'No identificado',\n ubicacion: 'No identificado',\n fecha_ejecucion: 'No identificado',\n que_se_implemento: 'No identificado',\n comentarios_resultados: '',\n resumen_ejecucion: texto || 'No se pudo parsear el JSON de Gemini.',\n evidencia_audio_resumen: base.texto_audio || '',\n evidencia_imagenes_resumen: base.texto_imagenes || '',\n evidencia_videos_resumen: base.texto_videos || '',\n nivel_confianza: 'BAJA',\n requiere_revision: 'SI',\n motivo_revision: `Error parseando JSON final: ${error.message}`\n };\n}\n\nconst normalizarTipoReporte = (valorTipo) => {\n const texto = String(valorTipo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, '_');\n\n if (\n texto === 'PROPUESTA_EJECUTADA' ||\n texto === 'EJECUTADA' ||\n texto === 'EJECUTADO'\n ) {\n return 'PROPUESTA_EJECUTADA';\n }\n\n if (\n texto === 'PROPUESTA_EXTERNA' ||\n texto === 'EXTERNA' ||\n texto === 'EXTERNO'\n ) {\n return 'PROPUESTA_EXTERNA';\n }\n\n return 'NO_DETERMINADO';\n};\n\nconst normalizarConfianza = (valorConfianza) => {\n const texto = String(valorConfianza ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['ALTA', 'MEDIA', 'BAJA'].includes(texto)) {\n return texto;\n }\n\n return 'BAJA';\n};\n\nconst normalizarSiNo = (valorSiNo, fallback = 'SI') => {\n const texto = String(valorSiNo ?? '')\n .trim()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n\n if (['SI', 'SÍ', 'YES', 'TRUE'].includes(texto)) return 'SI';\n if (['NO', 'FALSE'].includes(texto)) return 'NO';\n\n return fallback;\n};\n\nconst limpiarReferenciaVisual = (valorRef) => {\n let textoRef = limpiar(valorRef);\n\n if (esNoIdentificado(textoRef)) return 'No identificado';\n\n textoRef = textoRef\n .replace(/_/g, ' ')\n .replace(/\\+/g, '+')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return textoRef || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valorUbicacion) => {\n const textoUbicacion = normalizarPlano(valorUbicacion);\n\n if (!textoUbicacion) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bVERSIÓN\\b/,\n /\\bCODIGO\\b/,\n /\\bCÓDIGO\\b/,\n /\\bGUATE WMC\\b/,\n /\\bPOLLO CAMPERO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(textoUbicacion));\n};\n\nconst normalizarUbicacion = (valorUbicacion) => {\n const textoUbicacion = limpiar(valorUbicacion);\n\n if (esNoIdentificado(textoUbicacion)) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(textoUbicacion)) {\n return 'No identificado';\n }\n\n return textoUbicacion;\n};\n\nconst normalizarFechaEjecucion = (valorFecha) => {\n const textoFecha = limpiar(valorFecha);\n\n if (esNoIdentificado(textoFecha)) return 'No identificado';\n\n // Si solo viene el año, no es una fecha de ejecución real.\n if (/^20\\d{2}$/.test(textoFecha)) {\n return 'No identificado';\n }\n\n return textoFecha;\n};\n\nconst limitarTexto = (valorTexto, max = 1200, fallback = '') => {\n const textoLimpio = valor(valorTexto, fallback);\n\n if (!textoLimpio) return '';\n\n return textoLimpio.length > max\n ? `${textoLimpio.slice(0, max - 3)}...`\n : textoLimpio;\n};\n\nconst ahora = new Date().toISOString();\n\nconst tipoReporte = normalizarTipoReporte(parsed.tipo_reporte);\nconst tipoReporteConfianza = normalizarConfianza(parsed.tipo_reporte_confianza);\nconst tipoReporteMotivo = valor(parsed.tipo_reporte_motivo, 'No identificado');\n\nconst requiereRevision = normalizarSiNo(parsed.requiere_revision, 'SI');\n\nconst estadoRevision = requiereRevision === 'SI'\n ? 'PENDIENTE_REVISION'\n : 'ANALISIS_COMPLETADO';\n\nconst propuestaReferenciaOriginal = valor(parsed.propuesta_referencia);\nconst propuestaReferenciaLimpia = limpiarReferenciaVisual(propuestaReferenciaOriginal);\n\nconst fechaEjecucionOriginal = valor(parsed.fecha_ejecucion);\nconst fechaEjecucionLimpia = normalizarFechaEjecucion(fechaEjecucionOriginal);\n\nconst ubicacionOriginal = valor(parsed.ubicacion);\nconst ubicacionLimpia = normalizarUbicacion(ubicacionOriginal);\n\nconst evidenciaAudio = valorOpcional(parsed.evidencia_audio_resumen || base.texto_audio);\nconst evidenciaImagenes = valorOpcional(parsed.evidencia_imagenes_resumen || base.texto_imagenes);\nconst evidenciaVideos = valorOpcional(parsed.evidencia_videos_resumen || base.texto_videos);\n\nconst elementosDetectados = [\n evidenciaImagenes,\n evidenciaVideos\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .join('\\n\\n');\n\nconst tagsSet = new Set();\n\n[\n parsed.marca,\n parsed.cliente,\n parsed.pais,\n ubicacionLimpia\n]\n .map(v => valorOpcional(v))\n .filter(Boolean)\n .forEach(v => tagsSet.add(v));\n\nconst tags = [...tagsSet].join(', ');\n\nconst resumenIa = limitarTexto(parsed.resumen_ejecucion, 1200, '');\nconst descripcionEjecucion = limitarTexto(parsed.que_se_implemento, 1200, 'No identificado');\nconst comentariosResultados = limitarTexto(parsed.comentarios_resultados, 900, '');\n\nreturn [\n {\n json: {\n ...base,\n\n ejecucion_id: base.ejecucion_id || base.session_id || '',\n session_id: base.session_id || '',\n\n fecha_recepcion: base.fecha_recepcion || ahora,\n fecha_ejecucion: fechaEjecucionLimpia,\n fecha_ejecucion_original_ia: fechaEjecucionOriginal,\n\n manager_nombre: valor(base.manager_nombre),\n manager_telefono: valor(base.manager_telefono),\n canal_origen: base.canal_origen || 'WHATSAPP',\n\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza,\n tipo_reporte_motivo: tipoReporteMotivo,\n\n propuesta_referencia: propuestaReferenciaLimpia,\n propuesta_referencia_original_ia: propuestaReferenciaOriginal,\n\n propuesta_match_estado: 'PENDIENTE',\n propuesta_match_confianza: '',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_match_revision: 'PENDIENTE_MATCH_BANCO',\n\n marca: valor(parsed.marca),\n cliente: valor(parsed.cliente),\n pais: valor(parsed.pais),\n ubicacion: ubicacionLimpia,\n ubicacion_original_ia: ubicacionOriginal,\n\n comentario_original: evidenciaAudio || comentariosResultados || '',\n resumen_ia: resumenIa,\n descripcion_ejecucion: descripcionEjecucion,\n comentarios_resultados: comentariosResultados,\n elementos_detectados: elementosDetectados,\n tags,\n\n media_folder_url: base.media_folder_url || '',\n presentacion_ejecucion_url: '',\n\n fotos_count: base.total_analisis_imagenes || 0,\n videos_count: base.total_analisis_videos || 0,\n audios_count: base.total_analisis_audio || 0,\n\n estado_revision: estadoRevision,\n motivo_revision: valor(parsed.motivo_revision, ''),\n ultima_actualizacion: ahora,\n\n transcripcion_audio: evidenciaAudio,\n\n nivel_confianza: normalizarConfianza(parsed.nivel_confianza),\n requiere_revision: requiereRevision,\n\n gemini_json_raw: texto,\n\n normalizacion_json_final_debug: {\n propuesta_referencia_original: propuestaReferenciaOriginal,\n propuesta_referencia_limpia: propuestaReferenciaLimpia,\n fecha_ejecucion_original: fechaEjecucionOriginal,\n fecha_ejecucion_limpia: fechaEjecucionLimpia,\n ubicacion_original: ubicacionOriginal,\n ubicacion_limpia: ubicacionLimpia,\n tipo_reporte: tipoReporte,\n tipo_reporte_confianza: tipoReporteConfianza\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 61408, + 32208 + ], + "id": "802c8812-f0d2-4a69-ace0-2487f7cf1267", + "name": "Code - Normalizar JSON final propuesta ejecutada TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "fecha_ejecucion": "={{ $json.fecha_ejecucion }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "propuesta_referencia": "={{ $json.propuesta_referencia }}", + "propuesta_match_estado": "={{ $json.propuesta_match_estado }}", + "propuesta_match_confianza": "={{ $json.propuesta_match_confianza }}", + "propuesta_nombre_banco": "={{ $json.propuesta_nombre_banco }}", + "propuesta_link_banco": "={{ $json.propuesta_link_banco }}", + "marca": "={{ $json.marca }}", + "cliente": "={{ $json.cliente }}", + "pais": "={{ $json.pais }}", + "ubicacion": "={{ $json.ubicacion }}", + "comentario_original": "={{ $json.comentario_original }}", + "resumen_ia": "={{ $json.resumen_ia }}", + "descripcion_ejecucion": "={{ $json.descripcion_ejecucion }}", + "elementos_detectados": "={{ $json.elementos_detectados }}", + "tags": "={{ $json.tags }}", + "media_folder_url": "={{ $json.media_folder_url }}", + "presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}", + "fotos_count": "={{ $json.fotos_count }}", + "videos_count": "={{ $json.videos_count }}", + "audios_count": "={{ $json.audios_count }}", + "estado_revision": "={{ $json.estado_revision }}", + "motivo_revision": "={{ $json.motivo_revision }}", + "ultima_actualizacion": "={{ $json.ultima_actualizacion }}", + "session_id": "={{ $json.session_id }}", + "canal_origen": "={{ $json.canal_origen }}", + "transcripcion_audio": "={{ $json.transcripcion_audio }}", + "propuesta_match_revision": "={{ $json.propuesta_match_revision }}", + "tipo_reporte": "={{ $json.tipo_reporte }}", + "tipo_reporte_confianza": "={{ $json.tipo_reporte_confianza }}", + "tipo_reporte_motivo": "={{ $json.tipo_reporte_motivo }}", + "decision_automatica_banco": "={{ $json.decision_automatica_banco }}", + "motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}", + "banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_ejecucion", + "displayName": "fecha_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_referencia", + "displayName": "propuesta_referencia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_estado", + "displayName": "propuesta_match_estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_confianza", + "displayName": "propuesta_match_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_nombre_banco", + "displayName": "propuesta_nombre_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_link_banco", + "displayName": "propuesta_link_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "marca", + "displayName": "marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "cliente", + "displayName": "cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "pais", + "displayName": "pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ubicacion", + "displayName": "ubicacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comentario_original", + "displayName": "comentario_original", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "resumen_ia", + "displayName": "resumen_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "descripcion_ejecucion", + "displayName": "descripcion_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "elementos_detectados", + "displayName": "elementos_detectados", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tags", + "displayName": "tags", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_folder_url", + "displayName": "media_folder_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "presentacion_ejecucion_url", + "displayName": "presentacion_ejecucion_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fotos_count", + "displayName": "fotos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audios_count", + "displayName": "audios_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado_revision", + "displayName": "estado_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "transcripcion_audio", + "displayName": "transcripcion_audio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_revision", + "displayName": "propuesta_match_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_confianza", + "displayName": "tipo_reporte_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_motivo", + "displayName": "tipo_reporte_motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "decision_automatica_banco", + "displayName": "decision_automatica_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo_decision_automatica", + "displayName": "motivo_decision_automatica", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "banco_actualizado_auto", + "displayName": "banco_actualizado_auto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 63152, + 32288 + ], + "id": "370daef7-6f4d-466a-8df5-0dbc4770a357", + "name": "Sheets - Guardar propuesta ejecutada final TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "ultima_actividad": "={{ new Date().toISOString() }}", + "etapa": "={{ 'COMPLETADO' }}", + "estado": "={{ 'ANALISIS_COMPLETADO' }}", + "motivo_revision": "={{ $json.motivo_revision || 'ANALISIS_COMPLETADO' }}", + "tipo_reporte": "={{ $json.tipo_reporte }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 70480, + 32176 + ], + "id": "ba545fe7-4742-475b-90e0-f0a26d4e6394", + "name": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst normalizado = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst presentacion = getNodeJson('Code - Normalizar link presentación ejecución TEST');\nconst basePaso = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst actual = $json || {};\n\nconst data = {\n ...basePaso,\n ...normalizado,\n ...actual,\n ...presentacion,\n};\n\nconst clean = (value) => String(value ?? '').trim();\nconst digits = (value) => clean(value).replace(/\\D/g, '');\n\nconst whatsappTo = digits(\n data.whatsapp_to ||\n data.sender_phone ||\n data.manager_telefono ||\n data.whatsapp_remote_jid\n);\n\nif (!whatsappTo) {\n throw new Error('No se encontró el número privado para enviar el mensaje final.');\n}\n\nconst presentationUrl = clean(\n data.presentacion_ejecucion_url ||\n data.presentation_url\n);\n\nconst language = clean(\n data.idioma_flujo ||\n data.sesion_activa?.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst lines = [];\n\nif (language === 'EN') {\n lines.push('✅ *Report processed successfully*');\n} else {\n lines.push('✅ *Reporte procesado correctamente*');\n}\n\nif (presentationUrl) {\n lines.push('');\n lines.push(language === 'EN' ? '🔗 *Presentation:*' : '🔗 *Presentación:*');\n lines.push(presentationUrl);\n}\n\nlines.push('');\nlines.push(\n language === 'EN'\n ? 'You can start a new report or view the country leaderboard.'\n : 'Puedes iniciar un nuevo reporte o consultar el leaderboard por país.'\n);\n\nconst whatsappText = lines.join('\\n');\n\nreturn [\n {\n json: {\n ...data,\n is_group: false,\n group_id: '',\n group_jid: '',\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: 'individual',\n whatsapp_header: '',\n whatsapp_text: whatsappText,\n whatsapp_buttons: [\n { id: 'HEY', title: language === 'EN' ? 'New report' : 'Nuevo reporte' },\n { id: 'RANKING', title: language === 'EN' ? 'View ranking' : 'Ver ranking' }\n ]\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 70768, + 32144 + ], + "id": "d03a9722-6691-46d6-ae9f-f94d18a537f4", + "name": "Code - Preparar mensaje final WhatsApp TEST" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 59776, + 32176 + ], + "id": "ba1836dd-c92e-4404-b11a-59884fc35185", + "name": "Wait", + "webhookId": "3d858751-a035-4915-8ee0-caf8f19c78a5" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 62368, + 32240 + ], + "id": "e1f72e60-642b-4592-a88d-21a99b305340", + "name": "Sheets - Leer propuestas ejecutadas final TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const base = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\nconst rows = $input.all().map(item => item.json || {});\n\nconst sessionId = String(base.session_id || '').trim();\nconst ejecucionId = String(base.ejecucion_id || '').trim();\n\nconst existente = rows.find(row => {\n const rowSessionId = String(row.session_id || '').trim();\n const rowEjecucionId = String(row.ejecucion_id || '').trim();\n\n return (\n (sessionId && rowSessionId === sessionId) ||\n (ejecucionId && rowEjecucionId === ejecucionId)\n );\n});\n\nreturn [\n {\n json: {\n ...base,\n\n propuesta_final_ya_existe: Boolean(existente),\n propuesta_final_row_number: existente?.row_number || '',\n propuesta_final_existente: existente || null,\n\n estado_anti_duplicado: existente\n ? 'PROPUESTA_FINAL_YA_EXISTE'\n : 'PROPUESTA_FINAL_NUEVA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 62576, + 32240 + ], + "id": "303939b4-8a6d-4bd1-95c6-01adf62e3a46", + "name": "Code - Verificar duplicado propuesta ejecutada TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "80785332-9e50-44d5-9fd5-ecf9610876fd", + "leftValue": "={{ $json.propuesta_final_ya_existe === true }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 62784, + 32240 + ], + "id": "6a7ce17f-e14b-44c4-a844-1dba44e2c04f", + "name": "IF - Propuesta final ya existe TEST" + }, + { + "parameters": { + "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '1gHgQBDL2uFaa7yNwcyWrtE24B_UrCU1Dy-n63TPurPE';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 63360, + 32256 + ], + "id": "7328afc9-42bb-4c50-b493-b83d72c743eb", + "name": "Code - Preparar copia presentación ejecución TEST" + }, + { + "parameters": { + "operation": "copy", + "fileId": { + "__rl": true, + "value": "={{ $json.slides_template_id }}", + "mode": "id" + }, + "name": "={{ $json.presentation_name }}", + "sameFolder": false, + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "={{ $json.execution_folder_id }}", + "mode": "id" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 64912, + 32256 + ], + "id": "46d29cff-0099-4a6e-8771-cdc70545c541", + "name": "Drive - Copiar plantilla presentación ejecución TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const base = $('Code - Preparar copia presentación ejecución TEST').first().json || {};\nconst drive = $json || {};\n\nconst presentationId =\n drive.id ||\n drive.fileId ||\n drive.presentationId ||\n drive.data?.id ||\n '';\n\nif (!presentationId) {\n throw new Error('No se encontró el ID de la presentación copiada.');\n}\n\nconst presentationUrl =\n drive.webViewLink ||\n drive.webUrl ||\n `https://docs.google.com/presentation/d/${presentationId}/edit`;\n\nreturn [\n {\n json: {\n ...base,\n\n presentation_id: presentationId,\n presentacion_ejecucion_url: presentationUrl,\n\n estado_presentacion: 'PRESENTACION_CREADA'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 65184, + 32256 + ], + "id": "3544e093-75f6-4580-a6c6-bc5fb4cfb4ed", + "name": "Code - Normalizar link presentación ejecución TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "presentacion_ejecucion_url": "={{ $json.presentacion_ejecucion_url }}", + "ultima_actualizacion": "={{ new Date().toISOString() }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "session_id": "={{ $json.session_id }}", + "media_folder_url": "={{ $json.media_folder_url }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_ejecucion", + "displayName": "fecha_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_referencia", + "displayName": "propuesta_referencia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_estado", + "displayName": "propuesta_match_estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_confianza", + "displayName": "propuesta_match_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_nombre_banco", + "displayName": "propuesta_nombre_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_link_banco", + "displayName": "propuesta_link_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "marca", + "displayName": "marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "cliente", + "displayName": "cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "pais", + "displayName": "pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ubicacion", + "displayName": "ubicacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comentario_original", + "displayName": "comentario_original", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "resumen_ia", + "displayName": "resumen_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "descripcion_ejecucion", + "displayName": "descripcion_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "elementos_detectados", + "displayName": "elementos_detectados", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tags", + "displayName": "tags", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_folder_url", + "displayName": "media_folder_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "presentacion_ejecucion_url", + "displayName": "presentacion_ejecucion_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fotos_count", + "displayName": "fotos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "audios_count", + "displayName": "audios_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado_revision", + "displayName": "estado_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "transcripcion_audio", + "displayName": "transcripcion_audio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "propuesta_match_revision", + "displayName": "propuesta_match_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_confianza", + "displayName": "tipo_reporte_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "tipo_reporte_motivo", + "displayName": "tipo_reporte_motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "decision_automatica_banco", + "displayName": "decision_automatica_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo_decision_automatica", + "displayName": "motivo_decision_automatica", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "banco_actualizado_auto", + "displayName": "banco_actualizado_auto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 68400, + 32112 + ], + "id": "cf34fb57-81ec-44d9-a5e1-7ada19888936", + "name": "Sheets - Actualizar link presentación ejecución TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst carpetaPreparada = getNodeJson('Code - Preparar carpeta ejecución TEST');\nconst carpetaDrive = getNodeJson('Drive - Crear carpeta ejecución TEST');\nconst carpetaNormalizada = getNodeJson('Code - Normalizar carpeta ejecución TEST');\n\nconst copiaPresentacion = getNodeJson('Drive - Copiar plantilla presentación ejecución TEST');\nconst linkPresentacionNormalizado = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...carpetaPreparada,\n ...carpetaDrive,\n ...carpetaNormalizada,\n ...copiaPresentacion,\n ...linkPresentacionNormalizado,\n ...actual,\n ...match,\n ...enriquecido\n};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = limpiar(valor);\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n if (texto.toLowerCase() === 'no disponible') return fallback;\n\n return texto;\n};\n\nconst cortar = (valor, max = 900) => {\n const texto = limpiarTexto(valor, 'No disponible');\n return texto.length > max ? texto.slice(0, max - 3) + '...' : texto;\n};\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n ''\n);\n\nconst matchConfianza = Number(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n 0\n);\n\nconst matchAlta =\n matchEstado === 'MATCH_ALTA_CONFIANZA' ||\n matchConfianza >= 85;\n\nconst propuestaNombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n ''\n);\n\nconst propuestaReferenciaEnriquecida = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n jsonFinal.propuesta_referencia ||\n ''\n);\n\nconst propuestaReferenciaSlides = limpiarReferenciaVisual(\n matchAlta && propuestaNombreBanco\n ? propuestaNombreBanco\n : propuestaReferenciaEnriquecida\n);\n\nconst ubicacionSlides = limpiarTexto(\n enriquecido.ubicacion ||\n data.ubicacion ||\n jsonFinal.ubicacion,\n 'No identificado'\n);\n\nconst fechaEjecucionSlides = limpiarTexto(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n jsonFinal.fecha_ejecucion,\n 'No identificado'\n);\n\n// --------------------------------------------------\n// Resolver carpeta de evidencias\n// --------------------------------------------------\n\nconst mediaFolderId = limpiar(\n data.media_folder_id ||\n data.execution_folder_id ||\n data.folder_id ||\n data.id ||\n carpetaNormalizada.media_folder_id ||\n carpetaNormalizada.execution_folder_id ||\n carpetaNormalizada.folder_id ||\n carpetaDrive.id ||\n ''\n);\n\nlet mediaFolderUrl = limpiar(\n data.media_folder_url ||\n data.execution_folder_url ||\n data.folder_url ||\n carpetaNormalizada.media_folder_url ||\n carpetaNormalizada.execution_folder_url ||\n carpetaNormalizada.folder_url ||\n carpetaDrive.webViewLink ||\n ''\n);\n\nif (!mediaFolderUrl && mediaFolderId) {\n mediaFolderUrl = `https://drive.google.com/drive/folders/${mediaFolderId}`;\n}\n\n// --------------------------------------------------\n// Resolver link de presentación\n// --------------------------------------------------\n\nconst presentationId = limpiar(\n data.presentation_id ||\n data.presentacion_id ||\n linkPresentacionNormalizado.presentation_id ||\n copiaPresentacion.id ||\n actual.presentation_id ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n data.presentationUrl ||\n data.link_presentacion ||\n linkPresentacionNormalizado.presentacion_ejecucion_url ||\n linkPresentacionNormalizado.presentation_url ||\n linkPresentacionNormalizado.webViewLink ||\n copiaPresentacion.webViewLink ||\n actual.presentacion_ejecucion_url ||\n actual.presentation_url ||\n actual.webViewLink ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\nif (!presentationId) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nconst objetivoSlides = cortar(\n data.objetivo ||\n jsonFinal.objetivo ||\n data.descripcion_ejecucion ||\n jsonFinal.descripcion_ejecucion ||\n data.resumen_ia ||\n jsonFinal.resumen_ia ||\n 'No disponible',\n 900\n);\n\nconst replacements = {\n '{{MARCA}}': limpiarTexto(data.marca || jsonFinal.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente || jsonFinal.cliente),\n '{{PAIS}}': limpiarTexto(data.pais || jsonFinal.pais),\n '{{UBICACION}}': ubicacionSlides,\n\n '{{FECHA_EJECUCION}}': fechaEjecucionSlides,\n '{{PROPUESTA_REFERENCIA}}': propuestaReferenciaSlides,\n '{{OBJETIVO}}': objetivoSlides,\n\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 65392, + 32256 + ], + "id": "9164edd0-4c98-4d7b-845a-704c0786bbc8", + "name": "Code - Preparar reemplazos Slides ejecución TEST" + }, + { + "parameters": { + "method": "POST", + "url": "={{ 'https://slides.googleapis.com/v1/presentations/' + $json.presentation_id + ':batchUpdate' }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleOAuth2Api", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { requests: $json.slides_replace_requests } }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 65664, + 32192 + ], + "id": "a8986614-21f8-4487-81c1-6e54775130d6", + "name": "HTTP Request - Reemplazar textos Slides ejecución TEST", + "credentials": { + "googleOAuth2Api": { + "id": "eHseMeH39kRcXgOF", + "name": "Google account 2" + } + } + }, + { + "parameters": { + "jsCode": "const base = $('Code - Preparar reemplazos Slides ejecución TEST').first().json || {};\nconst respuestaSlides = $json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n slides_batch_update_response: respuestaSlides,\n estado_presentacion: 'TEXTOS_SLIDES_REEMPLAZADOS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 66272, + 32192 + ], + "id": "dffec0ed-b2f6-40b9-b809-fc4a85c49965", + "name": "Code - Normalizar respuesta Slides ejecución TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver sesión WhatsApp TEST');\nconst limpiar = (valor) => String(valor ?? '').trim();\nconst digits = (valor) => limpiar(valor).replace(/\\D/g, '');\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst decision = limpiar(\n actual.decision_flujo ||\n base.decision_flujo\n).toUpperCase();\n\nconst whatsappTo = digits(\n base.whatsapp_to ||\n actual.whatsapp_to ||\n base.whatsapp_remote_jid ||\n actual.whatsapp_remote_jid ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone\n);\n\nif (!whatsappTo) {\n throw new Error('No se encontró el número privado para responder.');\n}\n\nconst intentoCancelar = decision === 'SIN_SESION_PARA_CANCELAR';\n\nlet mensaje = '';\n\nif (idioma === 'EN') {\n mensaje = intentoCancelar\n ? [\n 'ℹ️ *There is no active report to cancel.*',\n '',\n 'You can start a new evidence registration whenever you are ready.',\n '',\n 'Tap *Start report* or write *Hey*.'\n ].join('\\n')\n : [\n '👋 *There is no active report right now.*',\n '',\n 'To register an executed or external proposal, tap *Start report* or write *Hey*.',\n '',\n 'You can also view the country leaderboard.'\n ].join('\\n');\n} else {\n mensaje = intentoCancelar\n ? [\n 'ℹ️ *No hay un reporte activo para cancelar.*',\n '',\n 'Puedes iniciar un nuevo registro de evidencias cuando estés listo.',\n '',\n 'Pulsa *Iniciar reporte* o escribe *Hey*.'\n ].join('\\n')\n : [\n '👋 *No tienes un reporte activo en este momento.*',\n '',\n 'Para registrar una propuesta ejecutada o externa, pulsa *Iniciar reporte* o escribe *Hey*.',\n '',\n 'También puedes consultar el leaderboard por país.'\n ].join('\\n');\n}\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n\n is_group: false,\n group_id: '',\n group_jid: '',\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: 'individual',\n whatsapp_header: idioma === 'EN' ? 'Fulgencio · Private chat' : 'Fulgencio · Chat privado',\n whatsapp_text: mensaje,\n whatsapp_buttons: [\n { id: 'HEY', title: idioma === 'EN' ? 'Start report' : 'Iniciar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ]\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51200, + 26272 + ], + "id": "712972fd-8f38-4f43-bba5-9f1e9033e6c1", + "name": "Code - Preparar sin sesión para cancelar WhatsApp TEST" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 52320, + 28896 + ], + "id": "82279360-59fa-45dd-90ca-380f65a8aab1", + "name": "Sheets - Leer eventos sesión fotos WhatsApp TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = String(valor ?? '').trim();\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n\n return texto;\n};\n\nconst limpiarNombreArchivo = (valor) => {\n return limpiarTexto(valor)\n .replace(/[\\\\/:*?\"<>|#%{}~&]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 120);\n};\n\nconst fechaBase = limpiarTexto(\n data.fecha_ejecucion ||\n data.fecha_recepcion ||\n data.ultima_actualizacion ||\n new Date().toISOString()\n);\n\nconst fecha = fechaBase.slice(0, 10);\n\nconst marca = limpiarNombreArchivo(data.marca);\nconst cliente = limpiarNombreArchivo(data.cliente);\nconst sessionId = limpiarNombreArchivo(data.session_id || data.ejecucion_id);\n\nconst folderName = `${fecha} - ${marca} - ${cliente} - ${sessionId}`;\n\n// Carpeta base PROPUESTAS EJECUTADAS\nconst parentFolderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nreturn [\n {\n json: {\n ...data,\n\n execution_folder_name: folderName,\n execution_parent_folder_id: parentFolderId\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 63568, + 32256 + ], + "id": "2266a140-5b35-4f7c-bf59-d26fa1834bd4", + "name": "Code - Preparar carpeta ejecución TEST" + }, + { + "parameters": { + "resource": "folder", + "name": "={{ $json.execution_folder_name }}", + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "={{ $json.execution_parent_folder_id }}", + "mode": "id" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 63776, + 32256 + ], + "id": "e3ac692e-ffa2-4805-bc96-ccaad95547ec", + "name": "Drive - Crear carpeta ejecución TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const carpeta = $json || {};\nconst base = $('Code - Preparar carpeta ejecución TEST').first().json || {};\n\nconst folderId =\n carpeta.id ||\n carpeta.fileId ||\n carpeta.folderId ||\n '';\n\nconst folderUrl = folderId\n ? `https://drive.google.com/drive/folders/${folderId}`\n : '';\n\nif (!folderId) {\n throw new Error('No se pudo obtener el ID de la carpeta de ejecución.');\n}\n\nreturn [\n {\n json: {\n ...base,\n\n execution_folder_id: folderId,\n execution_folder_url: folderUrl,\n\n media_folder_id: folderId,\n media_folder_url: folderUrl\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 63984, + 32256 + ], + "id": "2371f279-2ba7-42f1-ad70-5f0651a31f14", + "name": "Code - Normalizar carpeta ejecución TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst folderId = texto(data.execution_folder_id || data.media_folder_id);\n\nif (!folderId) {\n throw new Error('No existe execution_folder_id/media_folder_id para mover la multimedia.');\n}\n\nconst archivos = [];\nconst vistos = new Set();\n\nconst extraerFileIdDesdeUrl = (valor) => {\n const url = texto(valor);\n if (!url) return '';\n\n // Formato: https://drive.google.com/file/d/FILE_ID/view\n const matchFile = url.match(/\\/file\\/d\\/([^/]+)/);\n if (matchFile?.[1]) return matchFile[1];\n\n // Formato: https://drive.google.com/open?id=FILE_ID\n const matchOpen = url.match(/[?&]id=([^&]+)/);\n if (matchOpen?.[1]) return matchOpen[1];\n\n // Si ya viene como ID limpio\n if (!url.includes('http') && url.length > 15) return url;\n\n return '';\n};\n\nconst separarLista = (valor) => {\n return texto(valor)\n .split(/[\\n,]+/)\n .map(v => texto(v))\n .filter(Boolean);\n};\n\nconst agregarArchivo = (fileIdOrUrl, tipo) => {\n const fileId = extraerFileIdDesdeUrl(fileIdOrUrl);\n\n if (!fileId) return;\n if (vistos.has(fileId)) return;\n\n vistos.add(fileId);\n\n archivos.push({\n json: {\n ...data,\n\n move_file_id: fileId,\n move_file_type: tipo,\n\n execution_folder_id: folderId,\n media_folder_id: folderId,\n media_folder_url: data.media_folder_url,\n execution_folder_url: data.execution_folder_url\n }\n });\n};\n\n// 1. Archivo actual del item, si existe\nagregarArchivo(data.drive_file_id, data.media_type || 'media');\n\n// 2. URLs acumuladas por tipo\nfor (const url of separarLista(data.audio_drive_urls)) {\n agregarArchivo(url, 'audio');\n}\n\nfor (const url of separarLista(data.imagenes_drive_urls)) {\n agregarArchivo(url, 'image');\n}\n\nfor (const url of separarLista(data.videos_drive_urls)) {\n agregarArchivo(url, 'video');\n}\n\n// 3. Si tienes otros campos futuros\nfor (const url of separarLista(data.media_drive_urls)) {\n agregarArchivo(url, 'media');\n}\n\nif (archivos.length === 0) {\n return [\n {\n json: {\n ...data,\n media_move_status: 'SIN_ARCHIVOS_PARA_MOVER',\n media_move_debug: {\n drive_file_id: data.drive_file_id || '',\n audio_drive_urls: data.audio_drive_urls || '',\n imagenes_drive_urls: data.imagenes_drive_urls || '',\n videos_drive_urls: data.videos_drive_urls || ''\n }\n }\n }\n ];\n}\n\nreturn archivos;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 64192, + 32256 + ], + "id": "e97f4aa8-3246-44e7-834b-fb5d11d1b8ae", + "name": "Code - Preparar media para mover carpeta TEST" + }, + { + "parameters": { + "operation": "move", + "fileId": { + "__rl": true, + "value": "={{ $json.move_file_id }}", + "mode": "id" + }, + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "={{ $json.execution_folder_id }}", + "mode": "id" + } + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 64400, + 32256 + ], + "id": "1ffede49-d832-4faf-8881-67053e7033bc", + "name": "Drive - Mover media a carpeta ejecución TEST", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const movido = $json || {};\nconst base = $('Code - Preparar media para mover carpeta TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n media_move_status: 'MEDIA_MOVIDA_A_CARPETA',\n moved_file_id: base.move_file_id,\n moved_file_type: base.move_file_type,\n moved_file_url: base.move_file_url,\n\n drive_move_response_id: movido.id || movido.fileId || base.move_file_id\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 64608, + 32256 + ], + "id": "bcccfab4-334b-4172-a304-da015f923bcc", + "name": "Code - Confirmar media movida carpeta TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Preparar reemplazos Slides ejecución TEST');\n\nconst data = {\n ...base,\n ...actual,\n\n // Aseguramos estos campos desde el nodo base si el nodo Slides no los devuelve\n presentation_id: actual.presentation_id || base.presentation_id,\n imagenes_drive_urls: actual.imagenes_drive_urls || base.imagenes_drive_urls,\n imagen_drive_urls: actual.imagen_drive_urls || base.imagen_drive_urls,\n media_drive_urls: actual.media_drive_urls || base.media_drive_urls,\n};\n\nconst texto = (valor) => String(valor ?? '').trim();\n\nconst extraerFileIdDesdeUrl = (valor) => {\n const url = texto(valor);\n if (!url) return '';\n\n const matchFile = url.match(/\\/file\\/d\\/([^/]+)/);\n if (matchFile?.[1]) return matchFile[1];\n\n const matchOpen = url.match(/[?&]id=([^&]+)/);\n if (matchOpen?.[1]) return matchOpen[1];\n\n if (!url.includes('http') && url.length > 15) return url;\n\n return '';\n};\n\nconst separarLista = (valor) => {\n return texto(valor)\n .split(/[\\n,]+/)\n .map(v => texto(v))\n .filter(Boolean);\n};\n\nconst imagenesUrls = separarLista(\n data.imagenes_drive_urls ||\n data.imagen_drive_urls ||\n data.media_drive_urls ||\n ''\n);\n\nconst imageFileIds = imagenesUrls\n .map(extraerFileIdDesdeUrl)\n .filter(Boolean)\n .slice(0, 25)\n\nif (!data.presentation_id) {\n throw new Error('No llegó presentation_id para insertar imágenes en Slides.');\n}\n\nreturn [\n {\n json: {\n ...data,\n\n apps_script_insert_images_payload: {\n secret: 'glm_fulgencio_slides_2026_seguro',\n presentation_id: data.presentation_id,\n image_file_ids: imageFileIds\n },\n\n imagenes_para_slides_count: imageFileIds.length,\n imagenes_para_slides_ids: imageFileIds,\n\n estado_insertar_imagenes: 'PAYLOAD_INSERTAR_IMAGENES_PREPARADO'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 65872, + 32192 + ], + "id": "8b5a1035-fbb4-4852-a3ca-c01119d12e9f", + "name": "Code - Preparar payload insertar imágenes Slides TEST" + }, + { + "parameters": { + "method": "POST", + "url": "https://script.google.com/macros/s/AKfycbyw76HYjYMvq0KXA9IN5S8nRWk0drPkjKSK2SAbCL9Ha8is3vFZMyR6Ldby4c3YVs7J/exec", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.apps_script_insert_images_payload }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 66080, + 32192 + ], + "id": "5cad6df0-8176-460e-9f4f-7a67e8388695", + "name": "HTTP - Insertar imágenes en Slides TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst base = getNodeJson('Code - Resolver paso activo WhatsApp TEST');\nconst sesion = base.sesion_activa || actual.sesion_activa || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\nconst digits = (valor) => limpiar(valor).replace(/\\D/g, '');\n\nconst idioma = limpiar(\n actual.idioma_flujo ||\n base.idioma_flujo ||\n sesion.idioma_flujo ||\n 'ES'\n).toUpperCase();\n\nconst whatsappTo = digits(\n actual.whatsapp_to ||\n base.whatsapp_to ||\n sesion.manager_telefono ||\n base.manager_telefono ||\n actual.manager_telefono ||\n base.sender_phone ||\n actual.sender_phone\n);\n\nif (!whatsappTo) {\n throw new Error('No se encontró el número privado para enviar el aviso de procesamiento.');\n}\n\nconst mensaje = idioma === 'EN'\n ? [\n '⏳ *Report received.*',\n '',\n 'We are processing your evidence now.',\n '',\n 'I will send the final link in this private chat when everything is ready.'\n ].join('\\n')\n : [\n '⏳ *Reporte recibido.*',\n '',\n 'Estamos procesando tus evidencias.',\n '',\n 'Te enviaré el enlace final por este chat privado cuando todo esté listo.'\n ].join('\\n');\n\nreturn [\n {\n json: {\n ...base,\n ...actual,\n is_group: false,\n group_id: '',\n group_jid: '',\n whatsapp_to: whatsappTo,\n whatsapp_recipient_type: 'individual',\n whatsapp_text: mensaje,\n aviso_procesando_preparado: true\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 50208, + 31136 + ], + "id": "3e68bbb5-7c95-4d62-bf50-328cbb4886a5", + "name": "Code - Preparar aviso procesando WhatsApp TEST1" + }, + { + "parameters": { + "jsCode": "const respuesta = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nlet base =\n getNodeJson('Code - Preparar aviso procesando WhatsApp TEST') ||\n getNodeJson('Code - Preparar aviso procesando WhatsApp TEST1') ||\n {};\n\nif (!base || Object.keys(base).length === 0) {\n base = getNodeJson('Code - Resolver paso activo WhatsApp TEST') || {};\n}\n\nreturn [\n {\n json: {\n ...base,\n\n aviso_procesando_enviado: true,\n aviso_procesando_response: respuesta\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 50656, + 31136 + ], + "id": "5a676700-a3b8-4854-9a16-bdffb7733eda", + "name": "Code - Restaurar contexto aviso procesando TEST" + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v26.0/1151359118067218/messages", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "whatsAppApi", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ (() => {\n const text = (value) => String(value ?? '');\n\n const cleanMultiline = (value) =>\n text(value)\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\\\r\\\\n/g, '\\n')\n .replace(/\\\\n/g, '\\n')\n .replace(/[ \\t]+\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n\n const cleanSingleLine = (value) =>\n cleanMultiline(value)\n .replace(/\\s*\\n\\s*/g, ' ')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n\n const digits = (value) => text(value).replace(/\\D/g, '');\n\n const destination = digits(\n $json.whatsapp_to ||\n $json.sender_phone ||\n $json.manager_telefono ||\n $json.whatsapp_remote_jid ||\n ''\n );\n\n if (!destination) {\n throw new Error('No se encontró el número privado de destino de WhatsApp.');\n }\n\n const bodyText = cleanMultiline(\n $json.whatsapp_text ||\n $json.texto_respuesta ||\n $json.mensaje ||\n $json.text ||\n $json.message_text ||\n ''\n );\n\n const headerText = cleanSingleLine(\n $json.whatsapp_header || ''\n ).slice(0, 60);\n\n const footerText = cleanSingleLine(\n $json.whatsapp_footer ||\n 'GomezLee Marketing · Fulgencio'\n ).slice(0, 60);\n\n const buttons = Array.isArray($json.whatsapp_buttons)\n ? $json.whatsapp_buttons\n .map((button, index) => ({\n id: cleanSingleLine(\n button?.id || `OPTION_${index + 1}`\n ).slice(0, 256),\n title: cleanSingleLine(\n button?.title || `Opción ${index + 1}`\n ).slice(0, 20)\n }))\n .filter((button) => button.id && button.title)\n .slice(0, 3)\n : [];\n\n if (buttons.length > 0) {\n const interactive = {\n type: 'button',\n body: {\n text: (bodyText || 'Selecciona una opción:').slice(0, 1024)\n },\n action: {\n buttons: buttons.map((button) => ({\n type: 'reply',\n reply: {\n id: button.id,\n title: button.title\n }\n }))\n }\n };\n\n if (headerText) {\n interactive.header = {\n type: 'text',\n text: headerText\n };\n }\n\n if (footerText) {\n interactive.footer = {\n text: footerText\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: destination,\n type: 'interactive',\n interactive\n };\n }\n\n return {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: destination,\n type: 'text',\n text: {\n preview_url: true,\n body: (bodyText || 'Mensaje sin contenido').slice(0, 4096)\n }\n };\n})() }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 50432, + 31136 + ], + "id": "a3f7a9d8-b0ba-4bb7-87fc-a9f37f7abca3", + "name": "WhatsApp - Enviar aviso procesando API Oficial", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 5000, + "credentials": { + "whatsAppApi": { + "id": "t14kVayc9FurLReq", + "name": "WhatsApp API - GLM CDC" + } + } + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "options": { + "dataLocationOnSheet": { + "values": { + "rangeDefinition": "detectAutomatically" + } + }, + "outputFormatting": { + "values": { + "general": "UNFORMATTED_VALUE", + "date": "FORMATTED_STRING" + } + }, + "returnFirstMatch": false + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 61616, + 32208 + ], + "id": "f48fc623-48b5-4d0f-a2d4-c2e978d8eeba", + "name": "Sheets - Leer banco propuestas Fulgencio TEST", + "alwaysOutputData": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst propuesta =\n getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST') ||\n $json ||\n {};\n\nconst bancoRows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) => {\n return limpiar(valor)\n .replace(/\\+/g, ' mas ')\n .replace(/_/g, ' ')\n .replace(/-/g, ' ')\n .replace(/\\//g, ' ')\n .replace(/\\./g, ' ')\n .replace(/\\bGUATE\\b/gi, ' Guatemala ')\n .replace(/\\bGUA\\b/gi, ' Guatemala ')\n .replace(/\\bGT\\b/gi, ' Guatemala ')\n .replace(/\\bRD\\b/gi, ' Republica Dominicana ')\n .replace(/\\bREP DOM\\b/gi, ' Republica Dominicana ')\n .replace(/\\bREP DOMINICANA\\b/gi, ' Republica Dominicana ')\n .replace(/\\bDOMINICANA\\b/gi, ' Republica Dominicana ')\n .replace(/\\bCR\\b/gi, ' Costa Rica ')\n .replace(/\\bPAN\\b/gi, ' Panama ')\n .replace(/\\bPTY\\b/gi, ' Panama ')\n .replace(/\\bSV\\b/gi, ' El Salvador ')\n .replace(/\\bSALVADOR\\b/gi, ' El Salvador ')\n .replace(/\\bHND\\b/gi, ' Honduras ')\n .replace(/\\bNIC\\b/gi, ' Nicaragua ')\n .replace(/\\bWMC\\b/gi, ' WMC ')\n .replace(/\\bRTM\\b/gi, ' RTM ')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst tokens = (valor) => {\n const stopwords = new Set([\n 'de', 'del', 'la', 'el', 'los', 'las', 'y', 'en', 'para', 'por', 'con',\n 'una', 'un', 'uno', 'dos', 'tres', 'the', 'of', 'and', 'for', 'to', 'a',\n 'propuesta', 'presentacion', 'presentación', 'ejecucion', 'ejecución',\n 'ejecutada', 'ejecutado', 'reporte', 'evidencia', 'version', 'versión',\n 'archivo', 'link', 'slide', 'slides', 'prueba'\n ]);\n\n return normalizar(valor)\n .split(' ')\n .map(t => t.trim())\n .filter(Boolean)\n .filter(t => t.length >= 3 || /^v\\d+$/i.test(t))\n .filter(t => !stopwords.has(t));\n};\n\nconst tokenSet = (valor) => new Set(tokens(valor));\n\nconst similitudTokens = (a, b) => {\n const ta = tokenSet(a);\n const tb = tokenSet(b);\n\n if (ta.size === 0 || tb.size === 0) return 0;\n\n let interseccion = 0;\n\n for (const token of ta) {\n if (tb.has(token)) interseccion++;\n }\n\n const union = new Set([...ta, ...tb]).size;\n\n return union === 0 ? 0 : interseccion / union;\n};\n\nconst coberturaTokens = (textoBase, textoContra) => {\n const base = [...tokenSet(textoBase)];\n const contra = tokenSet(textoContra);\n\n if (base.length === 0) return 0;\n\n const encontrados = base.filter(t => contra.has(t));\n\n return encontrados.length / base.length;\n};\n\nconst contarTokensComunes = (a, b) => {\n const ta = tokenSet(a);\n const tb = tokenSet(b);\n\n let comunes = 0;\n\n for (const token of ta) {\n if (tb.has(token)) comunes++;\n }\n\n return comunes;\n};\n\nconst obtenerCampo = (row, posiblesNombres) => {\n const keys = Object.keys(row || {});\n\n for (const nombre of posiblesNombres) {\n const exacto = keys.find(k => normalizar(k) === normalizar(nombre));\n if (exacto && limpiar(row[exacto])) return limpiar(row[exacto]);\n }\n\n for (const nombre of posiblesNombres) {\n const parcial = keys.find(k => normalizar(k).includes(normalizar(nombre)));\n if (parcial && limpiar(row[parcial])) return limpiar(row[parcial]);\n }\n\n return '';\n};\n\nconst extraerVersion = (valor) => {\n const texto = normalizar(valor);\n const match = texto.match(/\\bv\\d+\\b/);\n return match ? match[0].toUpperCase() : '';\n};\n\nconst extraerAnio = (valor) => {\n const texto = limpiar(valor);\n const match = texto.match(/\\b20\\d{2}\\b/);\n return match ? match[0] : '';\n};\n\nconst contienePais = (texto, pais) => {\n const t = normalizar(texto);\n const p = normalizar(pais);\n\n if (!p) return false;\n\n if (t.includes(p)) return true;\n\n if (p.includes('guatemala') && t.includes('guatemala')) return true;\n if (p.includes('republica dominicana') && t.includes('republica dominicana')) return true;\n if (p.includes('costa rica') && t.includes('costa rica')) return true;\n if (p.includes('panama') && t.includes('panama')) return true;\n if (p.includes('salvador') && t.includes('salvador')) return true;\n if (p.includes('honduras') && t.includes('honduras')) return true;\n if (p.includes('nicaragua') && t.includes('nicaragua')) return true;\n\n return false;\n};\n\nconst tipoReporte = limpiar(propuesta.tipo_reporte).toUpperCase();\n\nif (tipoReporte !== 'PROPUESTA_EJECUTADA') {\n return [\n {\n json: {\n ...propuesta,\n\n propuesta_match_estado:\n tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'NO_APLICA_PROPUESTA_EXTERNA'\n : 'NO_APLICA_NO_DETERMINADO',\n\n propuesta_match_confianza: '0',\n propuesta_nombre_banco: '',\n propuesta_link_banco: '',\n propuesta_banco_row_number: '',\n propuesta_banco_enlaces_existentes: '',\n propuesta_banco_file_id: '',\n\n propuesta_match_revision:\n tipoReporte === 'PROPUESTA_EXTERNA'\n ? 'NO_REQUIERE_MATCH_BANCO'\n : 'REQUIERE_REVISION_TIPO_NO_DETERMINADO',\n\n match_banco_debug: {\n motivo: 'No se ejecutó match porque el tipo_reporte no es PROPUESTA_EJECUTADA.',\n tipo_reporte: tipoReporte || 'NO_DETERMINADO'\n }\n }\n }\n ];\n}\n\nconst referencia = limpiar(propuesta.propuesta_referencia);\nconst marca = limpiar(propuesta.marca);\nconst cliente = limpiar(propuesta.cliente);\nconst pais = limpiar(propuesta.pais);\nconst ubicacion = limpiar(propuesta.ubicacion);\nconst descripcion = limpiar(propuesta.descripcion_ejecucion || propuesta.resumen_ia);\nconst elementosDetectados = limpiar(propuesta.elementos_detectados);\nconst comentarioOriginal = limpiar(propuesta.comentario_original);\nconst transcripcionAudio = limpiar(propuesta.transcripcion_audio);\nconst textoAudio = limpiar(propuesta.texto_audio);\nconst textoImagenes = limpiar(propuesta.texto_imagenes);\nconst tags = limpiar(propuesta.tags);\n\nconst textoPropuestaCompleto = [\n referencia,\n marca,\n cliente,\n pais,\n ubicacion,\n descripcion,\n elementosDetectados,\n comentarioOriginal,\n transcripcionAudio,\n textoAudio,\n textoImagenes,\n tags\n].filter(Boolean).join(' ');\n\nconst textoClavePropuesta = [\n referencia,\n marca,\n cliente,\n pais,\n elementosDetectados,\n textoImagenes,\n tags\n].filter(Boolean).join(' ');\n\nconst versionPropuesta = extraerVersion(textoPropuestaCompleto);\nconst anioPropuesta = extraerAnio(textoPropuestaCompleto);\n\nconst candidatos = [];\n\nfor (const [index, row] of bancoRows.entries()) {\n const bancoRowNumber = String(index + 2);\n\n const bancoNombre = obtenerCampo(row, [\n 'NOMBRE',\n 'propuesta',\n 'nombre propuesta',\n 'nombre de propuesta',\n 'titulo',\n 'título',\n 'nombre',\n 'referencia',\n 'brief',\n 'idea'\n ]);\n\n const bancoTipoAccion = obtenerCampo(row, [\n 'TIPO DE ACCION',\n 'TIPO DE ACCIÓN',\n 'tipo accion',\n 'tipo acción'\n ]);\n\n const bancoCliente = obtenerCampo(row, [\n 'CLIENTE',\n 'cliente',\n 'client'\n ]);\n\n const bancoMarca = obtenerCampo(row, [\n 'MARCA',\n 'marca',\n 'brand'\n ]);\n\n const bancoPais = obtenerCampo(row, [\n 'PAIS',\n 'PAÍS',\n 'pais',\n 'país',\n 'country'\n ]);\n\n const bancoCanal = obtenerCampo(row, [\n 'CANAL',\n 'canal'\n ]);\n\n const bancoAmbiente = obtenerCampo(row, [\n 'AMBIENTE DE COMPRA',\n 'ambiente de compra',\n 'ambiente'\n ]);\n\n const bancoDescripcion = obtenerCampo(row, [\n 'Descripcion',\n 'Descripción',\n 'descripcion',\n 'descripción',\n 'detalle',\n 'mecanica',\n 'mecánica',\n 'comentario',\n 'observacion',\n 'observación'\n ]);\n\n const bancoLink = obtenerCampo(row, [\n 'Enlace a la propuesta',\n 'link',\n 'enlace',\n 'url',\n 'brief link',\n 'link brief',\n 'presentacion',\n 'presentación',\n 'drive'\n ]);\n\n const bancoFileId = obtenerCampo(row, [\n 'file_id',\n 'File ID',\n 'archivo_id'\n ]);\n\n const bancoEnlacesEjecutadas = obtenerCampo(row, [\n 'Enlaces a propuestas ejecutadas',\n 'Enlace a propuestas ejecutadas',\n 'Links propuestas ejecutadas',\n 'Link propuestas ejecutadas',\n 'Ejecuciones',\n 'Propuestas ejecutadas'\n ]);\n\n if (!bancoNombre && !bancoMarca && !bancoCliente && !bancoDescripcion) {\n continue;\n }\n\n const textoBancoCompleto = [\n bancoNombre,\n bancoTipoAccion,\n bancoCliente,\n bancoMarca,\n bancoPais,\n bancoCanal,\n bancoAmbiente,\n bancoDescripcion\n ].filter(Boolean).join(' ');\n\n const textoClaveBanco = [\n bancoNombre,\n bancoCliente,\n bancoMarca,\n bancoPais\n ].filter(Boolean).join(' ');\n\n const scoreReferenciaNombre = similitudTokens(referencia, bancoNombre);\n const scoreEvidenciaNombre = similitudTokens(textoClavePropuesta, bancoNombre);\n const coberturaNombreEnEvidencia = coberturaTokens(bancoNombre, textoPropuestaCompleto);\n const coberturaReferenciaEnBanco = coberturaTokens(referencia, textoBancoCompleto);\n\n const scoreNombre = Math.max(\n scoreReferenciaNombre,\n scoreEvidenciaNombre,\n coberturaNombreEnEvidencia,\n coberturaReferenciaEnBanco\n );\n\n const scoreMarcaContraBanco = Math.max(\n similitudTokens(marca, bancoMarca),\n similitudTokens(marca, bancoNombre),\n similitudTokens(marca, textoBancoCompleto)\n );\n\n const scoreClienteContraBanco = Math.max(\n similitudTokens(cliente, bancoCliente),\n similitudTokens(cliente, bancoNombre),\n similitudTokens(cliente, textoBancoCompleto)\n );\n\n const scorePais = Math.max(\n similitudTokens(pais, bancoPais),\n contienePais(bancoNombre, pais) ? 1 : 0,\n contienePais(textoBancoCompleto, pais) ? 1 : 0\n );\n\n const scoreGeneral = similitudTokens(textoPropuestaCompleto, textoBancoCompleto);\n const scoreClave = similitudTokens(textoClavePropuesta, textoClaveBanco);\n const coberturaBancoEnPropuesta = coberturaTokens(bancoNombre, textoPropuestaCompleto);\n const comunesNombreEvidencia = contarTokensComunes(bancoNombre, textoPropuestaCompleto);\n\n const versionBanco = extraerVersion(textoBancoCompleto);\n const anioBanco = extraerAnio(textoBancoCompleto);\n\n const versionCoincide =\n versionPropuesta &&\n versionBanco &&\n versionPropuesta === versionBanco;\n\n const versionContradice =\n versionPropuesta &&\n versionBanco &&\n versionPropuesta !== versionBanco;\n\n const anioCoincide =\n anioPropuesta &&\n anioBanco &&\n anioPropuesta === anioBanco;\n\n const anioContradice =\n anioPropuesta &&\n anioBanco &&\n anioPropuesta !== anioBanco;\n\n const paisCoincide =\n scorePais >= 0.80 ||\n contienePais(bancoNombre, pais) ||\n contienePais(textoBancoCompleto, pais);\n\n const marcaCoincide =\n scoreMarcaContraBanco >= 0.45 ||\n normalizar(textoBancoCompleto).includes(normalizar(marca)) ||\n normalizar(textoPropuestaCompleto).includes(normalizar(bancoMarca));\n\n const scorePonderado =\n scoreNombre * 0.46 +\n scoreClave * 0.22 +\n scoreMarcaContraBanco * 0.08 +\n scoreClienteContraBanco * 0.04 +\n scorePais * 0.10 +\n scoreGeneral * 0.10;\n\n let scoreFinal = scorePonderado;\n let reglaMatch = 'PONDERADO';\n\n // Regla 1: el nombre/referencia de la propuesta está claramente contenido en la evidencia.\n if (coberturaBancoEnPropuesta >= 0.85 && (paisCoincide || anioCoincide || versionCoincide)) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.97 : 0.93);\n reglaMatch = versionCoincide\n ? 'NOMBRE_BANCO_EN_EVIDENCIA_CON_VERSION'\n : 'NOMBRE_BANCO_EN_EVIDENCIA';\n }\n\n // Regla 2: combinación fuerte de clave propuesta-banco.\n if (scoreClave >= 0.85 && (paisCoincide || anioCoincide || marcaCoincide)) {\n scoreFinal = Math.max(scoreFinal, 0.95);\n reglaMatch = 'CLAVE_COMPLETA_PROPUESTA_BANCO';\n } else if (scoreClave >= 0.75 && (paisCoincide || versionCoincide)) {\n scoreFinal = Math.max(scoreFinal, 0.90);\n reglaMatch = 'CLAVE_FUERTE_PROPUESTA_BANCO';\n }\n\n // Regla 3: referencia parcial + país + versión.\n if (scoreNombre >= 0.60 && paisCoincide && versionCoincide) {\n scoreFinal = Math.max(scoreFinal, 0.94);\n reglaMatch = 'REFERENCIA_PAIS_VERSION';\n }\n\n // Regla 4: muchos tokens comunes, aunque Gemini haya escrito el nombre distinto.\n if (comunesNombreEvidencia >= 5 && (paisCoincide || anioCoincide)) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.96 : 0.88);\n reglaMatch = versionCoincide\n ? 'TOKENS_COMUNES_PAIS_VERSION'\n : 'TOKENS_COMUNES_PAIS';\n }\n\n // Regla 5: para casos como RTM+ vs RTM más, GUATE vs Guatemala.\n if (\n normalizar(textoPropuestaCompleto).includes('rtm') &&\n normalizar(textoBancoCompleto).includes('rtm') &&\n normalizar(textoPropuestaCompleto).includes('wmc') &&\n normalizar(textoBancoCompleto).includes('wmc') &&\n (paisCoincide || normalizar(textoBancoCompleto).includes('guatemala')) &&\n (marcaCoincide || normalizar(textoBancoCompleto).includes('campero'))\n ) {\n scoreFinal = Math.max(scoreFinal, versionCoincide ? 0.96 : 0.89);\n reglaMatch = versionCoincide\n ? 'RTM_WMC_MARCA_PAIS_VERSION'\n : 'RTM_WMC_MARCA_PAIS';\n }\n\n // Penalizaciones de seguridad.\n if (anioContradice) {\n scoreFinal = Math.min(scoreFinal, 0.59);\n reglaMatch = `${reglaMatch}_PENALIZADO_ANIO`;\n }\n\n if (versionContradice && scoreFinal >= 0.85) {\n scoreFinal = Math.min(scoreFinal, 0.79);\n reglaMatch = `${reglaMatch}_PENALIZADO_VERSION`;\n }\n\n // Si no coincide ni país, ni marca, ni año, no lo dejamos como alta confianza.\n if (\n scoreFinal >= 0.85 &&\n !paisCoincide &&\n !marcaCoincide &&\n !anioCoincide\n ) {\n scoreFinal = 0.74;\n reglaMatch = `${reglaMatch}_PENALIZADO_SIN_ANCLA`;\n }\n\n let bonusOrden = 0;\n\n if (versionCoincide) bonusOrden += 0.04;\n if (anioCoincide) bonusOrden += 0.02;\n if (paisCoincide) bonusOrden += 0.02;\n if (marcaCoincide) bonusOrden += 0.01;\n if (bancoFileId) bonusOrden += 0.005;\n\n candidatos.push({\n row,\n score: scoreFinal,\n score_orden: scoreFinal + bonusOrden,\n score_ponderado: scorePonderado,\n score_porcentaje: Math.round(scoreFinal * 100),\n\n score_nombre: Math.round(scoreNombre * 100),\n score_referencia_nombre: Math.round(scoreReferenciaNombre * 100),\n score_evidencia_nombre: Math.round(scoreEvidenciaNombre * 100),\n cobertura_nombre_en_evidencia: Math.round(coberturaNombreEnEvidencia * 100),\n cobertura_referencia_en_banco: Math.round(coberturaReferenciaEnBanco * 100),\n score_marca: Math.round(scoreMarcaContraBanco * 100),\n score_cliente: Math.round(scoreClienteContraBanco * 100),\n score_pais: Math.round(scorePais * 100),\n score_general: Math.round(scoreGeneral * 100),\n score_clave: Math.round(scoreClave * 100),\n cobertura_banco_en_propuesta: Math.round(coberturaBancoEnPropuesta * 100),\n comunes_nombre_evidencia: comunesNombreEvidencia,\n\n version_propuesta: versionPropuesta,\n version_banco: versionBanco,\n version_coincide: Boolean(versionCoincide),\n version_contradice: Boolean(versionContradice),\n\n anio_propuesta: anioPropuesta,\n anio_banco: anioBanco,\n anio_coincide: Boolean(anioCoincide),\n anio_contradice: Boolean(anioContradice),\n\n pais_coincide: Boolean(paisCoincide),\n marca_coincide: Boolean(marcaCoincide),\n\n regla_match: reglaMatch,\n\n banco_row_number: bancoRowNumber,\n banco_nombre: bancoNombre,\n banco_tipo_accion: bancoTipoAccion,\n banco_marca: bancoMarca,\n banco_cliente: bancoCliente,\n banco_pais: bancoPais,\n banco_canal: bancoCanal,\n banco_link: bancoLink,\n banco_file_id: bancoFileId,\n banco_enlaces_ejecutadas: bancoEnlacesEjecutadas\n });\n}\n\nconst candidatosValidos = candidatos\n .filter(c => c.score > 0)\n .sort((a, b) => {\n if (b.score_orden !== a.score_orden) return b.score_orden - a.score_orden;\n return Number(b.banco_row_number || 0) - Number(a.banco_row_number || 0);\n });\n\nconst mejor = candidatosValidos[0] || null;\nconst segundo = candidatosValidos[1] || null;\n\nlet propuestaMatchEstado = 'SIN_MATCH';\nlet propuestaMatchRevision = 'REQUIERE_REVISION_MANUAL';\nlet propuestaMatchConfianza = '0';\nlet propuestaNombreBanco = '';\nlet propuestaLinkBanco = '';\nlet propuestaBancoRowNumber = '';\nlet propuestaBancoEnlacesExistentes = '';\nlet propuestaBancoFileId = '';\n\nif (mejor) {\n let confianzaFinal = mejor.score_porcentaje;\n\n // Si el segundo está demasiado cerca, mantenemos match pero pedimos revisión.\n const segundoMuyCerca =\n segundo &&\n mejor.score_porcentaje >= 85 &&\n segundo.score_porcentaje >= 85 &&\n Math.abs(mejor.score_porcentaje - segundo.score_porcentaje) <= 2 &&\n normalizar(mejor.banco_nombre) !== normalizar(segundo.banco_nombre);\n\n propuestaNombreBanco = mejor.banco_nombre || 'No identificado';\n propuestaLinkBanco = mejor.banco_link || '';\n propuestaBancoRowNumber = mejor.banco_row_number || '';\n propuestaBancoEnlacesExistentes = mejor.banco_enlaces_ejecutadas || '';\n propuestaBancoFileId = mejor.banco_file_id || '';\n\n if (confianzaFinal >= 85 && !segundoMuyCerca) {\n propuestaMatchEstado = 'MATCH_ALTA_CONFIANZA';\n propuestaMatchRevision = 'NO_REQUIERE_REVISION';\n } else if (confianzaFinal >= 85 && segundoMuyCerca) {\n propuestaMatchEstado = 'MATCH_MEDIA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_VALIDACION_MANUAL';\n confianzaFinal = Math.min(confianzaFinal, 84);\n } else if (confianzaFinal >= 60) {\n propuestaMatchEstado = 'MATCH_MEDIA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_VALIDACION_MANUAL';\n } else {\n propuestaMatchEstado = 'MATCH_BAJA_CONFIANZA';\n propuestaMatchRevision = 'REQUIERE_REVISION_MANUAL';\n }\n\n propuestaMatchConfianza = String(confianzaFinal);\n}\n\nreturn [\n {\n json: {\n ...propuesta,\n\n propuesta_match_estado: propuestaMatchEstado,\n propuesta_match_confianza: propuestaMatchConfianza,\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_link_banco: propuestaLinkBanco,\n propuesta_match_revision: propuestaMatchRevision,\n\n propuesta_banco_row_number: propuestaBancoRowNumber,\n propuesta_banco_enlaces_existentes: propuestaBancoEnlacesExistentes,\n propuesta_banco_file_id: propuestaBancoFileId,\n\n match_banco_debug: {\n total_filas_banco: bancoRows.length,\n total_candidatos: candidatosValidos.length,\n propuesta_normalizada: {\n referencia,\n marca,\n cliente,\n pais,\n version_propuesta: versionPropuesta,\n anio_propuesta: anioPropuesta,\n texto_clave: textoClavePropuesta\n },\n mejor_match: mejor\n ? {\n score: mejor.score_porcentaje,\n score_orden: Math.round(mejor.score_orden * 100),\n regla_match: mejor.regla_match,\n row_number: mejor.banco_row_number,\n nombre: mejor.banco_nombre,\n tipo_accion: mejor.banco_tipo_accion,\n marca: mejor.banco_marca,\n cliente: mejor.banco_cliente,\n pais: mejor.banco_pais,\n canal: mejor.banco_canal,\n link: mejor.banco_link,\n file_id: mejor.banco_file_id,\n enlaces_ejecutadas_existentes: mejor.banco_enlaces_ejecutadas,\n score_nombre: mejor.score_nombre,\n score_referencia_nombre: mejor.score_referencia_nombre,\n score_evidencia_nombre: mejor.score_evidencia_nombre,\n cobertura_nombre_en_evidencia: mejor.cobertura_nombre_en_evidencia,\n cobertura_referencia_en_banco: mejor.cobertura_referencia_en_banco,\n score_marca: mejor.score_marca,\n score_cliente: mejor.score_cliente,\n score_pais: mejor.score_pais,\n score_general: mejor.score_general,\n score_clave: mejor.score_clave,\n cobertura_banco_en_propuesta: mejor.cobertura_banco_en_propuesta,\n comunes_nombre_evidencia: mejor.comunes_nombre_evidencia,\n version_banco: mejor.version_banco,\n version_coincide: mejor.version_coincide,\n anio_banco: mejor.anio_banco,\n anio_coincide: mejor.anio_coincide,\n pais_coincide: mejor.pais_coincide,\n marca_coincide: mejor.marca_coincide\n }\n : null,\n segundo_match: segundo\n ? {\n score: segundo.score_porcentaje,\n regla_match: segundo.regla_match,\n row_number: segundo.banco_row_number,\n nombre: segundo.banco_nombre,\n marca: segundo.banco_marca,\n cliente: segundo.banco_cliente,\n pais: segundo.banco_pais,\n file_id: segundo.banco_file_id\n }\n : null,\n top_5: candidatosValidos.slice(0, 5).map(c => ({\n score: c.score_porcentaje,\n score_orden: Math.round(c.score_orden * 100),\n regla_match: c.regla_match,\n row_number: c.banco_row_number,\n nombre: c.banco_nombre,\n marca: c.banco_marca,\n cliente: c.banco_cliente,\n pais: c.banco_pais,\n file_id: c.banco_file_id,\n score_nombre: c.score_nombre,\n score_clave: c.score_clave,\n cobertura_banco_en_propuesta: c.cobertura_banco_en_propuesta,\n comunes_nombre_evidencia: c.comunes_nombre_evidencia,\n version_banco: c.version_banco,\n version_coincide: c.version_coincide,\n anio_banco: c.anio_banco,\n anio_coincide: c.anio_coincide\n }))\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 61824, + 32208 + ], + "id": "c61e1a94-b6b9-4166-a670-473d407f9d9f", + "name": "Code - Match propuesta banco Fulgencio TEST" + }, + { + "parameters": { + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst toNumber = (valor) => {\n const limpio = limpiar(valor).replace(',', '.');\n const n = Number(limpio);\n return Number.isFinite(n) ? n : 0;\n};\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst slidesNormalizado1 = getNodeJson('Code - Normalizar respuesta Slides TEST');\nconst slidesNormalizado2 = getNodeJson('Code - Normalizar respuesta Slides ejecución TEST');\nconst slidesNormalizado3 = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...match,\n ...enriquecido,\n ...actual,\n ...slidesNormalizado1,\n ...slidesNormalizado2,\n ...slidesNormalizado3\n};\n\nconst matchDebug =\n match.match_banco_debug ||\n actual.match_banco_debug ||\n data.match_banco_debug ||\n {};\n\nconst mejorMatch = matchDebug.mejor_match || {};\n\n// --------------------------------------------------\n// 1. Resolver datos principales\n// --------------------------------------------------\n\nconst tipoReporte = normalizarPlano(\n jsonFinal.tipo_reporte ||\n data.tipo_reporte ||\n actual.tipo_reporte ||\n ''\n);\n\nlet matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n actual.propuesta_match_estado ||\n ''\n);\n\nlet matchConfianza = toNumber(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n actual.propuesta_match_confianza ||\n mejorMatch.score ||\n 0\n);\n\nlet nombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n actual.propuesta_nombre_banco ||\n mejorMatch.nombre ||\n ''\n);\n\nconst linkBancoOriginal = limpiar(\n match.propuesta_link_banco ||\n data.propuesta_link_banco ||\n actual.propuesta_link_banco ||\n mejorMatch.link ||\n ''\n);\n\nconst fileIdBanco = limpiar(\n match.propuesta_banco_file_id ||\n data.propuesta_banco_file_id ||\n actual.propuesta_banco_file_id ||\n mejorMatch.file_id ||\n ''\n);\n\nconst rowNumberBanco = limpiar(\n match.propuesta_banco_row_number ||\n data.propuesta_banco_row_number ||\n actual.propuesta_banco_row_number ||\n mejorMatch.row_number ||\n ''\n);\n\nconst enlacesExistentes = limpiar(\n match.propuesta_banco_enlaces_existentes ||\n data.propuesta_banco_enlaces_existentes ||\n actual.propuesta_banco_enlaces_existentes ||\n data.banco_enlaces_existentes ||\n actual.banco_enlaces_existentes ||\n mejorMatch.enlaces_ejecutadas_existentes ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n actual.presentacion_ejecucion_url ||\n data.presentation_url ||\n actual.presentation_url ||\n data.presentationUrl ||\n actual.presentationUrl ||\n data.webViewLink ||\n actual.webViewLink ||\n data.link_presentacion ||\n actual.link_presentacion ||\n slidesNormalizado3.presentacion_ejecucion_url ||\n slidesNormalizado3.presentation_url ||\n slidesNormalizado3.webViewLink ||\n ''\n);\n\nconst presentationId = limpiar(\n data.presentation_id ||\n actual.presentation_id ||\n slidesNormalizado3.presentation_id ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\n// --------------------------------------------------\n// 2. Normalizar estado de match de forma defensiva\n// --------------------------------------------------\n\nif (\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 85\n) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n}\n\nif (\n matchEstado !== 'MATCH_MEDIA_CONFIANZA' &&\n matchEstado !== 'MATCH_ALTA_CONFIANZA' &&\n matchConfianza >= 60 &&\n matchConfianza < 85\n) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n}\n\nif (!matchEstado || matchEstado === 'PENDIENTE') {\n if (matchConfianza >= 85) {\n matchEstado = 'MATCH_ALTA_CONFIANZA';\n } else if (matchConfianza >= 60) {\n matchEstado = 'MATCH_MEDIA_CONFIANZA';\n } else if (matchConfianza > 0) {\n matchEstado = 'MATCH_BAJA_CONFIANZA';\n } else {\n matchEstado = 'SIN_MATCH';\n }\n}\n\n// --------------------------------------------------\n// 3. Mantener valores enriquecidos\n// --------------------------------------------------\n\nconst propuestaReferenciaFinal = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n actual.propuesta_referencia ||\n ''\n);\n\nconst ubicacionFinal = limpiar(\n enriquecido.ubicacion ||\n data.ubicacion ||\n actual.ubicacion ||\n ''\n);\n\nconst fechaEjecucionFinal = limpiar(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n actual.fecha_ejecucion ||\n ''\n);\n\n// --------------------------------------------------\n// 4. Detectar duplicado de link de forma robusta\n// --------------------------------------------------\n\nconst limpiarUrl = (valor) => {\n return limpiar(valor)\n .replace(/[\\u200B-\\u200D\\uFEFF]/g, '')\n .trim();\n};\n\nconst normalizarUrlComparacion = (valor) => {\n return limpiarUrl(valor)\n .replace(/\\/edit.*$/i, '')\n .replace(/\\/view.*$/i, '')\n .replace(/\\?.*$/i, '')\n .replace(/#.*$/i, '')\n .replace(/\\/$/, '')\n .toLowerCase();\n};\n\nconst extraerGoogleId = (valor) => {\n const url = limpiarUrl(valor);\n\n if (!url) return '';\n\n const patrones = [\n /\\/presentation\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/document\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/spreadsheets\\/d\\/([a-zA-Z0-9_-]+)/,\n /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,\n /[?&]id=([a-zA-Z0-9_-]+)/\n ];\n\n for (const patron of patrones) {\n const match = url.match(patron);\n if (match?.[1]) return match[1];\n }\n\n return '';\n};\n\nconst extraerLinks = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return [];\n\n return texto\n .split(/[\\n\\r\\t;, ]+/)\n .map(limpiarUrl)\n .filter(Boolean)\n .filter(link => {\n return (\n link.startsWith('http://') ||\n link.startsWith('https://') ||\n link.includes('docs.google.com') ||\n link.includes('drive.google.com')\n );\n });\n};\n\nconst enlacesExistentesLista = extraerLinks(enlacesExistentes);\n\nconst presentationIdDetectado =\n presentationId ||\n extraerGoogleId(presentationUrl);\n\nconst presentationUrlNormalizada = normalizarUrlComparacion(presentationUrl);\n\nconst linkDuplicado = Boolean(\n presentationUrl &&\n enlacesExistentesLista.some((linkExistente) => {\n const idExistente = extraerGoogleId(linkExistente);\n\n if (\n presentationIdDetectado &&\n idExistente &&\n idExistente === presentationIdDetectado\n ) {\n return true;\n }\n\n return normalizarUrlComparacion(linkExistente) === presentationUrlNormalizada;\n })\n);\n\n// --------------------------------------------------\n// 5. Calcular enlaces propuestos\n// --------------------------------------------------\n\nlet enlacesPropuestos = enlacesExistentes;\n\nif (presentationUrl && !linkDuplicado) {\n enlacesPropuestos = enlacesExistentes\n ? `${enlacesExistentes}\\n${presentationUrl}`\n : presentationUrl;\n}\n\n// --------------------------------------------------\n// 6. Decisión automática final\n// --------------------------------------------------\n\nconst motivos = [];\n\nconst esPropuestaEjecutada = tipoReporte === 'PROPUESTA_EJECUTADA';\nconst esPropuestaExterna = tipoReporte === 'PROPUESTA_EXTERNA';\nconst matchAlta = matchEstado === 'MATCH_ALTA_CONFIANZA' && matchConfianza >= 85;\nconst hayMatchBanco = Boolean(rowNumberBanco && nombreBanco);\n\nlet decisionAutomaticaBanco = '';\nlet motivoDecisionAutomatica = '';\n\nif (!presentationUrl) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_ERROR_DATOS';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no existe link de presentación ejecutada.';\n motivos.push('no existe presentacion_ejecucion_url');\n\n} else if (esPropuestaExterna) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_PROPUESTA_EXTERNA';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el reporte fue clasificado como propuesta externa. Se genera presentación y se guarda el registro ejecutado.';\n\n} else if (!esPropuestaEjecutada) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_TIPO_NO_DETERMINADO';\n motivoDecisionAutomatica = `No se actualiza banco original porque tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}.`;\n motivos.push(`tipo_reporte es ${tipoReporte || 'NO_DETERMINADO'}`);\n\n} else if (linkDuplicado) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_LINK_DUPLICADO';\n motivoDecisionAutomatica = 'No se actualiza banco original porque el link de presentación ejecutada ya existe en la columna de enlaces ejecutados.';\n motivos.push('el link ya existe en la columna de enlaces ejecutados');\n\n} else if (!hayMatchBanco) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_SIN_MATCH';\n motivoDecisionAutomatica = 'No se actualiza banco original porque no se encontró una propuesta compatible con fila de banco válida.';\n if (!nombreBanco) motivos.push('no llegó propuesta_nombre_banco');\n if (!rowNumberBanco) motivos.push('no llegó propuesta_banco_row_number');\n\n} else if (!matchAlta) {\n decisionAutomaticaBanco = 'NO_ACTUALIZADO_CONFIANZA_INSUFICIENTE';\n motivoDecisionAutomatica = `No se actualiza banco original porque el match no tiene confianza suficiente. Estado: ${matchEstado || 'SIN_MATCH'}, confianza: ${matchConfianza}.`;\n motivos.push(`propuesta_match_estado es ${matchEstado || 'SIN_MATCH'}`);\n motivos.push(`propuesta_match_confianza es ${matchConfianza}`);\n\n} else {\n decisionAutomaticaBanco = 'ACTUALIZADO_BANCO_AUTO';\n motivoDecisionAutomatica = 'Banco original actualizado automáticamente: propuesta ejecutada, match alto, confianza suficiente y link nuevo.';\n}\n\nconst actualizarBanco = decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO';\n\nconst bancoActualizadoAuto = actualizarBanco ? 'SI' : 'NO';\n\nconst motivoNoActualizaBanco = actualizarBanco\n ? ''\n : motivoDecisionAutomatica;\n\n// --------------------------------------------------\n// 7. Seguridad para evitar updates accidentales\n// --------------------------------------------------\n// Si NO se actualiza, dejamos row_number y banco_match_value vacíos.\n// Así, aunque alguien conecte mal el nodo de Google Sheets,\n// no debería encontrar fila válida para actualizar.\nconst rowNumberSeguro = actualizarBanco ? rowNumberBanco : '';\nconst bancoMatchValueSeguro = actualizarBanco ? rowNumberBanco : '';\nconst enlacesActualizadosSeguro = actualizarBanco ? enlacesPropuestos : enlacesExistentes;\n\n// Campo compatible con lógica anterior.\n// Ya no significa revisión humana; es una decisión automática del sistema.\nlet propuestaMatchRevision = '';\n\nif (decisionAutomaticaBanco === 'ACTUALIZADO_BANCO_AUTO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_LINK_DUPLICADO') {\n propuestaMatchRevision = 'NO_REQUIERE_REVISION_LINK_DUPLICADO';\n} else if (decisionAutomaticaBanco === 'NO_ACTUALIZADO_PROPUESTA_EXTERNA') {\n propuestaMatchRevision = 'NO_APLICA_PROPUESTA_EXTERNA';\n} else {\n propuestaMatchRevision = 'NO_ACTUALIZADO_AUTOMATICAMENTE';\n}\n\n// --------------------------------------------------\n// 8. Salida final\n// --------------------------------------------------\n\nreturn [\n {\n json: {\n ...data,\n\n // Mantener explícitamente los valores enriquecidos.\n propuesta_referencia: propuestaReferenciaFinal || data.propuesta_referencia || '',\n ubicacion: ubicacionFinal || data.ubicacion || '',\n fecha_ejecucion: fechaEjecucionFinal || data.fecha_ejecucion || '',\n\n // Mantener explícitamente el link final para nodos posteriores.\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n // Decisión automática nueva.\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n // Compatibilidad con flujo actual.\n actualizar_banco_original: actualizarBanco,\n motivo_no_actualiza_banco: motivoNoActualizaBanco,\n\n propuesta_match_estado: matchEstado || data.propuesta_match_estado || 'SIN_MATCH',\n propuesta_match_confianza: String(matchConfianza || 0),\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n propuesta_banco_enlaces_existentes: enlacesExistentes,\n propuesta_match_revision: propuestaMatchRevision,\n\n banco_match_column: 'row_number',\n banco_match_value: bancoMatchValueSeguro,\n\n row_number: rowNumberSeguro,\n 'Enlaces a propuestas ejecutadas': enlacesActualizadosSeguro,\n\n banco_enlaces_existentes: enlacesExistentes,\n banco_enlaces_propuestos: enlacesPropuestos,\n banco_enlaces_actualizados: enlacesActualizadosSeguro,\n banco_presentacion_url: presentationUrl,\n\n banco_link_duplicado_detectado: linkDuplicado,\n banco_presentacion_id_detectado: presentationIdDetectado,\n banco_enlaces_existentes_lista: enlacesExistentesLista,\n\n datos_enriquecidos_match_banco: Boolean(\n enriquecido.datos_enriquecidos_match_banco ||\n data.datos_enriquecidos_match_banco\n ),\n\n banco_update_debug: {\n tipo_reporte: tipoReporte,\n\n propuesta_match_estado: matchEstado,\n propuesta_match_confianza: matchConfianza,\n match_alta: matchAlta,\n hay_match_banco: hayMatchBanco,\n\n decision_automatica_banco: decisionAutomaticaBanco,\n motivo_decision_automatica: motivoDecisionAutomatica,\n banco_actualizado_auto: bancoActualizadoAuto,\n\n propuesta_referencia_final: propuestaReferenciaFinal,\n ubicacion_final: ubicacionFinal,\n fecha_ejecucion_final: fechaEjecucionFinal,\n\n propuesta_nombre_banco: nombreBanco,\n propuesta_link_banco: linkBancoOriginal,\n propuesta_banco_file_id: fileIdBanco,\n propuesta_banco_row_number: rowNumberBanco,\n\n presentationUrl,\n presentationId,\n presentationIdDetectado,\n\n enlacesExistentes,\n enlacesExistentesLista,\n enlacesPropuestos,\n enlacesActualizadosSeguro,\n\n linkDuplicado,\n\n rowNumberSeguro,\n bancoMatchValueSeguro,\n\n actualizarBanco,\n motivoNoActualizaBanco,\n\n motivos,\n\n mejor_match: mejorMatch,\n match_debug: matchDebug,\n\n enriquecido_debug: {\n propuesta_referencia: enriquecido.propuesta_referencia || '',\n ubicacion: enriquecido.ubicacion || '',\n fecha_ejecucion: enriquecido.fecha_ejecucion || '',\n datos_enriquecidos_match_banco: enriquecido.datos_enriquecidos_match_banco || false\n }\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 68720, + 32016 + ], + "id": "8921425b-1053-429f-b78c-2c915c061105", + "name": "Code - Preparar actualización banco propuesta ejecutada TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dccbea1d-e9cf-4f51-b94d-7b0162737548", + "leftValue": "={{ $json.actualizar_banco_original === true }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 69488, + 32064 + ], + "id": "39433c13-8ef4-4693-8dd6-3440302cc7cc", + "name": "IF - Actualizar banco match alta confianza TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", + "mode": "list", + "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "propuestas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "Enlaces a propuestas ejecutadas": "={{ $json.banco_enlaces_actualizados }}", + "row_number": "={{ $json.propuesta_banco_row_number }}" + }, + "matchingColumns": [ + "row_number" + ], + "schema": [ + { + "id": "NOMBRE", + "displayName": "NOMBRE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "TIPO DE ACCION", + "displayName": "TIPO DE ACCION", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "CLIENTE", + "displayName": "CLIENTE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "MARCA", + "displayName": "MARCA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "PAIS", + "displayName": "PAIS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "CANAL", + "displayName": "CANAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "AMBIENTE DE COMPRA (RE)", + "displayName": "AMBIENTE DE COMPRA (RE)", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "TÁCTICA PROMOCIONAL", + "displayName": "TÁCTICA PROMOCIONAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "APROBADA", + "displayName": "APROBADA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ETIQUETAS", + "displayName": "ETIQUETAS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "AÑO", + "displayName": "AÑO", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Enlace a la propuesta", + "displayName": "Enlace a la propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Descripcion", + "displayName": "Descripcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "file_id", + "displayName": "file_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "nombre_archivo", + "displayName": "nombre_archivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "mime_type", + "displayName": "mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fuente_pais", + "displayName": "fuente_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "confianza_pais", + "displayName": "confianza_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "requiere_revision", + "displayName": "requiere_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "procesado_ia", + "displayName": "procesado_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivos_revision", + "displayName": "motivos_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Enlaces a propuestas ejecutadas", + "displayName": "Enlaces a propuestas ejecutadas", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tablero_sync_key", + "displayName": "tablero_sync_key", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tablero_project_id", + "displayName": "tablero_project_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tablero_origen", + "displayName": "tablero_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "drive_modified_time", + "displayName": "drive_modified_time", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultimo_refresco_ia", + "displayName": "ultimo_refresco_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "refresh_status", + "displayName": "refresh_status", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "refresh_attempts", + "displayName": "refresh_attempts", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "converted_google_slides_id", + "displayName": "converted_google_slides_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "converted_google_slides_link", + "displayName": "converted_google_slides_link", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 69840, + 31936 + ], + "id": "0284879d-9259-41c5-833b-a5e88150cecd", + "name": "Sheets - Actualizar link ejecutada en banco TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const respuestaBanco = $json || {};\n\nconst base = $('Code - Preparar actualización banco propuesta ejecutada TEST').first().json || {};\n\nreturn [\n {\n json: {\n ...base,\n\n banco_original_actualizado: true,\n banco_original_response: respuestaBanco\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 70128, + 31936 + ], + "id": "70839572-cb01-443f-ae5c-fffe1eebf8f3", + "name": "Code - Restaurar contexto actualización banco TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst normalizar = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bWMC([A-Z])/gi, 'WMC $1')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst pareceDatoTecnicoNoUbicacion = (valor) => {\n const texto = normalizar(valor);\n\n if (!texto) return true;\n\n const patronesTecnicos = [\n /\\bRTM\\b/,\n /\\bRTM\\+/,\n /\\bWMC\\b/,\n /\\bV\\d+\\b/,\n /\\b20\\d{2}\\b/,\n /\\bPROPUESTA\\b/,\n /\\bPROYECTO\\b/,\n /\\bVERSION\\b/,\n /\\bCODIGO\\b/\n ];\n\n return patronesTecnicos.some(rx => rx.test(texto));\n};\n\nconst normalizarUbicacion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (pareceDatoTecnicoNoUbicacion(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst normalizarFechaEjecucion = (valor) => {\n const texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n if (/^20\\d{2}$/.test(texto)) {\n return 'No identificado';\n }\n\n return texto;\n};\n\nconst matchAltaOMedia = [\n 'MATCH_ALTA_CONFIANZA',\n 'MATCH_MEDIA_CONFIANZA'\n].includes(normalizar(data.propuesta_match_estado));\n\nconst nombreBanco = limpiar(data.propuesta_nombre_banco);\n\nconst referenciaOriginal = limpiar(data.propuesta_referencia);\n\nconst referenciaCanonica = matchAltaOMedia && nombreBanco\n ? nombreBanco\n : referenciaOriginal;\n\nconst ubicacionLimpia = normalizarUbicacion(data.ubicacion);\nconst fechaLimpia = normalizarFechaEjecucion(data.fecha_ejecucion);\n\nreturn [\n {\n json: {\n ...data,\n\n propuesta_referencia_original_ia: referenciaOriginal,\n ubicacion_original_ia: data.ubicacion || '',\n fecha_ejecucion_original_ia: data.fecha_ejecucion || '',\n\n propuesta_referencia: limpiarReferenciaVisual(referenciaCanonica),\n ubicacion: ubicacionLimpia,\n fecha_ejecucion: fechaLimpia,\n\n datos_enriquecidos_match_banco: true\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 62032, + 32224 + ], + "id": "b64e3f26-964f-4e84-84d8-71389724dff3", + "name": "Code - Enriquecer datos con match banco TEST" + }, + { + "parameters": { + "jsCode": "const items = $input.all();\n\nconst clean = (value) => String(value ?? '').trim();\nconst digits = (value) => clean(value).replace(/\\D/g, '');\n\nreturn items.map((item) => {\n const evento = item.json || {};\n\n const messageType = clean(evento.message_type).toLowerCase();\n const text = clean(evento.texto);\n const senderPhone = digits(\n evento.sender_phone ||\n evento.manager_telefono ||\n evento.whatsapp_to ||\n evento.whatsapp_remote_jid\n );\n\n const rawMessage =\n evento.raw_event?.messages?.[0] ||\n evento.raw_event ||\n {};\n\n const isGroup = Boolean(\n evento.is_group ||\n evento.group_id ||\n evento.group_jid ||\n evento.whatsapp_recipient_type === 'group'\n );\n\n const isReaction =\n messageType === 'reaction' ||\n Boolean(rawMessage.reaction);\n\n const isStatus =\n messageType === 'status' ||\n Boolean(evento.raw_event?.statuses) ||\n Boolean(evento.raw_event?.status);\n\n const isEmpty =\n !text &&\n !evento.tiene_media &&\n !['audio', 'image', 'video', 'document'].includes(messageType);\n\n const reasons = [];\n\n if (!senderPhone) reasons.push('remitente sin número válido');\n if (evento.from_me) reasons.push('mensaje enviado por el propio número');\n if (isGroup) reasons.push('evento grupal no permitido en modo privado');\n if (isReaction) reasons.push('evento de reacción');\n if (isStatus) reasons.push('evento de estado');\n if (isEmpty) reasons.push('evento sin texto ni media útil');\n\n const process = reasons.length === 0;\n\n return {\n json: {\n ...evento,\n\n // Modo definitivo de esta versión: conversación individual.\n is_group: false,\n group_id: '',\n group_jid: '',\n group_name: '',\n whatsapp_to: senderPhone,\n whatsapp_remote_jid: senderPhone,\n whatsapp_recipient_type: 'individual',\n manager_telefono: senderPhone,\n sender_phone: senderPhone,\n sender_jid: senderPhone,\n canal_origen: 'WHATSAPP_OFICIAL_PRIVADO',\n\n filtro_origen_procesar: process,\n filtro_origen_decision: process ? 'PROCESAR_PRIVADO' : 'IGNORAR',\n filtro_origen_motivo: reasons.join('; '),\n\n chat_privado_permitido: process,\n modo_operacion: 'CHAT_PRIVADO_API_OFICIAL',\n\n filtro_origen_debug: {\n api: 'META_WHATSAPP_CLOUD_API',\n modo: 'CHAT_PRIVADO',\n sender_phone: senderPhone,\n message_type: messageType,\n accion: clean(evento.accion_flujo),\n texto: text,\n evento_grupal_detectado: isGroup,\n procesar: process,\n motivos_bloqueo: reasons\n }\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 47152, + 29040 + ], + "id": "42bb33d4-209b-4b91-aa23-32cec786b796", + "name": "Code - Validar chat privado WhatsApp TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "330d3843-6562-4ba6-afce-9a4bef9666b1", + "leftValue": "={{ $json.filtro_origen_procesar }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 47664, + 29008 + ], + "id": "8e3ef090-b3d8-4079-a95c-f69b3f867372", + "name": "IF - Procesar solo chat privado TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "decision_automatica_banco": "={{ $json.decision_automatica_banco }}", + "banco_actualizado_auto": "={{ $json.banco_actualizado_auto }}", + "propuesta_match_revision": "={{ $json.propuesta_match_revision }}", + "motivo_decision_automatica": "={{ $json.motivo_decision_automatica }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_ejecucion", + "displayName": "fecha_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_referencia", + "displayName": "propuesta_referencia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_match_estado", + "displayName": "propuesta_match_estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_match_confianza", + "displayName": "propuesta_match_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_nombre_banco", + "displayName": "propuesta_nombre_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_link_banco", + "displayName": "propuesta_link_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "marca", + "displayName": "marca", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "cliente", + "displayName": "cliente", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "pais", + "displayName": "pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ubicacion", + "displayName": "ubicacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "comentario_original", + "displayName": "comentario_original", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "resumen_ia", + "displayName": "resumen_ia", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "descripcion_ejecucion", + "displayName": "descripcion_ejecucion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "elementos_detectados", + "displayName": "elementos_detectados", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tags", + "displayName": "tags", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "media_folder_url", + "displayName": "media_folder_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "presentacion_ejecucion_url", + "displayName": "presentacion_ejecucion_url", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fotos_count", + "displayName": "fotos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "audios_count", + "displayName": "audios_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "estado_revision", + "displayName": "estado_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actualizacion", + "displayName": "ultima_actualizacion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "transcripcion_audio", + "displayName": "transcripcion_audio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "propuesta_match_revision", + "displayName": "propuesta_match_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte_confianza", + "displayName": "tipo_reporte_confianza", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte_motivo", + "displayName": "tipo_reporte_motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "decision_automatica_banco", + "displayName": "decision_automatica_banco", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "motivo_decision_automatica", + "displayName": "motivo_decision_automatica", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "banco_actualizado_auto", + "displayName": "banco_actualizado_auto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 68976, + 32096 + ], + "id": "de293272-8f03-408c-b33a-89bc06d876bc", + "name": "Sheets - Actualizar decisión automática banco TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst decisionOriginal = getNodeJson('Code - Preparar actualización banco propuesta ejecutada TEST');\nconst updateDecisionSheet = $json || {};\n\nreturn [\n {\n json: {\n ...decisionOriginal,\n\n decision_sheet_actualizada: true,\n decision_sheet_update_debug: {\n row_number: updateDecisionSheet.row_number || '',\n session_id: updateDecisionSheet.session_id || decisionOriginal.session_id || '',\n decision_automatica_banco:\n updateDecisionSheet.decision_automatica_banco ||\n decisionOriginal.decision_automatica_banco ||\n '',\n banco_actualizado_auto:\n updateDecisionSheet.banco_actualizado_auto ||\n decisionOriginal.banco_actualizado_auto ||\n ''\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 69184, + 32096 + ], + "id": "51622457-b8d4-4e03-a80e-f3bf3eacfb84", + "name": "Code - Restaurar contexto decisión automática banco TEST" + }, + { + "parameters": { + "jsCode": "const data = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst sessionId = limpiar(data.session_id);\nconst eventId = limpiar(data.event_id || data.media_source_id || `${Date.now()}`);\nconst mediaSourceId = limpiar(data.media_source_id || data.event_id || eventId);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para crear buffer Redis de imagen.');\n}\n\nif (!mediaSourceId) {\n throw new Error('No llegó media_source_id/event_id para crear buffer Redis de imagen.');\n}\n\nconst redisKey = `fulgencio:media:${sessionId}:image`;\n\nconst bufferItem = {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n\n event_id: eventId,\n media_source_id: mediaSourceId,\n message_type: 'image',\n media_type: 'image',\n\n buffer_received_at: new Date().toISOString(),\n buffer_received_at_ms: Date.now()\n};\n\nreturn [\n {\n json: {\n ...data,\n\n redis_buffer_tipo: 'image',\n redis_buffer_key: redisKey,\n redis_buffer_id: eventId,\n redis_payload: JSON.stringify(bufferItem),\n\n estado_buffer_redis: 'IMAGEN_ENVIADA_A_BUFFER'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51456, + 28416 + ], + "id": "8d130576-08ce-4164-9639-0c3b9b67c324", + "name": "Code - Preparar buffer imagen Redis TEST" + }, + { + "parameters": { + "operation": "push", + "list": "={{ $json.redis_buffer_key }}", + "messageData": "={{ $json.redis_payload }}", + "tail": true + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 51664, + 28416 + ], + "id": "39c47d49-2734-4552-a654-c483826a7127", + "name": "Redis - Push buffer imagen TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "jsCode": "const original = $('Code - Preparar buffer imagen Redis TEST').first().json || {};\nconst redisPushResult = $json || {};\n\nreturn [\n {\n json: {\n ...original,\n redis_push_result: redisPushResult,\n redis_push_ok: true,\n estado_buffer_redis: 'IMAGEN_GUARDADA_EN_REDIS'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51872, + 28416 + ], + "id": "209616e6-851a-4e61-b86e-b4900d02625a", + "name": "Code - Restaurar contexto buffer imagen TEST" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 52080, + 28416 + ], + "id": "6499785a-3a8b-4762-8fd0-68d6c9246ff8", + "name": "Wait - Esperar cierre buffer imagen TEST", + "webhookId": "5c73c08e-d77a-448d-a8ab-11080d08b476" + }, + { + "parameters": { + "operation": "get", + "propertyName": "redis_buffer_items", + "key": "={{ $('Code - Restaurar contexto buffer imagen TEST').first().json.redis_buffer_key }}", + "keyType": "list", + "options": {} + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 52288, + 28416 + ], + "id": "3a7c9906-c0f1-4254-b136-57207fc95a43", + "name": "Redis - Leer buffer imagen TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Restaurar contexto buffer imagen TEST').first().json || {};\nconst redisOutput = $json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst currentBufferId = limpiar(contexto.redis_buffer_id);\nconst sessionId = limpiar(contexto.session_id);\nconst redisKey = limpiar(contexto.redis_buffer_key);\n\nlet rawItems = redisOutput.redis_buffer_items || redisOutput.message || [];\n\nif (!Array.isArray(rawItems)) {\n rawItems = rawItems ? [rawItems] : [];\n}\n\nconst parsed = [];\n\nfor (const raw of rawItems) {\n try {\n const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;\n if (obj && typeof obj === 'object') {\n parsed.push(obj);\n }\n } catch (error) {\n // Ignorar items corruptos del buffer\n }\n}\n\n// Deduplicar por media_source_id/event_id\nconst vistos = new Set();\nconst lote = [];\n\nfor (const item of parsed) {\n const id = limpiar(item.media_source_id || item.event_id || item.redis_buffer_id);\n if (!id) continue;\n if (vistos.has(id)) continue;\n\n vistos.add(id);\n\n lote.push({\n ...item,\n redis_buffer_key: redisKey,\n session_id: limpiar(item.session_id || sessionId),\n media_source_id: limpiar(item.media_source_id || id),\n event_id: limpiar(item.event_id || id),\n message_type: 'image',\n media_type: 'image'\n });\n}\n\nlote.sort((a, b) => {\n return Number(a.buffer_received_at_ms || 0) - Number(b.buffer_received_at_ms || 0);\n});\n\nconst ultimo = lote[lote.length - 1] || {};\nconst ultimoId = limpiar(ultimo.redis_buffer_id || ultimo.event_id || ultimo.media_source_id);\n\nconst debeProcesar =\n lote.length > 0 &&\n currentBufferId &&\n ultimoId &&\n currentBufferId === ultimoId;\n\nconst imagenesPrevias = Number(\n contexto.sesion_activa?.imagenes_count ??\n contexto.imagenes_count ??\n 0\n) || 0;\n\nconst imagenesNuevas = lote.length;\nconst imagenesTotal = imagenesPrevias + imagenesNuevas;\n\nreturn [\n {\n json: {\n ...contexto,\n\n redis_buffer_items_count: rawItems.length,\n redis_buffer_lote_count: lote.length,\n redis_buffer_lote: lote,\n\n redis_buffer_ultimo_id: ultimoId,\n redis_buffer_current_id: currentBufferId,\n\n redis_debe_procesar_lote: debeProcesar,\n redis_decision: debeProcesar ? 'PROCESAR_LOTE_IMAGEN' : 'IGNORAR_EJECUCION_INTERMEDIA',\n\n imagenes_previas: imagenesPrevias,\n imagenes_nuevas_lote: imagenesNuevas,\n imagenes_count: imagenesTotal,\n\n estado_buffer_redis: debeProcesar\n ? 'LOTE_IMAGEN_LISTO_PARA_PROCESAR'\n : 'IMAGEN_EN_BUFFER_ESPERANDO_ULTIMA_EJECUCION',\n\n redis_debug: {\n redisKey,\n rawItemsCount: rawItems.length,\n loteCount: lote.length,\n currentBufferId,\n ultimoId,\n debeProcesar\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 52496, + 28416 + ], + "id": "4b48654c-a989-482f-a2c8-8a5100aae2a4", + "name": "Code - Decidir procesar lote imagen Redis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "f2394967-cd4b-4c6e-89be-7d9410d8a68b", + "leftValue": "={{ $json.redis_debe_procesar_lote }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 52720, + 28416 + ], + "id": "6dc2a95b-5f2d-405e-b7ed-4407a1a6ebd0", + "name": "IF - Procesar lote imagen Redis TEST" + }, + { + "parameters": { + "operation": "delete", + "key": "={{ $json.redis_buffer_key }}" + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 52992, + 28400 + ], + "id": "64835ce7-38d7-43e7-9c44-2aa491ba9a19", + "name": "Redis - Borrar buffer imagen TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst lote = Array.isArray(contexto.redis_buffer_lote)\n ? contexto.redis_buffer_lote\n : [];\n\nif (!lote.length) {\n return [];\n}\n\nreturn lote.map((item, index) => {\n return {\n json: {\n ...contexto,\n ...item,\n\n media_index: index + 1,\n media_total: lote.length,\n\n estado: 'IMAGEN_RECIBIDA',\n fecha_procesado: new Date().toISOString(),\n\n lote_imagen_redis_total: lote.length,\n redis_lote_procesado: true\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 53200, + 28400 + ], + "id": "4424e969-d8bf-44f3-859c-5b5b16b1ba81", + "name": "Code - Expandir lote imagen Redis TEST" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 306129743, + "mode": "list", + "cachedResultName": "wa_ejecuciones_eventos", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=306129743" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "event_id": "={{ $json.event_id }}", + "manager_telefono": "={{ $json.manager_telefono }}", + "manager_nombre": "={{ $json.manager_nombre }}", + "tiene_media": "={{ $json.tiene_media }}", + "estado": "=IMAGEN_RECIBIDA", + "session_id": "={{ $json.session_id }}", + "canal_origen": "=WHATSAPP", + "fecha_recepcion": "={{ $json.fecha_recepcion }}", + "message_type": "={{ $json.message_type }}", + "texto": "={{ $json.texto }}", + "media_count": "={{ $json.media_count }}", + "raw_preview": "={{ $json.raw_preview }}", + "etapa_recibida": "={{ $json.etapa_actual }}", + "media_source_id": "={{ $json.media_source_id }}", + "media_mime_type": "={{ $json.media_mime_type }}", + "media_file_name": "={{ $json.media_file_name }}", + "whatsapp_remote_jid": "={{ $json.whatsapp_remote_jid }}", + "comando": "={{ $json.accion_flujo }}", + "ejecucion_id": "={{ $json.ejecucion_id }}", + "fecha_procesado": "={{ new Date().toISOString() }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "event_id", + "displayName": "event_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_recepcion", + "displayName": "fecha_recepcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "message_type", + "displayName": "message_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "texto", + "displayName": "texto", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "comando", + "displayName": "comando", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "tiene_media", + "displayName": "tiene_media", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_count", + "displayName": "media_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "raw_preview", + "displayName": "raw_preview", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fecha_procesado", + "displayName": "fecha_procesado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "etapa_recibida", + "displayName": "etapa_recibida", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "media_source_id", + "displayName": "media_source_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_mime_type", + "displayName": "media_mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "media_file_name", + "displayName": "media_file_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "whatsapp_remote_jid", + "displayName": "whatsapp_remote_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 53440, + 28400 + ], + "id": "aca4b708-b182-43ff-8d15-ad79fc98b611", + "name": "Sheets - Guardar eventos imagen lote Redis TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Decidir procesar lote imagen Redis TEST').first().json || {};\nconst guardados = $input.all().map(item => item.json || {});\n\nconst loteCount = Number(contexto.redis_buffer_lote_count || guardados.length || 0);\nconst imagenesPrevias = Number(contexto.imagenes_previas || 0) || 0;\nconst imagenesTotal = Number(contexto.imagenes_count || (imagenesPrevias + loteCount)) || loteCount;\n\nreturn [\n {\n json: {\n ...contexto,\n\n eventos_imagen_guardados: guardados.length,\n imagenes_nuevas_lote: loteCount,\n imagenes_count: imagenesTotal,\n\n ultima_actividad: new Date().toISOString(),\n etapa: 'ESPERANDO_IMAGENES',\n estado: 'IMAGENES_RECIBIDAS',\n motivo_revision: `IMAGENES_RECIBIDAS_LOTE_${loteCount}`,\n\n redis_lote_imagen_guardado: true\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 53648, + 28400 + ], + "id": "97e84d15-c4aa-4750-bf09-93568e837d67", + "name": "Code - Consolidar lote imagen guardado Redis TEST" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 616166581, + "mode": "list", + "cachedResultName": "wa_ejecuciones_sesiones", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=616166581" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "session_id": "={{ $json.session_id }}", + "estado": "={{ $json.estado }}", + "imagenes_count": "={{ $json.imagenes_count }}", + "etapa": "={{ $json.etapa }}", + "ultima_actividad": "={{ $json.ultima_actividad }}", + "motivo_revision": "={{ $json.motivo_revision }}" + }, + "matchingColumns": [ + "session_id" + ], + "schema": [ + { + "id": "session_id", + "displayName": "session_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "manager_telefono", + "displayName": "manager_telefono", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "manager_nombre", + "displayName": "manager_nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "canal_origen", + "displayName": "canal_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "fecha_inicio", + "displayName": "fecha_inicio", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "ultima_actividad", + "displayName": "ultima_actividad", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "etapa", + "displayName": "etapa", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "audio_count", + "displayName": "audio_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "imagenes_count", + "displayName": "imagenes_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "videos_count", + "displayName": "videos_count", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "estado", + "displayName": "estado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "ejecucion_id", + "displayName": "ejecucion_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "motivo_revision", + "displayName": "motivo_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "is_group", + "displayName": "is_group", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_jid", + "displayName": "group_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "group_name", + "displayName": "group_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_jid", + "displayName": "sender_jid", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_phone", + "displayName": "sender_phone", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "sender_name", + "displayName": "sender_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "country_code", + "displayName": "country_code", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "pais_detectado", + "displayName": "pais_detectado", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "idioma_flujo", + "displayName": "idioma_flujo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "tipo_reporte", + "displayName": "tipo_reporte", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 53904, + 28400 + ], + "id": "b298c7a2-fd28-41a8-bdee-99af2b6fff3e", + "name": "Sheets - Actualizar sesión imágenes lote Redis TEST", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Consolidar lote imagen guardado Redis TEST').first().json || {};\nconst data = {\n ...contexto,\n ...($json || {})\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst idioma = limpiar(data.idioma_flujo || 'ES').toUpperCase();\n\nconst to = limpiar(\n data.whatsapp_to ||\n data.group_jid ||\n data.whatsapp_remote_jid ||\n data.sesion_activa?.group_jid ||\n data.sesion_activa?.whatsapp_to ||\n data.sender_jid ||\n ''\n);\n\nconst count = Number(\n data.imagenes_nuevas_lote ||\n data.redis_buffer_lote_count ||\n data.eventos_imagen_guardados ||\n 1\n) || 1;\n\nif (!to) {\n throw new Error('No llegó whatsapp_to/group_jid para enviar confirmación de imágenes.');\n}\n\nconst pluralEs = count === 1 ? 'imagen recibida' : 'imágenes recibidas';\nconst pluralEn = count === 1 ? 'image received' : 'images received';\n\nconst mensajeEs =\n`✅ ${count} ${pluralEs}.\n\nPuedes enviar más imágenes si hace falta.\n\nCuando termines de enviar las imágenes, selecciona: Fotos listas`;\n\nconst mensajeEn =\n`✅ ${count} ${pluralEn}.\n\nYou can send more images if needed.\n\nWhen you finish sending images, select: Photos ready`;\n\nconst mensajeFinal = idioma === 'EN' ? mensajeEn : mensajeEs;\n\nreturn [\n {\n json: {\n ...data,\n whatsapp_to: to,\n whatsapp_header: '',\n whatsapp_buttons: [\n { id: 'FOTOS_LISTAS', title: idioma === 'EN' ? 'Photos ready' : 'Fotos listas' },\n { id: 'CANCELAR', title: idioma === 'EN' ? 'Cancel report' : 'Cancelar reporte' },\n { id: 'RANKING', title: idioma === 'EN' ? 'View ranking' : 'Ver ranking' }\n ],\n whatsapp_text: mensajeFinal,\n texto_respuesta: mensajeFinal,\n mensaje: mensajeFinal,\n text: mensajeFinal,\n mensaje_whatsapp: mensajeFinal,\n message_text: mensajeFinal,\n estado_mensaje: 'CONFIRMACION_IMAGENES_LOTE_PREPARADA',\n confirmacion_imagenes_lote_debug: {\n idioma,\n whatsapp_to: to,\n imagenes_nuevas_lote: count\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 54240, + 28416 + ], + "id": "24c0d658-040e-4c73-9fef-84db85105528", + "name": "Code - Preparar confirmación imágenes lote Redis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "2cb38fef-879c-45f3-8886-b2f0a35d067b", + "leftValue": "={{ $json.analisis_debe_intentar_cierre }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 57312, + 32272 + ], + "id": "309753aa-4e4f-4246-8fb2-78b1b9f193fb", + "name": "IF - Análisis completo Redis TEST" + }, + { + "parameters": { + "operation": "incr", + "key": "={{ $json.redis_lock_key }}", + "expire": true, + "ttl": 86400 + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 58912, + 32176 + ], + "id": "5ac7ca64-e3a2-4fdd-83ab-32f06a6fcd16", + "name": "Redis - Lock análisis final TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const actual = $input.item.json || {};\n\nconst extraerNumero = (valor) => {\n if (typeof valor === 'number') return valor;\n\n if (typeof valor === 'string') {\n const n = Number(valor);\n if (!Number.isNaN(n)) return n;\n }\n\n if (valor && typeof valor === 'object') {\n const valores = Object.values(valor);\n for (const v of valores) {\n const n = extraerNumero(v);\n if (n) return n;\n }\n }\n\n return 0;\n};\n\nconst redisActual = actual.redis_lock_response || actual;\n\nconst lockValor = extraerNumero(redisActual);\nconst lockAdquirido = lockValor === 1;\n\nreturn {\n json: {\n ...actual,\n\n redis_lock_response: redisActual,\n redis_lock_value: lockValor,\n\n lock_analisis_adquirido: lockAdquirido,\n decision_lock_analisis: lockAdquirido\n ? 'LOCK_ADQUIRIDO_GENERAR_REPORTE_FINAL'\n : 'LOCK_YA_EXISTIA_NO_GENERAR_DUPLICADO',\n\n lock_analisis_debug: {\n session_id: actual.session_id || '',\n redis_lock_key: actual.redis_lock_key || '',\n redis_lock_value: lockValor,\n lock_analisis_adquirido: lockAdquirido,\n redis_lock_response: redisActual,\n },\n },\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 59120, + 32176 + ], + "id": "7c2dfb7d-e85f-4ca9-9420-81e0d95d3ba0", + "name": "Code - Validar lock análisis final TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "432f82ae-e1bb-4ea7-a964-0cc4bebef5de", + "leftValue": "={{ $json.lock_analisis_adquirido }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 59328, + 32176 + ], + "id": "1e768f97-7b9d-4bde-a69a-e4e543a3b1bd", + "name": "IF - Lock análisis final adquirido TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const actual = $input.item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n actual.session_id ||\n actual.sesion_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n actual.ejecucion_id ||\n sessionId\n);\n\nconst mediaEventId = limpiar(\n actual.media_event_id ||\n actual.message_id ||\n actual.id ||\n ''\n);\n\nconst mediaType = limpiar(\n actual.media_type ||\n actual.tipo_media ||\n ''\n).toLowerCase();\n\nconst mediaIndex = toNumber(\n actual.media_index ||\n actual.index ||\n 1,\n 1\n);\n\nconst mediaTotal = toNumber(\n actual.media_total_esperado ||\n actual.media_total ||\n actual.total_media ||\n 1,\n 1\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nif (!mediaEventId) {\n throw new Error('No llegó media_event_id al nodo Code - Preparar contador análisis Redis TEST');\n}\n\nconst redisCountKey = `fulgencio:analisis-count:${sessionId}`;\nconst redisLockKey = `fulgencio:analisis-final-lock:${sessionId}`;\n\nreturn {\n json: {\n ...actual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_event_id: mediaEventId,\n media_type: mediaType,\n\n media_index: mediaIndex,\n media_total: mediaTotal,\n media_total_esperado: mediaTotal,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n contador_analisis_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: mediaType,\n media_index: mediaIndex,\n media_total: mediaTotal,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 56448, + 32368 + ], + "id": "e2e99035-966e-4ee5-85c7-650d7447a4b1", + "name": "Code - Preparar contador análisis Redis TEST" + }, + { + "parameters": { + "operation": "incr", + "key": "={{ $json.redis_count_key }}", + "expire": true, + "ttl": 86400 + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 56704, + 32368 + ], + "id": "529ab021-bc1e-42e4-b76a-52e547b282f2", + "name": "Redis - Contar análisis media TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const redisActual = $input.item.json || {};\nconst ctx = $('Code - Preparar contador análisis Redis TEST').item.json || {};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(ctx.session_id);\nconst ejecucionId = limpiar(ctx.ejecucion_id || sessionId);\n\nconst mediaTotalEsperado = toNumber(\n ctx.media_total_esperado ||\n ctx.media_total ||\n 1,\n 1\n);\n\nconst redisCountKey = limpiar(\n ctx.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n ctx.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\n// El Redis Increment puede devolver:\n// { \"fulgencio:analisis-count:SESSION\": 2 }\n// o puede devolver otro nombre según n8n.\nlet redisConteoActual = 0;\n\nif (redisCountKey && redisActual[redisCountKey] !== undefined) {\n redisConteoActual = toNumber(redisActual[redisCountKey], 0);\n} else {\n const valoresNumericos = Object.values(redisActual)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n redisConteoActual = valoresNumericos.length ? valoresNumericos[0] : 0;\n}\n\nconst analisisCompleto =\n mediaTotalEsperado > 0 &&\n redisConteoActual >= mediaTotalEsperado;\n\nreturn {\n json: {\n ...ctx,\n\n redis_increment_response: redisActual,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n media_total_esperado: mediaTotalEsperado,\n total_analisis_contados_redis: redisConteoActual,\n\n analisis_debe_intentar_cierre: analisisCompleto,\n decision_cierre_analisis: analisisCompleto\n ? 'ANALISIS_COMPLETO_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_ESPERAR_OTRA_EJECUCION',\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n cierre_analisis_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n redisConteoActual,\n analisisCompleto,\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n media_actual: {\n media_event_id: ctx.media_event_id,\n media_type: ctx.media_type,\n media_index: ctx.media_index,\n media_total: ctx.media_total\n },\n redis_increment_response: redisActual\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 56944, + 32368 + ], + "id": "b0e5135b-79b3-48ac-a7bc-1aafcc7f89e1", + "name": "Code - Validar conteo análisis Redis TEST" + }, + { + "parameters": { + "amount": 4 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 57600, + 32448 + ], + "id": "6faad376-45c9-42b4-9042-73c4cb015344", + "name": "Wait - Verificar cierre análisis Redis TEST", + "webhookId": "b17e9d5b-ec57-49ea-a0a7-91bf65e5e41a" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1107394537, + "mode": "list", + "cachedResultName": "wa_ejecuciones_analisis_media", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=1107394537" + }, + "filtersUI": { + "values": [ + { + "lookupColumn": "session_id", + "lookupValue": "={{ $json.session_id }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 57872, + 32480 + ], + "id": "6dcc1ce9-30c0-4ae7-82fe-fe1522e88205", + "name": "Sheets - Leer análisis media cierre Redis TEST", + "alwaysOutputData": true, + "executeOnce": true, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const inputRows = $input.all().map(item => item.json || {});\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst contextos = [\n getNodeJson('Code - Validar reintento cierre análisis TEST'),\n getNodeJson('Wait - Reintento cierre análisis Redis TEST'),\n getNodeJson('Wait - Verificar cierre análisis Redis TEST'),\n getNodeJson('Code - Validar conteo análisis Redis TEST'),\n getNodeJson('Code - Preparar contador análisis Redis TEST')\n];\n\nconst contexto =\n contextos.find(ctx => limpiar(ctx.session_id)) ||\n inputRows.find(row => limpiar(row.session_id)) ||\n {};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n inputRows[0]?.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n inputRows[0]?.ejecucion_id ||\n sessionId\n);\n\nif (!sessionId) {\n throw new Error('No llegó session_id para decidir cierre de análisis por Sheet.');\n}\n\nconst redisCountKey = limpiar(\n contexto.redis_count_key ||\n `fulgencio:analisis-count:${sessionId}`\n);\n\nconst redisLockKey = limpiar(\n contexto.redis_lock_key ||\n `fulgencio:analisis-final-lock:${sessionId}`\n);\n\nconst rowsSesion = inputRows.filter(row => {\n return limpiar(row.session_id) === sessionId;\n});\n\nconst rowsCompletados = rowsSesion.filter(row => {\n const estado = limpiar(row.estado).toUpperCase();\n\n return (\n estado.includes('COMPLETADO') &&\n (\n estado.includes('ANALISIS_AUDIO') ||\n estado.includes('ANALISIS_IMAGEN') ||\n estado.includes('ANALISIS_VIDEO')\n )\n );\n});\n\nconst mapaUnicos = new Map();\n\nfor (const row of rowsCompletados) {\n const mediaEventId = limpiar(row.media_event_id);\n const mediaIndex = limpiar(row.media_index);\n const mediaType = limpiar(row.media_type).toLowerCase();\n\n const key = mediaEventId || `${mediaType}_${mediaIndex}`;\n\n if (!key) continue;\n\n if (!mapaUnicos.has(key)) {\n mapaUnicos.set(key, row);\n }\n}\n\nconst analisisUnicos = Array.from(mapaUnicos.values()).sort((a, b) => {\n return Number(a.media_index || 0) - Number(b.media_index || 0);\n});\n\nconst esperadoDesdeContexto = Number(\n contexto.media_total_esperado ||\n contexto.media_total ||\n 0\n);\n\nconst maxMediaTotalSheet = Math.max(\n 0,\n ...analisisUnicos\n .map(row => Number(row.media_total || row.media_total_esperado || 0))\n .filter(n => Number.isFinite(n))\n);\n\nconst mediaTotalEsperado = esperadoDesdeContexto || maxMediaTotalSheet || 0;\nconst totalAnalisisSheet = analisisUnicos.length;\n\nconst analisisCompletoPorSheet =\n mediaTotalEsperado > 0 &&\n totalAnalisisSheet >= mediaTotalEsperado;\n\nreturn [\n {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_count_key: redisCountKey,\n redis_lock_key: redisLockKey,\n\n media_total_esperado: mediaTotalEsperado,\n\n total_analisis_encontrados_sheet: totalAnalisisSheet,\n analisis_completo_por_sheet: analisisCompletoPorSheet,\n analisis_debe_intentar_cierre: analisisCompletoPorSheet,\n\n decision_cierre_analisis: analisisCompletoPorSheet\n ? 'ANALISIS_COMPLETO_POR_SHEET_INTENTAR_CIERRE'\n : 'ANALISIS_INCOMPLETO_POR_SHEET_ESPERAR_REINTENTO',\n\n analisis_rows_filtrados: analisisUnicos,\n\n cierre_analisis_sheet_debug: {\n sessionId,\n ejecucionId,\n mediaTotalEsperado,\n totalAnalisisSheet,\n analisisCompletoPorSheet,\n rows_leidas_sheet: inputRows.length,\n rows_misma_sesion: rowsSesion.length,\n rows_completadas: rowsCompletados.length,\n analisis_unicos: analisisUnicos.map(row => ({\n media_event_id: row.media_event_id,\n media_type: row.media_type,\n media_index: row.media_index,\n media_total: row.media_total,\n estado: row.estado\n }))\n }\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 58080, + 32480 + ], + "id": "7667cf5c-1cea-4083-b1a6-89b10788622c", + "name": "Code - Decidir cierre análisis por Sheet Redis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "5a5b3564-6292-486b-8154-5df844083a7d", + "leftValue": "={{ $json.analisis_debe_intentar_cierre }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 58288, + 32480 + ], + "id": "d7f91117-8d97-462d-b3f1-359b117699c0", + "name": "IF - Cierre análisis por Sheet Redis TEST" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const redisActual = $input.item.json || {};\n\nlet contexto = {};\ntry {\n contexto = $('IF - Cierre análisis por Sheet Redis TEST').item.json || {};\n} catch (error) {\n contexto = {};\n}\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst toNumber = (valor, fallback = 0) => {\n const numero = Number(valor);\n return Number.isFinite(numero) ? numero : fallback;\n};\n\nconst sessionId = limpiar(\n contexto.session_id ||\n redisActual.session_id ||\n ''\n);\n\nconst ejecucionId = limpiar(\n contexto.ejecucion_id ||\n redisActual.ejecucion_id ||\n sessionId\n);\n\nconst retryKey = limpiar(\n contexto.redis_retry_key ||\n redisActual.redis_retry_key ||\n `fulgencio:analisis-retry:${sessionId}`\n);\n\nconst extraerNumeroRedis = (obj) => {\n if (!obj || typeof obj !== 'object') return 0;\n\n if (retryKey && obj[retryKey] !== undefined) {\n return toNumber(obj[retryKey], 0);\n }\n\n const valoresNumericos = Object.values(obj)\n .map((valor) => Number(valor))\n .filter((valor) => Number.isFinite(valor));\n\n return valoresNumericos.length ? valoresNumericos[0] : 0;\n};\n\nconst retryCount = extraerNumeroRedis(redisActual);\n\nconst maxReintentos = 12;\nconst reintentar = retryCount <= maxReintentos;\n\nreturn {\n json: {\n ...contexto,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n\n redis_retry_key: retryKey,\n cierre_retry_count: retryCount,\n cierre_retry_max: maxReintentos,\n cierre_retry_permitido: reintentar,\n\n decision_reintento_cierre: reintentar\n ? 'REINTENTAR_LECTURA_ANALISIS'\n : 'REINTENTOS_AGOTADOS',\n\n cierre_retry_debug: {\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n retry_key: retryKey,\n redis_respuesta: redisActual,\n retry_count: retryCount,\n max_reintentos: maxReintentos,\n reintentar\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 58816, + 32560 + ], + "id": "1f3e80b8-b608-4e50-aeb5-d3a3d9302111", + "name": "Code - Validar reintento cierre análisis TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ad4f88ac-eda8-4cb1-bd08-34aca455e07c", + "leftValue": "={{ $json.cierre_retry_permitido }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 59024, + 32560 + ], + "id": "80040c3e-51a4-4d5d-b2e1-0daa92be6221", + "name": "IF - Reintentar cierre análisis Redis TEST" + }, + { + "parameters": { + "amount": 4 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 59392, + 32544 + ], + "id": "4fcf9142-c63d-4a03-b2ea-5135f09bf15d", + "name": "Wait - Reintento cierre análisis Redis TEST", + "webhookId": "b54b49e7-ede0-48e7-8973-0d45d8482cc8" + }, + { + "parameters": { + "operation": "incr", + "key": "={{ 'fulgencio:analisis-retry:' + $json.session_id }}", + "expire": true, + "ttl": 900 + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + 58608, + 32560 + ], + "id": "356e7264-cb9c-4a24-a2fe-298a62498dd7", + "name": "Redis - Contar reintento cierre análisis TEST", + "credentials": { + "redis": { + "id": "Fu2IevwVPaoyw9OS", + "name": "Redis Fulgencio" + } + } + }, + { + "parameters": { + "url": "={{ \n 'https://slides.googleapis.com/v1/presentations/' + \n (\n $json.presentation_id ||\n $json.presentacion_id ||\n $json.slides_presentation_id ||\n $json.presentacion_ejecucion_id ||\n String($json.presentacion_ejecucion_url || $json.presentation_url || '').match(/\\/presentation\\/d\\/([^/]+)/)?.[1]\n )\n}}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleOAuth2Api", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 66512, + 32192 + ], + "id": "3859c57e-67e0-4d89-ac01-648b9f9c51dc", + "name": "HTTP - Obtener presentación Slides limpiar TEST", + "credentials": { + "googleOAuth2Api": { + "id": "eHseMeH39kRcXgOF", + "name": "Google account 2" + } + } + }, + { + "parameters": { + "jsCode": "const inputItems = $input.all();\nconst presentation = inputItems[0]?.json || {};\n\n// Contexto anterior: aquí está el link, presentation_id, conteos, etc.\nconst contexto = $('Code - Normalizar respuesta Slides ejecución TEST').first().json || {};\n\nconst limpiar = (v) => String(v ?? '').trim();\n\nconst extraerIdPresentacion = (...valores) => {\n for (const valor of valores) {\n const txt = limpiar(valor);\n if (!txt) continue;\n\n const match = txt.match(/\\/presentation\\/d\\/([^/]+)/);\n if (match?.[1]) return match[1];\n\n // Si ya viene como ID limpio\n if (!txt.includes('/') && txt.length > 20) return txt;\n }\n return '';\n};\n\nconst presentationId = extraerIdPresentacion(\n contexto.presentation_id,\n contexto.presentacion_id,\n contexto.slides_presentation_id,\n contexto.presentacion_ejecucion_id,\n contexto.presentacion_ejecucion_url,\n contexto.presentation_url,\n presentation.presentationId\n);\n\nif (!presentationId) {\n throw new Error('No se pudo detectar presentation_id para limpiar slides vacíos.');\n}\n\n// Detectar cantidad de fotos reales.\n// Usamos varios posibles nombres porque el contexto puede variar según el nodo anterior.\nlet fotosCount = Number(\n contexto.fotos_count ??\n contexto.imagenes_count ??\n contexto.images_count ??\n contexto.media_fotos_count ??\n contexto.media_imagenes_count ??\n contexto.analisis_multimedia?.imagenes?.length ??\n contexto.analisis_multimedia?.fotos?.length ??\n 0\n);\n\n// Seguridad: si llega vacío, intenta contar imágenes insertadas desde posibles arrays.\nif (!fotosCount && Array.isArray(contexto.imagenes)) {\n fotosCount = contexto.imagenes.length;\n}\n\nif (!fotosCount && Array.isArray(contexto.fotos)) {\n fotosCount = contexto.fotos.length;\n}\n\n// Máximo de placeholders esperados en plantilla\nconst maxFotosPlantilla = 25;\n\n// Si por alguna razón no tenemos conteo, NO borramos nada.\nif (!fotosCount || fotosCount < 0) {\n fotosCount = 0;\n}\n\nfotosCount = Math.min(Number(fotosCount || 0), maxFotosPlantilla);\n\nconst extraerTextoSlide = (slide) => {\n const partes = [];\n\n const elementos = slide.pageElements || [];\n for (const el of elementos) {\n const textElements = el?.shape?.text?.textElements || [];\n for (const te of textElements) {\n const content = te?.textRun?.content;\n if (content) partes.push(content);\n }\n }\n\n return partes.join(' ').replace(/\\s+/g, ' ').trim();\n};\n\nconst slides = presentation.slides || [];\n\nconst slidesDetectados = [];\nconst slidesABorrar = [];\n\nfor (const slide of slides) {\n const objectId = slide.objectId;\n const textoSlide = extraerTextoSlide(slide);\n\n // Busca FOTO 1, FOTO 2, etc. Tolerante a espacios.\n const match = textoSlide.match(/\\bFOTO\\s*(\\d{1,2})\\b/i);\n\n if (!match) continue;\n\n const numeroFoto = Number(match[1]);\n if (numeroFoto < 1 || numeroFoto > maxFotosPlantilla) continue;\n\n slidesDetectados.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n\n if (numeroFoto > fotosCount) {\n slidesABorrar.push({\n objectId,\n numero_foto: numeroFoto,\n texto_detectado: match[0],\n });\n }\n}\n\nconst deleteRequests = slidesABorrar.map((s) => ({\n deleteObject: {\n objectId: s.objectId,\n },\n}));\n\nreturn [\n {\n json: {\n ...contexto,\n\n presentation_id: presentationId,\n slides_cleanup_fotos_count: fotosCount,\n slides_cleanup_total_slides: slides.length,\n\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n slides_fotos_vacias_count: slidesABorrar.length,\n\n delete_slide_requests: deleteRequests,\n\n slides_cleanup_debug: {\n presentation_id: presentationId,\n fotos_count_detectado: fotosCount,\n total_slides_presentacion: slides.length,\n slides_foto_detectados: slidesDetectados,\n slides_fotos_vacias_a_borrar: slidesABorrar,\n delete_requests_count: deleteRequests.length,\n },\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 66720, + 32192 + ], + "id": "077d6cd0-bab3-40a3-947c-66783d53d1ca", + "name": "Code - Preparar borrado slides fotos vacías TEST" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dca8dbb7-b14a-4def-9e43-27a3c1360d78", + "leftValue": "={{ Number($json.slides_fotos_vacias_count || 0) > 0 }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 66928, + 32192 + ], + "id": "afd81f91-c63a-4b6e-b454-471a5853c66e", + "name": "IF - Hay slides fotos vacías para borrar TEST" + }, + { + "parameters": { + "method": "POST", + "url": "={{ 'https://slides.googleapis.com/v1/presentations/' + $json.presentation_id + ':batchUpdate' }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleOAuth2Api", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ \n {\n requests: $json.delete_slide_requests || []\n }\n}}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 67216, + 32000 + ], + "id": "2131703b-fd8a-4216-bf1c-c310197046eb", + "name": "HTTP - Borrar slides fotos vacías TEST", + "credentials": { + "googleOAuth2Api": { + "id": "eHseMeH39kRcXgOF", + "name": "Google account 2" + } + } + }, + { + "parameters": { + "jsCode": "const contexto = $('Code - Preparar borrado slides fotos vacías TEST').first().json || {};\nconst respuestaBorrado = $input.first()?.json || {};\n\nreturn [\n {\n json: {\n ...contexto,\n slides_cleanup_done: true,\n slides_delete_response: respuestaBorrado,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 67424, + 32000 + ], + "id": "d23fa3aa-c79e-455c-940d-9584e8ec4731", + "name": "Code - Restaurar contexto slides limpios TEST" + }, + { + "parameters": { + "content": "## 📥 ENTRADA WHATSAPP — CHAT PRIVADO\n\nEste bloque recibe mensajes desde WhatsApp Cloud API y normaliza el evento.\n\nResponsabilidades:\n- Identificar al usuario por su número.\n- Detectar texto, audio, imagen, video o documento.\n- Interpretar botones y comandos oficiales.\n- Detectar país e idioma por prefijo telefónico.\n- Rechazar grupos, reacciones, estados y eventos vacíos.\n\nRegla principal:\nesta versión procesa únicamente conversaciones privadas iniciadas por el usuario.", + "height": 528, + "width": 848 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 46464, + 28720 + ], + "id": "4b32e84a-825a-4395-9176-f6dea26bb5c3", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## 🧾 CONTROL DE SESIONES PRIVADAS\n\nCada usuario tiene una sesión independiente vinculada a su número.\n\nDecisiones:\n- CREAR_SESION: inicia con Hey.\n- AVISO_SESION_ACTIVA: ya existe un reporte abierto.\n- CONTINUAR_SESION: continúa el reporte.\n- CANCELAR_SESION: cancela la sesión.\n- SIN_SESION_ACTIVA: invita a iniciar.\n- MOSTRAR_LEADERBOARD: muestra el ranking.\n\nRegla:\nUna propuesta o reporte activo por número de teléfono.\n", + "height": 624, + "width": 1024, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 47584, + 28624 + ], + "id": "db2a1857-3804-4ec3-9707-c00231fcfcce", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "## 👋 CREACIÓN Y MENSAJES DE CONTROL\n\nEste bloque crea sesiones nuevas y responde a casos operativos básicos.\n\nIncluye:\n- Crear sesión en Google Sheets.\n- Enviar bienvenida y pedir nota de voz.\n- Avisar si ya existe una sesión activa.\n- Avisar si no hay sesión activa.\n- Cancelar sesión cuando el usuario escribe CANCELAR.\n\nLa bienvenida inicia el flujo guiado de 3 pasos:\n1. Audio obligatorio\n2. Imágenes obligatorias\n3. Videos opcionales", + "height": 1904, + "width": 1200, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 50640, + 25136 + ], + "id": "ddbe02ee-a8a4-4184-8097-f1a36ef7e2c3", + "name": "Sticky Note2" + }, + { + "parameters": { + "content": "## 🧭 ROUTER DEL REPORTE ACTIVO\n\nEste bloque decide qué hacer según la etapa actual de la sesión.\n\nEtapas:\n- ESPERANDO_AUDIO\n- ESPERANDO_IMAGENES\n- ESPERANDO_VIDEOS\n- PROCESANDO\n\nDecisiones principales:\n- Guardar audio\n- Pedir audio\n- Guardar imágenes\n- Validar FOTOS LISTAS\n- Guardar videos\n- Cerrar sin video\n- Cerrar con videos\n- Avisar procesamiento\n- Ignorar eventos no válidos\n\nRegla:\ncada mensaje se procesa según la etapa activa, no de forma aislada.", + "height": 912, + "width": 1024, + "color": 2 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 48064, + 29360 + ], + "id": "8e59ac91-36d8-4472-b455-2759fcdb33c9", + "name": "Sticky Note3" + }, + { + "parameters": { + "content": "## 🎙️ PASO 1 — AUDIO OBLIGATORIO\n\nEl reporte no avanza sin nota de voz.\n\nCuando llega un audio:\n- Se guarda el evento en wa_ejecuciones_eventos.\n- Se actualiza la sesión.\n- Se incrementa audio_count.\n- La etapa cambia a ESPERANDO_IMAGENES.\n- El bot pide las imágenes obligatorias.\n\nLa nota de voz debe explicar:\n- Propuesta o referencia\n- Marca/cliente\n- País\n- Ubicación\n- Qué se ejecutó o reportó\n- Comentarios o resultados", + "height": 640, + "width": 1408, + "color": 7 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 50416, + 27232 + ], + "id": "c52a7cdc-e1cd-476f-8bb9-ad08ba2ec556", + "name": "Sticky Note4" + }, + { + "parameters": { + "content": "## 🖼️ PASO 2 — IMÁGENES OBLIGATORIAS\n\nLas imágenes se acumulan dentro de la misma sesión privada, aunque lleguen en mensajes separados.\n\nRedis:\n- Agrupa imágenes recibidas muy próximas.\n- Evita procesar dos veces el mismo lote.\n- Expande y registra cada imagen individualmente.\n- Conserva todas las imágenes anteriores de la sesión.\n\nCuando termine:\nel usuario pulsa o escribe FOTOS LISTAS.", + "height": 416, + "width": 3744, + "color": 6 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 50800, + 28256 + ], + "id": "85d5ebfa-7822-45c5-b0e3-3a0ccac37656", + "name": "Sticky Note5" + }, + { + "parameters": { + "content": "## 🎥 PASO 3 — VIDEOS OPCIONALES\n\nLos videos son opcionales.\n\nOpciones:\n- Si no hay videos: usuario escribe SIN VIDEO.\n- Si envió videos: usuario escribe LISTO.\n- Los videos deben enviarse uno por uno, en mensajes separados.\n\nAl cerrar:\n- La sesión pasa a PROCESANDO.\n- Se envía aviso de reporte recibido.\n- El flujo inicia recuperación, análisis y generación de presentación.\n\nRegla:\nlas imágenes sí pueden ir juntas; los videos deben ir separados.", + "height": 768, + "width": 1632, + "color": "#1D6362" + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 51360, + 29408 + ], + "id": "459549a9-14bf-4d05-ab7e-7c93eb68936e", + "name": "Sticky Note6" + }, + { + "parameters": { + "content": "## 📂 RECUPERACIÓN Y ORGANIZACIÓN DE EVIDENCIAS\n\nProceso:\n1. Lee todos los eventos multimedia de la sesión.\n2. Obtiene la URL temporal de cada media_id desde Meta.\n3. Descarga el archivo binario con Graph API.\n4. Sube audio, imágenes y videos a Google Drive.\n5. Guarda metadata en wa_ejecuciones_media.\n6. Consolida toda la evidencia de la sesión.\n\nCada archivo queda asociado a session_id y ejecucion_id.", + "height": 1728, + "width": 4400, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 49344, + 30336 + ], + "id": "21ffe33b-7fbe-481c-9343-e22f18dd96a2", + "name": "Sticky Note7" + }, + { + "parameters": { + "content": "## 🤖 ANÁLISIS IA Y CONTROL REDIS\n\nGemini analiza cada evidencia multimedia:\n- Audio\n- Imágenes\n- Videos opcionales\n\nCada análisis se guarda en:\nwa_ejecuciones_analisis_media\n\nRedis controla el cierre:\n- Cuenta análisis completados.\n- Verifica si ya están todos listos.\n- Usa lock para evitar cierre duplicado.\n- Si faltan análisis, espera y reintenta.\n\nRegla:\nel JSON final solo se genera cuando el análisis multimedia está completo.", + "height": 1504, + "width": 5712, + "color": 3 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 54512, + 31504 + ], + "id": "70c057eb-8b6f-49ed-a322-008a8e350807", + "name": "Sticky Note8" + }, + { + "parameters": { + "content": "## ✅ CIERRE FINAL DEL REPORTE\n\nEste bloque genera el resultado final del reporte.\n\nProceso:\n1. Consolida análisis multimedia.\n2. Gemini genera JSON final estructurado.\n3. Se normaliza la respuesta.\n4. Se hace match contra el Banco de Propuestas.\n5. Se verifica duplicado.\n6. Se guarda en propuestas_ejecutadas.\n7. Se crea carpeta de ejecución en Drive.\n8. Se copia la plantilla de Slides.\n9. Se insertan imágenes y se borran slides vacíos.\n10. Se actualiza el link final.\n11. Si el match es alto, se actualiza el banco original.\n12. Se envía el link final por WhatsApp.\n\nRegla:\npropuestas externas o match bajo no actualizan el banco original.", + "height": 1024, + "width": 10912, + "color": 6 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 60736, + 31552 + ], + "id": "35fb204a-a809-44b9-8923-2881a4e919f7", + "name": "Sticky Note9" + }, + { + "parameters": { + "content": "## 🔐 MODO PRIVADO TEMPORAL\n\n- Acceso por conversación individual.\n- El usuario debe escribir primero al número oficial.\n- No depende de grupos ni de wa_grupos_permitidos.\n- Respuestas normales dentro de la conversación activa.\n- Botones rápidos disponibles para avanzar, cancelar o consultar RANKING.", + "height": 288, + "width": 944, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 52240, + 28800 + ], + "id": "216d12b6-78d5-4dc9-9b73-5f4ef7626ace", + "name": "Sticky Note10" + }, + { + "parameters": { + "jsCode": "const metadataItems = $input.all();\nconst mediaItems = $('Code - Separar media en items WhatsApp TEST').all();\n\nconst clean = (value) => String(value ?? '').trim();\n\nreturn metadataItems.map((item, index) => {\n const metadata = item.json || {};\n const media = mediaItems[index]?.json || mediaItems[0]?.json || {};\n\n const downloadUrl = clean(metadata.url);\n if (!downloadUrl) {\n throw new Error(`Meta no devolvió URL para media_source_id: ${media.media_source_id || 'SIN_ID'}`);\n }\n\n return {\n json: {\n ...media,\n media_download_url: downloadUrl,\n media_mime_type: clean(metadata.mime_type) || media.media_mime_type || 'application/octet-stream',\n media_sha256: clean(metadata.sha256),\n media_file_size: Number(metadata.file_size || 0),\n estado_procesamiento: 'MEDIA_METADATA_META_RECUPERADA'\n }\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 51984, + 31168 + ], + "id": "4aaec325-8929-4135-bb3d-c6a3fba614da", + "name": "Code - Preparar descarga media Meta WhatsApp TEST" + }, + { + "parameters": { + "url": "={{ $json.media_download_url }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "whatsAppApi", + "options": { + "response": { + "response": { + "responseFormat": "file" + } + } + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 52176, + 31168 + ], + "id": "7eb86614-aadf-40f2-ad70-6a15394b5678", + "name": "HTTP - Descargar media Meta WhatsApp TEST", + "credentials": { + "whatsAppApi": { + "id": "t14kVayc9FurLReq", + "name": "WhatsApp API - GLM CDC" + } + } + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk", + "mode": "list", + "cachedResultName": "Fulgencio - Propuestas Ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 737153956, + "mode": "list", + "cachedResultName": "propuestas_ejecutadas", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nbeuaW2lxvWJYVFD-E0uJRhTGzUa-nuH2WYItHdTsKk/edit#gid=737153956" + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 49520, + 28944 + ], + "id": "5e35ce41-52ac-4fd5-9cdc-5472444ec10e", + "name": "Sheets - Leer propuestas ejecutadas para ranking", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const rows = $input.all().map(item => item.json || {});\nconst context = $('Code - Resolver sesión WhatsApp TEST').first().json || {};\n\nconst clean = (value) => String(value ?? '').trim();\nconst norm = (value) =>\n clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst detectCountryFromPhone = (value) => {\n const n = clean(value).replace(/\\D/g, '');\n\n if (n.startsWith('502')) return 'Guatemala';\n if (n.startsWith('503')) return 'El Salvador';\n if (n.startsWith('504')) return 'Honduras';\n if (n.startsWith('505')) return 'Nicaragua';\n if (n.startsWith('506')) return 'Costa Rica';\n if (n.startsWith('507')) return 'Panamá';\n if (n.startsWith('57')) return 'Colombia';\n if (n.startsWith('52')) return 'México';\n if (n.startsWith('58')) return 'Venezuela';\n if (n.startsWith('1809') || n.startsWith('1829') || n.startsWith('1849')) return 'República Dominicana';\n if (n.startsWith('1876')) return 'Jamaica';\n if (n.startsWith('1868')) return 'Trinidad y Tobago';\n if (n.startsWith('1787') || n.startsWith('1939')) return 'Puerto Rico';\n\n return 'No identificado';\n};\n\nconst isValidCountry = (value) => {\n const country = norm(value);\n\n if (!country) return false;\n\n const invalidValues = [\n 'NO IDENTIFICADO',\n 'PAIS NO IDENTIFICADO',\n 'PAIS +1 NO IDENTIFICADO',\n 'SIN PAIS',\n 'DESCONOCIDO',\n 'UNKNOWN',\n 'N/A',\n 'NO APLICA'\n ];\n\n return !invalidValues.some((invalid) =>\n country === invalid || country.includes(invalid)\n );\n};\n\nconst uniqueExecutions = new Map();\n\nfor (const row of rows) {\n const reportType = norm(\n row.tipo_reporte ||\n row.clasificacion ||\n row.tipo ||\n ''\n );\n\n // El leaderboard mide propuestas ejecutadas; no cuenta externas.\n if (reportType.includes('EXTERNA')) continue;\n\n const executionId = clean(\n row.ejecucion_id ||\n row.session_id ||\n row.presentacion_ejecucion_id ||\n row.presentation_id ||\n row.event_id\n );\n\n if (!executionId) continue;\n if (uniqueExecutions.has(executionId)) continue;\n\n let country = clean(\n row.pais ||\n row.PAIS ||\n row.pais_detectado ||\n row.country\n );\n\n if (!country) {\n country = detectCountryFromPhone(\n row.manager_telefono ||\n row.sender_phone ||\n row.telefono\n );\n }\n\n // El leaderboard debe mostrar exclusivamente países identificados.\n // Si el país no pudo determinarse, esta ejecución no participa en el ranking.\n if (!isValidCountry(country)) continue;\n\n uniqueExecutions.set(executionId, {\n execution_id: executionId,\n country\n });\n}\n\nconst counts = {};\n\nfor (const item of uniqueExecutions.values()) {\n counts[item.country] = (counts[item.country] || 0) + 1;\n}\n\nconst ranking = Object.entries(counts)\n .map(([pais, total]) => ({ pais, total }))\n .sort((a, b) => {\n if (b.total !== a.total) return b.total - a.total;\n return a.pais.localeCompare(b.pais, 'es');\n });\n\nconst medals = ['🥇', '🥈', '🥉'];\n\nconst lines = ranking.length\n ? ranking.map((item, index) =>\n `${medals[index] || `${index + 1}.`} *${item.pais}* — ${item.total}`\n )\n : ['Todavía no hay propuestas ejecutadas registradas.'];\n\nconst language = norm(context.idioma_flujo || 'ES');\nconst total = ranking.reduce((sum, item) => sum + item.total, 0);\nconst updatedAt = new Intl.DateTimeFormat(\n language === 'EN' ? 'en-US' : 'es-DO',\n {\n timeZone: 'America/Santo_Domingo',\n dateStyle: 'medium',\n timeStyle: 'short'\n }\n).format(new Date());\n\nconst message = language === 'EN'\n ? [\n '🏆 *Executed Proposals Leaderboard*',\n '',\n ...lines,\n '',\n `📊 *Total executed proposals:* ${total}`,\n `🕒 Updated: ${updatedAt}`,\n '',\n 'Keep documenting your executions so your country moves up the ranking. 🚀'\n ].join('\\n')\n : [\n '🏆 *Leaderboard de Propuestas Ejecutadas*',\n '',\n ...lines,\n '',\n `📊 *Total de propuestas ejecutadas:* ${total}`,\n `🕒 Actualizado: ${updatedAt}`,\n '',\n 'Sigan documentando sus ejecuciones para que su país suba en el ranking. 🚀'\n ].join('\\n');\n\nreturn [\n {\n json: {\n ...context,\n is_group: false,\n group_id: '',\n group_jid: '',\n leaderboard: ranking,\n leaderboard_total: total,\n whatsapp_to: clean(\n context.whatsapp_to ||\n context.sender_phone ||\n context.manager_telefono\n ).replace(/\\D/g, ''),\n whatsapp_recipient_type: 'individual',\n whatsapp_header: language === 'EN'\n ? 'Country leaderboard'\n : 'Leaderboard por país',\n whatsapp_text: message,\n whatsapp_buttons: [\n { id: 'HEY', title: language === 'EN' ? 'Start report' : 'Iniciar reporte' }\n ]\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 49760, + 28944 + ], + "id": "d00cc55d-e928-47b6-aaf4-4ba54dab0a47", + "name": "Code - Construir leaderboard de países" + }, + { + "parameters": { + "content": "## Preparación del Leaderboard de países", + "height": 272, + "width": 736, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 49312, + 28864 + ], + "id": "cce1ce16-dce8-482a-a0ac-6c917c109978", + "name": "Sticky Note11" + } + ], + "pinData": {}, + "connections": { + "Code - Normalizar evento WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Validar chat privado WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer sesiones existentes": { + "main": [ + [ + { + "node": "Code - Resolver sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Resolver sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Switch - Decisión sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch - Decisión sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Crear sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar aviso sesión activa WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar sin sesión para cancelar WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar cancelación sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar sin sesión para cancelar WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Resolver paso activo WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Leer propuestas ejecutadas para ranking", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar bienvenida WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Crear sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar bienvenida WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Resolver paso activo WhatsApp TEST": { + "main": [ + [ + { + "node": "Switch - Paso activo WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch - Paso activo WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar evento audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar solicitud audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [], + [], + [ + { + "node": "Code - Preparar buffer imagen Redis TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Leer eventos sesión fotos WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar aviso falta imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Guardar evento video WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "CERRAR_SIN_VIDEO", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar cierre con videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar recordatorio videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar aviso procesando WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar solicitud audio WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar evento audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar actualización audio recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización audio recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión audio recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión audio recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar solicitud imágenes WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso sesión activa WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar solicitud imágenes WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar cancelación sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Cancelar sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Cancelar sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar mensaje cancelación WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar mensaje cancelación WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso falta imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar actualización fotos listas": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión fotos listas WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión fotos listas WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar solicitud videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar solicitud videos WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar recordatorio videos WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "CERRAR_SIN_VIDEO": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión cierre sin video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión cierre sin video WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar aviso procesando WhatsApp TEST1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar evento video WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar actualización video recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización video recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión video recibido WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión video recibido WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar confirmación video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar confirmación video WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar cierre con videos WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión cierre con videos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión cierre con videos WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar aviso procesando WhatsApp TEST1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso procesando WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar búsqueda eventos media WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Leer eventos WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer eventos WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Filtrar eventos media de sesión WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Filtrar eventos media de sesión WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Separar media en items WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Separar media en items WhatsApp TEST": { + "main": [ + [ + { + "node": "HTTP - Obtener metadata media Meta WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Subir media WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar registro media Drive WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar registro media Drive WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar media Drive WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar media Drive WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Consolidar media recuperada WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Consolidar media recuperada WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión media recuperada WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión media recuperada WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar paquete análisis Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch - Tipo media para Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar Gemini audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar Gemini imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Preparar Gemini video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Gemini audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Gemini - Analizar audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Analizar audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Normalizar análisis audio WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Gemini imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "Gemini - Analizar imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Analizar imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Normalizar análisis imagen WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar Gemini video WhatsApp TEST": { + "main": [ + [ + { + "node": "Analyze video", + "type": "main", + "index": 0 + } + ] + ] + }, + "Analyze video": { + "main": [ + [ + { + "node": "Code - Normalizar análisis video WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar análisis video WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar análisis imagen WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar análisis audio WhatsApp TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar contador análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Consolidar análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Consolidar análisis media Gemini WhatsApp TEST": { + "main": [ + [ + { + "node": "Gemini - Generar JSON final propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Generar JSON final propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "Code - Normalizar JSON final propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar JSON final propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "Sheets - Leer banco propuestas Fulgencio TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar propuesta ejecutada final TEST": { + "main": [ + [ + { + "node": "Code - Preparar copia presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión análisis completado WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar mensaje final WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar mensaje final WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait": { + "main": [ + [ + { + "node": "Sheets - Leer análisis media Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer propuestas ejecutadas final TEST": { + "main": [ + [ + { + "node": "Code - Verificar duplicado propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Verificar duplicado propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "IF - Propuesta final ya existe TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Propuesta final ya existe TEST": { + "main": [ + [], + [ + { + "node": "Sheets - Guardar propuesta ejecutada final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar copia presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Copiar plantilla presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Normalizar link presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar link presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar reemplazos Slides ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar link presentación ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar actualización banco propuesta ejecutada TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar reemplazos Slides ejecución TEST": { + "main": [ + [ + { + "node": "HTTP Request - Reemplazar textos Slides ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request - Reemplazar textos Slides ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar payload insertar imágenes Slides TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar respuesta Slides ejecución TEST": { + "main": [ + [ + { + "node": "HTTP - Obtener presentación Slides limpiar TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar sin sesión para cancelar WhatsApp TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer eventos sesión fotos WhatsApp TEST": { + "main": [ + [ + { + "node": "Preparar actualización fotos listas", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Drive - Crear carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Crear carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Code - Normalizar carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Code - Preparar media para mover carpeta TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar media para mover carpeta TEST": { + "main": [ + [ + { + "node": "Drive - Mover media a carpeta ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Mover media a carpeta ejecución TEST": { + "main": [ + [ + { + "node": "Code - Confirmar media movida carpeta TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Confirmar media movida carpeta TEST": { + "main": [ + [ + { + "node": "Drive - Copiar plantilla presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar payload insertar imágenes Slides TEST": { + "main": [ + [ + { + "node": "HTTP - Insertar imágenes en Slides TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Insertar imágenes en Slides TEST": { + "main": [ + [ + { + "node": "Code - Normalizar respuesta Slides ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar aviso procesando WhatsApp TEST1": { + "main": [ + [ + { + "node": "WhatsApp - Enviar aviso procesando API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto aviso procesando TEST": { + "main": [ + [ + { + "node": "Code - Preparar búsqueda eventos media WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer banco propuestas Fulgencio TEST": { + "main": [ + [ + { + "node": "Code - Match propuesta banco Fulgencio TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Match propuesta banco Fulgencio TEST": { + "main": [ + [ + { + "node": "Code - Enriquecer datos con match banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar actualización banco propuesta ejecutada TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar decisión automática banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Actualizar banco match alta confianza TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar link ejecutada en banco TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar link ejecutada en banco TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto actualización banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto actualización banco TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión análisis completado WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Enriquecer datos con match banco TEST": { + "main": [ + [ + { + "node": "Sheets - Leer propuestas ejecutadas final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar decisión automática banco TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto decisión automática banco TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto decisión automática banco TEST": { + "main": [ + [ + { + "node": "IF - Actualizar banco match alta confianza TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar buffer imagen Redis TEST": { + "main": [ + [ + { + "node": "Redis - Push buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Push buffer imagen TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto buffer imagen TEST": { + "main": [ + [ + { + "node": "Wait - Esperar cierre buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait - Esperar cierre buffer imagen TEST": { + "main": [ + [ + { + "node": "Redis - Leer buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Leer buffer imagen TEST": { + "main": [ + [ + { + "node": "Code - Decidir procesar lote imagen Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Decidir procesar lote imagen Redis TEST": { + "main": [ + [ + { + "node": "IF - Procesar lote imagen Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Procesar lote imagen Redis TEST": { + "main": [ + [ + { + "node": "Redis - Borrar buffer imagen TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Borrar buffer imagen TEST": { + "main": [ + [ + { + "node": "Code - Expandir lote imagen Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Expandir lote imagen Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Guardar eventos imagen lote Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Guardar eventos imagen lote Redis TEST": { + "main": [ + [ + { + "node": "Code - Consolidar lote imagen guardado Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Consolidar lote imagen guardado Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar sesión imágenes lote Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Actualizar sesión imágenes lote Redis TEST": { + "main": [ + [ + { + "node": "Code - Preparar confirmación imágenes lote Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar confirmación imágenes lote Redis TEST": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Análisis completo Redis TEST": { + "main": [ + [ + { + "node": "Redis - Lock análisis final TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Wait - Verificar cierre análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Lock análisis final TEST": { + "main": [ + [ + { + "node": "Code - Validar lock análisis final TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar lock análisis final TEST": { + "main": [ + [ + { + "node": "IF - Lock análisis final adquirido TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Lock análisis final adquirido TEST": { + "main": [ + [ + { + "node": "Wait", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar contador análisis Redis TEST": { + "main": [ + [ + { + "node": "Redis - Contar análisis media TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Contar análisis media TEST": { + "main": [ + [ + { + "node": "Code - Validar conteo análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar conteo análisis Redis TEST": { + "main": [ + [ + { + "node": "IF - Análisis completo Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait - Verificar cierre análisis Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Leer análisis media cierre Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer análisis media cierre Redis TEST": { + "main": [ + [ + { + "node": "Code - Decidir cierre análisis por Sheet Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Decidir cierre análisis por Sheet Redis TEST": { + "main": [ + [ + { + "node": "IF - Cierre análisis por Sheet Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Cierre análisis por Sheet Redis TEST": { + "main": [ + [ + { + "node": "Redis - Lock análisis final TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Redis - Contar reintento cierre análisis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar reintento cierre análisis TEST": { + "main": [ + [ + { + "node": "IF - Reintentar cierre análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Reintentar cierre análisis Redis TEST": { + "main": [ + [ + { + "node": "Wait - Reintento cierre análisis Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait - Reintento cierre análisis Redis TEST": { + "main": [ + [ + { + "node": "Sheets - Leer análisis media cierre Redis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Redis - Contar reintento cierre análisis TEST": { + "main": [ + [ + { + "node": "Code - Validar reintento cierre análisis TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Obtener presentación Slides limpiar TEST": { + "main": [ + [ + { + "node": "Code - Preparar borrado slides fotos vacías TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar borrado slides fotos vacías TEST": { + "main": [ + [ + { + "node": "IF - Hay slides fotos vacías para borrar TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Hay slides fotos vacías para borrar TEST": { + "main": [ + [ + { + "node": "HTTP - Borrar slides fotos vacías TEST", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Actualizar link presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Borrar slides fotos vacías TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto slides limpios TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto slides limpios TEST": { + "main": [ + [ + { + "node": "Sheets - Actualizar link presentación ejecución TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "WhatsApp Trigger - API Oficial": { + "main": [ + [ + { + "node": "Code - Normalizar evento WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "WhatsApp - Enviar aviso procesando API Oficial": { + "main": [ + [ + { + "node": "Code - Restaurar contexto aviso procesando TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Obtener metadata media Meta WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Preparar descarga media Meta WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Restaurar contexto binario Meta WhatsApp TEST": { + "main": [ + [ + { + "node": "Drive - Subir media WhatsApp TEST", + "type": "main", + "index": 0 + }, + { + "node": "Switch - Tipo media para Gemini WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar descarga media Meta WhatsApp TEST": { + "main": [ + [ + { + "node": "HTTP - Descargar media Meta WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Descargar media Meta WhatsApp TEST": { + "main": [ + [ + { + "node": "Code - Restaurar contexto binario Meta WhatsApp TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer propuestas ejecutadas para ranking": { + "main": [ + [ + { + "node": "Code - Construir leaderboard de países", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Construir leaderboard de países": { + "main": [ + [ + { + "node": "WhatsApp - Enviar mensaje API Oficial", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Validar chat privado WhatsApp TEST": { + "main": [ + [ + { + "node": "IF - Procesar solo chat privado TEST", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Procesar solo chat privado TEST": { + "main": [ + [ + { + "node": "Sheets - Leer sesiones existentes", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "versionId": "32e19443-2c54-4d00-a3d2-9298b17a4189", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "b9s7cKNrgrmBEcHC", + "tags": [] +} \ No newline at end of file diff --git a/Fulgencio Alertas Sheets - GLM.json b/Fulgencio Alertas Sheets - GLM.json new file mode 100644 index 0000000..0884a8c --- /dev/null +++ b/Fulgencio Alertas Sheets - GLM.json @@ -0,0 +1,402 @@ +{ + "name": "Fulgencio Alertas Sheets - GLM", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "weeks", + "triggerAtDay": [ + 5 + ], + "triggerAtHour": 9, + "triggerAtMinute": 30 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -576, + -16 + ], + "id": "8ba8101b-5beb-4b95-ad54-240e1dc83c8f", + "name": "Schedule Trigger" + }, + { + "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" + }, + "filtersUI": { + "values": [] + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + -368, + -16 + ], + "id": "890d0701-b8fb-419e-bf9a-5cc781209c34", + "name": "Sheets - Leer propuestas", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const rows = $input.all();\n\nconst SHEET_URL = 'https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=sharing';\n\nconst MAX_FILAS_PRIORITARIAS = 40;\n\nconst fechaRevisionISO = new Date().toISOString();\n\nconst fechaRevisionVisible = new Intl.DateTimeFormat('es-DO', {\n timeZone: 'America/Santo_Domingo',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n hour12: true,\n}).format(new Date());\n\nconst columnasAValidar = [\n 'NOMBRE',\n 'TIPO DE ACCION',\n 'CLIENTE',\n 'MARCA',\n 'PAIS',\n 'CANAL',\n // APROBADA NO se valida. Ellos la completan luego manualmente.\n 'ETIQUETAS',\n 'AÑO',\n 'Enlace a la propuesta',\n 'Descripcion',\n];\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nfunction escapeHtml(value) {\n return clean(value)\n .replace(/&/g, '&')\n .replace(//g, '>');\n}\n\nfunction filaTieneData(row) {\n const camposClave = [\n 'NOMBRE',\n 'TIPO DE ACCION',\n 'CLIENTE',\n 'MARCA',\n 'PAIS',\n 'Enlace a la propuesta',\n 'Descripcion',\n 'file_id',\n 'nombre_archivo',\n ];\n\n return camposClave.some(campo => clean(row[campo]));\n}\n\nfunction campoPendiente(value, campo) {\n const raw = clean(value);\n const v = norm(value);\n\n if (!raw) {\n return { pendiente: true, motivo: 'vacío' };\n }\n\n if (['UNDEFINED', 'NULL', 'N/A'].includes(v)) {\n return { pendiente: true, motivo: 'valor inválido' };\n }\n\n if (v === 'PENDIENTE') {\n return { pendiente: true, motivo: 'pendiente' };\n }\n\n if (v === 'SIN PAIS DEFINIDO') {\n return { pendiente: true, motivo: 'país sin definir' };\n }\n\n // Detecta si por error cayó un valor de APROBADA en otra columna.\n if (\n campo !== 'APROBADA' &&\n ['PENDIENTE DE APROBACION', 'APROBADA', 'NO APROBADA'].includes(v)\n ) {\n return { pendiente: true, motivo: 'valor de aprobación en columna incorrecta' };\n }\n\n const partes = v.split(',').map(x => x.trim());\n\n if (partes.includes('OTRO')) {\n return { pendiente: true, motivo: 'valor OTRO requiere revisión' };\n }\n\n return { pendiente: false, motivo: '' };\n}\n\nconst detalles = [];\nconst resumenPorFila = new Map();\nconst resumenPorCampo = new Map();\n\nrows.forEach((item, index) => {\n const row = item.json || {};\n\n if (!filaTieneData(row)) {\n return;\n }\n\n const fila =\n row.row_number ||\n row.__row_number ||\n row.__rowNumber ||\n row.rowNumber ||\n index + 2;\n\n const propuesta =\n clean(row.NOMBRE) ||\n clean(row.nombre_archivo) ||\n 'Sin nombre';\n\n const link = clean(row['Enlace a la propuesta']);\n\n for (const campo of columnasAValidar) {\n const resultado = campoPendiente(row[campo], campo);\n\n if (resultado.pendiente) {\n detalles.push({\n fecha_revision: fechaRevisionVisible,\n fecha_revision_iso: fechaRevisionISO,\n fila_origen: fila,\n propuesta,\n campo,\n valor_actual: clean(row[campo]),\n motivo: resultado.motivo,\n enlace_propuesta: link,\n });\n\n if (!resumenPorFila.has(fila)) {\n resumenPorFila.set(fila, {\n fila,\n propuesta,\n link,\n campos: [],\n });\n }\n\n resumenPorFila.get(fila).campos.push(campo);\n resumenPorCampo.set(campo, (resumenPorCampo.get(campo) || 0) + 1);\n }\n }\n});\n\nconst filasPendientes = Array.from(resumenPorFila.values());\nconst columnasResumen = Array.from(resumenPorCampo.entries())\n .map(([campo, total]) => ({ campo, total }))\n .sort((a, b) => b.total - a.total);\n\nif (detalles.length === 0) {\n return [\n {\n json: {\n hay_pendientes: false,\n fecha_revision: fechaRevisionVisible,\n total_filas_pendientes: 0,\n total_campos_pendientes: 0,\n mensaje: '✅ No hay propuestas pendientes de revisión.',\n chat_payload: {\n text: '✅ No hay propuestas pendientes de revisión.',\n },\n reporte_rows: [],\n },\n },\n ];\n}\n\nconst topFilas = filasPendientes\n .sort((a, b) => b.campos.length - a.campos.length)\n .slice(0, MAX_FILAS_PRIORITARIAS);\n\nconst topColumnas = columnasResumen.slice(0, 10);\n\nconst columnasTexto = topColumnas\n .map(c => `• ${escapeHtml(c.campo)}: ${c.total}`)\n .join('
');\n\nconst filasTexto = topFilas\n .map(f => {\n const camposTexto = f.campos.join(', ');\n\n return `• Fila ${f.fila} — ${escapeHtml(f.propuesta)}
${escapeHtml(camposTexto)}`;\n })\n .join('

');\n\nconst textoExtra =\n filasPendientes.length > MAX_FILAS_PRIORITARIAS\n ? `

Mostrando las primeras ${MAX_FILAS_PRIORITARIAS} filas prioritarias. Abre el Banco de Propuestas para ver el resto.`\n : '';\n\nconst chatPayload = {\n text: '🔔 *Revisión Pendiente - Banco de Propuestas*',\n cardsV2: [\n {\n cardId: 'fulgencio-alertas-pendientes',\n card: {\n header: {\n title: '📋 Datos Pendientes - Fulgencio',\n subtitle: `${filasPendientes.length} fila(s) requieren atención`,\n },\n sections: [\n {\n widgets: [\n {\n decoratedText: {\n topLabel: 'Fecha de revisión',\n text: `${escapeHtml(fechaRevisionVisible)}`,\n },\n },\n {\n decoratedText: {\n topLabel: 'Resumen',\n text: `${detalles.length} campo(s) pendientes o por revisar`,\n },\n },\n ],\n },\n {\n header: 'Columnas con más pendientes',\n widgets: [\n {\n textParagraph: {\n text: columnasTexto,\n },\n },\n ],\n },\n {\n header: `Filas Prioritarias - Primeras ${MAX_FILAS_PRIORITARIAS}`,\n widgets: [\n {\n textParagraph: {\n text: filasTexto + textoExtra,\n },\n },\n ],\n },\n {\n widgets: [\n {\n buttonList: {\n buttons: [\n {\n text: 'Abrir Banco de Propuestas',\n onClick: {\n openLink: {\n url: SHEET_URL,\n },\n },\n },\n ],\n },\n },\n ],\n },\n ],\n },\n },\n ],\n};\n\nreturn [\n {\n json: {\n hay_pendientes: true,\n fecha_revision: fechaRevisionVisible,\n total_filas_pendientes: filasPendientes.length,\n total_campos_pendientes: detalles.length,\n mensaje: '🔔 *Revisión Pendiente - Banco de Propuestas*',\n chat_payload: chatPayload,\n reporte_rows: detalles,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -96, + -16 + ], + "id": "264afa9e-1714-42af-8784-a330df03b0d3", + "name": "Code - Detectar pendientes" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "a6b1c13d-7153-492b-88c8-23201d2ba19d", + "leftValue": "={{$json.hay_pendientes}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 176, + -16 + ], + "id": "3a272698-634d-456e-b5d8-4e6fb04af270", + "name": "IF - Hay pendientes" + }, + { + "parameters": { + "method": "POST", + "url": "https://chat.googleapis.com/v1/spaces/AAQADrFA_LI/messages?key=AIzaSyDdI0hCZtE6vySjMm-WEfRq3CPzqKqqsHI&token=OkRPuZYKTNfbs3GUq3i8b9OD7Q_SUzRaupzco1AQMy8", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.chat_payload }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 624, + -192 + ], + "id": "456bce00-b0ee-4dea-90e8-57f7e8c0d18c", + "name": "HTTP Request - Enviar alerta" + }, + { + "parameters": { + "jsCode": "const reporte = $input.first().json.reporte_rows || [];\n\nreturn reporte.map(row => ({\n json: row\n}));" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 624, + 32 + ], + "id": "d513986c-b5b0-493a-a5ef-87655ed050f8", + "name": "Code - Expandir reporte pendientes" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "1LH6TIupvwUVpWu9pOnaWs-jt_xn1ojM_o8bCKURz12E", + "mode": "list", + "cachedResultName": "Fulgencio - Reporte de Pendientes", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1LH6TIupvwUVpWu9pOnaWs-jt_xn1ojM_o8bCKURz12E/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": "gid=0", + "mode": "list", + "cachedResultName": "pendientes_revision", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1LH6TIupvwUVpWu9pOnaWs-jt_xn1ojM_o8bCKURz12E/edit#gid=0" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "fecha_revision": "={{$json.fecha_revision}}", + "fecha_revision_iso": "={{$json.fecha_revision_iso}}", + "fila_origen": "={{$json.fila_origen}}", + "propuesta": "={{$json.propuesta}}", + "campo": "={{$json.campo}}", + "valor_actual": "={{$json.valor_actual}}", + "motivo": "={{$json.motivo}}", + "enlace_propuesta": "={{$json.enlace_propuesta}}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "fecha_revision", + "displayName": "fecha_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "fecha_revision_iso", + "displayName": "fecha_revision_iso", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "fila_origen", + "displayName": "fila_origen", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "propuesta", + "displayName": "propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "campo", + "displayName": "campo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "valor_actual", + "displayName": "valor_actual", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "motivo", + "displayName": "motivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "enlace_propuesta", + "displayName": "enlace_propuesta", + "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": [ + 880, + 32 + ], + "id": "4cffedf9-6dba-4b42-a895-78115034d3df", + "name": "Sheets - Guardar detalle pendientes", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "content": "## ⏰ Revisión programada\n\nEste flujo se ejecuta en horarios definidos para revisar el Google Sheet de propuestas.\n\nLee todas las propuestas registradas y busca campos vacíos, pendientes o que requieran revisión.", + "height": 352, + "width": 432 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -624, + -176 + ], + "id": "84fe4bf9-ff8f-4426-86f3-fccb775fc261", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## 🔎 Validación de datos\n\nSe validan únicamente columnas de negocio:\nNOMBRE, TIPO DE ACCION, CLIENTE, MARCA, PAIS, CANAL, ETIQUETAS, AÑO, enlace y descripción.\n\nNo se validan columnas técnicas como APROBADA, AMBIENTE DE COMPRA ni TÁCTICA PROMOCIONAL.", + "height": 320, + "width": 512, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -128, + -176 + ], + "id": "b0a76191-979c-4378-beee-72b09345ae7e", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "## 📣 Alerta y detalle\n\nSi existen pendientes, se envía una tarjeta resumida al Google Chat de alertas. \n\nEl detalle completo se guarda en el Sheet de reporte para revisión, sin saturar el mensaje del chat. ", + "height": 544, + "width": 624, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 464, + -304 + ], + "id": "e43558a2-ef80-4ddf-8b8b-a9b721367e93", + "name": "Sticky Note2" + } + ], + "pinData": {}, + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Sheets - Leer propuestas", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Leer propuestas": { + "main": [ + [ + { + "node": "Code - Detectar pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Detectar pendientes": { + "main": [ + [ + { + "node": "IF - Hay pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF - Hay pendientes": { + "main": [ + [ + { + "node": "HTTP Request - Enviar alerta", + "type": "main", + "index": 0 + }, + { + "node": "Code - Expandir reporte pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Expandir reporte pendientes": { + "main": [ + [ + { + "node": "Sheets - Guardar detalle pendientes", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "timeSavedMode": "fixed", + "errorWorkflow": "puF4LUczoSz3hcek", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": true, + "timezone": "America/Santo_Domingo" + }, + "versionId": "da501808-d9b8-4050-a953-2490ff7a0f8f", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "UzzVI2AKIXuZGgTL", + "tags": [] +} \ No newline at end of file diff --git a/Fulgencio Procesar Propuestas en Bruto - GLM.json b/Fulgencio Procesar Propuestas en Bruto - GLM.json new file mode 100644 index 0000000..e6f389f --- /dev/null +++ b/Fulgencio Procesar Propuestas en Bruto - GLM.json @@ -0,0 +1,668 @@ +{ + "name": "Fulgencio Procesar Propuestas en Bruto - GLM", + "nodes": [ + { + "parameters": { + "text": "=Nombre del archivo original: {{$json.nombre_archivo}}\nNombre limpio de la propuesta: {{$json.nombre_limpio}}\nPaís indicado en el nombre del archivo: {{$json.pais}}\nTipo de archivo: {{$json.mime_type}}\nLink: {{$json.link_drive}}", + "attributes": { + "attributes": [ + { + "name": "nombre_propuesta", + "description": "Nombre limpio o comercial de la propuesta, basado en el nombre del archivo." + }, + { + "name": "cliente", + "description": "Empresa cliente principal de la propuesta. Si no se puede determinar, usar PENDIENTE." + }, + { + "name": "marca", + "description": "Marca o producto trabajado en la propuesta. Si no se puede determinar, usar PENDIENTE." + }, + { + "name": "cliente_normalizado", + "description": "Cliente escrito de forma estándar en mayúsculas, sin tildes ni variaciones innecesarias. Ejemplo: NESTLE, MOTOROLA, WHIRLPOOL. Si no se puede determinar, usar PENDIENTE." + }, + { + "name": "marca_normalizada", + "description": "Marca escrita de forma estándar en mayúsculas, sin tildes ni variaciones innecesarias. Ejemplo: KITKAT, MAGGI, NESCAFE, MOTOROLA. Si no se puede determinar, usar PENDIENTE." + }, + { + "name": "anio", + "description": "Año de la propuesta si aparece en el nombre o información disponible. Si no se puede determinar, usar PENDIENTE." + }, + { + "name": "tipo_accion", + "description": "Tipo de acción o formato de propuesta. Ejemplos: activación PDV, stand, evento, promoción, sampling, uniforme, diseño gráfico, campaña, mueble, exhibidor, material POP, render. Si no se puede determinar, usar PENDIENTE." + }, + { + "name": "canal", + "description": "Canal o lugar donde aplica la propuesta. Ejemplos: supermercado, farmacia, colmado, retail, evento, universidad, punto de venta, canal moderno, canal tradicional. Si no se puede determinar, usar PENDIENTE." + }, + { + "name": "tags", + "description": "Palabras clave separadas por comas para facilitar búsqueda. Ejemplos: sampling, ruleta, supermercado, premios, stand, degustación, material POP." + }, + { + "name": "descripcion", + "description": "Párrafo profesional de 35 a 70 palabras para describir la propuesta. Debe seguir el estilo del banco actual: \"La presentación muestra...\" o \"La pieza muestra...\", mencionar cliente/marca, tipo de acción, elementos incluidos y objetivo. No inventar detalles no disponibles." + }, + { + "name": "tactica_promocional", + "description": "Táctica promocional principal de la propuesta. Debe elegir o inferir una opción como: DEGUSTACIÓN, RULETA, SAMPLING - MUESTREO, PLINKO, WHATSAPP, LANDING PAGE, POP, PHOTOBOOTH, CANJE, SORTEO, PREMIOS INSTANTÁNEOS, JUEGO DIGITAL, TRIVIA, EXHIBICIÓN, IMPULSO, o PENDIENTE si no se puede determinar claramente." + } + ] + }, + "options": { + "systemPromptTemplate": "Analiza la siguiente propuesta usando el nombre del archivo y la información disponible.\n\nIMPORTANTE:\nNo inventes el país. El país ya viene definido por la carpeta y no debes cambiarlo.\nNo intentes determinar si la propuesta fue aprobada o no.\nNo intentes determinar el ambiente de compra.\nSi no puedes determinar un campo con claridad, usa \"PENDIENTE\".\n\nExtrae metadata para alimentar el banco de propuestas de Fulgencio Fumado.\n\nDatos disponibles:\nNombre del archivo original: {{$json.nombre_archivo}}\nNombre limpio de la propuesta: {{$json.nombre_limpio}}\nPaís indicado en el nombre del archivo: {{$json.pais}}\nTipo de archivo: {{$json.mime_type}}\nLink: {{$json.link_drive}}\n\nDevuelve la información estructurada con estos campos:\n- nombre_propuesta\n- cliente\n- marca\n- cliente_normalizado\n- marca_normalizada\n- anio\n- tipo_accion\n- canal\n- tags\n- descripcion\n- tactica_promocional\n\nReglas:\n1. El país viene indicado en el nombre del archivo con el formato [PAIS=...]. No inventes el país ni lo cambies. Si el país viene como SIN PAIS DEFINIDO, mantenlo como pendiente de revisión.\n2. No determines APROBADA. Ese campo se guardará como PENDIENTE.\n3. No determines AMBIENTE DE COMPRA (RE). Ese campo se guardará como PENDIENTE.\n4. Si no sabes cliente, marca, canal, año o cualquier otro campo, usa \"PENDIENTE\".\n5. El tipo de acción puede inferirse del nombre y contexto. Ejemplos: activación PDV, stand, evento, promoción, sampling, uniforme, diseño gráfico, campaña, mueble, exhibidor, material POP, render, propuesta creativa.\n6. Para tactica_promocional, intenta inferir la mecánica principal de la propuesta. Usa valores como: DEGUSTACIÓN, RULETA, SAMPLING - MUESTREO, PLINKO, WHATSAPP, LANDING PAGE, POP, PHOTOBOOTH, CANJE, SORTEO, PREMIOS INSTANTÁNEOS, JUEGO DIGITAL, TRIVIA, EXHIBICIÓN, IMPULSO o PENDIENTE.\n7. Tags debe ser una lista corta de palabras clave separadas por comas.\n8. La descripción debe ser un párrafo profesional de 35 a 70 palabras, similar al estilo usado en el banco de propuestas.\n\nDebe iniciar preferiblemente con:\n\"La presentación muestra...\" o \"La pieza muestra...\" o \"La propuesta corresponde a...\"\n\nLa descripción debe mencionar, cuando sea posible:\n- cliente o marca\n- tipo de acción\n- concepto o tema principal\n- elementos incluidos\n- objetivo comercial o creativo\n\nNo inventes detalles que no estén en el nombre del archivo, metadata disponible o información clara. Si no hay suficiente información, redacta una descripción general pero honesta, indicando el tipo de propuesta y la marca/cliente detectado.\n9. Normaliza cliente y marca en mayúsculas, sin tildes y sin variaciones innecesarias. Ejemplos: NESTLE, MOTOROLA, WHIRLPOOL, KITKAT, MAGGI, NESCAFE." + } + }, + "type": "@n8n/n8n-nodes-langchain.informationExtractor", + "typeVersion": 1.2, + "position": [ + 784, + 0 + ], + "id": "372000c5-2b6b-41bc-ae01-bb55d6608f16", + "name": "Gemini - Extraer metadata" + }, + { + "parameters": { + "modelName": "models/gemini-2.5-pro", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", + "typeVersion": 1, + "position": [ + 800, + 192 + ], + "id": "4dd11651-19f5-4832-88bd-1b3dbd2d0991", + "name": "Gemini - Extraer metadata1", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + }, + { + "parameters": { + "jsCode": "// ===============================\n// Code - Normalizar respuesta\n// Fulgencio - Procesar Propuestas en Bruto DEV\n// ===============================\n\n// Datos originales del archivo antes de Gemini\nconst original = $('Code - Preparar archivo').item.json || {};\n// En tu versión, Gemini devuelve la data dentro de \"output\"\nconst incoming = $input.first().json || {};\nconst ai = incoming.output || incoming;\n\n// -------------------------------\n// Helpers\n// -------------------------------\n\nfunction clean(value) {\n return String(value ?? '')\n .trim()\n .replace(/\\s+/g, ' ');\n}\n\nfunction removeAccents(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '');\n}\n\nfunction upper(value) {\n return removeAccents(value).toUpperCase();\n}\n\nfunction splitMulti(value) {\n return clean(value)\n .split(',')\n .map(v => upper(v))\n .map(v => v.trim())\n .filter(Boolean);\n}\n\nfunction unique(values) {\n return [...new Set(values.filter(Boolean))];\n}\n\nfunction joinMulti(values) {\n return unique(values).join(', ');\n}\n\nfunction normalizeCommon(value) {\n const v = upper(value);\n\n const synonyms = {\n 'KITKAT': 'KIT KAT',\n 'KIT KAT': 'KIT KAT',\n 'NESTLÉ': 'NESTLE',\n 'NESTLE': 'NESTLE',\n 'NESCAFÉ': 'NESCAFE',\n 'NESCAFE': 'NESCAFE',\n 'COFFEE MATE': 'COFFE MATE',\n 'COFFEEMATE': 'COFFE MATE',\n 'P&G': 'PROCTER & GAMBLE',\n 'PROCTER AND GAMBLE': 'PROCTER & GAMBLE',\n 'PROCTER & GAMBLE': 'PROCTER & GAMBLE',\n 'WHIRPOOL': 'WHIRLPOOL'\n };\n\n return synonyms[v] || v;\n}\n\nfunction normalizeTipoAccion(value) {\n const v = upper(value);\n\n const synonyms = {\n 'ACTIVACION PDV': 'ACTIVACION EN PDV',\n 'ACTIVACIÓN PDV': 'ACTIVACION EN PDV',\n 'ACTIVACION EN PUNTO DE VENTA': 'ACTIVACION EN PDV',\n 'ACTIVACION PUNTO DE VENTA': 'ACTIVACION EN PDV',\n 'PUNTO DE VENTA': 'ACTIVACION EN PDV',\n 'PDV': 'ACTIVACION EN PDV',\n 'STAND FERIA': 'STAND, FERIA',\n 'DISEÑO GRAFICO': 'DISENO GRAFICO',\n 'DISEÑO GRÁFICO': 'DISENO GRAFICO',\n 'CAMPANA': 'CAMPAÑA',\n 'PROMOCION': 'PROMOCION',\n 'PROMOCIÓN': 'PROMOCION',\n 'ACTIVACION': 'ACTIVACION',\n 'ACTIVACIÓN': 'ACTIVACIÓN',\n 'INAUGURACION': 'INAUGURACIÓN'\n };\n\n return synonyms[v] || v;\n}\n\nfunction normalizeCanal(value) {\n const v = upper(value);\n\n const synonyms = {\n 'PUNTO DE VENTA': 'PDV',\n 'PDV': 'PDV',\n 'CANAL MODERNO': 'MODERNO',\n 'MODERNO': 'MODERNO',\n 'CANAL TRADICIONAL': 'TRADICIONAL',\n 'TRADICIONAL': 'TRADICIONAL',\n 'DIGITAL': 'ONLINE',\n 'ONLINE': 'ONLINE',\n 'NO APLICA': 'NO APLICA',\n 'N/A': 'NO APLICA'\n };\n\n return synonyms[v] || v;\n}\n\nfunction normalizeTactica(value) {\n const v = upper(value);\n\n const synonyms = {\n 'SAMPLING': 'SAMPLING - MUESTREO',\n 'MUESTREO': 'SAMPLING - MUESTREO',\n 'DEGUSTACION': 'DEGUSTACIÓN',\n 'DEGUSTACIÓN': 'DEGUSTACIÓN',\n 'RULETA DIGITAL': 'RULETA',\n 'PLINKO': 'PLINKO',\n 'WHATSAPP': 'WHATSAPP',\n 'LANDING': 'LANDING PAGE',\n 'LANDING PAGE': 'LANDING PAGE',\n 'MATERIAL POP': 'POP',\n 'POP': 'POP',\n 'JUEGO DIGITAL': 'JUEGO DIGITAL',\n 'JUEGOS DIGITALES': 'JUEGO DIGITAL'\n };\n\n return synonyms[v] || v;\n}\n\nfunction normalizeEtiqueta(value) {\n const v = upper(value);\n\n const synonyms = {\n 'ACTIVACION': 'ACTIVACION',\n 'ACTIVACIÓN': 'ACTIVACION',\n 'EXHIBICION': 'EXHIBICION',\n 'EXHIBICIÓN': 'EXHIBICION',\n 'DEGUSTACION': 'DEGUSTACION',\n 'DEGUSTACIÓN': 'DEGUSTACION',\n 'PROMOCION': 'PROMOCIÓN',\n 'PROMOCIÓN': 'PROMOCIÓN',\n 'DISENO': 'DISEÑO',\n 'DISEÑO': 'DISEÑO',\n 'CAMPANA': 'CAMPAÑA',\n 'CAMPAÑA': 'CAMPAÑA',\n 'JUEGO DIGITAL': 'JUEGOS DIGITALES',\n 'JUEGOS DIGITALES': 'JUEGOS DIGITALES',\n 'PUNTO DE VENTA': 'PUNTO DE VENTA',\n 'PDV': 'PUNTO DE VENTA',\n 'F1': 'JUEGOS',\n 'SIMULADOR': 'JUEGOS DIGITALES',\n 'EXPERIENCIA': 'ACTIVIDADES'\n };\n\n return synonyms[v] || v;\n}\n\nfunction normalizeOne(value, allowedList, fallback = 'PENDIENTE', normalizer = normalizeCommon) {\n const raw = clean(value);\n if (!raw) return fallback;\n\n const mapped = normalizer(raw);\n return allowedList.includes(mapped) ? mapped : fallback;\n}\n\nfunction normalizeMulti(value, allowedList, fallback = 'PENDIENTE', unknownValue = 'OTRO', normalizer = normalizeCommon) {\n const parts = splitMulti(value);\n if (!parts.length) return fallback;\n\n const result = [];\n\n for (const part of parts) {\n const mapped = normalizer(part);\n\n // Si el normalizador devolvió varios valores separados por coma\n const expanded = String(mapped).split(',').map(v => v.trim()).filter(Boolean);\n\n for (const val of expanded) {\n if (allowedList.includes(val)) {\n result.push(val);\n } else {\n result.push(unknownValue);\n }\n }\n }\n\n const finalValues = unique(result);\n if (!finalValues.length) return fallback;\n\n return joinMulti(finalValues);\n}\n\n// -------------------------------\n// Listas permitidas\n// -------------------------------\n\nconst ALLOWED_TIPO_ACCION = [\n 'ACTIVACION EN PDV',\n 'IMPULSO DE VENTA',\n 'ACTIVACION FUERA DE PDV',\n 'EVENTO',\n 'KICK OFF',\n 'FERIA',\n 'STAND',\n 'MUEBLE',\n 'DISENO GRAFICO',\n 'DIGITAL',\n 'PUBLICIDAD',\n 'TUTORIAL',\n 'INFORMATIVA',\n 'LOGOTIPO',\n 'PROMOCION',\n 'UNIFORME',\n 'ACTIVACIÓN',\n 'INAUGURACIÓN',\n 'PARQUE',\n 'TROFEO',\n 'ANAQUEL',\n 'FOOD TRUCK',\n 'EMPAQUES',\n 'CAMPAÑA',\n 'QUICK COUNTER',\n 'DISEÑO',\n 'PPT',\n 'PENDIENTE',\n 'OTRO'\n];\n\nconst ALLOWED_CLIENTE = [\n 'CLARO',\n 'GRUPO AJE',\n 'KITCHENAID',\n 'MOLINO CRIOLLO',\n 'NESTLE',\n 'PROCTER & GAMBLE',\n 'WHIRLPOOL',\n 'XTRA',\n 'MOTOROLA',\n 'GLM',\n 'IQM',\n 'BERMUDEZ',\n 'STARBUCKS',\n 'ALCATEL',\n 'GUARINA',\n 'COLGATE',\n 'CESAR IGLESIAS',\n 'SKINPHARMA',\n 'UNIT',\n 'GRUPO UMA',\n 'COVO',\n 'CHICAGO CUBS',\n 'FORTINET',\n 'LENOVO',\n 'ALTICE',\n 'EL MACHETAZO',\n 'GRANDBAY',\n 'EY',\n 'BROWN-FORMAN',\n 'MALHER',\n 'PENDIENTE',\n 'OTRO'\n];\n\nconst ALLOWED_MARCA = [\n 'BIG COLA',\n 'CARNATION',\n 'CENTURIONES',\n 'CHOCOTRÍO',\n 'CLARO EMPRESAS',\n 'IMPULSO',\n 'KIT KAT',\n 'KITCHENAID',\n 'ACROS',\n 'MAGGI',\n 'MULTIMARCAS',\n 'QUESO QUE RICO',\n 'WHIRLPOOL',\n 'XTRA',\n 'BERMUDEZ',\n 'NESTLE',\n 'PURINA',\n 'NESCAFE',\n 'NIDO',\n 'IDEAL',\n 'LECHERA',\n 'STARBUCKS',\n 'HEREFORD BEEF',\n 'SKECHERS',\n 'BELCA',\n 'KLIM',\n 'PULSAR',\n 'NESQUIK',\n 'RON BERMUDEZ',\n 'US MEAT',\n 'BMI',\n 'BRUNI',\n 'MARS',\n 'NAICOM',\n 'CEMIX',\n 'COLGATE',\n 'PALMOLIVE',\n 'D_GUSSTO',\n 'FABULOSO',\n 'NAN',\n 'US RICE PRODUCERS',\n 'PEPSICO',\n 'ALKA SELTZER',\n 'HELADERÍA ESTRELLA AZUL',\n 'MAYTAG',\n 'MICROSOFT',\n 'MATERNA',\n 'SIRENA',\n 'EQUIPAO',\n 'FARMACIAS ARROCHA',\n 'TROPICARNATION',\n 'NESTÚM',\n 'M&M',\n \"FRITO-LAY'S\",\n 'PROPLAN',\n 'CORRIPIO',\n 'TACO BELL',\n 'EL MOLINITO',\n 'BRUGAL',\n 'PROTEX',\n 'SUAVITEL',\n 'IQOS',\n 'COFFE MATE',\n 'LANCO',\n 'ALCATEL',\n 'GUARINA',\n 'HATUEY',\n 'GALLETAS DINO',\n 'MILO',\n 'DIARIO LIBRE',\n 'NESTOGENO 3',\n 'PERAS USA',\n 'PERRIER',\n 'RAYOVAC',\n 'SUNIX',\n 'BON AGROINDUSTRIAL SA',\n 'MINI DOLCE',\n 'DOLCE GUSTO',\n 'V8 SPLASH',\n 'COLPAL',\n 'GERBER',\n 'MOTOROLA',\n 'CLARO',\n 'BRILLANTE',\n 'SKINPHARMA',\n 'AMANA',\n 'UNIT',\n 'GRUPO UMA',\n 'LUCKY CHARMS',\n 'CINNAMON TOAST CRUNCH',\n 'NATURE´S HEART',\n 'COVO',\n 'CHICAGO CUBS',\n 'FORTINET',\n 'LENOVO LEGION',\n 'LENOVO',\n 'ALTICE',\n 'ORAL- B',\n 'HEAD & SHOULDERS',\n 'PAMPERS',\n 'PROCTER & GAMBLE',\n 'EL MACHETAZO',\n 'GRANDBAY',\n 'EY',\n 'JIMADOR',\n 'TEQUILA HERRADURA',\n 'MALHER',\n 'PENDIENTE',\n 'OTRO'\n];\n\nconst ALLOWED_PAIS = [\n 'EL SALVADOR',\n 'PANAMA',\n 'REPUBLICA DOMINICANA',\n 'COLOMBIA',\n 'PUERTO RICO',\n 'HONDURAS',\n 'MEXICO',\n 'VENEZUELA',\n 'JAMAICA',\n 'TRINIDAD Y TOBAGO',\n 'COSTA RICA',\n 'NICARAGUA',\n 'GUATEMALA',\n 'SIN PAIS DEFINIDO'\n];\n\nconst ALLOWED_CANAL = [\n 'MODERNO',\n 'TRADICIONAL',\n 'ONLINE',\n 'NO APLICA',\n 'PDV',\n 'PENDIENTE'\n];\n\nconst ALLOWED_AMBIENTE = [\n 'HIPERMERCADO',\n 'SUPERMERCADO',\n 'MINI SUPER',\n 'FARMACIA',\n 'MAYORISTAS',\n 'MOM AND POP - COLMADOS - TIENDITAS',\n 'MERCADOS',\n 'TIENDA ON LINE',\n 'TIENDA DE ELECTRODOMÉSTICOS',\n 'TIENDA DE ELECTRÓNICA',\n 'PENDIENTE'\n];\n\nconst ALLOWED_TACTICA = [\n 'DEGUSTACIÓN',\n 'RULETA',\n 'SAMPLING - MUESTREO',\n 'PLINKO',\n 'WHATSAPP',\n 'LANDING PAGE',\n 'POP',\n 'PHOTOBOOTH',\n 'CANJE',\n 'SORTEO',\n 'PREMIOS INSTANTÁNEOS',\n 'JUEGO DIGITAL',\n 'TRIVIA',\n 'EXHIBICIÓN',\n 'IMPULSO',\n 'PENDIENTE',\n 'OTRO'\n];\n\nconst ALLOWED_ETIQUETAS = [\n 'ACTIVACION',\n 'ACTIVIDADES',\n 'BANDEJA',\n 'BTS',\n 'CORPORATIVO',\n 'DEGUSTACION',\n 'EXHIBICION',\n 'EXHIBIDOR',\n 'FUERZA DE VENTAS',\n 'JUEGOS',\n 'PREMIOS',\n 'PUNTO DE VENTA',\n 'REFERENCIAS',\n 'REUNIÓN',\n 'RULETA',\n 'SAMPLING',\n 'HALLOWEEN',\n 'LUCES NEON',\n 'INFORMATIVA',\n 'CARRITO DEGUSTACIÓN',\n 'RED SOCIAL/INFLUENCER',\n 'UNIFORMES',\n 'REGALIAS',\n 'STAND',\n 'CAPACITACIÓN',\n 'NAVIDAD',\n 'DÍA DE LAS MADRES',\n 'INDEPENDENCIA',\n 'LOGOTIPO',\n 'QUICK COUNTER',\n 'PPT',\n 'TEMPLATES',\n 'DISEÑO',\n 'PROMOCIÓN',\n 'MODERNO',\n 'MAYORISTA',\n 'PULPERÍAS',\n 'JUEGOS DIGITALES',\n 'WHATSAPP',\n 'CAMPAÑA',\n 'CONCURSO',\n 'REGALOS CORPORATIVOS',\n 'PENDIENTE',\n 'OTRO'\n];\n\nconst ALLOWED_ANIO = [\n '2026',\n '2025',\n '2024',\n '2023',\n '2022',\n '2021',\n '2020',\n '2019',\n '2018',\n '2017',\n '2016',\n '2015',\n '2014',\n '2013',\n '2008',\n 'PENDIENTE'\n];\n\n// -------------------------------\n// Normalización de campos\n// -------------------------------\n\nlet nombrePropuesta = clean(ai.nombre_propuesta || original.nombre_limpio || original.nombre_archivo || '');\nif (!nombrePropuesta) nombrePropuesta = 'PENDIENTE';\n\nconst tipoAccion = normalizeMulti(\n ai.tipo_accion,\n ALLOWED_TIPO_ACCION,\n 'PENDIENTE',\n 'OTRO',\n normalizeTipoAccion\n);\n\nconst cliente = normalizeOne(\n ai.cliente_normalizado || ai.cliente,\n ALLOWED_CLIENTE,\n 'OTRO',\n normalizeCommon\n);\n\nconst marca = normalizeMulti(\n ai.marca_normalizada || ai.marca,\n ALLOWED_MARCA,\n 'PENDIENTE',\n 'OTRO',\n normalizeCommon\n);\n\n// País viene de carpeta, no de Gemini\nlet pais = upper(original.pais);\nif (pais === 'PENDIENTE' || pais === 'SIN PAIS DEFINIDO' || !pais) {\n pais = 'SIN PAIS DEFINIDO';\n}\nif (!ALLOWED_PAIS.includes(pais)) {\n pais = 'SIN PAIS DEFINIDO';\n}\n\nconst canal = normalizeOne(\n ai.canal,\n ALLOWED_CANAL,\n 'PENDIENTE',\n normalizeCanal\n);\n\n// Por ahora no lo determina Gemini\nconst ambienteCompra = 'PENDIENTE';\n\nconst tacticaPromocional = normalizeMulti(\n ai.tactica_promocional,\n ALLOWED_TACTICA,\n 'PENDIENTE',\n 'OTRO',\n normalizeTactica\n);\n\n// APROBADA no la decide Gemini\nconst aprobada = 'PENDIENTE DE APROBACION';\n\nlet etiquetas = normalizeMulti(\n ai.tags,\n ALLOWED_ETIQUETAS,\n 'PENDIENTE',\n 'OTRO',\n normalizeEtiqueta\n);\n\n// Si hay etiquetas válidas y también salió OTRO, quitamos OTRO de la columna principal.\n// El motivo de revisión se mantiene más abajo.\nif (etiquetas.includes('OTRO') && etiquetas !== 'OTRO') {\n etiquetas = etiquetas\n .split(',')\n .map(v => v.trim())\n .filter(v => v !== 'OTRO')\n .join(', ');\n}\n\nlet anio = clean(ai.anio);\nif (!ALLOWED_ANIO.includes(anio)) {\n anio = 'PENDIENTE';\n}\n\nconst descripcion = clean(ai.descripcion);\n\n// Enlace: si Google Drive no devolvió webViewLink, construimos uno con file_id\nconst fileId = original.file_id || original.id || '';\nconst linkDrive = original.link_drive || (fileId ? `https://drive.google.com/file/d/${fileId}/view?usp=sharing` : '');\n\n// -------------------------------\n// Reglas de revisión\n// -------------------------------\n\nlet requiereRevision = false;\nconst revisionReasons = [];\n\nif (pais === 'SIN PAIS DEFINIDO') {\n requiereRevision = true;\n revisionReasons.push('pais_sin_definir');\n}\n\nif (tipoAccion.includes('OTRO') || tipoAccion === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('tipo_accion_revisar');\n}\n\nif (cliente === 'OTRO' || cliente === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('cliente_revisar');\n}\n\nif (marca.includes('OTRO') || marca === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('marca_revisar');\n}\n\nif (canal === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('canal_revisar');\n}\n\nif (tacticaPromocional.includes('OTRO') || tacticaPromocional === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('tactica_revisar');\n}\n\nif (etiquetas.includes('OTRO') || etiquetas === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('etiquetas_revisar');\n}\n\nif (anio === 'PENDIENTE') {\n requiereRevision = true;\n revisionReasons.push('anio_revisar');\n}\n\n// -------------------------------\n// Salida final para Google Sheets\n// -------------------------------\n\nreturn [\n {\n json: {\n NOMBRE: nombrePropuesta,\n 'TIPO DE ACCION': tipoAccion,\n CLIENTE: cliente,\n MARCA: marca,\n PAIS: pais,\n CANAL: canal,\n 'AMBIENTE DE COMPRA (RE)': ambienteCompra,\n 'TÁCTICA PROMOCIONAL': tacticaPromocional,\n APROBADA: aprobada,\n ETIQUETAS: etiquetas,\n 'AÑO': anio,\n 'Enlace a la propuesta': linkDrive,\n Descripcion: descripcion,\n\n file_id: fileId,\n nombre_archivo: original.nombre_archivo || '',\n mime_type: original.mime_type || '',\n fuente_pais: original.fuente_pais || '',\n confianza_pais: original.confianza_pais || '',\n requiere_revision: requiereRevision,\n motivos_revision: revisionReasons.join(', '),\n procesado_ia: true,\n ultima_actualizacion: new Date().toISOString()\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1136, + 0 + ], + "id": "3556d926-7b9d-421c-9145-1b19095ebc34", + "name": "Code - Normalizar respuesta" + }, + { + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "mode": "id", + "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng" + }, + "sheetName": { + "__rl": true, + "value": "0", + "mode": "id" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "file_id": "={{$json.file_id}}", + "nombre_archivo": "={{$json.nombre_archivo}}", + "CLIENTE": "={{$json.CLIENTE}}", + "MARCA": "={{$json.MARCA}}", + "PAIS": "={{$json.PAIS}}", + "fuente_pais": "={{$json.fuente_pais}}", + "confianza_pais": "={{$json.confianza_pais}}", + "AÑO": "={{$json[\"AÑO\"]}}", + "CANAL": "={{$json.CANAL}}", + "Descripcion": "={{$json.Descripcion}}", + "mime_type": "={{$json.mime_type}}", + "procesado_ia": "={{$json.procesado_ia}}", + "requiere_revision": "={{$json.requiere_revision}}", + "ultima_actualizacion": "={{$json.ultima_actualizacion}}", + "NOMBRE": "={{$json.NOMBRE}}", + "APROBADA": "={{$json.APROBADA}}", + "ETIQUETAS": "={{$json.ETIQUETAS}}", + "Enlace a la propuesta": "={{$json[\"Enlace a la propuesta\"]}}", + "AMBIENTE DE COMPRA (RE)": "={{$json[\"AMBIENTE DE COMPRA (RE)\"]}}", + "TÁCTICA PROMOCIONAL": "={{$json[\"TÁCTICA PROMOCIONAL\"]}}", + "TIPO DE ACCION": "={{$json[\"TIPO DE ACCION\"]}}", + "motivos_revision": "={{$json.motivos_revision}}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "NOMBRE", + "displayName": "NOMBRE", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "TIPO DE ACCION", + "displayName": "TIPO DE ACCION", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "CLIENTE", + "displayName": "CLIENTE", + "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": "PAIS", + "displayName": "PAIS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "CANAL", + "displayName": "CANAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "AMBIENTE DE COMPRA (RE)", + "displayName": "AMBIENTE DE COMPRA (RE)", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "TÁCTICA PROMOCIONAL", + "displayName": "TÁCTICA PROMOCIONAL", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "APROBADA", + "displayName": "APROBADA", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "ETIQUETAS", + "displayName": "ETIQUETAS", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "AÑO", + "displayName": "AÑO", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Enlace a la propuesta", + "displayName": "Enlace a la propuesta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "Descripcion", + "displayName": "Descripcion", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "file_id", + "displayName": "file_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "nombre_archivo", + "displayName": "nombre_archivo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "mime_type", + "displayName": "mime_type", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "fuente_pais", + "displayName": "fuente_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "confianza_pais", + "displayName": "confianza_pais", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "requiere_revision", + "displayName": "requiere_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "procesado_ia", + "displayName": "procesado_ia", + "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": "motivos_revision", + "displayName": "motivos_revision", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Enlaces a propuestas ejecutadas", + "displayName": "Enlaces a propuestas ejecutadas", + "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": [ + 1344, + 0 + ], + "id": "2fe2b17c-176c-4aa3-8c4a-7554a08f2612", + "name": "Sheets - Agregar propuesta", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "operation": "move", + "fileId": { + "__rl": true, + "value": "={{ $('Code - Normalizar respuesta').item.json.file_id }}", + "mode": "id" + }, + "driveId": { + "__rl": true, + "mode": "list", + "value": "My Drive" + }, + "folderId": { + "__rl": true, + "value": "1pu75Q4uzt3Tsf6uoXt4ZjYIvdddIdIJB", + "mode": "id" + } + }, + "type": "n8n-nodes-base.googleDrive", + "typeVersion": 3, + "position": [ + 1552, + 0 + ], + "id": "8d8418b0-3f41-4760-ac44-ef85b1c6cca8", + "name": "Drive - Mover a procesadas", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "jsCode": "const item = $input.first().json;\n\nconst fileName = item.name || '';\nconst fileId = item.id || '';\nconst mimeType = item.mimeType || '';\nconst linkDrive =\n item.webViewLink ||\n (fileId ? `https://drive.google.com/file/d/${fileId}/view?usp=sharing` : '');\n\nconst allowedCountries = [\n 'EL SALVADOR',\n 'PANAMA',\n 'REPUBLICA DOMINICANA',\n 'COLOMBIA',\n 'PUERTO RICO',\n 'HONDURAS',\n 'MEXICO',\n 'VENEZUELA',\n 'JAMAICA',\n 'TRINIDAD Y TOBAGO',\n 'COSTA RICA',\n 'NICARAGUA',\n 'GUATEMALA',\n 'SIN PAIS DEFINIDO'\n];\n\nfunction clean(value) {\n return String(value || '').trim().replace(/\\s+/g, ' ');\n}\n\nfunction normalize(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nfunction removePaisPrefix(name) {\n return name\n .replace(/^\\s*\\[PAIS\\s*=\\s*[^\\]]+\\]\\s*/i, '')\n .trim();\n}\n\n// Busca formato: [PAIS=COSTA RICA] Nombre archivo.pptx\nconst match = fileName.match(/\\[PAIS\\s*=\\s*([^\\]]+)\\]/i);\n\nlet paises = [];\nlet fuentePais = 'nombre_archivo';\nlet confianzaPais = 'alta';\nlet requiereRevision = false;\nlet motivosRevision = [];\n\nif (match && match[1]) {\n paises = match[1]\n .split(',')\n .map(p => normalize(p))\n .filter(Boolean);\n\n paises = [...new Set(paises)];\n\n const paisesInvalidos = paises.filter(p => !allowedCountries.includes(p));\n\n if (!paises.length || paisesInvalidos.length) {\n paises = ['SIN PAIS DEFINIDO'];\n confianzaPais = 'baja';\n requiereRevision = true;\n motivosRevision.push('pais_invalido_o_no_reconocido');\n }\n} else {\n paises = ['SIN PAIS DEFINIDO'];\n fuentePais = 'no_detectado';\n confianzaPais = 'baja';\n requiereRevision = true;\n motivosRevision.push('pais_no_detectado_en_nombre');\n}\n\nif (paises.includes('SIN PAIS DEFINIDO')) {\n confianzaPais = 'baja';\n requiereRevision = true;\n motivosRevision.push('pais_sin_definir');\n}\n\nconst nombreLimpio = removePaisPrefix(fileName);\n\nreturn [\n {\n json: {\n file_id: fileId,\n nombre_archivo: fileName,\n nombre_limpio: nombreLimpio,\n link_drive: linkDrive,\n mime_type: mimeType,\n\n pais: paises.join(', '),\n fuente_pais: fuentePais,\n confianza_pais: confianzaPais,\n requiere_revision_inicial: requiereRevision,\n motivos_revision_inicial: motivosRevision.join(', ')\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 576, + 0 + ], + "id": "4e1f55ae-8285-46cb-9449-a5d4fb18dbbb", + "name": "Code - Preparar archivo" + }, + { + "parameters": { + "pollTimes": { + "item": [ + { + "mode": "everyMinute" + } + ] + }, + "triggerOn": "specificFolder", + "folderToWatch": { + "__rl": true, + "value": "1QTZx4RfKyyjG0cQWLK14orY6kHcYCxDE", + "mode": "id" + }, + "event": "fileCreated", + "options": {} + }, + "type": "n8n-nodes-base.googleDriveTrigger", + "typeVersion": 1, + "position": [ + 0, + 0 + ], + "id": "e5a89295-b285-41c7-9eb4-f04548102669", + "name": "Trigger - Nueva propuesta en bruto", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + 208, + 0 + ], + "id": "eea9a53f-bd42-4357-bc31-68af3b138f77", + "name": "Loop - Archivos" + }, + { + "parameters": { + "content": "## 📥 Entrada automática\n\nEste flujo se activa cuando se sube una nueva propuesta a la carpeta:\nBANCO DE PROPUESTAS EN BRUTO.\n\nEl archivo debe usar el estándar:\n[PAIS=PAIS] Nombre de la propuesta AÑO", + "height": 576, + "width": 528, + "color": "#652525" + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -80, + -160 + ], + "id": "814d2c1a-26b4-43ae-8b60-6cbe2e2f5ca0", + "name": "Sticky Note" + }, + { + "parameters": { + "content": "## 🤖 Extracción y normalización\n\nSe prepara la información del archivo, Gemini extrae la metadata y luego se normalizan los valores para que coincidan con las listas del Google Sheet.\n\nSi falta país, año, cliente, marca u otro dato importante, se marca para revisión.", + "height": 528, + "width": 752, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 512, + -176 + ], + "id": "03967ea8-dd79-4133-8d2c-d3e250bd2fcd", + "name": "Sticky Note1" + }, + { + "parameters": { + "content": "## ✅ Registro y cierre\n\nLa propuesta se agrega automáticamente al Google Sheet de Fulgencio.\n\nDespués de registrarse correctamente, el archivo se mueve a:\nPROPUESTAS PROCESADAS.", + "height": 384, + "width": 448, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 1296, + -176 + ], + "id": "10ddb96a-5aac-4101-a7b4-806db8ce6248", + "name": "Sticky Note2" + }, + { + "parameters": { + "pollTimes": { + "item": [ + { + "mode": "everyMinute" + } + ] + }, + "triggerOn": "specificFolder", + "folderToWatch": { + "__rl": true, + "value": "1QTZx4RfKyyjG0cQWLK14orY6kHcYCxDE", + "mode": "id" + }, + "event": "fileUpdated", + "options": {} + }, + "type": "n8n-nodes-base.googleDriveTrigger", + "typeVersion": 1, + "position": [ + -16, + 224 + ], + "id": "9f6900b1-3f24-448c-98c5-0f08ded7ed69", + "name": "Trigger - Nueva propuesta en bruto1", + "credentials": { + "googleDriveOAuth2Api": { + "id": "g23xdGLZRzBGqKgH", + "name": "Isaac - Google Drive" + } + } + } + ], + "pinData": {}, + "connections": { + "Gemini - Extraer metadata1": { + "ai_languageModel": [ + [ + { + "node": "Gemini - Extraer metadata", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Gemini - Extraer metadata": { + "main": [ + [ + { + "node": "Code - Normalizar respuesta", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalizar respuesta": { + "main": [ + [ + { + "node": "Sheets - Agregar propuesta", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sheets - Agregar propuesta": { + "main": [ + [ + { + "node": "Drive - Mover a procesadas", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar archivo": { + "main": [ + [ + { + "node": "Gemini - Extraer metadata", + "type": "main", + "index": 0 + } + ] + ] + }, + "Trigger - Nueva propuesta en bruto": { + "main": [ + [ + { + "node": "Loop - Archivos", + "type": "main", + "index": 0 + } + ] + ] + }, + "Loop - Archivos": { + "main": [ + [], + [ + { + "node": "Code - Preparar archivo", + "type": "main", + "index": 0 + } + ] + ] + }, + "Drive - Mover a procesadas": { + "main": [ + [ + { + "node": "Loop - Archivos", + "type": "main", + "index": 0 + } + ] + ] + }, + "Trigger - Nueva propuesta en bruto1": { + "main": [ + [ + { + "node": "Loop - Archivos", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "timeSavedMode": "fixed", + "errorWorkflow": "puF4LUczoSz3hcek", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": true, + "timezone": "America/Santo_Domingo" + }, + "versionId": "9247f92f-022d-434c-91c2-764dbbdb1a57", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" + }, + "id": "NDHPXi6jzVifyZU1", + "tags": [] +} \ No newline at end of file