{
  "name": "Jsonpage - Verify and enrich a CSV file dropped in Google Drive",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "value": "YOUR_FOLDER_ID",
          "mode": "id"
        },
        "event": "fileCreated",
        "options": {}
      },
      "id": "6b9dee98-2871-57a7-ac7e-323bab4e209a",
      "name": "Google Drive Trigger",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "typeVersion": 1,
      "position": [
        0,
        300
      ],
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "",
          "name": "Google Drive account"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "828b69e5-fbf4-5bc0-8758-cf685ab2b35a",
              "name": "apiKey",
              "value": "PASTE_YOUR_JSONPAGE_API_KEY",
              "type": "string"
            },
            {
              "id": "64ce9908-c5f7-58f1-8936-09ef0522c598",
              "name": "baseUrl",
              "value": "https://api.jsonpage.com/v2",
              "type": "string"
            },
            {
              "id": "2baf4bb5-2298-51ed-a3c1-cc807a20c64e",
              "name": "defaultCountry",
              "value": "FR",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {
          "includeBinary": true
        }
      },
      "id": "ebcc5125-baff-579a-926a-4056654b4235",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        300
      ],
      "notes": "Edit the values of this node only: nothing else needs to change.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "resource": "file",
        "operation": "download",
        "fileId": {
          "__rl": true,
          "value": "={{ $json.id }}",
          "mode": "id"
        },
        "options": {}
      },
      "id": "dcaef2a9-6df1-53a9-95d3-077f87a0611c",
      "name": "Download the file",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        440,
        300
      ],
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "",
          "name": "Google Drive account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Reads the dropped CSV file and cuts it into batches of 100 identifiers (the limit of the batch route).\n// The file needs one column named siren, siret, vat, tva or id (SIREN, SIRET, VAT number or identifier like FR-552032534).\n\nfunction parseCsv(text) {\n  text = text.replace(/^﻿/, '');\n  const first = text.split(/\\r?\\n/)[0] || '';\n  const counts = { ';': 0, ',': 0, '\\t': 0 };\n  let quoted = false;\n  for (const ch of first) {\n    if (ch === '\"') quoted = !quoted;\n    else if (!quoted && ch in counts) counts[ch]++;\n  }\n  const delimiter = Object.keys(counts).sort((a, b) => counts[b] - counts[a])[0];\n  const rows = [];\n  let row = [], cell = '', inQuotes = false;\n  for (let i = 0; i < text.length; i++) {\n    const ch = text[i];\n    if (inQuotes) {\n      if (ch === '\"' && text[i + 1] === '\"') { cell += '\"'; i++; }\n      else if (ch === '\"') inQuotes = false;\n      else cell += ch;\n    } else if (ch === '\"') inQuotes = true;\n    else if (ch === delimiter) { row.push(cell); cell = ''; }\n    else if (ch === '\\n' || ch === '\\r') {\n      if (ch === '\\r' && text[i + 1] === '\\n') i++;\n      row.push(cell); cell = '';\n      if (row.some((c) => c !== '')) rows.push(row);\n      row = [];\n    } else cell += ch;\n  }\n  row.push(cell);\n  if (row.some((c) => c !== '')) rows.push(row);\n  return rows;\n}\n\nconst cfg = $('Config').first().json;\nconst name = String($('Google Drive Trigger').first().json.name || 'file.csv');\n// The result file lands in the same folder: never process our own output again.\nif (!/\\.csv$/i.test(name) || /-verified\\.csv$/i.test(name)) return [];\n\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst rows = parseCsv(buffer.toString('utf8'));\nif (rows.length < 2) return [];\nconst headers = rows[0].map((h) => h.trim().toLowerCase());\nconst wanted = ['id', 'siren', 'siret', 'vat', 'tva'];\nconst col = wanted.map((w) => headers.indexOf(w)).find((i) => i >= 0);\nif (col === undefined) throw new Error('No column named id, siren, siret, vat or tva in ' + name);\nconst country = (cfg.defaultCountry || 'FR').toUpperCase();\n\nfunction toId(value) {\n  const raw = String(value || '').trim();\n  if (/^[A-Za-z]{2}-[0-9A-Za-z]+$/.test(raw)) return raw.toUpperCase();\n  const compact = raw.toUpperCase().replace(/[\\s.\\-]/g, '');\n  const vat = /^FR[0-9A-Z]{2}([0-9]{9})$/.exec(compact);\n  if (vat) return 'FR-' + vat[1];\n  if (/^[0-9]{14}$/.test(compact)) return country + '-' + compact.slice(0, 9);\n  if (/^[0-9]{9}$/.test(compact)) return country + '-' + compact;\n  return '';\n}\n\nconst data = rows.slice(1).map((r) => ({ input: r[col] ?? '', id: toId(r[col]) }));\nconst valid = data.map((d, i) => (d.id ? i : -1)).filter((i) => i >= 0);\nconst out = [];\nfor (let start = 0; start < valid.length; start += 100) {\n  const slice = valid.slice(start, start + 100);\n  out.push({ json: { ids: slice.map((i) => data[i].id), positions: slice, baseName: name.replace(/\\.csv$/i, ''), ...(start === 0 ? { rows: data } : {}) } });\n}\nif (out.length === 0) throw new Error('No valid identifier in ' + name);\nreturn out;"
      },
      "id": "ae7aa837-cf56-5db4-8ad5-a186e07b3810",
      "name": "Read the file",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').first().json.baseUrl }}/companies/batch",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-API-Key",
              "value": "={{ $('Config').first().json.apiKey }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ ids: $json.ids, include: ['address', 'vat'] }) }}",
        "options": {}
      },
      "id": "f456bef2-a763-5a6b-99a3-73de543a99e8",
      "name": "Verify and enrich (batch)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        880,
        300
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Joins the answers of the batch route to the rows of the file and writes the result CSV (UTF-8, semicolons).\nconst batches = $('Read the file').all();\nconst rows = batches[0].json.rows;\nconst results = rows.map((r) => ({ input: r.input, id: r.id, found: 'no', error: r.id ? '' : 'invalid_identifier' }));\n$input.all().forEach((answer, k) => {\n  const positions = batches[k].json.positions;\n  (answer.json.data || []).forEach((item, j) => {\n    const target = results[positions[j]];\n    if (item.data) {\n      const c = item.data, a = c.address || {}, v = c.vat || {};\n      Object.assign(target, {\n        found: 'yes', id: c.id, name: c.name, status: c.status, legal_form: c.legal_form ? c.legal_form.label : '', nace: c.activity ? c.activity.nace : '', activity_code: c.activity ? c.activity.code : '',\n        address: a.line || '', postal_code: a.postal_code || '', city: a.city || '', country: c.country, vat_number: v.number || '', vat_status: v.status || '', error: '' });\n    } else {\n      target.error = item.error ? item.error.code : 'not_found';\n    }\n  });\n});\nconst columns = ['input', 'id', 'found', 'name', 'status', 'legal_form', 'activity_code', 'nace', 'address', 'postal_code', 'city', 'country', 'vat_number', 'vat_status', 'error'];\nconst cell = (v) => { const s = String(v ?? ''); return /[\";\\r\\n]/.test(s) ? '\"' + s.replace(/\"/g, '\"\"') + '\"' : s; };\nconst csv = '﻿' + [columns.join(';'), ...results.map((r) => columns.map((c) => cell(r[c])).join(';'))].join('\\r\\n') + '\\r\\n';\nconst fileName = batches[0].json.baseName + '-verified.csv';\nreturn [{ json: { fileName, total: results.length, found: results.filter((r) => r.found === 'yes').length },\n          binary: { data: await this.helpers.prepareBinaryData(Buffer.from(csv, 'utf8'), fileName, 'text/csv') } }];"
      },
      "id": "e177e219-c197-5df7-ae5b-38056eb50d90",
      "name": "Build the result file",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        300
      ]
    },
    {
      "parameters": {
        "resource": "file",
        "operation": "upload",
        "inputDataFieldName": "data",
        "name": "={{ $json.fileName }}",
        "driveId": {
          "__rl": true,
          "value": "My Drive",
          "mode": "list",
          "cachedResultName": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "value": "={{ $('Google Drive Trigger').item.json.parents[0] }}",
          "mode": "id"
        },
        "options": {}
      },
      "id": "dab1718c-8b63-50d3-a248-99b16d1f2e23",
      "name": "Save next to the original",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        1320,
        300
      ],
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "",
          "name": "Google Drive account"
        }
      }
    }
  ],
  "connections": {
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Download the file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download the file": {
      "main": [
        [
          {
            "node": "Read the file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the file": {
      "main": [
        [
          {
            "node": "Verify and enrich (batch)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify and enrich (batch)": {
      "main": [
        [
          {
            "node": "Build the result file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build the result file": {
      "main": [
        [
          {
            "node": "Save next to the original",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
