From b022c2857dcd02089d4fc4feabf0907e0247b428 Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Sun, 21 Jun 2026 04:00:19 +0000 Subject: [PATCH] Create backup: Tablero CDC - Sync Tarifario Sheet a Supabase --- ...o-cdc-sync-tarifario-sheet-a-supabase.json | 889 ++++++++++++++++++ 1 file changed, 889 insertions(+) create mode 100644 tablero-cdc-sync-tarifario-sheet-a-supabase.json diff --git a/tablero-cdc-sync-tarifario-sheet-a-supabase.json b/tablero-cdc-sync-tarifario-sheet-a-supabase.json new file mode 100644 index 0000000..24e8d35 --- /dev/null +++ b/tablero-cdc-sync-tarifario-sheet-a-supabase.json @@ -0,0 +1,889 @@ +{ + "updatedAt": "2026-06-21T01:53:57.071Z", + "createdAt": "2026-06-20T14:15:17.560Z", + "id": "7eyfZEkGuDSUodDX", + "name": "Tablero CDC - Sync Tarifario Sheet a Supabase", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "jsCode": "function cleanText(value) {\n return String(value ?? \"\").trim();\n}\n\nfunction normalizeForCompare(value) {\n return cleanText(value)\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\");\n}\n\nfunction slugify(value) {\n return String(value ?? \"\")\n .trim()\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/&/g, \"y\")\n .replace(/ñ/g, \"n\")\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n}\n\nfunction parseAmountRange(value) {\n const raw = cleanText(value);\n\n if (!raw) {\n return {\n reference: \"\",\n reference_min: null,\n reference_max: null,\n };\n }\n\n const reference = raw.replace(/\\s+/g, \" \");\n\n const withoutCurrency = reference\n .replace(/USD/gi, \"\")\n .replace(/US\\$/gi, \"\")\n .replace(/\\$/g, \"\")\n .replace(/\\+/g, \"\")\n .trim();\n\n const matches = withoutCurrency.match(/-?\\d[\\d,]*(\\.\\d+)?/g) || [];\n\n const numbers = matches\n .map((match) => Number(match.replace(/,/g, \"\")))\n .filter((number) => Number.isFinite(number));\n\n if (numbers.length === 0) {\n return {\n reference,\n reference_min: null,\n reference_max: null,\n };\n }\n\n if (numbers.length === 1) {\n return {\n reference,\n reference_min: numbers[0],\n reference_max: numbers[0],\n };\n }\n\n const min = Math.min(numbers[0], numbers[1]);\n const max = Math.max(numbers[0], numbers[1]);\n\n return {\n reference,\n reference_min: min,\n reference_max: max,\n };\n}\n\nfunction normalizeSection(value) {\n const raw = normalizeForCompare(value);\n\n if (!raw) {\n throw new Error(\"La columna Sección está vacía en una fila del tarifario.\");\n }\n\n if (raw === \"tarifario grafico cdc\") {\n return \"grafico\";\n }\n\n if (raw === \"estrategia y creatividad\") {\n return \"estrategia\";\n }\n\n throw new Error(\n `Sección inválida en tarifario: \"${value}\". Usa solo \"Tarifario Gráfico CDC\" o \"Estrategia y Creatividad\".`\n );\n}\n\nfunction buildBaseId(row, section, category, service, workTypeId) {\n const rawCodigoBase = cleanText(row[\"Código base\"]);\n\n if (rawCodigoBase) {\n return slugify(rawCodigoBase);\n }\n\n return slugify(\n [section, category, service, workTypeId].filter(Boolean).join(\"_\")\n );\n}\n\nfunction getSourceRowNumber(row, index) {\n return (\n row.row_number ||\n row.rowNumber ||\n row.__rowNumber ||\n index + 2\n );\n}\n\nconst records = [];\nconst generatedCodeUpdates = [];\n\nitems.forEach((item, index) => {\n const row = item.json;\n\n const rawSection = cleanText(row[\"Sección\"]);\n const rawCategory = cleanText(row[\"Categoría\"]);\n const rawService = cleanText(row[\"Servicio / ítem\"]);\n const rawWorkType = cleanText(row[\"Tipo de trabajo\"] || \"Referencia\");\n const shortLabel = cleanText(row[\"Etiqueta corta\"] || \"Ref.\");\n const hourReference = cleanText(row[\"Hora hombre ref.\"]);\n const notes = cleanText(row[\"Observaciones / incluye\"]);\n\n // Ignora filas totalmente vacías.\n const hasAnyBusinessData = [\n rawSection,\n rawCategory,\n rawService,\n rawWorkType,\n row[\"Nivel 1 nombre\"],\n row[\"Nivel 1 monto/rango\"],\n row[\"Nivel 2 nombre\"],\n row[\"Nivel 2 monto/rango\"],\n row[\"Nivel 3 nombre\"],\n row[\"Nivel 3 monto/rango\"],\n ].some((value) => cleanText(value));\n\n if (!hasAnyBusinessData) {\n return;\n }\n\n const section = normalizeSection(rawSection);\n const category = rawCategory;\n const service = rawService;\n const workTypeLabel = rawWorkType;\n const workTypeId = slugify(workTypeLabel || \"reference\") || \"reference\";\n\n if (!category || !service) {\n throw new Error(\n `Fila ${getSourceRowNumber(row, index)} incompleta: Categoría y Servicio / ítem son obligatorios.`\n );\n }\n\n const catalogItemId = slugify(\n [section, category, service].filter(Boolean).join(\"_\")\n );\n\n const baseId = buildBaseId(row, section, category, service, workTypeId);\n\n if (!baseId) {\n throw new Error(`No se pudo generar Código base en la fila ${getSourceRowNumber(row, index)}.`);\n }\n\n const sourceRowNumber = getSourceRowNumber(row, index);\n\n if (!cleanText(row[\"Código base\"])) {\n generatedCodeUpdates.push({\n row_number: sourceRowNumber,\n codigo_base: baseId,\n });\n }\n\n const levels = [\n {\n number: 1,\n name: cleanText(row[\"Nivel 1 nombre\"]),\n amountRange: cleanText(row[\"Nivel 1 monto/rango\"]),\n note: cleanText(row[\"Nivel 1 nota\"]),\n },\n {\n number: 2,\n name: cleanText(row[\"Nivel 2 nombre\"]),\n amountRange: cleanText(row[\"Nivel 2 monto/rango\"]),\n note: cleanText(row[\"Nivel 2 nota\"]),\n },\n {\n number: 3,\n name: cleanText(row[\"Nivel 3 nombre\"]),\n amountRange: cleanText(row[\"Nivel 3 monto/rango\"]),\n note: cleanText(row[\"Nivel 3 nota\"]),\n },\n ];\n\n const validLevels = levels.filter((level) => level.name && level.amountRange);\n\n if (validLevels.length === 0) {\n throw new Error(\n `Fila ${sourceRowNumber}: agrega al menos un nivel con nombre y monto/rango.`\n );\n }\n\n for (const level of validLevels) {\n const levelLabel = level.name;\n const levelId = slugify(levelLabel || \"project\") || \"project\";\n const amount = parseAmountRange(level.amountRange);\n\n const id = slugify(\n [baseId, levelId].filter(Boolean).join(\"_\")\n );\n\n records.push({\n id,\n catalog_item_id: catalogItemId,\n section,\n category,\n service,\n notes,\n work_type_id: workTypeId,\n work_type_label: workTypeLabel,\n work_type_short_label: shortLabel,\n hour_reference: hourReference,\n level_id: levelId,\n level_label: levelLabel,\n reference: amount.reference,\n reference_min: amount.reference_min,\n reference_max: amount.reference_max,\n level_hint: level.note,\n sort_order: 10000 + index * 10 + level.number,\n is_active: true,\n });\n }\n});\n\nif (records.length === 0) {\n throw new Error(\"No se encontraron tarifas válidas. Se detiene para no afectar el catálogo.\");\n}\n\nconst seen = new Set();\n\nfor (const record of records) {\n if (seen.has(record.id)) {\n throw new Error(`Código único duplicado detectado en tarifario: ${record.id}`);\n }\n\n seen.add(record.id);\n\n if (\n record.reference_min !== null &&\n record.reference_max !== null &&\n record.reference_min > record.reference_max\n ) {\n throw new Error(`La tarifa ${record.id} tiene mínimo mayor que máximo.`);\n }\n}\n\nreturn [\n {\n json: {\n total_source_rows: items.length,\n total_records_to_sync: records.length,\n total_generated_codes: generatedCodeUpdates.length,\n records,\n generated_code_updates: generatedCodeUpdates,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 416, + 0 + ], + "id": "25030323-2bde-460c-b1ad-02e6260d67da", + "name": "Code - Normalizar Tarifario" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_tariff_catalog?on_conflict=id", + "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": "={{ JSON.stringify($json.records) }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 624, + 0 + ], + "id": "a21e1648-fd8c-4cf3-acc3-50f91af1e4d9", + "name": "Supabase - Upsert Tarifario" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg", + "mode": "list", + "cachedResultName": "Tarifario de Creatividad - Sep 2004", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1041164995, + "mode": "list", + "cachedResultName": "Catálogo Tarifario App", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit#gid=1041164995" + }, + "options": { + "dataLocationOnSheet": { + "values": { + "rangeDefinition": "detectAutomatically" + } + } + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 208, + 0 + ], + "id": "e6d4f9ef-30da-4640-9002-96b43aa48729", + "name": "Google Sheets - Leer Catálogo Tarifario App", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const source = $('Code - Normalizar Tarifario').first().json;\n\nconst updates = source.generated_code_updates || [];\n\nreturn updates.map((update) => ({\n json: {\n row_number: update.row_number,\n codigo_base: update.codigo_base,\n },\n}));" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 832, + 0 + ], + "id": "247f2f76-a1e4-4e51-b35f-a32bdaadf354", + "name": "Code - Preparar códigos generados" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg", + "mode": "list", + "cachedResultName": "Tarifario de Creatividad - Sep 2004", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1041164995, + "mode": "list", + "cachedResultName": "Catálogo Tarifario App", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit#gid=1041164995" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "row_number": "={{ $json[\"row_number\"] }}", + "Código base": "={{ $json[\"codigo_base\"] }}" + }, + "matchingColumns": [ + "row_number" + ], + "schema": [ + { + "id": "Código base", + "displayName": "Código base", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Sección", + "displayName": "Sección", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Categoría", + "displayName": "Categoría", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Servicio / ítem", + "displayName": "Servicio / ítem", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Tipo de trabajo", + "displayName": "Tipo de trabajo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Etiqueta corta", + "displayName": "Etiqueta corta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Hora hombre ref.", + "displayName": "Hora hombre ref.", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 1 nombre", + "displayName": "Nivel 1 nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 1 monto/rango", + "displayName": "Nivel 1 monto/rango", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 1 nota", + "displayName": "Nivel 1 nota", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 2 nombre", + "displayName": "Nivel 2 nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 2 monto/rango", + "displayName": "Nivel 2 monto/rango", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 2 nota", + "displayName": "Nivel 2 nota", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 3 nombre", + "displayName": "Nivel 3 nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 3 monto/rango", + "displayName": "Nivel 3 monto/rango", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 3 nota", + "displayName": "Nivel 3 nota", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Observaciones / incluye", + "displayName": "Observaciones / incluye", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 1072, + 0 + ], + "id": "60367c04-79f6-4c8a-99d1-bc9369c8ca61", + "name": "Google Sheets - Escribir códigos únicos faltantes", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours", + "hoursInterval": 8 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -16, + 0 + ], + "id": "1a45e645-65af-4727-bf4f-15c7000ba2a2", + "name": "Schedule Trigger" + } + ], + "connections": { + "Code - Normalizar Tarifario": { + "main": [ + [ + { + "node": "Supabase - Upsert Tarifario", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Upsert Tarifario": { + "main": [ + [ + { + "node": "Code - Preparar códigos generados", + "type": "main", + "index": 0 + } + ] + ] + }, + "Google Sheets - Leer Catálogo Tarifario App": { + "main": [ + [ + { + "node": "Code - Normalizar Tarifario", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar códigos generados": { + "main": [ + [ + { + "node": "Google Sheets - Escribir códigos únicos faltantes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "Google Sheets - Leer Catálogo Tarifario App", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "staticData": { + "node:Schedule Trigger": { + "recurrenceRules": [] + } + }, + "meta": { + "templateCredsSetupCompleted": true + }, + "versionId": "806aa39c-41e1-4c6a-8d58-60fb431346a1", + "activeVersionId": "806aa39c-41e1-4c6a-8d58-60fb431346a1", + "versionCounter": 94, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-06-20T14:15:17.562Z", + "createdAt": "2026-06-20T14:15:17.562Z", + "role": "workflow:owner", + "workflowId": "7eyfZEkGuDSUodDX", + "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-06-21T01:54:00.000Z", + "createdAt": "2026-06-21T01:53:57.073Z", + "versionId": "806aa39c-41e1-4c6a-8d58-60fb431346a1", + "workflowId": "7eyfZEkGuDSUodDX", + "nodes": [ + { + "parameters": { + "jsCode": "function cleanText(value) {\n return String(value ?? \"\").trim();\n}\n\nfunction normalizeForCompare(value) {\n return cleanText(value)\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\");\n}\n\nfunction slugify(value) {\n return String(value ?? \"\")\n .trim()\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/&/g, \"y\")\n .replace(/ñ/g, \"n\")\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n}\n\nfunction parseAmountRange(value) {\n const raw = cleanText(value);\n\n if (!raw) {\n return {\n reference: \"\",\n reference_min: null,\n reference_max: null,\n };\n }\n\n const reference = raw.replace(/\\s+/g, \" \");\n\n const withoutCurrency = reference\n .replace(/USD/gi, \"\")\n .replace(/US\\$/gi, \"\")\n .replace(/\\$/g, \"\")\n .replace(/\\+/g, \"\")\n .trim();\n\n const matches = withoutCurrency.match(/-?\\d[\\d,]*(\\.\\d+)?/g) || [];\n\n const numbers = matches\n .map((match) => Number(match.replace(/,/g, \"\")))\n .filter((number) => Number.isFinite(number));\n\n if (numbers.length === 0) {\n return {\n reference,\n reference_min: null,\n reference_max: null,\n };\n }\n\n if (numbers.length === 1) {\n return {\n reference,\n reference_min: numbers[0],\n reference_max: numbers[0],\n };\n }\n\n const min = Math.min(numbers[0], numbers[1]);\n const max = Math.max(numbers[0], numbers[1]);\n\n return {\n reference,\n reference_min: min,\n reference_max: max,\n };\n}\n\nfunction normalizeSection(value) {\n const raw = normalizeForCompare(value);\n\n if (!raw) {\n throw new Error(\"La columna Sección está vacía en una fila del tarifario.\");\n }\n\n if (raw === \"tarifario grafico cdc\") {\n return \"grafico\";\n }\n\n if (raw === \"estrategia y creatividad\") {\n return \"estrategia\";\n }\n\n throw new Error(\n `Sección inválida en tarifario: \"${value}\". Usa solo \"Tarifario Gráfico CDC\" o \"Estrategia y Creatividad\".`\n );\n}\n\nfunction buildBaseId(row, section, category, service, workTypeId) {\n const rawCodigoBase = cleanText(row[\"Código base\"]);\n\n if (rawCodigoBase) {\n return slugify(rawCodigoBase);\n }\n\n return slugify(\n [section, category, service, workTypeId].filter(Boolean).join(\"_\")\n );\n}\n\nfunction getSourceRowNumber(row, index) {\n return (\n row.row_number ||\n row.rowNumber ||\n row.__rowNumber ||\n index + 2\n );\n}\n\nconst records = [];\nconst generatedCodeUpdates = [];\n\nitems.forEach((item, index) => {\n const row = item.json;\n\n const rawSection = cleanText(row[\"Sección\"]);\n const rawCategory = cleanText(row[\"Categoría\"]);\n const rawService = cleanText(row[\"Servicio / ítem\"]);\n const rawWorkType = cleanText(row[\"Tipo de trabajo\"] || \"Referencia\");\n const shortLabel = cleanText(row[\"Etiqueta corta\"] || \"Ref.\");\n const hourReference = cleanText(row[\"Hora hombre ref.\"]);\n const notes = cleanText(row[\"Observaciones / incluye\"]);\n\n // Ignora filas totalmente vacías.\n const hasAnyBusinessData = [\n rawSection,\n rawCategory,\n rawService,\n rawWorkType,\n row[\"Nivel 1 nombre\"],\n row[\"Nivel 1 monto/rango\"],\n row[\"Nivel 2 nombre\"],\n row[\"Nivel 2 monto/rango\"],\n row[\"Nivel 3 nombre\"],\n row[\"Nivel 3 monto/rango\"],\n ].some((value) => cleanText(value));\n\n if (!hasAnyBusinessData) {\n return;\n }\n\n const section = normalizeSection(rawSection);\n const category = rawCategory;\n const service = rawService;\n const workTypeLabel = rawWorkType;\n const workTypeId = slugify(workTypeLabel || \"reference\") || \"reference\";\n\n if (!category || !service) {\n throw new Error(\n `Fila ${getSourceRowNumber(row, index)} incompleta: Categoría y Servicio / ítem son obligatorios.`\n );\n }\n\n const catalogItemId = slugify(\n [section, category, service].filter(Boolean).join(\"_\")\n );\n\n const baseId = buildBaseId(row, section, category, service, workTypeId);\n\n if (!baseId) {\n throw new Error(`No se pudo generar Código base en la fila ${getSourceRowNumber(row, index)}.`);\n }\n\n const sourceRowNumber = getSourceRowNumber(row, index);\n\n if (!cleanText(row[\"Código base\"])) {\n generatedCodeUpdates.push({\n row_number: sourceRowNumber,\n codigo_base: baseId,\n });\n }\n\n const levels = [\n {\n number: 1,\n name: cleanText(row[\"Nivel 1 nombre\"]),\n amountRange: cleanText(row[\"Nivel 1 monto/rango\"]),\n note: cleanText(row[\"Nivel 1 nota\"]),\n },\n {\n number: 2,\n name: cleanText(row[\"Nivel 2 nombre\"]),\n amountRange: cleanText(row[\"Nivel 2 monto/rango\"]),\n note: cleanText(row[\"Nivel 2 nota\"]),\n },\n {\n number: 3,\n name: cleanText(row[\"Nivel 3 nombre\"]),\n amountRange: cleanText(row[\"Nivel 3 monto/rango\"]),\n note: cleanText(row[\"Nivel 3 nota\"]),\n },\n ];\n\n const validLevels = levels.filter((level) => level.name && level.amountRange);\n\n if (validLevels.length === 0) {\n throw new Error(\n `Fila ${sourceRowNumber}: agrega al menos un nivel con nombre y monto/rango.`\n );\n }\n\n for (const level of validLevels) {\n const levelLabel = level.name;\n const levelId = slugify(levelLabel || \"project\") || \"project\";\n const amount = parseAmountRange(level.amountRange);\n\n const id = slugify(\n [baseId, levelId].filter(Boolean).join(\"_\")\n );\n\n records.push({\n id,\n catalog_item_id: catalogItemId,\n section,\n category,\n service,\n notes,\n work_type_id: workTypeId,\n work_type_label: workTypeLabel,\n work_type_short_label: shortLabel,\n hour_reference: hourReference,\n level_id: levelId,\n level_label: levelLabel,\n reference: amount.reference,\n reference_min: amount.reference_min,\n reference_max: amount.reference_max,\n level_hint: level.note,\n sort_order: 10000 + index * 10 + level.number,\n is_active: true,\n });\n }\n});\n\nif (records.length === 0) {\n throw new Error(\"No se encontraron tarifas válidas. Se detiene para no afectar el catálogo.\");\n}\n\nconst seen = new Set();\n\nfor (const record of records) {\n if (seen.has(record.id)) {\n throw new Error(`Código único duplicado detectado en tarifario: ${record.id}`);\n }\n\n seen.add(record.id);\n\n if (\n record.reference_min !== null &&\n record.reference_max !== null &&\n record.reference_min > record.reference_max\n ) {\n throw new Error(`La tarifa ${record.id} tiene mínimo mayor que máximo.`);\n }\n}\n\nreturn [\n {\n json: {\n total_source_rows: items.length,\n total_records_to_sync: records.length,\n total_generated_codes: generatedCodeUpdates.length,\n records,\n generated_code_updates: generatedCodeUpdates,\n },\n },\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 416, + 0 + ], + "id": "25030323-2bde-460c-b1ad-02e6260d67da", + "name": "Code - Normalizar Tarifario" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/tablero_cdc_tariff_catalog?on_conflict=id", + "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": "={{ JSON.stringify($json.records) }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 624, + 0 + ], + "id": "a21e1648-fd8c-4cf3-acc3-50f91af1e4d9", + "name": "Supabase - Upsert Tarifario" + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg", + "mode": "list", + "cachedResultName": "Tarifario de Creatividad - Sep 2004", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1041164995, + "mode": "list", + "cachedResultName": "Catálogo Tarifario App", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit#gid=1041164995" + }, + "options": { + "dataLocationOnSheet": { + "values": { + "rangeDefinition": "detectAutomatically" + } + } + } + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 208, + 0 + ], + "id": "e6d4f9ef-30da-4640-9002-96b43aa48729", + "name": "Google Sheets - Leer Catálogo Tarifario App", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "jsCode": "const source = $('Code - Normalizar Tarifario').first().json;\n\nconst updates = source.generated_code_updates || [];\n\nreturn updates.map((update) => ({\n json: {\n row_number: update.row_number,\n codigo_base: update.codigo_base,\n },\n}));" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 832, + 0 + ], + "id": "247f2f76-a1e4-4e51-b35f-a32bdaadf354", + "name": "Code - Preparar códigos generados" + }, + { + "parameters": { + "operation": "update", + "documentId": { + "__rl": true, + "value": "1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg", + "mode": "list", + "cachedResultName": "Tarifario de Creatividad - Sep 2004", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1041164995, + "mode": "list", + "cachedResultName": "Catálogo Tarifario App", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BGTCXbeqo-QAaoBx2pHJo9p7Trzd7Qh0FpEFypSyTAg/edit#gid=1041164995" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "row_number": "={{ $json[\"row_number\"] }}", + "Código base": "={{ $json[\"codigo_base\"] }}" + }, + "matchingColumns": [ + "row_number" + ], + "schema": [ + { + "id": "Código base", + "displayName": "Código base", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "Sección", + "displayName": "Sección", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Categoría", + "displayName": "Categoría", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Servicio / ítem", + "displayName": "Servicio / ítem", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Tipo de trabajo", + "displayName": "Tipo de trabajo", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Etiqueta corta", + "displayName": "Etiqueta corta", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Hora hombre ref.", + "displayName": "Hora hombre ref.", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 1 nombre", + "displayName": "Nivel 1 nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 1 monto/rango", + "displayName": "Nivel 1 monto/rango", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 1 nota", + "displayName": "Nivel 1 nota", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 2 nombre", + "displayName": "Nivel 2 nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 2 monto/rango", + "displayName": "Nivel 2 monto/rango", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 2 nota", + "displayName": "Nivel 2 nota", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 3 nombre", + "displayName": "Nivel 3 nombre", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 3 monto/rango", + "displayName": "Nivel 3 monto/rango", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Nivel 3 nota", + "displayName": "Nivel 3 nota", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "Observaciones / incluye", + "displayName": "Observaciones / incluye", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": true + }, + { + "id": "row_number", + "displayName": "row_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "readOnly": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4.7, + "position": [ + 1072, + 0 + ], + "id": "60367c04-79f6-4c8a-99d1-bc9369c8ca61", + "name": "Google Sheets - Escribir códigos únicos faltantes", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "K0hDZh3a85MpOHCs", + "name": "Google Sheets account 2" + } + } + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours", + "hoursInterval": 8 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -16, + 0 + ], + "id": "1a45e645-65af-4727-bf4f-15c7000ba2a2", + "name": "Schedule Trigger" + } + ], + "connections": { + "Code - Normalizar Tarifario": { + "main": [ + [ + { + "node": "Supabase - Upsert Tarifario", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - Upsert Tarifario": { + "main": [ + [ + { + "node": "Code - Preparar códigos generados", + "type": "main", + "index": 0 + } + ] + ] + }, + "Google Sheets - Leer Catálogo Tarifario App": { + "main": [ + [ + { + "node": "Code - Normalizar Tarifario", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Preparar códigos generados": { + "main": [ + [ + { + "node": "Google Sheets - Escribir códigos únicos faltantes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "Google Sheets - Leer Catálogo Tarifario App", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "authors": "Isaac Aracena", + "name": "Version 806aa39c", + "description": "", + "autosaved": true, + "workflowPublishHistory": [ + { + "createdAt": "2026-06-21T01:54:00.165Z", + "id": 1683, + "workflowId": "7eyfZEkGuDSUodDX", + "versionId": "806aa39c-41e1-4c6a-8d58-60fb431346a1", + "event": "activated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + ] + } +} \ No newline at end of file