diff --git a/Seguimiento de Impuestos GLM - Buscar contactos en BambooHR.json b/Seguimiento de Impuestos GLM - Buscar contactos en BambooHR.json
new file mode 100644
index 0000000..9edce69
--- /dev/null
+++ b/Seguimiento de Impuestos GLM - Buscar contactos en BambooHR.json
@@ -0,0 +1,299 @@
+{
+ "name": "Seguimiento de Impuestos GLM - Buscar contactos en BambooHR",
+ "nodes": [
+ {
+ "parameters": {
+ "httpMethod": "POST",
+ "path": "seguimiento-impuestos-bamboohr-contacto",
+ "responseMode": "responseNode",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.webhook",
+ "typeVersion": 2.1,
+ "position": [
+ -1040,
+ 80
+ ],
+ "id": "83895861-f8e5-48bb-8eac-128ad55295f6",
+ "name": "Webhook - Buscar contacto BambooHR",
+ "webhookId": "seguimiento-impuestos-bamboohr-contacto"
+ },
+ {
+ "parameters": {
+ "jsCode": "let body = $json.body ?? $json;\nif (typeof body === 'string') {\n try { body = JSON.parse(body); }\n catch { body = {}; }\n}\nconst fullName = String(body.full_name ?? body.name ?? '').trim().replace(/\\s+/g, ' ');\nconst valid = fullName.split(' ').filter(Boolean).length >= 2 && fullName.length >= 5;\nreturn [{\n json: {\n valid,\n full_name: fullName,\n requested_by: String(body.requested_by ?? '').trim(),\n message: valid ? '' : 'Escribe el nombre y apellido de la persona.',\n },\n}];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ -800,
+ 80
+ ],
+ "id": "b3415338-ef9e-4dfc-9f95-133caf07e9e4",
+ "name": "Validar solicitud"
+ },
+ {
+ "parameters": {
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict",
+ "version": 2
+ },
+ "conditions": [
+ {
+ "id": "ac176ddc-1e99-4d7b-b9f9-bea5a4182baa",
+ "leftValue": "={{ $json.valid }}",
+ "rightValue": true,
+ "operator": {
+ "type": "boolean",
+ "operation": "true",
+ "singleValue": true
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "options": {}
+ },
+ "type": "n8n-nodes-base.if",
+ "typeVersion": 2.2,
+ "position": [
+ -560,
+ 80
+ ],
+ "id": "b9a78df7-ddbd-4b20-9bad-0912bd6e7c41",
+ "name": "Solicitud válida"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://glm.bamboohr.com/api/v1/reports/custom?format=JSON&onlyCurrent=false",
+ "authentication": "genericCredentialType",
+ "genericAuthType": "httpBasicAuth",
+ "sendHeaders": true,
+ "headerParameters": {
+ "parameters": [
+ {
+ "name": "Accept",
+ "value": "application/json"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": {
+ "title": "Información completa de empleados BambooHR - Seguimiento de Impuestos",
+ "fields": [
+ "firstName",
+ "middleName",
+ "lastName",
+ "displayName",
+ "department",
+ "division",
+ "location",
+ "customPosicion-Cliente",
+ "hireDate",
+ "originalHireDate",
+ "status",
+ "employeeNumber",
+ "workEmail",
+ "terminationDate",
+ "supervisor",
+ "startDate",
+ "jobTitle",
+ "employmentStatus",
+ "email",
+ "country",
+ "city",
+ "address1"
+ ]
+ },
+ "options": {
+ "response": {
+ "response": {
+ "responseFormat": "json"
+ }
+ },
+ "timeout": 300000
+ }
+ },
+ "id": "9ddaf55d-54a9-4059-bc9b-3142cef78ecb",
+ "name": "Generar reporte completo BambooHR",
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ -304,
+ 0
+ ],
+ "retryOnFail": true,
+ "maxTries": 3,
+ "waitBetweenTries": 2000,
+ "credentials": {
+ "httpBasicAuth": {
+ "id": "7VrpNZ2jBLmiJ35q",
+ "name": "BambooHR GLM Full Access"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "jsCode": "const query = String($('Validar solicitud').item.json.full_name ?? '').trim();\nconst response = $json ?? {};\nconst employees = Array.isArray(response.employees)\n ? response.employees\n : Array.isArray(response.data)\n ? response.data\n : Array.isArray(response)\n ? response\n : [];\n\nconst normalize = (value) => String(value ?? '')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst tokens = (value) => normalize(value).split(' ').filter((token) => token.length > 1);\n\nconst levenshtein = (a, b) => {\n if (!a) return b.length;\n if (!b) return a.length;\n const previous = Array.from({ length: b.length + 1 }, (_, index) => index);\n for (let i = 1; i <= a.length; i += 1) {\n let diagonal = previous[0];\n previous[0] = i;\n for (let j = 1; j <= b.length; j += 1) {\n const old = previous[j];\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n previous[j] = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + cost);\n diagonal = old;\n }\n }\n return previous[b.length];\n};\n\nconst similarity = (a, b) => {\n const longest = Math.max(a.length, b.length, 1);\n return 1 - levenshtein(a, b) / longest;\n};\n\nconst tokenF1 = (aTokens, bTokens) => {\n if (!aTokens.length || !bTokens.length) return 0;\n const a = new Set(aTokens);\n const b = new Set(bTokens);\n const common = [...a].filter((token) => b.has(token)).length;\n if (!common) return 0;\n const precision = common / b.size;\n const recall = common / a.size;\n return (2 * precision * recall) / (precision + recall);\n};\n\nconst countryAliases = [\n { code: 'DO', name: 'República Dominicana', aliases: ['dominican republic', 'republica dominicana', 'santo domingo', 'dominicana', 'rep dom'] },\n { code: 'PR', name: 'Puerto Rico', aliases: ['puerto rico'] },\n { code: 'JM', name: 'Jamaica', aliases: ['jamaica'] },\n { code: 'TT', name: 'Trinidad y Tobago', aliases: ['trinidad and tobago', 'trinidad & tobago', 'trinidad y tobago', 'trinidad', 'tobago'] },\n { code: 'SV', name: 'El Salvador', aliases: ['el salvador', 'salvador'] },\n { code: 'GT', name: 'Guatemala', aliases: ['guatemala'] },\n { code: 'HN', name: 'Honduras', aliases: ['honduras'] },\n { code: 'CR', name: 'Costa Rica', aliases: ['costa rica'] },\n { code: 'CO', name: 'Colombia', aliases: ['colombia'] },\n { code: 'MX', name: 'México', aliases: ['mexico', 'méxico'] },\n { code: 'NI', name: 'Nicaragua', aliases: ['nicaragua'] },\n { code: 'PA', name: 'Panamá', aliases: ['panama', 'panamá'] },\n];\n\nconst resolveCountry = (employee) => {\n const haystack = normalize([\n employee.country,\n employee.location,\n employee.city,\n employee.division,\n employee.department,\n employee.address1,\n ].filter(Boolean).join(' '));\n\n if (/\\bregional\\b|\\bregion\\b/.test(haystack)) {\n return { code: 'REG', name: 'Regional' };\n }\n\n for (const country of countryAliases) {\n if (country.aliases.some((alias) => haystack.includes(normalize(alias)))) {\n return { code: country.code, name: country.name };\n }\n }\n\n return { code: 'REG', name: 'Regional' };\n};\n\nconst resolveArea = (employee) => {\n const text = normalize([\n employee.department,\n employee.division,\n employee.jobTitle,\n employee['customPosicion-Cliente'],\n ].filter(Boolean).join(' '));\n\n if (/nomina|payroll|compensacion|compensation|beneficios|benefits/.test(text)) return 'nomina';\n if (/administracion|administrativo|administrative|finance|finanzas|contabilidad|accounting|impuesto|tax|tesoreria|treasury|conciliacion|compras|procurement/.test(text)) return 'admin';\n return 'other';\n};\n\n\nconst queryNormalized = normalize(query);\nconst queryTokens = tokens(query);\nconst candidates = [];\n\nfor (const employee of employees) {\n const constructedName = [employee.firstName, employee.middleName, employee.lastName]\n .filter(Boolean)\n .join(' ')\n .replace(/\\s+/g, ' ')\n .trim();\n const displayName = String(employee.displayName ?? '').trim();\n const fullName = displayName || constructedName;\n if (!fullName) continue;\n\n const nameVariants = [...new Set([fullName, constructedName, displayName].filter(Boolean))];\n let score = 0;\n for (const variant of nameVariants) {\n const normalizedVariant = normalize(variant);\n const variantTokens = tokens(variant);\n let current = 0;\n if (normalizedVariant === queryNormalized) current = 100;\n else if (normalizedVariant.startsWith(queryNormalized)) current = 91;\n else if (normalizedVariant.includes(queryNormalized)) current = 85;\n else if (queryNormalized.includes(normalizedVariant)) current = 78;\n\n const f1 = tokenF1(queryTokens, variantTokens);\n const editSimilarity = similarity(queryNormalized, normalizedVariant);\n current = Math.max(current, f1 * 82, editSimilarity * 76);\n if (queryTokens.every((token) => variantTokens.includes(token))) current += 8;\n score = Math.max(score, current);\n }\n\n const status = String(employee.status ?? employee.employmentStatus ?? '').trim();\n if (/active|activo/i.test(status)) score += 3;\n if (score < 45) continue;\n\n const country = resolveCountry(employee);\n const email = String(employee.workEmail || employee.email || '').trim().toLowerCase();\n\n candidates.push({\n id: String(employee.id ?? employee.employeeId ?? employee.employeeNumber ?? fullName),\n employeeNumber: String(employee.employeeNumber ?? ''),\n fullName,\n displayName: displayName || fullName,\n email,\n jobTitle: String(employee.jobTitle ?? employee['customPosicion-Cliente'] ?? '').trim(),\n department: String(employee.department ?? '').trim(),\n division: String(employee.division ?? '').trim(),\n location: String(employee.location ?? employee.city ?? '').trim(),\n countryCode: country.code,\n countryName: country.name,\n area: resolveArea(employee),\n status,\n score: Math.min(100, Math.round(score)),\n matchQuality: score >= 95 ? 'exacta' : score >= 72 ? 'alta' : 'media',\n });\n}\n\nconst unique = new Map();\nfor (const candidate of candidates.sort((a, b) => {\n const activeA = /active|activo/i.test(a.status) ? 1 : 0;\n const activeB = /active|activo/i.test(b.status) ? 1 : 0;\n return b.score - a.score || activeB - activeA || a.fullName.localeCompare(b.fullName);\n})) {\n const key = candidate.id || candidate.email || normalize(candidate.fullName);\n if (!unique.has(key)) unique.set(key, candidate);\n}\n\nreturn [{\n json: {\n ok: true,\n query,\n count: Math.min(unique.size, 8),\n candidates: [...unique.values()].slice(0, 8),\n },\n}];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ -48,
+ 0
+ ],
+ "id": "4742718e-f429-4096-a974-f811e4cd5a91",
+ "name": "Buscar coincidencias robustas"
+ },
+ {
+ "parameters": {
+ "respondWith": "json",
+ "responseBody": "={{ $json }}",
+ "options": {
+ "responseCode": 200,
+ "responseHeaders": {
+ "entries": [
+ {
+ "name": "Access-Control-Allow-Origin",
+ "value": "*"
+ },
+ {
+ "name": "Cache-Control",
+ "value": "no-store"
+ }
+ ]
+ }
+ }
+ },
+ "type": "n8n-nodes-base.respondToWebhook",
+ "typeVersion": 1.4,
+ "position": [
+ 208,
+ 0
+ ],
+ "id": "d7250da6-9a93-42c8-b7c8-c28adadbe7f1",
+ "name": "Responder coincidencias"
+ },
+ {
+ "parameters": {
+ "respondWith": "json",
+ "responseBody": "={{ { ok: false, message: $json.message, candidates: [] } }}",
+ "options": {
+ "responseCode": 400,
+ "responseHeaders": {
+ "entries": [
+ {
+ "name": "Access-Control-Allow-Origin",
+ "value": "*"
+ }
+ ]
+ }
+ }
+ },
+ "type": "n8n-nodes-base.respondToWebhook",
+ "typeVersion": 1.4,
+ "position": [
+ -304,
+ 192
+ ],
+ "id": "1ed906bf-3e3c-42ea-9978-df44387c1f44",
+ "name": "Responder solicitud inválida"
+ },
+ {
+ "parameters": {
+ "content": "## BÚSQUEDA DE CONTACTOS EN BAMBOOHR\n\nRecibe desde la aplicación el nombre de una persona y busca coincidencias dentro del directorio actualizado de empleados de BambooHR.\n\n- Valida que la solicitud incluya un nombre válido antes de consultar BambooHR.\n- Devuelve una respuesta controlada cuando faltan datos o la solicitud es incorrecta.\n- Obtiene el reporte completo de empleados con su información laboral y de contacto.\n- Normaliza nombres, espacios, mayúsculas y caracteres especiales para realizar una búsqueda más confiable.\n- Identifica coincidencias exactas y aproximadas para reducir errores por nombres incompletos o diferencias de escritura.\n- Devuelve a la aplicación las personas encontradas para agregarlas automáticamente o permitir que el usuario seleccione la coincidencia correcta.",
+ "height": 576,
+ "width": 1552,
+ "color": 6
+ },
+ "type": "n8n-nodes-base.stickyNote",
+ "typeVersion": 1,
+ "position": [
+ -1136,
+ -224
+ ],
+ "id": "549c09fb-34a0-44d8-b5a7-7cb71f804025",
+ "name": "Sticky Note"
+ }
+ ],
+ "pinData": {},
+ "connections": {
+ "Webhook - Buscar contacto BambooHR": {
+ "main": [
+ [
+ {
+ "node": "Validar solicitud",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Validar solicitud": {
+ "main": [
+ [
+ {
+ "node": "Solicitud válida",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Solicitud válida": {
+ "main": [
+ [
+ {
+ "node": "Generar reporte completo BambooHR",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Responder solicitud inválida",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Generar reporte completo BambooHR": {
+ "main": [
+ [
+ {
+ "node": "Buscar coincidencias robustas",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Buscar coincidencias robustas": {
+ "main": [
+ [
+ {
+ "node": "Responder coincidencias",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": true,
+ "settings": {
+ "executionOrder": "v1",
+ "binaryMode": "separate"
+ },
+ "versionId": "4cc439c7-5187-44ab-a0ca-dc0230cdfcbc",
+ "meta": {
+ "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
+ },
+ "id": "nZ53koYjMAYN5tTe",
+ "tags": []
+}
\ No newline at end of file
diff --git a/Seguimiento de Impuestos GLM - Google Calendar.json b/Seguimiento de Impuestos GLM - Google Calendar.json
new file mode 100644
index 0000000..833fe49
--- /dev/null
+++ b/Seguimiento de Impuestos GLM - Google Calendar.json
@@ -0,0 +1,824 @@
+{
+ "name": "Seguimiento de Impuestos GLM - Google Calendar",
+ "nodes": [
+ {
+ "parameters": {
+ "httpMethod": "POST",
+ "path": "seguimiento-impuestos-google-calendar",
+ "responseMode": "responseNode",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.webhook",
+ "typeVersion": 2.1,
+ "position": [
+ -1648,
+ 432
+ ],
+ "id": "c1faefdb-cec7-4565-9397-f2ff1487dfba",
+ "name": "Webhook - Google Calendar",
+ "webhookId": "seguimiento-impuestos-google-calendar"
+ },
+ {
+ "parameters": {
+ "jsCode": "let body = $json.body ?? $json;\nif (typeof body === 'string') {\n try { body = JSON.parse(body); }\n catch { body = {}; }\n}\nconst action = String(body.action ?? '').trim().toLowerCase();\nconst payload = body.payload ?? {};\nconst obligationId = String(payload.obligation_id ?? body.obligation_id ?? '').trim();\nconst allowed = ['obligation.created', 'obligation.updated', 'obligation.deleted'];\nreturn [{\n json: {\n valid: allowed.includes(action) && Boolean(obligationId),\n action,\n obligation_id: obligationId,\n message: allowed.includes(action)\n ? (obligationId ? '' : 'Falta obligation_id.')\n : 'Acción no soportada.',\n },\n}];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ -1408,
+ 432
+ ],
+ "id": "a052bc4b-6571-4105-8d41-634152a727b9",
+ "name": "Preparar solicitud"
+ },
+ {
+ "parameters": {
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict",
+ "version": 2
+ },
+ "conditions": [
+ {
+ "id": "771f008a-734d-436d-9122-448758a2263a",
+ "leftValue": "={{ $json.valid }}",
+ "rightValue": true,
+ "operator": {
+ "type": "boolean",
+ "operation": "true",
+ "singleValue": true
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "options": {}
+ },
+ "type": "n8n-nodes-base.if",
+ "typeVersion": 2.2,
+ "position": [
+ -1168,
+ 432
+ ],
+ "id": "8b860c5e-b8f6-4d90-9829-a192c5bd807e",
+ "name": "Solicitud válida"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_google_calendar_payload",
+ "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"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ { p_obligation_id: $json.obligation_id } }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ -912,
+ 336
+ ],
+ "id": "b20b6a2b-6ec1-4e6c-a8eb-5d10404ab80e",
+ "name": "Supabase - Obtener obligación y asistentes"
+ },
+ {
+ "parameters": {
+ "jsCode": "const source = $('Preparar solicitud').item.json;\nconst value = $json;\n\nconst row = Array.isArray(value)\n ? value[0]\n : Array.isArray(value?.data)\n ? value.data[0]\n : value;\n\nif (!row?.obligation_id) {\n return [\n {\n json: {\n ...source,\n operation: 'noop',\n reason: 'No se encontró la obligación.',\n },\n },\n ];\n}\n\nconst formatDateDMY = (value) => {\n const iso = String(value ?? '').trim();\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso);\n\n if (!match) {\n return iso;\n }\n\n const [, year, month, day] = match;\n return `${day}-${month}-${year}`;\n};\n\nconst attendees = Array.isArray(row.attendees)\n ? [\n ...new Set(\n row.attendees\n .map((email) => String(email ?? '').trim().toLowerCase())\n .filter(Boolean),\n ),\n ]\n : [];\n\nconst start = `${row.due_date}T08:00:00-04:00`;\nconst end = `${row.due_date}T09:00:00-04:00`;\n\nconst categoryLabel =\n row.category === 'nomina'\n ? 'Nómina'\n : 'Administración';\n\nconst summary =\n `Impuestos · ${row.country_name} · ${row.description}`;\n\nconst description = [\n 'Seguimiento de Impuestos GLM',\n '',\n `País: ${row.country_name}`,\n `Categoría: ${categoryLabel}`,\n `Fecha límite: ${formatDateDMY(row.due_date)}`,\n `Obligación: ${row.description}`,\n '',\n 'Abrir aplicación: https://digitalcompass.agency/calendario-impuestos/',\n].join('\\n');\n\nconst calendarBody = {\n summary,\n description,\n\n start: {\n dateTime: start,\n timeZone: 'America/Santo_Domingo',\n },\n\n end: {\n dateTime: end,\n timeZone: 'America/Santo_Domingo',\n },\n\n attendees: attendees.map((email) => ({\n email,\n })),\n\n reminders: {\n useDefault: false,\n overrides: [\n {\n method: 'email',\n minutes: 0,\n },\n {\n method: 'popup',\n minutes: 0,\n },\n ],\n },\n\n extendedProperties: {\n private: {\n glmTaxObligationId: row.obligation_id,\n glmTaxCountryCode: row.country_code,\n glmTaxCategory: row.category,\n },\n },\n};\n\nlet operation = 'create';\n\nif (\n source.action === 'obligation.deleted' ||\n row.active === false\n) {\n operation = row.google_event_id\n ? 'delete'\n : 'noop';\n} else if (row.google_event_id) {\n operation = 'update';\n}\n\nreturn [\n {\n json: {\n ...source,\n ...row,\n attendees,\n start,\n end,\n summary,\n calendar_description: description,\n calendar_body: calendarBody,\n calendar_id: row.calendar_id || 'primary',\n operation,\n },\n },\n];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ -656,
+ 336
+ ],
+ "id": "86e2c9f1-46cb-42b0-83a2-43d420b6a85f",
+ "name": "Preparar payload Calendar"
+ },
+ {
+ "parameters": {
+ "jsCode": "return $input.all().filter((item) => item.json.operation === 'create');"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 64,
+ -64
+ ],
+ "id": "2d6990ec-a638-463b-8364-7d6a77b9bb9b",
+ "name": "Preparar crear"
+ },
+ {
+ "parameters": {
+ "jsCode": "return $input.all().filter((item) => item.json.operation === 'update');"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 80,
+ 352
+ ],
+ "id": "868962b2-1894-41e4-9028-598b96a9ccc1",
+ "name": "Preparar actualizar"
+ },
+ {
+ "parameters": {
+ "jsCode": "return $input.all().filter((item) => item.json.operation === 'delete');"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 192,
+ 1088
+ ],
+ "id": "57b9d7e3-084a-4d95-8ba2-309b83a71551",
+ "name": "Preparar eliminar"
+ },
+ {
+ "parameters": {
+ "jsCode": "return $input.all().filter((item) => item.json.operation === 'noop');"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 272,
+ 1344
+ ],
+ "id": "8ffaf543-58e2-45c6-bcd3-0e56d9c18bc7",
+ "name": "Preparar sin acción"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://www.googleapis.com/calendar/v3/calendars/primary/events?sendUpdates=all",
+ "authentication": "predefinedCredentialType",
+ "nodeCredentialType": "googleCalendarOAuth2Api",
+ "sendHeaders": true,
+ "headerParameters": {
+ "parameters": [
+ {
+ "name": "Content-Type",
+ "value": "application/json"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ $json.calendar_body }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 320,
+ -64
+ ],
+ "id": "6a998d06-8660-4b98-9f5b-58af9d92bda1",
+ "name": "Google Calendar - Crear evento",
+ "credentials": {
+ "googleCalendarOAuth2Api": {
+ "id": "QabWebE9XbAY72tU",
+ "name": "Google Calendar account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "method": "PATCH",
+ "url": "={{ 'https://www.googleapis.com/calendar/v3/calendars/primary/events/' + encodeURIComponent($json.google_event_id) + '?sendUpdates=all' }}",
+ "authentication": "predefinedCredentialType",
+ "nodeCredentialType": "googleCalendarOAuth2Api",
+ "sendHeaders": true,
+ "headerParameters": {
+ "parameters": [
+ {
+ "name": "Content-Type",
+ "value": "application/json"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ $json.calendar_body }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 336,
+ 352
+ ],
+ "id": "7831e93c-af0a-4505-80c0-c9707d5abff0",
+ "name": "Google Calendar - Actualizar evento",
+ "credentials": {
+ "googleCalendarOAuth2Api": {
+ "id": "QabWebE9XbAY72tU",
+ "name": "Google Calendar account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "method": "DELETE",
+ "url": "={{ 'https://www.googleapis.com/calendar/v3/calendars/primary/events/' + encodeURIComponent($json.google_event_id) + '?sendUpdates=all' }}",
+ "authentication": "predefinedCredentialType",
+ "nodeCredentialType": "googleCalendarOAuth2Api",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 448,
+ 1088
+ ],
+ "id": "d43bb286-0ce0-4e44-a9d9-5be15e325877",
+ "name": "Google Calendar - Eliminar evento",
+ "credentials": {
+ "googleCalendarOAuth2Api": {
+ "id": "QabWebE9XbAY72tU",
+ "name": "Google Calendar account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "jsCode": "const source = $('Preparar crear').item.json; return [{ json: { ...source, google_event_id: String($json.id ?? ''), google_response: $json, sync_action: 'create', sync_status: 'active' } }];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 576,
+ -64
+ ],
+ "id": "3bc4fa25-5ab2-4f01-a36f-bb3ab678cff7",
+ "name": "Preparar registro creado"
+ },
+ {
+ "parameters": {
+ "jsCode": "const source = $('Preparar actualizar').item.json; return [{ json: { ...source, google_event_id: source.google_event_id, google_response: $json, sync_action: 'update', sync_status: 'active' } }];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 592,
+ 352
+ ],
+ "id": "19961094-0e2b-4b63-8afe-5996a8fe2d46",
+ "name": "Preparar registro actualizado"
+ },
+ {
+ "parameters": {
+ "jsCode": "const source = $('Preparar eliminar').item.json; return [{ json: { ...source, google_event_id: source.google_event_id, google_response: $json ?? {}, sync_action: 'delete', sync_status: 'deleted' } }];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 704,
+ 1088
+ ],
+ "id": "2108e3aa-935b-4fa3-ad66-1ddce0676212",
+ "name": "Preparar registro eliminado"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_record_google_calendar_sync",
+ "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"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ { p_obligation_id: $('Preparar registro creado').item.json.obligation_id, p_google_event_id: $('Preparar registro creado').item.json.google_event_id, p_calendar_id: 'primary', p_attendees: $('Preparar registro creado').item.json.attendees, p_status: $('Preparar registro creado').item.json.sync_status, p_last_action: $('Preparar registro creado').item.json.sync_action, p_provider_response: $('Preparar registro creado').item.json.google_response } }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 832,
+ -64
+ ],
+ "id": "bf61e834-6dd9-4838-b74a-927839a91ddc",
+ "name": "Supabase - Registrar creación Calendar"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_record_google_calendar_sync",
+ "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"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ { p_obligation_id: $('Preparar registro actualizado').item.json.obligation_id, p_google_event_id: $('Preparar registro actualizado').item.json.google_event_id, p_calendar_id: 'primary', p_attendees: $('Preparar registro actualizado').item.json.attendees, p_status: $('Preparar registro actualizado').item.json.sync_status, p_last_action: $('Preparar registro actualizado').item.json.sync_action, p_provider_response: $('Preparar registro actualizado').item.json.google_response } }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 848,
+ 352
+ ],
+ "id": "78bda986-5713-427a-8852-d791dbc8a1cc",
+ "name": "Supabase - Registrar actualización Calendar"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_record_google_calendar_sync",
+ "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"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ { p_obligation_id: $('Preparar registro eliminado').item.json.obligation_id, p_google_event_id: $('Preparar registro eliminado').item.json.google_event_id, p_calendar_id: 'primary', p_attendees: $('Preparar registro eliminado').item.json.attendees, p_status: $('Preparar registro eliminado').item.json.sync_status, p_last_action: $('Preparar registro eliminado').item.json.sync_action, p_provider_response: $('Preparar registro eliminado').item.json.google_response } }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 960,
+ 1088
+ ],
+ "id": "b05991ba-18fa-498a-a52e-f3b4bf296793",
+ "name": "Supabase - Registrar eliminación Calendar"
+ },
+ {
+ "parameters": {
+ "respondWith": "json",
+ "responseBody": "={{ { ok: true, message: 'Evento creado en Google Calendar.' } }}",
+ "options": {
+ "responseCode": 200,
+ "responseHeaders": {
+ "entries": [
+ {
+ "name": "Access-Control-Allow-Origin",
+ "value": "*"
+ },
+ {
+ "name": "Cache-Control",
+ "value": "no-store"
+ }
+ ]
+ }
+ }
+ },
+ "type": "n8n-nodes-base.respondToWebhook",
+ "typeVersion": 1.4,
+ "position": [
+ 1088,
+ -64
+ ],
+ "id": "8237b91a-ec81-4e45-9641-9138e354febe",
+ "name": "Responder creación"
+ },
+ {
+ "parameters": {
+ "respondWith": "json",
+ "responseBody": "={{ { ok: true, message: 'Evento actualizado en Google Calendar.' } }}",
+ "options": {
+ "responseCode": 200,
+ "responseHeaders": {
+ "entries": [
+ {
+ "name": "Access-Control-Allow-Origin",
+ "value": "*"
+ },
+ {
+ "name": "Cache-Control",
+ "value": "no-store"
+ }
+ ]
+ }
+ }
+ },
+ "type": "n8n-nodes-base.respondToWebhook",
+ "typeVersion": 1.4,
+ "position": [
+ 1104,
+ 352
+ ],
+ "id": "954c7fd8-82d7-4460-9e0e-53c5e1bb30be",
+ "name": "Responder actualización"
+ },
+ {
+ "parameters": {
+ "respondWith": "json",
+ "responseBody": "={{ { ok: true, message: 'Evento eliminado de Google Calendar.' } }}",
+ "options": {
+ "responseCode": 200,
+ "responseHeaders": {
+ "entries": [
+ {
+ "name": "Access-Control-Allow-Origin",
+ "value": "*"
+ },
+ {
+ "name": "Cache-Control",
+ "value": "no-store"
+ }
+ ]
+ }
+ }
+ },
+ "type": "n8n-nodes-base.respondToWebhook",
+ "typeVersion": 1.4,
+ "position": [
+ 1216,
+ 1088
+ ],
+ "id": "c4542b04-2b4e-49da-b189-ed6d4fda092d",
+ "name": "Responder eliminación"
+ },
+ {
+ "parameters": {
+ "respondWith": "json",
+ "responseBody": "={{ { ok: true, message: $json.reason || 'No había cambios que sincronizar.' } }}",
+ "options": {
+ "responseCode": 200,
+ "responseHeaders": {
+ "entries": [
+ {
+ "name": "Access-Control-Allow-Origin",
+ "value": "*"
+ }
+ ]
+ }
+ }
+ },
+ "type": "n8n-nodes-base.respondToWebhook",
+ "typeVersion": 1.4,
+ "position": [
+ 528,
+ 1344
+ ],
+ "id": "8a17b617-3311-47a9-9ac6-153894215600",
+ "name": "Responder sin acción"
+ },
+ {
+ "parameters": {
+ "respondWith": "json",
+ "responseBody": "={{ { ok: false, message: $json.message } }}",
+ "options": {
+ "responseCode": 400,
+ "responseHeaders": {
+ "entries": [
+ {
+ "name": "Access-Control-Allow-Origin",
+ "value": "*"
+ }
+ ]
+ }
+ }
+ },
+ "type": "n8n-nodes-base.respondToWebhook",
+ "typeVersion": 1.4,
+ "position": [
+ -912,
+ 608
+ ],
+ "id": "328640f6-5d22-46a0-89dc-46580ec2f6a8",
+ "name": "Responder solicitud inválida"
+ },
+ {
+ "parameters": {
+ "content": "## 1. VALIDACIÓN Y PREPARACIÓN\n\nRecibe desde la aplicación una solicitud relacionada con un evento fiscal y prepara toda la información necesaria para procesarla.\n\n- Normaliza los datos recibidos y determina la acción solicitada.\n- Valida que la obligación, las fechas y los parámetros obligatorios sean correctos.\n- Devuelve una respuesta controlada cuando la solicitud es inválida.\n- Consulta en Supabase la obligación y las personas que deben asistir al evento.\n- Construye el payload final que se utilizará para crear, actualizar, eliminar o ignorar el evento en Google Calendar.",
+ "height": 640,
+ "width": 1248,
+ "color": 3
+ },
+ "type": "n8n-nodes-base.stickyNote",
+ "typeVersion": 1,
+ "position": [
+ -1744,
+ 144
+ ],
+ "id": "11ca21e4-8360-45fe-a2d3-6ce1cccde67a",
+ "name": "Sticky Note"
+ },
+ {
+ "parameters": {
+ "content": "## 2. CREACIÓN Y ACTUALIZACIÓN\n\nGestiona la creación de nuevos eventos y la actualización de eventos existentes en Google Calendar.\n\n- Prepara los datos específicos según la operación solicitada.\n- Crea o actualiza el evento con sus fechas, descripción y asistentes.\n- Normaliza la respuesta devuelta por Google Calendar.\n- Registra en Supabase el identificador del evento y el resultado de la sincronización.\n- Devuelve a la aplicación una confirmación de la operación realizada.",
+ "height": 864,
+ "width": 1360,
+ "color": 5
+ },
+ "type": "n8n-nodes-base.stickyNote",
+ "typeVersion": 1,
+ "position": [
+ -64,
+ -288
+ ],
+ "id": "60ca5c9c-62fa-40ac-93a2-653efbe41002",
+ "name": "Sticky Note1"
+ },
+ {
+ "parameters": {
+ "content": "## 3. ELIMINACIÓN Y CONTROL SIN ACCIÓN\n\nGestiona la eliminación de eventos y los casos en los que no es necesario modificar Google Calendar.\n\n- Prepara y ejecuta la eliminación del evento correspondiente.\n- Registra en Supabase que el evento fue eliminado y limpia su referencia.\n- Devuelve una confirmación cuando la eliminación finaliza correctamente.\n- Detecta solicitudes que no requieren crear, actualizar ni eliminar.\n- Responde de forma controlada sin realizar cambios innecesarios en el calendario.",
+ "height": 624,
+ "width": 1328
+ },
+ "type": "n8n-nodes-base.stickyNote",
+ "typeVersion": 1,
+ "position": [
+ 96,
+ 880
+ ],
+ "id": "d38de08e-0f21-43a6-a75f-8ebc5be4dd34",
+ "name": "Sticky Note2"
+ }
+ ],
+ "pinData": {},
+ "connections": {
+ "Webhook - Google Calendar": {
+ "main": [
+ [
+ {
+ "node": "Preparar solicitud",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar solicitud": {
+ "main": [
+ [
+ {
+ "node": "Solicitud válida",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Solicitud válida": {
+ "main": [
+ [
+ {
+ "node": "Supabase - Obtener obligación y asistentes",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Responder solicitud inválida",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Supabase - Obtener obligación y asistentes": {
+ "main": [
+ [
+ {
+ "node": "Preparar payload Calendar",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar payload Calendar": {
+ "main": [
+ [
+ {
+ "node": "Preparar crear",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Preparar actualizar",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Preparar eliminar",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Preparar sin acción",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar crear": {
+ "main": [
+ [
+ {
+ "node": "Google Calendar - Crear evento",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Google Calendar - Crear evento": {
+ "main": [
+ [
+ {
+ "node": "Preparar registro creado",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar registro creado": {
+ "main": [
+ [
+ {
+ "node": "Supabase - Registrar creación Calendar",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Supabase - Registrar creación Calendar": {
+ "main": [
+ [
+ {
+ "node": "Responder creación",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar actualizar": {
+ "main": [
+ [
+ {
+ "node": "Google Calendar - Actualizar evento",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Google Calendar - Actualizar evento": {
+ "main": [
+ [
+ {
+ "node": "Preparar registro actualizado",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar registro actualizado": {
+ "main": [
+ [
+ {
+ "node": "Supabase - Registrar actualización Calendar",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Supabase - Registrar actualización Calendar": {
+ "main": [
+ [
+ {
+ "node": "Responder actualización",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar eliminar": {
+ "main": [
+ [
+ {
+ "node": "Google Calendar - Eliminar evento",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Google Calendar - Eliminar evento": {
+ "main": [
+ [
+ {
+ "node": "Preparar registro eliminado",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar registro eliminado": {
+ "main": [
+ [
+ {
+ "node": "Supabase - Registrar eliminación Calendar",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Supabase - Registrar eliminación Calendar": {
+ "main": [
+ [
+ {
+ "node": "Responder eliminación",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar sin acción": {
+ "main": [
+ [
+ {
+ "node": "Responder sin acción",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": true,
+ "settings": {
+ "executionOrder": "v1",
+ "binaryMode": "separate"
+ },
+ "versionId": "5b0b0ad1-26c8-4965-b5d1-3bda834ae3a4",
+ "meta": {
+ "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
+ },
+ "id": "4ysmdoCtcyBHB0pW",
+ "tags": []
+}
\ No newline at end of file
diff --git a/Seguimiento de Impuestos GLM - Recordatorios Gmail y Google Chat.json b/Seguimiento de Impuestos GLM - Recordatorios Gmail y Google Chat.json
new file mode 100644
index 0000000..34d0237
--- /dev/null
+++ b/Seguimiento de Impuestos GLM - Recordatorios Gmail y Google Chat.json
@@ -0,0 +1,603 @@
+{
+ "name": "Seguimiento de Impuestos GLM - Recordatorios Gmail y Google Chat",
+ "nodes": [
+ {
+ "parameters": {
+ "rule": {
+ "interval": [
+ {
+ "field": "cronExpression",
+ "expression": "0 8 * * *"
+ }
+ ]
+ }
+ },
+ "type": "n8n-nodes-base.scheduleTrigger",
+ "typeVersion": 1.2,
+ "position": [
+ -32,
+ 1456
+ ],
+ "id": "d9890ecf-a9ba-4604-9a68-03568e80bbe5",
+ "name": "Diario 8:00 AM"
+ },
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [
+ -32,
+ 1616
+ ],
+ "id": "58d5e31d-10bb-4dec-af43-d3ea3225a4ad",
+ "name": "Ejecutar manualmente"
+ },
+ {
+ "parameters": {
+ "httpMethod": "POST",
+ "path": "seguimiento-impuestos",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.webhook",
+ "typeVersion": 2.1,
+ "position": [
+ -32,
+ 1808
+ ],
+ "id": "b1a94a40-005a-419c-9d3d-697b95d16749",
+ "name": "Webhook seguimiento-impuestos",
+ "webhookId": "seguimiento-impuestos-glm"
+ },
+ {
+ "parameters": {
+ "jsCode": "function dateInTimeZone(timeZone) {\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n }).formatToParts(new Date());\n const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));\n return `${values.year}-${values.month}-${values.day}`;\n}\n\nconst input = $json ?? {};\nconst runDate = input.run_date || dateInTimeZone('America/Santo_Domingo');\n\nreturn [{\n json: {\n run_date: runDate,\n source: input.source || 'schedule',\n },\n}];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 224,
+ 1536
+ ],
+ "id": "da51143c-6cbd-4ea1-a49e-d0c4b097b952",
+ "name": "Preparar ejecución"
+ },
+ {
+ "parameters": {
+ "jsCode": "const body = $json.body ?? $json;\nconst action = String(body.action ?? '').trim().toLowerCase();\n\nif (!['run_now', 'run_reminders', 'ejecutar_recordatorios'].includes(action)) {\n return [];\n}\n\nreturn [{\n json: {\n run_date: body.run_date || body.date || '',\n source: 'webhook',\n },\n}];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 224,
+ 1808
+ ],
+ "id": "0863e7a7-bc95-42f6-982f-f09f859ec6d6",
+ "name": "Validar ejecución por Webhook"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_due_reminders",
+ "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"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ { p_run_date: $json.run_date } }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 480,
+ 1616
+ ],
+ "id": "00dfa1be-5e07-4860-8322-d9f490ac4e4a",
+ "name": "Supabase - Recordatorios pendientes"
+ },
+ {
+ "parameters": {
+ "jsCode": "const htmlEscape = (value) => String(value ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst rows = [];\nfor (const item of $input.all()) {\n const value = item.json;\n if (Array.isArray(value)) rows.push(...value);\n else if (Array.isArray(value?.data)) rows.push(...value.data);\n else if (Array.isArray(value?.body)) rows.push(...value.body);\n else if (value?.obligation_id) rows.push(value);\n}\n\nconst logoUrl = 'https://home.digitalcompass.agency/glm-logo-completo.png';\nconst appUrl = 'https://digitalcompass.agency/calendario-impuestos/';\n\nconst formatDate = (iso) => {\n const [year, month, day] = String(iso).split('-').map(Number);\n return new Intl.DateTimeFormat('es-DO', {\n timeZone: 'UTC',\n day: '2-digit',\n month: 'long',\n year: 'numeric',\n }).format(new Date(Date.UTC(year, month - 1, day)));\n};\n\nreturn rows.map((row) => {\n const dueDate = formatDate(row.due_date);\n const isSameDay = row.alert_type === 'same_day';\n const reminderLabel = isSameDay ? 'Vence hoy' : 'Recordatorio previo';\n const categoryLabel = row.category === 'nomina' ? 'Nómina' : 'Administración';\n const firstName = String(row.contact_name ?? '').trim().split(/\\s+/)[0] || 'equipo';\n const subject = `${isSameDay ? 'Vence hoy' : 'Recordatorio'} · ${row.country_name} · ${row.description}`;\n\n const emailHtml = `\n\n\n
\n \n \n \n }\") | \n | | \n | \n ${htmlEscape(reminderLabel)} \n Seguimiento de Impuestos GLM\n Hola ${htmlEscape(firstName)}, este es el recordatorio correspondiente a la siguiente obligación tributaria: \n \n | País | ${htmlEscape(row.country_name)} | \n | Categoría | ${htmlEscape(categoryLabel)} | \n | Fecha límite | ${htmlEscape(dueDate)} | \n | Obligación | ${htmlEscape(row.description)} | \n \n Este aviso forma parte del seguimiento automático: miércoles anterior y fecha límite, incluidos sábados y domingos. \n \n | \n | GOMEZLEE MARKETING | Seguimiento de Impuestos GLM | Uso interno | \n \n |
\n
\n \n`.trim();\n\n const googleChatText = [\n `*${reminderLabel.toUpperCase()} · SEGUIMIENTO DE IMPUESTOS GLM*`,\n '',\n `Hola ${firstName},`,\n '',\n `*País:* ${row.country_name}`,\n `*Categoría:* ${categoryLabel}`,\n `*Fecha límite:* ${dueDate}`,\n `*Obligación:* ${row.description}`,\n '',\n 'Seguimiento automático: miércoles anterior y fecha límite, incluidos sábados y domingos.',\n '',\n `Abrir calendario: ${appUrl}`,\n ].join('\\n');\n\n return {\n json: {\n ...row,\n email_subject: subject,\n email_html: emailHtml,\n google_chat_text: googleChatText,\n },\n };\n});"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 736,
+ 1616
+ ],
+ "id": "780bc937-c373-43cc-9343-e3c73d076934",
+ "name": "Construir mensajes GLM"
+ },
+ {
+ "parameters": {
+ "jsCode": "return $input.all().filter((item) => item.json.email_enabled === true && String(item.json.contact_email ?? '').trim());"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 1008,
+ 1472
+ ],
+ "id": "343dc1d3-cb5b-452c-b5d4-f69583b15280",
+ "name": "Preparar correos"
+ },
+ {
+ "parameters": {
+ "sendTo": "={{ $json.contact_email }}",
+ "subject": "={{ $json.email_subject }}",
+ "message": "={{ $json.email_html }}",
+ "options": {
+ "appendAttribution": false
+ }
+ },
+ "type": "n8n-nodes-base.gmail",
+ "typeVersion": 2.1,
+ "position": [
+ 1264,
+ 1472
+ ],
+ "id": "7f16857a-3674-4255-9674-cc040682f518",
+ "name": "Gmail - Enviar recordatorio",
+ "webhookId": "16bfebbd-f4e3-4b1e-aba2-028fecf98da1",
+ "credentials": {
+ "gmailOAuth2": {
+ "id": "UDcO1FLJqA453V2D",
+ "name": "Gmail account 3"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_record_notification",
+ "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"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ { p_obligation_id: $('Preparar correos').item.json.obligation_id, p_contact_id: $('Preparar correos').item.json.contact_id, p_alert_date: $('Preparar correos').item.json.alert_date, p_channel: 'email', p_status: 'sent', p_provider_response: $json } }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 1520,
+ 1472
+ ],
+ "id": "bea9fc06-b8de-4091-bde0-d7be8c18e4cd",
+ "name": "Supabase - Registrar correo"
+ },
+ {
+ "parameters": {
+ "jsCode": "return $input.all().filter((item) => item.json.google_chat_enabled === true && String(item.json.contact_email ?? '').trim());"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 1008,
+ 1744
+ ],
+ "id": "3addf2bd-60c2-493c-8919-56b603aad7e8",
+ "name": "Preparar Google Chat"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://chat.googleapis.com/v1/spaces:setup",
+ "authentication": "predefinedCredentialType",
+ "nodeCredentialType": "googleChatOAuth2Api",
+ "sendHeaders": true,
+ "headerParameters": {
+ "parameters": [
+ {
+ "name": "Content-Type",
+ "value": "application/json"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ { space: { spaceType: 'DIRECT_MESSAGE', singleUserBotDm: false }, memberships: [ { member: { name: 'users/' + $json.contact_email, type: 'HUMAN' } } ] } }}",
+ "options": {
+ "timeout": 30000
+ }
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 1264,
+ 1744
+ ],
+ "id": "630cdc84-f7aa-4db8-8356-942056f3ac22",
+ "name": "Google Chat - Abrir DM",
+ "retryOnFail": true,
+ "maxTries": 3,
+ "waitBetweenTries": 1500,
+ "credentials": {
+ "googleChatOAuth2Api": {
+ "id": "V4eVQvEoy5QcBvUu",
+ "name": "Chat account"
+ }
+ },
+ "onError": "continueRegularOutput"
+ },
+ {
+ "parameters": {
+ "jsCode": "const source = $('Preparar Google Chat').item.json;\nconst spaceName = String($json.name ?? '').trim();\nreturn [{ json: { ...source, chat_space_name: spaceName, chat_setup_ok: Boolean(spaceName), google_chat_status: spaceName ? 'pending' : 'failed', google_chat_provider_response: $json } }];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 1520,
+ 1744
+ ],
+ "id": "1002151d-cb96-46c1-9868-3e9aca7846c0",
+ "name": "Evaluar DM Google Chat"
+ },
+ {
+ "parameters": {
+ "jsCode": "return $input.all().filter((item) => item.json.chat_setup_ok === true);"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 1776,
+ 1632
+ ],
+ "id": "5978dd34-7cb2-46be-9536-65c22b7f06a4",
+ "name": "Preparar envío Google Chat"
+ },
+ {
+ "parameters": {
+ "jsCode": "return $input.all().filter((item) => item.json.chat_setup_ok !== true);"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 1776,
+ 1888
+ ],
+ "id": "cbc9f8d3-35f8-48a2-9251-c85c96d15be0",
+ "name": "Preparar fallo DM Google Chat"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "={{ 'https://chat.googleapis.com/v1/' + $json.chat_space_name + '/messages' }}",
+ "authentication": "predefinedCredentialType",
+ "nodeCredentialType": "googleChatOAuth2Api",
+ "sendHeaders": true,
+ "headerParameters": {
+ "parameters": [
+ {
+ "name": "Content-Type",
+ "value": "application/json"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ { text: $json.google_chat_text, createMessageNotificationOptions: { notificationType: 'NOTIFICATION_TYPE_FORCE_NOTIFY' } } }}",
+ "options": {
+ "timeout": 30000
+ }
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 2032,
+ 1632
+ ],
+ "id": "d4d67181-4079-4a46-9fd7-a10afe57db71",
+ "name": "Google Chat - Enviar recordatorio",
+ "retryOnFail": true,
+ "maxTries": 3,
+ "waitBetweenTries": 1500,
+ "credentials": {
+ "googleChatOAuth2Api": {
+ "id": "V4eVQvEoy5QcBvUu",
+ "name": "Chat account"
+ }
+ },
+ "onError": "continueRegularOutput"
+ },
+ {
+ "parameters": {
+ "jsCode": "const source = $('Preparar envío Google Chat').item.json;\nconst sent = Boolean($json.name) && !$json.error;\nreturn [{ json: { ...source, google_chat_status: sent ? 'sent' : 'failed', google_chat_provider_response: $json } }];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 2288,
+ 1632
+ ],
+ "id": "6398165b-9e0a-483f-9db6-71f0d85a69d0",
+ "name": "Evaluar envío Google Chat"
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_record_notification",
+ "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"
+ }
+ ]
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ { p_obligation_id: $json.obligation_id, p_contact_id: $json.contact_id, p_alert_date: $json.alert_date, p_channel: 'google_chat', p_status: $json.google_chat_status, p_provider_response: $json.google_chat_provider_response } }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.4,
+ "position": [
+ 2544,
+ 1744
+ ],
+ "id": "29a93cc5-c157-4199-81c1-1fed7664edc6",
+ "name": "Supabase - Registrar Google Chat"
+ },
+ {
+ "parameters": {
+ "content": "## RECORDATORIOS DE IMPUESTOS POR GMAIL Y GOOGLE CHAT\n\nEjecuta diariamente el proceso de recordatorios fiscales y notifica a los contactos correspondientes por correo electrónico y mensaje directo de Google Chat.\n\n- Puede iniciarse automáticamente a las 8:00 a. m., manualmente o desde el webhook de la aplicación.\n- Valida y normaliza el origen de la ejecución antes de continuar.\n- Consulta en Supabase las obligaciones que requieren recordatorio y sus destinatarios.\n- Construye mensajes personalizados con el formato corporativo de GLM.\n- Envía los recordatorios por Gmail y registra en Supabase el resultado de cada correo.\n- Abre o identifica la conversación directa del destinatario en Google Chat.\n- Envía el recordatorio por Google Chat cuando el DM está disponible.\n- Evalúa cada intento y registra tanto los envíos exitosos como los errores para mantener trazabilidad.",
+ "height": 1184,
+ "width": 2880,
+ "color": "#204750"
+ },
+ "type": "n8n-nodes-base.stickyNote",
+ "typeVersion": 1,
+ "position": [
+ -144,
+ 896
+ ],
+ "id": "8c9fe360-936b-4e86-bd17-5a396dd09419",
+ "name": "Sticky Note"
+ }
+ ],
+ "pinData": {},
+ "connections": {
+ "Diario 8:00 AM": {
+ "main": [
+ [
+ {
+ "node": "Preparar ejecución",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Ejecutar manualmente": {
+ "main": [
+ [
+ {
+ "node": "Preparar ejecución",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Webhook seguimiento-impuestos": {
+ "main": [
+ [
+ {
+ "node": "Validar ejecución por Webhook",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar ejecución": {
+ "main": [
+ [
+ {
+ "node": "Supabase - Recordatorios pendientes",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Validar ejecución por Webhook": {
+ "main": [
+ [
+ {
+ "node": "Preparar ejecución",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Supabase - Recordatorios pendientes": {
+ "main": [
+ [
+ {
+ "node": "Construir mensajes GLM",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Construir mensajes GLM": {
+ "main": [
+ [
+ {
+ "node": "Preparar correos",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Preparar Google Chat",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar correos": {
+ "main": [
+ [
+ {
+ "node": "Gmail - Enviar recordatorio",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Gmail - Enviar recordatorio": {
+ "main": [
+ [
+ {
+ "node": "Supabase - Registrar correo",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar Google Chat": {
+ "main": [
+ [
+ {
+ "node": "Google Chat - Abrir DM",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Google Chat - Abrir DM": {
+ "main": [
+ [
+ {
+ "node": "Evaluar DM Google Chat",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Evaluar DM Google Chat": {
+ "main": [
+ [
+ {
+ "node": "Preparar envío Google Chat",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Preparar fallo DM Google Chat",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar envío Google Chat": {
+ "main": [
+ [
+ {
+ "node": "Google Chat - Enviar recordatorio",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Google Chat - Enviar recordatorio": {
+ "main": [
+ [
+ {
+ "node": "Evaluar envío Google Chat",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Evaluar envío Google Chat": {
+ "main": [
+ [
+ {
+ "node": "Supabase - Registrar Google Chat",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Preparar fallo DM Google Chat": {
+ "main": [
+ [
+ {
+ "node": "Supabase - Registrar Google Chat",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": true,
+ "settings": {
+ "executionOrder": "v1",
+ "binaryMode": "separate",
+ "availableInMCP": true,
+ "timeSavedMode": "fixed",
+ "errorWorkflow": "puF4LUczoSz3hcek",
+ "timezone": "America/Santo_Domingo",
+ "callerPolicy": "workflowsFromSameOwner"
+ },
+ "versionId": "ab159702-cf57-4e47-8de5-95da61a56b9b",
+ "meta": {
+ "templateCredsSetupCompleted": true,
+ "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
+ },
+ "id": "cFu3Xx23KQBKfr0M",
+ "tags": []
+}
\ No newline at end of file