Files
rutero/n8n-workflow/OptimizadorRutasPDV TEST - Fixed v2.json

403 lines
25 KiB
JSON

{
"name": "OptimizadorRutasPDV TEST - Fixed v4",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "calcular-camino",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-736,
544
],
"id": "f84fe2f2-2875-4e5c-ad99-4b4cc7a2c8d2",
"name": "Webhook",
"webhookId": "01fc4a9c-bad3-42b8-9f60-b87d91302502"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "c1",
"leftValue": "={{$binary.pdv_file.fileName}}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
}
},
{
"id": "c2",
"leftValue": "={{$binary.usuarios_file.fileName}}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
-496,
544
],
"id": "5ecfc62e-849c-442b-90b8-4885494e3820",
"name": "Validate Files"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "{\n \"error\": \"Debe subir ambos archivos: PDV y Usuarios\"\n}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
-496,
768
],
"id": "2c0adaf5-050d-4c2e-a509-ed77cc5a6784",
"name": "Error: Missing Files"
},
{
"parameters": {
"operation": "xlsx",
"binaryPropertyName": "pdv_file",
"options": {
"headerRow": true
}
},
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1.1,
"position": [
-160,
416
],
"id": "90f15538-1ae9-454b-9038-b3d3cbf39f00",
"name": "Extract PDV"
},
{
"parameters": {
"operation": "xlsx",
"binaryPropertyName": "usuarios_file",
"options": {
"headerRow": true
}
},
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1.1,
"position": [
-160,
640
],
"id": "a8a72a9e-7e38-486a-9db2-7efb130c6963",
"name": "Extract Usuarios"
},
{
"parameters": {
"jsCode": "const allRows = $input.all().map(item => item.json);\nif (!allRows.length) {\n return [{ json: { success: false, data: [], message: 'PDV: Archivo vacío' } }];\n}\nconst keys = new Set(allRows.flatMap(r => Object.keys(r)));\nconst required = ['ID','Nombre del PDV','Segmentación','Latitud','Longitud','Visitas semanales','Duración visita(horas)','Prioridad'];\nconst missing = required.filter(c => !keys.has(c));\nlet message = '';\nlet success = true;\nif (missing.length) {\n success = false;\n message = `PDV: Columnas faltantes: ${missing.join(', ')}`;\n}\nif (allRows.length > 2000) {\n success = false;\n message = 'PDV: Máximo 2000 filas permitidas';\n}\nreturn [{ json: { success, data: $input.all(), message } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
80,
416
],
"id": "e7acdf1f-4741-4189-91b9-30a90d42b531",
"name": "Validate PDV"
},
{
"parameters": {
"jsCode": "const allRows = $input.all().map(item => item.json);\nif (!allRows.length) {\n return [{ json: { success: false, data: [], message: 'Usuarios: Archivo vacío' } }];\n}\nconst keys = new Set(allRows.flatMap(r => Object.keys(r)));\nconst required = ['ID','Nombre del empleado','Latitud','Longitud','Horas por día usuario','Horas por semana usuario'];\nconst missing = required.filter(c => !keys.has(c));\nlet message = '';\nlet success = true;\nif (missing.length) {\n success = false;\n message = `Usuarios: Columnas faltantes: ${missing.join(', ')}`;\n}\nif (allRows.length > 50) {\n success = false;\n message = 'Usuarios: Máximo 50 filas permitidas';\n}\nreturn [{ json: { success, data: $input.all(), message } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
80,
640
],
"id": "0d734c5f-bf19-4eaa-9d82-16657523b53c",
"name": "Validate Usuarios"
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
336,
544
],
"id": "e13702a2-1b82-4a1f-bba6-e8880c8346aa",
"name": "Merge Validations"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ error: $json[0].message || $json[1].message || 'Error de validación' }) }}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
576,
768
],
"id": "d496c883-3206-47f5-89f4-f829ed167845",
"name": "Error: Validation Failed"
},
{
"parameters": {
"jsCode": "return await (async () => {\nconst items = $input.all();\n\nconst pdvResult = items[0]?.json;\nconst userResult = items[1]?.json;\n\nif (!pdvResult?.success) {\n return [{ json: { error: pdvResult?.message || 'Error validación PDVs' } }];\n}\nif (!userResult?.success) {\n return [{ json: { error: userResult?.message || 'Error validación Usuarios' } }];\n}\n\nconst pdvsRaw = pdvResult.data.map(item => item.json);\nconst usuariosRaw = userResult.data.map(item => item.json);\n\nif (!pdvsRaw.length || !usuariosRaw.length) {\n return [{ json: { error: 'Archivos vacíos o sin datos válidos' } }];\n}\n\nconst DIAS_SEMANA = ['Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes'];\n\nconst asignaciones = [];\nconst PDVnoAsignados = [];\n\nfunction calcularDistancia(lat1, lon1, lat2, lon2) {\n const R = 6371;\n const dLat = (lat2 - lat1) * Math.PI / 180;\n const dLon = (lon2 - lon1) * Math.PI / 180;\n const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLon/2)**2;\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));\n}\n\nfunction formatHeader(value) {\n return value.toLowerCase()\n .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9 ]/gi, '').trim().replace(/\\s+/g, '_');\n}\n\nfunction cleanNum(val) {\n if (val === null || val === undefined) return 0;\n if (typeof val === 'number') return val;\n return parseFloat(String(val).replace(/[^0-9.\\-]/g, '')) || 0;\n}\n\nfunction renameHeaders(rows) {\n return rows.map(row => {\n const newRow = {};\n for (const key in row) {\n const newKey = formatHeader(key);\n let val = row[key];\n if (typeof val === 'string') val = val.trim();\n newRow[newKey] = val;\n }\n return newRow;\n });\n}\n\nconst pdvs = renameHeaders(pdvsRaw);\nconst usuarios = renameHeaders(usuariosRaw);\n\n// Mapear columnas del Excel a nombres internos — P1 robustez\nfunction mapPdv(p) {\n const duracion = cleanNum(p.duracion_visitahoras ?? p.duracion_visita_horas ?? p.duracion_visita ?? 0);\n return {\n id: p.id,\n nombre_del_pdv: p.nombre_del_pdv,\n segmentacion: (p.segmentacion || 'D').toString().trim().toUpperCase(),\n latitud: cleanNum(p.latitud),\n longitud: cleanNum(p.longitud),\n visitas_semanales: Math.max(1, Math.min(7, Math.round(cleanNum(p.visitas_semanales) || 1))),\n duracion_visita_horas: duracion,\n prioridad: Math.round(cleanNum(p.prioridad) || 5),\n activo: (p.activo ?? 'Si').toString().trim(),\n };\n}\n\nfunction mapUsuario(u) {\n return {\n id: u.id,\n nombre_del_empleado: u.nombre_del_empleado,\n latitud: cleanNum(u.latitud),\n longitud: cleanNum(u.longitud),\n horas_por_dia_usuario: cleanNum(u.horas_por_dia_usuario),\n horas_por_semana_usuario: cleanNum(u.horas_por_semana_usuario),\n visitas_maximas_por_dia: Math.max(1, Math.round(cleanNum(u.visitas_maximas_por_dia) || 8)),\n usuario_activo: (u.usuario_activo ?? u.usuarioactivo ?? 'Si').toString().trim(),\n };\n}\n\nlet pdvsMapeados = pdvs.map(mapPdv).filter(p => {\n const v = p.activo.toLowerCase();\n return v !== 'n' && v !== 'no' && v !== '0';\n});\nlet usuariosMapeados = usuarios.map(mapUsuario).filter(u => {\n const v = u.usuario_activo.toLowerCase();\n return v !== 'n' && v !== 'no' && v !== '0';\n});\n\nif (!pdvsMapeados.length) {\n return [{ json: { error: 'No hay PDVs activos para asignar' } }];\n}\nif (!usuariosMapeados.length) {\n return [{ json: { error: 'No hay usuarios activos disponibles' } }];\n}\n\nfunction getDiasParaVisitas(n) {\n n = Math.max(1, Math.min(5, Math.round(n)));\n if (n === 1) return [DIAS_SEMANA[0]];\n if (n === 2) return [DIAS_SEMANA[0], DIAS_SEMANA[3]];\n if (n === 3) return [DIAS_SEMANA[0], DIAS_SEMANA[2], DIAS_SEMANA[4]];\n if (n === 4) return [DIAS_SEMANA[0], DIAS_SEMANA[1], DIAS_SEMANA[3], DIAS_SEMANA[4]];\n return [...DIAS_SEMANA];\n}\n\nfunction asignarPdvUsuarios() {\n const orden = ['AA', 'A', 'B', 'C', 'D'];\n const segmentWeight = { AA: 100, A: 80, B: 60, C: 40, D: 20 };\n const horasDisp = {};\n const horasNecesita = {};\n const pdvsAsignados = new Set();\n\n for (const u of usuariosMapeados) horasDisp[u.id] = u.horas_por_semana_usuario;\n for (const p of pdvsMapeados) horasNecesita[p.id] = p.duracion_visita_horas * p.visitas_semanales;\n\n const pdvsPendientes = [...pdvsMapeados].sort((a, b) => {\n const idxA = orden.indexOf(a.segmentacion);\n const idxB = orden.indexOf(b.segmentacion);\n const ia = idxA === -1 ? 99 : idxA;\n const ib = idxB === -1 ? 99 : idxB;\n const scoreA = (segmentWeight[a.segmentacion] || 0) + (6 - a.prioridad) * 6;\n const scoreB = (segmentWeight[b.segmentacion] || 0) + (6 - b.prioridad) * 6;\n return scoreB - scoreA || ia - ib || a.prioridad - b.prioridad;\n });\n\n function candidatosViablesParaUsuario(usuario) {\n return pdvsPendientes\n .filter(pdv => !pdvsAsignados.has(pdv.id) && horasNecesita[pdv.id] > 0.001 && horasNecesita[pdv.id] <= horasDisp[usuario.id] + 0.001)\n .map(pdv => ({\n pdv,\n distancia: calcularDistancia(usuario.latitud, usuario.longitud, pdv.latitud, pdv.longitud),\n score: (segmentWeight[pdv.segmentacion] || 0) + (6 - pdv.prioridad) * 6\n }))\n .sort((a, b) => a.distancia - b.distancia || b.score - a.score);\n }\n\n let assignedInRound = true;\n while (assignedInRound) {\n assignedInRound = false;\n const opcionesPorUsuario = usuariosMapeados\n .filter(usuario => horasDisp[usuario.id] > 0.001)\n .map(usuario => ({ usuario, candidatos: candidatosViablesParaUsuario(usuario) }))\n .filter(item => item.candidatos.length > 0)\n .sort((a, b) => b.candidatos[0].distancia - a.candidatos[0].distancia);\n\n const usuariosAsignadosEnRonda = new Set();\n for (const opcion of opcionesPorUsuario) {\n const usuario = opcion.usuario;\n if (usuariosAsignadosEnRonda.has(usuario.id)) continue;\n\n const candidato = candidatosViablesParaUsuario(usuario)[0];\n if (!candidato) continue;\n\n const pdv = candidato.pdv;\n const horasAsignadas = horasNecesita[pdv.id];\n\n asignaciones.push({\n usuario_id: usuario.id,\n pdv_id: pdv.id,\n distancia: candidato.distancia,\n horas_asignadas: horasAsignadas,\n horasDiariasUsuario: usuario.horas_por_dia_usuario,\n visitas_semanales: pdv.visitas_semanales,\n visitas_maximas_por_dia: usuario.visitas_maximas_por_dia,\n });\n\n horasDisp[usuario.id] -= horasAsignadas;\n horasNecesita[pdv.id] = 0;\n pdvsAsignados.add(pdv.id);\n usuariosAsignadosEnRonda.add(usuario.id);\n assignedInRound = true;\n }\n }\n\n for (const pdv of pdvsPendientes) {\n const faltante = horasNecesita[pdv.id];\n if (faltante > 0.001) {\n PDVnoAsignados.push({\n pdv_id: pdv.id,\n pdv_nombre: pdv.nombre_del_pdv,\n minimo_horas_semana: Number(faltante.toFixed(2)),\n latitud: pdv.latitud,\n longitud: pdv.longitud,\n motivo: 'No hay usuario disponible con capacidad por carga para asignar el PDV completo'\n });\n }\n }\n}\n\nfunction asignarDiasHorasVisita() {\n const visitas = [];\n const visitasPorDia = {}; // usuario_id -> dia -> count\n for (const u of usuariosMapeados) {\n visitasPorDia[u.id] = {};\n for (const d of DIAS_SEMANA) visitasPorDia[u.id][d] = 0;\n }\n\n for (const a of asignaciones) {\n const nVisitas = a.visitas_semanales || 1;\n const diasAsignados = getDiasParaVisitas(nVisitas);\n const horasPorVisita = a.horas_asignadas / diasAsignados.length;\n const tiempoDesplazamiento = 0;\n\n // Validar horas por día antes de asignar\n if (horasPorVisita > a.horasDiariasUsuario + 0.001) {\n PDVnoAsignados.push({\n pdv_id: a.pdv_id,\n pdv_nombre: pdvsById[a.pdv_id]?.nombre_del_pdv || '',\n minimo_horas_semana: Number(a.horas_asignadas.toFixed(2)),\n latitud: pdvsById[a.pdv_id]?.latitud || 0,\n longitud: pdvsById[a.pdv_id]?.longitud || 0,\n motivo: `Excede horas por día del usuario (${horasPorVisita.toFixed(1)}h > ${a.horasDiariasUsuario}h)`\n });\n continue;\n }\n\n for (const dia of diasAsignados) {\n // Respetar visitas máximas por día\n const maxVisitas = a.visitas_maximas_por_dia || 8;\n if (visitasPorDia[a.usuario_id][dia] >= maxVisitas) {\n // Buscar siguiente día con cupo\n let diaAlterno = null;\n for (const d of DIAS_SEMANA) {\n if (visitasPorDia[a.usuario_id][d] < maxVisitas) { diaAlterno = d; break; }\n }\n if (!diaAlterno) {\n PDVnoAsignados.push({\n pdv_id: a.pdv_id,\n pdv_nombre: pdvsById[a.pdv_id]?.nombre_del_pdv || '',\n minimo_horas_semana: Number(horasPorVisita.toFixed(2)),\n latitud: pdvsById[a.pdv_id]?.latitud || 0,\n longitud: pdvsById[a.pdv_id]?.longitud || 0,\n motivo: `Visitas máximas por día excedidas para ${dia} (máx ${maxVisitas})`\n });\n continue;\n }\n visitas.push({\n usuario_id: a.usuario_id,\n pdv_id: a.pdv_id,\n dia: diaAlterno,\n horas_trabajo: Number(horasPorVisita.toFixed(2)),\n horas_desplazamiento: Number(tiempoDesplazamiento.toFixed(2)),\n distancia: Number(a.distancia.toFixed(2)),\n });\n visitasPorDia[a.usuario_id][diaAlterno]++;\n } else {\n visitas.push({\n usuario_id: a.usuario_id,\n pdv_id: a.pdv_id,\n dia: dia,\n horas_trabajo: Number(horasPorVisita.toFixed(2)),\n horas_desplazamiento: Number(tiempoDesplazamiento.toFixed(2)),\n distancia: Number(a.distancia.toFixed(2)),\n });\n visitasPorDia[a.usuario_id][dia]++;\n }\n }\n }\n return visitas;\n}\n\n// Necesitamos pdvsById antes para asignarDias\nconst pdvsByIdPre = Object.fromEntries(pdvsMapeados.map(p => [p.id, p]));\n// Exponer global para asignarDiasHorasVisita (usado en push de error)\nglobalThis.pdvsById = pdvsByIdPre;\nvar pdvsById = pdvsByIdPre;\n\nasignarPdvUsuarios();\nlet visitas = asignarDiasHorasVisita();\n\n// --- P2.3: Orden intra-día greedy por cercanía Haversine (mejora listado sin cambiar día) ---\ntry {\n const grupoKey = (v) => `${v.usuario_id}__${v.dia}`;\n const grupos = {};\n for (const v of visitas) {\n const k = grupoKey(v);\n if (!grupos[k]) grupos[k] = [];\n grupos[k].push(v);\n }\n let visitasOrdenadas = [];\n for (const k in grupos) {\n const arr = grupos[k];\n if (arr.length <= 2) { visitasOrdenadas.push(...arr); continue; }\n const usuariosByIdPreLocal = Object.fromEntries(usuariosMapeados.map(u=>[String(u.id), u]));\n const firstU = usuariosByIdPreLocal[String(arr[0].usuario_id)] || usuariosMapeados.find(u=> String(u.id)===String(arr[0].usuario_id));\n // Nearest neighbor greedy starting from usuario location\n let restantes = [...arr];\n // Start with pdv closest to usuario\n restantes.sort((a,b)=>{\n const pa = pdvsByIdPre[a.pdv_id], pb = pdvsByIdPre[b.pdv_id];\n if (!pa||!pb) return 0;\n return calcularDistancia(firstU.latitud, firstU.longitud, pa.latitud, pa.longitud) - calcularDistancia(firstU.latitud, firstU.longitud, pb.latitud, pb.longitud);\n });\n let ordenado = [restantes.shift()];\n while (restantes.length) {\n const last = ordenado[ordenado.length-1];\n const lp = pdvsByIdPre[last.pdv_id];\n let bestIdx = 0, bestDist = Infinity;\n for (let i=0;i<restantes.length;i++) {\n const cand = pdvsByIdPre[restantes[i].pdv_id];\n if (!cand||!lp) continue;\n const d = calcularDistancia(lp.latitud, lp.longitud, cand.latitud, cand.longitud);\n if (d < bestDist) { bestDist = d; bestIdx = i; }\n }\n ordenado.push(restantes.splice(bestIdx,1)[0]);\n }\n visitasOrdenadas.push(...ordenado);\n }\n visitas = visitasOrdenadas;\n} catch(e) { /* fallback a visitas sin ordenar */ }\n\nconst usuariosById = Object.fromEntries(usuariosMapeados.map(u => [u.id, u]));\nconst pdvsByIdFinal = Object.fromEntries(pdvsMapeados.map(p => [p.id, p]));\n// Reasignar para uso posterior\npdvsById = pdvsByIdFinal;\n\nconst resultadoFinal = visitas.map(v => {\n const u = usuariosById[v.usuario_id];\n const p = pdvsById[v.pdv_id];\n if (!u || !p) return null;\n return {\n usuario_id: v.usuario_id,\n nombre_usuario: u.nombre_del_empleado,\n latitud_usuario: u.latitud,\n longitud_usuario: u.longitud,\n hora_laboral_semanal_usuario: u.horas_por_semana_usuario,\n pdv_id: v.pdv_id,\n nombre_pdv: p.nombre_del_pdv,\n latitud_pdv: p.latitud,\n longitud_pdv: p.longitud,\n hora_minima_semanal_pdv: p.duracion_visita_horas * p.visitas_semanales,\n dia: v.dia,\n horas_trabajo: v.horas_trabajo,\n horas_desplazamiento: v.horas_desplazamiento,\n distancia: v.distancia,\n };\n}).filter(Boolean);\n\nconst a = resultadoFinal.map(r => ({ json: r }));\n// P1: ya no necesita bFiltrado tramposo, pero lo mantenemos por compatibilidad sin duplicar pdv_id con horas faltantes\nconst pdvIdsAsignados = new Set(a.map(x => x.json.pdv_id));\nconst bFiltrado = PDVnoAsignados.filter(pdv => {\n if (pdv.motivo && (pdv.motivo.includes('faltan') || pdv.motivo.includes('Visitas m') || pdv.motivo.includes('Excede'))) return true;\n return !pdvIdsAsignados.has(pdv.pdv_id);\n});\n\nreturn [{ assigned: a }, { noAssigned: bFiltrado }];\n\n})();"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
816,
544
],
"id": "e9fe9826-4755-497e-ae6e-274e7c1d4f66",
"name": "Optimize Routes"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1024,
544
],
"id": "b024c203-1b36-4f7c-97bb-3923481b7a9b",
"name": "Respond Success"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst pdv = items[0]; // Validate PDV output\nconst usuarios = items[1]; // Validate Usuarios output\n\nconst pdvOk = pdv?.json?.success === true;\nconst usuariosOk = usuarios?.json?.success === true;\n\nif (pdvOk && usuariosOk) {\n // Both passed → send to Optimize Routes (output 0)\n return items;\n} else {\n // Failed → send to Error (output 1)\n const msgs = [];\n if (!pdvOk) msgs.push(pdv?.json?.message || 'Error PDV');\n if (!usuariosOk) msgs.push(usuarios?.json?.message || 'Error Usuarios');\n throw new Error(msgs.join(' | '));\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
576,
544
],
"id": "27eb73de-4b08-4c44-97de-4d122a98749c",
"name": "Code in JavaScript",
"onError": "continueErrorOutput"
}
],
"pinData": {
"Webhook": [
{
"json": {
"headers": {
"host": "agenteit.digitalcompass.agency",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
"content-length": "19829",
"accept": "application/json, text/plain, */*",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "en-US,en;q=0.6",
"content-type": "multipart/form-data; boundary=----WebKitFormBoundaryeTQ0nhU3IDSv0jrS",
"origin": "http://localhost:4200",
"priority": "u=1, i",
"referer": "http://localhost:4200/",
"sec-ch-ua": "\"Not=A?Brand\";v=\"99\", \"Brave\";v=\"151\", \"Chromium\";v=\"151\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"sec-gpc": "1",
"x-forwarded-for": "152.0.172.53",
"x-forwarded-host": "agenteit.digitalcompass.agency",
"x-forwarded-port": "443",
"x-forwarded-proto": "https",
"x-forwarded-server": "07b4f09d2c65",
"x-real-ip": "152.0.172.53"
},
"params": {},
"query": {},
"body": {},
"webhookUrl": "https://agenteit.digitalcompass.agency/webhook/calcular-camino",
"executionMode": "production"
},
"binary": {
"pdv_file": {
"mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"fileExtension": "xlsx",
"data": "filesystem-v2",
"fileName": "Plantilla_PDV (2).xlsx",
"id": "filesystem-v2:workflows/IkKMqgb7uZuRk9d9/executions/48469/binary_data/48a414e1-9c74-469b-9bec-04283032f093",
"fileSize": "9.53 kB",
"bytes": 9528
},
"usuarios_file": {
"mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"fileExtension": "xlsx",
"data": "filesystem-v2",
"fileName": "Plantilla_Usuarios (2).xlsx",
"id": "filesystem-v2:workflows/IkKMqgb7uZuRk9d9/executions/48469/binary_data/6553aefd-4d40-45dd-9cdd-bfb1b1d1ec84",
"fileSize": "9.82 kB",
"bytes": 9825
}
},
"pairedItem": {
"item": 0
}
}
]
},
"connections": {
"Webhook": {
"main": [
[
{
"node": "Validate Files",
"type": "main",
"index": 0
}
]
]
},
"Validate Files": {
"main": [
[
{
"node": "Extract PDV",
"type": "main",
"index": 0
},
{
"node": "Extract Usuarios",
"type": "main",
"index": 0
}
],
[
{
"node": "Error: Missing Files",
"type": "main",
"index": 0
}
]
]
},
"Extract PDV": {
"main": [
[
{
"node": "Validate PDV",
"type": "main",
"index": 0
}
]
]
},
"Extract Usuarios": {
"main": [
[
{
"node": "Validate Usuarios",
"type": "main",
"index": 0
}
]
]
},
"Validate PDV": {
"main": [
[
{
"node": "Merge Validations",
"type": "main",
"index": 0
}
]
]
},
"Validate Usuarios": {
"main": [
[
{
"node": "Merge Validations",
"type": "main",
"index": 1
}
]
]
},
"Merge Validations": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Optimize Routes": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "Optimize Routes",
"type": "main",
"index": 0
}
],
[
{
"node": "Error: Validation Failed",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"timeSavedMode": "fixed",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": true
},
"versionId": "f0dcd3f6-8043-4c22-9ca9-a477ae69db53",
"meta": {
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
},
"id": "IkKMqgb7uZuRk9d9",
"tags": []
}