Files
fulgencio-procesar-propuest…/fulgencio-procesar-propuestas-en-bruto-glm.json

1366 lines
80 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"updatedAt": "2026-06-25T11:37:22.088Z",
"createdAt": "2026-05-15T13:33:30.911Z",
"id": "NDHPXi6jzVifyZU1",
"name": "Fulgencio Procesar Propuestas en Bruto - GLM",
"description": null,
"active": true,
"isArchived": false,
"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": [
688,
0
],
"id": "372000c5-2b6b-41bc-ae01-bb55d6608f16",
"name": "Gemini - Extraer metadata"
},
{
"parameters": {
"modelName": "models/gemini-3.1-pro-preview",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"typeVersion": 1,
"position": [
704,
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": [
1040,
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": [
1248,
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": [
1456,
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": [
480,
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": 352,
"width": 448,
"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": [
416,
-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": [
1200,
-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"
}
}
}
],
"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
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"timeSavedMode": "fixed",
"errorWorkflow": "puF4LUczoSz3hcek",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false
},
"staticData": {
"node:Trigger - Nueva propuesta en bruto": {
"lastTimeChecked": "2026-06-25T11:37:23Z"
},
"node:Trigger - Nueva propuesta en bruto1": {
"lastTimeChecked": "2026-06-25T11:37:23Z"
}
},
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "6133feb9-8ecc-4229-ad28-ace83fe5b9a6",
"activeVersionId": "6133feb9-8ecc-4229-ad28-ace83fe5b9a6",
"versionCounter": 151,
"triggerCount": 2,
"shared": [
{
"updatedAt": "2026-05-15T13:33:30.919Z",
"createdAt": "2026-05-15T13:33:30.919Z",
"role": "workflow:owner",
"workflowId": "NDHPXi6jzVifyZU1",
"projectId": "PJpTANzTXIFibWsW",
"project": {
"updatedAt": "2026-04-22T14:25:09.686Z",
"createdAt": "2026-04-22T14:22:54.790Z",
"id": "PJpTANzTXIFibWsW",
"name": "Isaac Aracena <iaracena@gomezleemarketing.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
}
],
"tags": [],
"activeVersion": {
"updatedAt": "2026-06-25T11:37:23.000Z",
"createdAt": "2026-06-25T11:37:22.091Z",
"versionId": "6133feb9-8ecc-4229-ad28-ace83fe5b9a6",
"workflowId": "NDHPXi6jzVifyZU1",
"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": [
688,
0
],
"id": "372000c5-2b6b-41bc-ae01-bb55d6608f16",
"name": "Gemini - Extraer metadata"
},
{
"parameters": {
"modelName": "models/gemini-3.1-pro-preview",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"typeVersion": 1,
"position": [
704,
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": [
1040,
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": [
1248,
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": [
1456,
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": [
480,
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": 352,
"width": 448,
"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": [
416,
-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": [
1200,
-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"
}
}
}
],
"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
}
]
]
}
},
"authors": "Isaac Aracena",
"name": "Version 6133feb9",
"description": "",
"autosaved": false,
"workflowPublishHistory": [
{
"createdAt": "2026-06-25T11:37:23.671Z",
"id": 1873,
"workflowId": "NDHPXi6jzVifyZU1",
"versionId": "6133feb9-8ecc-4229-ad28-ace83fe5b9a6",
"event": "activated",
"userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
]
}
}