{
  "name": "Logixb2b — Migrar datos CRM sin perder automatizaciones",
  "meta": {
    "templateCredsSetupCompleted": false,
    "description": "Manual Trigger → Config migración → Fetch lote origen → Mapear campos → Upsert destino → Inventario automatismos (Sheets) → Slack resumen. Europe/Madrid. Placeholders de credenciales."
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Madrid"
  },
  "active": false,
  "pinData": {},
  "tags": [
    {
      "name": "Logixb2b"
    },
    {
      "name": "recursos"
    },
    {
      "name": "crm"
    },
    {
      "name": "migracion"
    }
  ],
  "nodes": [
    {
      "id": "sticky-instrucciones-logix",
      "name": "INSTRUCCIONES (conectar + VPS)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -480,
        40
      ],
      "parameters": {
        "content": "## INSTRUCCIONES — Migrar datos CRM sin perder automatizaciones\n\n**Recurso:** `/recursos/migrar-datos-crm-automatizaciones-n8n`\n**Guía:** /migrar-datos-crm-sin-perder-automatizaciones\n**Zona:** Europe/Madrid · `active: false` al importar (tú lo activas).\n\n### 1) Importar\n1. n8n → ⋯ → **Import from File** → este JSON.\n2. El flujo llega **inactive**. No pegues secretos en el lienzo ni en el archivo.\n\n### 2) Credenciales y configuración por nodo\n- **Manual Trigger migración** (`manual`): sin credencial. Úsalo para pruebas; en producción sustituye por Webhook o Schedule si quieres automatismo.\n- **Config migración CRM** (`Set` Config): **no lleva credencial**. Aquí van URLs/placeholders (`REEMPLAZA_*`, `example.com`, `TU-CRM`). Sustituye todos antes de Active.\n- **Fetch lote CRM origen** (`HTTP Request` CRM): Credentials → **Header Auth** / OAuth del CRM. Sustituye `crm*Url` en el nodo **Config**. Prueba con un Lead de sandbox.\n- **Split personas** (`Split Out`): sin credencial. Confirma el campo a expandir (array de personas/items).\n- **Mapear campos origen→destino** (`Code`): sin credencial. Revisa límites/umbrales en el JS; no hardcodees secretos.\n- **IF email válido** (`IF`): sin credencial. Comprueba la condición (email, umbral, over_limit, etc.) con datos reales de prueba.\n- **Upsert CRM destino (HTTP)** (`HTTP Request` CRM): Credentials → **Header Auth** / OAuth del CRM. Sustituye `crm*Url` en el nodo **Config**. Prueba con un Lead de sandbox.\n- **Append inventario automatismos** (`Google Sheets`): Credentials → **Google Sheets OAuth2**. Sustituye Spreadsheet ID / Sheet name. Comparte la hoja con la cuenta OAuth.\n- **Slack resumen lote** (`HTTP Request` → Slack/Teams): crea Incoming Webhook en Slack (o Adaptive Card URL en Teams). Pega la URL en **Config** o en este nodo. Auth: none (la URL ya es secreta).\n\n### 3) Orden recomendado de puesta en marcha\n1. Rellena el nodo **Config** (URLs reales, umbrales, webhooks de Slack).\n2. Crea Credentials en n8n (candado de cada nodo HTTP / Sheets / OpenAI) y **enlázalas**.\n3. Ejecuta **una vez** (Manual / Test URL del Webhook / \"Execute workflow\").\n4. Verifica CRM + Slack/Sheets (y que la IA no escriba en producción sin HITL).\n5. Solo entonces: interruptor **Active**.\n\n### 4) Producción 24/7 en VPS (UE)\nImportar en localhost no basta: webhooks y cron necesitan n8n encendido.\n\n1. VPS en UE (Hetzner/OVH/IONOS…) + Docker Compose + **Postgres** + HTTPS (Caddy/Nginx).\n2. Variables mínimas: `GENERIC_TIMEZONE=Europe/Madrid`, `WEBHOOK_URL=https://n8n.tudominio.com/`, `N8N_ENCRYPTION_KEY`, `N8N_HOST` / `N8N_PROTOCOL=https`.\n3. Firewall: solo 80/443 (+ SSH). Backups del volumen Postgres + export de workflows.\n4. Importa este JSON en la instancia de producción, vuelve a crear Credentials (no se copian solas), actualiza URLs del Config al dominio real.\n5. Pon **Active**. Prueba el Webhook de producción o espera el próximo cron.\n6. Alternativa: **n8n Cloud** (mismo JSON; Credentials en la UI Cloud).\n\n### 5) RGPD + EU AI Act (resumen)\nSi hay nodos IA sois **deployers** (Reglamento UE 2024/1689). Art. 4 alfabetización. **HITL** antes de outreach, scores masivos o pausas de cuenta. Minimiza PII en Slack/logs. Informativo — no es asesoramiento legal. /uso-responsable-de-ia-y-transparencia\n",
        "height": 1500,
        "width": 580,
        "color": 4
      }
    },
    {
      "id": "mig-trigger",
      "name": "Manual Trigger migración",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ],
      "parameters": {}
    },
    {
      "id": "mig-config",
      "name": "Config migración CRM",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        320
      ],
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "m1",
              "name": "sourceProvider",
              "value": "pipedrive",
              "type": "string"
            },
            {
              "id": "m2",
              "name": "destProvider",
              "value": "hubspot",
              "type": "string"
            },
            {
              "id": "m3",
              "name": "sourceBaseUrl",
              "value": "https://api.pipedrive.com/v1",
              "type": "string"
            },
            {
              "id": "m4",
              "name": "sourceToken",
              "value": "REEMPLAZA_SOURCE_TOKEN",
              "type": "string"
            },
            {
              "id": "m5",
              "name": "destBaseUrl",
              "value": "https://api.hubapi.com",
              "type": "string"
            },
            {
              "id": "m6",
              "name": "destToken",
              "value": "REEMPLAZA_DEST_TOKEN",
              "type": "string"
            },
            {
              "id": "m7",
              "name": "batchSize",
              "value": "25",
              "type": "string"
            },
            {
              "id": "m8",
              "name": "startOffset",
              "value": "0",
              "type": "string"
            },
            {
              "id": "m9",
              "name": "fieldMapJson",
              "value": "{\"email\":\"email\",\"firstname\":\"first_name\",\"lastname\":\"last_name\",\"phone\":\"phone\",\"company\":\"org_name\"}",
              "type": "string"
            },
            {
              "id": "m10",
              "name": "dedupeProperty",
              "value": "email",
              "type": "string"
            },
            {
              "id": "m11",
              "name": "sheetsInventoryId",
              "value": "REEMPLAZA_SHEETS_ID_OPCIONAL",
              "type": "string"
            },
            {
              "id": "m12",
              "name": "slackWebhookUrl",
              "value": "",
              "type": "string"
            },
            {
              "id": "m13",
              "name": "migrationLabel",
              "value": "crm-mig-2026",
              "type": "string"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "mig-fetch",
      "name": "Fetch lote CRM origen",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        540,
        320
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $json.sourceBaseUrl }}/persons?limit={{ $json.batchSize }}&start={{ $json.startOffset }}&api_token={{ $json.sourceToken }}",
        "authentication": "none",
        "options": {
          "timeout": 30000
        }
      }
    },
    {
      "id": "mig-split",
      "name": "Split personas",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        800,
        320
      ],
      "parameters": {
        "fieldToSplitOut": "data",
        "options": {}
      }
    },
    {
      "id": "mig-map",
      "name": "Mapear campos origen→destino",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1060,
        320
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const cfg = $('Config migración CRM').first().json;\nlet map = {};\ntry { map = JSON.parse(cfg.fieldMapJson || '{}'); } catch (e) { map = {}; }\nconst src = item.json || {};\nconst out = {\n  migrationLabel: cfg.migrationLabel,\n  sourceId: src.id || src.Id || '',\n  destProvider: cfg.destProvider,\n  dedupeProperty: cfg.dedupeProperty || 'email',\n  properties: {}\n};\nfor (const [destKey, srcKey] of Object.entries(map)) {\n  let v = src[srcKey];\n  if (v == null && srcKey.includes('.')) {\n    v = srcKey.split('.').reduce((a, k) => (a == null ? undefined : a[k]), src);\n  }\n  if (Array.isArray(v) && v[0] && typeof v[0] === 'object' && 'value' in v[0]) v = v[0].value;\n  if (v != null && String(v).trim() !== '') out.properties[destKey] = String(v).trim();\n}\nout.email = (out.properties.email || '').toLowerCase();\nout.ok = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(out.email);\nreturn { json: out };"
      }
    },
    {
      "id": "mig-if",
      "name": "IF email válido",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1320,
        320
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "mig-upsert",
      "name": "Upsert CRM destino (HTTP)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1600,
        220
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config migración CRM').item.json.destBaseUrl }}/crm/v3/objects/contacts/batch/upsert",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('Config migración CRM').item.json.destToken }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"inputs\": [{\n    \"idProperty\": \"email\",\n    \"id\": \"{{ $json.email }}\",\n    \"properties\": {{ JSON.stringify($json.properties) }}\n  }]\n}",
        "options": {
          "timeout": 30000
        }
      }
    },
    {
      "id": "mig-inventory",
      "name": "Append inventario automatismos",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1880,
        220
      ],
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Config migración CRM').item.json.sheetsInventoryId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Inventario",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "fecha": "={{ $now.setZone('Europe/Madrid').toFormat('yyyy-MM-dd HH:mm') }}",
            "migrationLabel": "={{ $('Config migración CRM').item.json.migrationLabel }}",
            "sourceId": "={{ $('Mapear campos origen→destino').item.json.sourceId }}",
            "email": "={{ $('Mapear campos origen→destino').item.json.email }}",
            "destProvider": "={{ $('Config migración CRM').item.json.destProvider }}",
            "estado": "upsert_ok",
            "nota_reenganche": "Revisar workflows: webhook form, alertas Lead caliente, WhatsApp, scoring, assignment territorio"
          }
        },
        "options": {}
      }
    },
    {
      "id": "mig-slack",
      "name": "Slack resumen lote",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2160,
        220
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config migración CRM').item.json.slackWebhookUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"text\": \"Migración CRM {{ $('Config migración CRM').item.json.migrationLabel }}: upsert {{ $('Mapear campos origen→destino').item.json.email }} (origen {{ $('Mapear campos origen→destino').item.json.sourceId }}) → {{ $('Config migración CRM').item.json.destProvider }}. Revisa inventario Sheets y reengancha automatismos.\"\n}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "mig-skip",
      "name": "Skip sin email",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1600,
        460
      ],
      "parameters": {}
    }
  ],
  "connections": {
    "Manual Trigger migración": {
      "main": [
        [
          {
            "node": "Config migración CRM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config migración CRM": {
      "main": [
        [
          {
            "node": "Fetch lote CRM origen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch lote CRM origen": {
      "main": [
        [
          {
            "node": "Split personas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split personas": {
      "main": [
        [
          {
            "node": "Mapear campos origen→destino",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mapear campos origen→destino": {
      "main": [
        [
          {
            "node": "IF email válido",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF email válido": {
      "main": [
        [
          {
            "node": "Upsert CRM destino (HTTP)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip sin email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert CRM destino (HTTP)": {
      "main": [
        [
          {
            "node": "Append inventario automatismos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append inventario automatismos": {
      "main": [
        [
          {
            "node": "Slack resumen lote",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
