diff --git a/portal-de-verificacion-de-nomina-reportar-correccion-bamboohr-a-rrhh.json b/portal-de-verificacion-de-nomina-reportar-correccion-bamboohr-a-rrhh.json new file mode 100644 index 0000000..25581aa --- /dev/null +++ b/portal-de-verificacion-de-nomina-reportar-correccion-bamboohr-a-rrhh.json @@ -0,0 +1,1057 @@ +{ + "updatedAt": "2026-08-11T18:48:49.761Z", + "createdAt": "2026-08-11T18:47:42.698Z", + "id": "0yaiagL1tWdhRUXy", + "name": "Portal de Verificación de Nómina - Reportar corrección BambooHR a RRHH", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "cruce-cuentas-bamboo-correccion", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + 0, + 0 + ], + "id": "7e321136-0268-4f7e-bde1-b3a7b1b60caf", + "name": "Webhook", + "webhookId": "df5cb56b-75f3-4e8b-a1f2-f9a05c8ec317" + }, + { + "parameters": { + "jsCode": "const incoming = $input.first().json || {};\nconst rawBody = incoming.body ?? incoming;\n\nlet body = rawBody;\n\nif (rawBody && typeof rawBody === 'object' && typeof rawBody.payload === 'string') {\n try {\n body = JSON.parse(rawBody.payload);\n } catch (error) {\n throw new Error('El payload recibido no contiene JSON válido.');\n }\n}\n\nif (!body || typeof body !== 'object') {\n throw new Error('No se recibió una solicitud válida.');\n}\n\nconst accessToken = String(\n (rawBody && typeof rawBody === 'object' ? rawBody.access_token : '') ||\n body.access_token ||\n ''\n).trim();\n\nfunction clean(value) {\n return String(value ?? '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction normalizeName(value) {\n return clean(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nconst country = clean(body.country).toUpperCase();\nif (!['GT', 'TT'].includes(country)) {\n throw new Error('País no permitido para reportar correcciones BambooHR.');\n}\n\nif (!accessToken) {\n throw new Error('No se recibió una sesión autenticada de Supabase.');\n}\n\nif (body.confirmed_same_person !== true) {\n throw new Error('La revisión manual debe confirmar que se trata de la misma persona.');\n}\n\nconst sourceNames = Array.from(new Set([\n ...(Array.isArray(body.source_names) ? body.source_names : []),\n body.source_name,\n body.bank_name_file,\n body.bank_account_holder,\n]\n .map(clean)\n .filter(Boolean)\n .filter((value) => normalizeName(value))));\n\nif (!sourceNames.length) {\n throw new Error('No se recibió el nombre detectado en Banco sin BambooHR.');\n}\n\nconst bambooName = clean(body.bamboo_name);\nif (!bambooName) {\n throw new Error('Debes indicar el nombre correcto en BambooHR.');\n}\n\nreturn [{\n json: {\n accessToken,\n request: {\n country,\n countryName: country === 'GT' ? 'Guatemala' : 'Trinidad y Tobago',\n sourceName: clean(body.source_name) || sourceNames[0],\n sourceNames,\n bankNameFile: clean(body.bank_name_file),\n bankAccountHolder: clean(body.bank_account_holder),\n bankAccount: clean(body.bank_account),\n bankAmount: Number.isFinite(Number(body.bank_amount)) ? Number(body.bank_amount) : null,\n currency: clean(body.currency) || (country === 'GT' ? 'GTQ' : 'TTD'),\n bambooName,\n bambooEmployeeNumber: clean(body.bamboo_employee_number),\n reason: clean(body.reason) || 'Corrección de identidad en BambooHR',\n comment: clean(body.comment),\n executionId: clean(body.execution_id),\n reportUrl: clean(body.report_url),\n periodLabel: clean(body.period_label),\n periodStart: clean(body.period_start),\n periodEnd: clean(body.period_end),\n requestedByNameClaimed: clean(body.requested_by_name),\n requestedByEmailClaimed: clean(body.requested_by_email).toLowerCase(),\n },\n },\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 240, + 0 + ], + "id": "507c0065-11d8-4693-86d7-acaaf826d8a0", + "name": "Preparar solicitud" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cruce_cuentas_mi_acceso", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ 'Bearer ' + $('Preparar solicitud').first().json.accessToken }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": {}, + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 480, + 0 + ], + "id": "9b9f3800-4182-4cc4-a6e3-11e200c44b1f", + "name": "Validar acceso Supabase", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1000 + }, + { + "parameters": { + "jsCode": "const raw = $input.first().json || {};\nconst access = Array.isArray(raw) ? (raw[0] || {}) : raw;\nconst prepared = $('Preparar solicitud').first().json || {};\nconst request = prepared.request || {};\n\nconst email = String(access.email || '').trim().toLowerCase();\nconst countries = Array.isArray(access.paises)\n ? access.paises.map((value) => String(value || '').trim().toUpperCase()).filter(Boolean)\n : [];\n\nif (\n access.ok !== true ||\n access.autenticado !== true ||\n access.autorizado !== true ||\n access.activo !== true ||\n access.acceso_app !== true ||\n !email\n) {\n throw new Error(access.motivo || 'El usuario no tiene acceso autorizado al Cruce de Cuentas.');\n}\n\nif (countries.length && !countries.includes(request.country)) {\n throw new Error(`El usuario autenticado no tiene acceso al país ${request.country}.`);\n}\n\nreturn [{\n json: {\n ...request,\n requestedByName: String(access.nombre || request.requestedByNameClaimed || email).trim(),\n requestedByEmail: email,\n },\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 720, + 0 + ], + "id": "b20cb8fa-fa37-48a5-93aa-dd1740e8a6c0", + "name": "Validar usuario" + }, + { + "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": "Validación manual - Corrección identidad Cruce de Cuentas", + "fields": [ + "firstName", + "middleName", + "lastName", + "displayName", + "location", + "status", + "employeeNumber" + ] + }, + "options": { + "response": { + "response": { + "responseFormat": "json" + } + }, + "timeout": 300000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 960, + 0 + ], + "id": "5dc107fa-c704-4643-9479-1f57e163d5fd", + "name": "Consultar empleado BambooHR", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 3000, + "credentials": { + "httpBasicAuth": { + "id": "7VrpNZ2jBLmiJ35q", + "name": "BambooHR GLM Full Access" + } + } + }, + { + "parameters": { + "jsCode": "const request = $('Validar usuario').first().json || {};\nconst raw = $input.first().json || {};\n\nfunction clean(value) {\n return String(value ?? '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction normalizeName(value) {\n return clean(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeIdentifier(value) {\n return clean(value).toUpperCase().replace(/[^A-Z0-9]/g, '');\n}\n\nfunction countryMatches(employee) {\n const location = normalizeName(\n employee.location ||\n employee.jobInformationLocation ||\n employee.jobLocation ||\n employee.country ||\n ''\n );\n\n if (request.country === 'GT') {\n return location.includes('guatemala');\n }\n\n return location.includes('trinidad') || location.includes('tobago');\n}\n\nfunction employeeAliases(employee) {\n const first = clean(employee.firstName || employee.first_name);\n const middle = clean(employee.middleName || employee.middle_name);\n const last = clean(employee.lastName || employee.last_name);\n const display = clean(employee.displayName || employee.fullName || employee.full_name);\n\n return Array.from(new Set([\n display,\n [first, middle, last].filter(Boolean).join(' '),\n [first, last].filter(Boolean).join(' '),\n ].map(clean).filter(Boolean)));\n}\n\nconst employees = Array.isArray(raw.employees)\n ? raw.employees\n : Array.isArray(raw.data?.employees)\n ? raw.data.employees\n : [];\n\nif (!employees.length) {\n throw new Error('BambooHR no devolvió empleados para validar la corrección.');\n}\n\nconst candidates = employees.filter(countryMatches);\nif (!candidates.length) {\n throw new Error(`No se encontraron empleados de ${request.countryName} en BambooHR.`);\n}\n\nconst requestedNumber = normalizeIdentifier(request.bambooEmployeeNumber);\nlet matches = [];\nlet matchedBy = '';\n\nif (requestedNumber) {\n matches = candidates.filter((employee) =>\n normalizeIdentifier(employee.employeeNumber || employee.employee_number) === requestedNumber\n );\n matchedBy = 'employee_number';\n}\n\nif (!matches.length) {\n const targetName = normalizeName(request.bambooName);\n matches = candidates.filter((employee) =>\n employeeAliases(employee).some((alias) => normalizeName(alias) === targetName)\n );\n matchedBy = 'exact_bamboo_name';\n}\n\nif (matches.length !== 1) {\n if (matches.length > 1) {\n throw new Error('La identificación en BambooHR es ambigua. Indica el Employee Number para seleccionar una sola persona.');\n }\n\n throw new Error(\n 'No se encontró exactamente ese empleado en BambooHR para el país seleccionado. Revisa el nombre o indica el Employee Number.'\n );\n}\n\nconst employee = matches[0];\nconst aliases = employeeAliases(employee);\nconst actualName = clean(employee.displayName) || aliases[0] || request.bambooName;\nconst actualEmployeeNumber = clean(employee.employeeNumber || employee.employee_number);\nconst bambooId = clean(employee.employeeId || employee.id);\nconst now = new Date().toISOString();\n\nconst recipients = request.country === 'TT'\n ? ['lnicomejack@gomezleemarketing.com']\n : [\n 'pgamboa@gomezleemarketing.com',\n 'smeyer@gomezleemarketing.com',\n 'mjmorales@gomezleemarketing.com',\n ];\n\nconst aliasPayload = request.sourceNames.map((sourceName) => ({\n pais: request.country,\n nombre_origen: sourceName,\n nombre_origen_normalizado: normalizeName(sourceName),\n cuenta_bancaria: String(request.bankAccount || '').replace(/[^0-9A-Za-z]+/g, ''),\n bamboo_employee_number: actualEmployeeNumber || null,\n bamboo_employee_id: bambooId || null,\n bamboo_nombre: actualName,\n motivo: request.reason,\n comentario: request.comment || null,\n activo: true,\n validado_por_email: request.requestedByEmail,\n validado_por_nombre: request.requestedByName,\n last_reported_at: now,\n updated_at: now,\n}));\n\nreturn [{\n json: {\n request,\n matchedBy,\n employee: {\n bambooId,\n employeeNumber: actualEmployeeNumber,\n name: actualName,\n status: clean(employee.status),\n location: clean(employee.location),\n },\n recipients,\n aliasPayload,\n },\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1200, + 0 + ], + "id": "504eed05-c6be-442e-9707-8cd4aca1d19c", + "name": "Resolver empleado BambooHR" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/cruce_cuentas_bamboo_aliases?on_conflict=pais,nombre_origen_normalizado,cuenta_bancaria", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Prefer", + "value": "resolution=merge-duplicates,return=representation" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $('Resolver empleado BambooHR').first().json.aliasPayload }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1440, + 0 + ], + "id": "6fc69ac2-6999-4810-9e8d-675c4c4d40ac", + "name": "Guardar alias Supabase", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1000 + }, + { + "parameters": { + "jsCode": "const resolved = $('Resolver empleado BambooHR').first().json || {};\nconst request = resolved.request || {};\nconst employee = resolved.employee || {};\nconst aliasResponseItems = $input.all().map((item) => item.json || {});\n\nfunction flattenRows(items) {\n const rows = [];\n for (const item of items) {\n if (Array.isArray(item)) rows.push(...item);\n else if (Array.isArray(item.data)) rows.push(...item.data);\n else rows.push(item);\n }\n return rows.filter((row) => row && typeof row === 'object');\n}\n\nconst aliasRows = flattenRows(aliasResponseItems);\nconst aliasIds = aliasRows\n .map((row) => row.id)\n .filter(Boolean);\n\nconst requestPayload = {\n pais: request.country,\n nombre_origen_principal: request.sourceName,\n nombres_origen: request.sourceNames,\n cuenta_bancaria: request.bankAccount || null,\n monto_banco: request.bankAmount,\n moneda: request.currency,\n bamboo_employee_number: employee.employeeNumber || null,\n bamboo_employee_id: employee.bambooId || null,\n bamboo_nombre: employee.name,\n motivo: request.reason,\n comentario: request.comment || null,\n execution_id: request.executionId || null,\n report_url: request.reportUrl || null,\n period_label: request.periodLabel || null,\n period_start: request.periodStart || null,\n period_end: request.periodEnd || null,\n solicitado_por_email: request.requestedByEmail,\n solicitado_por_nombre: request.requestedByName,\n destinatarios_rrhh: resolved.recipients || [],\n alias_ids: aliasIds,\n estado: 'pendiente_notificacion',\n correo_enviado: false,\n};\n\nreturn [{ json: { requestPayload, aliasIds } }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 0 + ], + "id": "9d565bf5-a6df-4ac9-bad1-abe48a277c37", + "name": "Preparar auditoría" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/cruce_cuentas_bamboo_correcciones", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Prefer", + "value": "return=representation" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.requestPayload }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1920, + 0 + ], + "id": "45f87d7b-a746-4eb2-a54e-d2de7645f854", + "name": "Guardar solicitud Supabase", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1000 + }, + { + "parameters": { + "jsCode": "const resolved = $('Resolver empleado BambooHR').first().json || {};\nconst request = resolved.request || {};\nconst employee = resolved.employee || {};\nconst auditInput = $input.all().map((item) => item.json || {});\n\nfunction firstObject(value) {\n if (Array.isArray(value)) return value[0] || {};\n if (value?.data && Array.isArray(value.data)) return value.data[0] || {};\n return value || {};\n}\n\nconst auditRow = firstObject(auditInput[0]);\nconst requestId = String(auditRow.id || '');\nif (!requestId) {\n throw new Error('Supabase no devolvió el ID de la solicitud de corrección.');\n}\n\nfunction escapeHtml(value) {\n return String(value ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nconst logoUrl = 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\nconst flagUrl = request.country === 'TT'\n ? 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/Flag_of_Trinidad_and_Tobago.svg.webp'\n : 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/Flag_of_Guatemala.svg.webp';\n\nconst to = (resolved.recipients || []).join(',');\nconst subject = `Corrección requerida en BambooHR - ${request.countryName} - ${request.sourceName}`;\n\nconst reportButton = request.reportUrl\n ? `Abrir reporte de Cruce de Cuentas`\n : '';\n\nconst htmlBody = `\n\n\n\n\n
\n\n\n
 
\n\n\n\n
\"GomezLee\"Bandera
\n
\n
Corrección de identidad · BambooHR
\n

Revisión solicitada para ${escapeHtml(request.countryName)}

\n

Durante la revisión manual de Banco sin BambooHR se confirmó que el pago corresponde a una persona existente en BambooHR, pero su nombre no está permitiendo identificarla correctamente en el cruce.

\n\n\n\n\n\n\n\n\n
Nombre detectado${escapeHtml(request.sourceName)}
Empleado en BambooHR${escapeHtml(employee.name)}
Employee Number${escapeHtml(employee.employeeNumber || 'No disponible')}
Motivo${escapeHtml(request.reason)}
Período${escapeHtml(request.periodLabel || 'No especificado')}
Reportado por${escapeHtml(request.requestedByName)} · ${escapeHtml(request.requestedByEmail)}
\n\n${request.comment ? `
Comentario: ${escapeHtml(request.comment)}
` : ''}\n\n

Por favor, revisen y corrijan el nombre o la información correspondiente en BambooHR. La equivalencia validada ya quedó registrada en el Cruce de Cuentas para evitar que este mismo falso positivo se repita mientras se realiza la corrección.

\n\n${reportButton ? `
${reportButton}
` : ''}\n\n
\n
Notificación automática · Portal Cruce de Cuentas GLM · Uso interno
\n
\n
\n`;\n\nreturn [{\n json: {\n requestId,\n to,\n subject,\n htmlBody,\n country: request.country,\n countryName: request.countryName,\n aliasCount: Array.isArray(resolved.aliasPayload) ? resolved.aliasPayload.length : 0,\n },\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2160, + 0 + ], + "id": "68be3c1d-42eb-4bc5-aa35-36333261f2a6", + "name": "Preparar correo RRHH" + }, + { + "parameters": { + "sendTo": "={{$json.to}}", + "subject": "={{$json.subject}}", + "message": "={{$json.htmlBody}}", + "options": { + "appendAttribution": false + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.2, + "position": [ + 2400, + 0 + ], + "id": "28f7c657-0c5b-47c6-898b-f68f1cf21378", + "name": "Enviar correo RRHH", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 2000, + "webhookId": "8fd723dd-b333-4db8-9c43-2f5a9d86f86e", + "credentials": { + "gmailOAuth2": { + "id": "UDcO1FLJqA453V2D", + "name": "Gmail account 3" + } + } + }, + { + "parameters": { + "method": "PATCH", + "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cruce_cuentas_bamboo_correcciones?id=eq.' + $('Preparar correo RRHH').first().json.requestId }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Prefer", + "value": "return=minimal" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { correo_enviado: true, correo_enviado_at: new Date().toISOString(), estado: 'notificado_rrhh' } }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2640, + 0 + ], + "id": "87b7910e-0465-40b1-89d4-6b160726b7f7", + "name": "Marcar correo enviado", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1000 + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ { ok: true, message: 'Caso validado, equivalencia registrada y correo enviado a RRHH.', country: $('Preparar correo RRHH').first().json.country, countryName: $('Preparar correo RRHH').first().json.countryName, aliasCount: $('Preparar correo RRHH').first().json.aliasCount, requestId: $('Preparar correo RRHH').first().json.requestId } }}", + "options": { + "responseCode": 200, + "responseHeaders": { + "entries": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + } + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 2880, + 0 + ], + "id": "1db48431-4661-49de-a495-66c740b2aac1", + "name": "Respond to Webhook" + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Preparar solicitud", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar solicitud": { + "main": [ + [ + { + "node": "Validar acceso Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validar acceso Supabase": { + "main": [ + [ + { + "node": "Validar usuario", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validar usuario": { + "main": [ + [ + { + "node": "Consultar empleado BambooHR", + "type": "main", + "index": 0 + } + ] + ] + }, + "Consultar empleado BambooHR": { + "main": [ + [ + { + "node": "Resolver empleado BambooHR", + "type": "main", + "index": 0 + } + ] + ] + }, + "Resolver empleado BambooHR": { + "main": [ + [ + { + "node": "Guardar alias Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, + "Guardar alias Supabase": { + "main": [ + [ + { + "node": "Preparar auditoría", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar auditoría": { + "main": [ + [ + { + "node": "Guardar solicitud Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, + "Guardar solicitud Supabase": { + "main": [ + [ + { + "node": "Preparar correo RRHH", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar correo RRHH": { + "main": [ + [ + { + "node": "Enviar correo RRHH", + "type": "main", + "index": 0 + } + ] + ] + }, + "Enviar correo RRHH": { + "main": [ + [ + { + "node": "Marcar correo enviado", + "type": "main", + "index": 0 + } + ] + ] + }, + "Marcar correo enviado": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "staticData": null, + "meta": null, + "versionId": "7e4daa40-244f-43ed-9d74-14a9cb589ddf", + "activeVersionId": "7e4daa40-244f-43ed-9d74-14a9cb589ddf", + "versionCounter": 5, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-08-11T18:47:42.710Z", + "createdAt": "2026-08-11T18:47:42.710Z", + "role": "workflow:owner", + "workflowId": "0yaiagL1tWdhRUXy", + "projectId": "PJpTANzTXIFibWsW", + "project": { + "updatedAt": "2026-04-22T14:25:09.686Z", + "createdAt": "2026-04-22T14:22:54.790Z", + "id": "PJpTANzTXIFibWsW", + "name": "Isaac Aracena ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-08-11T18:47:55.000Z", + "createdAt": "2026-08-11T18:47:42.715Z", + "versionId": "7e4daa40-244f-43ed-9d74-14a9cb589ddf", + "workflowId": "0yaiagL1tWdhRUXy", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "cruce-cuentas-bamboo-correccion", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + 0, + 0 + ], + "id": "7e321136-0268-4f7e-bde1-b3a7b1b60caf", + "name": "Webhook", + "webhookId": "df5cb56b-75f3-4e8b-a1f2-f9a05c8ec317" + }, + { + "parameters": { + "jsCode": "const incoming = $input.first().json || {};\nconst rawBody = incoming.body ?? incoming;\n\nlet body = rawBody;\n\nif (rawBody && typeof rawBody === 'object' && typeof rawBody.payload === 'string') {\n try {\n body = JSON.parse(rawBody.payload);\n } catch (error) {\n throw new Error('El payload recibido no contiene JSON válido.');\n }\n}\n\nif (!body || typeof body !== 'object') {\n throw new Error('No se recibió una solicitud válida.');\n}\n\nconst accessToken = String(\n (rawBody && typeof rawBody === 'object' ? rawBody.access_token : '') ||\n body.access_token ||\n ''\n).trim();\n\nfunction clean(value) {\n return String(value ?? '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction normalizeName(value) {\n return clean(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nconst country = clean(body.country).toUpperCase();\nif (!['GT', 'TT'].includes(country)) {\n throw new Error('País no permitido para reportar correcciones BambooHR.');\n}\n\nif (!accessToken) {\n throw new Error('No se recibió una sesión autenticada de Supabase.');\n}\n\nif (body.confirmed_same_person !== true) {\n throw new Error('La revisión manual debe confirmar que se trata de la misma persona.');\n}\n\nconst sourceNames = Array.from(new Set([\n ...(Array.isArray(body.source_names) ? body.source_names : []),\n body.source_name,\n body.bank_name_file,\n body.bank_account_holder,\n]\n .map(clean)\n .filter(Boolean)\n .filter((value) => normalizeName(value))));\n\nif (!sourceNames.length) {\n throw new Error('No se recibió el nombre detectado en Banco sin BambooHR.');\n}\n\nconst bambooName = clean(body.bamboo_name);\nif (!bambooName) {\n throw new Error('Debes indicar el nombre correcto en BambooHR.');\n}\n\nreturn [{\n json: {\n accessToken,\n request: {\n country,\n countryName: country === 'GT' ? 'Guatemala' : 'Trinidad y Tobago',\n sourceName: clean(body.source_name) || sourceNames[0],\n sourceNames,\n bankNameFile: clean(body.bank_name_file),\n bankAccountHolder: clean(body.bank_account_holder),\n bankAccount: clean(body.bank_account),\n bankAmount: Number.isFinite(Number(body.bank_amount)) ? Number(body.bank_amount) : null,\n currency: clean(body.currency) || (country === 'GT' ? 'GTQ' : 'TTD'),\n bambooName,\n bambooEmployeeNumber: clean(body.bamboo_employee_number),\n reason: clean(body.reason) || 'Corrección de identidad en BambooHR',\n comment: clean(body.comment),\n executionId: clean(body.execution_id),\n reportUrl: clean(body.report_url),\n periodLabel: clean(body.period_label),\n periodStart: clean(body.period_start),\n periodEnd: clean(body.period_end),\n requestedByNameClaimed: clean(body.requested_by_name),\n requestedByEmailClaimed: clean(body.requested_by_email).toLowerCase(),\n },\n },\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 240, + 0 + ], + "id": "507c0065-11d8-4693-86d7-acaaf826d8a0", + "name": "Preparar solicitud" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cruce_cuentas_mi_acceso", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ 'Bearer ' + $('Preparar solicitud').first().json.accessToken }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": {}, + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 480, + 0 + ], + "id": "9b9f3800-4182-4cc4-a6e3-11e200c44b1f", + "name": "Validar acceso Supabase", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1000 + }, + { + "parameters": { + "jsCode": "const raw = $input.first().json || {};\nconst access = Array.isArray(raw) ? (raw[0] || {}) : raw;\nconst prepared = $('Preparar solicitud').first().json || {};\nconst request = prepared.request || {};\n\nconst email = String(access.email || '').trim().toLowerCase();\nconst countries = Array.isArray(access.paises)\n ? access.paises.map((value) => String(value || '').trim().toUpperCase()).filter(Boolean)\n : [];\n\nif (\n access.ok !== true ||\n access.autenticado !== true ||\n access.autorizado !== true ||\n access.activo !== true ||\n access.acceso_app !== true ||\n !email\n) {\n throw new Error(access.motivo || 'El usuario no tiene acceso autorizado al Cruce de Cuentas.');\n}\n\nif (countries.length && !countries.includes(request.country)) {\n throw new Error(`El usuario autenticado no tiene acceso al país ${request.country}.`);\n}\n\nreturn [{\n json: {\n ...request,\n requestedByName: String(access.nombre || request.requestedByNameClaimed || email).trim(),\n requestedByEmail: email,\n },\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 720, + 0 + ], + "id": "b20cb8fa-fa37-48a5-93aa-dd1740e8a6c0", + "name": "Validar usuario" + }, + { + "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": "Validación manual - Corrección identidad Cruce de Cuentas", + "fields": [ + "firstName", + "middleName", + "lastName", + "displayName", + "location", + "status", + "employeeNumber" + ] + }, + "options": { + "response": { + "response": { + "responseFormat": "json" + } + }, + "timeout": 300000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 960, + 0 + ], + "id": "5dc107fa-c704-4643-9479-1f57e163d5fd", + "name": "Consultar empleado BambooHR", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 3000, + "credentials": { + "httpBasicAuth": { + "id": "7VrpNZ2jBLmiJ35q", + "name": "BambooHR GLM Full Access" + } + } + }, + { + "parameters": { + "jsCode": "const request = $('Validar usuario').first().json || {};\nconst raw = $input.first().json || {};\n\nfunction clean(value) {\n return String(value ?? '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction normalizeName(value) {\n return clean(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeIdentifier(value) {\n return clean(value).toUpperCase().replace(/[^A-Z0-9]/g, '');\n}\n\nfunction countryMatches(employee) {\n const location = normalizeName(\n employee.location ||\n employee.jobInformationLocation ||\n employee.jobLocation ||\n employee.country ||\n ''\n );\n\n if (request.country === 'GT') {\n return location.includes('guatemala');\n }\n\n return location.includes('trinidad') || location.includes('tobago');\n}\n\nfunction employeeAliases(employee) {\n const first = clean(employee.firstName || employee.first_name);\n const middle = clean(employee.middleName || employee.middle_name);\n const last = clean(employee.lastName || employee.last_name);\n const display = clean(employee.displayName || employee.fullName || employee.full_name);\n\n return Array.from(new Set([\n display,\n [first, middle, last].filter(Boolean).join(' '),\n [first, last].filter(Boolean).join(' '),\n ].map(clean).filter(Boolean)));\n}\n\nconst employees = Array.isArray(raw.employees)\n ? raw.employees\n : Array.isArray(raw.data?.employees)\n ? raw.data.employees\n : [];\n\nif (!employees.length) {\n throw new Error('BambooHR no devolvió empleados para validar la corrección.');\n}\n\nconst candidates = employees.filter(countryMatches);\nif (!candidates.length) {\n throw new Error(`No se encontraron empleados de ${request.countryName} en BambooHR.`);\n}\n\nconst requestedNumber = normalizeIdentifier(request.bambooEmployeeNumber);\nlet matches = [];\nlet matchedBy = '';\n\nif (requestedNumber) {\n matches = candidates.filter((employee) =>\n normalizeIdentifier(employee.employeeNumber || employee.employee_number) === requestedNumber\n );\n matchedBy = 'employee_number';\n}\n\nif (!matches.length) {\n const targetName = normalizeName(request.bambooName);\n matches = candidates.filter((employee) =>\n employeeAliases(employee).some((alias) => normalizeName(alias) === targetName)\n );\n matchedBy = 'exact_bamboo_name';\n}\n\nif (matches.length !== 1) {\n if (matches.length > 1) {\n throw new Error('La identificación en BambooHR es ambigua. Indica el Employee Number para seleccionar una sola persona.');\n }\n\n throw new Error(\n 'No se encontró exactamente ese empleado en BambooHR para el país seleccionado. Revisa el nombre o indica el Employee Number.'\n );\n}\n\nconst employee = matches[0];\nconst aliases = employeeAliases(employee);\nconst actualName = clean(employee.displayName) || aliases[0] || request.bambooName;\nconst actualEmployeeNumber = clean(employee.employeeNumber || employee.employee_number);\nconst bambooId = clean(employee.employeeId || employee.id);\nconst now = new Date().toISOString();\n\nconst recipients = request.country === 'TT'\n ? ['lnicomejack@gomezleemarketing.com']\n : [\n 'pgamboa@gomezleemarketing.com',\n 'smeyer@gomezleemarketing.com',\n 'mjmorales@gomezleemarketing.com',\n ];\n\nconst aliasPayload = request.sourceNames.map((sourceName) => ({\n pais: request.country,\n nombre_origen: sourceName,\n nombre_origen_normalizado: normalizeName(sourceName),\n cuenta_bancaria: String(request.bankAccount || '').replace(/[^0-9A-Za-z]+/g, ''),\n bamboo_employee_number: actualEmployeeNumber || null,\n bamboo_employee_id: bambooId || null,\n bamboo_nombre: actualName,\n motivo: request.reason,\n comentario: request.comment || null,\n activo: true,\n validado_por_email: request.requestedByEmail,\n validado_por_nombre: request.requestedByName,\n last_reported_at: now,\n updated_at: now,\n}));\n\nreturn [{\n json: {\n request,\n matchedBy,\n employee: {\n bambooId,\n employeeNumber: actualEmployeeNumber,\n name: actualName,\n status: clean(employee.status),\n location: clean(employee.location),\n },\n recipients,\n aliasPayload,\n },\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1200, + 0 + ], + "id": "504eed05-c6be-442e-9707-8cd4aca1d19c", + "name": "Resolver empleado BambooHR" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/cruce_cuentas_bamboo_aliases?on_conflict=pais,nombre_origen_normalizado,cuenta_bancaria", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Prefer", + "value": "resolution=merge-duplicates,return=representation" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $('Resolver empleado BambooHR').first().json.aliasPayload }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1440, + 0 + ], + "id": "6fc69ac2-6999-4810-9e8d-675c4c4d40ac", + "name": "Guardar alias Supabase", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1000 + }, + { + "parameters": { + "jsCode": "const resolved = $('Resolver empleado BambooHR').first().json || {};\nconst request = resolved.request || {};\nconst employee = resolved.employee || {};\nconst aliasResponseItems = $input.all().map((item) => item.json || {});\n\nfunction flattenRows(items) {\n const rows = [];\n for (const item of items) {\n if (Array.isArray(item)) rows.push(...item);\n else if (Array.isArray(item.data)) rows.push(...item.data);\n else rows.push(item);\n }\n return rows.filter((row) => row && typeof row === 'object');\n}\n\nconst aliasRows = flattenRows(aliasResponseItems);\nconst aliasIds = aliasRows\n .map((row) => row.id)\n .filter(Boolean);\n\nconst requestPayload = {\n pais: request.country,\n nombre_origen_principal: request.sourceName,\n nombres_origen: request.sourceNames,\n cuenta_bancaria: request.bankAccount || null,\n monto_banco: request.bankAmount,\n moneda: request.currency,\n bamboo_employee_number: employee.employeeNumber || null,\n bamboo_employee_id: employee.bambooId || null,\n bamboo_nombre: employee.name,\n motivo: request.reason,\n comentario: request.comment || null,\n execution_id: request.executionId || null,\n report_url: request.reportUrl || null,\n period_label: request.periodLabel || null,\n period_start: request.periodStart || null,\n period_end: request.periodEnd || null,\n solicitado_por_email: request.requestedByEmail,\n solicitado_por_nombre: request.requestedByName,\n destinatarios_rrhh: resolved.recipients || [],\n alias_ids: aliasIds,\n estado: 'pendiente_notificacion',\n correo_enviado: false,\n};\n\nreturn [{ json: { requestPayload, aliasIds } }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 0 + ], + "id": "9d565bf5-a6df-4ac9-bad1-abe48a277c37", + "name": "Preparar auditoría" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/cruce_cuentas_bamboo_correcciones", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Prefer", + "value": "return=representation" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.requestPayload }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1920, + 0 + ], + "id": "45f87d7b-a746-4eb2-a54e-d2de7645f854", + "name": "Guardar solicitud Supabase", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1000 + }, + { + "parameters": { + "jsCode": "const resolved = $('Resolver empleado BambooHR').first().json || {};\nconst request = resolved.request || {};\nconst employee = resolved.employee || {};\nconst auditInput = $input.all().map((item) => item.json || {});\n\nfunction firstObject(value) {\n if (Array.isArray(value)) return value[0] || {};\n if (value?.data && Array.isArray(value.data)) return value.data[0] || {};\n return value || {};\n}\n\nconst auditRow = firstObject(auditInput[0]);\nconst requestId = String(auditRow.id || '');\nif (!requestId) {\n throw new Error('Supabase no devolvió el ID de la solicitud de corrección.');\n}\n\nfunction escapeHtml(value) {\n return String(value ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nconst logoUrl = 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\nconst flagUrl = request.country === 'TT'\n ? 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/Flag_of_Trinidad_and_Tobago.svg.webp'\n : 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/Flag_of_Guatemala.svg.webp';\n\nconst to = (resolved.recipients || []).join(',');\nconst subject = `Corrección requerida en BambooHR - ${request.countryName} - ${request.sourceName}`;\n\nconst reportButton = request.reportUrl\n ? `Abrir reporte de Cruce de Cuentas`\n : '';\n\nconst htmlBody = `\n\n\n\n\n
\n\n\n
 
\n\n\n\n
\"GomezLee\"Bandera
\n
\n
Corrección de identidad · BambooHR
\n

Revisión solicitada para ${escapeHtml(request.countryName)}

\n

Durante la revisión manual de Banco sin BambooHR se confirmó que el pago corresponde a una persona existente en BambooHR, pero su nombre no está permitiendo identificarla correctamente en el cruce.

\n\n\n\n\n\n\n\n\n
Nombre detectado${escapeHtml(request.sourceName)}
Empleado en BambooHR${escapeHtml(employee.name)}
Employee Number${escapeHtml(employee.employeeNumber || 'No disponible')}
Motivo${escapeHtml(request.reason)}
Período${escapeHtml(request.periodLabel || 'No especificado')}
Reportado por${escapeHtml(request.requestedByName)} · ${escapeHtml(request.requestedByEmail)}
\n\n${request.comment ? `
Comentario: ${escapeHtml(request.comment)}
` : ''}\n\n

Por favor, revisen y corrijan el nombre o la información correspondiente en BambooHR. La equivalencia validada ya quedó registrada en el Cruce de Cuentas para evitar que este mismo falso positivo se repita mientras se realiza la corrección.

\n\n${reportButton ? `
${reportButton}
` : ''}\n\n
\n
Notificación automática · Portal Cruce de Cuentas GLM · Uso interno
\n
\n
\n`;\n\nreturn [{\n json: {\n requestId,\n to,\n subject,\n htmlBody,\n country: request.country,\n countryName: request.countryName,\n aliasCount: Array.isArray(resolved.aliasPayload) ? resolved.aliasPayload.length : 0,\n },\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2160, + 0 + ], + "id": "68be3c1d-42eb-4bc5-aa35-36333261f2a6", + "name": "Preparar correo RRHH" + }, + { + "parameters": { + "sendTo": "={{$json.to}}", + "subject": "={{$json.subject}}", + "message": "={{$json.htmlBody}}", + "options": { + "appendAttribution": false + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.2, + "position": [ + 2400, + 0 + ], + "id": "28f7c657-0c5b-47c6-898b-f68f1cf21378", + "name": "Enviar correo RRHH", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 2000, + "webhookId": "8fd723dd-b333-4db8-9c43-2f5a9d86f86e", + "credentials": { + "gmailOAuth2": { + "id": "UDcO1FLJqA453V2D", + "name": "Gmail account 3" + } + } + }, + { + "parameters": { + "method": "PATCH", + "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cruce_cuentas_bamboo_correcciones?id=eq.' + $('Preparar correo RRHH').first().json.requestId }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Prefer", + "value": "return=minimal" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { correo_enviado: true, correo_enviado_at: new Date().toISOString(), estado: 'notificado_rrhh' } }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2640, + 0 + ], + "id": "87b7910e-0465-40b1-89d4-6b160726b7f7", + "name": "Marcar correo enviado", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1000 + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ { ok: true, message: 'Caso validado, equivalencia registrada y correo enviado a RRHH.', country: $('Preparar correo RRHH').first().json.country, countryName: $('Preparar correo RRHH').first().json.countryName, aliasCount: $('Preparar correo RRHH').first().json.aliasCount, requestId: $('Preparar correo RRHH').first().json.requestId } }}", + "options": { + "responseCode": 200, + "responseHeaders": { + "entries": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + } + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.5, + "position": [ + 2880, + 0 + ], + "id": "1db48431-4661-49de-a495-66c740b2aac1", + "name": "Respond to Webhook" + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Preparar solicitud", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar solicitud": { + "main": [ + [ + { + "node": "Validar acceso Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validar acceso Supabase": { + "main": [ + [ + { + "node": "Validar usuario", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validar usuario": { + "main": [ + [ + { + "node": "Consultar empleado BambooHR", + "type": "main", + "index": 0 + } + ] + ] + }, + "Consultar empleado BambooHR": { + "main": [ + [ + { + "node": "Resolver empleado BambooHR", + "type": "main", + "index": 0 + } + ] + ] + }, + "Resolver empleado BambooHR": { + "main": [ + [ + { + "node": "Guardar alias Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, + "Guardar alias Supabase": { + "main": [ + [ + { + "node": "Preparar auditoría", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar auditoría": { + "main": [ + [ + { + "node": "Guardar solicitud Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, + "Guardar solicitud Supabase": { + "main": [ + [ + { + "node": "Preparar correo RRHH", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar correo RRHH": { + "main": [ + [ + { + "node": "Enviar correo RRHH", + "type": "main", + "index": 0 + } + ] + ] + }, + "Enviar correo RRHH": { + "main": [ + [ + { + "node": "Marcar correo enviado", + "type": "main", + "index": 0 + } + ] + ] + }, + "Marcar correo enviado": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "Isaac Aracena", + "name": "Version 7e4daa40", + "description": "", + "autosaved": true, + "workflowPublishHistory": [ + { + "createdAt": "2026-08-11T18:47:55.639Z", + "id": 4454, + "workflowId": "0yaiagL1tWdhRUXy", + "versionId": "7e4daa40-244f-43ed-9d74-14a9cb589ddf", + "event": "activated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + ] + } +} \ No newline at end of file