{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "303a50d1-5b14-42e0-9fef-8f50b9cb04df",
      "name": "Split Items to Create",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        4544,
        2032
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "itemsToCreate"
      },
      "typeVersion": 1
    },
    {
      "id": "d4e09bce-aee2-4a4f-bbcf-4fe7d8c96f11",
      "name": "Create Items",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4768,
        2032
      ],
      "parameters": {
        "url": "https://sandbox-quickbooks.api.intuit.com/v3/company/{companyId}/item?minorversion=75",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"Name\": \"{{ $json.name }}\",\n  \"Description\": \"{{ $json.description }}\",\n  \"Type\": \"{{ $json.type }}\",\n  \"IncomeAccountRef\": {\n    \"value\": \"{{ $json.incomeAccountRef }}\"\n  },\n  \"ExpenseAccountRef\": {\n    \"value\": \"{{ $json.expenseAccountRef }}\"\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "quickBooksOAuth2Api"
      },
      "typeVersion": 4.3
    },
    {
      "id": "2805b745-5725-460f-be2f-920bf4d444f4",
      "name": "Merge Item Creation Paths",
      "type": "n8n-nodes-base.merge",
      "position": [
        4992,
        2104
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3,
      "alwaysOutputData": true
    },
    {
      "id": "b7827159-538e-464a-870f-fcc0af7afd67",
      "name": "Collect All Item Mappings",
      "type": "n8n-nodes-base.code",
      "position": [
        5216,
        2104
      ],
      "parameters": {
        "jsCode": "// Collect all item mappings from both existing and newly created items\nconst checkResult = $('Check Which Items to Create').first().json;\nconst itemMapping = { ...checkResult.existingItemMapping };\n\n// Add newly created items to mapping\ntry {\n  const createdItems = $('Create Items').all();\n  createdItems.forEach(node => {\n    const item = node.json;\n    // QuickBooks returns the item in an 'Item' wrapper\n    const itemData = item.Item || item;\n    if (itemData.Name && itemData.Id) {\n      itemMapping[itemData.Name] = itemData.Id;\n    }\n  });\n} catch (error) {\n  // No items were created, that's fine\n}\n\n// Get the extracted data\nconst extractedData = $('Prepare Items to Check').first().json.extractedData;\n\nreturn [{\n  json: {\n    extractedData: extractedData,\n    allItemMapping: itemMapping\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "965884c8-9a44-4e8a-9603-501d43d34b2f",
      "name": "Find Vendor",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        5440,
        2104
      ],
      "parameters": {
        "filters": {
          "query": "=WHERE DisplayName = '{{ $json.extractedData.vendor.name }}'"
        },
        "resource": "vendor",
        "operation": "getAll"
      },
      "typeVersion": 1
    },
    {
      "id": "a6548ed0-e56f-4c74-b9ea-d391122c855a",
      "name": "Vendor Exists?",
      "type": "n8n-nodes-base.if",
      "position": [
        5664,
        2104
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1",
              "operator": {
                "type": "string",
                "operation": "empty"
              },
              "leftValue": "={{ $json.Id }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "7eb10846-d37c-4b5b-a11e-d4392e2b77c2",
      "name": "Create Vendor",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        5904,
        2032
      ],
      "parameters": {
        "resource": "vendor",
        "operation": "create",
        "displayName": "={{ $('Collect All Item Mappings').first().json.extractedData.vendor.name }}",
        "additionalFields": {
          "CompanyName": "={{ $('Collect All Item Mappings').first().json.extractedData.vendor.name }}",
          "PrimaryEmailAddr": "={{ $('Collect All Item Mappings').first().json.extractedData.vendor.email }}"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "e96e6a97-14b1-4269-a7cc-d3130a1a8a63",
      "name": "Create Bill ",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        6368,
        2144
      ],
      "parameters": {
        "url": "https://sandbox-quickbooks.api.intuit.com/v3/company/{companyId}/bill?minorversion=75",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.billPayload) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "quickBooksOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "49f364de-8174-4e01-bf7b-d2be3286818a",
      "name": "Need to Create Items?",
      "type": "n8n-nodes-base.if",
      "position": [
        4320,
        2096
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.needsCreation }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "6c671cc9-6009-484a-ae8a-67f2a7e90324",
      "name": "Get All QB Items",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        3872,
        2096
      ],
      "parameters": {
        "filters": {},
        "resource": "item",
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 1
    },
    {
      "id": "84340514-dab8-4a14-b9ba-edc76ab396fd",
      "name": "Prepare Items to Check",
      "type": "n8n-nodes-base.code",
      "position": [
        3680,
        2096
      ],
      "parameters": {
        "jsCode": "const extractedData = $input.first().json.output;\nconst lineItems = extractedData.line_items || [];\n\nconst itemsToCheck = lineItems\n  .filter(item => item.item_name && item.item_name.trim() !== '')\n  .map(item => ({\n    name: item.item_name,\n    description: item.description || item.item_name,\n    unitPrice: parseFloat(item.unit_price) || parseFloat(item.amount),\n    type: 'Service'\n  }));\n\nreturn [{\n  json: {\n    extractedData: extractedData,\n    itemsToCheck: itemsToCheck\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ed2369bb-2c7f-461f-aeb5-eed9391e4402",
      "name": "Extract Invoice Data",
      "type": "@n8n/n8n-nodes-langchain.informationExtractor",
      "position": [
        3296,
        2104
      ],
      "parameters": {
        "text": "={{ $json.cleaned_text }}",
        "options": {
          "systemPromptTemplate": "Extract invoice data accurately. Extract vendor info, line items with item names, quantities, unit prices, and amounts. Return numbers as numbers, not strings."
        },
        "schemaType": "fromJson",
        "jsonSchemaExample": "{\n  \"invoice_number\": \"\",\n  \"invoice_date\": \"\",\n  \"due_date\": \"\",\n  \"vendor\": {\n    \"name\": \"\",\n    \"address\": \"\",\n    \"email\": \"\",\n    \"phone\": \"\"\n  },\n  \"line_items\": [\n    {\n      \"item_name\": \"\",\n      \"description\": \"\",\n      \"quantity\": 0,\n      \"unit_price\": 0,\n      \"amount\": 0\n    }\n  ],\n  \"subtotal\": 0,\n  \"tax\": 0,\n  \"total\": 0\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "ecf48035-31bc-43b8-8a2a-f4de560f134b",
      "name": "Clean Text",
      "type": "n8n-nodes-base.code",
      "position": [
        3056,
        2096
      ],
      "parameters": {
        "jsCode": "const text = $json[\"text\"] || '';\nlet data = '';\n\nif (Object.keys($binary || {}).length) {\n  data = $binary.data ? Buffer.from($binary.data.data).toString('utf8') : '';\n}\n\nconst cleaned = (text || data || '').replace(/\\r\\n/g,'\\n').replace(/\\n+/g,'\\n').trim();\n\nreturn [{ \n  json: { \n    cleaned_text: cleaned \n  } \n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3ee694dd-3899-4355-8687-d8bd83ad8bf8",
      "name": "Loop Over Invoices",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        2624,
        2228
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "11125910-971f-4829-a010-d98ce0de67d8",
      "name": "Convert to Separate Items",
      "type": "n8n-nodes-base.code",
      "position": [
        2400,
        2228
      ],
      "parameters": {
        "jsCode": "const item = $input.first();\nconst binaries = item.binary || {};\nconst output = [];\n\nfor (const key of Object.keys(binaries)) {\n  output.push({\n    json: { \n      file: key,\n      filename: key,\n      mimeType: binaries[key].mimeType,\n      fileSize: binaries[key].fileSize\n    },\n    binary: {\n      data: binaries[key]\n    }\n  });\n}\n\nreturn output;"
      },
      "typeVersion": 2
    },
    {
      "id": "c123a41e-5799-4f30-876b-addff0693431",
      "name": "Extract from PDF",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        2848,
        2104
      ],
      "parameters": {
        "options": {},
        "operation": "pdf"
      },
      "typeVersion": 1.1
    },
    {
      "id": "5020b30e-b4ba-415d-b506-63db5368419c",
      "name": "Check Which Items to Create",
      "type": "n8n-nodes-base.code",
      "position": [
        4096,
        2096
      ],
      "parameters": {
        "jsCode": "const itemsToCheck = $('Prepare Items to Check').first().json.itemsToCheck;\nconst existingItems = $input.all();\n\nconst existingItemMap = {};\nexistingItems.forEach(itemNode => {\n  const item = itemNode.json;\n  if (item.Name) {\n    existingItemMap[item.Name.toLowerCase()] = {\n      id: item.Id,\n      name: item.Name\n    };\n  }\n});\n\nconst itemsToCreate = [];\nconst itemMapping = {};\n\nitemsToCheck.forEach(item => {\n  const itemNameLower = item.name.toLowerCase();\n  \n  if (existingItemMap[itemNameLower]) {\n    itemMapping[item.name] = existingItemMap[itemNameLower].id;\n  } else {\n    itemsToCreate.push({\n      name: item.name,\n      description: item.description,\n      type: item.type,\n      incomeAccountRef: \"79\",\n      expenseAccountRef: \"80\"\n    });\n  }\n});\n\nreturn [{\n  json: {\n    itemsToCreate: itemsToCreate,\n    existingItemMapping: itemMapping,\n    needsCreation: itemsToCreate.length > 0\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "30ab1e9d-703f-4c78-8495-3399f8e793dd",
      "name": "Build Bill Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        6144,
        2144
      ],
      "parameters": {
        "jsCode": "const extractedData = $('Collect All Item Mappings').first().json.extractedData;\nconst itemMapping = $('Collect All Item Mappings').first().json.allItemMapping;\n\n// Get vendor info\nlet vendorId, vendorName;\n\ntry {\n  const vendorNode = $('Create Vendor').first();\n  vendorId = vendorNode.json.Id;\n  vendorName = vendorNode.json.DisplayName;\n} catch {\n  const vendorNode = $('Find Vendor').first();\n  vendorId = vendorNode.json.Id;\n  vendorName = vendorNode.json.DisplayName;\n}\n\nif (!vendorId) {\n  throw new Error(`Vendor not found: ${extractedData.vendor.name}`);\n}\n\n// Build line items\nconst lineItems = [];\n\nextractedData.line_items.forEach((item, index) => {\n  // Try exact match first, then case-insensitive match\n  let itemId = itemMapping[item.item_name];\n  \n  if (!itemId) {\n    // Try case-insensitive lookup\n    const itemNameLower = item.item_name.toLowerCase();\n    for (const [key, value] of Object.entries(itemMapping)) {\n      if (key.toLowerCase() === itemNameLower) {\n        itemId = value;\n        break;\n      }\n    }\n  }\n  \n  if (!itemId) {\n    throw new Error(`Item not found: ${item.item_name}`);\n  }\n  \n  const quantity = parseFloat(item.quantity) || 1;\n  const unitPrice = parseFloat(item.unit_price) || (parseFloat(item.amount) / quantity);\n  \n  lineItems.push({\n    LineNum: index + 1,\n    Description: item.description || item.item_name,\n    Amount: parseFloat(item.amount),\n    DetailType: \"ItemBasedExpenseLineDetail\",\n    ItemBasedExpenseLineDetail: {\n      ItemRef: {\n        value: itemId,\n        name: item.item_name\n      },\n      Qty: quantity,\n      UnitPrice: unitPrice,\n      BillableStatus: \"NotBillable\",\n      TaxCodeRef: {\n        value: \"NON\"\n      }\n    }\n  });\n});\n\n// Add tax if exists\nif (extractedData.tax && parseFloat(extractedData.tax) > 0) {\n  lineItems.push({\n    LineNum: lineItems.length + 1,\n    Description: 'Tax',\n    Amount: parseFloat(extractedData.tax),\n    DetailType: 'AccountBasedExpenseLineDetail',\n    AccountBasedExpenseLineDetail: {\n      AccountRef: {\n        value: '1'\n      },\n      BillableStatus: 'NotBillable',\n      TaxCodeRef: {\n        value: 'NON'\n      }\n    }\n  });\n}\n\n// Parse dates\nlet billDate = extractedData.invoice_date;\nlet dueDate = extractedData.due_date;\n\nif (billDate && !billDate.match(/^\\d{4}-\\d{2}-\\d{2}$/)) {\n  billDate = new Date(billDate).toISOString().split('T')[0];\n}\n\nif (dueDate && !dueDate.match(/^\\d{4}-\\d{2}-\\d{2}$/)) {\n  dueDate = new Date(dueDate).toISOString().split('T')[0];\n}\n\n// Build bill payload\nconst billPayload = {\n  Line: lineItems,\n  VendorRef: {\n    value: vendorId,\n    name: vendorName\n  },\n  DocNumber: extractedData.invoice_number,\n  TxnDate: billDate,\n  DueDate: dueDate || billDate\n};\n\nif (extractedData.vendor?.address) {\n  const addressLines = extractedData.vendor.address.split('\\n');\n  billPayload.VendorAddr = {\n    Line1: addressLines[0] || '',\n    Line2: addressLines[1] || '',\n    Line3: addressLines[2] || ''\n  };\n}\n\nreturn {\n  json: {\n    billPayload: billPayload,\n    summary: {\n      vendor: vendorName,\n      billNumber: extractedData.invoice_number,\n      total: extractedData.total,\n      lineItems: lineItems.length\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "6058dc16-7403-48c5-ad87-c27f9bde5138",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        3296,
        2400
      ],
      "parameters": {
        "model": "openai/gpt-oss-20b:free",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "dd2fbbf5-ca57-4821-be26-3872991e6f0c",
      "name": "On bill submission",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        2176,
        2228
      ],
      "parameters": {
        "options": {},
        "formTitle": "Invoice Parser + QuickBooks Bill Creator",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "Invoice File",
              "requiredField": true,
              "acceptFileTypes": ".pdf"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "77a10310-6a8c-4eba-a058-15e0af6d8b32",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1696,
        2096
      ],
      "parameters": {
        "width": 420,
        "height": 400,
        "content": "## How it works\n1. **Upload**: Submit PDF invoices via web form\n2. **Extract**: AI reads invoice data (vendor, items, amounts, dates)\n3. **Reconcile**: Checks if items/vendors exist in QuickBooks\n4. **Create**: Auto-creates missing items and vendors\n5. **Bill**: Generates complete bill in QuickBooks with all line items\n\n## Setup steps\n1. Connect QuickBooks OAuth2 credentials\n2. Add OpenRouter API key for AI extraction\n3. Configure income/expense account refs (currently 79/80)\n4. Test with sample invoice PDF\n5. Access form URL to submit invoices"
      },
      "typeVersion": 1
    },
    {
      "id": "d3ec43fd-2232-4e5c-a537-39573a03d725",
      "name": "PDF Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2128,
        2096
      ],
      "parameters": {
        "color": 7,
        "width": 652,
        "height": 380,
        "content": "## PDF Processing\nConverts uploaded PDFs to separate items and loops through each invoice for extraction."
      },
      "typeVersion": 1
    },
    {
      "id": "adde0363-ce66-413b-8448-ebcbadaeb596",
      "name": "AI Extraction",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3216,
        1952
      ],
      "parameters": {
        "color": 7,
        "width": 364,
        "height": 604,
        "content": "## AI Extraction\n\nExtracts structured data from invoice text using OpenRouter LLM (vendor, line items, dates, amounts)."
      },
      "typeVersion": 1
    },
    {
      "id": "82616fc6-1e70-4c65-ac8d-b8b6f0beff8d",
      "name": "Item Management",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3600,
        1952
      ],
      "parameters": {
        "color": 7,
        "width": 1572,
        "height": 396,
        "content": "## Item Management\n\nChecks existing QuickBooks items against invoice line items. Creates missing items automatically with default accounts."
      },
      "typeVersion": 1
    },
    {
      "id": "57243061-86f8-472b-a02a-df79f67370b3",
      "name": "Vendor Management",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5184,
        1952
      ],
      "parameters": {
        "color": 7,
        "width": 884,
        "height": 396,
        "content": "## Vendor Management\n\nSearches for vendor by name. Creates new vendor record if not found, using extracted contact info."
      },
      "typeVersion": 1
    },
    {
      "id": "9ffda906-d1b5-4fc5-98ce-c2311d849e7f",
      "name": "Bill Creation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6080,
        1952
      ],
      "parameters": {
        "color": 7,
        "width": 440,
        "height": 396,
        "content": "## Bill Creation\n\nBuilds complete bill payload with all line items, vendor ref, dates, and tax. Posts to QuickBooks API."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Clean Text": {
      "main": [
        [
          {
            "node": "Extract Invoice Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Vendor": {
      "main": [
        [
          {
            "node": "Vendor Exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Bill ": {
      "main": [
        [
          {
            "node": "Loop Over Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Items": {
      "main": [
        [
          {
            "node": "Merge Item Creation Paths",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Vendor": {
      "main": [
        [
          {
            "node": "Build Bill Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vendor Exists?": {
      "main": [
        [
          {
            "node": "Create Vendor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Bill Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from PDF": {
      "main": [
        [
          {
            "node": "Clean Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All QB Items": {
      "main": [
        [
          {
            "node": "Check Which Items to Create",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Bill Payload": {
      "main": [
        [
          {
            "node": "Create Bill ",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Invoices": {
      "main": [
        [],
        [
          {
            "node": "Extract from PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On bill submission": {
      "main": [
        [
          {
            "node": "Convert to Separate Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Invoice Data": {
      "main": [
        [
          {
            "node": "Prepare Items to Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Need to Create Items?": {
      "main": [
        [
          {
            "node": "Split Items to Create",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge Item Creation Paths",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Extract Invoice Data",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Split Items to Create": {
      "main": [
        [
          {
            "node": "Create Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Items to Check": {
      "main": [
        [
          {
            "node": "Get All QB Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect All Item Mappings": {
      "main": [
        [
          {
            "node": "Find Vendor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to Separate Items": {
      "main": [
        [
          {
            "node": "Loop Over Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Item Creation Paths": {
      "main": [
        [
          {
            "node": "Collect All Item Mappings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Which Items to Create": {
      "main": [
        [
          {
            "node": "Need to Create Items?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}