{
  "name": "FLUF + Airtable \u2014 sales, consignment and packing",
  "nodes": [
    {
      "id": "3286ee74-28e7-5aa8-b602-2049c615ff78",
      "name": "Setup guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -400
      ],
      "parameters": {
        "content": "## Before enabling\nRead https://fluf.io/support/connect-airtable/\n1. Set table IDs in Configure workspace.\n2. Select your Airtable PAT credential on Airtable requests.\n3. For FLUF HTTP requests use Header Auth: Authorization = Bearer YOUR_FLUF_PAT.\n4. Select FLUF credentials on each trigger.\n5. Choose n8n as workflow owner for this base.\n6. Publishing requires allowPublish=true AND Approved for publishing on the row.\nTest with zero-stock drafts first. Use the same currency as your FLUF draft currency.",
        "height": 360,
        "width": 440
      }
    },
    {
      "id": "719b8a9c-0f8c-551c-ada6-2ad0867cfa43",
      "name": "New sale",
      "type": "n8n-nodes-fluf-connect.flufConnectTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {
        "event": "new_sale"
      },
      "webhookId": "eb0bf344-babc-52ce-a466-56012168aa2a"
    },
    {
      "id": "8a4d53f4-dff9-5df9-8ac3-212e52f75dbe",
      "name": "Configure workspace",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        300,
        0
      ],
      "parameters": {
        "jsCode": "// Choose a base and its four FLUF tables. Use standard field names from the guide.\nconst config = { baseId: 'REPLACE_BASE_ID', inventory: 'REPLACE_INVENTORY_TABLE_ID', sales: 'REPLACE_SALES_TABLE_ID', tasks: 'REPLACE_TASKS_TABLE_ID', allowPublish: false };\nif (!/^app[A-Za-z0-9]+$/.test(config.baseId)) throw new Error('Set your Airtable base ID in Configure workspace.');\nfor (const key of ['inventory','sales','tasks']) if (!/^tbl[A-Za-z0-9]+$/.test(config[key])) throw new Error('Set the '+key+' table ID.');\nreturn $input.all().map((item,i)=>({json:{...item.json,config},pairedItem:{item:i}}));"
      }
    },
    {
      "id": "95fd987f-4adf-5b29-b4fa-c5d0540a229b",
      "name": "Prepare sale lines",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().flatMap((item,i)=>(item.json.items||[]).map((line,index)=>({json:{event:item.json,line,index,config:item.json.config},pairedItem:{item:i}})));"
      }
    },
    {
      "id": "27db9813-8fb9-50c5-8d25-404178494266",
      "name": "Request Read item costs",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1140,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.airtable.com/v0/'+$json.config.baseId+'/'+$json.config.inventory+'?maxRecords=2&filterByFormula='+encodeURIComponent('{FLUF ID}=\"'+Number($json.line.fluf_id||0)+'\"') }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 350
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        },
        "nodeCredentialType": "airtableTokenApi"
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "df94ce56-7fe2-5f48-877c-6d6148e6c5e6",
      "name": "Prepare sale and packing writes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1940,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().flatMap((item,i)=>{\n const ctx=$('Prepare sale lines').itemMatching(i).json, {event:e,line:l,index,config:c}=ctx;\n if ((item.json.records||[]).length>1) throw new Error('Duplicate FLUF IDs in inventory. Merge the duplicate rows before retrying.');\n const p=item.json.records?.[0]?.fields||{}, key=[e.channel,e.connection_id||'',e.order_id,index].join(':');\n const quantity=Number(l.quantity??1), price=l.price==null?null:Number(l.price);\n const fields={'Sale key':key,'Order ID':String(e.order_id),'FLUF ID':l.fluf_id?String(l.fluf_id):'','SKU':l.sku||'','Title':l.title||'','Marketplace':e.channel||'','Store':String(e.connection_id||''),'Sold at':e.sold_at||'','Currency':e.currency||'','Quantity':quantity,'Unit price':price,'Revenue':price===null?null:Math.round(price*quantity*100)/100,'Consignor':p.Consignor||''};\n if(p.Cost!=null && p.Currency===e.currency) fields.Cost=Number(p.Cost)*quantity;\n if(p['Commission percent']!=null) fields['Commission percent']=Number(p['Commission percent']);\n const task={'Task key':'pack:'+key,'FLUF ID':fields['FLUF ID'],'SKU':fields.SKU,'Type':'Packing','Marketplace':fields.Marketplace,'Details':'Pack order '+e.order_id,'Created at':fields['Sold at']};\n return [{table:c.sales,key:'Sale key',fields},{table:c.tasks,key:'Task key',fields:task}].map(w=>({json:{...w,config:c},pairedItem:{item:i}}));\n});"
      }
    },
    {
      "id": "913d2e3f-1d4d-5cd5-b213-d873b929cfe9",
      "name": "Request Write sales and packing tasks",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2480,
        0
      ],
      "parameters": {
        "method": "PATCH",
        "url": "={{ 'https://api.airtable.com/v0/'+$json.config.baseId+'/'+$json.table }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 350
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        },
        "nodeCredentialType": "airtableTokenApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {performUpsert:{fieldsToMergeOn:[$json.key]},records:[{fields:$json.fields}]} }}"
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "adbef4b7-ca0c-5172-9932-a13abc461815",
      "name": "Attempt Read item costs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>({json:{...item.json,__airtableAttempt:Number(item.json.__airtableAttempt||0)+1},pairedItem:{item:i}}));"
      }
    },
    {
      "id": "e185cc74-0bf5-5007-bf26-2e21bd2992bb",
      "name": "Read item costs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1380,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>{\n const before=$(\"Attempt Read item costs\").itemMatching(i).json, response=item.json;\n const status=Number(response.statusCode||0);\n if(status>=200 && status<300) return {json:{...response.body,__airtableRetry:false},pairedItem:{item:i}};\n if((status===0||status===429||status>=500) && before.__airtableAttempt<3) return {json:{...before,__airtableRetry:true},pairedItem:{item:i}};\n const error=response.body?.message||response.body?.error?.message||'The request failed. Check the connection, field mapping and service allowance.';\n if(!false) throw new Error(String(error));\n return {json:{error:String(error),__airtableRetry:false},pairedItem:{item:i}};\n});"
      }
    },
    {
      "id": "0cc5d435-12ec-5cd8-9bca-71dc971c3dfe",
      "name": "Retry Read item costs",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1620,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "retry",
              "leftValue": "={{ $json.__airtableRetry }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "fd66f0af-ff4c-55f4-a977-56396a608292",
      "name": "Wait for Read item costs",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1620,
        200
      ],
      "parameters": {
        "amount": 35,
        "unit": "seconds"
      },
      "webhookId": "dc87dfce-cdc2-5208-96e2-6638f475a258"
    },
    {
      "id": "38d897dd-7579-5a2e-a597-fe00dbd41b02",
      "name": "Attempt Write sales and packing tasks",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>({json:{...item.json,__airtableAttempt:Number(item.json.__airtableAttempt||0)+1},pairedItem:{item:i}}));"
      }
    },
    {
      "id": "1b2ae6b7-1622-5589-9377-9baabd16f0e4",
      "name": "Write sales and packing tasks",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2720,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>{\n const before=$(\"Attempt Write sales and packing tasks\").itemMatching(i).json, response=item.json;\n const status=Number(response.statusCode||0);\n if(status>=200 && status<300) return {json:{...response.body,__airtableRetry:false},pairedItem:{item:i}};\n if((status===0||status===429||status>=500) && before.__airtableAttempt<3) return {json:{...before,__airtableRetry:true},pairedItem:{item:i}};\n const error=response.body?.message||response.body?.error?.message||'The request failed. Check the connection, field mapping and service allowance.';\n if(!false) throw new Error(String(error));\n return {json:{error:String(error),__airtableRetry:false},pairedItem:{item:i}};\n});"
      }
    },
    {
      "id": "ec215a2f-8596-58fe-ac74-fee3a5e6aac3",
      "name": "Retry Write sales and packing tasks",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2960,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "retry",
              "leftValue": "={{ $json.__airtableRetry }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "d7a79e8e-fb82-508f-968b-665d21d7b2aa",
      "name": "Wait for Write sales and packing tasks",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        2960,
        200
      ],
      "parameters": {
        "amount": 35,
        "unit": "seconds"
      },
      "webhookId": "afb5a3f8-3bb1-5bc3-bc63-be021a50b665"
    }
  ],
  "connections": {
    "New sale": {
      "main": [
        [
          {
            "node": "Configure workspace",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure workspace": {
      "main": [
        [
          {
            "node": "Prepare sale lines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare sale lines": {
      "main": [
        [
          {
            "node": "Attempt Read item costs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare sale and packing writes": {
      "main": [
        [
          {
            "node": "Attempt Write sales and packing tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attempt Read item costs": {
      "main": [
        [
          {
            "node": "Request Read item costs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Read item costs": {
      "main": [
        [
          {
            "node": "Read item costs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read item costs": {
      "main": [
        [
          {
            "node": "Retry Read item costs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Read item costs": {
      "main": [
        [
          {
            "node": "Attempt Read item costs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attempt Write sales and packing tasks": {
      "main": [
        [
          {
            "node": "Request Write sales and packing tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Write sales and packing tasks": {
      "main": [
        [
          {
            "node": "Write sales and packing tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write sales and packing tasks": {
      "main": [
        [
          {
            "node": "Retry Write sales and packing tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Write sales and packing tasks": {
      "main": [
        [
          {
            "node": "Attempt Write sales and packing tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retry Read item costs": {
      "main": [
        [
          {
            "node": "Wait for Read item costs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare sale and packing writes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retry Write sales and packing tasks": {
      "main": [
        [
          {
            "node": "Wait for Write sales and packing tasks",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 300
  },
  "tags": []
}
