{
  "name": "ezGPT - XLSX Export Roundtrip",
  "active": false,
  "nodes": [
    {
      "id": "trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        200,
        300
      ],
      "parameters": {}
    },
    {
      "id": "sample",
      "name": "Sample Orders",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [\n  {\n    \"order_id\": \"0001\",\n    \"product\": \"Кофе\",\n    \"quantity\": 2,\n    \"price_rub\": 1499.9\n  },\n  {\n    \"order_id\": \"0002\",\n    \"product\": \"Чай\",\n    \"quantity\": 0,\n    \"price_rub\": 0\n  },\n  {\n    \"order_id\": \"0003\",\n    \"product\": \"Кружка\",\n    \"quantity\": 1,\n    \"price_rub\": 350\n  }\n].map(json => ({ json }));"
      }
    },
    {
      "id": "validate",
      "name": "Check Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        640,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nif (items.length < 1 || items.length > 20) throw new Error('Expected 1-20 rows');\nconst seen = new Set();\nreturn items.map(({ json: row }) => {\n  if (Object.keys(row).sort().join('|') !== 'order_id|price_rub|product|quantity') {\n    throw new Error('Unexpected or missing columns');\n  }\n  if (typeof row.order_id !== 'string' || !/^[0-9]{4}$/.test(row.order_id) || seen.has(row.order_id)) {\n    throw new Error('Expected unique four-digit text ID');\n  }\n  seen.add(row.order_id);\n  if (typeof row.product !== 'string' || !row.product.trim() || row.product.length > 80 || row.product.includes('\\u0000')) {\n    throw new Error('Invalid product');\n  }\n  if (!Number.isSafeInteger(row.quantity) || row.quantity < 0 || row.quantity > 1000000) {\n    throw new Error('Quantity must be a nonnegative integer');\n  }\n  if (typeof row.price_rub !== 'number' || !Number.isFinite(row.price_rub) || row.price_rub < 0 || row.price_rub > 1000000) {\n    throw new Error('Price must be a nonnegative number');\n  }\n  return { json: {\n    order_id: row.order_id, product: row.product,\n    quantity: row.quantity, price_rub: row.price_rub,\n  } };\n});"
      }
    },
    {
      "id": "make-xlsx",
      "name": "Make XLSX",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        860,
        300
      ],
      "parameters": {
        "operation": "xlsx",
        "binaryPropertyName": "export",
        "options": {
          "fileName": "orders-demo.xlsx",
          "sheetName": "Orders",
          "headerRow": true,
          "compression": true
        }
      }
    },
    {
      "id": "check-file",
      "name": "Check File",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1080,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nif (items.length !== 1 || Object.keys(items[0].binary ?? {}).join('|') !== 'export') {\n  throw new Error('Expected one file in binary.export');\n}\nconst item = items[0];\nconst file = item.binary.export;\nif (file.fileName !== 'orders-demo.xlsx' || file.mimeType !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {\n  throw new Error('Expected orders-demo.xlsx');\n}\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'export');\nif (buffer.length < 4 || buffer.length > 100000 || buffer.readUInt32LE(0) !== 0x04034b50) {\n  throw new Error('Expected a small XLSX ZIP package');\n}\nreturn [{ json: {}, binary: { export: file } }];"
      }
    },
    {
      "id": "read-xlsx",
      "name": "Read XLSX",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        1300,
        300
      ],
      "parameters": {
        "operation": "xlsx",
        "binaryPropertyName": "export",
        "options": {
          "sheetName": "Orders",
          "headerRow": true,
          "includeEmptyCells": true
        }
      }
    },
    {
      "id": "verify",
      "name": "Verify Export",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const actual = $input.all().map(item => item.json);\nconst expected = $('Check Rows').all().map(item => item.json);\nconst columns = ['order_id', 'product', 'quantity', 'price_rub'];\nif (actual.length !== expected.length) throw new Error('Row count changed');\nfor (let i = 0; i < expected.length; i++) {\n  if (Object.keys(actual[i]).sort().join('|') !== [...columns].sort().join('|')) {\n    throw new Error('Column names changed');\n  }\n  for (const key of columns) {\n    if (actual[i][key] !== expected[i][key]) {\n      throw new Error('Value or type changed: ' + key + ', row ' + (i + 1));\n    }\n  }\n}\nconst file = $('Check File').first().binary.export;\nreturn [{\n  json: {\n    file_name: 'orders-demo.xlsx', sheet: 'Orders', rows: actual.length,\n    ids: actual.map(row => row.order_id), roundtrip: true, action: false,\n  },\n  binary: { export: file },\n}];"
      }
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Sample Orders",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample Orders": {
      "main": [
        [
          {
            "node": "Check Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Rows": {
      "main": [
        [
          {
            "node": "Make XLSX",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Make XLSX": {
      "main": [
        [
          {
            "node": "Check File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check File": {
      "main": [
        [
          {
            "node": "Read XLSX",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read XLSX": {
      "main": [
        [
          {
            "node": "Verify Export",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}
