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 \n\nCorrección de identidad · BambooHR \nRevisión solicitada para ${escapeHtml(request.countryName)}\nDurante 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| Nombre detectado | ${escapeHtml(request.sourceName)} | \n| Empleado en BambooHR | ${escapeHtml(employee.name)} | \n| Employee Number | ${escapeHtml(employee.employeeNumber || 'No disponible')} | \n| Motivo | ${escapeHtml(request.reason)} | \n| Período | ${escapeHtml(request.periodLabel || 'No especificado')} | \n| Reportado por | ${escapeHtml(request.requestedByName)} · ${escapeHtml(request.requestedByEmail)} | \n \n\n${request.comment ? `Comentario: ${escapeHtml(request.comment)} ` : ''}\n\nPor 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\nNotificació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 \n\nCorrección de identidad · BambooHR \nRevisión solicitada para ${escapeHtml(request.countryName)}\nDurante 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| Nombre detectado | ${escapeHtml(request.sourceName)} | \n| Empleado en BambooHR | ${escapeHtml(employee.name)} | \n| Employee Number | ${escapeHtml(employee.employeeNumber || 'No disponible')} | \n| Motivo | ${escapeHtml(request.reason)} | \n| Período | ${escapeHtml(request.periodLabel || 'No especificado')} | \n| Reportado por | ${escapeHtml(request.requestedByName)} · ${escapeHtml(request.requestedByEmail)} | \n \n\n${request.comment ? `Comentario: ${escapeHtml(request.comment)} ` : ''}\n\nPor 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\nNotificació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