Files
cruces-seguridad-social/Cruce de Seguridad Social - Guatemala.json
T

1834 lines
163 KiB
JSON

{
"name": "Cruce de Seguridad Social - Guatemala",
"nodes": [
{
"parameters": {
"formTitle": "Cruce Seguridad Social Guatemala",
"formDescription": "Sube las dos nóminas de Guatemala del mes, el archivo Sistema Propio IGSS y la Planilla Consolidada mensual. El flujo generará un Google Sheets con hallazgos de afiliación y montos.",
"formFields": {
"values": [
{
"fieldLabel": "Mes a validar",
"fieldType": "dropdown",
"fieldOptions": {
"values": [
{
"option": "01"
},
{
"option": "02"
},
{
"option": "03"
},
{
"option": "04"
},
{
"option": "05"
},
{
"option": "06"
},
{
"option": "07"
},
{
"option": "08"
},
{
"option": "09"
},
{
"option": "10"
},
{
"option": "11"
},
{
"option": "12"
}
]
},
"requiredField": true
},
{
"fieldLabel": "Año a validar",
"fieldType": "number",
"requiredField": true
},
{
"fieldLabel": "Nómina Q1",
"fieldType": "file",
"multipleFiles": false,
"acceptFileTypes": ".xlsx, .xls, .xlsm, .csv",
"requiredField": true
},
{
"fieldLabel": "Nómina Q2",
"fieldType": "file",
"multipleFiles": false,
"acceptFileTypes": ".xlsx, .xls, .xlsm, .csv",
"requiredField": true
},
{
"fieldLabel": "Sistema Propio IGSS",
"fieldType": "file",
"multipleFiles": false,
"acceptFileTypes": ".xlsx, .xls, .xlsm, .csv",
"requiredField": true
},
{
"fieldLabel": "Planilla Consolidada",
"fieldType": "file",
"multipleFiles": false,
"acceptFileTypes": ".xlsx, .xls, .xlsm, .csv",
"requiredField": true
}
]
},
"options": {
"ignoreBots": true
}
},
"type": "n8n-nodes-base.formTrigger",
"typeVersion": 2.3,
"position": [
8128,
2368
],
"id": "92ada1f0-69df-4a58-ab94-e9ebacd28ec3",
"name": "On form submission",
"webhookId": "109715d7-2a28-4e4c-b0c9-2e039a1f0a3a"
},
{
"parameters": {
"jsCode": "const item = $input.all()[0];\nconst now = new Date();\nconst pad = n => String(n).padStart(2, '0');\nconst ts = `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;\nconst runId = `GT_SEG_${ts}_${Math.random().toString(36).slice(2, 8)}`;\n\nconst mes = String(item.json['Mes a validar'] || '').padStart(2, '0');\nconst anio = String(item.json['Año a validar'] || item.json['Ano a validar'] || now.getFullYear()).trim();\nconst meses = {\n '01':'Enero','02':'Febrero','03':'Marzo','04':'Abril',\n '05':'Mayo','06':'Junio','07':'Julio','08':'Agosto',\n '09':'Septiembre','10':'Octubre','11':'Noviembre','12':'Diciembre'\n};\nconst fechaTitulo = `${pad(now.getDate())}-${pad(now.getMonth()+1)}-${now.getFullYear()}`;\nconst periodoLabel = `${meses[mes] || mes} ${anio}`;\nconst reportTitle = `Cruce Seguridad Social Guatemala - ${periodoLabel} - ${fechaTitulo}`;\n\nconst data = $getWorkflowStaticData('global');\ndata.gtSeguridadSocial = data.gtSeguridadSocial || {};\ndata.gtSeguridadSocial[runId] = {\n startedAt: now.toISOString(),\n mes,\n anio,\n periodoLabel,\n files: { q1:'', q2:'', sistema:'', consolidado:'' },\n nomina: [],\n ajustes: [],\n empleados: [],\n planillaMensual: [],\n bamboo: []\n};\n\nreturn [{\n json: { ...item.json, runId, mes, anio, periodoLabel, reportTitle, emailTo: 'msoto@gomezleemarketing.com, iaracena@gomezleemarketing.com, iherrera@gomezleemarketing.com, vparamo@gomezleemarketing.com, asrodriguez@gomezleemarketing.com, mgomez@gomezleemarketing.com, ymadera@gomezleemarketing.com' },\n binary: item.binary\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
8368,
2368
],
"id": "25a92da5-24bf-4b26-b084-afc571cf5f4a",
"name": "Inicializar memoria"
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0];\nconst bin = init.binary || {};\nconst type = 'q1';\nfunction norm(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase(); }\nfunction looksExcel(fileName, mime) { const lower = norm(fileName); const m = norm(mime); return lower.endsWith('.xlsx') || lower.endsWith('.xls') || lower.endsWith('.xlsm') || lower.endsWith('.csv') || m.includes('spreadsheet') || m.includes('excel') || m.includes('csv'); }\nfunction scoreCandidate(key, fileName) {\n const text = norm(`${key} ${fileName}`);\n let score = 0;\n if (type === 'q1') { if (text.includes('nomina')) score += 20; if (/\\b1q\\b|q1|primera|mayo 15|15 2026/.test(text)) score += 120; if (/\\b2q\\b|q2|segunda|mayo 30|30 2026/.test(text)) score -= 150; if (/sistema|consolidada|consolidado/.test(text)) score -= 200; }\n if (type === 'q2') { if (text.includes('nomina')) score += 20; if (/\\b2q\\b|q2|segunda|mayo 30|30 2026/.test(text)) score += 120; if (/\\b1q\\b|q1|primera|mayo 15|15 2026/.test(text)) score -= 150; if (/sistema|consolidada|consolidado/.test(text)) score -= 200; }\n if (type === 'sistema') { if (/sistema|propio|2\\.2|igss/.test(text)) score += 120; if (/consolidada|consolidado|planilla consolidada|nomina/.test(text)) score -= 80; }\n if (type === 'consolidado') { if (/consolidada|consolidado|planilla consolidada/.test(text)) score += 120; if (/sistema|propio|2\\.2|nomina/.test(text)) score -= 80; }\n return score;\n}\nconst candidates = [];\nfor (const [key, file] of Object.entries(bin)) {\n const fileName = String(file.fileName || key);\n const mime = String(file.mimeType || '');\n if (!looksExcel(fileName, mime)) continue;\n candidates.push({ key, file, fileName, score: scoreCandidate(key, fileName) });\n}\ncandidates.sort((a,b) => b.score - a.score);\nconst chosen = candidates[0];\nif (!chosen || chosen.score <= 0) throw new Error(`No encontré el archivo requerido para ${type}. Revisa los campos del formulario y los nombres de archivo.`);\nconst data = $getWorkflowStaticData('global');\nconst slot = data.gtSeguridadSocial?.[init.json.runId];\nif (slot) slot.files[type] = chosen.fileName;\nreturn [{ json: { runId: init.json.runId, fileType: type, fileName: chosen.fileName }, binary: { data: chosen.file } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
8608,
2368
],
"id": "93f0bc45-0145-4a93-96c5-e0a524afb2b9",
"name": "Preparar Nómina 1Q"
},
{
"parameters": {
"operation": "update",
"fileId": {
"__rl": true,
"value": "1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4",
"mode": "list",
"cachedResultName": "Nomina Guatemala",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4/edit"
},
"changeFileContent": true,
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
8976,
2368
],
"id": "be6acc75-0da2-40cd-b194-5d2ecfa4c9fd",
"name": "Actualizar staging - Nómina 1Q",
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
}
},
{
"parameters": {
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.id || '1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4' }}?fields=spreadsheetId,sheets(properties(title,sheetId,hidden))",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
9584,
2368
],
"id": "b56ae934-213a-43bd-b0b8-73189146f745",
"name": "Listar hojas - Nómina 1Q",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "AM9qcemWjo0CZB83",
"name": "Google Sheets - Isaac"
}
}
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst spreadsheetId = $json.spreadsheetId || $json.id || '1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4';\n\nfunction norm(s) {\n return String(s || '')\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '');\n}\nfunction shouldReadPayrollSheet(title) {\n const n = norm(title);\n\n // Primero se excluyen copias y hojas auxiliares. Abril 2026 trae\n // \"Copia de 1) Nomina General 2\"; si se permite por includes('nominageneral'),\n // duplica Q1 y genera diferencias falsas.\n const excluded = [\n 'calculos', 'resumen', 'temporales', 'auditorias', 'auditoria',\n 'bono', 'movilidad', 'viaticos', 'viatico', 'combustible',\n 'combustibles', 'tabla', 'dinamica', 'copia', 'copy', 'boletas',\n 'nvscripts', 'autocrat', 'donotdelete', 'bajas'\n ];\n if (excluded.some(x => n.includes(x))) return false;\n\n // Solo la hoja principal real. No usar includes para evitar hojas copiadas.\n if (n === '1nominageneral' || n === 'nominageneral') return true;\n\n // Cualquier otra hoja no excluida se leerá como posible ajuste/complemento,\n // pero luego el nodo de parseo valida que tenga encabezado real de nómina.\n return true;\n}\n\nconst sheets = ($json.sheets || []).map(s => s.properties || {});\nconst selected = sheets\n .filter(p => p.title && shouldReadPayrollSheet(p.title))\n .map(p => p.title);\n\nif (!selected.some(t => norm(t).includes('nominageneral'))) {\n throw new Error(`No encontré la hoja 1) Nomina General. Hojas disponibles: ${sheets.map(s => s.title).join(', ')}`);\n}\nif (!selected.length) {\n throw new Error(`No encontré hojas válidas para leer en la nómina. Hojas disponibles: ${sheets.map(s => s.title).join(', ')}`);\n}\n\nreturn [{\n json: {\n runId: init.runId,\n spreadsheetId,\n sheetTitles: selected,\n ranges: selected.map(t => `'${String(t).replace(/'/g, \"''\")}'!A:AZ`)\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
9824,
2368
],
"id": "7640a1ed-965b-4a52-b651-ce8faa210a43",
"name": "Resolver hoja Nómina 1Q"
},
{
"parameters": {
"url": "={{ 'https://sheets.googleapis.com/v4/spreadsheets/' + $json.spreadsheetId + '/values:batchGet?' + $json.ranges.map(r => 'ranges=' + encodeURIComponent(r)).join('&') + '&valueRenderOption=UNFORMATTED_VALUE&dateTimeRenderOption=SERIAL_NUMBER' }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
10080,
2368
],
"id": "b36c03d2-4dc8-44e6-9131-ffd9d824d8cb",
"name": "Leer Nómina 1Q",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const qLabel = '1Q';\nconst init = $items('Inicializar memoria')[0].json;\nconst valueRanges = $json.valueRanges || [];\n\nfunction clean(s) { return String(s ?? '').replace(/\\s+/g, ' ').trim(); }\nfunction removeAccents(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, ''); }\nfunction normSheet(s) { return removeAccents(s).toLowerCase().replace(/[^a-z0-9]/g, ''); }\nfunction normalizeName(s) {\n return clean(s)\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/[^A-Z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\nfunction toNumber(v) {\n if (v === null || v === undefined || v === '') return 0;\n if (typeof v === 'number') return Number.isFinite(v) ? v : 0;\n let s = String(v).trim().replace(/Q|\\$|USD|GTQ|,/gi, '').replace(/\\s/g, '');\n if (/^\\((.*)\\)$/.test(s)) s = '-' + s.replace(/^\\(|\\)$/g, '');\n const n = Number(s);\n return Number.isFinite(n) ? n : 0;\n}\nfunction normalizeAf(v) {\n const raw = clean(v);\n if (!raw || /pendiente|n\\/a|na|sin/i.test(raw)) {\n return { value:'', status: raw ? 'Pendiente/Vacío' : 'Vacío', raw };\n }\n let digits = '';\n if (typeof v === 'number') {\n digits = Math.trunc(v).toString();\n } else {\n const s = raw.replace(/,/g, '').trim();\n if (/e\\+?/i.test(s)) {\n const n = Number(s);\n if (Number.isFinite(n)) digits = Math.trunc(n).toString();\n } else {\n digits = s.replace(/\\D/g, '');\n }\n }\n if (!digits || /^0+$/.test(digits)) return { value:'', status:'Inválido', raw };\n if (digits.length < 8 || digits.length > 13) return { value:'', status:'Longitud inválida', raw };\n return { value:digits, status: /e\\+?/i.test(String(raw)) ? 'Válido científico' : 'Válido', raw };\n}\nfunction sheetNameFromRange(range) {\n let left = String(range || '').split('!')[0] || '';\n left = left.replace(/^'/, '').replace(/'$/, '').replace(/''/g, \"'\");\n return left;\n}\nfunction isMainSheet(title) {\n const n = normSheet(title);\n // Solo la hoja principal real; NO contar copias como \"Copia de 1) Nomina General 2\".\n return n === '1nominageneral' || n === 'nominageneral';\n}\nfunction looksLikePayrollSheet(values) {\n const header = (values[0] || []).map(v => removeAccents(clean(v)).toLowerCase().replace(/[^a-z0-9]/g, ''));\n return header[0] === 'proyecto'\n && header[5] === 'igss'\n && (header[7] === 'nombrecompleto' || header[7].includes('nombre'))\n && header[14].includes('salario')\n && header[19].includes('factor');\n}\nfunction parsePayrollRows(values, sheetTitle, sourceType) {\n const records = [];\n if (!looksLikePayrollSheet(values)) return records;\n let seen = false;\n for (let i = 1; i < values.length; i++) {\n const row = values[i] || [];\n const proyecto = clean(row[0]);\n const moneda = clean(row[1]);\n const igss = normalizeAf(row[5]);\n const nombre = clean(row[7]);\n\n if (seen && !proyecto && !nombre && !clean(row[5])) break;\n if (!proyecto || !nombre || /^total/i.test(proyecto) || /^total/i.test(nombre)) continue;\n seen = true;\n\n const factor = clean(row[19]);\n const factorNorm = removeAccents(factor).toLowerCase();\n const factorSi = /^(si|sí|yes|y|true|1)$/i.test(factorNorm);\n\n const salario = toNumber(row[14]);\n const vacaciones = toNumber(row[16]);\n const horasExtras = toNumber(row[18]);\n const variable = toNumber(row[21]);\n const montoAfectoOriginal = salario + vacaciones + horasExtras + (factorSi ? variable : 0);\n if (Math.abs(montoAfectoOriginal) <= 0.004 && sourceType === 'ajuste_candidato') continue;\n\n records.push({\n q: qLabel,\n sheetTitle,\n rowNumber: i + 1,\n sourceType,\n proyecto_nomina: proyecto,\n moneda,\n nombre_nomina: nombre,\n nombre_norm: normalizeName(nombre),\n igss_original: igss.raw,\n igss_norm: igss.value,\n igss_estado: igss.status,\n factor_prestacional: factor,\n salario_periodo: Number(salario.toFixed(2)),\n vacaciones: Number(vacaciones.toFixed(2)),\n horas_extras: Number(horasExtras.toFixed(2)),\n variable: Number(variable.toFixed(2)),\n variable_afecta: factorSi ? 'Sí' : 'No',\n monto_afecto_original: Number(montoAfectoOriginal.toFixed(2)),\n monto_afecto_gtq: Number(montoAfectoOriginal.toFixed(2)),\n regla_aplicada: factorSi ? 'Sueldo + vacaciones + horas extras + variable' : 'Sueldo + vacaciones + horas extras'\n });\n }\n return records;\n}\nfunction samePerson(a, b) {\n if (a.igss_norm && b.igss_norm && a.igss_norm === b.igss_norm) return true;\n return normalizeName(a.nombre_nomina) === normalizeName(b.nombre_nomina);\n}\nfunction amountEquals(a, b) {\n return Math.abs(Number(a || 0) - Number(b || 0)) <= 0.05;\n}\nfunction deriveAdjustments(baseRecords, candidateRecords) {\n const ajustes = [];\n\n function pushAdjustment(c, amount, tipo, referencia, observacion) {\n if (Math.abs(amount) <= 0.05) return;\n ajustes.push({\n ...c,\n sourceType: 'ajuste',\n monto_ajuste_original: Number(amount.toFixed(2)),\n monto_ajuste_gtq: Number(amount.toFixed(2)),\n tipo_ajuste: tipo,\n referencia: referencia || '',\n observacion_ajuste: observacion || (tipo === 'Corrección de monto'\n ? 'Se incluyó solo la diferencia entre la fila corregida y la fila principal.'\n : 'Se incluyó como complemento de nómina.')\n });\n }\n\n const used = new Set();\n const groups = new Map();\n candidateRecords.forEach((c, idx) => {\n c.__idx = idx;\n const key = `${c.sheetTitle || ''}||${c.igss_norm || c.nombre_norm || normalizeName(c.nombre_nomina)}`;\n if (!groups.has(key)) groups.set(key, []);\n groups.get(key).push(c);\n });\n\n // Caso abril 2026: la hoja Complemento trae valor anterior y valor corregido\n // de la variable de Oliver. No se suman ambas filas completas; se suma solo\n // la diferencia entre variable corregida y variable ya incluida en la nómina.\n for (const group of groups.values()) {\n if (!group.length) continue;\n const c0 = group[0];\n const isComplementSheet = /complement|complemento/i.test(removeAccents(c0.sheetTitle || '').toLowerCase());\n if (!isComplementSheet) continue;\n\n const possibleBase = baseRecords.filter(b => samePerson(b, c0));\n const baseAny = possibleBase[0];\n if (!baseAny) continue;\n\n const baseVariable = Number(baseAny.variable || 0);\n if (Math.abs(baseVariable) <= 0.05) continue;\n\n const variableOnly = group.filter(c =>\n Math.abs(Number(c.salario_periodo || 0)) <= 0.05 &&\n Math.abs(Number(c.vacaciones || 0)) <= 0.05 &&\n Math.abs(Number(c.horas_extras || 0)) <= 0.05 &&\n Math.abs(Number(c.variable || 0)) > 0.05 &&\n amountEquals(c.monto_afecto_original, c.variable)\n );\n\n const oldValue = variableOnly.find(c => amountEquals(c.monto_afecto_original, baseVariable));\n const corrected = variableOnly\n .filter(c => c.monto_afecto_original > baseVariable + 0.05)\n .sort((a,b) => Number(b.monto_afecto_original || 0) - Number(a.monto_afecto_original || 0))[0];\n\n if (oldValue && corrected) {\n for (const c of variableOnly) used.add(c.__idx);\n const diff = Number((corrected.monto_afecto_original - baseVariable).toFixed(2));\n pushAdjustment(\n corrected,\n diff,\n 'Corrección de variable',\n `Variable principal ${baseVariable}`,\n 'Se incluyó solo la diferencia entre la variable corregida y la variable ya incluida en la nómina principal.'\n );\n }\n }\n\n for (const c of candidateRecords) {\n if (used.has(c.__idx)) continue;\n const possibleBase = baseRecords.filter(b => samePerson(b, c));\n const baseMatch = possibleBase.find(b => amountEquals(b.monto_afecto_original, c.monto_afecto_original));\n const baseAny = possibleBase[0];\n\n if (baseMatch) {\n // Esta fila es una repetición de la fila principal, no se suma como ajuste.\n continue;\n }\n\n let adjustmentAmount = c.monto_afecto_original;\n let tipo = 'Complemento';\n let baseReference = '';\n\n const isComplementSheet = /complement|complemento/i.test(removeAccents(c.sheetTitle || '').toLowerCase());\n if (!isComplementSheet && baseAny && c.monto_afecto_original > baseAny.monto_afecto_original && possibleBase.some(b => amountEquals(b.monto_afecto_original, baseAny.monto_afecto_original))) {\n // Ejemplo: hoja Oliver trae una fila corregida completa. Se suma solo la diferencia contra la fila principal.\n adjustmentAmount = Number((c.monto_afecto_original - baseAny.monto_afecto_original).toFixed(2));\n tipo = 'Corrección de monto';\n baseReference = `Monto principal ${baseAny.monto_afecto_original}`;\n }\n\n pushAdjustment(c, Number(adjustmentAmount || 0), tipo, baseReference, tipo === 'Corrección de monto'\n ? 'Se incluyó solo la diferencia entre la fila corregida y la fila principal.'\n : 'Se incluyó como complemento de nómina.');\n }\n\n return ajustes;\n}\n\nconst mainRecords = [];\nconst candidates = [];\n\nfor (const vr of valueRanges) {\n const sheetTitle = sheetNameFromRange(vr.range);\n const values = vr.values || [];\n if (!values.length) continue;\n\n if (isMainSheet(sheetTitle)) {\n mainRecords.push(...parsePayrollRows(values, sheetTitle, 'principal'));\n } else {\n candidates.push(...parsePayrollRows(values, sheetTitle, 'ajuste_candidato'));\n }\n}\n\nif (!mainRecords.length) {\n throw new Error(`No pude leer registros válidos desde 1) Nomina General para ${qLabel}.`);\n}\n\nconst ajustes = deriveAdjustments(mainRecords, candidates);\n\nconst data = $getWorkflowStaticData('global');\ndata.gtSeguridadSocial = data.gtSeguridadSocial || {};\ndata.gtSeguridadSocial[init.runId] = data.gtSeguridadSocial[init.runId] || {};\ndata.gtSeguridadSocial[init.runId].nomina.push(...mainRecords);\ndata.gtSeguridadSocial[init.runId].ajustes = data.gtSeguridadSocial[init.runId].ajustes || [];\ndata.gtSeguridadSocial[init.runId].ajustes.push(...ajustes);\n\nreturn [{\n json: {\n runId: init.runId,\n quincena: qLabel,\n payrollRows: mainRecords.length,\n invalidIgssRows: mainRecords.filter(r => !r.igss_norm).length,\n adjustmentRows: ajustes.length,\n adjustmentSheets: [...new Set(ajustes.map(a => a.sheetTitle))]\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
10320,
2368
],
"id": "89e8a0c5-03cb-4f40-84f6-6a53b14a339f",
"name": "Guardar Nómina 1Q en memoria"
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0];\nconst bin = init.binary || {};\nconst type = 'q2';\nfunction norm(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase(); }\nfunction looksExcel(fileName, mime) { const lower = norm(fileName); const m = norm(mime); return lower.endsWith('.xlsx') || lower.endsWith('.xls') || lower.endsWith('.xlsm') || lower.endsWith('.csv') || m.includes('spreadsheet') || m.includes('excel') || m.includes('csv'); }\nfunction scoreCandidate(key, fileName) {\n const text = norm(`${key} ${fileName}`);\n let score = 0;\n if (type === 'q1') { if (text.includes('nomina')) score += 20; if (/\\b1q\\b|q1|primera|mayo 15|15 2026/.test(text)) score += 120; if (/\\b2q\\b|q2|segunda|mayo 30|30 2026/.test(text)) score -= 150; if (/sistema|consolidada|consolidado/.test(text)) score -= 200; }\n if (type === 'q2') { if (text.includes('nomina')) score += 20; if (/\\b2q\\b|q2|segunda|mayo 30|30 2026/.test(text)) score += 120; if (/\\b1q\\b|q1|primera|mayo 15|15 2026/.test(text)) score -= 150; if (/sistema|consolidada|consolidado/.test(text)) score -= 200; }\n if (type === 'sistema') { if (/sistema|propio|2\\.2|igss/.test(text)) score += 120; if (/consolidada|consolidado|planilla consolidada|nomina/.test(text)) score -= 80; }\n if (type === 'consolidado') { if (/consolidada|consolidado|planilla consolidada/.test(text)) score += 120; if (/sistema|propio|2\\.2|nomina/.test(text)) score -= 80; }\n return score;\n}\nconst candidates = [];\nfor (const [key, file] of Object.entries(bin)) {\n const fileName = String(file.fileName || key);\n const mime = String(file.mimeType || '');\n if (!looksExcel(fileName, mime)) continue;\n candidates.push({ key, file, fileName, score: scoreCandidate(key, fileName) });\n}\ncandidates.sort((a,b) => b.score - a.score);\nconst chosen = candidates[0];\nif (!chosen || chosen.score <= 0) throw new Error(`No encontré el archivo requerido para ${type}. Revisa los campos del formulario y los nombres de archivo.`);\nconst data = $getWorkflowStaticData('global');\nconst slot = data.gtSeguridadSocial?.[init.json.runId];\nif (slot) slot.files[type] = chosen.fileName;\nreturn [{ json: { runId: init.json.runId, fileType: type, fileName: chosen.fileName }, binary: { data: chosen.file } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
10560,
2368
],
"id": "a8a53d13-4855-49d0-9d96-42bc29af33a1",
"name": "Preparar Nómina 2Q"
},
{
"parameters": {
"operation": "update",
"fileId": {
"__rl": true,
"value": "1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4",
"mode": "list",
"cachedResultName": "Nomina Guatemala",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4/edit"
},
"changeFileContent": true,
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
10816,
2368
],
"id": "47e13a5e-1b18-4c3c-bbdc-d534b2321a85",
"name": "Actualizar staging - Nómina 2Q",
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
}
},
{
"parameters": {
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.id || '1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4' }}?fields=spreadsheetId,sheets(properties(title,sheetId,hidden))",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
11344,
2368
],
"id": "be1c4951-cd20-4e4b-8398-91067347e86d",
"name": "Listar hojas - Nómina 2Q",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst spreadsheetId = $json.spreadsheetId || $json.id || '1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4';\n\nfunction norm(s) {\n return String(s || '')\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '');\n}\nfunction shouldReadPayrollSheet(title) {\n const n = norm(title);\n\n // Primero se excluyen copias y hojas auxiliares. Abril 2026 trae\n // \"Copia de 1) Nomina General 2\"; si se permite por includes('nominageneral'),\n // duplica Q1 y genera diferencias falsas.\n const excluded = [\n 'calculos', 'resumen', 'temporales', 'auditorias', 'auditoria',\n 'bono', 'movilidad', 'viaticos', 'viatico', 'combustible',\n 'combustibles', 'tabla', 'dinamica', 'copia', 'copy', 'boletas',\n 'nvscripts', 'autocrat', 'donotdelete', 'bajas'\n ];\n if (excluded.some(x => n.includes(x))) return false;\n\n // Solo la hoja principal real. No usar includes para evitar hojas copiadas.\n if (n === '1nominageneral' || n === 'nominageneral') return true;\n\n // Cualquier otra hoja no excluida se leerá como posible ajuste/complemento,\n // pero luego el nodo de parseo valida que tenga encabezado real de nómina.\n return true;\n}\n\nconst sheets = ($json.sheets || []).map(s => s.properties || {});\nconst selected = sheets\n .filter(p => p.title && shouldReadPayrollSheet(p.title))\n .map(p => p.title);\n\nif (!selected.some(t => norm(t).includes('nominageneral'))) {\n throw new Error(`No encontré la hoja 1) Nomina General. Hojas disponibles: ${sheets.map(s => s.title).join(', ')}`);\n}\nif (!selected.length) {\n throw new Error(`No encontré hojas válidas para leer en la nómina. Hojas disponibles: ${sheets.map(s => s.title).join(', ')}`);\n}\n\nreturn [{\n json: {\n runId: init.runId,\n spreadsheetId,\n sheetTitles: selected,\n ranges: selected.map(t => `'${String(t).replace(/'/g, \"''\")}'!A:AZ`)\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
11664,
2368
],
"id": "bc2e3bfd-ab5f-4e9f-9348-6c0e0d79cfa1",
"name": "Resolver hoja Nómina 2Q"
},
{
"parameters": {
"url": "={{ 'https://sheets.googleapis.com/v4/spreadsheets/' + $json.spreadsheetId + '/values:batchGet?' + $json.ranges.map(r => 'ranges=' + encodeURIComponent(r)).join('&') + '&valueRenderOption=UNFORMATTED_VALUE&dateTimeRenderOption=SERIAL_NUMBER' }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
11984,
2368
],
"id": "47505774-a5a7-443a-bcc8-65a2b94cc773",
"name": "Leer Nómina 2Q",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const qLabel = '2Q';\nconst init = $items('Inicializar memoria')[0].json;\nconst valueRanges = $json.valueRanges || [];\n\nfunction clean(s) { return String(s ?? '').replace(/\\s+/g, ' ').trim(); }\nfunction removeAccents(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, ''); }\nfunction normSheet(s) { return removeAccents(s).toLowerCase().replace(/[^a-z0-9]/g, ''); }\nfunction normalizeName(s) {\n return clean(s)\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/[^A-Z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\nfunction toNumber(v) {\n if (v === null || v === undefined || v === '') return 0;\n if (typeof v === 'number') return Number.isFinite(v) ? v : 0;\n let s = String(v).trim().replace(/Q|\\$|USD|GTQ|,/gi, '').replace(/\\s/g, '');\n if (/^\\((.*)\\)$/.test(s)) s = '-' + s.replace(/^\\(|\\)$/g, '');\n const n = Number(s);\n return Number.isFinite(n) ? n : 0;\n}\nfunction normalizeAf(v) {\n const raw = clean(v);\n if (!raw || /pendiente|n\\/a|na|sin/i.test(raw)) {\n return { value:'', status: raw ? 'Pendiente/Vacío' : 'Vacío', raw };\n }\n let digits = '';\n if (typeof v === 'number') {\n digits = Math.trunc(v).toString();\n } else {\n const s = raw.replace(/,/g, '').trim();\n if (/e\\+?/i.test(s)) {\n const n = Number(s);\n if (Number.isFinite(n)) digits = Math.trunc(n).toString();\n } else {\n digits = s.replace(/\\D/g, '');\n }\n }\n if (!digits || /^0+$/.test(digits)) return { value:'', status:'Inválido', raw };\n if (digits.length < 8 || digits.length > 13) return { value:'', status:'Longitud inválida', raw };\n return { value:digits, status: /e\\+?/i.test(String(raw)) ? 'Válido científico' : 'Válido', raw };\n}\nfunction sheetNameFromRange(range) {\n let left = String(range || '').split('!')[0] || '';\n left = left.replace(/^'/, '').replace(/'$/, '').replace(/''/g, \"'\");\n return left;\n}\nfunction isMainSheet(title) {\n const n = normSheet(title);\n // Solo la hoja principal real; NO contar copias como \"Copia de 1) Nomina General 2\".\n return n === '1nominageneral' || n === 'nominageneral';\n}\nfunction looksLikePayrollSheet(values) {\n const header = (values[0] || []).map(v => removeAccents(clean(v)).toLowerCase().replace(/[^a-z0-9]/g, ''));\n return header[0] === 'proyecto'\n && header[5] === 'igss'\n && (header[7] === 'nombrecompleto' || header[7].includes('nombre'))\n && header[14].includes('salario')\n && header[19].includes('factor');\n}\nfunction parsePayrollRows(values, sheetTitle, sourceType) {\n const records = [];\n if (!looksLikePayrollSheet(values)) return records;\n let seen = false;\n for (let i = 1; i < values.length; i++) {\n const row = values[i] || [];\n const proyecto = clean(row[0]);\n const moneda = clean(row[1]);\n const igss = normalizeAf(row[5]);\n const nombre = clean(row[7]);\n\n if (seen && !proyecto && !nombre && !clean(row[5])) break;\n if (!proyecto || !nombre || /^total/i.test(proyecto) || /^total/i.test(nombre)) continue;\n seen = true;\n\n const factor = clean(row[19]);\n const factorNorm = removeAccents(factor).toLowerCase();\n const factorSi = /^(si|sí|yes|y|true|1)$/i.test(factorNorm);\n\n const salario = toNumber(row[14]);\n const vacaciones = toNumber(row[16]);\n const horasExtras = toNumber(row[18]);\n const variable = toNumber(row[21]);\n const montoAfectoOriginal = salario + vacaciones + horasExtras + (factorSi ? variable : 0);\n if (Math.abs(montoAfectoOriginal) <= 0.004 && sourceType === 'ajuste_candidato') continue;\n\n records.push({\n q: qLabel,\n sheetTitle,\n rowNumber: i + 1,\n sourceType,\n proyecto_nomina: proyecto,\n moneda,\n nombre_nomina: nombre,\n nombre_norm: normalizeName(nombre),\n igss_original: igss.raw,\n igss_norm: igss.value,\n igss_estado: igss.status,\n factor_prestacional: factor,\n salario_periodo: Number(salario.toFixed(2)),\n vacaciones: Number(vacaciones.toFixed(2)),\n horas_extras: Number(horasExtras.toFixed(2)),\n variable: Number(variable.toFixed(2)),\n variable_afecta: factorSi ? 'Sí' : 'No',\n monto_afecto_original: Number(montoAfectoOriginal.toFixed(2)),\n monto_afecto_gtq: Number(montoAfectoOriginal.toFixed(2)),\n regla_aplicada: factorSi ? 'Sueldo + vacaciones + horas extras + variable' : 'Sueldo + vacaciones + horas extras'\n });\n }\n return records;\n}\nfunction samePerson(a, b) {\n if (a.igss_norm && b.igss_norm && a.igss_norm === b.igss_norm) return true;\n return normalizeName(a.nombre_nomina) === normalizeName(b.nombre_nomina);\n}\nfunction amountEquals(a, b) {\n return Math.abs(Number(a || 0) - Number(b || 0)) <= 0.05;\n}\nfunction deriveAdjustments(baseRecords, candidateRecords) {\n const ajustes = [];\n\n function pushAdjustment(c, amount, tipo, referencia, observacion) {\n if (Math.abs(amount) <= 0.05) return;\n ajustes.push({\n ...c,\n sourceType: 'ajuste',\n monto_ajuste_original: Number(amount.toFixed(2)),\n monto_ajuste_gtq: Number(amount.toFixed(2)),\n tipo_ajuste: tipo,\n referencia: referencia || '',\n observacion_ajuste: observacion || (tipo === 'Corrección de monto'\n ? 'Se incluyó solo la diferencia entre la fila corregida y la fila principal.'\n : 'Se incluyó como complemento de nómina.')\n });\n }\n\n const used = new Set();\n const groups = new Map();\n candidateRecords.forEach((c, idx) => {\n c.__idx = idx;\n const key = `${c.sheetTitle || ''}||${c.igss_norm || c.nombre_norm || normalizeName(c.nombre_nomina)}`;\n if (!groups.has(key)) groups.set(key, []);\n groups.get(key).push(c);\n });\n\n // Caso abril 2026: la hoja Complemento trae valor anterior y valor corregido\n // de la variable de Oliver. No se suman ambas filas completas; se suma solo\n // la diferencia entre variable corregida y variable ya incluida en la nómina.\n for (const group of groups.values()) {\n if (!group.length) continue;\n const c0 = group[0];\n const isComplementSheet = /complement|complemento/i.test(removeAccents(c0.sheetTitle || '').toLowerCase());\n if (!isComplementSheet) continue;\n\n const possibleBase = baseRecords.filter(b => samePerson(b, c0));\n const baseAny = possibleBase[0];\n if (!baseAny) continue;\n\n const baseVariable = Number(baseAny.variable || 0);\n if (Math.abs(baseVariable) <= 0.05) continue;\n\n const variableOnly = group.filter(c =>\n Math.abs(Number(c.salario_periodo || 0)) <= 0.05 &&\n Math.abs(Number(c.vacaciones || 0)) <= 0.05 &&\n Math.abs(Number(c.horas_extras || 0)) <= 0.05 &&\n Math.abs(Number(c.variable || 0)) > 0.05 &&\n amountEquals(c.monto_afecto_original, c.variable)\n );\n\n const oldValue = variableOnly.find(c => amountEquals(c.monto_afecto_original, baseVariable));\n const corrected = variableOnly\n .filter(c => c.monto_afecto_original > baseVariable + 0.05)\n .sort((a,b) => Number(b.monto_afecto_original || 0) - Number(a.monto_afecto_original || 0))[0];\n\n if (oldValue && corrected) {\n for (const c of variableOnly) used.add(c.__idx);\n const diff = Number((corrected.monto_afecto_original - baseVariable).toFixed(2));\n pushAdjustment(\n corrected,\n diff,\n 'Corrección de variable',\n `Variable principal ${baseVariable}`,\n 'Se incluyó solo la diferencia entre la variable corregida y la variable ya incluida en la nómina principal.'\n );\n }\n }\n\n for (const c of candidateRecords) {\n if (used.has(c.__idx)) continue;\n const possibleBase = baseRecords.filter(b => samePerson(b, c));\n const baseMatch = possibleBase.find(b => amountEquals(b.monto_afecto_original, c.monto_afecto_original));\n const baseAny = possibleBase[0];\n\n if (baseMatch) {\n // Esta fila es una repetición de la fila principal, no se suma como ajuste.\n continue;\n }\n\n let adjustmentAmount = c.monto_afecto_original;\n let tipo = 'Complemento';\n let baseReference = '';\n\n const isComplementSheet = /complement|complemento/i.test(removeAccents(c.sheetTitle || '').toLowerCase());\n if (!isComplementSheet && baseAny && c.monto_afecto_original > baseAny.monto_afecto_original && possibleBase.some(b => amountEquals(b.monto_afecto_original, baseAny.monto_afecto_original))) {\n // Ejemplo: hoja Oliver trae una fila corregida completa. Se suma solo la diferencia contra la fila principal.\n adjustmentAmount = Number((c.monto_afecto_original - baseAny.monto_afecto_original).toFixed(2));\n tipo = 'Corrección de monto';\n baseReference = `Monto principal ${baseAny.monto_afecto_original}`;\n }\n\n pushAdjustment(c, Number(adjustmentAmount || 0), tipo, baseReference, tipo === 'Corrección de monto'\n ? 'Se incluyó solo la diferencia entre la fila corregida y la fila principal.'\n : 'Se incluyó como complemento de nómina.');\n }\n\n return ajustes;\n}\n\nconst mainRecords = [];\nconst candidates = [];\n\nfor (const vr of valueRanges) {\n const sheetTitle = sheetNameFromRange(vr.range);\n const values = vr.values || [];\n if (!values.length) continue;\n\n if (isMainSheet(sheetTitle)) {\n mainRecords.push(...parsePayrollRows(values, sheetTitle, 'principal'));\n } else {\n candidates.push(...parsePayrollRows(values, sheetTitle, 'ajuste_candidato'));\n }\n}\n\nif (!mainRecords.length) {\n throw new Error(`No pude leer registros válidos desde 1) Nomina General para ${qLabel}.`);\n}\n\nconst ajustes = deriveAdjustments(mainRecords, candidates);\n\nconst data = $getWorkflowStaticData('global');\ndata.gtSeguridadSocial = data.gtSeguridadSocial || {};\ndata.gtSeguridadSocial[init.runId] = data.gtSeguridadSocial[init.runId] || {};\ndata.gtSeguridadSocial[init.runId].nomina.push(...mainRecords);\ndata.gtSeguridadSocial[init.runId].ajustes = data.gtSeguridadSocial[init.runId].ajustes || [];\ndata.gtSeguridadSocial[init.runId].ajustes.push(...ajustes);\n\nreturn [{\n json: {\n runId: init.runId,\n quincena: qLabel,\n payrollRows: mainRecords.length,\n invalidIgssRows: mainRecords.filter(r => !r.igss_norm).length,\n adjustmentRows: ajustes.length,\n adjustmentSheets: [...new Set(ajustes.map(a => a.sheetTitle))]\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
12288,
2368
],
"id": "76fa52d2-2baf-46da-ada0-c096ef313973",
"name": "Guardar Nómina 2Q en memoria"
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0];\nconst bin = init.binary || {};\nconst type = 'sistema';\nfunction norm(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase(); }\nfunction looksExcel(fileName, mime) { const lower = norm(fileName); const m = norm(mime); return lower.endsWith('.xlsx') || lower.endsWith('.xls') || lower.endsWith('.xlsm') || lower.endsWith('.csv') || m.includes('spreadsheet') || m.includes('excel') || m.includes('csv'); }\nfunction scoreCandidate(key, fileName) {\n const text = norm(`${key} ${fileName}`);\n let score = 0;\n if (type === 'q1') { if (text.includes('nomina')) score += 20; if (/\\b1q\\b|q1|primera|mayo 15|15 2026/.test(text)) score += 120; if (/\\b2q\\b|q2|segunda|mayo 30|30 2026/.test(text)) score -= 150; if (/sistema|consolidada|consolidado/.test(text)) score -= 200; }\n if (type === 'q2') { if (text.includes('nomina')) score += 20; if (/\\b2q\\b|q2|segunda|mayo 30|30 2026/.test(text)) score += 120; if (/\\b1q\\b|q1|primera|mayo 15|15 2026/.test(text)) score -= 150; if (/sistema|consolidada|consolidado/.test(text)) score -= 200; }\n if (type === 'sistema') { if (/sistema|propio|2\\.2|igss/.test(text)) score += 120; if (/consolidada|consolidado|planilla consolidada|nomina/.test(text)) score -= 80; }\n if (type === 'consolidado') { if (/consolidada|consolidado|planilla consolidada/.test(text)) score += 120; if (/sistema|propio|2\\.2|nomina/.test(text)) score -= 80; }\n return score;\n}\nconst candidates = [];\nfor (const [key, file] of Object.entries(bin)) {\n const fileName = String(file.fileName || key);\n const mime = String(file.mimeType || '');\n if (!looksExcel(fileName, mime)) continue;\n candidates.push({ key, file, fileName, score: scoreCandidate(key, fileName) });\n}\ncandidates.sort((a,b) => b.score - a.score);\nconst chosen = candidates[0];\nif (!chosen || chosen.score <= 0) throw new Error(`No encontré el archivo requerido para ${type}. Revisa los campos del formulario y los nombres de archivo.`);\nconst data = $getWorkflowStaticData('global');\nconst slot = data.gtSeguridadSocial?.[init.json.runId];\nif (slot) slot.files[type] = chosen.fileName;\nreturn [{ json: { runId: init.json.runId, fileType: type, fileName: chosen.fileName }, binary: { data: chosen.file } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
12864,
2384
],
"id": "74d9bb06-3447-4e27-9a64-28b31455e6fa",
"name": "Preparar Sistema Propio"
},
{
"parameters": {
"operation": "update",
"fileId": {
"__rl": true,
"value": "1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4",
"mode": "list",
"cachedResultName": "Nomina Guatemala",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4/edit"
},
"changeFileContent": true,
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
13072,
2384
],
"id": "275ca7f4-9ac9-4a61-9b86-e7e2618c7591",
"name": "Actualizar staging - Sistema Propio",
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
}
},
{
"parameters": {
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.id || '1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4' }}?fields=spreadsheetId,sheets(properties(title,sheetId,hidden))",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
13504,
2384
],
"id": "3d7072e9-bcb0-402e-9738-b98e8192797e",
"name": "Listar hojas - Sistema Propio",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst target = 'empleados';\nconst spreadsheetId = $json.spreadsheetId || $json.id || '1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4';\nfunction norm(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, ''); }\nconst wanted = { nomina: ['1nominageneral','nominageneral'], empleados: ['empleados'], planilla: ['planillamensual'] }[target];\nconst sheets = ($json.sheets || []).map(s => s.properties || {});\nlet match = null;\nfor (const w of wanted) { match = sheets.find(p => norm(p.title) === w || norm(p.title).includes(w)); if (match) break; }\nif (!match) throw new Error(`No encontré la hoja esperada para ${target}. Hojas disponibles: ${sheets.map(s => s.title).join(', ')}`);\nreturn [{ json: { runId: init.runId, spreadsheetId, sheetTitle: match.title, sheetId: match.sheetId, target } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
13728,
2384
],
"id": "27b72e62-9b86-49b6-9cc5-09bf47fa418b",
"name": "Resolver hoja EMPLEADOS"
},
{
"parameters": {
"url": "={{ 'https://sheets.googleapis.com/v4/spreadsheets/' + $json.spreadsheetId + '/values/' + encodeURIComponent(\"'\" + $json.sheetTitle.replace(/'/g, \"''\") + \"'!A:AZ\") + '?valueRenderOption=UNFORMATTED_VALUE&dateTimeRenderOption=SERIAL_NUMBER' }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
13952,
2384
],
"id": "c0650aa2-fab6-4dee-a15a-6f633fa225ee",
"name": "Leer EMPLEADOS",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst values = $json.values || [];\nfunction clean(s) { return String(s ?? '').replace(/\\s+/g, ' ').trim(); }\nfunction normalizeName(s) { return clean(s).normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toUpperCase().replace(/[^A-Z0-9 ]/g, ' ').replace(/\\s+/g, ' ').trim(); }\nfunction normalizeAf(v) { const raw = clean(v); if (!raw) return ''; let digits = ''; if (typeof v === 'number') digits = Math.trunc(v).toString(); else { const s = raw.replace(/,/g, '').trim(); if (/e\\+?/i.test(s)) { const n = Number(s); if (Number.isFinite(n)) digits = Math.trunc(n).toString(); } else digits = s.replace(/\\D/g, ''); } return digits && digits.length >= 8 && digits.length <= 13 && !/^0+$/.test(digits) ? digits : ''; }\nfunction toNumber(v) { const n = Number(String(v ?? '').replace(/Q|\\$|USD|GTQ|,/gi, '').trim()); return Number.isFinite(n) ? n : 0; }\nconst records = [];\nfor (let i = 1; i < values.length; i++) { const row = values[i] || []; const afiliacion = normalizeAf(row[2]); if (!afiliacion) continue; const nombre = [row[4], row[6], row[8], row[10], row[12]].map(clean).filter(Boolean).join(' '); records.push({ afiliacion, nombre_contabilidad: nombre, nombre_norm: normalizeName(nombre), sueldo_devengado: Number(toNumber(row[14]).toFixed(2)) }); }\nconst by = new Map(); for (const r of records) if (!by.has(r.afiliacion)) by.set(r.afiliacion, r);\nconst finalRecords = [...by.values()];\nif (!finalRecords.length) throw new Error('No pude leer afiliaciones desde EMPLEADOS columna C.');\nconst data = $getWorkflowStaticData('global'); data.gtSeguridadSocial = data.gtSeguridadSocial || {}; data.gtSeguridadSocial[init.runId] = data.gtSeguridadSocial[init.runId] || {}; data.gtSeguridadSocial[init.runId].empleados = finalRecords;\nreturn [{ json: { runId: init.runId, empleadosRows: finalRecords.length } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
14192,
2384
],
"id": "367a8301-888f-4779-a749-f30c61f271b2",
"name": "Guardar EMPLEADOS en memoria"
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0];\nconst bin = init.binary || {};\nconst type = 'consolidado';\nfunction norm(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase(); }\nfunction looksExcel(fileName, mime) { const lower = norm(fileName); const m = norm(mime); return lower.endsWith('.xlsx') || lower.endsWith('.xls') || lower.endsWith('.xlsm') || lower.endsWith('.csv') || m.includes('spreadsheet') || m.includes('excel') || m.includes('csv'); }\nfunction scoreCandidate(key, fileName) {\n const text = norm(`${key} ${fileName}`);\n let score = 0;\n if (type === 'q1') { if (text.includes('nomina')) score += 20; if (/\\b1q\\b|q1|primera|mayo 15|15 2026/.test(text)) score += 120; if (/\\b2q\\b|q2|segunda|mayo 30|30 2026/.test(text)) score -= 150; if (/sistema|consolidada|consolidado/.test(text)) score -= 200; }\n if (type === 'q2') { if (text.includes('nomina')) score += 20; if (/\\b2q\\b|q2|segunda|mayo 30|30 2026/.test(text)) score += 120; if (/\\b1q\\b|q1|primera|mayo 15|15 2026/.test(text)) score -= 150; if (/sistema|consolidada|consolidado/.test(text)) score -= 200; }\n if (type === 'sistema') { if (/sistema|propio|2\\.2|igss/.test(text)) score += 120; if (/consolidada|consolidado|planilla consolidada|nomina/.test(text)) score -= 80; }\n if (type === 'consolidado') { if (/consolidada|consolidado|planilla consolidada/.test(text)) score += 120; if (/sistema|propio|2\\.2|nomina/.test(text)) score -= 80; }\n return score;\n}\nconst candidates = [];\nfor (const [key, file] of Object.entries(bin)) {\n const fileName = String(file.fileName || key);\n const mime = String(file.mimeType || '');\n if (!looksExcel(fileName, mime)) continue;\n candidates.push({ key, file, fileName, score: scoreCandidate(key, fileName) });\n}\ncandidates.sort((a,b) => b.score - a.score);\nconst chosen = candidates[0];\nif (!chosen || chosen.score <= 0) throw new Error(`No encontré el archivo requerido para ${type}. Revisa los campos del formulario y los nombres de archivo.`);\nconst data = $getWorkflowStaticData('global');\nconst slot = data.gtSeguridadSocial?.[init.json.runId];\nif (slot) slot.files[type] = chosen.fileName;\nreturn [{ json: { runId: init.json.runId, fileType: type, fileName: chosen.fileName }, binary: { data: chosen.file } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
14848,
2384
],
"id": "c6a4305d-eec6-4534-8ef3-feeac981f898",
"name": "Preparar Planilla Consolidada"
},
{
"parameters": {
"operation": "update",
"fileId": {
"__rl": true,
"value": "1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4",
"mode": "list",
"cachedResultName": "Nomina Guatemala",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4/edit"
},
"changeFileContent": true,
"options": {}
},
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
15056,
2384
],
"id": "9dbd810f-c51a-4de2-922e-625cb3c495d6",
"name": "Actualizar staging - Consolidada",
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
}
},
{
"parameters": {
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.id || '1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4' }}?fields=spreadsheetId,sheets(properties(title,sheetId,hidden))",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
15408,
2384
],
"id": "c97bd6c8-4e62-403b-85f5-045fce057169",
"name": "Listar hojas - Consolidada",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst target = 'planilla';\nconst spreadsheetId = $json.spreadsheetId || $json.id || '1za7ZdPAGsEbfktl5tuC6LbEZ8f7tiTCi_BszQeA-EO4';\nfunction norm(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, ''); }\nconst wanted = { nomina: ['1nominageneral','nominageneral'], empleados: ['empleados'], planilla: ['planillamensual'] }[target];\nconst sheets = ($json.sheets || []).map(s => s.properties || {});\nlet match = null;\nfor (const w of wanted) { match = sheets.find(p => norm(p.title) === w || norm(p.title).includes(w)); if (match) break; }\nif (!match) throw new Error(`No encontré la hoja esperada para ${target}. Hojas disponibles: ${sheets.map(s => s.title).join(', ')}`);\nreturn [{ json: { runId: init.runId, spreadsheetId, sheetTitle: match.title, sheetId: match.sheetId, target } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
15600,
2384
],
"id": "8494033b-b9f7-4997-a5f1-f2cd787ebbbe",
"name": "Resolver hoja PLANILLA MENSUAL"
},
{
"parameters": {
"url": "={{ 'https://sheets.googleapis.com/v4/spreadsheets/' + $json.spreadsheetId + '/values/' + encodeURIComponent(\"'\" + $json.sheetTitle.replace(/'/g, \"''\") + \"'!A:AZ\") + '?valueRenderOption=UNFORMATTED_VALUE&dateTimeRenderOption=SERIAL_NUMBER' }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
15776,
2384
],
"id": "40cfbe01-46ed-4159-973f-100b1fbbc9a5",
"name": "Leer PLANILLA MENSUAL",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst values = $json.values || [];\nfunction clean(s) { return String(s ?? '').replace(/\\s+/g, ' ').trim(); }\nfunction normalizeName(s) { return clean(s).normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toUpperCase().replace(/[^A-Z0-9 ]/g, ' ').replace(/\\s+/g, ' ').trim(); }\nfunction toNumber(v) { if (v === null || v === undefined || v === '') return 0; if (typeof v === 'number') return Number.isFinite(v) ? v : 0; const n = Number(String(v).replace(/Q|\\$|USD|GTQ|,/gi, '').trim()); return Number.isFinite(n) ? n : 0; }\nfunction normalizeAf(v) { const raw = clean(v); if (!raw) return ''; let digits = ''; if (typeof v === 'number') digits = Math.trunc(v).toString(); else { const s = raw.replace(/,/g, '').trim(); if (/e\\+?/i.test(s)) { const n = Number(s); if (Number.isFinite(n)) digits = Math.trunc(n).toString(); } else digits = s.replace(/\\D/g, ''); } return digits && digits.length >= 8 && digits.length <= 13 && !/^0+$/.test(digits) ? digits : ''; }\nconst records = [];\nfor (let i = 0; i < values.length; i++) { const row = values[i] || []; const afiliacion = normalizeAf(row[1]); if (!afiliacion) continue; const nombre = clean(row[2]); if (!nombre || /^total/i.test(nombre)) continue; records.push({ afiliacion, nombre_planilla: nombre, nombre_norm: normalizeName(nombre), proyecto_planilla: clean(row[4]), monto_planilla_mensual: Number(toNumber(row[25]).toFixed(2)), liquido_planilla: Number(toNumber(row[23]).toFixed(2)) }); }\nconst by = new Map();\nfor (const r of records) { if (!by.has(r.afiliacion)) by.set(r.afiliacion, { ...r }); else { const ex = by.get(r.afiliacion); ex.monto_planilla_mensual = Number((ex.monto_planilla_mensual + r.monto_planilla_mensual).toFixed(2)); ex.liquido_planilla = Number((ex.liquido_planilla + r.liquido_planilla).toFixed(2)); } }\nconst finalRecords = [...by.values()];\nif (!finalRecords.length) throw new Error('No pude leer afiliaciones y monto desde PLANILLA MENSUAL.');\nconst data = $getWorkflowStaticData('global'); data.gtSeguridadSocial = data.gtSeguridadSocial || {}; data.gtSeguridadSocial[init.runId] = data.gtSeguridadSocial[init.runId] || {}; data.gtSeguridadSocial[init.runId].planillaMensual = finalRecords;\nreturn [{ json: { runId: init.runId, planillaRows: finalRecords.length } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
15952,
2384
],
"id": "f63da741-6f40-4b65-9408-ee158b333983",
"name": "Guardar PLANILLA MENSUAL en memoria"
},
{
"parameters": {
"jsCode": "function clean(s) { return String(s ?? '').replace(/\\s+/g, ' ').trim(); }\nfunction money(n) { return Number((Number(n) || 0).toFixed(2)); }\nfunction blankCell(v) { return v === '' ? null : v; }\nfunction sanitizeRows(rows) { return (rows || []).map(row => (row || []).map(blankCell)); }\nfunction removeAccents(s) { return String(s || '').normalize('NFD').replace(/[\\u0300-\\u036f]/g, ''); }\nfunction normalizeName(s) {\n return clean(s)\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/[^A-Z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\nconst MATCH_TOKEN_EQUIVALENCES = new Map([\n ['VASQUEZ', 'VAZQUEZ']\n]);\nfunction canonicalMatchName(s) {\n return normalizeName(s)\n .split(' ')\n .map(token => MATCH_TOKEN_EQUIVALENCES.get(token) || token)\n .join(' ');\n}\nfunction tokens(name) {\n const stop = new Set(['DE','DEL','LA','LAS','LOS','Y','DA','DAS','DO','VDA']);\n return canonicalMatchName(name).split(' ').filter(t => t.length > 1 && !stop.has(t));\n}\nfunction nameScore(a, b) {\n const A = tokens(a), B = tokens(b);\n if (!A.length || !B.length) return 0;\n const setB = new Set(B);\n const inter = A.filter(x => setB.has(x)).length;\n if (inter < 2) return 0;\n return inter / Math.min(A.length, B.length);\n}\nfunction bestByName(name, list, field) {\n const exact = list.find(r => canonicalMatchName(r[field]) === canonicalMatchName(name));\n if (exact) return { record: exact, score: 1, method: 'nombre exacto' };\n let best = { record: null, score: 0, method: '' };\n for (const r of list) {\n const sc = nameScore(name, r[field] || '');\n if (sc > best.score) best = { record: r, score: sc, method: 'nombre parecido' };\n }\n return best.score >= 0.70 ? best : { record: null, score: 0, method: '' };\n}\nfunction makeMap(records, key) {\n const m = new Map();\n for (const r of records || []) if (r[key]) m.set(String(r[key]), r);\n return m;\n}\nfunction uniqueJoin(arr) {\n return [...new Set((arr || []).filter(Boolean).map(clean))].join(' | ');\n}\nfunction fmt(n) {\n return Number(n || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\n}\nfunction bambooStatusLabel(value) {\n const status = normalizeName(value);\n if (status === 'ACTIVE' || status === 'ACTIVO') return 'Activo';\n if (status === 'INACTIVE' || status === 'INACTIVO') return 'Inactivo';\n return clean(value);\n}\nfunction isBambooActive(value) {\n return bambooStatusLabel(value) === 'Activo';\n}\n\n// Personas que intencionalmente no se administran en BambooHR.\n// Se valida primero por IGSS y se conserva el nombre como respaldo.\nconst BAMBOO_EXCLUSIONS = [\n {\n afiliacion: '273223693',\n nombre_norm: normalizeName('SONIA LORENA GUAMUCH'),\n motivo: 'Personal de limpieza fuera de BambooHR por regla autorizada.'\n }\n];\nfunction getBambooExclusion(afiliacion, nombre) {\n const af = String(afiliacion || '').replace(/\\D/g, '');\n const nombreNorm = normalizeName(nombre);\n return BAMBOO_EXCLUSIONS.find(x =>\n (x.afiliacion && af && x.afiliacion === af) ||\n (x.nombre_norm && nombreNorm && x.nombre_norm === nombreNorm)\n ) || null;\n}\n\nfunction chooseBestName(records) {\n const names = (records || []).map(r => clean(r.nombre_nomina)).filter(Boolean);\n if (!names.length) return '';\n names.sort((a,b) => tokens(b).length - tokens(a).length || b.length - a.length);\n return names[0];\n}\nfunction buildNameIndex(rows, field) {\n const exact = new Map();\n const byToken = new Map();\n\n for (const row of rows || []) {\n const canonical = canonicalMatchName(row[field] || '');\n if (!canonical) continue;\n\n if (!exact.has(canonical)) exact.set(canonical, []);\n exact.get(canonical).push(row);\n\n for (const token of new Set(tokens(canonical))) {\n if (!byToken.has(token)) byToken.set(token, []);\n byToken.get(token).push(row);\n }\n }\n\n return { exact, byToken };\n}\nfunction indexedCandidates(name, index) {\n const counts = new Map();\n\n for (const token of new Set(tokens(name))) {\n for (const row of index.byToken.get(token) || []) {\n counts.set(row, (counts.get(row) || 0) + 1);\n }\n }\n\n return [...counts.entries()]\n .filter(([, shared]) => shared >= 2)\n .map(([row]) => row);\n}\nfunction matchBambooByName(name, nameIndex) {\n const target = canonicalMatchName(name);\n if (!target) return { record:null, score:0, method:'sin nombre' };\n\n const exact = [...(nameIndex.exact.get(target) || [])];\n if (exact.length) {\n exact.sort((a,b) => {\n const activeA = isBambooActive(a.estado_bamboo) ? 1 : 0;\n const activeB = isBambooActive(b.estado_bamboo) ? 1 : 0;\n return activeB - activeA;\n });\n return { record:exact[0], score:1, method:'nombre exacto' };\n }\n\n // Solo se evalúan empleados que comparten al menos dos partes del nombre.\n // Esto evita recorrer toda la compañía y mantiene el nodo por debajo\n // del límite del task runner incluso con miles de empleados en BambooHR.\n const candidates = indexedCandidates(name, nameIndex)\n .map(r => ({ record:r, score:nameScore(name, r.nombre_bamboo || '') }))\n .filter(x => x.score >= 0.70)\n .sort((a,b) => b.score - a.score);\n\n if (!candidates.length) return { record:null, score:0, method:'sin coincidencia' };\n\n const best = candidates[0];\n const second = candidates[1];\n const margin = second ? best.score - second.score : best.score;\n const sharedTokens = tokens(name).filter(t => new Set(tokens(best.record.nombre_bamboo || '')).has(t)).length;\n\n if (sharedTokens >= 2 && (best.score >= 0.85 || margin >= 0.15)) {\n return { record:best.record, score:best.score, method:'nombre parecido' };\n }\n return { record:null, score:0, method:'coincidencia ambigua' };\n}\n\nconst init = $items('Inicializar memoria')[0].json;\nconst data = $getWorkflowStaticData('global');\nconst slot = data.gtSeguridadSocial?.[init.runId];\nif (!slot) throw new Error('No encontré la memoria temporal de esta ejecución.');\n\nconst payroll = slot.nomina || [];\nconst ajustesRaw = slot.ajustes || [];\nconst empleados = slot.empleados || [];\nconst planilla = slot.planillaMensual || [];\nconst bamboo = slot.bamboo || [];\n\nif (!payroll.length || !empleados.length || !planilla.length || !bamboo.length) {\n throw new Error(`Faltan datos: nómina=${payroll.length}, empleados=${empleados.length}, planilla=${planilla.length}, bamboo=${bamboo.length}`);\n}\n\nconst bambooNameIndex = buildNameIndex(bamboo, 'nombre_bamboo');\n\nconst empByAf = makeMap(empleados, 'afiliacion');\nconst planByAf = makeMap(planilla, 'afiliacion');\n\nconst resolvedRows = [];\nconst sinIgssRows = [];\n\nfor (const r of payroll) {\n let resolvedAf = r.igss_norm || '';\n let resolveMethod = resolvedAf ? 'Número de IGSS en nómina' : '';\n let corrected = false;\n\n if (!resolvedAf || (!empByAf.has(resolvedAf) && !planByAf.has(resolvedAf))) {\n const bestEmp = bestByName(r.nombre_nomina, empleados, 'nombre_contabilidad');\n if (bestEmp.record) {\n corrected = !!resolvedAf && resolvedAf !== bestEmp.record.afiliacion;\n resolvedAf = bestEmp.record.afiliacion;\n resolveMethod = `${bestEmp.method} en EMPLEADOS`;\n } else {\n const bestPlan = bestByName(r.nombre_nomina, planilla, 'nombre_planilla');\n if (bestPlan.record) {\n corrected = !!resolvedAf && resolvedAf !== bestPlan.record.afiliacion;\n resolvedAf = bestPlan.record.afiliacion;\n resolveMethod = `${bestPlan.method} en PLANILLA MENSUAL`;\n }\n }\n }\n\n if (!r.igss_norm || corrected) {\n sinIgssRows.push({\n ...r,\n resolvedAf,\n resolveMethod,\n corrected,\n accion: corrected ? 'Corregir IGSS en nómina' : 'Completar IGSS en nómina'\n });\n }\n\n resolvedRows.push({ ...r, resolvedAf, resolveMethod, corrected });\n}\n\n// Tipo de cambio para nóminas en dólar, inferido con casos donde el monto coincide contra PLANILLA MENSUAL.\nconst ratios = [];\nfor (const r of resolvedRows) {\n if (!/dolar|dólar|usd/i.test(r.moneda || '') || !r.resolvedAf) continue;\n const pl = planByAf.get(r.resolvedAf);\n if (!pl || !r.monto_afecto_original) continue;\n const ratio = pl.monto_planilla_mensual / r.monto_afecto_original;\n if (ratio >= 5 && ratio <= 10) ratios.push(ratio);\n}\nratios.sort((a,b)=>a-b);\nconst usdRate = ratios.length ? money(ratios[Math.floor(ratios.length/2)]) : 1;\n\nfor (const r of resolvedRows) {\n r.tipo_cambio_usd = /dolar|dólar|usd/i.test(r.moneda || '') ? usdRate : 1;\n r.monto_afecto_gtq = money(r.monto_afecto_original * r.tipo_cambio_usd);\n}\n\nconst groups = new Map();\nconst unresolvedRows = [];\n\nfor (const r of resolvedRows) {\n if (!r.resolvedAf) {\n unresolvedRows.push(r);\n continue;\n }\n\n if (!groups.has(r.resolvedAf)) {\n groups.set(r.resolvedAf, { afiliacion:r.resolvedAf, records:[], ajustes:[], monto:0, montoPrincipal:0, montoAjustes:0 });\n }\n const g = groups.get(r.resolvedAf);\n g.records.push(r);\n g.montoPrincipal = money(g.montoPrincipal + r.monto_afecto_gtq);\n g.monto = money(g.monto + r.monto_afecto_gtq);\n}\n\n// Resolver e incluir ajustes/complementos sin contaminar el cruce de afiliaciones.\nconst ajustesIncluidos = [];\nconst ajustesNoResueltos = [];\n\nfor (const a0 of ajustesRaw) {\n let resolvedAf = a0.igss_norm || '';\n let resolveMethod = resolvedAf ? 'Número de IGSS en ajuste' : '';\n\n if (!resolvedAf || (!empByAf.has(resolvedAf) && !planByAf.has(resolvedAf))) {\n const bestEmp = bestByName(a0.nombre_nomina, empleados, 'nombre_contabilidad');\n if (bestEmp.record) {\n resolvedAf = bestEmp.record.afiliacion;\n resolveMethod = `${bestEmp.method} en EMPLEADOS`;\n } else {\n const bestPlan = bestByName(a0.nombre_nomina, planilla, 'nombre_planilla');\n if (bestPlan.record) {\n resolvedAf = bestPlan.record.afiliacion;\n resolveMethod = `${bestPlan.method} en PLANILLA MENSUAL`;\n }\n }\n }\n\n const rate = /dolar|dólar|usd/i.test(a0.moneda || '') ? usdRate : 1;\n const montoGtq = money((a0.monto_ajuste_original || 0) * rate);\n const ajuste = { ...a0, resolvedAf, resolveMethod, monto_ajuste_gtq: montoGtq, tipo_cambio_usd: rate };\n\n if (!resolvedAf) {\n ajustesNoResueltos.push(ajuste);\n continue;\n }\n\n if (!groups.has(resolvedAf)) {\n groups.set(resolvedAf, { afiliacion:resolvedAf, records:[], ajustes:[], monto:0, montoPrincipal:0, montoAjustes:0 });\n }\n const g = groups.get(resolvedAf);\n g.ajustes.push(ajuste);\n g.montoAjustes = money(g.montoAjustes + montoGtq);\n g.monto = money(g.monto + montoGtq);\n ajustesIncluidos.push(ajuste);\n}\n\n// Cruce adicional contra BambooHR. Se usa el nombre normalizado porque las nóminas\n// no contienen el ID interno de BambooHR. Solo se acepta coincidencia exacta o\n// coincidencia de nombre suficientemente clara y no ambigua.\nconst bambooMatchCache = new Map();\nfunction getBambooMatch(name) {\n const key = canonicalMatchName(name);\n if (!bambooMatchCache.has(key)) bambooMatchCache.set(key, matchBambooByName(name, bambooNameIndex));\n return bambooMatchCache.get(key);\n}\n\nconst payrollBambooMap = new Map();\nfor (const r of resolvedRows) {\n const key = r.resolvedAf ? `AF:${r.resolvedAf}` : `NM:${normalizeName(r.nombre_nomina)}`;\n if (!payrollBambooMap.has(key)) {\n payrollBambooMap.set(key, {\n afiliacion: r.resolvedAf || r.igss_norm || '',\n records: []\n });\n }\n payrollBambooMap.get(key).records.push(r);\n}\n\nconst payrollBambooGroups = [...payrollBambooMap.values()];\nfor (const g of payrollBambooGroups) {\n g.nombre = chooseBestName(g.records);\n g.quincenas = uniqueJoin(g.records.map(r => r.q));\n g.proyectos = uniqueJoin(g.records.map(r => r.proyecto_nomina));\n g.exclusion_bamboo = getBambooExclusion(g.afiliacion, g.nombre);\n\n if (g.exclusion_bamboo) {\n g.match = { record:null, score:0, method:'exclusión autorizada' };\n g.esta_bamboo = 'No aplica';\n g.fecha_ingreso = '';\n g.estado_bamboo = '';\n g.nombre_bamboo = '';\n g.metodo_bamboo = 'exclusión autorizada';\n } else {\n g.match = getBambooMatch(g.nombre);\n g.esta_bamboo = g.match.record ? 'Sí' : 'No';\n g.fecha_ingreso = g.match.record?.fecha_ingreso || '';\n g.estado_bamboo = bambooStatusLabel(g.match.record?.estado_bamboo || '');\n g.nombre_bamboo = g.match.record?.nombre_bamboo || '';\n g.metodo_bamboo = g.match.method || '';\n }\n}\n\nconst payrollBambooByAf = new Map(\n payrollBambooGroups\n .filter(g => g.afiliacion)\n .map(g => [String(g.afiliacion), g])\n);\nconst bambooExclusionsApplied = payrollBambooGroups.filter(g => g.exclusion_bamboo);\nconst payrollBambooEvaluados = payrollBambooGroups.filter(g => !g.exclusion_bamboo);\nconst nominaSinBamboo = payrollBambooEvaluados.filter(g => !g.match.record);\n\nfor (const [af, g] of groups) {\n const nombre = chooseBestName(g.records);\n const info = payrollBambooByAf.get(String(af)) || {\n nombre,\n exclusion_bamboo: getBambooExclusion(af, nombre),\n match: getBambooMatch(nombre)\n };\n g.exclusion_bamboo = info.exclusion_bamboo || null;\n g.bambooMatch = g.exclusion_bamboo ? null : (info.match?.record || null);\n g.esta_bamboo = g.exclusion_bamboo ? 'No aplica' : (g.bambooMatch ? 'Sí' : 'No');\n g.fecha_ingreso = g.bambooMatch?.fecha_ingreso || '';\n g.estado_bamboo = bambooStatusLabel(g.bambooMatch?.estado_bamboo || '');\n g.nombre_bamboo = g.bambooMatch?.nombre_bamboo || '';\n g.metodo_bamboo = g.exclusion_bamboo ? 'exclusión autorizada' : (info.match?.method || '');\n}\n\nconst matchedEmpAf = new Set([...groups.keys()].filter(af => empByAf.has(af)));\nconst matchedPlanAf = new Set([...groups.keys()].filter(af => planByAf.has(af)));\n\nconst soloContabilidad = empleados.filter(e => !matchedEmpAf.has(e.afiliacion));\nconst soloNomina = [...groups.values()].filter(g => !empByAf.has(g.afiliacion));\nconst afiliacionDiferencias = soloContabilidad.length + soloNomina.length;\n\n// Pestaña 1: Cruce Afiliaciones\nconst afiliacionRows = [];\nafiliacionRows.push(['Cruce de afiliaciones - Guatemala']);\nafiliacionRows.push([`Período: ${init.periodoLabel}`]);\nafiliacionRows.push(['Resultado', afiliacionDiferencias ? 'REVISAR DIFERENCIAS' : 'SIN DIFERENCIAS']);\nafiliacionRows.push(['']);\nafiliacionRows.push(['Resumen', 'Cantidad']);\nafiliacionRows.push(['Registros de nómina revisados (Q1 + Q2)', payroll.length]);\nafiliacionRows.push(['Empleados únicos en nómina', groups.size]);\nafiliacionRows.push(['Empleados en contabilidad', empleados.length]);\nafiliacionRows.push(['Empleados de nómina evaluados contra BambooHR', payrollBambooEvaluados.length]);\nafiliacionRows.push(['Empleados fuera de BambooHR por regla autorizada', bambooExclusionsApplied.length]);\nafiliacionRows.push(['Empleados de nómina no encontrados en BambooHR', nominaSinBamboo.length]);\nafiliacionRows.push(['Diferencias encontradas', afiliacionDiferencias]);\nafiliacionRows.push(['']);\nafiliacionRows.push(['Lectura rápida']);\nafiliacionRows.push([afiliacionDiferencias ? 'Hay empleados que aparecen en una fuente y no en la otra. Revisar las secciones de abajo.' : 'No hay empleados faltantes ni sobrantes entre nómina y contabilidad.']);\nafiliacionRows.push(['']);\nafiliacionRows.push(['No se muestran los empleados que coinciden correctamente.']);\nafiliacionRows.push(['']);\nafiliacionRows.push(['Empleados en contabilidad que NO aparecen en nómina']);\nafiliacionRows.push(['Afiliación', 'Nombre completo', 'Qué significa', 'Está en Bamboo?', 'Fecha de Ingreso', 'Activo/Inactivo', 'Nombre en BambooHR']);\nif (soloContabilidad.length) {\n for (const e of soloContabilidad) {\n const bm = getBambooMatch(e.nombre_contabilidad);\n afiliacionRows.push([\n e.afiliacion,\n e.nombre_contabilidad,\n 'Está en contabilidad, pero no aparece en las nóminas Q1/Q2.',\n bm.record ? 'Sí' : 'No',\n bm.record?.fecha_ingreso || '',\n bambooStatusLabel(bm.record?.estado_bamboo || ''),\n bm.record?.nombre_bamboo || ''\n ]);\n }\n} else {\n afiliacionRows.push(['Sin diferencias', '', '', '', '', '', '']);\n}\nafiliacionRows.push(['']);\nafiliacionRows.push(['Empleados en nómina que NO aparecen en contabilidad']);\nafiliacionRows.push(['Afiliación / IGSS', 'Nombre completo', 'Quincena(s)', 'Proyecto(s)', 'Qué significa', 'Está en Bamboo?', 'Fecha de Ingreso', 'Activo/Inactivo', 'Nombre en BambooHR']);\nif (soloNomina.length) {\n for (const g of soloNomina) {\n afiliacionRows.push([\n g.afiliacion,\n uniqueJoin(g.records.map(r=>r.nombre_nomina)),\n uniqueJoin(g.records.map(r=>r.q)),\n uniqueJoin(g.records.map(r=>r.proyecto_nomina)),\n 'Está en nómina, pero no existe en EMPLEADOS de contabilidad.',\n g.esta_bamboo || 'No',\n g.fecha_ingreso || '',\n g.estado_bamboo || '',\n g.nombre_bamboo || ''\n ]);\n }\n} else {\n afiliacionRows.push(['Sin diferencias', '', '', '', '', '', '', '', '']);\n}\n\n// Pestaña 2: Sin IGSS en nómina\nconst sinIgssSheetRows = [];\nsinIgssSheetRows.push(['Sin IGSS en nómina']);\nsinIgssSheetRows.push([`Período: ${init.periodoLabel}`]);\nsinIgssSheetRows.push(['Resultado', sinIgssRows.length ? 'REGISTROS PARA COMPLETAR / CORREGIR' : 'SIN REGISTROS']);\nsinIgssSheetRows.push(['']);\nsinIgssSheetRows.push(['Resumen', 'Cantidad']);\nsinIgssSheetRows.push(['Registros con IGSS pendiente/vacío o a corregir', sinIgssRows.length]);\nsinIgssSheetRows.push(['']);\nsinIgssSheetRows.push(['Lectura rápida']);\nsinIgssSheetRows.push([sinIgssRows.length ? 'Estos empleados existen en contabilidad, pero la columna IGSS de la nómina debe completarse o corregirse.' : 'No hay registros de nómina con IGSS pendiente/vacío o a corregir.']);\nsinIgssSheetRows.push(['']);\nsinIgssSheetRows.push(['Quincena', 'Fila nómina', 'Nombre completo', 'Proyecto', 'IGSS en nómina', 'Afiliación correcta', 'Acción sugerida']);\nif (sinIgssRows.length) {\n for (const r of sinIgssRows.sort((a,b) => String(a.q).localeCompare(String(b.q)) || Number(a.rowNumber||0)-Number(b.rowNumber||0))) {\n sinIgssSheetRows.push([\n r.q,\n r.rowNumber,\n r.nombre_nomina,\n r.proyecto_nomina,\n r.igss_original || '',\n r.resolvedAf || '',\n r.accion\n ]);\n }\n} else {\n sinIgssSheetRows.push(['Sin registros', '', '', '', '', '', '']);\n}\n\n// Pestaña 3: Nómina sin BambooHR\nconst nominaSinBambooRows = [];\nnominaSinBambooRows.push(['Nómina sin BambooHR']);\nnominaSinBambooRows.push([`Período: ${init.periodoLabel}`]);\nnominaSinBambooRows.push(['Resultado', nominaSinBamboo.length ? 'REVISAR EMPLEADOS NO ENCONTRADOS' : 'SIN DIFERENCIAS']);\nnominaSinBambooRows.push(['']);\nnominaSinBambooRows.push(['Resumen', 'Cantidad']);\nnominaSinBambooRows.push(['Empleados únicos de nómina evaluados', payrollBambooEvaluados.length]);\nnominaSinBambooRows.push(['Empleados fuera de BambooHR por regla autorizada', bambooExclusionsApplied.length]);\nnominaSinBambooRows.push(['Empleados de nómina no encontrados en BambooHR', nominaSinBamboo.length]);\nnominaSinBambooRows.push(['']);\nnominaSinBambooRows.push(['Lectura rápida']);\nnominaSinBambooRows.push([nominaSinBamboo.length\n ? 'Estos empleados aparecen en Nómina Q1/Q2, pero no se encontró una coincidencia suficientemente clara en BambooHR.'\n : 'Todos los empleados únicos de nómina fueron encontrados en BambooHR.']);\nnominaSinBambooRows.push(['']);\nnominaSinBambooRows.push(['Empleados de nómina que NO aparecen en BambooHR']);\nnominaSinBambooRows.push(['Afiliación / IGSS', 'Nombre completo', 'Quincena(s)', 'Proyecto(s)', 'Está en Bamboo?', 'Fecha de Ingreso', 'Qué significa']);\nif (nominaSinBamboo.length) {\n for (const g of nominaSinBamboo.sort((a,b) => String(a.nombre).localeCompare(String(b.nombre)))) {\n nominaSinBambooRows.push([\n g.afiliacion || '',\n g.nombre,\n g.quincenas,\n g.proyectos,\n 'No',\n '',\n 'Está en nómina, pero no se encontró en BambooHR por nombre exacto o coincidencia clara.'\n ]);\n }\n} else {\n nominaSinBambooRows.push(['Sin diferencias', '', '', '', '', '', '']);\n}\n\n// Cruce de montos después de ajustes\nconst montoFindings = [];\nconst noComparados = [];\nlet montoOk = 0;\n\nfor (const [af,g] of groups) {\n const pl = planByAf.get(af);\n if (!pl) {\n noComparados.push({estado:'Solo en nóminas',af,g,pl:null,obs:'No existe en PLANILLA MENSUAL.'});\n continue;\n }\n const diff = money(g.monto - pl.monto_planilla_mensual);\n if (Math.abs(diff) <= 0.05) {\n montoOk++;\n } else {\n montoFindings.push({af,g,pl,diff});\n }\n}\nfor (const pl of planilla) {\n if (!matchedPlanAf.has(pl.afiliacion)) {\n noComparados.push({estado:'Solo en PLANILLA MENSUAL',af:pl.afiliacion,g:null,pl,obs:'Existe en PLANILLA MENSUAL, pero no se relacionó con las nóminas Q1/Q2.'});\n }\n}\nfor (const r of unresolvedRows) {\n noComparados.push({estado:'IGSS sin resolver',af:'',g:{records:[r],monto:r.monto_afecto_gtq},pl:null,obs:'No se pudo resolver afiliación por número ni por nombre.'});\n}\nfor (const a of ajustesNoResueltos) {\n noComparados.push({estado:'Ajuste sin resolver',af:'',g:{records:[a],monto:a.monto_ajuste_gtq},pl:null,obs:'No se pudo relacionar el ajuste/complemento por IGSS ni por nombre.'});\n}\nmontoFindings.sort((a,b)=>Math.abs(b.diff)-Math.abs(a.diff));\nconst montoResultado = (montoFindings.length || noComparados.length) ? 'REVISAR DIFERENCIAS' : 'SIN DIFERENCIAS';\n\n// Pestaña 4: Cruce Montos\nconst montosRows = [];\nmontosRows.push(['Cruce de montos afectos - Guatemala']);\nmontosRows.push([`Período: ${init.periodoLabel}`]);\nmontosRows.push(['Resultado', montoResultado]);\nmontosRows.push([`Tipo de cambio USD usado: ${usdRate === 1 ? 'No aplicado' : usdRate}`]);\nmontosRows.push(['']);\nmontosRows.push(['Resumen', 'Cantidad']);\nmontosRows.push(['Afiliaciones comparadas con monto correcto', montoOk]);\nmontosRows.push(['Diferencias reales de monto', montoFindings.length]);\nmontosRows.push(['Casos no comparados', noComparados.length]);\nmontosRows.push(['Ajustes/complementos incluidos', ajustesIncluidos.length]);\nmontosRows.push(['']);\nmontosRows.push(['Lectura rápida']);\nmontosRows.push([montoResultado === 'SIN DIFERENCIAS' ? 'No hay diferencias de monto después de sumar nómina Q1, nómina Q2 y ajustes/complementos.' : 'Hay diferencias o casos no comparados. Revisar las secciones de abajo.']);\nmontosRows.push(['']);\nmontosRows.push(['No se muestran los empleados que cuadran correctamente.']);\nmontosRows.push(['']);\nmontosRows.push(['Diferencias reales de monto']);\nmontosRows.push(['Afiliación', 'Nombre completo', 'Proyecto', 'Monto nómina Q1+Q2 + ajustes', 'Monto planilla mensual', 'Diferencia', 'Detalle', 'Qué revisar']);\nif (montoFindings.length) {\n for (const f of montoFindings) {\n const details = [\n ...f.g.records.map(r => `${r.q}: ${fmt(r.monto_afecto_gtq)}${r.tipo_cambio_usd !== 1 ? ' GTQ (USD convertido)' : ''}`),\n ...f.g.ajustes.map(a => `Ajuste ${a.q}/${a.sheetTitle}: ${fmt(a.monto_ajuste_gtq)}`)\n ].join(' | ');\n montosRows.push([\n f.af,\n f.pl?.nombre_planilla || uniqueJoin(f.g.records.map(r=>r.nombre_nomina)),\n f.pl?.proyecto_planilla || uniqueJoin(f.g.records.map(r=>r.proyecto_nomina)),\n f.g.monto,\n f.pl.monto_planilla_mensual,\n f.diff,\n details,\n 'Revisar diferencia entre nómina combinada y PLANILLA MENSUAL.'\n ]);\n }\n} else {\n montosRows.push(['Sin diferencias', '', '', '', '', '', '', '']);\n}\nmontosRows.push(['']);\nmontosRows.push(['Casos no comparados']);\nmontosRows.push(['Estado','Afiliación','Nombre completo','Monto nómina / ajuste','Monto planilla mensual','Qué significa']);\nif (noComparados.length) {\n for (const f of noComparados) {\n montosRows.push([\n f.estado,\n f.af,\n f.pl?.nombre_planilla || uniqueJoin((f.g?.records || []).map(r=>r.nombre_nomina)),\n f.g?.monto || '',\n f.pl?.monto_planilla_mensual || '',\n f.obs\n ]);\n }\n} else {\n montosRows.push(['Sin casos', '', '', '', '', '']);\n}\n\n// Pestaña 5: Ajustes incluidos\nconst ajustesRows = [];\najustesRows.push(['Ajustes incluidos']);\najustesRows.push([`Período: ${init.periodoLabel}`]);\najustesRows.push(['Resultado', ajustesIncluidos.length ? `${ajustesIncluidos.length} ajuste(s) incluidos en el cruce de montos` : 'Sin ajustes incluidos']);\najustesRows.push(['']);\najustesRows.push(['Lectura rápida']);\najustesRows.push([ajustesIncluidos.length ? 'Estos ajustes/complementos se sumaron para comparar correctamente contra PLANILLA MENSUAL.' : 'No se encontraron ajustes/complementos para sumar al cruce de montos.']);\najustesRows.push(['']);\najustesRows.push(['Quincena', 'Hoja origen', 'Fila', 'Afiliación', 'Nombre completo', 'Proyecto', 'Monto ajuste', 'Tipo', 'Observación']);\nif (ajustesIncluidos.length) {\n for (const a of ajustesIncluidos.sort((a,b) => String(a.q).localeCompare(String(b.q)) || String(a.sheetTitle).localeCompare(String(b.sheetTitle)) || Number(a.rowNumber||0)-Number(b.rowNumber||0))) {\n ajustesRows.push([\n a.q,\n a.sheetTitle,\n a.rowNumber,\n a.resolvedAf,\n a.nombre_nomina,\n a.proyecto_nomina,\n a.monto_ajuste_gtq,\n a.tipo_ajuste,\n a.observacion_ajuste\n ]);\n }\n} else {\n ajustesRows.push(['Sin ajustes', '', '', '', '', '', '', '', '']);\n}\n\nconst q1Count = payroll.filter(r=>r.q==='1Q').length;\nconst q2Count = payroll.filter(r=>r.q==='2Q').length;\n\nconst summary = {\n periodo:init.periodoLabel,\n q1Rows:q1Count,\n q2Rows:q2Count,\n empleados:empleados.length,\n planilla:planilla.length,\n empleadosUnicosNomina:groups.size,\n afiliacionSoloContabilidad:soloContabilidad.length,\n afiliacionSoloNomina:soloNomina.length,\n afiliacionDiferencias,\n sinIgss:sinIgssRows.length,\n montoOk,\n montoFindings:montoFindings.length,\n noComparados:noComparados.length,\n ajustesIncluidos:ajustesIncluidos.length,\n bambooTotal:bamboo.length,\n empleadosNominaEvaluadosBamboo:payrollBambooEvaluados.length,\n bambooExcluidos:bambooExclusionsApplied.length,\n nominaSinBamboo:nominaSinBamboo.length,\n usdRate,\n files:slot.files || {}\n};\n\ndelete data.gtSeguridadSocial[init.runId];\n\nreturn [{\n json: {\n runId:init.runId,\n reportTitle:init.reportTitle,\n emailTo:init.emailTo || 'iaracena@gomezleemarketing.com',\n afiliacionRows: sanitizeRows(afiliacionRows),\n sinIgssRows: sanitizeRows(sinIgssSheetRows),\n nominaSinBambooRows: sanitizeRows(nominaSinBambooRows),\n montosRows: sanitizeRows(montosRows),\n ajustesRows: sanitizeRows(ajustesRows),\n summary,\n rowCounts:{\n afiliacion:afiliacionRows.length,\n sinIgss:sinIgssSheetRows.length,\n nominaSinBamboo:nominaSinBambooRows.length,\n montos:montosRows.length,\n ajustes:ajustesRows.length\n }\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
16416,
2384
],
"id": "ffee68ac-df72-4561-939d-1b4dae369e15",
"name": "Construir reporte producción"
},
{
"parameters": {
"method": "POST",
"url": "https://sheets.googleapis.com/v4/spreadsheets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ properties: { title: $json.reportTitle }, sheets: [ { properties: { title: 'Cruce Afiliaciones' } }, { properties: { title: 'Sin IGSS en nómina' } }, { properties: { title: 'Nómina sin BambooHR' } }, { properties: { title: 'Cruce Montos' } }, { properties: { title: 'Ajustes incluidos' } } ] }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
16656,
2384
],
"id": "2597a490-b065-45d1-902b-5abd11019177",
"name": "Crear Google Sheet Reporte",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/values:batchUpdate",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ data: [ { range: 'Cruce Afiliaciones!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.afiliacionRows }, { range: 'Sin IGSS en nómina!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.sinIgssRows }, { range: 'Nómina sin BambooHR!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.nominaSinBambooRows }, { range: 'Cruce Montos!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.montosRows }, { range: 'Ajustes incluidos!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.ajustesRows } ], valueInputOption: 'RAW' }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
16896,
2384
],
"id": "7971ea0b-9fca-47d9-9595-2f1c5cb59046",
"name": "Escribir reporte",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}:batchUpdate",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ (() => {\n const sheets = $('Crear Google Sheet Reporte').item.json.sheets || [];\n const colors = [\n { red: 0.91, green: 0.96, blue: 0.90 },\n { red: 1.00, green: 0.96, blue: 0.80 },\n { red: 1.00, green: 0.90, blue: 0.84 },\n { red: 0.91, green: 0.96, blue: 0.90 },\n { red: 0.93, green: 0.94, blue: 0.96 }\n ];\n const requests = [];\n sheets.forEach((sheet, index) => {\n const sheetId = sheet.properties.sheetId;\n requests.push(\n { autoResizeDimensions: { dimensions: { sheetId, dimension: 'COLUMNS', startIndex: 0, endIndex: 14 } } },\n { updateSheetProperties: { properties: { sheetId, gridProperties: { frozenRowCount: 1 } }, fields: 'gridProperties.frozenRowCount' } },\n { repeatCell: { range: { sheetId, startRowIndex: 0, endRowIndex: 1 }, cell: { userEnteredFormat: { textFormat: { bold: true, fontSize: 14 }, backgroundColor: colors[index] || colors[0] } }, fields: 'userEnteredFormat.textFormat,userEnteredFormat.backgroundColor' } },\n { repeatCell: { range: { sheetId }, cell: { userEnteredFormat: { wrapStrategy: 'WRAP' } }, fields: 'userEnteredFormat.wrapStrategy' } }\n );\n });\n return JSON.stringify({ requests });\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
17136,
2384
],
"id": "0d71593a-4ce2-4f35-8f9e-e0e3c75b5d78",
"name": "Formatear reporte",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "K0hDZh3a85MpOHCs",
"name": "Google Sheets account 2"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleDriveOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'iaracena@gomezleemarketing.com' }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
17376,
2384
],
"id": "f941fae6-def7-4417-a4c8-62a6efe8e11a",
"name": "Compartir con Isaac",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleDriveOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'ymadera@gomezleemarketing.com' }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
17616,
2384
],
"id": "97660ee4-c52e-4996-ad4d-7a01aa954010",
"name": "Compartir con Yanelly editor",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"sendTo": "msoto@gomezleemarketing.com, iaracena@gomezleemarketing.com, iherrera@gomezleemarketing.com, vparamo@gomezleemarketing.com, asrodriguez@gomezleemarketing.com, mgomez@gomezleemarketing.com, ymadera@gomezleemarketing.com",
"subject": "={{ 'Cruce Seguridad Social Guatemala - ' + ($('Construir reporte producción').item.json.summary.periodo || '') }}",
"message": "={{ (() => {\nconst s = $('Construir reporte producción').item.json.summary || {};\nconst url = $('Crear Google Sheet Reporte').item.json.spreadsheetUrl || '#';\nconst afiliacionDiff = Number(s.afiliacionDiferencias || 0);\nconst montoDiff = Number(s.montoFindings || 0);\nconst noComparados = Number(s.noComparados || 0);\nconst sinIgss = Number(s.sinIgss || 0);\nconst ajustes = Number(s.ajustesIncluidos || 0);\nconst sinBamboo = Number(s.nominaSinBamboo || 0);\nconst hardDiff = afiliacionDiff + montoDiff + noComparados + sinBamboo;\nconst status = hardDiff ? 'REQUIERE REVISIÓN' : (sinIgss ? 'COMPLETADO · DATOS A CORREGIR' : 'COMPLETADO · SIN DIFERENCIAS');\nconst statusBg = hardDiff ? '#fff7ed' : (sinIgss ? '#fffbeb' : '#f0fdf4');\nconst statusBorder = hardDiff ? '#fed7aa' : (sinIgss ? '#fde68a' : '#bbf7d0');\nconst statusColor = hardDiff ? '#9a3412' : (sinIgss ? '#92400e' : '#166534');\nreturn `<div style=\"font-family:Arial,Helvetica,sans-serif;background:#f6f8fb;padding:0;margin:0\">\n <div style=\"max-width:720px;margin:0 auto;background:#ffffff;border-radius:14px;overflow:hidden;border:1px solid #e3e8ef\">\n <div style=\"border-top:5px solid #58bd45;padding:30px 34px 20px\">\n <img src=\"data:image/png;base64,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\" alt=\"GomezLee Marketing\" style=\"width:210px;max-width:70%;height:auto;display:block;margin-bottom:28px\" />\n <div style=\"margin-top:8px;color:#58bd45;font-weight:700;font-size:12px;letter-spacing:2px\">SEGURIDAD SOCIAL · GUATEMALA</div>\n <h1 style=\"margin:8px 0 8px;color:#4f7185;font-size:26px\">Cruce mensual completado</h1>\n <p style=\"margin:0;color:#5f7f92\">${s.periodo || ''} · Nóminas Q1 + Q2</p>\n </div>\n <div style=\"padding:26px 34px\">\n <div style=\"padding:16px;border-radius:12px;background:${statusBg};border:1px solid ${statusBorder};color:${statusColor};font-weight:700;margin-bottom:18px\">${status}</div>\n <div style=\"display:flex;gap:14px;flex-wrap:wrap\">\n <div style=\"flex:1;min-width:150px;border:1px solid #d8e0e7;border-top:3px solid #4f7185;border-radius:12px;padding:16px\">\n <div style=\"font-size:11px;color:#6d8da1;font-weight:700;letter-spacing:1px\">NÓMINA Q1</div>\n <div style=\"font-size:28px;color:#4f7185;font-weight:700\">${s.q1Rows||0}</div>\n </div>\n <div style=\"flex:1;min-width:150px;border:1px solid #d8e0e7;border-top:3px solid #4f7185;border-radius:12px;padding:16px\">\n <div style=\"font-size:11px;color:#6d8da1;font-weight:700;letter-spacing:1px\">NÓMINA Q2</div>\n <div style=\"font-size:28px;color:#4f7185;font-weight:700\">${s.q2Rows||0}</div>\n </div>\n <div style=\"flex:1;min-width:150px;border:1px solid #d8e0e7;border-top:3px solid ${hardDiff ? '#ff8a00' : '#58bd45'};border-radius:12px;padding:16px\">\n <div style=\"font-size:11px;color:#6d8da1;font-weight:700;letter-spacing:1px\">HALLAZGOS A REVISAR</div>\n <div style=\"font-size:28px;color:#4f7185;font-weight:700\">${hardDiff}</div>\n </div>\n </div>\n <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:22px\">\n <div style=\"border:1px solid #d8e0e7;border-radius:12px;padding:16px\">\n <h3 style=\"margin:0 0 12px;color:#4f7185;font-size:16px\">Afiliaciones</h3>\n <p style=\"margin:8px 0;color:#333\">Resultado <strong style=\"float:right;color:#4f7185\">${afiliacionDiff ? 'Revisar' : 'Sin diferencias'}</strong></p>\n <p style=\"margin:8px 0;color:#333\">Empleados únicos nómina <strong style=\"float:right;color:#4f7185\">${s.empleadosUnicosNomina||0}</strong></p>\n <p style=\"margin:8px 0;color:#333\">Sin IGSS en nómina <strong style=\"float:right;color:#4f7185\">${sinIgss}</strong></p>\n <p style=\"margin:8px 0;color:#333\">Nómina sin BambooHR <strong style=\"float:right;color:#4f7185\">${sinBamboo}</strong></p>\n </div>\n <div style=\"border:1px solid #d8e0e7;border-radius:12px;padding:16px\">\n <h3 style=\"margin:0 0 12px;color:#4f7185;font-size:16px\">Montos afectos</h3>\n <p style=\"margin:8px 0;color:#333\">Diferencias reales <strong style=\"float:right;color:#4f7185\">${montoDiff}</strong></p>\n <p style=\"margin:8px 0;color:#333\">Casos no comparados <strong style=\"float:right;color:#4f7185\">${noComparados}</strong></p>\n <p style=\"margin:8px 0;color:#333\">Ajustes incluidos <strong style=\"float:right;color:#4f7185\">${ajustes}</strong></p>\n </div>\n </div>\n <div style=\"margin-top:24px\">\n <a href=\"${url}\" style=\"display:inline-block;background:#58bd45;color:white;text-decoration:none;border-radius:24px;padding:13px 22px;font-weight:700\">Abrir reporte en Google Sheets</a>\n </div>\n <div style=\"margin-top:22px;color:#6d8da1;font-size:12px;line-height:1.5\">\n <strong>Archivos procesados</strong><br>\n Nómina Q1: ${s.files?.q1||''}<br>\n Nómina Q2: ${s.files?.q2||''}<br>\n Sistema Propio IGSS: ${s.files?.sistema||''}<br>\n Planilla Consolidada: ${s.files?.consolidado||''}\n </div>\n </div>\n </div>\n</div>`;\n})() }}",
"options": {
"appendAttribution": false
}
},
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
19072,
2576
],
"id": "ef4cb153-8fe4-43d4-acaf-49d0d927951b",
"name": "Enviar correo",
"webhookId": "a3513342-026e-470b-a4f1-319761ae1bca",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
}
},
{
"parameters": {
"content": "## Inicio de Flujo\nRecibir adjuntos a través de formulario y preparación de nóminas",
"height": 624,
"width": 4512,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
8048,
1936
],
"id": "a6eefbde-4ba8-4137-a6ff-c483675a0145",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## Preparar archivos de contadores\n\nPreparar archivos Sistema Propio y Planilla Consolidada",
"height": 320,
"width": 3344
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
12784,
2256
],
"id": "19e73e16-4886-4683-8323-9beef2ff14e1",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## Preparar reporte y enviar correo\n\nConstruir reporte y prepararlo en el formato adecuado. Preparar correo para envío.",
"height": 944,
"width": 3296,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
16272,
1840
],
"id": "794fb56f-e352-4440-aa18-20a992dfbadf",
"name": "Sticky Note2"
},
{
"parameters": {
"amount": 8
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
9328,
2368
],
"id": "ab82d494-0163-4ed9-9256-8839ba8a8684",
"name": "Esperar conversión - Nómina 1Q",
"webhookId": "12e87d62-e245-459a-ad10-dcccee2beca9"
},
{
"parameters": {
"amount": 8
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
11072,
2368
],
"id": "453f854e-31ce-4056-9246-55092399eff2",
"name": "Esperar conversión - Nómina 2Q",
"webhookId": "6be5dfdc-e90d-40f2-9349-35688e3532f1"
},
{
"parameters": {
"amount": 8
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
13280,
2384
],
"id": "657d681f-d26c-4294-957a-811ec0e20039",
"name": "Esperar conversión - Sistema Propio",
"webhookId": "8b362815-600a-4846-ad4f-f09897acfb9f"
},
{
"parameters": {
"amount": 8
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
15248,
2384
],
"id": "95a9a0ac-7ac3-4df9-9b93-ff66640bbb64",
"name": "Esperar conversión - Consolidada",
"webhookId": "a36aedc6-3818-4bad-8b81-f4b2f7f2f02d"
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst source = $items('Inicializar memoria')[0];\nconst bin = source.binary || {};\nconst archivos = {};\nfor (const [key, file] of Object.entries(bin)) {\n archivos[key] = {\n nombre_archivo: file.fileName || key,\n mime_type: file.mimeType || '',\n tamano_bytes: file.fileSize || file.size || null\n };\n}\n\nreturn [{\n json: {\n id_ejecucion: init.runId,\n nombre_flujo: 'Cruce de Seguridad Social Guatemala',\n id_flujo_n8n: $workflow.id || null,\n id_ejecucion_n8n: $execution.id || null,\n pais_codigo: 'GT',\n pais_nombre: 'Guatemala',\n entidad_seguridad_social: 'IGSS',\n mes: Number(init.mes || 0),\n anio: Number(init.anio || 0),\n periodo: init.periodoLabel || `${init.mes}/${init.anio}`,\n estado: 'procesando',\n resultado: 'Cruce iniciado',\n usuario_ejecutor_email: null,\n destinatarios_correo: ['iaracena@gomezleemarketing.com', 'ymadera@gomezleemarketing.com'],\n moneda: 'GTQ',\n archivos_procesados: archivos,\n conteos_fuente: {},\n metricas: {\n etapa: 'inicio',\n formulario: 'Cruce Seguridad Social Guatemala'\n },\n resumen_completo: {\n runId: init.runId,\n periodo: init.periodoLabel,\n archivos_recibidos: archivos\n }\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
8368,
2144
],
"id": "86ae8a39-aa36-4f3d-9f94-603629c34598",
"name": "Preparar auditoría inicial Supabase",
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "=https://dbit.digitalcompass.agency/rest/v1/auditoria_cruces_seguridad_social?on_conflict=id_ejecucion",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Authorization",
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "resolution=merge-duplicates,return=minimal"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
8608,
2144
],
"id": "b8ce027b-f3db-485b-ace8-b69a6b044e71",
"name": "Supabase - Crear auditoría inicial",
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "function n(v){ const x = Number(v || 0); return Number.isFinite(x) ? x : 0; }\nfunction money(v){ return Number((Number(v || 0)).toFixed(2)); }\nfunction clean(v){ return String(v ?? '').replace(/\\s+/g,' ').trim(); }\nfunction parseRunStart(runId){\n const m = String(runId || '').match(/GT_SEG_(\\d{4})(\\d{2})(\\d{2})_(\\d{2})(\\d{2})(\\d{2})/);\n if (!m) return null;\n const [,y,mo,d,h,mi,s] = m;\n const dt = new Date(Number(y), Number(mo)-1, Number(d), Number(h), Number(mi), Number(s));\n return Number.isFinite(dt.getTime()) ? dt : null;\n}\nfunction sumMontoDiff(rows){\n const data = Array.isArray(rows) ? rows : [];\n const start = data.findIndex(r => r && r[0] === 'Diferencias reales de monto');\n if (start < 0) return 0;\n let total = 0;\n for (let i = start + 2; i < data.length; i++) {\n const row = data[i] || [];\n if (!row.length || row.every(x => clean(x) === '')) break;\n if (clean(row[0]) === 'Sin diferencias') continue;\n total += n(row[5]);\n }\n return money(total);\n}\n\nconst init = $items('Inicializar memoria')[0].json || {};\nconst reporte = $items('Construir reporte producción')[0].json || {};\nconst sheet = $items('Crear Google Sheet Reporte')[0].json || {};\nconst s = reporte.summary || {};\nconst now = new Date();\nconst start = parseRunStart(init.runId);\nconst executionSeconds = start ? Math.max(0, (now.getTime() - start.getTime()) / 1000) : null;\n\nconst diferenciasIdentidad = n(s.afiliacionDiferencias);\nconst diferenciasMonetarias = n(s.montoFindings);\nconst noComparados = n(s.noComparados);\nconst alertasDocumentales = n(s.sinIgss);\nconst sinBamboo = n(s.nominaSinBamboo);\nconst diferenciasTotal = diferenciasIdentidad + diferenciasMonetarias + noComparados + alertasDocumentales + sinBamboo;\nconst hardDiff = diferenciasIdentidad + diferenciasMonetarias + noComparados + sinBamboo;\nconst resultado = hardDiff\n ? 'REQUIERE REVISIÓN'\n : (alertasDocumentales ? 'COMPLETADO · DATOS A CORREGIR' : 'COMPLETADO · SIN DIFERENCIAS');\n\nconst minutosManual = 90;\nconst minutosAutomatizacion = executionSeconds === null ? null : money(executionSeconds / 60);\nconst minutosAhorrados = minutosAutomatizacion === null ? null : money(Math.max(0, minutosManual - minutosAutomatizacion));\nconst diferenciaMonto = sumMontoDiff(reporte.montosRows);\n\nconst finalPayload = {\n estado: 'exitoso',\n resultado,\n fecha_fin: now.toISOString(),\n duracion_segundos: executionSeconds === null ? null : money(executionSeconds),\n url_reporte: sheet.spreadsheetUrl || null,\n id_google_sheet: sheet.spreadsheetId || null,\n destinatarios_correo: [\"msoto@gomezleemarketing.com\", \"iaracena@gomezleemarketing.com\", \"iherrera@gomezleemarketing.com\", \"vparamo@gomezleemarketing.com\", \"asrodriguez@gomezleemarketing.com\", \"mgomez@gomezleemarketing.com\", \"ymadera@gomezleemarketing.com\"],\n moneda: 'GTQ',\n\n empleados_nomina: n(s.empleadosUnicosNomina),\n empleados_entidad: n(s.planilla),\n empleados_coincidentes: Math.max(0, n(s.empleadosUnicosNomina) - n(s.afiliacionSoloNomina)),\n solo_nomina: n(s.afiliacionSoloNomina),\n solo_entidad: n(s.afiliacionSoloContabilidad),\n\n diferencias_total: diferenciasTotal,\n diferencias_monetarias: diferenciasMonetarias,\n diferencias_identidad: diferenciasIdentidad,\n alertas_documentales: alertasDocumentales + noComparados + sinBamboo,\n registros_duplicados: 0,\n alertas_periodo: 0,\n\n total_nomina: null,\n total_entidad: null,\n diferencia_monto: diferenciaMonto,\n\n minutos_estimados_manual: minutosManual,\n minutos_automatizacion: minutosAutomatizacion,\n minutos_ahorrados: minutosAhorrados,\n\n archivos_procesados: s.files || {},\n conteos_fuente: {\n nomina_q1: n(s.q1Rows),\n nomina_q2: n(s.q2Rows),\n sistema_propio_empleados: n(s.empleados),\n planilla_consolidada: n(s.planilla),\n empleados_unicos_nomina: n(s.empleadosUnicosNomina),\n empleados_bamboo: n(s.bambooTotal),\n empleados_nomina_evaluados_bamboo: n(s.empleadosNominaEvaluadosBamboo),\n empleados_nomina_excluidos_bamboo: n(s.bambooExcluidos),\n nomina_sin_bamboo: sinBamboo\n },\n metricas: {\n monto_correcto: n(s.montoOk),\n monto_diferencias: diferenciasMonetarias,\n casos_no_comparados: noComparados,\n sin_igss: alertasDocumentales,\n nomina_sin_bamboo: sinBamboo,\n empleados_bamboo_consultados: n(s.bambooTotal),\n empleados_excluidos_bamboo: n(s.bambooExcluidos),\n ajustes_incluidos: n(s.ajustesIncluidos),\n tipo_cambio_usd: s.usdRate || null,\n filas_reporte: reporte.rowCounts || {},\n impacto: {\n minutos_manual_estimado: minutosManual,\n minutos_automatizacion: minutosAutomatizacion,\n minutos_ahorrados: minutosAhorrados\n }\n },\n resumen_completo: {\n summary: s,\n rowCounts: reporte.rowCounts || {},\n reportTitle: reporte.reportTitle,\n spreadsheetId: sheet.spreadsheetId || null,\n spreadsheetUrl: sheet.spreadsheetUrl || null\n },\n mensaje_error: null,\n nodo_error: null\n};\n\nreturn [{\n json: {\n id_ejecucion: init.runId,\n pais_codigo: 'GT',\n pais_nombre: 'Guatemala',\n moneda: 'GTQ',\n finalPayload,\n source: {\n summary: s,\n afiliacionRows: reporte.afiliacionRows || [],\n sinIgssRows: reporte.sinIgssRows || [],\n nominaSinBambooRows: reporte.nominaSinBambooRows || [],\n montosRows: reporte.montosRows || [],\n ajustesRows: reporte.ajustesRows || [],\n files: s.files || {},\n rowCounts: reporte.rowCounts || {},\n spreadsheetId: sheet.spreadsheetId || null,\n spreadsheetUrl: sheet.spreadsheetUrl || null\n }\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
18816,
2144
],
"id": "322f0550-6f12-472d-9233-a17db84c9d7d",
"name": "Preparar auditoría final Supabase",
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "PATCH",
"url": "=https://dbit.digitalcompass.agency/rest/v1/auditoria_cruces_seguridad_social?id_ejecucion=eq.{{$json.id_ejecucion}}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Authorization",
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.finalPayload) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
19056,
2144
],
"id": "65df2fd6-97eb-4bdc-ba5d-9590442b4181",
"name": "Supabase - Actualizar auditoría final",
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "function clean(v){ return String(v ?? '').replace(/\\s+/g,' ').trim(); }\nfunction n(v){ const x = Number(v || 0); return Number.isFinite(x) ? x : null; }\nfunction push(rows, obj){ rows.push({ json: obj }); }\nfunction parseSection(rows, title){\n const data = Array.isArray(rows) ? rows : [];\n const start = data.findIndex(r => r && r[0] === title);\n if (start < 0) return [];\n const out = [];\n for (let i = start + 2; i < data.length; i++) {\n const row = data[i] || [];\n if (!row.length || row.every(x => clean(x) === '')) break;\n if (String(row[0] || '').toLowerCase().startsWith('sin ')) continue;\n out.push(row);\n }\n return out;\n}\n\nconst base = $input.first().json;\nconst src = base.source || {};\nconst common = {\n id_ejecucion: base.id_ejecucion,\n pais_codigo: 'GT',\n pais_nombre: 'Guatemala',\n moneda: 'GTQ'\n};\nconst out = [];\n\nfor (const r of parseSection(src.afiliacionRows, 'Empleados en contabilidad que NO aparecen en nómina')) {\n push(out, {\n ...common,\n categoria: 'Solo en contabilidad / Sistema Propio IGSS',\n severidad: 'revision',\n identificador_empleado: clean(r[0]),\n nombre_empleado: clean(r[1]),\n fuente_a: 'Sistema Propio IGSS / Contabilidad',\n fuente_b: 'Nómina Q1/Q2',\n diferencia_monto: null,\n descripcion: clean(r[2]) || 'Está en contabilidad, pero no aparece en las nóminas Q1/Q2.',\n accion_sugerida: 'Validar si el empleado debe estar en nómina del período o si corresponde depurar contabilidad.',\n datos_originales: { fila_reporte: r }\n });\n}\n\nfor (const r of parseSection(src.afiliacionRows, 'Empleados en nómina que NO aparecen en contabilidad')) {\n push(out, {\n ...common,\n categoria: 'Solo en nómina',\n severidad: 'revision',\n identificador_empleado: clean(r[0]),\n nombre_empleado: clean(r[1]),\n fuente_a: 'Nómina Q1/Q2',\n fuente_b: 'Sistema Propio IGSS / Contabilidad',\n diferencia_monto: null,\n descripcion: clean(r[4]) || 'Está en nómina, pero no existe en EMPLEADOS de contabilidad.',\n accion_sugerida: 'Completar o corregir afiliación en Sistema Propio IGSS / Contabilidad.',\n datos_originales: { fila_reporte: r, quincenas: r[2], proyectos: r[3] }\n });\n}\n\nfor (const r of parseSection(src.sinIgssRows, 'Quincena')) {\n push(out, {\n ...common,\n categoria: 'IGSS pendiente o inválido en nómina',\n severidad: 'advertencia',\n identificador_empleado: clean(r[5]) || clean(r[4]) || null,\n nombre_empleado: clean(r[2]),\n fuente_a: 'Nómina Q1/Q2',\n fuente_b: 'Sistema Propio IGSS / Contabilidad',\n diferencia_monto: null,\n descripcion: 'La nómina tiene IGSS pendiente, vacío o a corregir.',\n accion_sugerida: clean(r[6]) || 'Completar/corregir IGSS en nómina.',\n datos_originales: { fila_reporte: r, quincena: r[0], fila_nomina: r[1], proyecto: r[3], igss_nomina: r[4], afiliacion_correcta: r[5] }\n });\n}\n\nfor (const r of parseSection(src.nominaSinBambooRows, 'Empleados de nómina que NO aparecen en BambooHR')) {\n push(out, {\n ...common,\n categoria: 'Nómina sin BambooHR',\n severidad: 'revision',\n identificador_empleado: clean(r[0]) || null,\n nombre_empleado: clean(r[1]),\n fuente_a: 'Nómina Q1/Q2',\n fuente_b: 'BambooHR',\n diferencia_monto: null,\n descripcion: clean(r[6]) || 'Está en nómina, pero no se encontró en BambooHR.',\n accion_sugerida: 'Validar si el empleado debe crearse, reactivarse o corregirse en BambooHR.',\n datos_originales: { fila_reporte: r, quincenas: r[2], proyectos: r[3], esta_en_bamboo: r[4], fecha_ingreso: r[5] }\n });\n}\n\nfor (const r of parseSection(src.montosRows, 'Diferencias reales de monto')) {\n push(out, {\n ...common,\n categoria: 'Diferencia de monto afecto',\n severidad: 'revision',\n identificador_empleado: clean(r[0]),\n nombre_empleado: clean(r[1]),\n fuente_a: 'Nómina Q1 + Q2 + ajustes',\n fuente_b: 'Planilla Consolidada mensual',\n diferencia_monto: n(r[5]),\n descripcion: `Diferencia entre monto afecto de nómina y Planilla Consolidada: ${clean(r[5])}.`,\n accion_sugerida: clean(r[7]) || 'Revisar diferencia entre nómina combinada y PLANILLA MENSUAL.',\n datos_originales: { fila_reporte: r, proyecto: r[2], monto_nomina: r[3], monto_planilla: r[4], detalle: r[6] }\n });\n}\n\nfor (const r of parseSection(src.montosRows, 'Casos no comparados')) {\n push(out, {\n ...common,\n categoria: 'Caso no comparado',\n severidad: 'revision',\n identificador_empleado: clean(r[1]) || null,\n nombre_empleado: clean(r[2]),\n fuente_a: 'Nómina / Ajustes',\n fuente_b: 'Planilla Consolidada mensual',\n diferencia_monto: null,\n descripcion: clean(r[5]) || clean(r[0]) || 'Caso no comparado automáticamente.',\n accion_sugerida: 'Revisar afiliación y relación entre nómina, ajustes y Planilla Consolidada.',\n datos_originales: { fila_reporte: r, estado: r[0], monto_nomina_ajuste: r[3], monto_planilla: r[4] }\n });\n}\n\nreturn out;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
19056,
1920
],
"id": "d411fcba-0919-4b90-aa65-38a43d7284be",
"name": "Preparar hallazgos Supabase",
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "=https://dbit.digitalcompass.agency/rest/v1/auditoria_cruces_hallazgos",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Authorization",
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
19296,
1920
],
"id": "f1af589a-31f1-40d4-8133-c2e0f3e0bd07",
"name": "Supabase - Insertar hallazgos",
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "function clean(v){ return String(v ?? '').replace(/\\s+/g,' ').trim(); }\nfunction n(v){ const x = Number(v || 0); return Number.isFinite(x) ? x : null; }\nconst base = $input.first().json;\nconst s = (base.source || {}).summary || {};\nconst files = (base.source || {}).files || {};\nconst common = { id_ejecucion: base.id_ejecucion, pais_codigo: 'GT' };\nconst mapping = [\n { key:'q1', rol:'Nómina Q1', registros:s.q1Rows },\n { key:'q2', rol:'Nómina Q2', registros:s.q2Rows },\n { key:'sistema', rol:'Sistema Propio IGSS', registros:s.empleados },\n { key:'consolidado', rol:'Planilla Consolidada', registros:s.planilla },\n];\nconst out = [];\nfor (const m of mapping) {\n const name = clean(files[m.key]);\n if (!name) continue;\n out.push({ json: {\n ...common,\n rol_archivo: m.rol,\n nombre_archivo: name,\n tipo_archivo: name.toLowerCase().endsWith('.csv') ? 'csv' : 'excel',\n registros_detectados: n(m.registros),\n monto_total: null,\n datos_originales: { key: m.key, summary: s }\n }});\n}\nreturn out;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
19072,
2304
],
"id": "b1900419-b8f6-4908-b1fe-433775196b28",
"name": "Preparar archivos Supabase",
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "=https://dbit.digitalcompass.agency/rest/v1/auditoria_cruces_archivos",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Authorization",
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
19296,
2368
],
"id": "bec47dac-56d9-4607-af87-da34ad9da828",
"name": "Supabase - Insertar archivos procesados",
"onError": "continueRegularOutput"
},
{
"parameters": {
"url": "https://glm.bamboohr.com/api/v1/employees?fields=fullName5,displayName,firstName,middleName,lastName,hireDate,originalHireDate,status,employeeNumber,location,country,workEmail&page%5Blimit%5D=2500",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "json"
}
},
"pagination": {
"pagination": {
"parameters": {
"parameters": [
{
"name": "page[after]",
"value": "={{ $response.body.meta.page.nextCursor }}"
}
]
},
"paginationCompleteWhen": "other",
"completeExpression": "={{ !$response.body?.meta?.page?.nextCursor }}",
"limitPagesFetched": true,
"requestInterval": 500
}
},
"timeout": 300000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
14432,
2384
],
"id": "c1fad1e0-9d86-42c5-b718-443e8e47d8d2",
"name": "Consultar empleados BambooHR",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"credentials": {
"httpBasicAuth": {
"id": "7VrpNZ2jBLmiJ35q",
"name": "BambooHR GLM Full Access"
}
}
},
{
"parameters": {
"jsCode": "const init = $items('Inicializar memoria')[0].json;\nconst inputItems = $input.all();\n\nfunction clean(value) {\n return String(value ?? '').replace(/\\s+/g, ' ').trim();\n}\nfunction normalizeName(value) {\n return clean(value)\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/[^A-Z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\nfunction normalizeDate(value) {\n const raw = clean(value);\n if (!raw) return '';\n const match = raw.match(/^(\\d{4}-\\d{2}-\\d{2})/);\n return match ? match[1] : raw;\n}\nfunction unwrapResponse(item) {\n const payload = item?.json || {};\n // Permite que el nodo siga funcionando aunque en el futuro se active\n // \"Include Response Headers and Status\".\n if (payload.body && typeof payload.body === 'object') return payload.body;\n return payload;\n}\n\nconst responses = inputItems.map(unwrapResponse);\nconst rows = [];\n\nfor (const response of responses) {\n if (Array.isArray(response.data)) {\n rows.push(...response.data);\n } else if (Array.isArray(response.employees)) {\n rows.push(...response.employees);\n } else if (Array.isArray(response)) {\n rows.push(...response);\n }\n}\n\nconst totals = responses\n .map(response => Number(response?.meta?.total || 0))\n .filter(value => Number.isFinite(value) && value > 0);\nconst totalReported = totals.length ? Math.max(...totals) : rows.length;\n\n// Comprobación independiente de integridad antes de normalizar nombres.\nconst rawByEmployee = new Map();\nrows.forEach((row, index) => {\n const key =\n clean(row.employeeId || row.id) ||\n clean(row.employeeNumber) ||\n `${normalizeName(row.fullName5 || row.displayName || [row.firstName, row.middleName, row.lastName].filter(Boolean).join(' '))}|${normalizeDate(row.hireDate || row.originalHireDate)}` ||\n `fila-${index}`;\n\n if (!rawByEmployee.has(key)) rawByEmployee.set(key, row);\n});\n\nconst uniqueRawRows = [...rawByEmployee.values()];\n\nif (totalReported > uniqueRawRows.length) {\n throw new Error(\n `BambooHR devolvió ${uniqueRawRows.length} empleados únicos de ${totalReported}. ` +\n `La paginación no terminó correctamente y el cruce no sería completo.`\n );\n}\n\nconst normalized = [];\nfor (const row of uniqueRawRows) {\n const nombre = clean(\n row.fullName5 ||\n row.displayName ||\n row.fullName1 ||\n [row.firstName, row.middleName, row.lastName].map(clean).filter(Boolean).join(' ')\n );\n if (!nombre) continue;\n\n normalized.push({\n employee_id: clean(row.employeeId || row.id),\n employee_number: clean(row.employeeNumber),\n nombre_bamboo: nombre,\n nombre_norm: normalizeName(nombre),\n fecha_ingreso: normalizeDate(row.hireDate || row.originalHireDate),\n estado_bamboo: clean(row.status),\n ubicacion: clean(row.location),\n pais: clean(row.country),\n correo: clean(row.workEmail)\n });\n}\n\nconst byEmployee = new Map();\nfor (const row of normalized) {\n const key =\n row.employee_id ||\n row.employee_number ||\n `${row.nombre_norm}|${row.fecha_ingreso}`;\n\n if (!byEmployee.has(key)) byEmployee.set(key, row);\n}\n\nconst bamboo = [...byEmployee.values()];\n\nif (!bamboo.length) {\n throw new Error(\n 'BambooHR no devolvió empleados legibles. Revisa la credencial y los permisos para nombres y fecha de ingreso.'\n );\n}\n\nconst data = $getWorkflowStaticData('global');\ndata.gtSeguridadSocial = data.gtSeguridadSocial || {};\ndata.gtSeguridadSocial[init.runId] = data.gtSeguridadSocial[init.runId] || {};\ndata.gtSeguridadSocial[init.runId].bamboo = bamboo;\n\nreturn [{\n json: {\n runId: init.runId,\n empleadosBamboo: bamboo.length,\n totalReportadoBamboo: totalReported,\n paginasBamboo: responses.length,\n empleadosRecibidosBamboo: rows.length,\n empleadosUnicosBamboo: uniqueRawRows.length\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
14672,
2384
],
"id": "b1be220f-2711-4ce2-bf7a-95fbaf1ecaa2",
"name": "Guardar BambooHR en memoria"
},
{
"parameters": {
"method": "POST",
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleDriveOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'msoto@gomezleemarketing.com' }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
17856,
2384
],
"id": "a22df7c7-e68a-44ec-abe4-5b8b6506b943",
"name": "Compartir con Mati Soto - Editor",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleDriveOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'iherrera@gomezleemarketing.com' }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
18096,
2384
],
"id": "cad3778c-b256-4765-a6d0-de70b6afb584",
"name": "Compartir con Iveth Herrera - Editor",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleDriveOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'vparamo@gomezleemarketing.com' }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
18336,
2384
],
"id": "d2f69a4b-1848-4a34-9d40-881a64e368e2",
"name": "Compartir con Viviana Paramo - Editor",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleDriveOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'asrodriguez@gomezleemarketing.com' }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
18576,
2384
],
"id": "7c040257-30fe-487c-9405-02d78c812de9",
"name": "Compartir con Ada Rodríguez - Editor",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Crear Google Sheet Reporte').item.json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleDriveOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: 'mgomez@gomezleemarketing.com' }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
18816,
2384
],
"id": "a4d89434-807d-4f08-9172-8b07fc65737a",
"name": "Compartir con Máximo Gomez - Editor",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"credentials": {
"googleDriveOAuth2Api": {
"id": "g23xdGLZRzBGqKgH",
"name": "Isaac - Google Drive"
}
},
"onError": "continueRegularOutput"
}
],
"pinData": {},
"connections": {
"On form submission": {
"main": [
[
{
"node": "Inicializar memoria",
"type": "main",
"index": 0
}
]
]
},
"Inicializar memoria": {
"main": [
[
{
"node": "Preparar Nómina 1Q",
"type": "main",
"index": 0
},
{
"node": "Preparar auditoría inicial Supabase",
"type": "main",
"index": 0
}
]
]
},
"Preparar Nómina 1Q": {
"main": [
[
{
"node": "Actualizar staging - Nómina 1Q",
"type": "main",
"index": 0
}
]
]
},
"Actualizar staging - Nómina 1Q": {
"main": [
[
{
"node": "Esperar conversión - Nómina 1Q",
"type": "main",
"index": 0
}
]
]
},
"Listar hojas - Nómina 1Q": {
"main": [
[
{
"node": "Resolver hoja Nómina 1Q",
"type": "main",
"index": 0
}
]
]
},
"Resolver hoja Nómina 1Q": {
"main": [
[
{
"node": "Leer Nómina 1Q",
"type": "main",
"index": 0
}
]
]
},
"Leer Nómina 1Q": {
"main": [
[
{
"node": "Guardar Nómina 1Q en memoria",
"type": "main",
"index": 0
}
]
]
},
"Guardar Nómina 1Q en memoria": {
"main": [
[
{
"node": "Preparar Nómina 2Q",
"type": "main",
"index": 0
}
]
]
},
"Preparar Nómina 2Q": {
"main": [
[
{
"node": "Actualizar staging - Nómina 2Q",
"type": "main",
"index": 0
}
]
]
},
"Actualizar staging - Nómina 2Q": {
"main": [
[
{
"node": "Esperar conversión - Nómina 2Q",
"type": "main",
"index": 0
}
]
]
},
"Listar hojas - Nómina 2Q": {
"main": [
[
{
"node": "Resolver hoja Nómina 2Q",
"type": "main",
"index": 0
}
]
]
},
"Resolver hoja Nómina 2Q": {
"main": [
[
{
"node": "Leer Nómina 2Q",
"type": "main",
"index": 0
}
]
]
},
"Leer Nómina 2Q": {
"main": [
[
{
"node": "Guardar Nómina 2Q en memoria",
"type": "main",
"index": 0
}
]
]
},
"Guardar Nómina 2Q en memoria": {
"main": [
[
{
"node": "Preparar Sistema Propio",
"type": "main",
"index": 0
}
]
]
},
"Preparar Sistema Propio": {
"main": [
[
{
"node": "Actualizar staging - Sistema Propio",
"type": "main",
"index": 0
}
]
]
},
"Actualizar staging - Sistema Propio": {
"main": [
[
{
"node": "Esperar conversión - Sistema Propio",
"type": "main",
"index": 0
}
]
]
},
"Listar hojas - Sistema Propio": {
"main": [
[
{
"node": "Resolver hoja EMPLEADOS",
"type": "main",
"index": 0
}
]
]
},
"Resolver hoja EMPLEADOS": {
"main": [
[
{
"node": "Leer EMPLEADOS",
"type": "main",
"index": 0
}
]
]
},
"Leer EMPLEADOS": {
"main": [
[
{
"node": "Guardar EMPLEADOS en memoria",
"type": "main",
"index": 0
}
]
]
},
"Guardar EMPLEADOS en memoria": {
"main": [
[
{
"node": "Consultar empleados BambooHR",
"type": "main",
"index": 0
}
]
]
},
"Preparar Planilla Consolidada": {
"main": [
[
{
"node": "Actualizar staging - Consolidada",
"type": "main",
"index": 0
}
]
]
},
"Actualizar staging - Consolidada": {
"main": [
[
{
"node": "Esperar conversión - Consolidada",
"type": "main",
"index": 0
}
]
]
},
"Listar hojas - Consolidada": {
"main": [
[
{
"node": "Resolver hoja PLANILLA MENSUAL",
"type": "main",
"index": 0
}
]
]
},
"Resolver hoja PLANILLA MENSUAL": {
"main": [
[
{
"node": "Leer PLANILLA MENSUAL",
"type": "main",
"index": 0
}
]
]
},
"Leer PLANILLA MENSUAL": {
"main": [
[
{
"node": "Guardar PLANILLA MENSUAL en memoria",
"type": "main",
"index": 0
}
]
]
},
"Guardar PLANILLA MENSUAL en memoria": {
"main": [
[
{
"node": "Construir reporte producción",
"type": "main",
"index": 0
}
]
]
},
"Construir reporte producción": {
"main": [
[
{
"node": "Crear Google Sheet Reporte",
"type": "main",
"index": 0
}
]
]
},
"Crear Google Sheet Reporte": {
"main": [
[
{
"node": "Escribir reporte",
"type": "main",
"index": 0
}
]
]
},
"Escribir reporte": {
"main": [
[
{
"node": "Formatear reporte",
"type": "main",
"index": 0
}
]
]
},
"Formatear reporte": {
"main": [
[
{
"node": "Compartir con Isaac",
"type": "main",
"index": 0
}
]
]
},
"Compartir con Isaac": {
"main": [
[
{
"node": "Compartir con Yanelly editor",
"type": "main",
"index": 0
}
]
]
},
"Compartir con Yanelly editor": {
"main": [
[
{
"node": "Compartir con Mati Soto - Editor",
"type": "main",
"index": 0
}
]
]
},
"Esperar conversión - Nómina 1Q": {
"main": [
[
{
"node": "Listar hojas - Nómina 1Q",
"type": "main",
"index": 0
}
]
]
},
"Esperar conversión - Nómina 2Q": {
"main": [
[
{
"node": "Listar hojas - Nómina 2Q",
"type": "main",
"index": 0
}
]
]
},
"Esperar conversión - Sistema Propio": {
"main": [
[
{
"node": "Listar hojas - Sistema Propio",
"type": "main",
"index": 0
}
]
]
},
"Esperar conversión - Consolidada": {
"main": [
[
{
"node": "Listar hojas - Consolidada",
"type": "main",
"index": 0
}
]
]
},
"Preparar auditoría inicial Supabase": {
"main": [
[
{
"node": "Supabase - Crear auditoría inicial",
"type": "main",
"index": 0
}
]
]
},
"Preparar auditoría final Supabase": {
"main": [
[
{
"node": "Supabase - Actualizar auditoría final",
"type": "main",
"index": 0
},
{
"node": "Preparar hallazgos Supabase",
"type": "main",
"index": 0
},
{
"node": "Preparar archivos Supabase",
"type": "main",
"index": 0
}
]
]
},
"Preparar hallazgos Supabase": {
"main": [
[
{
"node": "Supabase - Insertar hallazgos",
"type": "main",
"index": 0
}
]
]
},
"Preparar archivos Supabase": {
"main": [
[
{
"node": "Supabase - Insertar archivos procesados",
"type": "main",
"index": 0
}
]
]
},
"Consultar empleados BambooHR": {
"main": [
[
{
"node": "Guardar BambooHR en memoria",
"type": "main",
"index": 0
}
]
]
},
"Guardar BambooHR en memoria": {
"main": [
[
{
"node": "Preparar Planilla Consolidada",
"type": "main",
"index": 0
}
]
]
},
"Compartir con Mati Soto - Editor": {
"main": [
[
{
"node": "Compartir con Iveth Herrera - Editor",
"type": "main",
"index": 0
}
]
]
},
"Compartir con Iveth Herrera - Editor": {
"main": [
[
{
"node": "Compartir con Viviana Paramo - Editor",
"type": "main",
"index": 0
}
]
]
},
"Compartir con Viviana Paramo - Editor": {
"main": [
[
{
"node": "Compartir con Ada Rodríguez - Editor",
"type": "main",
"index": 0
}
]
]
},
"Compartir con Ada Rodríguez - Editor": {
"main": [
[
{
"node": "Compartir con Máximo Gomez - Editor",
"type": "main",
"index": 0
}
]
]
},
"Compartir con Máximo Gomez - Editor": {
"main": [
[
{
"node": "Enviar correo",
"type": "main",
"index": 0
},
{
"node": "Preparar auditoría final Supabase",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": true,
"timeSavedMode": "fixed",
"errorWorkflow": "puF4LUczoSz3hcek",
"timezone": "America/Santo_Domingo",
"callerPolicy": "workflowsFromSameOwner"
},
"versionId": "da7519ba-a547-40b8-b673-c25408feba16",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
},
"id": "V73ZjH7QBjLndXQV",
"tags": []
}