{
"name": "Portal de Verificación de Nómina Puerto Rico - Envío de Reporte",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "cruce-cuentas-pr-marcar-resuelto",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
784,
592
],
"id": "67ea20ff-7e47-4605-be07-b9c1fea50d78",
"name": "Webhook Marcar Resuelto",
"webhookId": "9b76b71d-729d-4559-a6d8-4321986e4e5d"
},
{
"parameters": {
"jsCode": "const body = $input.first().json.body || $input.first().json || {};\n\nfunction clean(value) {\n return String(value ?? '').replace(/\\s+/g, ' ').trim();\n}\n\nconst reportId = clean(body.reportId || body.id);\nconst comentarioResolucion = clean(body.comentarioResolucion || body.comentario || '');\nconst resueltoPorNombre = clean(body.resueltoPorNombre || body.userName || 'Usuario GLM');\nconst resueltoPorEmail = clean(body.resueltoPorEmail || body.userEmail || '');\n\nconst errors = [];\n\nif (!reportId) {\n errors.push('No se recibió el ID del reporte.');\n}\n\nif (!comentarioResolucion) {\n errors.push('Debe indicar un comentario de resolución.');\n}\n\nif (comentarioResolucion.length < 10) {\n errors.push('El comentario de resolución debe ser más descriptivo.');\n}\n\nif (errors.length > 0) {\n return [\n {\n json: {\n ok: false,\n stage: 'validacion_resolucion',\n errors,\n reportId,\n },\n },\n ];\n}\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'resolucion_validada',\n reportId,\n comentarioResolucion,\n resueltoPorNombre,\n resueltoPorEmail,\n resolvedAt: new Date().toISOString(),\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
992,
592
],
"id": "57d081f4-b2ff-4272-b660-84b71692c0b8",
"name": "Validar resolución"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "c89884c6-de96-4b08-bd20-54644bb95bc7",
"leftValue": "={{ $json.ok }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1200,
592
],
"id": "0e91368c-172f-44bf-bf5f-0c1e4e4842e7",
"name": "¿Solicitud válida?"
},
{
"parameters": {
"method": "PATCH",
"url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes?id=eq.' + encodeURIComponent($json.reportId) + '&country=eq.PR' }}",
"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": "={{\n {\n estado: 'resuelto',\n resuelto_por_nombre: $json.resueltoPorNombre,\n resuelto_por_email: $json.resueltoPorEmail,\n resuelto_en: $json.resolvedAt,\n comentario_resolucion: $json.comentarioResolucion,\n updated_at: $json.resolvedAt\n }\n}}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1536,
480
],
"id": "13114fe0-2945-432d-ac06-02b46682b9c0",
"name": "Actualizar reporte Supabase"
},
{
"parameters": {
"jsCode": "const input = $input.first().json || {};\nconst report = Array.isArray(input) ? input[0] : input;\n\nif (!report || !report.id) {\n throw new Error(\n 'No se encontró un reporte de Puerto Rico con el ID recibido.'\n );\n}\n\nif (String(report.country || '').toUpperCase() !== 'PR') {\n throw new Error(\n 'El reporte localizado no pertenece a Puerto Rico.'\n );\n}\nconst original = $('Validar resolución').first().json || {};\n\n\nfunction escapeHtml(value) {\n return String(value ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction numberValue(value) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nconst periodo = report.period_label || 'Período no especificado';\nconst reportUrl = report.report_url || '';\n\nconst discrepancias = numberValue(report.discrepancias);\nconst bancoSinNomina = numberValue(report.banco_sin_nomina);\nconst bancoSinBamboo = numberValue(report.banco_sin_bamboo);\nconst nominaSinCuenta = numberValue(report.nomina_sin_cuenta);\n\nconst resolvedAt = original.resolvedAt || new Date().toISOString();\n\nconst resolvedDateLabel = new Date(resolvedAt).toLocaleString('es-PR', {\n timeZone: 'America/Puerto_Rico',\n dateStyle: 'long',\n timeStyle: 'short',\n});\n\nconst resolvedByName =\n original.resueltoPorNombre || 'Usuario GLM';\n\nconst resolvedByEmail =\n original.resueltoPorEmail || '';\n\nconst resolutionComment =\n original.comentarioResolucion || 'Sin comentario registrado.';\n\nconst logoUrl =\n 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\n\nconst flagUrl =\n 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/bandera-nacional-tela-puerto-rico_113767-1759.avif';\n\nconst subject =\n `Reporte resuelto - Cruce de Cuentas Puerto Rico - ${periodo}`;\n\nconst textBody = `\nHola equipo,\n\nSe marcó como resuelto un reporte de Cruce de Cuentas Puerto Rico.\n\nPeríodo:\n${periodo}\n\nResuelto por:\n${resolvedByName}${resolvedByEmail ? ` (${resolvedByEmail})` : ''}\n\nFecha/hora:\n${resolvedDateLabel}\n\nResumen:\n- Discrepancias: ${discrepancias}\n- Banco sin nómina: ${bancoSinNomina}\n- Banco sin Bamboo: ${bancoSinBamboo}\n- Nómina sin cuenta: ${nominaSinCuenta}\n\nComentario:\n${resolutionComment}\n\nGoogle Sheet:\n${reportUrl || 'No disponible'}\n\nSaludos,\nPortal Cruce de Cuentas GLM\n`.trim();\n\nconst htmlBody = `\n\n\n
\n \n \n ${escapeHtml(subject)}\n\n\n\n\n \n \n \n\n \n\n \n \n \n | \n \n\n \n | | \n \n\n \n \n \n Cruce de Cuentas Puerto Rico\n }\") \n \n\n \n Reporte marcado como resuelto\n \n | \n \n\n \n | \n \n Hola equipo,\n \n\n \n El siguiente reporte fue revisado y marcado como\n resuelto.\n \n | \n \n\n \n \n \n \n | \n Período\n | \n\n \n ${escapeHtml(periodo)}\n | \n \n\n \n | \n Resuelto por\n | \n\n \n ${escapeHtml(resolvedByName)}\n ${\n resolvedByEmail\n ? ` ${escapeHtml(resolvedByEmail)}`\n : ''\n }\n | \n \n\n \n | \n Fecha y hora\n | \n\n \n ${escapeHtml(resolvedDateLabel)}\n | \n \n \n | \n \n\n \n | \n \n Resumen del reporte\n \n | \n \n\n \n \n \n \n | \n \n ${discrepancias}\n \n\n \n Discrepancias\n \n | \n\n \n \n ${bancoSinBamboo}\n \n\n \n Banco sin Bamboo\n \n | \n\n \n \n ${bancoSinNomina}\n \n\n \n Banco sin nómina\n \n | \n\n \n \n ${nominaSinCuenta}\n \n\n \n Nómina sin cuenta\n \n | \n \n \n | \n \n\n \n | \n \n Comentario de resolución\n \n | \n \n\n \n | \n \n ${escapeHtml(resolutionComment).replace(/\\n/g, ' ')}\n \n | \n \n\n ${\n reportUrl\n ? `\n \n | \n \n Abrir Google Sheet\n \n | \n \n `\n : ''\n }\n\n \n | \n Mensaje automático del Portal Cruce de Cuentas GLM.\n | \n \n\n \n | \n
\n
\n\n\n\n`.trim();\n\nreturn [\n {\n json: {\n to: [\n 'iaracena@gomezleemarketing.com',\n 'jgomez@gomezleemarketing.com',\n 'mgomez@gomezleemarketing.com',\n 'vparamo@gomezleemarketing.com',\n 'ymadera@gomezleemarketing.com',\n 'adminregional@gomezleemarketing.com',\n ].join(','),\n subject,\n textBody,\n htmlBody,\n report,\n resolution: original,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1744,
480
],
"id": "f756f935-f1ab-4c87-b46a-eb159ce95cef",
"name": "Preparar correo equipo PR"
},
{
"parameters": {
"sendTo": "={{$json.to}}",
"subject": "={{$json.subject}}",
"message": "={{ $json.htmlBody }}",
"options": {
"appendAttribution": false,
"ccList": "={{ $json.cc }}"
}
},
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.2,
"position": [
1952,
480
],
"id": "127fa51c-cc53-41c9-8c5f-6c0cd65e8ca9",
"name": "Enviar correo equipo PR",
"webhookId": "ad819366-b163-4e58-86de-a7cd158630b1",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
}
},
{
"parameters": {
"jsCode": "const original = $('Validar resolución').first().json || {};\nconst updatedResponse = $('Actualizar reporte Supabase').first().json || {};\nconst updatedReport = Array.isArray(updatedResponse) ? updatedResponse[0] : updatedResponse;\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'reporte_resuelto',\n message: 'Reporte marcado como resuelto correctamente.',\n report: {\n id: original.reportId,\n estado: 'resuelto',\n estadoLabel: 'RESUELTO',\n country: 'PR',\n countryName: 'Puerto Rico',\n resueltoPorNombre: original.resueltoPorNombre,\n resueltoPorEmail: original.resueltoPorEmail,\n resueltoEn: original.resolvedAt,\n comentarioResolucion: original.comentarioResolucion,\n reportUrl: updatedReport.report_url || '',\n },\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2160,
480
],
"id": "7f19e2dd-b8a2-427c-83d3-345d18fbf267",
"name": "Preparar respuesta resuelto"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Access-Control-Allow-Methods",
"value": "GET,POST,OPTIONS"
},
{
"name": "Access-Control-Allow-Headers",
"value": "Content-Type, Authorization"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
2368,
480
],
"id": "ebb6b338-b872-4167-acea-85b9e235cb38",
"name": "Responder resuelto app"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Access-Control-Allow-Methods",
"value": "GET,POST,OPTIONS"
},
{
"name": "Access-Control-Allow-Headers",
"value": "Content-Type, Authorization"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1408,
688
],
"id": "cf478ef5-e94d-4fce-afe6-2a5a2b16c673",
"name": "Responder error resolución"
},
{
"parameters": {
"content": "# ✅ CIERRE DE REPORTE — PUERTO RICO\n\nRecibe desde el Portal de Verificación de Nóminas la solicitud para marcar un reporte de Puerto Rico como resuelto.\n\n## Validación inicial\n\nEl Webhook recibe la información enviada por la aplicación y valida datos como:\n\n- Identificador del reporte.\n- Usuario que realizó el cierre.\n- Estado solicitado.\n- Comentario o detalle de resolución.\n- Datos necesarios para localizar el registro en Supabase.\n\nLa solicitud solamente continúa cuando contiene información válida y suficiente.\n\n## Actualización del reporte\n\nCuando la solicitud es correcta:\n\n1. Localiza el reporte correspondiente en Supabase.\n2. Cambia su estado a RESUELTO.\n3. Registra quién realizó la resolución.\n4. Guarda la fecha y hora del cierre.\n5. Conserva el comentario o información de seguimiento disponible.\n\nSupabase es la fuente oficial del estado del reporte dentro del portal.\n\n## Notificación al equipo de Puerto Rico\n\nDespués de actualizar el registro:\n\n1. Se prepara un correo para el equipo responsable de Puerto Rico.\n2. Se incluyen los datos principales del reporte.\n3. Se informa que el caso fue marcado como resuelto.\n4. Se identifica al usuario que realizó el cierre.\n5. Se envía el correo mediante Gmail.\n\nLa notificación permite que el equipo conozca el cambio sin tener que revisar manualmente el portal.\n\n## Respuesta a la aplicación\n\nCuando el proceso termina correctamente:\n\n- Se construye una respuesta de éxito.\n- Se confirma que el reporte fue actualizado.\n- Se devuelve el nuevo estado a la aplicación.\n- El portal puede actualizar inmediatamente su interfaz.\n\n## Ruta de error\n\nCuando la solicitud es inválida:\n\n- No se modifica Supabase.\n- No se envía el correo al equipo de Puerto Rico.\n- Se devuelve una respuesta de error a la aplicación.\n- Se informa que faltan datos o que la solicitud no pudo procesarse.\n\n## Reglas\n\n- No marcar un reporte como resuelto sin un identificador válido.\n- No enviar la notificación antes de confirmar la actualización en Supabase.\n- No declarar éxito cuando la base de datos no fue actualizada.\n- Toda respuesta del Webhook debe cerrarse correctamente para evitar solicitudes abiertas.\n- La ruta de error debe terminar sin realizar cambios parciales.",
"height": 1264,
"width": 2624,
"color": "#28765F"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
0,
0
],
"id": "e8c96878-0be9-4c70-bea5-e445119cad84",
"name": "Sticky Note"
}
],
"pinData": {},
"connections": {
"Webhook Marcar Resuelto": {
"main": [
[
{
"node": "Validar resolución",
"type": "main",
"index": 0
}
]
]
},
"Validar resolución": {
"main": [
[
{
"node": "¿Solicitud válida?",
"type": "main",
"index": 0
}
]
]
},
"¿Solicitud válida?": {
"main": [
[
{
"node": "Actualizar reporte Supabase",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder error resolución",
"type": "main",
"index": 0
}
]
]
},
"Actualizar reporte Supabase": {
"main": [
[
{
"node": "Preparar correo equipo PR",
"type": "main",
"index": 0
}
]
]
},
"Preparar respuesta resuelto": {
"main": [
[
{
"node": "Responder resuelto app",
"type": "main",
"index": 0
}
]
]
},
"Preparar correo equipo PR": {
"main": [
[
{
"node": "Enviar correo equipo PR",
"type": "main",
"index": 0
}
]
]
},
"Enviar correo equipo PR": {
"main": [
[
{
"node": "Preparar respuesta resuelto",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"versionId": "b51451ef-379a-479a-88b4-cfcd3e183c20",
"meta": {
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
},
"tags": []
}