889 lines
41 KiB
JSON
889 lines
41 KiB
JSON
{
|
|
"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 <iaracena@gomezleemarketing.com>",
|
|
"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"
|
|
}
|
|
]
|
|
}
|
|
} |