Files
seguimiento-de-impuestos-gl…/seguimiento-de-impuestos-glm-buscar-contactos-en-bamboohr.json
T

627 lines
31 KiB
JSON

{
"updatedAt": "2026-08-07T13:30:01.528Z",
"createdAt": "2026-07-29T18:41:29.732Z",
"id": "nZ53koYjMAYN5tTe",
"name": "Seguimiento de Impuestos GLM - Buscar contactos en BambooHR",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "seguimiento-impuestos-bamboohr-contacto",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-1040,
80
],
"id": "83895861-f8e5-48bb-8eac-128ad55295f6",
"name": "Webhook - Buscar contacto BambooHR",
"webhookId": "seguimiento-impuestos-bamboohr-contacto"
},
{
"parameters": {
"jsCode": "let body = $json.body ?? $json;\nif (typeof body === 'string') {\n try { body = JSON.parse(body); }\n catch { body = {}; }\n}\nconst fullName = String(body.full_name ?? body.name ?? '').trim().replace(/\\s+/g, ' ');\nconst valid = fullName.split(' ').filter(Boolean).length >= 2 && fullName.length >= 5;\nreturn [{\n json: {\n valid,\n full_name: fullName,\n requested_by: String(body.requested_by ?? '').trim(),\n message: valid ? '' : 'Escribe el nombre y apellido de la persona.',\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-800,
80
],
"id": "b3415338-ef9e-4dfc-9f95-133caf07e9e4",
"name": "Validar solicitud"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "ac176ddc-1e99-4d7b-b9f9-bea5a4182baa",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-560,
80
],
"id": "b9a78df7-ddbd-4b20-9bad-0912bd6e7c41",
"name": "Solicitud válida"
},
{
"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"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": {
"title": "Información completa de empleados BambooHR - Seguimiento de Impuestos",
"fields": [
"firstName",
"middleName",
"lastName",
"displayName",
"department",
"division",
"location",
"customPosicion-Cliente",
"hireDate",
"originalHireDate",
"status",
"employeeNumber",
"workEmail",
"terminationDate",
"supervisor",
"startDate",
"jobTitle",
"employmentStatus",
"email",
"country",
"city",
"address1"
]
},
"options": {
"response": {
"response": {
"responseFormat": "json"
}
},
"timeout": 300000
}
},
"id": "9ddaf55d-54a9-4059-bc9b-3142cef78ecb",
"name": "Generar reporte completo BambooHR",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
-304,
0
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"credentials": {
"httpBasicAuth": {
"id": "7VrpNZ2jBLmiJ35q",
"name": "BambooHR GLM Full Access"
}
}
},
{
"parameters": {
"jsCode": "const query = String($('Validar solicitud').item.json.full_name ?? '').trim();\nconst response = $json ?? {};\nconst employees = Array.isArray(response.employees)\n ? response.employees\n : Array.isArray(response.data)\n ? response.data\n : Array.isArray(response)\n ? response\n : [];\n\nconst normalize = (value) => String(value ?? '')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst tokens = (value) => normalize(value).split(' ').filter((token) => token.length > 1);\n\nconst levenshtein = (a, b) => {\n if (!a) return b.length;\n if (!b) return a.length;\n const previous = Array.from({ length: b.length + 1 }, (_, index) => index);\n for (let i = 1; i <= a.length; i += 1) {\n let diagonal = previous[0];\n previous[0] = i;\n for (let j = 1; j <= b.length; j += 1) {\n const old = previous[j];\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n previous[j] = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + cost);\n diagonal = old;\n }\n }\n return previous[b.length];\n};\n\nconst similarity = (a, b) => {\n const longest = Math.max(a.length, b.length, 1);\n return 1 - levenshtein(a, b) / longest;\n};\n\nconst tokenF1 = (aTokens, bTokens) => {\n if (!aTokens.length || !bTokens.length) return 0;\n const a = new Set(aTokens);\n const b = new Set(bTokens);\n const common = [...a].filter((token) => b.has(token)).length;\n if (!common) return 0;\n const precision = common / b.size;\n const recall = common / a.size;\n return (2 * precision * recall) / (precision + recall);\n};\n\nconst countryAliases = [\n { code: 'DO', name: 'República Dominicana', aliases: ['dominican republic', 'republica dominicana', 'santo domingo', 'dominicana', 'rep dom'] },\n { code: 'PR', name: 'Puerto Rico', aliases: ['puerto rico'] },\n { code: 'JM', name: 'Jamaica', aliases: ['jamaica'] },\n { code: 'TT', name: 'Trinidad y Tobago', aliases: ['trinidad and tobago', 'trinidad & tobago', 'trinidad y tobago', 'trinidad', 'tobago'] },\n { code: 'SV', name: 'El Salvador', aliases: ['el salvador', 'salvador'] },\n { code: 'GT', name: 'Guatemala', aliases: ['guatemala'] },\n { code: 'HN', name: 'Honduras', aliases: ['honduras'] },\n { code: 'CR', name: 'Costa Rica', aliases: ['costa rica'] },\n { code: 'CO', name: 'Colombia', aliases: ['colombia'] },\n { code: 'MX', name: 'México', aliases: ['mexico', 'méxico'] },\n { code: 'NI', name: 'Nicaragua', aliases: ['nicaragua'] },\n { code: 'PA', name: 'Panamá', aliases: ['panama', 'panamá'] },\n];\n\nconst resolveCountry = (employee) => {\n const haystack = normalize([\n employee.country,\n employee.location,\n employee.city,\n employee.division,\n employee.department,\n employee.address1,\n ].filter(Boolean).join(' '));\n\n if (/\\bregional\\b|\\bregion\\b/.test(haystack)) {\n return { code: 'REG', name: 'Regional' };\n }\n\n for (const country of countryAliases) {\n if (country.aliases.some((alias) => haystack.includes(normalize(alias)))) {\n return { code: country.code, name: country.name };\n }\n }\n\n return { code: 'REG', name: 'Regional' };\n};\n\nconst resolveArea = (employee) => {\n const text = normalize([\n employee.department,\n employee.division,\n employee.jobTitle,\n employee['customPosicion-Cliente'],\n ].filter(Boolean).join(' '));\n\n if (/nomina|payroll|compensacion|compensation|beneficios|benefits/.test(text)) return 'nomina';\n if (/administracion|administrativo|administrative|finance|finanzas|contabilidad|accounting|impuesto|tax|tesoreria|treasury|conciliacion|compras|procurement/.test(text)) return 'admin';\n return 'other';\n};\n\n\nconst queryNormalized = normalize(query);\nconst queryTokens = tokens(query);\nconst candidates = [];\n\nfor (const employee of employees) {\n const constructedName = [employee.firstName, employee.middleName, employee.lastName]\n .filter(Boolean)\n .join(' ')\n .replace(/\\s+/g, ' ')\n .trim();\n const displayName = String(employee.displayName ?? '').trim();\n const fullName = displayName || constructedName;\n if (!fullName) continue;\n\n const nameVariants = [...new Set([fullName, constructedName, displayName].filter(Boolean))];\n let score = 0;\n for (const variant of nameVariants) {\n const normalizedVariant = normalize(variant);\n const variantTokens = tokens(variant);\n let current = 0;\n if (normalizedVariant === queryNormalized) current = 100;\n else if (normalizedVariant.startsWith(queryNormalized)) current = 91;\n else if (normalizedVariant.includes(queryNormalized)) current = 85;\n else if (queryNormalized.includes(normalizedVariant)) current = 78;\n\n const f1 = tokenF1(queryTokens, variantTokens);\n const editSimilarity = similarity(queryNormalized, normalizedVariant);\n current = Math.max(current, f1 * 82, editSimilarity * 76);\n if (queryTokens.every((token) => variantTokens.includes(token))) current += 8;\n score = Math.max(score, current);\n }\n\n const status = String(employee.status ?? employee.employmentStatus ?? '').trim();\n if (/active|activo/i.test(status)) score += 3;\n if (score < 45) continue;\n\n const country = resolveCountry(employee);\n const email = String(employee.workEmail || employee.email || '').trim().toLowerCase();\n\n candidates.push({\n id: String(employee.id ?? employee.employeeId ?? employee.employeeNumber ?? fullName),\n employeeNumber: String(employee.employeeNumber ?? ''),\n fullName,\n displayName: displayName || fullName,\n email,\n jobTitle: String(employee.jobTitle ?? employee['customPosicion-Cliente'] ?? '').trim(),\n department: String(employee.department ?? '').trim(),\n division: String(employee.division ?? '').trim(),\n location: String(employee.location ?? employee.city ?? '').trim(),\n countryCode: country.code,\n countryName: country.name,\n area: resolveArea(employee),\n status,\n score: Math.min(100, Math.round(score)),\n matchQuality: score >= 95 ? 'exacta' : score >= 72 ? 'alta' : 'media',\n });\n}\n\nconst unique = new Map();\nfor (const candidate of candidates.sort((a, b) => {\n const activeA = /active|activo/i.test(a.status) ? 1 : 0;\n const activeB = /active|activo/i.test(b.status) ? 1 : 0;\n return b.score - a.score || activeB - activeA || a.fullName.localeCompare(b.fullName);\n})) {\n const key = candidate.id || candidate.email || normalize(candidate.fullName);\n if (!unique.has(key)) unique.set(key, candidate);\n}\n\nreturn [{\n json: {\n ok: true,\n query,\n count: Math.min(unique.size, 8),\n candidates: [...unique.values()].slice(0, 8),\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-48,
0
],
"id": "4742718e-f429-4096-a974-f811e4cd5a91",
"name": "Buscar coincidencias robustas"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
208,
0
],
"id": "d7250da6-9a93-42c8-b7c8-c28adadbe7f1",
"name": "Responder coincidencias"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: false, message: $json.message, candidates: [] } }}",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-304,
192
],
"id": "1ed906bf-3e3c-42ea-9978-df44387c1f44",
"name": "Responder solicitud inválida"
},
{
"parameters": {
"content": "## BÚSQUEDA DE CONTACTOS EN BAMBOOHR\n\nRecibe desde la aplicación el nombre de una persona y busca coincidencias dentro del directorio actualizado de empleados de BambooHR.\n\n- Valida que la solicitud incluya un nombre válido antes de consultar BambooHR.\n- Devuelve una respuesta controlada cuando faltan datos o la solicitud es incorrecta.\n- Obtiene el reporte completo de empleados con su información laboral y de contacto.\n- Normaliza nombres, espacios, mayúsculas y caracteres especiales para realizar una búsqueda más confiable.\n- Identifica coincidencias exactas y aproximadas para reducir errores por nombres incompletos o diferencias de escritura.\n- Devuelve a la aplicación las personas encontradas para agregarlas automáticamente o permitir que el usuario seleccione la coincidencia correcta.",
"height": 576,
"width": 1552,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1136,
-224
],
"id": "549c09fb-34a0-44d8-b5a7-7cb71f804025",
"name": "Sticky Note"
}
],
"connections": {
"Webhook - Buscar contacto BambooHR": {
"main": [
[
{
"node": "Validar solicitud",
"type": "main",
"index": 0
}
]
]
},
"Validar solicitud": {
"main": [
[
{
"node": "Solicitud válida",
"type": "main",
"index": 0
}
]
]
},
"Solicitud válida": {
"main": [
[
{
"node": "Generar reporte completo BambooHR",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder solicitud inválida",
"type": "main",
"index": 0
}
]
]
},
"Generar reporte completo BambooHR": {
"main": [
[
{
"node": "Buscar coincidencias robustas",
"type": "main",
"index": 0
}
]
]
},
"Buscar coincidencias robustas": {
"main": [
[
{
"node": "Responder coincidencias",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"staticData": null,
"meta": null,
"versionId": "4cc439c7-5187-44ab-a0ca-dc0230cdfcbc",
"activeVersionId": "4cc439c7-5187-44ab-a0ca-dc0230cdfcbc",
"versionCounter": 16,
"triggerCount": 1,
"shared": [
{
"updatedAt": "2026-07-29T18:41:29.749Z",
"createdAt": "2026-07-29T18:41:29.749Z",
"role": "workflow:owner",
"workflowId": "nZ53koYjMAYN5tTe",
"projectId": "PJpTANzTXIFibWsW",
"project": {
"updatedAt": "2026-04-22T14:25:09.686Z",
"createdAt": "2026-04-22T14:22:54.790Z",
"id": "PJpTANzTXIFibWsW",
"name": "Isaac Aracena <iaracena@gomezleemarketing.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
}
],
"tags": [],
"activeVersion": {
"updatedAt": "2026-08-03T14:31:58.000Z",
"createdAt": "2026-08-03T14:31:56.672Z",
"versionId": "4cc439c7-5187-44ab-a0ca-dc0230cdfcbc",
"workflowId": "nZ53koYjMAYN5tTe",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "seguimiento-impuestos-bamboohr-contacto",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-1040,
80
],
"id": "83895861-f8e5-48bb-8eac-128ad55295f6",
"name": "Webhook - Buscar contacto BambooHR",
"webhookId": "seguimiento-impuestos-bamboohr-contacto"
},
{
"parameters": {
"jsCode": "let body = $json.body ?? $json;\nif (typeof body === 'string') {\n try { body = JSON.parse(body); }\n catch { body = {}; }\n}\nconst fullName = String(body.full_name ?? body.name ?? '').trim().replace(/\\s+/g, ' ');\nconst valid = fullName.split(' ').filter(Boolean).length >= 2 && fullName.length >= 5;\nreturn [{\n json: {\n valid,\n full_name: fullName,\n requested_by: String(body.requested_by ?? '').trim(),\n message: valid ? '' : 'Escribe el nombre y apellido de la persona.',\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-800,
80
],
"id": "b3415338-ef9e-4dfc-9f95-133caf07e9e4",
"name": "Validar solicitud"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "ac176ddc-1e99-4d7b-b9f9-bea5a4182baa",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-560,
80
],
"id": "b9a78df7-ddbd-4b20-9bad-0912bd6e7c41",
"name": "Solicitud válida"
},
{
"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"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": {
"title": "Información completa de empleados BambooHR - Seguimiento de Impuestos",
"fields": [
"firstName",
"middleName",
"lastName",
"displayName",
"department",
"division",
"location",
"customPosicion-Cliente",
"hireDate",
"originalHireDate",
"status",
"employeeNumber",
"workEmail",
"terminationDate",
"supervisor",
"startDate",
"jobTitle",
"employmentStatus",
"email",
"country",
"city",
"address1"
]
},
"options": {
"response": {
"response": {
"responseFormat": "json"
}
},
"timeout": 300000
}
},
"id": "9ddaf55d-54a9-4059-bc9b-3142cef78ecb",
"name": "Generar reporte completo BambooHR",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
-304,
0
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"credentials": {
"httpBasicAuth": {
"id": "7VrpNZ2jBLmiJ35q",
"name": "BambooHR GLM Full Access"
}
}
},
{
"parameters": {
"jsCode": "const query = String($('Validar solicitud').item.json.full_name ?? '').trim();\nconst response = $json ?? {};\nconst employees = Array.isArray(response.employees)\n ? response.employees\n : Array.isArray(response.data)\n ? response.data\n : Array.isArray(response)\n ? response\n : [];\n\nconst normalize = (value) => String(value ?? '')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst tokens = (value) => normalize(value).split(' ').filter((token) => token.length > 1);\n\nconst levenshtein = (a, b) => {\n if (!a) return b.length;\n if (!b) return a.length;\n const previous = Array.from({ length: b.length + 1 }, (_, index) => index);\n for (let i = 1; i <= a.length; i += 1) {\n let diagonal = previous[0];\n previous[0] = i;\n for (let j = 1; j <= b.length; j += 1) {\n const old = previous[j];\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n previous[j] = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + cost);\n diagonal = old;\n }\n }\n return previous[b.length];\n};\n\nconst similarity = (a, b) => {\n const longest = Math.max(a.length, b.length, 1);\n return 1 - levenshtein(a, b) / longest;\n};\n\nconst tokenF1 = (aTokens, bTokens) => {\n if (!aTokens.length || !bTokens.length) return 0;\n const a = new Set(aTokens);\n const b = new Set(bTokens);\n const common = [...a].filter((token) => b.has(token)).length;\n if (!common) return 0;\n const precision = common / b.size;\n const recall = common / a.size;\n return (2 * precision * recall) / (precision + recall);\n};\n\nconst countryAliases = [\n { code: 'DO', name: 'República Dominicana', aliases: ['dominican republic', 'republica dominicana', 'santo domingo', 'dominicana', 'rep dom'] },\n { code: 'PR', name: 'Puerto Rico', aliases: ['puerto rico'] },\n { code: 'JM', name: 'Jamaica', aliases: ['jamaica'] },\n { code: 'TT', name: 'Trinidad y Tobago', aliases: ['trinidad and tobago', 'trinidad & tobago', 'trinidad y tobago', 'trinidad', 'tobago'] },\n { code: 'SV', name: 'El Salvador', aliases: ['el salvador', 'salvador'] },\n { code: 'GT', name: 'Guatemala', aliases: ['guatemala'] },\n { code: 'HN', name: 'Honduras', aliases: ['honduras'] },\n { code: 'CR', name: 'Costa Rica', aliases: ['costa rica'] },\n { code: 'CO', name: 'Colombia', aliases: ['colombia'] },\n { code: 'MX', name: 'México', aliases: ['mexico', 'méxico'] },\n { code: 'NI', name: 'Nicaragua', aliases: ['nicaragua'] },\n { code: 'PA', name: 'Panamá', aliases: ['panama', 'panamá'] },\n];\n\nconst resolveCountry = (employee) => {\n const haystack = normalize([\n employee.country,\n employee.location,\n employee.city,\n employee.division,\n employee.department,\n employee.address1,\n ].filter(Boolean).join(' '));\n\n if (/\\bregional\\b|\\bregion\\b/.test(haystack)) {\n return { code: 'REG', name: 'Regional' };\n }\n\n for (const country of countryAliases) {\n if (country.aliases.some((alias) => haystack.includes(normalize(alias)))) {\n return { code: country.code, name: country.name };\n }\n }\n\n return { code: 'REG', name: 'Regional' };\n};\n\nconst resolveArea = (employee) => {\n const text = normalize([\n employee.department,\n employee.division,\n employee.jobTitle,\n employee['customPosicion-Cliente'],\n ].filter(Boolean).join(' '));\n\n if (/nomina|payroll|compensacion|compensation|beneficios|benefits/.test(text)) return 'nomina';\n if (/administracion|administrativo|administrative|finance|finanzas|contabilidad|accounting|impuesto|tax|tesoreria|treasury|conciliacion|compras|procurement/.test(text)) return 'admin';\n return 'other';\n};\n\n\nconst queryNormalized = normalize(query);\nconst queryTokens = tokens(query);\nconst candidates = [];\n\nfor (const employee of employees) {\n const constructedName = [employee.firstName, employee.middleName, employee.lastName]\n .filter(Boolean)\n .join(' ')\n .replace(/\\s+/g, ' ')\n .trim();\n const displayName = String(employee.displayName ?? '').trim();\n const fullName = displayName || constructedName;\n if (!fullName) continue;\n\n const nameVariants = [...new Set([fullName, constructedName, displayName].filter(Boolean))];\n let score = 0;\n for (const variant of nameVariants) {\n const normalizedVariant = normalize(variant);\n const variantTokens = tokens(variant);\n let current = 0;\n if (normalizedVariant === queryNormalized) current = 100;\n else if (normalizedVariant.startsWith(queryNormalized)) current = 91;\n else if (normalizedVariant.includes(queryNormalized)) current = 85;\n else if (queryNormalized.includes(normalizedVariant)) current = 78;\n\n const f1 = tokenF1(queryTokens, variantTokens);\n const editSimilarity = similarity(queryNormalized, normalizedVariant);\n current = Math.max(current, f1 * 82, editSimilarity * 76);\n if (queryTokens.every((token) => variantTokens.includes(token))) current += 8;\n score = Math.max(score, current);\n }\n\n const status = String(employee.status ?? employee.employmentStatus ?? '').trim();\n if (/active|activo/i.test(status)) score += 3;\n if (score < 45) continue;\n\n const country = resolveCountry(employee);\n const email = String(employee.workEmail || employee.email || '').trim().toLowerCase();\n\n candidates.push({\n id: String(employee.id ?? employee.employeeId ?? employee.employeeNumber ?? fullName),\n employeeNumber: String(employee.employeeNumber ?? ''),\n fullName,\n displayName: displayName || fullName,\n email,\n jobTitle: String(employee.jobTitle ?? employee['customPosicion-Cliente'] ?? '').trim(),\n department: String(employee.department ?? '').trim(),\n division: String(employee.division ?? '').trim(),\n location: String(employee.location ?? employee.city ?? '').trim(),\n countryCode: country.code,\n countryName: country.name,\n area: resolveArea(employee),\n status,\n score: Math.min(100, Math.round(score)),\n matchQuality: score >= 95 ? 'exacta' : score >= 72 ? 'alta' : 'media',\n });\n}\n\nconst unique = new Map();\nfor (const candidate of candidates.sort((a, b) => {\n const activeA = /active|activo/i.test(a.status) ? 1 : 0;\n const activeB = /active|activo/i.test(b.status) ? 1 : 0;\n return b.score - a.score || activeB - activeA || a.fullName.localeCompare(b.fullName);\n})) {\n const key = candidate.id || candidate.email || normalize(candidate.fullName);\n if (!unique.has(key)) unique.set(key, candidate);\n}\n\nreturn [{\n json: {\n ok: true,\n query,\n count: Math.min(unique.size, 8),\n candidates: [...unique.values()].slice(0, 8),\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-48,
0
],
"id": "4742718e-f429-4096-a974-f811e4cd5a91",
"name": "Buscar coincidencias robustas"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
208,
0
],
"id": "d7250da6-9a93-42c8-b7c8-c28adadbe7f1",
"name": "Responder coincidencias"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: false, message: $json.message, candidates: [] } }}",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-304,
192
],
"id": "1ed906bf-3e3c-42ea-9978-df44387c1f44",
"name": "Responder solicitud inválida"
},
{
"parameters": {
"content": "## BÚSQUEDA DE CONTACTOS EN BAMBOOHR\n\nRecibe desde la aplicación el nombre de una persona y busca coincidencias dentro del directorio actualizado de empleados de BambooHR.\n\n- Valida que la solicitud incluya un nombre válido antes de consultar BambooHR.\n- Devuelve una respuesta controlada cuando faltan datos o la solicitud es incorrecta.\n- Obtiene el reporte completo de empleados con su información laboral y de contacto.\n- Normaliza nombres, espacios, mayúsculas y caracteres especiales para realizar una búsqueda más confiable.\n- Identifica coincidencias exactas y aproximadas para reducir errores por nombres incompletos o diferencias de escritura.\n- Devuelve a la aplicación las personas encontradas para agregarlas automáticamente o permitir que el usuario seleccione la coincidencia correcta.",
"height": 576,
"width": 1552,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1136,
-224
],
"id": "549c09fb-34a0-44d8-b5a7-7cb71f804025",
"name": "Sticky Note"
}
],
"connections": {
"Webhook - Buscar contacto BambooHR": {
"main": [
[
{
"node": "Validar solicitud",
"type": "main",
"index": 0
}
]
]
},
"Validar solicitud": {
"main": [
[
{
"node": "Solicitud válida",
"type": "main",
"index": 0
}
]
]
},
"Solicitud válida": {
"main": [
[
{
"node": "Generar reporte completo BambooHR",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder solicitud inválida",
"type": "main",
"index": 0
}
]
]
},
"Generar reporte completo BambooHR": {
"main": [
[
{
"node": "Buscar coincidencias robustas",
"type": "main",
"index": 0
}
]
]
},
"Buscar coincidencias robustas": {
"main": [
[
{
"node": "Responder coincidencias",
"type": "main",
"index": 0
}
]
]
}
},
"authors": "Isaac Aracena",
"name": "Version 4cc439c7",
"description": "",
"autosaved": false,
"workflowPublishHistory": [
{
"createdAt": "2026-08-03T14:31:58.149Z",
"id": 3862,
"workflowId": "nZ53koYjMAYN5tTe",
"versionId": "4cc439c7-5187-44ab-a0ca-dc0230cdfcbc",
"event": "activated",
"userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
]
}
}