1176 lines
124 KiB
JSON
1176 lines
124 KiB
JSON
{
|
||
"name": "Cruce de Seguridad Social - Costa Rica",
|
||
"nodes": [
|
||
{
|
||
"parameters": {
|
||
"formTitle": "Cruce Seguridad Social Costa Rica",
|
||
"formDescription": "Sube la nómina mensual de Costa Rica y la planilla oficial de la CCSS en PDF. El flujo cruza identidad, cédulas y montos; genera un Google Sheet, lo comparte y envía el resumen por correo.",
|
||
"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",
|
||
"placeholder": "2026",
|
||
"requiredField": true
|
||
},
|
||
{
|
||
"fieldLabel": "Nómina",
|
||
"fieldType": "file",
|
||
"multipleFiles": false,
|
||
"acceptFileTypes": ".xlsx,.xls",
|
||
"requiredField": true
|
||
},
|
||
{
|
||
"fieldLabel": "Planilla CCSS PDF",
|
||
"fieldType": "file",
|
||
"multipleFiles": false,
|
||
"acceptFileTypes": ".pdf",
|
||
"requiredField": true
|
||
}
|
||
]
|
||
},
|
||
"options": {
|
||
"ignoreBots": true
|
||
}
|
||
},
|
||
"type": "n8n-nodes-base.formTrigger",
|
||
"typeVersion": 2.3,
|
||
"position": [
|
||
912,
|
||
1376
|
||
],
|
||
"id": "a16fe0ae-ecbb-4b18-bb4d-38997fc83f9e",
|
||
"name": "On form submission",
|
||
"webhookId": "6dc6c5cd-1994-431b-b85a-1353a3346576"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const item = $input.first();\nconst now = new Date();\nconst pad = n => String(n).padStart(2, '0');\nconst mes = String(item.json['Mes a validar'] || '').replace(/\\D/g, '').padStart(2, '0');\nconst anioRaw = String(item.json['Año a validar'] || item.json['Ano a validar'] || '').trim();\nconst anio = Number(anioRaw.replace(/\\D/g, ''));\nif (!/^\\d{2}$/.test(mes) || Number(mes) < 1 || Number(mes) > 12) throw new Error('Selecciona un mes válido entre 01 y 12.');\nif (!/^\\d{4}$/.test(String(anio)) || anio < 2020 || anio > 2100) throw new Error('Escribe un año válido de cuatro dígitos, por ejemplo 2026.');\nconst meses = { '01':'Enero','02':'Febrero','03':'Marzo','04':'Abril','05':'Mayo','06':'Junio','07':'Julio','08':'Agosto','09':'Septiembre','10':'Octubre','11':'Noviembre','12':'Diciembre' };\nconst ts = `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;\nconst runId = `CR_CCSS_${ts}_${Math.random().toString(36).slice(2,8)}`;\nconst periodoLabel = `${meses[mes]} ${anio}`;\nconst fechaTitulo = `${pad(now.getDate())}-${pad(now.getMonth()+1)}-${now.getFullYear()}`;\nconst reportTitle = `Cruce Seguridad Social Costa Rica - ${periodoLabel} - ${fechaTitulo}`;\nreturn [{ json: { runId, mes, anio, periodoLabel, reportTitle, startedAt: now.toISOString() }, binary: item.binary }];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1152,
|
||
1376
|
||
],
|
||
"id": "6b74cc38-f3c5-4d8e-8aa7-fd411c52b753",
|
||
"name": "Inicializar ejecución"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const base = $items('Inicializar ejecución')[0];\nconst bin = base.binary || {};\nfunction ascii(s){return String(s||'').normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase();}\nconst candidates = Object.entries(bin).map(([key,file]) => {\n const name = String(file.fileName || key);\n const s = ascii(`${key} ${name} ${file.mimeType || ''}`);\n let score = 0;\n if (/\\.xlsx$|\\.xls$|spreadsheet|excel/.test(s)) score += 100;\n if (/nomina|n[oó]mina|field force|payroll/.test(s)) score += 100;\n if (/ccss.*pdf|planilla.*pdf|\\.pdf$/.test(s)) score -= 300;\n return {key,file,name,score};\n}).sort((a,b)=>b.score-a.score);\nif (!candidates.length || candidates[0].score < 100) throw new Error('No pude identificar el archivo de Nómina. Adjunta un archivo .xlsx o .xls en el campo Nómina.');\nconst chosen = candidates[0];\nreturn [{json:{runId:base.json.runId,mes:base.json.mes,anio:base.json.anio,periodoLabel:base.json.periodoLabel,reportTitle:base.json.reportTitle,fileName:chosen.name},binary:{data:chosen.file}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1392,
|
||
1136
|
||
],
|
||
"id": "d380541d-185a-486e-91d5-cb993a55026a",
|
||
"name": "Preparar Nómina"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"method": "POST",
|
||
"url": "https://sheets.googleapis.com/v4/spreadsheets",
|
||
"authentication": "predefinedCredentialType",
|
||
"nodeCredentialType": "googleSheetsOAuth2Api",
|
||
"sendBody": true,
|
||
"specifyBody": "json",
|
||
"jsonBody": "={{ JSON.stringify({ properties: { title: `TEMP CR Nómina - ${$('Inicializar ejecución').item.json.runId}` } }) }}",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.3,
|
||
"position": [
|
||
1632,
|
||
1136
|
||
],
|
||
"id": "9bc731aa-8b7c-4aea-b84a-160885731e20",
|
||
"name": "Crear staging Nómina",
|
||
"credentials": {
|
||
"googleSheetsOAuth2Api": {
|
||
"id": "K0hDZh3a85MpOHCs",
|
||
"name": "Google Sheets account 2"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const staging = $input.first().json || {};\nconst prep = $items('Preparar Nómina')[0];\nconst spreadsheetId = staging.spreadsheetId || staging.id;\nif (!spreadsheetId) throw new Error('No se pudo crear el archivo temporal para convertir la nómina.');\nreturn [{json:{...prep.json,spreadsheetId},binary:prep.binary}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1872,
|
||
1136
|
||
],
|
||
"id": "9307716f-e990-4139-9400-19ea81b16a20",
|
||
"name": "Reunir staging y Nómina"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"operation": "update",
|
||
"fileId": {
|
||
"__rl": true,
|
||
"value": "={{ $json.spreadsheetId }}",
|
||
"mode": "id"
|
||
},
|
||
"changeFileContent": true,
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.googleDrive",
|
||
"typeVersion": 3,
|
||
"position": [
|
||
2112,
|
||
1136
|
||
],
|
||
"id": "c45519cc-4254-40c0-9b2b-141a01785d65",
|
||
"name": "Actualizar staging Nómina",
|
||
"credentials": {
|
||
"googleDriveOAuth2Api": {
|
||
"id": "g23xdGLZRzBGqKgH",
|
||
"name": "Isaac - Google Drive"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {},
|
||
"type": "n8n-nodes-base.wait",
|
||
"typeVersion": 1.1,
|
||
"position": [
|
||
2352,
|
||
1136
|
||
],
|
||
"id": "63aec9da-2e25-48ac-bb30-2e61c02ab884",
|
||
"name": "Esperar conversión Nómina",
|
||
"webhookId": "9d908850-0944-4b6d-8858-12ef55951f9d"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('Crear staging Nómina').item.json.spreadsheetId }}?fields=spreadsheetId,sheets(properties(title,sheetId,hidden))",
|
||
"authentication": "predefinedCredentialType",
|
||
"nodeCredentialType": "googleSheetsOAuth2Api",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.3,
|
||
"position": [
|
||
2592,
|
||
1136
|
||
],
|
||
"id": "1d50a0b6-1bfa-49a7-aad8-88d578d48961",
|
||
"name": "Listar hojas Nómina",
|
||
"credentials": {
|
||
"googleSheetsOAuth2Api": {
|
||
"id": "K0hDZh3a85MpOHCs",
|
||
"name": "Google Sheets account 2"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const init = $items('Inicializar ejecución')[0].json;\nconst prep = $items('Preparar Nómina')[0].json;\nconst spreadsheetId = $json.spreadsheetId || $('Crear staging Nómina').item.json.spreadsheetId;\nconst sheets = Array.isArray($json.sheets) ? $json.sheets : [];\nfunction norm(s){return String(s||'').normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]/g,'');}\nlet chosen = sheets.find(s => norm(s?.properties?.title) === 'ccssins');\nif (!chosen) chosen = sheets.find(s => /ccss|ins/.test(norm(s?.properties?.title)) && !s?.properties?.hidden);\nif (!chosen) chosen = sheets.find(s => !s?.properties?.hidden);\nif (!chosen) throw new Error('No encontré una hoja legible dentro de la nómina. Se esperaba la hoja “CCSS, INS”.');\nconst title = String(chosen.properties.title);\nconst range = `'${title.replace(/'/g,\"''\")}'!A1:M5000`;\nreturn [{json:{runId:init.runId,mes:init.mes,anio:init.anio,periodoLabel:init.periodoLabel,reportTitle:init.reportTitle,spreadsheetId,sheetTitle:title,fileName:prep.fileName,rangeQuery:'ranges='+encodeURIComponent(range)}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
2832,
|
||
1136
|
||
],
|
||
"id": "61a7c4eb-c72c-4514-8b98-b76f40a572f9",
|
||
"name": "Preparar rango Nómina"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.spreadsheetId }}/values:batchGet?{{ $json.rangeQuery }}&majorDimension=ROWS&valueRenderOption=UNFORMATTED_VALUE&dateTimeRenderOption=SERIAL_NUMBER",
|
||
"authentication": "predefinedCredentialType",
|
||
"nodeCredentialType": "googleSheetsOAuth2Api",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.3,
|
||
"position": [
|
||
3072,
|
||
1136
|
||
],
|
||
"id": "cc7e5c03-f3bb-4f78-b755-1ba57e3f24d8",
|
||
"name": "Leer hoja Nómina",
|
||
"credentials": {
|
||
"googleSheetsOAuth2Api": {
|
||
"id": "K0hDZh3a85MpOHCs",
|
||
"name": "Google Sheets account 2"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const init=$items('Inicializar ejecución')[0].json;\nconst prep=$items('Preparar rango Nómina')[0].json;\nconst payload=$json.body&&typeof $json.body==='object'?$json.body:$json;\nconst values=payload?.valueRanges?.[0]?.values||payload?.values||[];\n\nfunction clean(value){\n return String(value??'').replace(/\\s+/g,' ').trim();\n}\nfunction ascii(value){\n return clean(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toUpperCase();\n}\nfunction money(value){\n if(value===null||value===undefined||value==='') return 0;\n if(typeof value==='number'){\n return Math.round((value+Math.sign(value||1)*1e-9)*100)/100;\n }\n let text=String(value).replace(/₡|CRC|COLONES|,/gi,'').replace(/\\s/g,'').trim();\n if(/^\\(.*\\)$/.test(text)) text='-'+text.slice(1,-1);\n const number=Number(text);\n return Number.isFinite(number)\n ?Math.round((number+Math.sign(number||1)*1e-9)*100)/100\n :0;\n}\nfunction normalizedNumberText(value){\n let text=clean(value);\n if(!text) return '';\n if(/^-?\\d+(\\.\\d+)?e[+-]?\\d+$/i.test(text)){\n const number=Number(text);\n if(Number.isFinite(number)){\n text=number.toLocaleString('fullwide',{useGrouping:false,maximumFractionDigits:0});\n }\n }\n if(/^\\d+\\.0+$/.test(text)) text=text.replace(/\\.0+$/,'');\n return text;\n}\nfunction normalizeId(value){\n const text=normalizedNumberText(value);\n if(!text) return '';\n const digits=text.replace(/\\D/g,'').replace(/^0+/,'');\n return digits||'0';\n}\nfunction displayId(value){\n return normalizedNumberText(value);\n}\nfunction findHeader(rows){\n for(let index=0;index<Math.min(rows.length,30);index++){\n const row=(rows[index]||[]).map(ascii);\n const joined=row.join('|');\n if(joined.includes('NOMBRE')&&joined.includes('VALOR')&&joined.includes('ID')){\n return index;\n }\n }\n return 0;\n}\n\nconst headerIndex=findHeader(values);\nconst headers=(values[headerIndex]||[]).map(ascii);\n\nfunction columnIndex(...patterns){\n for(const pattern of patterns){\n const index=headers.findIndex(value=>pattern.test(value));\n if(index>=0) return index;\n }\n return -1;\n}\n\nconst columns={\n novedad:columnIndex(/^NOVEDAD/),\n primary:columnIndex(/^ID$/,/CEDULA/),\n ccss:columnIndex(/ID CCSS/,/CEDULA CCSS/),\n nombre:columnIndex(/^NOMBRE$/),\n apellido1:columnIndex(/APELLIDO 1/,/^APELLIDO$/),\n apellido2:columnIndex(/APELLIDO 2/),\n monto:columnIndex(/^VALOR$/,/SALARIO CCSS/),\n estado:columnIndex(/^ESTADO$/),\n proyecto:columnIndex(/^CUENTA$/,/PROYECTO/)\n};\n\nconst hasCcssColumn=columns.ccss>=0;\nif(!hasCcssColumn){\n if(columns.apellido1<0) columns.apellido1=columns.nombre+1;\n if(columns.apellido2<0) columns.apellido2=columns.nombre+2;\n if(columns.estado<0) columns.estado=12;\n}\n\nif(\n columns.nombre<0||\n columns.monto<0||\n (columns.ccss<0&&columns.primary<0)\n){\n throw new Error(\n 'La hoja de nómina no tiene la estructura esperada. '+\n 'Debe contener ID, NOMBRE/APELLIDOS y VALOR.'\n );\n}\n\nconst recordsMap=new Map();\nconst identityByName=new Map();\nlet lastName='';\nlet lastOfficialId='';\nlet lastPrimaryId='';\nlet lastPrimaryDisplay='';\nlet lastCcssDisplay='';\n\nfor(let rowIndex=headerIndex+1;rowIndex<values.length;rowIndex++){\n const row=values[rowIndex]||[];\n const name=clean([\n row[columns.nombre],\n columns.apellido1>=0?row[columns.apellido1]:'',\n columns.apellido2>=0?row[columns.apellido2]:''\n ].filter(Boolean).join(' '));\n\n let officialRaw=hasCcssColumn?row[columns.ccss]:row[columns.primary];\n let primaryRaw=columns.primary>=0?row[columns.primary]:officialRaw;\n\n let officialId=normalizeId(officialRaw)||normalizeId(primaryRaw);\n let primaryId=normalizeId(primaryRaw);\n let primaryDisplay=displayId(primaryRaw);\n let ccssDisplay=hasCcssColumn?displayId(officialRaw):'';\n const nameKey=ascii(name);\n\n if(!officialId&&nameKey&&identityByName.has(nameKey)){\n const saved=identityByName.get(nameKey);\n officialId=saved.officialId;\n primaryId=saved.primaryId;\n primaryDisplay=saved.primaryDisplay;\n ccssDisplay=saved.ccssDisplay;\n }\n\n if(!officialId&&nameKey&&nameKey===lastName){\n officialId=lastOfficialId;\n primaryId=lastPrimaryId;\n primaryDisplay=lastPrimaryDisplay;\n ccssDisplay=lastCcssDisplay;\n }\n\n if(!name||!officialId) continue;\n\n identityByName.set(nameKey,{\n officialId,\n primaryId:primaryId||officialId,\n primaryDisplay,\n ccssDisplay\n });\n lastName=nameKey;\n lastOfficialId=officialId;\n lastPrimaryId=primaryId||officialId;\n lastPrimaryDisplay=primaryDisplay;\n lastCcssDisplay=ccssDisplay;\n\n if(/TOTAL|SUBTOTAL|VERIFICACION/.test(ascii(name))) continue;\n\n const amount=money(row[columns.monto]);\n const status=columns.estado>=0?clean(row[columns.estado]):'';\n const project=columns.proyecto>=0?clean(row[columns.proyecto]):'';\n const key=`${officialId}|${nameKey}`;\n\n if(!recordsMap.has(key)){\n recordsMap.set(key,{\n official_id:officialId,\n has_ccss_id:hasCcssColumn,\n primary_ids:new Set(),\n primary_displays:new Set(),\n ccss_displays:new Set(),\n nombres:new Set(),\n monto:0,\n proyectos:new Set(),\n filas:[],\n estado:''\n });\n }\n\n const record=recordsMap.get(key);\n if(primaryId) record.primary_ids.add(primaryId);\n if(primaryDisplay) record.primary_displays.add(primaryDisplay);\n if(ccssDisplay) record.ccss_displays.add(ccssDisplay);\n record.nombres.add(name);\n record.monto=Math.round((record.monto+amount+1e-9)*100)/100;\n if(project) record.proyectos.add(project);\n record.filas.push(rowIndex+1);\n if(status) record.estado=status;\n}\n\nconst records=[...recordsMap.values()].map(record=>{\n const names=[...record.nombres].sort(\n (a,b)=>b.split(' ').length-a.split(' ').length||b.length-a.length\n );\n const primaryDisplays=[...record.primary_displays];\n const ccssDisplays=[...record.ccss_displays];\n\n return {\n official_id:record.official_id,\n has_ccss_id:record.has_ccss_id,\n primary_ids:[...record.primary_ids],\n primary_display:primaryDisplays[0]||[...record.primary_ids][0]||'',\n ccss_display:record.has_ccss_id?(ccssDisplays[0]||''):'',\n official_display:record.has_ccss_id?(ccssDisplays[0]||''):'',\n nombre:names[0]||'',\n nombres:names,\n monto:Number(record.monto.toFixed(2)),\n proyectos:[...record.proyectos],\n filas:record.filas,\n estado:record.estado\n };\n}).filter(record=>record.nombre);\n\nif(!records.length){\n throw new Error('No se detectaron empleados válidos en la hoja de nómina.');\n}\n\nreturn [{json:{\n runId:init.runId,\n mes:init.mes,\n anio:init.anio,\n periodoLabel:init.periodoLabel,\n reportTitle:init.reportTitle,\n fileName:prep.fileName,\n spreadsheetId:prep.spreadsheetId,\n payrollRecords:records,\n payrollCount:records.length,\n payrollTotal:Number(records.reduce((sum,record)=>sum+record.monto,0).toFixed(2)),\n payrollHasCcssColumn:hasCcssColumn\n}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
3312,
|
||
1136
|
||
],
|
||
"id": "38e763fe-6eda-4f26-abe1-08e5e8f801a1",
|
||
"name": "Extraer Nómina"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const base=$items('Inicializar ejecución')[0];\nconst bin=base.binary||{};\nfunction ascii(s){return String(s||'').normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase();}\nconst candidates=Object.entries(bin).map(([key,file])=>{\n const name=String(file.fileName||key); const s=ascii(`${key} ${name} ${file.mimeType||''}`);\n let score=0; if(/\\.pdf$|application\\/pdf/.test(s))score+=100; if(/ccss|planilla|seguridad social/.test(s))score+=100; if(/nomina|xlsx|xls/.test(s))score-=200;\n return {key,file,name,score};\n}).sort((a,b)=>b.score-a.score);\nif(!candidates.length||candidates[0].score<100) throw new Error('No pude identificar la planilla CCSS en PDF.');\nconst chosen=candidates[0];\nreturn [{json:{runId:base.json.runId,mes:base.json.mes,anio:base.json.anio,periodoLabel:base.json.periodoLabel,reportTitle:base.json.reportTitle,fileName:chosen.name},binary:{data:chosen.file}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1392,
|
||
1376
|
||
],
|
||
"id": "bbc7c7e1-abfd-4eca-bb58-944bb60a9b68",
|
||
"name": "Preparar Planilla CCSS"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"operation": "pdf",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.extractFromFile",
|
||
"typeVersion": 1.1,
|
||
"position": [
|
||
1632,
|
||
1376
|
||
],
|
||
"id": "0e68067a-b7b5-4296-8ffc-c976303d6718",
|
||
"name": "Extraer texto Planilla CCSS"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const init=$items('Inicializar ejecución')[0].json;\nconst prep=$items('Preparar Planilla CCSS')[0].json;\nconst text=String($json.text||$json.data||$json.content||$json.body||'');\nif(!text.trim()) throw new Error('No se pudo extraer texto de la planilla CCSS PDF.');\n\nfunction clean(value){\n return String(value??'').replace(/\\s+/g,' ').trim();\n}\nfunction ascii(value){\n return clean(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toUpperCase();\n}\nfunction asciiPreservingLength(value){\n return String(value??'')\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n .toUpperCase();\n}\nfunction money(value){\n let text=String(value??'').replace(/₡|CRC|COLONES|,/gi,'').replace(/\\s/g,'').trim();\n if(!text) return 0;\n if(/^\\(.*\\)$/.test(text)) text='-'+text.slice(1,-1);\n const number=Number(text);\n return Number.isFinite(number)?Number(number.toFixed(2)):0;\n}\nfunction normalizeId(value){\n const digits=String(value??'').replace(/\\D/g,'').replace(/^0+/,'');\n return digits||'0';\n}\n\nconst MONTH_NAMES={\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 MONTH_ALIASES={\n '01':['ENERO'],'02':['FEBRERO'],'03':['MARZO'],'04':['ABRIL'],\n '05':['MAYO'],'06':['JUNIO'],'07':['JULIO'],'08':['AGOSTO'],\n '09':['SEPTIEMBRE','SETIEMBRE'],'10':['OCTUBRE'],\n '11':['NOVIEMBRE'],'12':['DICIEMBRE']\n};\n\nfunction splitPdfPages(rawText){\n const raw=String(rawText??'').replace(/\\r/g,'');\n const formFeedPages=raw.split(/\\f+/).filter(page=>page.trim());\n if(formFeedPages.length>1) return formFeedPages;\n\n const title='PLANILLA MENSUAL PARA EL MOVIMIENTO DE LOS TRABAJADORES';\n const upper=asciiPreservingLength(raw);\n const indexes=[];\n let offset=0;\n\n while(true){\n const index=upper.indexOf(title,offset);\n if(index<0) break;\n indexes.push(index);\n offset=index+title.length;\n }\n\n if(indexes.length>1){\n return indexes.map((start,index)=>\n raw.slice(start,indexes[index+1]??raw.length)\n ).filter(page=>page.trim());\n }\n\n return [raw];\n}\n\nfunction collectPeriod(rawText,fileName){\n const firstPage=ascii(splitPdfPages(rawText)[0]||rawText);\n const evidence=[];\n\n function add(month,year,score,source){\n const normalizedMonth=String(month??'').padStart(2,'0');\n const normalizedYear=Number(year);\n if(\n !MONTH_NAMES[normalizedMonth]||\n !Number.isInteger(normalizedYear)||\n normalizedYear<2020||\n normalizedYear>2100\n ) return;\n\n evidence.push({\n month:normalizedMonth,\n year:normalizedYear,\n score,\n source\n });\n }\n\n let start=firstPage.indexOf('PERIODO (3)');\n if(start<0) start=0;\n let end=firstPage.indexOf('SALARIOS Y MOVIMIENTOS PARA',start);\n if(end<0||end<=start) end=Math.min(firstPage.length,start+2200);\n const scope=firstPage.slice(start,end);\n\n for(const [month,aliases] of Object.entries(MONTH_ALIASES)){\n for(const alias of aliases){\n const regex=new RegExp(`\\\\b${alias}\\\\b`,'g');\n for(const match of scope.matchAll(regex)){\n const years=[...scope.matchAll(/\\b(20\\d{2})\\b/g)]\n .map(year=>({\n year:Number(year[1]),\n distance:Math.abs(year.index-match.index)\n }))\n .filter(year=>year.distance<=450)\n .sort((a,b)=>a.distance-b.distance);\n\n if(years.length){\n add(\n month,\n years[0].year,\n 320-Math.min(years[0].distance,200),\n 'encabezado'\n );\n }\n }\n }\n }\n\n const invoiceScope=firstPage.slice(0,Math.min(firstPage.length,5000));\n for(const match of invoiceScope.matchAll(/\\b\\d{4}(20\\d{2})(0[1-9]|1[0-2])\\d{5,}\\b/g)){\n add(match[2],Number(match[1]),280,'factura');\n }\n\n const fileScope=ascii(fileName||'');\n const fileYears=[...fileScope.matchAll(/\\b(20\\d{2})\\b/g)]\n .map(match=>Number(match[1]));\n\n for(const [month,aliases] of Object.entries(MONTH_ALIASES)){\n if(!aliases.some(alias=>new RegExp(`\\\\b${alias}\\\\b`).test(fileScope))) continue;\n for(const year of fileYears) add(month,year,160,'archivo');\n }\n\n for(const match of fileScope.matchAll(/\\b(20\\d{2})(0[1-9]|1[0-2])\\b/g)){\n add(match[2],Number(match[1]),150,'archivo compacto');\n }\n\n const grouped=new Map();\n for(const item of evidence){\n const key=`${item.month}|${item.year}`;\n if(!grouped.has(key)){\n grouped.set(key,{\n month:item.month,\n year:item.year,\n score:0,\n sources:[]\n });\n }\n const group=grouped.get(key);\n group.score+=item.score;\n group.sources.push(item.source);\n }\n\n return [...grouped.values()].sort((a,b)=>b.score-a.score)[0]||\n {month:'',year:null,score:0,sources:[]};\n}\n\nconst idRegex=/^\\d-\\d{8,10}$/;\nconst salaryRegex=/^(?:\\d{1,3}(?:,\\d{3})*|\\d+)?\\.\\d{2}$/;\nconst occupationRegex=/^\\d{4}$/;\n\nconst nonNames=new Set([\n 'C','SA','IN','EX','IC','JP','*S','*I','*A','SALARIO','CS',\n ...Object.values(MONTH_NAMES),'SETIEMBRE'\n]);\n\nconst nameBlockers=[\n 'PLANILLA MENSUAL','CAJA COSTARRICENSE','COSTARRICENSE DE SEGURO SOCIAL',\n 'SEGURO SOCIAL','CONSECUTIVO','DOCUMENTO EMITIDO','PATRONAL',\n 'NOMBRE DEL PATRONO','SOCIEDAD ANONIMA','GOMEZLEE','CODIGO ACTIVIDAD',\n 'DIRECCION DE CORRESPONDENCIA','TELEF','PERIODO','FACTURA',\n 'DATOS DE TRABAJADORES','TIPO /','APELLIDOS Y NOMBRES','OCUP',\n 'SALARIO GANADO','Nº.ORDEN','CAMBIO','OBSERVACIONES','TOTAL SALARIOS',\n 'FIRMA AUTORIZADA','AJUSTE AL MINIMO','LUGAR DE PAGO','OFI. CENTRALES',\n 'RP_PLANILLA','RP PLANILLA','PRE/WEB','NOMBRE COMERCIAL','FECHA NACIMIENTO',\n 'SALARIOS Y MOVIMIENTOS PARA','VALIDACION DE DOCUMENTOS',\n 'URBANIZACION LA PRIMAVERA','EN CASO QUE NECESITE','SALARIOS CAMB'\n];\n\nfunction isEmployeeName(line){\n const raw=clean(line);\n const normalized=ascii(raw);\n\n if(normalized.length<5||nonNames.has(normalized)) return false;\n if(idRegex.test(raw)||salaryRegex.test(raw)||/^\\d+$/.test(raw)) return false;\n if(nameBlockers.some(blocker=>normalized.includes(blocker))) return false;\n if(!/^[A-ZÁÉÍÓÚÜÑ'’.\\- ]+$/i.test(raw)) return false;\n\n return normalized.split(' ').filter(Boolean).length>=2;\n}\n\nfunction findConsecutiveRun(lines,startIndex,count,predicate){\n for(let index=startIndex;index<=lines.length-count;index++){\n let valid=true;\n for(let offset=0;offset<count;offset++){\n if(!predicate(lines[index+offset])){\n valid=false;\n break;\n }\n }\n if(valid) return index;\n }\n return -1;\n}\n\nfunction parsePage(rawPage,pageNumber){\n const lines=String(rawPage??'')\n .replace(/\\r/g,'')\n .split('\\n')\n .map(clean)\n .filter(Boolean);\n\n const idIndexes=[];\n for(let index=0;index<lines.length;index++){\n if(idRegex.test(lines[index])) idIndexes.push(index);\n }\n\n if(!idIndexes.length){\n throw new Error(`La página ${pageNumber} no contiene cédulas de empleados legibles.`);\n }\n\n const ids=idIndexes.map(index=>lines[index]);\n const count=ids.length;\n const lastIdIndex=idIndexes[idIndexes.length-1];\n\n const occupationStart=findConsecutiveRun(\n lines,\n lastIdIndex+1,\n count,\n line=>\n occupationRegex.test(line)&&\n !(Number(line)>=2020&&Number(line)<=2100)&&\n line!=='7414'\n );\n\n if(occupationStart<0){\n throw new Error(\n `No se pudo identificar la columna de ocupaciones de la página ${pageNumber}.`\n );\n }\n\n const names=lines\n .slice(lastIdIndex+1,occupationStart)\n .filter(isEmployeeName);\n\n if(names.length!==count){\n throw new Error(\n `La página ${pageNumber} contiene ${count} cédulas, pero `+\n `${names.length} nombres válidos.`\n );\n }\n\n const salaryStart=findConsecutiveRun(\n lines,\n occupationStart+count,\n count,\n line=>salaryRegex.test(line)\n );\n\n if(salaryStart<0){\n throw new Error(\n `No se pudo identificar la columna de salarios de la página ${pageNumber}.`\n );\n }\n\n const occupations=lines.slice(occupationStart,occupationStart+count);\n const salaries=lines.slice(salaryStart,salaryStart+count);\n const records=ids.map((idDisplay,index)=>({\n ccss_id:normalizeId(idDisplay),\n ccss_display:idDisplay,\n nombre:names[index],\n occupation:occupations[index],\n monto:money(salaries[index])\n }));\n\n const officialTotals=lines\n .slice(salaryStart+count)\n .filter(line=>salaryRegex.test(line))\n .map(money)\n .filter(value=>value>0);\n\n return {records,officialTotals};\n}\n\nconst pages=splitPdfPages(text);\nconst expectedPages=Number($json.numpages||0);\nif(expectedPages&&pages.length!==expectedPages){\n throw new Error(\n `El PDF informa ${expectedPages} páginas, pero el lector pudo separar ${pages.length}.`\n );\n}\n\nconst parsedRecords=[];\nconst officialTotals=[];\n\nfor(let index=0;index<pages.length;index++){\n const parsed=parsePage(pages[index],index+1);\n parsedRecords.push(...parsed.records);\n officialTotals.push(...parsed.officialTotals);\n}\n\nconst byId=new Map();\nfor(const record of parsedRecords){\n if(!record.ccss_id) continue;\n\n if(!byId.has(record.ccss_id)){\n byId.set(record.ccss_id,record);\n continue;\n }\n\n const existing=byId.get(record.ccss_id);\n if(\n Math.abs(existing.monto-record.monto)>0.01||\n ascii(existing.nombre)!==ascii(record.nombre)\n ){\n throw new Error(\n `La cédula ${record.ccss_display} aparece duplicada con datos diferentes.`\n );\n }\n}\n\nconst unique=[...byId.values()];\nif(!unique.length){\n throw new Error('No se detectaron trabajadores en la planilla CCSS.');\n}\n\nconst suspicious=unique.filter(record=>\n /SEGURO SOCIAL|GOMEZLEE|PLANILLA|TOTAL SALARIOS|PATRONAL/.test(ascii(record.nombre))\n);\nif(suspicious.length){\n throw new Error(\n 'La lectura de nombres de la CCSS quedó desalineada. '+\n 'El proceso fue detenido para evitar generar un reporte incorrecto.'\n );\n}\n\nconst detectedPeriod=collectPeriod(text,prep.fileName);\nconst expectedMonth=String(init.mes).padStart(2,'0');\nconst expectedYear=Number(init.anio);\n\nif(\n detectedPeriod.month!==expectedMonth||\n detectedPeriod.year!==expectedYear\n){\n const detectedLabel=detectedPeriod.month&&detectedPeriod.year\n ?`${MONTH_NAMES[detectedPeriod.month]} ${detectedPeriod.year}`\n :'un período no identificable';\n\n throw new Error(\n `La planilla CCSS corresponde a ${detectedLabel}, no a ${init.periodoLabel}. `+\n 'Revisa el mes, el año y el archivo seleccionados.'\n );\n}\n\nconst total=Number(\n unique.reduce((sum,record)=>sum+record.monto,0).toFixed(2)\n);\nconst officialTotal=officialTotals.length\n ?Math.max(...officialTotals.map(value=>Number(value)||0))\n :null;\n\nif(officialTotal!==null&&Math.abs(total-officialTotal)>0.01){\n throw new Error(\n `La lectura no concilia con el TOTAL SALARIOS oficial: `+\n `se extrajeron ₡${total.toFixed(2)} y el PDF muestra ₡${officialTotal.toFixed(2)}.`\n );\n}\n\nreturn [{json:{\n runId:init.runId,\n mes:init.mes,\n anio:init.anio,\n periodoLabel:init.periodoLabel,\n reportTitle:init.reportTitle,\n fileName:prep.fileName,\n ccssRecords:unique,\n ccssCount:unique.length,\n ccssTotal:total,\n ccssOfficialTotal:officialTotal,\n detectedPeriod:{\n month:detectedPeriod.month,\n year:detectedPeriod.year,\n sources:detectedPeriod.sources\n },\n parsedPages:pages.length\n}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1872,
|
||
1376
|
||
],
|
||
"id": "cb3d5b10-1bdf-48f2-9097-549c5cc9f82b",
|
||
"name": "Extraer Planilla CCSS"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"method": "POST",
|
||
"url": "https://glm.bamboohr.com/api/v1/reports/custom?format=JSON&onlyCurrent=false",
|
||
"authentication": "genericCredentialType",
|
||
"genericAuthType": "httpBasicAuth",
|
||
"sendHeaders": true,
|
||
"headerParameters": {
|
||
"parameters": [
|
||
{
|
||
"name": "Accept",
|
||
"value": "application/json"
|
||
},
|
||
{
|
||
"name": "Content-Type",
|
||
"value": "application/json"
|
||
}
|
||
]
|
||
},
|
||
"sendBody": true,
|
||
"specifyBody": "json",
|
||
"jsonBody": {
|
||
"title": "Información de BambooHR - Proyecto y Fecha de Ingreso",
|
||
"fields": [
|
||
"firstName",
|
||
"middleName",
|
||
"lastName",
|
||
"displayName",
|
||
"department",
|
||
"division",
|
||
"location",
|
||
"customPosicion-Cliente",
|
||
"hireDate",
|
||
"originalHireDate",
|
||
"status",
|
||
"employeeNumber"
|
||
]
|
||
},
|
||
"options": {
|
||
"response": {
|
||
"response": {
|
||
"responseFormat": "json"
|
||
}
|
||
},
|
||
"timeout": 300000
|
||
}
|
||
},
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.4,
|
||
"position": [
|
||
1392,
|
||
1616
|
||
],
|
||
"id": "7a5f9f84-588d-41ea-906d-d4717d5a30eb",
|
||
"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 ejecución')[0].json;\n\nfunction clean(value){\n return String(value ?? '').replace(/\\s+/g,' ').trim();\n}\nfunction norm(value){\n return clean(value)\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n .toUpperCase().replace(/[^A-Z0-9 ]/g,' ')\n .replace(/\\s+/g,' ').trim();\n}\nfunction date(value){\n const text=clean(value);\n if(!text) return '';\n const iso=text.match(/^(\\d{4}-\\d{2}-\\d{2})/);\n if(iso) return iso[1];\n const us=text.match(/^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{4})$/);\n if(us) return `${us[3]}-${String(us[1]).padStart(2,'0')}-${String(us[2]).padStart(2,'0')}`;\n return text;\n}\nfunction toArray(value){\n if(Array.isArray(value)) return value;\n if(value && typeof value==='object') return Object.values(value);\n return [];\n}\n\nconst inputs=$input.all().map(item=>{\n const json=item.json || {};\n return json.body && typeof json.body==='object' ? json.body : json;\n});\n\nconst rows=[];\nfor(const payload of inputs){\n const candidates=[\n payload?.employees,\n payload?.data,\n payload?.results,\n payload?.data?.employees,\n payload?.report?.employees,\n payload\n ];\n for(const candidate of candidates){\n const found=toArray(candidate);\n if(found.length && found.some(row=>row && typeof row==='object')){\n rows.push(...found);\n break;\n }\n }\n}\n\nconst map=new Map();\nfor(const row of rows){\n if(!row || typeof row!=='object') continue;\n\n const firstName=clean(row.firstName ?? row['First Name']);\n const middleName=clean(row.middleName ?? row['Middle Name']);\n const lastName=clean(row.lastName ?? row['Last Name']);\n const displayName=clean(row.displayName ?? row['Display Name']);\n const fullConstructed=clean([firstName,middleName,lastName].filter(Boolean).join(' '));\n const shortConstructed=clean([firstName,lastName].filter(Boolean).join(' '));\n\n const variants=[displayName,fullConstructed,shortConstructed]\n .filter(Boolean)\n .filter((value,index,array)=>array.findIndex(other=>norm(other)===norm(value))===index);\n\n if(!variants.length) continue;\n\n const preferred=variants.slice().sort((a,b)=>b.split(' ').length-a.split(' ').length || b.length-a.length)[0];\n const location=clean(row.location ?? row['Location']);\n const division=clean(row.division ?? row['Division']);\n const department=clean(row.department ?? row['Department']);\n const countrySignal=norm(`${location} ${division} ${department}`);\n const esCostaRica=/COSTA RICA|\\bCR\\b/.test(countrySignal);\n\n const rec={\n employee_id:clean(row.employeeId ?? row.id ?? row.employeeID),\n employee_number:clean(row.employeeNumber ?? row['Employee #'] ?? row['Employee Number']),\n nombre_bamboo:preferred,\n nombres_bamboo:variants,\n nombre_norm:norm(preferred),\n fecha_ingreso:date(row.hireDate ?? row.originalHireDate ?? row['Hire Date'] ?? row['Original Hire Date']),\n estado_bamboo:clean(row.status ?? row['Status']),\n ubicacion:location,\n division,\n departamento:department,\n posicion_cliente:clean(row['customPosicion-Cliente'] ?? row.customPosicionCliente),\n es_costa_rica:esCostaRica\n };\n\n const key=rec.employee_id || rec.employee_number || `${rec.nombre_norm}|${rec.fecha_ingreso}`;\n if(!map.has(key)){\n map.set(key,rec);\n }else{\n const current=map.get(key);\n current.nombres_bamboo=[...new Set([...current.nombres_bamboo,...rec.nombres_bamboo])];\n if(!current.nombre_bamboo || rec.nombre_bamboo.length>current.nombre_bamboo.length) current.nombre_bamboo=rec.nombre_bamboo;\n if(!current.fecha_ingreso) current.fecha_ingreso=rec.fecha_ingreso;\n if(!current.estado_bamboo) current.estado_bamboo=rec.estado_bamboo;\n current.es_costa_rica=current.es_costa_rica || rec.es_costa_rica;\n }\n}\n\nconst bambooRecords=[...map.values()];\nif(!bambooRecords.length){\n throw new Error('El reporte personalizado de BambooHR no devolvió empleados legibles.');\n}\n\nreturn [{json:{\n runId:init.runId,\n bambooRecords,\n empleadosBamboo:bambooRecords.length,\n empleadosCostaRica:bambooRecords.filter(row=>row.es_costa_rica).length,\n paginasBamboo:inputs.length\n}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1632,
|
||
1616
|
||
],
|
||
"id": "e2648e20-95a2-446a-af12-8db5aba548fb",
|
||
"name": "Normalizar empleados BambooHR"
|
||
},
|
||
{
|
||
"parameters": {},
|
||
"type": "n8n-nodes-base.merge",
|
||
"typeVersion": 3.2,
|
||
"position": [
|
||
4640,
|
||
1248
|
||
],
|
||
"id": "cdcc7bed-0ca9-4852-a86e-60557882ffa8",
|
||
"name": "Esperar Nómina y CCSS"
|
||
},
|
||
{
|
||
"parameters": {},
|
||
"type": "n8n-nodes-base.merge",
|
||
"typeVersion": 3.2,
|
||
"position": [
|
||
4880,
|
||
1376
|
||
],
|
||
"id": "732bf02e-14c9-4293-b0f7-5a0ff371f26f",
|
||
"name": "Esperar BambooHR y cruces"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const init=$items('Inicializar ejecución')[0].json;\nconst payrollOut=$items('Extraer Nómina')[0].json;\nconst ccssOut=$items('Extraer Planilla CCSS')[0].json;\nconst bambooOut=$items('Normalizar empleados BambooHR')[0].json;\n\nconst payroll=Array.isArray(payrollOut.payrollRecords)?payrollOut.payrollRecords:[];\nconst ccss=Array.isArray(ccssOut.ccssRecords)?ccssOut.ccssRecords:[];\nconst bamboo=Array.isArray(bambooOut.bambooRecords)?bambooOut.bambooRecords:[];\n\nconst BLANK=null;\nconst STOPWORDS=new Set(['DE','DEL','LA','LAS','LOS','Y','NOINDICAOTRO']);\nconst TOKEN_ALIAS={\n BREYNER:'BRAINER',\n BRAYNER:'BRAINER',\n SEGRUA:'SEGURA',\n SANDI:'SANDINO',\n JEREMIS:'JEREMY',\n JEREMYS:'JEREMY',\n JEREMI:'JEREMY',\n YEREMIS:'JEREMY',\n YEREMY:'JEREMY'\n};\n\nfunction clean(value){\n return String(value ?? '').replace(/\\s+/g,' ').trim();\n}\nfunction canon(value){\n return clean(value)\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n .toUpperCase().replace(/[^A-Z0-9 ]/g,' ')\n .replace(/\\s+/g,' ').trim();\n}\nfunction rawTokens(value){\n return canon(value).split(' ').filter(token=>token.length>1&&!STOPWORDS.has(token));\n}\nfunction tokens(value){\n return rawTokens(value).map(token=>TOKEN_ALIAS[token]||token);\n}\nfunction levenshtein(a,b){\n a=String(a); b=String(b);\n const n=b.length;\n let previous=Array.from({length:n+1},(_,index)=>index);\n for(let i=1;i<=a.length;i++){\n const current=[i];\n for(let j=1;j<=n;j++){\n current[j]=Math.min(\n current[j-1]+1,\n previous[j]+1,\n previous[j-1]+(a[i-1]===b[j-1]?0:1)\n );\n }\n previous=current;\n }\n return previous[n];\n}\nfunction adjacentTranspose(a,b){\n if(a.length!==b.length) return false;\n const diff=[];\n for(let i=0;i<a.length;i++) if(a[i]!==b[i]) diff.push(i);\n return diff.length===2 && diff[1]===diff[0]+1 &&\n a[diff[0]]===b[diff[1]] && a[diff[1]]===b[diff[0]];\n}\nfunction tokenSimilarity(a,b){\n if(a===b) return 1;\n if(a.length<3||b.length<3) return 0;\n if(adjacentTranspose(a,b)) return 0.94;\n if((a.startsWith(b)||b.startsWith(a)) && Math.abs(a.length-b.length)<=2) return 0.88;\n const distance=levenshtein(a,b);\n return 1-distance/Math.max(a.length,b.length);\n}\nfunction bestTokenMatch(aTokens,bTokens){\n const used=new Set();\n let matched=0;\n let exact=0;\n let totalScore=0;\n\n for(const source of aTokens){\n let bestIndex=-1;\n let bestScore=0;\n for(let index=0;index<bTokens.length;index++){\n if(used.has(index)) continue;\n const score=tokenSimilarity(source,bTokens[index]);\n if(score>bestScore){\n bestScore=score;\n bestIndex=index;\n }\n }\n if(bestIndex>=0 && bestScore>=0.68){\n used.add(bestIndex);\n matched++;\n totalScore+=bestScore;\n if(source===bTokens[bestIndex]) exact++;\n }\n }\n\n return {\n matched,\n exact,\n average:matched?totalScore/matched:0,\n unmatchedA:aTokens.length-matched,\n unmatchedB:bTokens.length-matched\n };\n}\nfunction nameScoreDetail(a,b){\n const A=tokens(a);\n const B=tokens(b);\n if(!A.length||!B.length) return {score:0,matched:0,exact:0};\n\n const match=bestTokenMatch(A,B);\n const coverage=match.matched/Math.max(A.length,B.length);\n const shorter=match.matched/Math.min(A.length,B.length);\n let score=coverage*68+shorter*22+match.average*10;\n\n if(match.matched===Math.min(A.length,B.length)){\n score=Math.max(score,92-Math.abs(A.length-B.length)*1.5);\n }\n if(canon(a)===canon(b)) score=100;\n\n return {...match,score:Math.min(100,score)};\n}\nfunction nameScore(a,b){\n return nameScoreDetail(a,b).score;\n}\nfunction materialNameDifference(a,b){\n const A=tokens(a);\n const B=tokens(b);\n if(!A.length||!B.length) return false;\n\n const fuzzy=bestTokenMatch(A,B);\n\n // No se reportan como diferencia los acentos, errores pequeños,\n // letras transpuestas, nombres adicionales ni apellidos abreviados.\n if(fuzzy.matched===Math.min(A.length,B.length)) return false;\n\n // Solo se reporta cuando ambos documentos conservan tokens\n // significativos sin emparejar; por ejemplo, apellidos realmente distintos.\n return (\n fuzzy.matched>=2&&\n fuzzy.unmatchedA>0&&\n fuzzy.unmatchedB>0\n );\n}\nfunction money(value){\n const number=Number(value)||0;\n return Math.round((number+Math.sign(number||1)*1e-9)*100)/100;\n}\nfunction comparableId(value){\n const digits=String(value||'').replace(/\\D/g,'').replace(/^0+/,'');\n return digits||'0';\n}\nfunction statusEs(value){\n const status=canon(value);\n if(status==='ACTIVE'||status==='ACTIVO') return 'Activo';\n if(status==='INACTIVE'||status==='INACTIVO') return 'Inactivo';\n return clean(value)||'No disponible';\n}\nfunction payrollPrimaryDisplay(record){\n return clean(\n record.primary_display||\n record.primary_ids?.[0]||\n ''\n );\n}\nfunction payrollCcssDisplay(record){\n return record.has_ccss_id\n ?clean(record.ccss_display||record.official_display||'')\n :'';\n}\nfunction sortByName(list,selector){\n return list.slice().sort((a,b)=>\n canon(selector(a)).localeCompare(canon(selector(b)),'es')\n );\n}\n\nconst bambooExact=new Map();\nconst bambooTokenIndex=new Map();\n\nfor(let index=0;index<bamboo.length;index++){\n const employee=bamboo[index];\n const variants=Array.isArray(employee.nombres_bamboo)&&employee.nombres_bamboo.length\n ? employee.nombres_bamboo\n : [employee.nombre_bamboo];\n\n for(const variant of variants.filter(Boolean)){\n const exactKey=canon(variant);\n if(exactKey){\n if(!bambooExact.has(exactKey)) bambooExact.set(exactKey,[]);\n bambooExact.get(exactKey).push(index);\n }\n for(const token of new Set(tokens(variant))){\n if(!bambooTokenIndex.has(token)) bambooTokenIndex.set(token,new Set());\n bambooTokenIndex.get(token).add(index);\n }\n }\n}\n\nfunction bambooCandidateScore(payrollName,employee){\n const variants=Array.isArray(employee.nombres_bamboo)&&employee.nombres_bamboo.length\n ? employee.nombres_bamboo\n : [employee.nombre_bamboo];\n let best={score:0,matched:0,exact:0,variant:employee.nombre_bamboo||''};\n for(const variant of variants.filter(Boolean)){\n const detail=nameScoreDetail(payrollName,variant);\n if(detail.score>best.score) best={...detail,variant};\n }\n return best;\n}\n\nfunction matchBamboo(name){\n const exactIndexes=bambooExact.get(canon(name))||[];\n if(exactIndexes.length===1) return bamboo[exactIndexes[0]];\n if(exactIndexes.length>1){\n const costa=exactIndexes.map(index=>bamboo[index]).filter(employee=>employee.es_costa_rica);\n if(costa.length===1) return costa[0];\n return null;\n }\n\n const hitCounts=new Map();\n for(const token of new Set(tokens(name))){\n for(const index of bambooTokenIndex.get(token)||[]){\n hitCounts.set(index,(hitCounts.get(index)||0)+1);\n }\n }\n\n let indexes=[...hitCounts.entries()]\n .filter(([,hits])=>hits>=2)\n .sort((a,b)=>b[1]-a[1])\n .map(([index])=>index);\n\n if(!indexes.length){\n indexes=[...hitCounts.entries()]\n .filter(([,hits])=>hits>=1)\n .sort((a,b)=>b[1]-a[1])\n .slice(0,160)\n .map(([index])=>index);\n }\n if(!indexes.length) return null;\n\n const ranked=indexes.map(index=>{\n const employee=bamboo[index];\n const detail=bambooCandidateScore(name,employee);\n const adjusted=detail.score+(employee.es_costa_rica?3:0);\n return {employee,detail,adjusted};\n }).sort((a,b)=>b.adjusted-a.adjusted||b.detail.score-a.detail.score);\n\n const best=ranked[0];\n const second=ranked[1];\n\n if(!best || best.detail.score<80 || best.detail.matched<2) return null;\n\n if(second){\n const sameStrength=Math.abs(best.adjusted-second.adjusted)<5;\n const bothCosta=Boolean(best.employee.es_costa_rica)===Boolean(second.employee.es_costa_rica);\n if(sameStrength && bothCosta && best.detail.score<96) return null;\n if(Math.abs(best.adjusted-second.adjusted)<1.5 && best.detail.score<99) return null;\n }\n\n return best.employee;\n}\n\nconst ccssById=new Map(ccss.map(record=>[record.ccss_id,record]));\nconst usedCcss=new Set();\nconst matched=[];\nconst soloNomina=[];\n\nfor(const payrollRecord of payroll){\n let ccssRecord=ccssById.get(payrollRecord.official_id)||null;\n let method=ccssRecord?'ID CCSS':'';\n\n if(ccssRecord && (usedCcss.has(ccssRecord.ccss_id) || nameScore(payrollRecord.nombre,ccssRecord.nombre)<50)){\n ccssRecord=null;\n method='';\n }\n\n if(!ccssRecord){\n const candidates=ccss\n .filter(record=>!usedCcss.has(record.ccss_id))\n .map(record=>({\n record,\n score:nameScore(payrollRecord.nombre,record.nombre),\n amountDifference:Math.abs(money(payrollRecord.monto)-money(record.monto))\n }))\n .sort((a,b)=>(b.score-a.score)||(a.amountDifference-b.amountDifference));\n\n const best=candidates[0];\n const second=candidates[1];\n if(\n best &&\n best.score>=82 &&\n best.amountDifference<=1 &&\n (!second || best.score-second.score>=6 || best.score>=96)\n ){\n ccssRecord=best.record;\n method='Nombre + monto';\n }\n }\n\n if(ccssRecord){\n usedCcss.add(ccssRecord.ccss_id);\n matched.push({p:payrollRecord,c:ccssRecord,method});\n }else{\n soloNomina.push(payrollRecord);\n }\n}\n\nconst soloCcss=ccss.filter(record=>!usedCcss.has(record.ccss_id));\nconst minimumSource=Math.min(payroll.length,ccss.length);\nconst matchRate=minimumSource?matched.length/minimumSource:0;\n\nif(minimumSource>=20&&matchRate<0.75){\n throw new Error(\n `El cruce solo logró emparejar ${matched.length} de ${minimumSource} personas `+\n `(${(matchRate*100).toFixed(1)}%). `+\n 'El proceso fue detenido porque la lectura de las fuentes parece desalineada.'\n );\n}\nconst cedulaDiffs=[];\nconst nameDiffs=[];\nconst amountDiffs=[];\n\nfor(const match of matched){\n const primary=match.p.primary_ids?.[0]||'';\n if(primary && comparableId(primary)!==comparableId(match.c.ccss_id)){\n cedulaDiffs.push({...match,primary});\n }\n if(materialNameDifference(match.p.nombre,match.c.nombre)){\n nameDiffs.push(match);\n }\n const difference=money(match.p.monto-match.c.monto);\n if(Math.abs(difference)>0.01){\n amountDiffs.push({...match,dif:difference});\n }\n}\n\nconst bambooMatches=new Map();\nfor(const payrollRecord of payroll){\n bambooMatches.set(payrollRecord,matchBamboo(payrollRecord.nombre));\n}\n\nconst soloNominaEnriched=soloNomina.map(p=>({p,b:bambooMatches.get(p)||null}));\nconst payrollWithoutBamboo=payroll\n .map(p=>({p,b:bambooMatches.get(p)||null}))\n .filter(item=>!item.b);\n\nconst totalPayroll=money(payroll.reduce((sum,record)=>sum+record.monto,0));\nconst totalCcss=money(ccss.reduce((sum,record)=>sum+record.monto,0));\nconst matchedPayroll=money(matched.reduce((sum,item)=>sum+item.p.monto,0));\nconst matchedCcss=money(matched.reduce((sum,item)=>sum+item.c.monto,0));\n\nconst soloNominaSorted=sortByName(soloNomina,record=>record.nombre);\nconst soloCcssSorted=sortByName(soloCcss,record=>record.nombre);\nconst cedulaDiffsSorted=sortByName(cedulaDiffs,item=>item.p.nombre);\nconst nameDiffsSorted=sortByName(nameDiffs,item=>item.p.nombre);\nconst amountDiffsSorted=sortByName(amountDiffs,item=>item.p.nombre);\n\nconst soloNominaEnrichedSorted=soloNominaSorted.map(p=>({\n p,\n b:bambooMatches.get(p)||null\n}));\n\nconst identityCount=\n soloNominaSorted.length+\n soloCcssSorted.length+\n cedulaDiffsSorted.length+\n nameDiffsSorted.length;\nconst amountCount=\n amountDiffsSorted.length+\n soloNominaSorted.length+\n soloCcssSorted.length;\n\nlet statusText='COMPLETADO · SIN DIFERENCIAS';\nlet statusDetail='No se detectaron diferencias de identidad, cédulas ni montos.';\nif(identityCount||amountCount){\n statusText='COMPLETADO · REVISAR HALLAZGOS';\n statusDetail='El reporte contiene diferencias que Administración debe revisar.';\n}\n\nconst identityRows=[];\nconst idMeta={\n titleRows:[1,2],\n sectionRows:[4],\n summaryRows:[5,6],\n headerRows:[],\n maxColumns:10\n};\n\nidentityRows.push([`GOMEZLEE MARKETING | Cruce de Seguridad Social Costa Rica - ${init.periodoLabel}`]);\nidentityRows.push([`Identidad, cédulas y presencia Nómina vs. CCSS · ${statusText}`]);\nidentityRows.push([]);\nidentityRows.push(['Resumen']);\nidentityRows.push([\n 'Personas en Nómina',payroll.length,\n 'Personas en CCSS',ccss.length,\n 'Coincidentes',matched.length,\n 'Solo Nómina',soloNomina.length,\n 'Solo CCSS',soloCcss.length\n]);\nidentityRows.push([\n 'Cédulas distintas',cedulaDiffs.length,\n 'Nombres/apellidos distintos',nameDiffs.length,\n 'Total diferencias',identityCount,\n BLANK,BLANK,BLANK,BLANK\n]);\n\nfunction addIdentitySection(title,headers,rows){\n if(!rows.length) return;\n identityRows.push([]);\n identityRows.push([title]);\n idMeta.sectionRows.push(identityRows.length);\n identityRows.push(headers);\n idMeta.headerRows.push(identityRows.length);\n identityRows.push(...rows);\n}\n\naddIdentitySection(\n 'SOLO EN NÓMINA',\n ['Tipo','Nombre en Nómina','Cédula principal','ID CCSS registrado en Nómina','Monto Nómina (CRC)','Activo/Inactivo BambooHR','Fecha de Ingreso','Nombre en BambooHR','Proyecto(s)','Observación'],\n soloNominaEnrichedSorted.map(({p,b})=>[\n 'Solo en Nómina',\n p.nombre,\n payrollPrimaryDisplay(p)||BLANK,\n payrollCcssDisplay(p)||BLANK,\n p.monto,\n b?statusEs(b.estado_bamboo):'No encontrado',\n b?.fecha_ingreso||BLANK,\n b?.nombre_bamboo||BLANK,\n (p.proyectos||[]).join(', ')||BLANK,\n 'No aparece en la planilla CCSS del período.'\n ])\n);\n\naddIdentitySection(\n 'SOLO EN PLANILLA CCSS',\n ['Tipo','Nombre en CCSS','Cédula CCSS','Monto CCSS (CRC)','Observación',BLANK,BLANK,BLANK,BLANK,BLANK],\n soloCcssSorted.map(c=>[\n 'Solo en CCSS',c.nombre,c.ccss_display,c.monto,\n 'No aparece en la nómina del período.',\n BLANK,BLANK,BLANK,BLANK,BLANK\n ])\n);\n\naddIdentitySection(\n 'CÉDULAS DISTINTAS',\n ['Tipo','Nombre en Nómina','Cédula principal Nómina','ID CCSS registrado en Nómina','Nombre en CCSS','Cédula CCSS','Monto Nómina (CRC)','Monto CCSS (CRC)','Proyecto(s)','Observación'],\n cedulaDiffsSorted.map(item=>[\n 'Cédula distinta',\n item.p.nombre,\n payrollPrimaryDisplay(item.p)||item.primary,\n payrollCcssDisplay(item.p)||BLANK,\n item.c.nombre,\n item.c.ccss_display,\n item.p.monto,\n item.c.monto,\n (item.p.proyectos||[]).join(', ')||BLANK,\n 'La cédula principal de nómina no coincide con la utilizada en la planilla CCSS.'\n ])\n);\n\naddIdentitySection(\n 'NOMBRE O APELLIDO DISTINTO',\n ['Tipo','Nombre en Nómina','Cédula/ID','Nombre en CCSS','Cédula CCSS','Monto Nómina (CRC)','Monto CCSS (CRC)','Método de cruce','Proyecto(s)','Observación'],\n nameDiffsSorted.map(item=>[\n 'Nombre/apellido distinto',\n item.p.nombre,\n payrollPrimaryDisplay(item.p)||item.p.official_id,\n item.c.nombre,\n item.c.ccss_display,\n item.p.monto,\n item.c.monto,\n item.method,\n (item.p.proyectos||[]).join(', ')||BLANK,\n 'La identidad coincide, pero existe una diferencia material de nombre o apellido.'\n ])\n);\n\nif(!identityCount){\n identityRows.push([]);\n identityRows.push(['RESULTADO']);\n idMeta.sectionRows.push(identityRows.length);\n identityRows.push(['Sin diferencias de identidad ni cédulas.']);\n}\n\nconst amountRows=[];\nconst amountMeta={\n titleRows:[1,2],\n sectionRows:[4,8],\n summaryRows:[5,6],\n headerRows:[9],\n maxColumns:10\n};\n\namountRows.push([`GOMEZLEE MARKETING | Cruce de Montos CCSS Costa Rica - ${init.periodoLabel}`]);\namountRows.push([`Comparación del salario sujeto a CCSS · ${statusText}`]);\namountRows.push([]);\namountRows.push(['Resumen']);\namountRows.push([\n 'Personas comparadas',matched.length,\n 'Diferencias por empleado',amountDiffs.length,\n 'Total Nómina (CRC)',totalPayroll,\n 'Total CCSS (CRC)',totalCcss,\n 'Diferencia total',money(totalPayroll-totalCcss)\n]);\namountRows.push([\n 'Total coincidente Nómina',matchedPayroll,\n 'Total coincidente CCSS',matchedCcss,\n 'Diferencia coincidente',money(matchedPayroll-matchedCcss),\n 'Casos solo en una fuente',soloNomina.length+soloCcss.length,\n BLANK,BLANK\n]);\namountRows.push([]);\namountRows.push(['DIFERENCIAS DE MONTOS']);\namountRows.push([\n 'Tipo','Nombre en Nómina','Cédula Nómina','Monto Nómina (CRC)',\n 'Nombre en CCSS','Cédula CCSS','Monto CCSS (CRC)',\n 'Diferencia (CRC)','Proyecto(s)','Observación'\n]);\n\nfor(const item of amountDiffsSorted){\n amountRows.push([\n 'Diferencia de monto',\n item.p.nombre,\n payrollPrimaryDisplay(item.p)||item.p.official_id,\n item.p.monto,\n item.c.nombre,\n item.c.ccss_display,\n item.c.monto,\n item.dif,\n (item.p.proyectos||[]).join(', ')||BLANK,\n 'Revisar el salario sujeto a CCSS.'\n ]);\n}\nfor(const p of soloNominaSorted){\n amountRows.push([\n 'Solo en Nómina',p.nombre,payrollPrimaryDisplay(p)||p.official_id,p.monto,\n BLANK,BLANK,0,p.monto,\n (p.proyectos||[]).join(', ')||BLANK,\n 'Diferencia explicada por ausencia en CCSS.'\n ]);\n}\nfor(const c of soloCcssSorted){\n amountRows.push([\n 'Solo en CCSS',BLANK,BLANK,0,c.nombre,c.ccss_display,c.monto,-c.monto,\n BLANK,'Diferencia explicada por ausencia en Nómina.'\n ]);\n}\nif(!amountCount){\n amountRows.push([\n 'Sin diferencias',BLANK,BLANK,0,BLANK,BLANK,0,0,BLANK,\n 'Los montos por empleado y los totales concilian.'\n ]);\n}\n\nconst noBambooRows=[];\nconst bambooMeta={\n titleRows:[1,2],\n sectionRows:[4],\n summaryRows:[],\n headerRows:[5],\n maxColumns:1\n};\n\nnoBambooRows.push([`GOMEZLEE MARKETING | Nómina sin BambooHR - ${init.periodoLabel}`]);\nnoBambooRows.push([\n `${payrollWithoutBamboo.length} empleado(s) de la Nómina sin una coincidencia suficientemente confiable en BambooHR.`\n]);\nnoBambooRows.push([]);\nnoBambooRows.push(['RESULTADO']);\nnoBambooRows.push(['Nombre en Nómina']);\nif(payrollWithoutBamboo.length){\n noBambooRows.push(...payrollWithoutBamboo.map(item=>[item.p.nombre]));\n}else{\n noBambooRows.push(['Sin diferencias']);\n}\n\nreturn [{json:{\n runId:init.runId,\n mes:init.mes,\n anio:init.anio,\n periodoLabel:init.periodoLabel,\n reportTitle:init.reportTitle,\n identityRows,\n amountRows,\n nominaSinBambooRows:noBambooRows,\n formatMeta:{identity:idMeta,amount:amountMeta,bamboo:bambooMeta},\n diferenciasIdentidad:{soloNomina:soloNominaEnriched,soloCcss,cedulaDiffs,nameDiffs},\n diferenciasMontos:{amountDiffs,soloNomina,soloCcss},\n summary:{\n statusText,\n statusDetail,\n payrollCount:payroll.length,\n ccssCount:ccss.length,\n matchedCount:matched.length,\n soloNomina:soloNomina.length,\n soloCcss:soloCcss.length,\n cedulaDifferences:cedulaDiffs.length,\n nameDifferences:nameDiffs.length,\n identityDifferences:identityCount,\n amountDifferences:amountCount,\n totalPayroll,\n totalCcss,\n totalDifference:money(totalPayroll-totalCcss),\n matchedPayroll,\n matchedCcss,\n nominaSinBamboo:payrollWithoutBamboo.length,\n bambooCount:bamboo.length,\n files:{nomina:payrollOut.fileName,ccss:ccssOut.fileName},\n stagingSpreadsheetId:payrollOut.spreadsheetId\n }\n}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
5120,
|
||
1376
|
||
],
|
||
"id": "75f06abd-c6ae-4424-8cb1-1f31b4ed66c8",
|
||
"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: $('Construir reporte producción').item.json.reportTitle }, sheets: [{ properties: { title: 'Cruce de Identidad' } }, { properties: { title: 'Cruce de Montos' } }, { properties: { title: 'Nómina sin BambooHR' } }] }) }}",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.3,
|
||
"position": [
|
||
5360,
|
||
1376
|
||
],
|
||
"id": "9e46c1d4-b148-40d9-8bdf-21a460e3aa54",
|
||
"name": "Crear Google Sheet Reporte",
|
||
"credentials": {
|
||
"googleSheetsOAuth2Api": {
|
||
"id": "K0hDZh3a85MpOHCs",
|
||
"name": "Google Sheets account 2"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {
|
||
"method": "POST",
|
||
"url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.spreadsheetId }}/values:batchUpdate",
|
||
"authentication": "predefinedCredentialType",
|
||
"nodeCredentialType": "googleSheetsOAuth2Api",
|
||
"sendBody": true,
|
||
"specifyBody": "json",
|
||
"jsonBody": "={{ JSON.stringify({ valueInputOption: 'RAW', data: [{ range: 'Cruce de Identidad!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.identityRows }, { range: 'Cruce de Montos!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.amountRows }, { range: 'Nómina sin BambooHR!A1', majorDimension: 'ROWS', values: $('Construir reporte producción').item.json.nominaSinBambooRows }] }) }}",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.3,
|
||
"position": [
|
||
5600,
|
||
1376
|
||
],
|
||
"id": "e65051cf-c498-4e17-843f-2670a33b3963",
|
||
"name": "Escribir reporte",
|
||
"credentials": {
|
||
"googleSheetsOAuth2Api": {
|
||
"id": "K0hDZh3a85MpOHCs",
|
||
"name": "Google Sheets account 2"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const created=$items('Crear Google Sheet Reporte')[0].json;\nconst report=$items('Construir reporte producción')[0].json;\n\nconst ids={};\nfor(const sheet of (created.sheets||[])){\n ids[sheet.properties.title]=sheet.properties.sheetId;\n}\n\nconst C={\n blue:{red:79/255,green:117/255,blue:139/255},\n blue2:{red:91/255,green:127/255,blue:149/255},\n label:{red:238/255,green:246/255,blue:232/255},\n section:{red:214/255,green:232/255,blue:244/255},\n gray:{red:245/255,green:245/255,blue:245/255},\n text:{red:74/255,green:74/255,blue:74/255},\n white:{red:1,green:1,blue:1},\n border:{red:190/255,green:198/255,blue:204/255}\n};\n\nconst requests=[];\nconst borders={\n top:{style:'SOLID',color:C.border},\n bottom:{style:'SOLID',color:C.border},\n left:{style:'SOLID',color:C.border},\n right:{style:'SOLID',color:C.border}\n};\n\nfunction repeat(sheetId,startRow,endRow,startColumn,endColumn,format,fields='userEnteredFormat'){\n if(endRow<=startRow||endColumn<=startColumn) return;\n requests.push({\n repeatCell:{\n range:{\n sheetId,\n startRowIndex:startRow,\n endRowIndex:endRow,\n startColumnIndex:startColumn,\n endColumnIndex:endColumn\n },\n cell:{userEnteredFormat:format},\n fields\n }\n });\n}\nfunction setRowHeight(sheetId,startRow,endRow,pixelSize){\n if(endRow<=startRow) return;\n requests.push({\n updateDimensionProperties:{\n range:{\n sheetId,\n dimension:'ROWS',\n startIndex:startRow,\n endIndex:endRow\n },\n properties:{pixelSize},\n fields:'pixelSize'\n }\n });\n}\nfunction setColumnWidth(sheetId,column,pixelSize){\n requests.push({\n updateDimensionProperties:{\n range:{\n sheetId,\n dimension:'COLUMNS',\n startIndex:column,\n endIndex:column+1\n },\n properties:{pixelSize},\n fields:'pixelSize'\n }\n });\n}\nfunction autoResizeRows(sheetId,rowCount){\n requests.push({\n autoResizeDimensions:{\n dimensions:{\n sheetId,\n dimension:'ROWS',\n startIndex:0,\n endIndex:rowCount\n }\n }\n });\n}\nfunction mergeRow(sheetId,rowIndex,columnCount){\n if(columnCount<=1) return;\n requests.push({\n mergeCells:{\n range:{\n sheetId,\n startRowIndex:rowIndex,\n endRowIndex:rowIndex+1,\n startColumnIndex:0,\n endColumnIndex:columnCount\n },\n mergeType:'MERGE_ALL'\n }\n });\n}\nfunction blankRow(row){\n return !Array.isArray(row)||\n row.every(value=>\n value===null||\n value===undefined||\n String(value)===''\n );\n}\nfunction normalized(value){\n return String(value??'')\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n .toUpperCase();\n}\nfunction tableEnd(rows,headerRowOneBased){\n let index=headerRowOneBased;\n while(index<rows.length&&!blankRow(rows[index])) index++;\n return index;\n}\nfunction isMoneyHeader(value){\n return /MONTO|DIFERENCIA/.test(normalized(value));\n}\nfunction isCenteredHeader(value){\n return /CEDULA|ID CCSS|FECHA|ACTIVO|INACTIVO|MONTO|DIFERENCIA|BAMBOO/.test(\n normalized(value)\n );\n}\nfunction isIdentifierHeader(value){\n const text=normalized(value);\n return /CEDULA|ID CCSS|CEDULA\\/ID/.test(text);\n}\nfunction summaryValueIsMoney(sheetTitle,label){\n if(sheetTitle!=='Cruce de Montos') return false;\n const text=normalized(label);\n if(/PERSONAS|CASOS|EMPLEADO/.test(text)) return false;\n return /TOTAL|DIFERENCIA|MONTO/.test(text);\n}\n\nfunction styleSheet(title,rows,meta){\n const sheetId=ids[title];\n if(sheetId===undefined) return;\n\n const rowCount=Math.max(rows.length,6);\n const columnCount=Math.max(meta.maxColumns||1,1);\n const specialHeights=[];\n\n requests.push({\n updateSheetProperties:{\n properties:{\n sheetId,\n gridProperties:{\n hideGridlines:true,\n frozenRowCount:2\n }\n },\n fields:'gridProperties.hideGridlines,gridProperties.frozenRowCount'\n }\n });\n\n repeat(\n sheetId,0,rowCount,0,columnCount,\n {\n backgroundColor:C.white,\n textFormat:{\n fontFamily:'Arial',\n fontSize:10,\n foregroundColor:C.text\n },\n verticalAlignment:'MIDDLE',\n wrapStrategy:'WRAP'\n }\n );\n\n if(title==='Cruce de Identidad'){\n [190,300,190,210,300,190,175,175,285,430]\n .forEach((width,index)=>setColumnWidth(sheetId,index,width));\n }else if(title==='Cruce de Montos'){\n [190,300,190,180,300,190,180,180,285,430]\n .forEach((width,index)=>setColumnWidth(sheetId,index,width));\n }else if(title==='Nómina sin BambooHR'){\n setColumnWidth(sheetId,0,580);\n }\n\n // Cuerpo de cada tabla: bordes, alternancia y alineación.\n for(const headerRow of meta.headerRows||[]){\n const headerIndex=headerRow-1;\n const endIndex=tableEnd(rows,headerRow);\n const headers=rows[headerIndex]||[];\n\n for(let rowIndex=headerIndex+1;rowIndex<endIndex;rowIndex++){\n repeat(\n sheetId,rowIndex,rowIndex+1,0,columnCount,\n {\n backgroundColor:(rowIndex-headerIndex)%2===0?C.gray:C.white,\n textFormat:{\n fontFamily:'Arial',\n fontSize:10,\n foregroundColor:C.text\n },\n verticalAlignment:'MIDDLE',\n wrapStrategy:'WRAP',\n borders\n }\n );\n }\n\n for(let column=0;column<columnCount;column++){\n if(isIdentifierHeader(headers[column])){\n repeat(\n sheetId,headerIndex+1,endIndex,column,column+1,\n {\n numberFormat:{type:'TEXT'},\n horizontalAlignment:'CENTER'\n },\n 'userEnteredFormat.numberFormat,userEnteredFormat.horizontalAlignment'\n );\n }else if(isMoneyHeader(headers[column])){\n repeat(\n sheetId,headerIndex+1,endIndex,column,column+1,\n {\n numberFormat:{type:'CURRENCY',pattern:'₡#,##0.00'},\n horizontalAlignment:'CENTER'\n },\n 'userEnteredFormat.numberFormat,userEnteredFormat.horizontalAlignment'\n );\n }else if(isCenteredHeader(headers[column])){\n repeat(\n sheetId,headerIndex+1,endIndex,column,column+1,\n {horizontalAlignment:'CENTER'},\n 'userEnteredFormat.horizontalAlignment'\n );\n }\n }\n\n // Se aplica de último para que ninguna columna pierda el azul del encabezado.\n repeat(\n sheetId,headerIndex,headerIndex+1,0,columnCount,\n {\n backgroundColor:C.blue,\n textFormat:{\n fontFamily:'Arial',\n fontSize:10,\n bold:true,\n foregroundColor:C.white\n },\n horizontalAlignment:'CENTER',\n verticalAlignment:'MIDDLE',\n wrapStrategy:'WRAP',\n borders\n }\n );\n specialHeights.push([headerIndex,headerIndex+1,54]);\n }\n\n // Resúmenes cuadriculados.\n for(const summaryRow of meta.summaryRows||[]){\n const rowIndex=summaryRow-1;\n const values=rows[rowIndex]||[];\n\n repeat(\n sheetId,rowIndex,rowIndex+1,0,columnCount,\n {\n backgroundColor:C.white,\n textFormat:{\n fontFamily:'Arial',\n fontSize:10,\n foregroundColor:C.text\n },\n verticalAlignment:'MIDDLE',\n wrapStrategy:'WRAP',\n borders\n }\n );\n\n for(let column=0;column<columnCount;column+=2){\n const label=values[column];\n if(label===null||label===undefined||String(label)==='') continue;\n\n repeat(\n sheetId,rowIndex,rowIndex+1,column,column+1,\n {\n backgroundColor:C.label,\n textFormat:{\n fontFamily:'Arial',\n fontSize:10,\n bold:true,\n foregroundColor:C.blue\n },\n horizontalAlignment:'LEFT',\n verticalAlignment:'MIDDLE',\n wrapStrategy:'WRAP',\n borders\n }\n );\n\n if(column+1<columnCount){\n repeat(\n sheetId,rowIndex,rowIndex+1,column+1,column+2,\n {\n backgroundColor:C.white,\n textFormat:{\n fontFamily:'Arial',\n fontSize:10,\n bold:true,\n foregroundColor:C.text\n },\n horizontalAlignment:'CENTER',\n verticalAlignment:'MIDDLE',\n wrapStrategy:'WRAP',\n borders\n }\n );\n\n if(summaryValueIsMoney(title,label)){\n repeat(\n sheetId,rowIndex,rowIndex+1,column+1,column+2,\n {\n numberFormat:{type:'CURRENCY',pattern:'₡#,##0.00'},\n horizontalAlignment:'CENTER'\n },\n 'userEnteredFormat.numberFormat,userEnteredFormat.horizontalAlignment'\n );\n }\n }\n }\n\n specialHeights.push([rowIndex,rowIndex+1,38]);\n }\n\n // Secciones.\n for(const sectionRow of meta.sectionRows||[]){\n const rowIndex=sectionRow-1;\n mergeRow(sheetId,rowIndex,columnCount);\n repeat(\n sheetId,rowIndex,rowIndex+1,0,columnCount,\n {\n backgroundColor:C.section,\n textFormat:{\n fontFamily:'Arial',\n fontSize:10,\n bold:true,\n foregroundColor:C.blue\n },\n horizontalAlignment:'LEFT',\n verticalAlignment:'MIDDLE',\n wrapStrategy:'WRAP',\n borders\n }\n );\n specialHeights.push([rowIndex,rowIndex+1,32]);\n }\n\n // Títulos.\n for(const titleRow of meta.titleRows||[]){\n const rowIndex=titleRow-1;\n mergeRow(sheetId,rowIndex,columnCount);\n repeat(\n sheetId,rowIndex,rowIndex+1,0,columnCount,\n {\n backgroundColor:titleRow===1?C.blue:C.blue2,\n textFormat:{\n fontFamily:'Arial',\n fontSize:titleRow===1?13:10,\n bold:titleRow===1,\n italic:titleRow===2,\n foregroundColor:C.white\n },\n horizontalAlignment:'CENTER',\n verticalAlignment:'MIDDLE',\n wrapStrategy:'WRAP'\n }\n );\n specialHeights.push([\n rowIndex,\n rowIndex+1,\n titleRow===1?44:64\n ]);\n }\n\n // Primero se autoajustan todas las filas de contenido según los anchos finales.\n autoResizeRows(sheetId,rowCount);\n\n // Luego se fijan alturas mínimas para títulos, secciones y encabezados.\n for(const [start,end,height] of specialHeights){\n setRowHeight(sheetId,start,end,height);\n }\n\n if(title==='Nómina sin BambooHR'){\n repeat(\n sheetId,5,rowCount,0,1,\n {\n horizontalAlignment:'LEFT',\n verticalAlignment:'MIDDLE'\n },\n 'userEnteredFormat.horizontalAlignment,userEnteredFormat.verticalAlignment'\n );\n }\n}\n\nstyleSheet(\n 'Cruce de Identidad',\n report.identityRows,\n report.formatMeta.identity\n);\nstyleSheet(\n 'Cruce de Montos',\n report.amountRows,\n report.formatMeta.amount\n);\nstyleSheet(\n 'Nómina sin BambooHR',\n report.nominaSinBambooRows,\n report.formatMeta.bamboo\n);\n\nreturn [{json:{\n formatBody:{requests},\n spreadsheetId:created.spreadsheetId,\n spreadsheetUrl:\n created.spreadsheetUrl||\n `https://docs.google.com/spreadsheets/d/${created.spreadsheetId}/edit`\n}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
5840,
|
||
1376
|
||
],
|
||
"id": "b3edaec0-b6f0-48e1-bbd1-0361c62680af",
|
||
"name": "Preparar formato reporte"
|
||
},
|
||
{
|
||
"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": "={{ JSON.stringify($json.formatBody) }}",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.3,
|
||
"position": [
|
||
6080,
|
||
1376
|
||
],
|
||
"id": "b2cc32e6-bd43-4935-8771-1f88e7b32503",
|
||
"name": "Formatear reporte",
|
||
"credentials": {
|
||
"googleSheetsOAuth2Api": {
|
||
"id": "K0hDZh3a85MpOHCs",
|
||
"name": "Google Sheets account 2"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const DESTINATARIOS_TO='iaracena@gomezleemarketing.com, ymadera@gomezleemarketing.com, msoto@gomezleemarketing.com, iherrera@gomezleemarketing.com, mgomez@gomezleemarketing.com, vparamo@gomezleemarketing.com, asrodriguez@gomezleemarketing.com';\nconst emails=[...new Set((DESTINATARIOS_TO.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/gi)||[]).map(x=>x.toLowerCase()))];\nif(!emails.length)throw new Error('No hay destinatarios configurados.');\nconst sheet=$items('Crear Google Sheet Reporte')[0].json;\nreturn [{json:{toEmails:emails,allEmails:emails,sendTo:emails.join(', '),spreadsheetId:sheet.spreadsheetId,spreadsheetUrl:sheet.spreadsheetUrl||`https://docs.google.com/spreadsheets/d/${sheet.spreadsheetId}/edit`,permissionRole:'writer'}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
7280,
|
||
1392
|
||
],
|
||
"id": "6529d3ea-f207-4284-93e7-e81d412640e4",
|
||
"name": "CONFIG - Destinatarios correo y permisos"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const c=$input.first().json;return c.allEmails.map(email=>({json:{emailAddress:email,spreadsheetId:c.spreadsheetId,spreadsheetUrl:c.spreadsheetUrl,permissionRole:'writer'}}));"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
7520,
|
||
1392
|
||
],
|
||
"id": "9ea32f6e-67c7-40bb-9702-60a81855286c",
|
||
"name": "Expandir permisos destinatarios"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"method": "POST",
|
||
"url": "=https://www.googleapis.com/drive/v3/files/{{ $json.spreadsheetId }}/permissions?sendNotificationEmail=false&supportsAllDrives=true",
|
||
"authentication": "predefinedCredentialType",
|
||
"nodeCredentialType": "googleDriveOAuth2Api",
|
||
"sendBody": true,
|
||
"specifyBody": "json",
|
||
"jsonBody": "={{ JSON.stringify({ role: 'writer', type: 'user', emailAddress: $json.emailAddress }) }}",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.3,
|
||
"position": [
|
||
7760,
|
||
1392
|
||
],
|
||
"id": "b00b2624-a547-4437-9a11-49a79dfe82c2",
|
||
"name": "Compartir reporte como editor",
|
||
"credentials": {
|
||
"googleDriveOAuth2Api": {
|
||
"id": "g23xdGLZRzBGqKgH",
|
||
"name": "Isaac - Google Drive"
|
||
}
|
||
},
|
||
"continueOnFail": true
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const cfg=$items('CONFIG - Destinatarios correo y permisos')[0].json;\nconst r=$items('Construir reporte producción')[0].json;\nconst s=r.summary||{};\nconst nf=n=>Number(n||0).toLocaleString('es-CR');\nconst money=n=>'₡'+Number(n||0).toLocaleString('es-CR',{minimumFractionDigits:2,maximumFractionDigits:2});\nconst has=Number(s.identityDifferences||0)>0||Number(s.amountDifferences||0)>0;\nconst statusBg=has?'#FFF8E1':'#E8F5E9';\nconst statusColor=has?'#9A6700':'#166534';\nconst html=`<!doctype html><html><body style=\"margin:0;padding:0;background:#F5F5F5;font-family:Arial,Helvetica,sans-serif;color:#4A4A4A;\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"background:#F5F5F5;padding:28px 12px;\"><tr><td align=\"center\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"max-width:680px;background:#FFFFFF;border:1px solid #D0D0D0;\"><tr><td style=\"height:6px;background:#6CC24A;font-size:0;line-height:0;\"> </td></tr><tr><td style=\"padding:30px 34px 12px;\"><img src=\"data:image/png;base64,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\" alt=\"GomezLee Marketing\" width=\"280\" style=\"display:block;max-width:100%;height:auto;border:0;\"></td></tr><tr><td style=\"padding:14px 34px 0;\"><div style=\"font-size:11px;font-weight:700;letter-spacing:1.4px;color:#6CC24A;\">SEGURIDAD SOCIAL · COSTA RICA</div><h1 style=\"margin:8px 0 6px;font-size:26px;line-height:1.2;color:#4F758B;\">Cruce Nómina vs. CCSS</h1><p style=\"margin:0;font-size:15px;color:#6B8FA3;\">${r.periodoLabel}</p></td></tr><tr><td style=\"padding:22px 34px 0;\"><div style=\"background:${statusBg};border-left:5px solid #6CC24A;padding:14px 16px;color:${statusColor};font-size:14px;font-weight:700;\">${s.statusText}<div style=\"font-size:12px;font-weight:400;margin-top:5px;color:#4A4A4A;\">${s.statusDetail}</div></div></td></tr><tr><td style=\"padding:22px 34px 0;\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;font-size:13px;\"><tr><td style=\"background:#4F758B;color:#FFFFFF;font-weight:700;padding:10px;border:1px solid #D0D0D0;\">Métrica</td><td style=\"background:#4F758B;color:#FFFFFF;font-weight:700;padding:10px;border:1px solid #D0D0D0;text-align:right;\">Resultado</td></tr><tr><td style=\"padding:9px 10px;border:1px solid #D0D0D0;\">Personas en Nómina</td><td style=\"padding:9px 10px;border:1px solid #D0D0D0;text-align:right;font-weight:700;color:#4F758B;\">${nf(s.payrollCount)}</td></tr><tr><td style=\"padding:9px 10px;border:1px solid #D0D0D0;background:#F5F5F5;\">Personas en CCSS</td><td style=\"padding:9px 10px;border:1px solid #D0D0D0;background:#F5F5F5;text-align:right;font-weight:700;color:#4F758B;\">${nf(s.ccssCount)}</td></tr><tr><td style=\"padding:9px 10px;border:1px solid #D0D0D0;\">Diferencias de identidad/cédula</td><td style=\"padding:9px 10px;border:1px solid #D0D0D0;text-align:right;font-weight:700;color:#4F758B;\">${nf(s.identityDifferences)}</td></tr><tr><td style=\"padding:9px 10px;border:1px solid #D0D0D0;background:#F5F5F5;\">Casos con diferencia de monto o presencia</td><td style=\"padding:9px 10px;border:1px solid #D0D0D0;background:#F5F5F5;text-align:right;font-weight:700;color:#4F758B;\">${nf(s.amountDifferences)}</td></tr><tr><td style=\"padding:9px 10px;border:1px solid #D0D0D0;\">Total Nómina</td><td style=\"padding:9px 10px;border:1px solid #D0D0D0;text-align:right;font-weight:700;color:#4F758B;\">${money(s.totalPayroll)}</td></tr><tr><td style=\"padding:9px 10px;border:1px solid #D0D0D0;background:#F5F5F5;\">Total CCSS</td><td style=\"padding:9px 10px;border:1px solid #D0D0D0;background:#F5F5F5;text-align:right;font-weight:700;color:#4F758B;\">${money(s.totalCcss)}</td></tr><tr><td style=\"padding:9px 10px;border:1px solid #D0D0D0;\">Nómina sin BambooHR</td><td style=\"padding:9px 10px;border:1px solid #D0D0D0;text-align:right;font-weight:700;color:#4F758B;\">${nf(s.nominaSinBamboo)}</td></tr></table></td></tr><tr><td style=\"padding:26px 34px;\"><a href=\"${cfg.spreadsheetUrl}\" style=\"display:inline-block;background:#6CC24A;color:#FFFFFF;text-decoration:none;padding:13px 22px;font-size:14px;font-weight:700;\">Abrir reporte en Google Sheets</a><div style=\"margin-top:22px;font-size:11px;line-height:1.6;color:#6B8FA3;\">Archivos procesados:<br>Nómina: ${s.files?.nomina||''}<br>Planilla CCSS: ${s.files?.ccss||''}</div></td></tr><tr><td style=\"border-top:1px solid #D0D0D0;padding:14px 34px;font-size:10px;color:#6B8FA3;\">GOMEZLEE MARKETING · Uso interno</td></tr></table></td></tr></table></body></html>`;\nreturn [{json:{sendTo:cfg.sendTo,spreadsheetId:cfg.spreadsheetId,spreadsheetUrl:cfg.spreadsheetUrl,subject:`${has?'[REVISAR]':'[OK]'} Cruce Seguridad Social Costa Rica - ${r.periodoLabel}`,html}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
8000,
|
||
1392
|
||
],
|
||
"id": "602d8caa-fa4c-4062-90b9-9ee5b9ea0559",
|
||
"name": "Preparar correo HTML GLM"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"sendTo": "={{ $json.sendTo }}",
|
||
"subject": "={{ $json.subject }}",
|
||
"message": "={{ $json.html }}",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.gmail",
|
||
"typeVersion": 2.1,
|
||
"position": [
|
||
8240,
|
||
1392
|
||
],
|
||
"id": "4c1605b6-7ba8-4e41-aba3-774585862e3e",
|
||
"name": "Enviar correo resumen",
|
||
"webhookId": "1bdf4396-6002-4925-9a9d-74ae0e398f75",
|
||
"executeOnce": true,
|
||
"credentials": {
|
||
"gmailOAuth2": {
|
||
"id": "UDcO1FLJqA453V2D",
|
||
"name": "Gmail account 3"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const init=$items('Inicializar ejecución')[0].json;\nconst workflowName=typeof $workflow!=='undefined'&&$workflow.name?$workflow.name:'Cruce de Seguridad Social - Costa Rica';\nconst workflowId=typeof $workflow!=='undefined'&&$workflow.id?String($workflow.id):'';\nconst executionId=typeof $execution!=='undefined'&&$execution.id?String($execution.id):'';\nreturn [{json:{id_ejecucion:init.runId,nombre_flujo:workflowName,id_flujo_n8n:workflowId||null,id_ejecucion_n8n:executionId||null,pais_codigo:'CR',pais_nombre:'Costa Rica',entidad_seguridad_social:'CCSS',mes:Number(init.mes),anio:Number(init.anio),periodo:init.periodoLabel,estado:'procesando',resultado:'Cruce iniciado',fecha_inicio:new Date().toISOString(),moneda:'CRC',archivos_procesados:{},conteos_fuente:{},metricas:{etapa:'inicio',origen:'n8n',formulario:'Cruce Seguridad Social Costa Rica'},resumen_completo:{reportTitle:init.reportTitle,runId:init.runId}}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
1392,
|
||
1856
|
||
],
|
||
"id": "11b571c2-32ae-4498-a5c6-cd5a10f7b81b",
|
||
"name": "Preparar auditoría inicial Supabase",
|
||
"continueOnFail": true
|
||
},
|
||
{
|
||
"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.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOjE2NDE3NjkyMDAsCiAgICAiZXhwIjoxNzk5NTM1NjAwCn0.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||
},
|
||
{
|
||
"name": "Authorization",
|
||
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOjE2NDE3NjkyMDAsCiAgICAiZXhwIjoxNzk5NTM1NjAwCn0.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": [
|
||
1632,
|
||
1856
|
||
],
|
||
"id": "e9859a9a-7cdb-4aa9-bf54-a7155a26a74a",
|
||
"name": "Registrar auditoría inicial Supabase",
|
||
"continueOnFail": true
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const r=$items('Construir reporte producción')[0].json;const s=r.summary||{};const cfg=$items('CONFIG - Destinatarios correo y permisos')[0].json;\nconst workflowName=typeof $workflow!=='undefined'&&$workflow.name?$workflow.name:'Cruce de Seguridad Social - Costa Rica';\nconst workflowId=typeof $workflow!=='undefined'&&$workflow.id?String($workflow.id):'';const executionId=typeof $execution!=='undefined'&&$execution.id?String($execution.id):'';\nconst num=v=>Number((Number(v)||0).toFixed(2));\nreturn [{json:{id_ejecucion:r.runId,nombre_flujo:workflowName,id_flujo_n8n:workflowId||null,id_ejecucion_n8n:executionId||null,pais_codigo:'CR',pais_nombre:'Costa Rica',entidad_seguridad_social:'CCSS',mes:Number(r.mes),anio:Number(r.anio),periodo:r.periodoLabel,estado:'exitoso',resultado:s.statusText||'Completado',fecha_fin:new Date().toISOString(),url_reporte:cfg.spreadsheetUrl,id_google_sheet:cfg.spreadsheetId,destinatarios_correo:cfg.allEmails,moneda:'CRC',empleados_nomina:Number(s.payrollCount||0),empleados_entidad:Number(s.ccssCount||0),empleados_coincidentes:Number(s.matchedCount||0),solo_nomina:Number(s.soloNomina||0),solo_entidad:Number(s.soloCcss||0),diferencias_total:Number(s.identityDifferences||0)+Number(s.amountDifferences||0),diferencias_monetarias:Number(s.amountDifferences||0),diferencias_identidad:Number(s.identityDifferences||0),alertas_documentales:Number(s.cedulaDifferences||0)+Number(s.nameDifferences||0),registros_duplicados:0,alertas_periodo:0,total_nomina:num(s.totalPayroll),total_entidad:num(s.totalCcss),diferencia_monto:num(s.totalDifference),minutos_estimados_manual:90,minutos_automatizacion:7,minutos_ahorrados:83,archivos_procesados:s.files||{},conteos_fuente:{empleados_nomina:s.payrollCount,empleados_ccss:s.ccssCount,coincidentes:s.matchedCount,solo_nomina:s.soloNomina,solo_ccss:s.soloCcss},metricas:{cedulas_distintas:s.cedulaDifferences,nombres_distintos:s.nameDifferences,nomina_sin_bamboohr:s.nominaSinBamboo,empleados_bamboo:s.bambooCount,total_nomina:num(s.totalPayroll),total_ccss:num(s.totalCcss)},resumen_completo:{summary:s,diferenciasIdentidad:r.diferenciasIdentidad,diferenciasMontos:r.diferenciasMontos,reporte:{spreadsheetId:cfg.spreadsheetId,spreadsheetUrl:cfg.spreadsheetUrl}},mensaje_error:null,nodo_error:null}}];"
|
||
},
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
8480,
|
||
1392
|
||
],
|
||
"id": "b8c32c98-4108-4091-b425-13e0c160c59a",
|
||
"name": "Preparar auditoría final Supabase",
|
||
"continueOnFail": true
|
||
},
|
||
{
|
||
"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.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOjE2NDE3NjkyMDAsCiAgICAiZXhwIjoxNzk5NTM1NjAwCn0.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||
},
|
||
{
|
||
"name": "Authorization",
|
||
"value": "=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOjE2NDE3NjkyMDAsCiAgICAiZXhwIjoxNzk5NTM1NjAwCn0.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": [
|
||
8720,
|
||
1392
|
||
],
|
||
"id": "37cdf6da-a29d-4971-873d-2aa36f82c1a2",
|
||
"name": "Guardar auditoría final Supabase",
|
||
"continueOnFail": true
|
||
},
|
||
{
|
||
"parameters": {
|
||
"method": "DELETE",
|
||
"url": "=https://www.googleapis.com/drive/v3/files/{{ $('Construir reporte producción').item.json.summary.stagingSpreadsheetId }}?supportsAllDrives=true",
|
||
"authentication": "predefinedCredentialType",
|
||
"nodeCredentialType": "googleDriveOAuth2Api",
|
||
"options": {}
|
||
},
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.3,
|
||
"position": [
|
||
8960,
|
||
1392
|
||
],
|
||
"id": "12e1dd61-f164-405f-ba53-c2af81189f8d",
|
||
"name": "Eliminar staging Nómina",
|
||
"credentials": {
|
||
"googleDriveOAuth2Api": {
|
||
"id": "g23xdGLZRzBGqKgH",
|
||
"name": "Isaac - Google Drive"
|
||
}
|
||
},
|
||
"continueOnFail": true
|
||
},
|
||
{
|
||
"parameters": {
|
||
"operation": "completion",
|
||
"respondWith": "showText",
|
||
"responseText": "<div style=\"font-family:Arial,Helvetica,sans-serif;max-width:620px;margin:48px auto;padding:36px 30px;text-align:center;background:#ffffff;border:1px solid #D0D0D0;color:#4A4A4A;\">\n <div style=\"height:6px;background:#6CC24A;margin:-36px -30px 28px;\"></div>\n <div style=\"font-size:13px;font-weight:700;letter-spacing:1.4px;color:#6CC24A;margin-bottom:10px;\">GOMEZLEE MARKETING</div>\n <h1 style=\"margin:0 0 12px;font-size:28px;color:#4F758B;\">Proceso completado</h1>\n <p style=\"margin:0 0 28px;font-size:16px;line-height:1.6;color:#6B8FA3;\">El reporte de Seguridad Social de Costa Rica fue generado, compartido como editor y enviado por correo correctamente.</p>\n <a href=\"https://agenteit.digitalcompass.agency/form/6dc6c5cd-1994-431b-b85a-1353a3346576\"_top\" rel=\"noopener\" style=\"display:inline-block;background:#6CC24A;color:#ffffff;text-decoration:none;padding:14px 24px;font-size:15px;font-weight:700;\">Regresar al formulario</a>\n <p style=\"margin:18px 0 0;font-size:12px;line-height:1.5;color:#6B8FA3;\">Si el botón no responde, abre nuevamente el formulario desde este enlace: <a href=\"https://agenteit.digitalcompass.agency/form/6dc6c5cd-1994-431b-b85a-1353a3346576\"_top\" style=\"color:#4F758B;\">volver al inicio</a>.</p>\n</div>"
|
||
},
|
||
"type": "n8n-nodes-base.form",
|
||
"typeVersion": 2.5,
|
||
"position": [
|
||
9200,
|
||
1392
|
||
],
|
||
"id": "e58f8d0a-c3a0-4212-b947-c538cadde546",
|
||
"name": "Confirmación y regreso al formulario",
|
||
"webhookId": "b940fa07-8c45-47f0-b9d5-8aa8ace8885b",
|
||
"notesInFlow": true,
|
||
"notes": "Página final con botón de regreso al formulario de Costa Rica en la misma pestaña."
|
||
},
|
||
{
|
||
"parameters": {
|
||
"content": "# 📥 RECEPCIÓN Y PREPARACIÓN DE FUENTES — COSTA RICA\n\nRecibe desde el formulario la nómina, la planilla de la CCSS y los parámetros del período que serán utilizados en el cruce.\n\n## Nómina\n\nEl archivo de nómina se prepara y convierte temporalmente en un Google Sheet de staging para facilitar su lectura.\n\nEl flujo:\n\n1. Prepara el archivo recibido.\n2. Crea el documento temporal de staging.\n3. Relaciona el archivo original con el staging.\n4. Espera que Google Drive complete la conversión.\n5. Consulta las hojas disponibles.\n6. Determina el rango que debe leer.\n7. Recupera los datos.\n8. Extrae y normaliza los registros de nómina.\n\n## Planilla CCSS\n\nLa planilla oficial se procesa desde PDF:\n\n- Se prepara el archivo.\n- Se extrae su contenido textual.\n- Se identifican los empleados, identificaciones y montos relevantes.\n- Se transforma la información en registros aptos para el cruce.\n\n## BambooHR\n\nSe consulta el directorio de empleados de BambooHR y se normalizan datos como:\n\n- Nombre.\n- Identificación disponible.\n- Estado activo o inactivo.\n- Fecha de ingreso.\n- Nombre oficial en BambooHR.\n\n## Auditoría inicial\n\nAl comenzar se registra en Supabase:\n\n- País y período.\n- Usuario que ejecutó el flujo.\n- Fecha y hora de inicio.\n- Identificador de ejecución.\n- Estado inicial del proceso.\n\nReglas:\n\n- No iniciar el cruce sin nómina y planilla CCSS.\n- Mantener separadas las tres fuentes hasta la etapa de comparación.\n- No asumir que el archivo de nómina contiene una única hoja.\n- No perder el archivo original durante la conversión temporal.\n- No modificar información en BambooHR.",
|
||
"height": 1328,
|
||
"width": 3248,
|
||
"color": 7
|
||
},
|
||
"type": "n8n-nodes-base.stickyNote",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
288,
|
||
752
|
||
],
|
||
"id": "68364d34-8402-499b-bbff-0125abf9217e",
|
||
"name": "Sticky Note"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"content": "# 🔍 CRUCES Y GENERACIÓN DEL REPORTE — COSTA RICA\n\nSincroniza los resultados de la nómina, la planilla CCSS y BambooHR antes de construir el reporte final.\n\n## Cruce de nómina y CCSS\n\nCompara los empleados de ambas fuentes para detectar:\n\n- Diferencias de identificación.\n- Personas presentes solamente en nómina.\n- Personas presentes solamente en la CCSS.\n- Diferencias en montos reportados.\n- Coincidencias que requieren revisión manual.\n\n## Validación con BambooHR\n\nLos casos relevantes se enriquecen con información del directorio para indicar:\n\n- Si el empleado existe en BambooHR.\n- Nombre oficial registrado.\n- Fecha de ingreso.\n- Estado activo o inactivo.\n- Posibles diferencias de identidad.\n\nBambooHR funciona como fuente complementaria; la comparación principal continúa siendo Nómina vs. CCSS.\n\n## Construcción del reporte\n\nCuando todas las ramas terminan:\n\n1. Se consolidan los cruces.\n2. Se organizan las hojas y columnas del entregable.\n3. Se crea un nuevo Google Sheet.\n4. Se escriben los resultados.\n5. Se preparan las instrucciones de formato.\n6. Se aplica el formato final.\n\nEl reporte puede organizar información como:\n\n- Cruce de identidad.\n- Cruce de montos.\n- Casos presentes en una sola fuente.\n- Nómina sin BambooHR.\n- Resumen de resultados.\n\nFormato esperado:\n\n- Encabezados visibles.\n- Filtros y filas congeladas.\n- Fechas estandarizadas.\n- Montos con formato numérico.\n- Columnas ajustadas.\n- Diferencias claramente identificables.\n\nReglas:\n\n- Esperar todas las fuentes antes de construir el reporte.\n- No ocultar empleados sin coincidencia.\n- No considerar una variación de nombre como ausencia automática.\n- No informar una diferencia de monto sin conservar ambos valores.\n- No declarar terminado el reporte hasta completar escritura y formato.",
|
||
"height": 1168,
|
||
"width": 2656,
|
||
"color": 6
|
||
},
|
||
"type": "n8n-nodes-base.stickyNote",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
3664,
|
||
800
|
||
],
|
||
"id": "2e0deb3c-fcc4-4809-af06-4766a5ea5012",
|
||
"name": "Sticky Note1"
|
||
},
|
||
{
|
||
"parameters": {
|
||
"content": "# ✅ DISTRIBUCIÓN Y CIERRE DEL PROCESO\n\nComparte el reporte terminado, notifica a los responsables, registra la auditoría final y elimina los archivos temporales.\n\n## Permisos del reporte\n\nLa configuración central define los destinatarios autorizados.\n\nEl flujo:\n\n1. Recupera la lista de correos.\n2. Genera un registro independiente por destinatario.\n3. Comparte el Google Sheet como editor.\n4. Conserva el enlace final del reporte.\n\nSolamente las personas incluidas en la configuración deben recibir acceso.\n\n## Correo de resultados\n\nSe prepara un correo HTML con la identidad visual de GLM que puede incluir:\n\n- País y período procesado.\n- Resumen de hallazgos.\n- Cantidad de empleados analizados.\n- Cantidad de diferencias.\n- Enlace directo al Google Sheet.\n- Información necesaria para la revisión.\n\nEl correo se envía únicamente después de crear, escribir, formatear y compartir el reporte.\n\n## Auditoría final\n\nSupabase registra el cierre de la ejecución con información como:\n\n- Fecha y hora de finalización.\n- Estado final.\n- Cantidad de registros procesados.\n- Hallazgos detectados.\n- Enlace del reporte.\n- Usuario ejecutor.\n- Resultado del envío y distribución.\n\n## Limpieza y confirmación\n\nAl finalizar:\n\n1. Se elimina el archivo temporal de staging.\n2. Se conserva intacta la nómina original.\n3. Se muestra una página de confirmación.\n4. Se ofrece al usuario regresar al formulario para iniciar otro período.\n\nReglas:\n\n- No enviar el correo antes de compartir correctamente el reporte.\n- No eliminar el staging antes de terminar todas las lecturas.\n- No eliminar el archivo original cargado por el usuario.\n- No registrar la auditoría como exitosa si falló la generación del reporte.\n- No finalizar sin mostrar una confirmación clara al usuario.",
|
||
"height": 1088,
|
||
"width": 3200,
|
||
"color": 5
|
||
},
|
||
"type": "n8n-nodes-base.stickyNote",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
6432,
|
||
832
|
||
],
|
||
"id": "0db53b76-a971-4334-8321-8eb406d2c8e9",
|
||
"name": "Sticky Note2"
|
||
}
|
||
],
|
||
"pinData": {},
|
||
"connections": {
|
||
"On form submission": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Inicializar ejecución",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Inicializar ejecución": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Preparar Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
},
|
||
{
|
||
"node": "Preparar Planilla CCSS",
|
||
"type": "main",
|
||
"index": 0
|
||
},
|
||
{
|
||
"node": "Consultar empleados BambooHR",
|
||
"type": "main",
|
||
"index": 0
|
||
},
|
||
{
|
||
"node": "Preparar auditoría inicial Supabase",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Preparar Nómina": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Crear staging Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Crear staging Nómina": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Reunir staging y Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Reunir staging y Nómina": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Actualizar staging Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Actualizar staging Nómina": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Esperar conversión Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Esperar conversión Nómina": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Listar hojas Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Listar hojas Nómina": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Preparar rango Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Preparar rango Nómina": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Leer hoja Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Leer hoja Nómina": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Extraer Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Preparar Planilla CCSS": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Extraer texto Planilla CCSS",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Extraer texto Planilla CCSS": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Extraer Planilla CCSS",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Extraer Nómina": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Esperar Nómina y CCSS",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Extraer Planilla CCSS": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Esperar Nómina y CCSS",
|
||
"type": "main",
|
||
"index": 1
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Esperar Nómina y CCSS": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Esperar BambooHR y cruces",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Consultar empleados BambooHR": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Normalizar empleados BambooHR",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Normalizar empleados BambooHR": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Esperar BambooHR y cruces",
|
||
"type": "main",
|
||
"index": 1
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Esperar BambooHR y cruces": {
|
||
"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": "Preparar formato reporte",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Preparar formato reporte": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Formatear reporte",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Formatear reporte": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "CONFIG - Destinatarios correo y permisos",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"CONFIG - Destinatarios correo y permisos": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Expandir permisos destinatarios",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Expandir permisos destinatarios": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Compartir reporte como editor",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Compartir reporte como editor": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Preparar correo HTML GLM",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Preparar correo HTML GLM": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Enviar correo resumen",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Enviar correo resumen": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Preparar auditoría final Supabase",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Preparar auditoría final Supabase": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Guardar auditoría final Supabase",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Guardar auditoría final Supabase": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Eliminar staging Nómina",
|
||
"type": "main",
|
||
"index": 0
|
||
},
|
||
{
|
||
"node": "Confirmación y regreso al formulario",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Preparar auditoría inicial Supabase": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Registrar auditoría inicial 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": "66808436-7f89-414b-a47f-c2e804bb1dc7",
|
||
"meta": {
|
||
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
|
||
},
|
||
"id": "cmtY0sJInCi21otP",
|
||
"tags": []
|
||
} |