{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "a6f5bd1b-9488-4414-afca-e9ed592e52b6",
      "name": "Extract PDF Text",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -3264,
        2456
      ],
      "parameters": {
        "options": {
          "password": "YOUR_CREDENTIAL_HERE"
        },
        "operation": "pdf",
        "binaryPropertyName": "Bank_Statement_PDF"
      },
      "typeVersion": 1.1
    },
    {
      "id": "340e4c28-389a-4efc-af96-86c601cccdf0",
      "name": "Transaction Extractor AI",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "onError": "continueRegularOutput",
      "position": [
        -3040,
        2456
      ],
      "parameters": {
        "text": "=Extract all financial transactions from the provided bank statement text.\n\n{{ $json.text }}\n\nFor EACH transaction, do the following carefully:\n\n1. Determine whether the transaction is:\n   - \"credit\" \u2192 money received / deposited\n   - \"debit\" \u2192 money spent / withdrawn\n\n2. Extract:\n   - Transaction date (convert to YYYY-MM-DD format)\n   - Full transaction description\n   - Transaction amount (always a positive number)\n   - Reference number if present (otherwise return an empty string)\n   - Payee or counterparty name (clean and human-readable)\n\nImportant rules:\n- Do NOT return explanations or extra text.\n- Do NOT wrap the result in an object.\n- Return ONLY a JSON array.\n- Each transaction must strictly follow the defined schema.\n- If any field is missing in the statement, return an empty string (not null).\n\nReturn format example:\n[\n  {\n    \"transactionType\": \"debit\",\n    \"date\": \"YYYY-MM-DD\",\n    \"description\": \"string\",\n    \"amount\": number,\n    \"referenceNumber\": \"string\",\n    \"payee\": \"string\"\n  }\n]",
        "options": {
          "systemMessage": "You are a financial data extraction expert. You specialize in reading bank statements and converting them into structured transaction data. You must be precise with dates, amounts, and credit/debit classification. Output must strictly match the requested JSON schema with no additional text."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "6444a755-5ad6-4019-b0e5-683745b66876",
      "name": "JSON Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        -2864,
        2688
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"array\",\n  \"items\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"transactionType\": {\n        \"type\": \"string\",\n        \"enum\": [\"credit\", \"debit\"]\n      },\n      \"date\": {\n        \"type\": \"string\",\n        \"pattern\": \"^\\\\d{4}-\\\\d{2}-\\\\d{2}$\"\n      },\n      \"description\": {\n        \"type\": \"string\"\n      },\n      \"amount\": {\n        \"type\": \"number\"\n      },\n      \"referenceNumber\": {\n        \"type\": \"string\"\n      },\n      \"payee\": {\n        \"type\": \"string\"\n      }\n    },\n    \"required\": [\n      \"transactionType\",\n      \"date\",\n      \"description\",\n      \"amount\",\n      \"referenceNumber\",\n      \"payee\"\n    ]\n  }\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "b0ab778f-6d40-4311-8f4d-421c13662097",
      "name": "Create QuickBooks SalesReceipt",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -208,
        2368
      ],
      "parameters": {
        "url": "https://sandbox-quickbooks.api.intuit.com/v3/company/company_id/salesreceipt?minorversion=75",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "quickBooksOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "ae994181-acf4-427a-93e3-f93a15901e9b",
      "name": "Bank Statement Form",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -3488,
        2456
      ],
      "parameters": {
        "options": {
          "appendAttribution": false
        },
        "formTitle": "QuickBooks Bank Statement Upload",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "Bank Statement PDF",
              "requiredField": true,
              "acceptFileTypes": ".pdf"
            }
          ]
        },
        "formDescription": "Upload your bank statement PDF to automatically create entries in QuickBooks"
      },
      "typeVersion": 2.3
    },
    {
      "id": "52ea829a-a625-4793-ab63-b40c41520f84",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        -3040,
        2688
      ],
      "parameters": {
        "model": "openai/gpt-oss-20b:free",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "af43d4d4-9ca9-44d3-96d6-0a1c63eb7e67",
      "name": "Split Transactions",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -2688,
        2456
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "output"
      },
      "typeVersion": 1
    },
    {
      "id": "dee42064-962d-48bb-a33d-09a2fc48c455",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -2464,
        2456
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "b41ef097-b891-4704-8684-470f43d44ee9",
      "name": "Build Salesreceipt Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        -416,
        2368
      ],
      "parameters": {
        "jsCode": "// // --------------------------------\n// // 1. Get transaction correctly\n// // --------------------------------\n// const txn = $('Loop Over Items').first().json;\n\n// // --------------------------------\n// // 2. Get resolved itemRef\n// // --------------------------------\n// const itemRef = $('Collect All Item Mappings').first().json.itemRef;\n\n// // --------------------------------\n// // 3. Hard validation\n// // --------------------------------\n// if (!txn.amount) {\n//   throw new Error('Transaction amount missing');\n// }\n\n// if (!itemRef?.value) {\n//   throw new Error('ItemRef not resolved');\n// }\n\n// const amount = Number(txn.amount);\n\n// if (isNaN(amount)) {\n//   throw new Error('Amount is not a number');\n// }\n\n// // --------------------------------\n// // 4. Build SalesReceipt payload\n// // --------------------------------\n// const payload = {\n//   Line: [\n//     {\n//       LineNum: 1,\n//       Amount: amount,\n//       Description: txn.description || '',\n//       DetailType: 'SalesItemLineDetail',\n//       SalesItemLineDetail: {\n//         ItemRef: {\n//           value: itemRef.value,\n//           name: itemRef.name\n//         },\n//         Qty: 1,\n//         UnitPrice: amount,\n//         TaxCodeRef: {\n//           value: 'NON'\n//         }\n//       }\n//     }\n//   ],\n//   TxnDate: txn.date,\n//   PrivateNote: `${txn.description} - Ref: ${txn.referenceNumber || 'N/A'}`,\n//   CustomerRef: {\n//     value: '1'\n//   },\n//   DepositToAccountRef: {\n//     value: '35'\n//   },\n//   TotalAmt: amount\n// };\n\n// return { json: payload };\n\n\n\n// --------------------------------\n// 1. Transaction\n// --------------------------------\nconst txn = $('Loop Over Items').first().json;\n\n// --------------------------------\n// 2. ItemRef\n// --------------------------------\nconst itemRef = $('Collect All Item Mappings').first().json.itemRef;\n\n// --------------------------------\n// 3. Resolve CustomerRef DIRECTLY\n// --------------------------------\nlet customer;\n\n// Try existing customer first\ntry {\n  customer = $('Get many customers').first().json;\n} catch (_) {}\n\n// If not found, use newly created customer\nif (!customer?.Id) {\n  customer = $('Create a Customer').first().json;\n}\n\nif (!customer?.Id) {\n  throw new Error('Customer not resolved');\n}\n\nconst customerRef = {\n  value: customer.Id,\n  name: customer.DisplayName\n};\n\n// --------------------------------\n// 4. Validation\n// --------------------------------\nconst amount = Number(txn.amount);\n\nif (!amount || isNaN(amount)) {\n  throw new Error('Invalid transaction amount');\n}\nif (!itemRef?.value) {\n  throw new Error('ItemRef missing');\n}\n\n// --------------------------------\n// 5. SalesReceipt Payload\n// --------------------------------\nconst payload = {\n  Line: [\n    {\n      LineNum: 1,\n      Amount: amount,\n      Description: txn.description || '',\n      DetailType: 'SalesItemLineDetail',\n      SalesItemLineDetail: {\n        ItemRef: {\n          value: itemRef.value,\n          name: itemRef.name\n        },\n        Qty: 1,\n        UnitPrice: amount,\n        TaxCodeRef: { value: 'NON' }\n      }\n    }\n  ],\n  TxnDate: txn.date,\n  PrivateNote: `${txn.description} - Ref: ${txn.referenceNumber || 'N/A'}`,\n  CustomerRef: customerRef,\n  DepositToAccountRef: { value: '35' },\n  TotalAmt: amount\n};\n\nreturn { json: payload };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "5733b401-f55c-4062-8717-35befaeddee3",
      "name": "Get many customers",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        -976,
        2336
      ],
      "parameters": {
        "filters": {
          "query": "=WHERE DisplayName = '{{ $json.itemRef.name }}'"
        },
        "operation": "getAll"
      },
      "typeVersion": 1
    },
    {
      "id": "cf1debb7-ccf5-4d31-96e8-593ac5592260",
      "name": "Create Vendor",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        -1280,
        2672
      ],
      "parameters": {
        "resource": "vendor",
        "operation": "create",
        "displayName": "={{ $('Loop Over Items').item.json.payee }}",
        "additionalFields": {
          "CompanyName": "={{ $('Loop Over Items').item.json.payee }}"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2b8e13cf-c84d-4d03-950f-1a2819cb435a",
      "name": "Vendor Exists?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1504,
        2752
      ],
      "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": "47ab4b59-e851-4956-987a-1607cee94918",
      "name": "Find Vendor",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        -1728,
        2752
      ],
      "parameters": {
        "filters": {
          "query": "=WHERE DisplayName = '{{ $('Loop Over Items').item.json.payee }}'"
        },
        "resource": "vendor",
        "operation": "getAll"
      },
      "typeVersion": 1
    },
    {
      "id": "9411680b-8cd9-4c75-b8c4-9011caff2531",
      "name": "Search Categories",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1952,
        2752
      ],
      "parameters": {
        "url": "https://sandbox-quickbooks.api.intuit.com/v3/company/company_id/query?minorversion=75",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "SELECT Id, Name, AccountType, AccountSubType FROM Account"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "quickBooksOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "789c680c-6b68-4d79-8656-4c145c188a08",
      "name": "Create Items",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1360,
        2272
      ],
      "parameters": {
        "url": "https://sandbox-quickbooks.api.intuit.com/v3/company/company_id/item?minorversion=75",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"Name\": \"{{ $json.itemsToCreate[0].name }}\",\n  \"Description\": \"{{ $json.itemsToCreate[0].description }}\",\n  \"Type\": \"{{ $json.itemsToCreate[0].type }}\",\n  \"IncomeAccountRef\": {\n    \"value\": \"{{ $json.itemsToCreate[0].incomeAccountRef }}\"\n  },\n  \"ExpenseAccountRef\": {\n    \"value\": \"{{ $json.itemsToCreate[0].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": "eeb77754-2940-483b-8d6d-31247eff0196",
      "name": "Collect All Item Mappings",
      "type": "n8n-nodes-base.code",
      "position": [
        -1168,
        2336
      ],
      "parameters": {
        "jsCode": "// // --------------------------------\n// // 1. From previous decision node\n// // --------------------------------\n// const decision = $('Check Which Items to Create').first().json;\n\n// // --------------------------------\n// // 2. If item was created\n// // --------------------------------\n// if (decision.needsCreation) {\n//   const created = $('Create Items').first().json;\n//   const item = created.Item || created;\n\n//   return [{\n//     json: {\n//       itemRef: {\n//         value: item.Id,\n//         name: item.Name\n//       }\n//     }\n//   }];\n// }\n\n// // --------------------------------\n// // 3. If existing item was found\n// // --------------------------------\n// const key = decision.desiredItemName;\n// const existing = decision.existingItemMapping[key];\n\n// return [{\n//   json: {\n//     itemRef: {\n//       value: existing.id,\n//       name: existing.name\n//     }\n//   }\n// }];\n\n\n\n// --------------------------------\n// 1. Decision\n// --------------------------------\nconst decision = $('Check Which Items to Create').first().json;\n\n// --------------------------------\n// 2. If item was newly created\n// --------------------------------\nif (decision.needsCreation) {\n  const created = $('Create Items').first().json;\n  const item = created.Item || created;\n\n  return [{\n    json: {\n      itemRef: {\n        value: item.Id,\n        name: item.Name\n      }\n    }\n  }];\n}\n\n// --------------------------------\n// 3. If existing item was found\n// --------------------------------\nconst key = decision.desiredItemName;\nconst existing = decision.existingItemMapping[key];\n\n// \ud83d\udd10 SAFETY CHECK\nif (!existing) {\n  throw new Error(`Item mapping missing for: ${key}`);\n}\n\nreturn [{\n  json: {\n    itemRef: {\n      value: existing.id,\n      name: existing.name\n    }\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2b87de0a-cca5-46b4-82ee-a001b356af29",
      "name": "Need to Create Items?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1584,
        2320
      ],
      "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": "fdce277b-d161-4117-88e6-36cdabd6ae09",
      "name": "Get All QB Items",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        -1968,
        2320
      ],
      "parameters": {
        "filters": {},
        "resource": "item",
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 1
    },
    {
      "id": "96c9f261-ee74-4a10-8214-59ec302eaa72",
      "name": "Check Which Items to Create",
      "type": "n8n-nodes-base.code",
      "position": [
        -1776,
        2320
      ],
      "parameters": {
        "jsCode": "// // --------------------------------\n// // 1. Inputs\n// // --------------------------------\n// const txn = $('Loop Over Items').item.json;\n// const qbItems = $input.all(); // from \"Get All QB Items\"\n\n// // --------------------------------\n// // 2. Normalize name logic\n// // --------------------------------\n// const normalize = (s) =>\n//   s.toLowerCase().replace(/[^a-z0-9]/g, ' ').trim();\n\n// // Item name derived from transaction\n// const desiredItemName = normalize(txn.payee || txn.description);\n\n// // --------------------------------\n// // 3. Build existing item lookup\n// // --------------------------------\n// const existingMap = {};\n\n// qbItems.forEach(n => {\n//   if (n.json?.Name && n.json?.Id) {\n//     existingMap[normalize(n.json.Name)] = {\n//       id: n.json.Id,\n//       name: n.json.Name\n//     };\n//   }\n// });\n\n// // --------------------------------\n// // 4. Decide create or reuse\n// // --------------------------------\n// const itemsToCreate = [];\n// const existingItemMapping = {};\n\n// if (existingMap[desiredItemName]) {\n//   existingItemMapping[desiredItemName] = existingMap[desiredItemName];\n// } else {\n//   itemsToCreate.push({\n//     name: txn.payee || 'Service',\n//     description: txn.description,\n//     type: 'Service',\n//     incomeAccountRef: '79',   // Income account\n//     expenseAccountRef: '80'   // Expense account\n//   });\n// }\n\n// return [{\n//   json: {\n//     desiredItemName,\n//     itemsToCreate,\n//     existingItemMapping,\n//     needsCreation: itemsToCreate.length > 0\n//   }\n// }];\n\n\n\n// --------------------------------\n// 1. Inputs\n// --------------------------------\nconst txn = $('Loop Over Items').first().json;\nconst qbItems = $input.all();\n\n// --------------------------------\n// 2. Utility\n// --------------------------------\nconst normalize = s =>\n  s.toLowerCase().replace(/[^a-z0-9]/g, ' ').trim();\n\nconst shortenName = (name) => {\n  if (!name) return 'Service';\n  return name\n    .replace(/services?/gi, '')\n    .replace(/limited|ltd|pvt|private/gi, '')\n    .trim();\n};\n\n// --------------------------------\n// 3. Desired item name\n// --------------------------------\nconst desiredItemName = normalize(shortenName(txn.payee || txn.description));\n\n// --------------------------------\n// 4. Existing item map\n// --------------------------------\nconst existingMap = {};\n\nqbItems.forEach(n => {\n  if (n.json?.Name && n.json?.Id) {\n    existingMap[normalize(n.json.Name)] = {\n      id: n.json.Id,\n      name: n.json.Name\n    };\n  }\n});\n\n// --------------------------------\n// 5. Decide create or reuse\n// --------------------------------\nconst itemsToCreate = [];\nconst existingItemMapping = {};\n\nif (existingMap[desiredItemName]) {\n  existingItemMapping[desiredItemName] = existingMap[desiredItemName];\n} else {\n  itemsToCreate.push({\n    name: shortenName(txn.payee),\n    description: txn.description,\n    type: 'Service',\n    incomeAccountRef: '79',\n    expenseAccountRef: '80'\n  });\n}\n\nreturn [{\n  json: {\n    desiredItemName,\n    itemsToCreate,\n    existingItemMapping,\n    needsCreation: itemsToCreate.length > 0\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fdc9ab25-31f3-4d09-b478-79f87fa05914",
      "name": "Credit or Debit?",
      "type": "n8n-nodes-base.switch",
      "position": [
        -2192,
        2464
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Credit",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "655116ac-b3b9-42d8-965e-7a6dc73d096b",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.transactionType }}",
                    "rightValue": "credit"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Debit",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "179cb4e7-89b9-4b98-9f95-109d6efedf20",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.transactionType }}",
                    "rightValue": "debit"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "0bc3819d-3c03-4fd7-a411-c55f21564300",
      "name": "Customers Exists?1",
      "type": "n8n-nodes-base.if",
      "position": [
        -784,
        2336
      ],
      "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": "12636d7f-5d9a-4882-8216-f63566c6dc99",
      "name": "Create a Customer",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        -576,
        2272
      ],
      "parameters": {
        "operation": "create",
        "displayName": "={{ $('Collect All Item Mappings').item.json.itemRef.name }}",
        "additionalFields": {
          "CompanyName": "={{ $('Collect All Item Mappings').item.json.itemRef.name }}"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "8d87eca6-2b68-44c7-a81b-a7b6595fb2b4",
      "name": "Create QuickBooks Expense",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -832,
        2752
      ],
      "parameters": {
        "url": "https://sandbox-quickbooks.api.intuit.com/v3/company/company_id/purchase?minorversion=75",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"TxnDate\": \"{{ $json.TxnDate }}\",\n  \"PrivateNote\": \"{{ $json.PrivateNote }}\",\n\n  \"EntityRef\": {\n    \"type\": \"Vendor\",\n    \"value\": \"{{ $json.EntityRef.value }}\",\n    \"name\": \"{{ $json.EntityRef.name }}\"\n  },\n\n  \"PaymentType\": \"{{ $json.PaymentType }}\",\n\n  \"AccountRef\": {\n    \"value\": \"35\",\n    \"name\": \"Checking\"\n  },\n\n  \"Line\": [\n    {\n      \"Amount\": {{ $json.Line[0].Amount }},\n      \"DetailType\": \"AccountBasedExpenseLineDetail\",\n      \"AccountBasedExpenseLineDetail\": {\n        \"AccountRef\": {\n          \"value\": \"{{ $json.Line[0].AccountBasedExpenseLineDetail.AccountRef.value }}\",\n          \"name\": \"{{ $json.Line[0].AccountBasedExpenseLineDetail.AccountRef.name }}\"\n        },\n        \"TaxCodeRef\": {\n          \"value\": \"NON\"\n        }\n      }\n    }\n  ]\n}\n",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "quickBooksOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "bcaacfb1-d417-4372-b1e5-0870a00c8897",
      "name": "Build Expense Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        -1056,
        2752
      ],
      "parameters": {
        "jsCode": "// // -----------------------------\n// // 1. Transaction\n// // -----------------------------\n// const txn = $('Loop Over Items').item.json;\n\n// // -----------------------------\n// // 2. Vendor (from Merge node)\n// // -----------------------------\n// const vendor = $input.first().json;\n\n// if (!vendor || !vendor.Id) {\n//   throw new Error(`Vendor not resolved for payee: ${txn.payee}`);\n// }\n\n// // -----------------------------\n// // 3. Build Expense Payload\n// // -----------------------------\n// const expensePayload = {\n//   TxnDate: txn.date,\n//   PrivateNote: `${txn.description} - Ref: ${txn.referenceNumber || 'N/A'}`,\n//   EntityRef: {\n//     type: \"Vendor\",\n//     value: vendor.Id,\n//     name: vendor.DisplayName || vendor.CompanyName\n//   },\n//   PaymentType: \"Cash\",\n//   TotalAmt: txn.amount,\n//   Line: [\n//     {\n//       Amount: txn.amount,\n//       DetailType: \"AccountBasedExpenseLineDetail\",\n//       AccountBasedExpenseLineDetail: {\n//         AccountRef: {\n//           value: \"35\",   // \u2705 Bank Account (Checking)\n//           name: \"Checking\"\n//         },\n//         TaxCodeRef: {\n//           value: \"NON\"\n//         }\n//       }\n//     }\n//   ]\n// };\n\n// return { json: expensePayload };\n\n\n\n\n// -----------------------------\n// 1. Transaction\n// -----------------------------\nconst txn = $('Loop Over Items').item.json;\nconst description = (txn.description || '').toLowerCase();\n\n// -----------------------------\n// 2. Vendor (from Merge logic)\n// -----------------------------\nconst vendor = $input.first().json;\n\nif (!vendor || !vendor.Id) {\n  throw new Error(`Vendor not resolved for payee: ${txn.payee}`);\n}\n\n// -----------------------------\n// 3. Load Categories (Chart of Accounts)\n// -----------------------------\nconst categoryItems =\n  $items('Search Categories')[0]?.json?.QueryResponse?.Account || [];\n\nif (!categoryItems.length) {\n  throw new Error('No categories found from QuickBooks');\n}\n\n// Build lookup map: name -> account\nconst categoryMap = {};\nfor (const acc of categoryItems) {\n  if (acc.AccountType === 'Expense') {\n    categoryMap[acc.Name.toLowerCase()] = acc;\n  }\n}\n\n// -----------------------------\n// 4. Auto Category Mapping Logic\n// -----------------------------\nlet selectedCategory = null;\n\nif (description.includes('upi') || description.includes('bank') || description.includes('charge')) {\n  selectedCategory = categoryMap['bank charges'];\n}\nelse if (description.includes('insurance')) {\n  selectedCategory = categoryMap['insurance expense'];\n}\nelse if (description.includes('ads') || description.includes('advertising')) {\n  selectedCategory = categoryMap['advertising'];\n}\nelse if (description.includes('vendor') || description.includes('service')) {\n  selectedCategory = categoryMap['professional fees'];\n}\n\n// Default fallback\nif (!selectedCategory) {\n  selectedCategory = categoryMap['services'];\n}\n\nif (!selectedCategory) {\n  throw new Error('No matching expense category found');\n}\n\n// -----------------------------\n// 5. Build Expense Payload (CORRECT)\n// -----------------------------\nconst expensePayload = {\n  TxnDate: txn.date,\n  PrivateNote: `${txn.description} - Ref: ${txn.referenceNumber || 'N/A'}`,\n  EntityRef: {\n    type: 'Vendor',\n    value: vendor.Id,\n    name: vendor.DisplayName || vendor.CompanyName\n  },\n  PaymentType: 'Cash',\n  TotalAmt: txn.amount,\n  Line: [\n    {\n      Amount: txn.amount,\n      DetailType: 'AccountBasedExpenseLineDetail',\n      AccountBasedExpenseLineDetail: {\n        AccountRef: {\n          value: selectedCategory.Id,\n          name: selectedCategory.Name\n        },\n        BillableStatus: 'NotBillable',\n        TaxCodeRef: {\n          value: 'NON'\n        }\n      }\n    }\n  ]\n};\n\nreturn { json: expensePayload };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7c1aedbd-4bc1-4d3c-84e0-2c84f273b474",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3984,
        2288
      ],
      "parameters": {
        "width": 380,
        "height": 508,
        "content": "## How it works\nUpload a password-protected or normal bank statement PDF through a web form. AI extracts all transactions and classifies them as credits (income) or debits (expenses). Credits create Sales Receipts with auto-generated customers and items. Debits create Purchase records with vendors and expense categories. The workflow handles duplicate checking and creates missing QuickBooks entities automatically.\n\n## Setup steps\n1. Connect your QuickBooks Online account (sandbox or production)\n2. Add OpenRouter API credentials for AI extraction\n3. Update the PDF password in Extract PDF Text node\n4. Replace 'company_id' in HTTP Request URLs with your QuickBooks Company ID\n5. Verify account IDs (35 for Checking, 79/80 for Income/Expense)\n6. Test with a sample bank statement"
      },
      "typeVersion": 1
    },
    {
      "id": "683f4fc6-6b8a-4b6e-bc24-21be977797b2",
      "name": "PDF Extraction",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3296,
        2256
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 588,
        "content": "## PDF Extraction\nForm trigger receives bank statement PDF. Text is extracted and sent to AI agent with structured output parser to identify all transactions with dates, amounts, descriptions, and payees."
      },
      "typeVersion": 1
    },
    {
      "id": "97fdacea-2f20-461f-8ddc-b4d2703bace2",
      "name": "Transaction Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2720,
        2256
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 588,
        "content": "## Transaction Processing\nSplit extracted transactions into individual items. Loop processes each one, routing credits and debits to separate paths based on transaction type."
      },
      "typeVersion": 1
    },
    {
      "id": "c77e9110-11ff-4932-acc4-50e1f12a362a",
      "name": "Credit Path",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2016,
        2144
      ],
      "parameters": {
        "color": 7,
        "width": 1752,
        "height": 420,
        "content": "## Credit Path (Income)\nFetches existing QuickBooks items or creates new service items. Checks for existing customers by name, creates if missing. Builds and posts Sales Receipt to record income transaction."
      },
      "typeVersion": 1
    },
    {
      "id": "ba30de8a-1dbf-474b-aef5-927c4bb45459",
      "name": "Debit Path",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2016,
        2576
      ],
      "parameters": {
        "color": 7,
        "width": 1376,
        "height": 420,
        "content": "## Debit Path (Expenses)\nLoads expense categories from Chart of Accounts. Searches for vendor by payee name, creates if not found. Auto-maps transaction to expense category based on description keywords. Posts Purchase record to QuickBooks."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Find Vendor": {
      "main": [
        [
          {
            "node": "Vendor Exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Items": {
      "main": [
        [
          {
            "node": "Collect All Item Mappings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Vendor": {
      "main": [
        [
          {
            "node": "Build Expense Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vendor Exists?": {
      "main": [
        [
          {
            "node": "Create Vendor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Expense Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Credit or Debit?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Credit or Debit?": {
      "main": [
        [
          {
            "node": "Get All QB Items",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Search Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract PDF Text": {
      "main": [
        [
          {
            "node": "Transaction Extractor AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All QB Items": {
      "main": [
        [
          {
            "node": "Check Which Items to Create",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a Customer": {
      "main": [
        [
          {
            "node": "Build Salesreceipt Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Categories": {
      "main": [
        [
          {
            "node": "Find Vendor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Customers Exists?1": {
      "main": [
        [
          {
            "node": "Create a Customer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Salesreceipt Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get many customers": {
      "main": [
        [
          {
            "node": "Customers Exists?1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JSON Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Transaction Extractor AI",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Split Transactions": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bank Statement Form": {
      "main": [
        [
          {
            "node": "Extract PDF Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Expense Payload": {
      "main": [
        [
          {
            "node": "Create QuickBooks Expense",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Need to Create Items?": {
      "main": [
        [
          {
            "node": "Create Items",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Collect All Item Mappings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Transaction Extractor AI",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Transaction Extractor AI": {
      "main": [
        [
          {
            "node": "Split Transactions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect All Item Mappings": {
      "main": [
        [
          {
            "node": "Get many customers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create QuickBooks Expense": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Salesreceipt Payload": {
      "main": [
        [
          {
            "node": "Create QuickBooks SalesReceipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Which Items to Create": {
      "main": [
        [
          {
            "node": "Need to Create Items?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create QuickBooks SalesReceipt": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}