{ "name": "1 - WFDASH QUICKBOOKS INVOICE1 - WFDASH QUICKBOOKS INVOICE_2TEST", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "wfdash-quickbooks-invoice-creator", "responseMode": "responseNode", "options": {} }, "id": "e78f7da7-d821-4ec4-864b-34b6d9f2a326", "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -1760, 2240 ], "webhookId": "b9523463-a997-4103-8a6c-75885a178e01" }, { "parameters": { "jsCode": "const body = $json.body ?? $json;\n\n// Public webhook mode: do not require X-Webhook-Token.\nconst orderNumber = (body.order_number ?? '').toString().trim();\nconst billingCompany = (body.billing_company ?? '').toString().trim().toUpperCase();\nconst invoiceCompany = (body.invoice_company ?? body.company ?? '').toString().trim();\nconst customerId = (body.customer_id ?? body.qb_customer_id ?? '').toString().trim();\nconst customerName = (body.customer_name ?? '').toString().trim();\nconst items = Array.isArray(body.items) ? body.items : [];\nconst orderItemIds = Array.isArray(body.order_item_ids)\n ? body.order_item_ids.map((value) => value.toString().trim()).filter(Boolean)\n : [];\nconst sentById = (body.sent_by_id ?? '').toString().trim();\nconst sentByName = (body.sent_by_name ?? '').toString().trim();\n\nconsole.log(JSON.stringify({\n event: 'wfdash.invoice.request',\n order_number: orderNumber,\n billing_company: billingCompany,\n customer_id: customerId || null,\n line_count: items.length,\n order_item_count: orderItemIds.length,\n sent_by_id: sentById || null\n}));\n\nreturn [{\n json: {\n invoice_company: invoiceCompany,\n company: invoiceCompany,\n billing_company: billingCompany,\n order_number: orderNumber,\n customer_id: customerId,\n customer_name: customerName,\n order_item_ids: orderItemIds,\n sent_by_id: sentById,\n sent_by_name: sentByName,\n items,\n line_count: items.length,\n auth_ok: true\n }\n}];" }, "id": "cc33bc60-6969-4009-acce-47af5005d541", "name": "Normalize Request", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -1296, 2256 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "auth-ok-check", "leftValue": "={{ $json.auth_ok }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "d63860be-7af3-461e-9f98-fd86f755f91b", "name": "Is Authorized?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ -1072, 2256 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "unauth-status", "name": "status", "value": "error", "type": "string" }, { "id": "unauth-message", "name": "message", "value": "Unauthorized: invalid or missing X-Webhook-Token", "type": "string" }, { "id": "unauth-order", "name": "order_number", "value": "={{ $json.order_number }}", "type": "string" }, { "id": "unauth-billing", "name": "billing_company", "value": "={{ $json.billing_company }}", "type": "string" }, { "id": "unauth-http", "name": "http_status", "value": 401, "type": "number" } ] }, "options": {} }, "id": "1b3e608d-4165-4dc4-8c77-b03123b774b1", "name": "Unauthorized Response", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3408, 2960 ] }, { "parameters": { "jsCode": "const errors = [];\nconst allowedCompanies = ['CNL', 'ISHERWOOD'];\n\nif (!allowedCompanies.includes($json.billing_company)) {\n errors.push('billing_company must be one of CNL, ISHERWOOD');\n}\n\nif (!$json.order_number || typeof $json.order_number !== 'string' || $json.order_number.trim() === '') {\n errors.push('order_number is required');\n}\n\nif (!$json.customer_id || typeof $json.customer_id !== 'string' || $json.customer_id.trim() === '') {\n errors.push('customer_id is required (select a QuickBooks customer from suggestions)');\n}\n\nif (!Array.isArray($json.items) || $json.items.length < 1) {\n errors.push('items must be an array with at least 1 item');\n}\n\nif (Array.isArray($json.items)) {\n $json.items.forEach((item, index) => {\n const row = index + 1;\n if (!item || typeof item.name !== 'string' || item.name.trim() === '') {\n errors.push(`items[${row}] name is required`);\n }\n if (typeof item.qty !== 'number' || Number(item.qty) <= 0) {\n errors.push(`items[${row}] qty must be > 0`);\n }\n if (typeof item.price !== 'number' || Number(item.price) < 0) {\n errors.push(`items[${row}] price must be >= 0`);\n }\n });\n}\n\nif (!Array.isArray($json.order_item_ids) || $json.order_item_ids.length < 1) {\n errors.push('order_item_ids must be an array with at least 1 id');\n}\n\nif (!$json.sent_by_id || typeof $json.sent_by_id !== 'string' || $json.sent_by_id.trim() === '') {\n errors.push('sent_by_id is required');\n}\n\nif (!$json.sent_by_name || typeof $json.sent_by_name !== 'string' || $json.sent_by_name.trim() === '') {\n errors.push('sent_by_name is required');\n}\n\nif (errors.length > 0) {\n return [{\n json: {\n ...$json,\n status: 'error',\n message: `Validation failed: ${errors.join('; ')}`,\n http_status: 400,\n valid: false\n }\n }];\n}\n\nreturn [{\n json: {\n ...$json,\n valid: true\n }\n}];" }, "id": "643e3226-d2c6-4137-817b-ff47133f6c97", "name": "Validate Payload", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -848, 2160 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "payload-valid-check", "leftValue": "={{ $json.valid }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "20967ae3-bccb-44b1-b89d-8c526d0e14ad", "name": "Is Payload Valid?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ -624, 2160 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "validation-status", "name": "status", "value": "={{ $json.status }}", "type": "string" }, { "id": "validation-message", "name": "message", "value": "={{ $json.message }}", "type": "string" }, { "id": "validation-order", "name": "order_number", "value": "={{ $json.order_number }}", "type": "string" }, { "id": "validation-billing", "name": "billing_company", "value": "={{ $json.billing_company }}", "type": "string" }, { "id": "validation-http", "name": "http_status", "value": "={{ $json.http_status }}", "type": "number" } ] }, "options": {} }, "id": "5de33c57-b278-45a9-855a-40d793d0b370", "name": "Validation Error Response", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3408, 2768 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "set-cnl-context-item", "name": "qb_item_ref", "value": "1", "type": "string" }, { "id": "aa418315-fa29-4c57-b224-d05bd67e3b4d", "name": "qb_realm_id", "value": "123146321422364", "type": "string" }, { "id": "439061c4-f708-4e25-85e9-92e4e1f7acfb", "name": "letterhead", "value": "={{ $('Normalize Request').item.json.billing_company }}", "type": "string" } ] }, "includeOtherFields": true, "options": {} }, "id": "85669920-1c1b-4cd6-bdb1-d26548abdcbc", "name": "Set CNL Context", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ -176, 1968 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "set-isherwood-context-item", "name": "qb_item_ref", "value": "1", "type": "string" }, { "id": "36efc6b6-a874-4c66-a5a7-d705e8a75270", "name": "qb_realm_id", "value": "123146321437969", "type": "string" }, { "id": "2f5aca75-1314-41a0-8934-d605ed16e8b0", "name": "letterhead", "value": "={{ $('Normalize Request').item.json.billing_company }}", "type": "string" } ] }, "includeOtherFields": true, "options": {} }, "id": "27b4f8d2-fb73-4764-bda5-5bf5ac1d236b", "name": "Set ISHERWOOD Context", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ -176, 2592 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "mapping-status", "name": "status", "value": "error", "type": "string" }, { "id": "mapping-message", "name": "message", "value": "Unsupported billing_company mapping", "type": "string" }, { "id": "mapping-order", "name": "order_number", "value": "={{ $json.order_number }}", "type": "string" }, { "id": "mapping-company", "name": "billing_company", "value": "={{ $json.billing_company }}", "type": "string" }, { "id": "mapping-http", "name": "http_status", "value": 400, "type": "number" } ] }, "options": {} }, "id": "4135d354-637e-4a87-b5f1-0714915355f7", "name": "Mapping Error Response", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3408, 3152 ] }, { "parameters": { "jsCode": "return [{ json: { ...$json } }];" }, "id": "06cf6199-8e91-4b2d-b592-db460160f1ac", "name": "QB Context", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 48, 2064 ] }, { "parameters": { "url": "={{ 'https://quickbooks.api.intuit.com/v3/company/' + $json.qb_realm_id + '/query?query=' + encodeURIComponent(\"select * from Invoice order by MetaData.CreateTime desc maxresults 200\") + '&minorversion=65' }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "quickBooksOAuth2Api", "options": {} }, "id": "0b14c94f-df33-4c24-a0fb-4cef80a1852d", "name": "Search Existing Invoice (CNL)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 496, 1968 ], "credentials": { "quickBooksOAuth2Api": { "id": "BPkRunJXtL1mqASg", "name": "CNL QUICKBOOKS" } }, "continueOnFail": true }, { "parameters": { "jsCode": "const ctx = $('QB Context').item.json;\nconst response = $json;\n\nif (response.error) {\n const err = response.error;\n const statusCode = err?.statusCode ?? err?.status ?? err?.httpStatusCode ?? err?.response?.statusCode;\n\n const rawBody = err?.response?.body ?? err?.response?.data ?? err?.body;\n const bodyText = typeof rawBody === 'string' ? rawBody : (rawBody ? JSON.stringify(rawBody) : '');\n\n const primary = err?.message || err?.Message || err?.description || err?.error;\n const fallback = typeof err === 'string' ? err : JSON.stringify(err);\n\n const parts = [];\n if (statusCode) parts.push(`status=${statusCode}`);\n if (primary) parts.push(String(primary));\n if (bodyText) parts.push(bodyText);\n\n const details = (parts.length ? parts.join(' | ') : fallback);\n const safeDetails = String(details).length > 2000 ? String(details).slice(0, 2000) + '…' : String(details);\n\n return [{\n json: {\n ...ctx,\n qb_action: 'error',\n status: 'error',\n message: `QuickBooks lookup failed: ${safeDetails}`,\n http_status: statusCode || 502\n }\n }];\n}\n\nconst invoices = response.QueryResponse?.Invoice ?? [];\nconst needle = `order_number:${ctx.order_number}`;\n\nconst normalizeText = (value) => String(value ?? '').trim().replace(/\\s+/g, ' ').toLowerCase();\nconst normalizeNumber = (value, decimals = 4) => {\n const num = Number(value);\n if (!Number.isFinite(num)) return null;\n return num.toFixed(decimals);\n};\n\nconst signatureFromLines = (lines) => {\n const normalized = lines\n .map((line) => ({\n desc: normalizeText(line.desc),\n qty: normalizeNumber(line.qty, 4),\n unitPrice: normalizeNumber(line.unitPrice, 4)\n }))\n .filter((line) => line.qty !== null && line.unitPrice !== null)\n .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));\n\n return JSON.stringify(normalized);\n};\n\nconst incomingSignature = signatureFromLines(\n (Array.isArray(ctx.items) ? ctx.items : []).map((item) => ({\n desc: item?.name,\n qty: item?.qty,\n unitPrice: item?.price\n }))\n);\n\nconst getInvoiceSignature = (invoice) => signatureFromLines(\n (Array.isArray(invoice?.Line) ? invoice.Line : [])\n .filter((line) => line?.DetailType === 'SalesItemLineDetail')\n .map((line) => ({\n desc: line?.Description,\n qty: line?.SalesItemLineDetail?.Qty,\n unitPrice: line?.SalesItemLineDetail?.UnitPrice\n }))\n);\n\nif (Array.isArray(invoices) && invoices.length > 0) {\n const matchingOrderInvoices = invoices.filter((inv) => {\n const note = String(inv?.PrivateNote ?? '').trim();\n return note === needle || note.startsWith(needle);\n });\n\n if (matchingOrderInvoices.length > 0) {\n const exactDuplicate = matchingOrderInvoices.find((inv) => getInvoiceSignature(inv) === incomingSignature);\n\n if (exactDuplicate) {\n const invoiceId = exactDuplicate.Id ? String(exactDuplicate.Id) : '';\n const invoiceNumber = exactDuplicate.DocNumber ? String(exactDuplicate.DocNumber) : '';\n const invoiceLink = invoiceId ? `https://app.qbo.intuit.com/app/invoice?txnId=${invoiceId}` : '';\n\n console.log(JSON.stringify({\n event: 'wfdash.invoice.idempotent-hit',\n order_number: ctx.order_number,\n billing_company: ctx.billing_company,\n invoice_id: invoiceId,\n match_type: 'exact-items'\n }));\n\n return [{\n json: {\n ...ctx,\n qb_action: 'existing',\n status: 'success',\n invoice_number: invoiceNumber,\n invoice_id: invoiceId,\n invoice_link: invoiceLink\n }\n }];\n }\n\n console.log(JSON.stringify({\n event: 'wfdash.invoice.partial-create',\n order_number: ctx.order_number,\n billing_company: ctx.billing_company,\n existing_order_invoice_count: matchingOrderInvoices.length,\n reason: 'items-differ'\n }));\n }\n}\n\nconst lineItems = ctx.items.map(item => ({\n Amount: Number(item.qty) * Number(item.price),\n Description: String(item.name ?? ''),\n DetailType: 'SalesItemLineDetail',\n SalesItemLineDetail: {\n Qty: Number(item.qty),\n UnitPrice: Number(item.price),\n ItemRef: {value: String(ctx.qb_item_ref)},\n TaxCodeRef: { value: '5' } \n }\n}));\n\nconst payload = {\n Line: lineItems,\n CustomerRef: {\n value: String(ctx.customer_id || ctx.qb_customer_ref)\n },\n // Set Billing Email\n BillEmail: ctx.customer_email ? { Address: String(ctx.customer_email) } : undefined,\n \n // Keep order number in both internal note and PO/order field.\n PrivateNote: `order_number:${ctx.order_number}`,\n PONumber: String(ctx.order_number),\n CustomerMemo: {\n value: `Order: ${ctx.order_number}`\n },\n CustomField: [\n {\n DefinitionId: \"3\",\n Type: \"StringType\",\n StringValue: String(ctx.order_number)\n }\n ]\n};\n\nreturn [{\n json: {\n ...ctx,\n qb_action: 'create',\n qb_invoice_payload: payload\n }\n}];\n" }, "id": "ba19062b-f562-4de1-b952-9b5018047ec5", "name": "Decide Invoice Action", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 720, 2064 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "action-error-check", "leftValue": "={{ $json.qb_action }}", "rightValue": "error", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "effcd23a-b425-4b45-969a-85bca3afa798", "name": "Action Is Error?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 944, 2064 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "action-existing-check", "leftValue": "={{ $json.qb_action }}", "rightValue": "existing", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "682fabd2-4a05-4eb1-b269-9f72ebdc9b8f", "name": "Action Is Existing?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 1200, 1904 ] }, { "parameters": { "method": "POST", "url": "={{ 'https://quickbooks.api.intuit.com/v3/company/' + $json.qb_realm_id + '/invoice?minorversion=65' }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "quickBooksOAuth2Api", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Accept", "value": "application/json" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ {\n ...$json.qb_invoice_payload,\n DocNumber: $json.current_doc_number || $json.generated_invoice_number\n} }}", "options": {} }, "id": "1d3a895a-521c-4103-a55f-700642a0b305", "name": "Create Invoice (CNL)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 2064, 1824 ], "credentials": { "quickBooksOAuth2Api": { "id": "BPkRunJXtL1mqASg", "name": "CNL QUICKBOOKS" } }, "continueOnFail": true }, { "parameters": { "jsCode": "const ctx = $('QB Context').item.json;\n\n// 1. Get the invoice data safely from the creation branch\nlet createResponse = null;\ntry { createResponse = $('Create Invoice (CNL)').item.json; } catch (e) {}\nif (!createResponse) {\n try { createResponse = $('Create Invoice (ISHERWOOD)').item.json; } catch (e) {}\n}\n\nconst invoice = createResponse?.Invoice ?? {};\nconst invoiceId = invoice.Id ? String(invoice.Id) : '';\nconst invoiceNumber = invoice.DocNumber ? String(invoice.DocNumber) : '';\nconst invoiceLink = invoiceId ? `https://app.qbo.intuit.com/app/invoice?txnId=${invoiceId}` : '';\nconst invoiceDate = invoice.TxnDate ? String(invoice.TxnDate) : '';\nconst invoiceCompany = String(ctx.invoice_company ?? ctx.company ?? '').trim();\n\n// 2. Grab the PDF base64 from the PREVIOUS node (Extract from File)\nconst pdfBase64 = $json.pdf_base64 || null;\n\n// 3. Log for debugging\nconsole.log(JSON.stringify({\n event: 'wfdash.invoice.created',\n order_number: ctx.order_number,\n invoice_id: invoiceId,\n has_pdf: !!pdfBase64\n}));\n\n// 4. Return to WFDASH\nreturn [{\n json: {\n status: 'success',\n invoice_number: invoiceNumber,\n invoice_id: invoiceId,\n invoice_link: invoiceLink,\n qb_invoice_date: invoiceDate,\n invoice_company: invoiceCompany,\n company: invoiceCompany,\n order_number: ctx.order_number,\n billing_company: ctx.billing_company,\n order_item_ids: Array.isArray(ctx.order_item_ids) ? ctx.order_item_ids : [],\n sent_by_id: String(ctx.sent_by_id ?? ''),\n sent_by_name: String(ctx.sent_by_name ?? ''),\n line_count: ctx.items.length,\n pdf_base64: pdfBase64\n }\n}];\n" }, "id": "02261bba-dc90-432d-bdf3-3d269aacdc56", "name": "Build Created Response", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 3184, 1712 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "existing-status", "name": "status", "value": "success", "type": "string" }, { "id": "existing-number", "name": "invoice_number", "value": "={{ $json.invoice_number }}", "type": "string" }, { "id": "existing-id", "name": "invoice_id", "value": "={{ $json.invoice_id }}", "type": "string" }, { "id": "existing-link", "name": "invoice_link", "value": "={{ $json.invoice_link }}", "type": "string" }, { "id": "existing-order", "name": "order_number", "value": "={{ $json.order_number }}", "type": "string" }, { "id": "existing-billing", "name": "billing_company", "value": "={{ $json.billing_company }}", "type": "string" }, { "id": "existing-lines", "name": "line_count", "value": "={{ $json.items.length }}", "type": "number" } ] }, "options": {} }, "id": "f363fe43-c582-43b7-af63-2cd941644ec7", "name": "Build Existing Response", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3408, 1520 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "qb-error-status", "name": "status", "value": "error", "type": "string" }, { "id": "qb-error-message", "name": "message", "value": "={{ $json.message }}", "type": "string" }, { "id": "qb-error-order", "name": "order_number", "value": "={{ $json.order_number }}", "type": "string" }, { "id": "qb-error-billing", "name": "billing_company", "value": "={{ $json.billing_company }}", "type": "string" }, { "id": "qb-error-http", "name": "http_status", "value": "={{ $json.http_status ?? 500 }}", "type": "number" } ] }, "options": {} }, "id": "0c7d50a3-b1f1-473f-8c13-b3d84785d86d", "name": "Build QB Error Response", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3408, 2576 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "created-success-check", "leftValue": "={{ $json.status }}", "rightValue": "success", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "b07852ba-015f-4ede-8bda-06df1bc11879", "name": "Created Is Success?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 3408, 1712 ] }, { "parameters": { "respondWith": "json", "responseBody": "={{ $json }}", "options": { "responseCode": 200 } }, "id": "a2107d46-a8c3-4db0-a8cb-afc13d3d06a0", "name": "Respond Success", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ 3696, 1552 ] }, { "parameters": { "respondWith": "json", "responseBody": "={{ { status: 'error', message: $json.message, order_number: $json.order_number, billing_company: $json.billing_company } }}", "options": { "responseCode": "={{ $json.http_status ?? 500 }}" } }, "id": "cb5a7965-381e-4006-baf1-69d9c510f7d7", "name": "Respond Error", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ 3632, 2576 ] }, { "parameters": { "operation": "getAll", "returnAll": true, "filters": {} }, "type": "n8n-nodes-base.quickbooks", "typeVersion": 1, "position": [ -240, 48 ], "id": "3052f1c8-d9c7-48d7-a66d-5f6c5b2570ee", "name": "GET ISHERWOOD CUSTOMERS1", "credentials": { "quickBooksOAuth2Api": { "id": "qIOWSWNJQneDXGmH", "name": "ISHERWOODMINING" } } }, { "parameters": { "operation": "getAll", "returnAll": true, "filters": {} }, "type": "n8n-nodes-base.quickbooks", "typeVersion": 1, "position": [ -240, 240 ], "id": "c0c5db1e-c2e2-4457-b499-45f5349019a2", "name": "GET CNL CUSTOMERS1", "credentials": { "quickBooksOAuth2Api": { "id": "BPkRunJXtL1mqASg", "name": "CNL QUICKBOOKS" } } }, { "parameters": { "httpMethod": "POST", "path": "wfdash-get-customers", "responseMode": "responseNode", "options": {} }, "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -1584, 336 ], "id": "677cc284-2595-473c-97c9-2c8ac35ac476", "name": "GET CUSTOMERS WEBHOOK", "webhookId": "a6b0fde0-592a-49bb-a345-84d7d690feb1" }, { "parameters": { "respondWith": "json", "responseBody": "={{ $json }}", "options": { "responseCode": "={{ $json.http_status ?? 200 }}" } }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [ 208, 512 ], "id": "5175f76d-81e3-47a5-9025-d9b03ba33c66", "name": "Respond to Webhook" }, { "parameters": { "jsCode": "const body = $json.body ?? $json;\n\n// Public webhook mode: do not require X-Webhook-Token.\nconst billingCompany = (body.billing_company ?? '').toString().trim().toUpperCase();\nconst query = (body.customer_query ?? body.company ?? body.customer_name ?? '').toString().trim();\n\nreturn [{\n json: {\n billing_company: billingCompany,\n customer_query: query,\n limit: Number(body.limit ?? 15),\n auth_ok: true\n }\n}];" }, "id": "b39545e6-e2a5-4030-9e7b-c4f73ac53f7a", "name": "Normalize Customer Search Request", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -1360, 336 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "customer-auth-ok", "leftValue": "={{ $json.auth_ok }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "0d0045a4-b689-4025-8c96-af11481d862d", "name": "Is Customer Authorized?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ -1136, 336 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "customer-unauth-status", "name": "status", "value": "error", "type": "string" }, { "id": "customer-unauth-message", "name": "message", "value": "Unauthorized: invalid or missing X-Webhook-Token", "type": "string" }, { "id": "customer-unauth-billing", "name": "billing_company", "value": "={{ $json.billing_company }}", "type": "string" }, { "id": "customer-unauth-http", "name": "http_status", "value": 401, "type": "number" } ] }, "options": {} }, "id": "9b75db94-8d6b-498d-9a23-1c7c9efa7e1f", "name": "Build Customer Unauthorized", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ -16, 608 ] }, { "parameters": { "jsCode": "const errors = [];\nconst allowedCompanies = ['CNL', 'ISHERWOOD'];\n\nif (!allowedCompanies.includes($json.billing_company)) {\n errors.push('billing_company must be one of CNL, ISHERWOOD');\n}\n\nif (!$json.customer_query || typeof $json.customer_query !== 'string' || $json.customer_query.trim() === '') {\n errors.push('customer_query (or company/customer_name) is required');\n}\n\nif (errors.length > 0) {\n return [{\n json: {\n ...$json,\n valid: false,\n status: 'error',\n message: `Validation failed: ${errors.join('; ')}`,\n http_status: 400\n }\n }];\n}\n\nreturn [{\n json: {\n ...$json,\n valid: true\n }\n}];" }, "id": "675d98c5-cd32-44e5-838f-810a4f11b990", "name": "Validate Customer Search", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -912, 240 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "customer-validate-ok", "leftValue": "={{ $json.valid }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "d858f6fa-fe65-47c1-b8a3-7ce6ea3155d9", "name": "Is Customer Search Valid?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ -688, 240 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "customer-validation-status", "name": "status", "value": "={{ $json.status }}", "type": "string" }, { "id": "customer-validation-message", "name": "message", "value": "={{ $json.message }}", "type": "string" }, { "id": "customer-validation-billing", "name": "billing_company", "value": "={{ $json.billing_company }}", "type": "string" }, { "id": "customer-validation-http", "name": "http_status", "value": "={{ $json.http_status ?? 400 }}", "type": "number" } ] }, "options": {} }, "id": "773f4819-991f-4573-90d4-9558b712df9a", "name": "Build Customer Validation Error", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ -16, 416 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "customer-mapping-status", "name": "status", "value": "error", "type": "string" }, { "id": "customer-mapping-message", "name": "message", "value": "Unsupported billing_company mapping", "type": "string" }, { "id": "customer-mapping-billing", "name": "billing_company", "value": "={{ $json.billing_company }}", "type": "string" }, { "id": "customer-mapping-http", "name": "http_status", "value": 400, "type": "number" } ] }, "options": {} }, "id": "14270791-1004-4e59-89ec-1094ca7336a7", "name": "Build Customer Mapping Error", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ -16, 800 ] }, { "parameters": { "jsCode": "const source = $input.all().map((item) => item.json);\nconst ctx = $('If1').item.json;\nconst queryRaw = String(ctx.customer_query || '').toLowerCase().trim();\nconst queryTokens = queryRaw.split(/[^a-z0-9]+/).filter(Boolean);\nconst limit = Math.max(1, Math.min(Number(ctx.limit || 15), 50));\n\nfunction scoreMatch(name) {\n const candidate = String(name || '').toLowerCase().trim();\n if (!candidate) return 0;\n if (candidate === queryRaw) return 1;\n\n let score = 0;\n if (candidate.includes(queryRaw)) score += 0.6;\n\n const candidateTokens = candidate.split(/[^a-z0-9]+/).filter(Boolean);\n if (queryTokens.length > 0) {\n const overlap = queryTokens.filter((t) => candidateTokens.includes(t)).length;\n score += 0.4 * (overlap / queryTokens.length);\n }\n\n return Math.min(score, 0.99);\n}\n\nconst mapped = source\n .map((customer) => {\n const displayName = customer.DisplayName || customer.CompanyName || customer.FullyQualifiedName || '';\n const companyName = customer.CompanyName || '';\n const finalScore = Math.max(scoreMatch(displayName), scoreMatch(companyName));\n return {\n id: String(customer.Id || ''),\n display_name: String(displayName),\n company_name: String(companyName),\n fully_qualified_name: String(customer.FullyQualifiedName || ''),\n active: customer.Active !== false,\n score: Number(finalScore.toFixed(4))\n };\n })\n .filter((c) => c.id && c.display_name && c.score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, limit);\n\nreturn [{\n json: {\n status: 'success',\n billing_company: ctx.billing_company,\n query: ctx.customer_query,\n customers: mapped,\n count: mapped.length\n }\n}];" }, "id": "92336b32-e1e5-48db-b0d2-77413d50c841", "name": "Fuzzy Match Customers", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -16, 144 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "d493ef7f-18b8-4bd4-9f49-bc5c117b077f", "leftValue": "={{ $json.billing_company }}", "rightValue": "ISHERWOOD", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ -464, 144 ], "id": "a53ce3b7-9059-48e6-a091-f4c746cffacc", "name": "If1" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "d493ef7f-18b8-4bd4-9f49-bc5c117b077f", "leftValue": "={{ $json.billing_company }}", "rightValue": "CNL", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ -400, 2064 ], "id": "fb278251-abfe-4242-ac47-46a62dd1cb7c", "name": "If2" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "route-search-cnl", "leftValue": "={{ $json.letterhead }}", "rightValue": "CNL", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "79065bca-abb3-41e3-bed1-09c34dc3c835", "name": "Route Search Company", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 272, 2064 ] }, { "parameters": { "url": "={{ 'https://quickbooks.api.intuit.com/v3/company/' + $json.qb_realm_id + '/query?query=' + encodeURIComponent(\"select * from Invoice order by MetaData.CreateTime desc maxresults 200\") + '&minorversion=65' }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "quickBooksOAuth2Api", "options": {} }, "id": "b25d42e3-906c-4b29-9b13-f435b3d0f15f", "name": "Search Existing Invoice (ISHERWOOD)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 496, 2592 ], "credentials": { "quickBooksOAuth2Api": { "id": "qIOWSWNJQneDXGmH", "name": "ISHERWOODMINING" } }, "continueOnFail": true }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "route-create-cnl", "leftValue": "={{ $json.billing_company }}", "rightValue": "CNL", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "821eb736-5815-4838-b296-4a0181f76c94", "name": "Route Create Company", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 1392, 2000 ] }, { "parameters": { "url": "={{ 'https://quickbooks.api.intuit.com/v3/company/' + $json.qb_realm_id + '/query?query=' + encodeURIComponent(\"select * from Invoice order by MetaData.CreateTime desc maxresults 200\") + '&minorversion=65' }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "quickBooksOAuth2Api", "options": {} }, "id": "30b1a90f-2096-4e47-8ef0-fb4b911e865d", "name": "Get Last Invoices (CNL)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1616, 1824 ], "credentials": { "quickBooksOAuth2Api": { "id": "BPkRunJXtL1mqASg", "name": "CNL QUICKBOOKS" } } }, { "parameters": { "jsCode": "const ctx = $('Decide Invoice Action').item.json;\nconst response = $json;\n\nif (response?.error) {\n throw new Error(`Failed to fetch last invoices: ${response.error.message ?? 'unknown error'}`);\n}\n\nconst invoices = response?.QueryResponse?.Invoice ?? [];\n\n// Required pattern: INV#### (example: INV9243)\nconst prefix = 'INV';\nlet maxNumeric = null;\n\nfor (const inv of invoices) {\n const doc = String(inv?.DocNumber ?? '').trim();\n const match = doc.match(/^INV(\\d+)$/i);\n if (!match) continue;\n\n const n = Number.parseInt(match[1], 10);\n if (!Number.isFinite(n)) continue;\n if (maxNumeric === null || n > maxNumeric) maxNumeric = n;\n}\n\nif (maxNumeric === null) {\n throw new Error('No INV#### invoice DocNumber found in the last 200 invoices. Increase maxresults or confirm QuickBooks invoice numbering uses INV + digits.');\n}\n\nconst nextDoc = `${prefix}${maxNumeric + 1}`;\n\nreturn [{\n json: {\n ...ctx,\n qb_invoice_payload: {\n ...(ctx.qb_invoice_payload ?? {}),\n DocNumber: nextDoc\n },\n generated_invoice_number: nextDoc\n }\n}];" }, "id": "06268211-b46a-476c-975c-8250ba8e7fde", "name": "Set Next DocNumber (CNL)", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1840, 1824 ] }, { "parameters": { "url": "={{ 'https://quickbooks.api.intuit.com/v3/company/' + $json.qb_realm_id + '/query?query=' + encodeURIComponent(\"select * from Invoice order by MetaData.CreateTime desc maxresults 200\") + '&minorversion=65' }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "quickBooksOAuth2Api", "options": {} }, "id": "43b167e3-74aa-4d3d-af14-69a3f9208d9c", "name": "Get Last Invoices (ISHERWOOD)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1616, 2208 ], "credentials": { "quickBooksOAuth2Api": { "id": "qIOWSWNJQneDXGmH", "name": "ISHERWOODMINING" } } }, { "parameters": { "jsCode": "const ctx = $('Decide Invoice Action').item.json;\nconst response = $json;\n\nif (response?.error) {\n throw new Error(`Failed to fetch last invoices: ${response.error.message ?? 'unknown error'}`);\n}\n\nconst invoices = response?.QueryResponse?.Invoice ?? [];\n\n// Required pattern: INV#### (example: INV9243)\nconst prefix = 'INV';\nlet maxNumeric = null;\n\nfor (const inv of invoices) {\n const doc = String(inv?.DocNumber ?? '').trim();\n const match = doc.match(/^INV(\\d+)$/i);\n if (!match) continue;\n\n const n = Number.parseInt(match[1], 10);\n if (!Number.isFinite(n)) continue;\n if (maxNumeric === null || n > maxNumeric) maxNumeric = n;\n}\n\nif (maxNumeric === null) {\n throw new Error('No INV#### invoice DocNumber found in the last 200 invoices. Increase maxresults or confirm QuickBooks invoice numbering uses INV + digits.');\n}\n\nconst nextDoc = `${prefix}${maxNumeric + 1}`;\n\nreturn [{\n json: {\n ...ctx,\n qb_invoice_payload: {\n ...(ctx.qb_invoice_payload ?? {}),\n DocNumber: nextDoc\n },\n generated_invoice_number: nextDoc\n }\n}];" }, "id": "f07c0330-8088-460e-a096-6efd4fb00da7", "name": "Set Next DocNumber (ISHERWOOD)", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1840, 2208 ] }, { "parameters": { "method": "POST", "url": "={{ 'https://quickbooks.api.intuit.com/v3/company/' + $json.qb_realm_id + '/invoice?minorversion=65' }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "quickBooksOAuth2Api", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Accept", "value": "application/json" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ {\n ...$json.qb_invoice_payload,\n DocNumber: $json.current_doc_number || $json.generated_invoice_number\n} }}", "options": {} }, "id": "330e1f60-6d89-4048-921c-98a7c29c9858", "name": "Create Invoice (ISHERWOOD)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 2064, 2208 ], "credentials": { "quickBooksOAuth2Api": { "id": "qIOWSWNJQneDXGmH", "name": "ISHERWOODMINING" } }, "continueOnFail": true }, { "parameters": { "operation": "binaryToPropery", "destinationKey": "pdf_base64", "options": { "encoding": "base64", "keepSource": "both" } }, "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ 2960, 2384 ], "id": "d3d27e1c-f604-4c23-8c57-4dd860af0c1e", "name": "Extract from File" }, { "parameters": { "operation": "binaryToPropery", "destinationKey": "pdf_base64", "options": { "encoding": "base64", "keepSource": "both" } }, "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ 2960, 1712 ], "id": "b95f3e9b-e29e-4485-b4f1-02044c0bc4b9", "name": "Extract from File1" }, { "parameters": { "jsCode": "const MAX_RETRIES = 10;\nconst ctx = $('Set Next DocNumber (ISHERWOOD)').item.json;\nconst createResponse = $input.first().json;\n\n// --- Case 1: Success ---\nif (!createResponse.error && createResponse.Invoice?.Id) {\n const invoiceId = String(createResponse.Invoice.Id);\n const docNumber = String(createResponse.Invoice.DocNumber || '');\n\n return [{\n json: {\n ...ctx,\n qb_action: 'created',\n status: 'success',\n created_invoice_id: invoiceId,\n generated_invoice_number: docNumber,\n current_doc_number: docNumber,\n http_status: 200\n }\n }];\n}\n\n// --- Case 2: Duplicate Document Number ---\nconst rawBody = createResponse?.error?.response?.body\n || createResponse?.error?.body\n || createResponse?.error?.description \n || createResponse?.error?.message\n || '';\nconst bodyText = typeof rawBody === 'string' ? rawBody : JSON.stringify(rawBody);\n\nif (bodyText.includes('Duplicate Document Number Error') || bodyText.includes('code\":\"6140')) {\n \n // 1. Extract the exact DocNumber QuickBooks complained about\n const match = bodyText.match(/DocNumber=([A-Z0-9]+)/i);\n const conflictingDoc = match ? match[1] : null;\n\n if (!conflictingDoc) {\n return [{\n json: {\n ...ctx,\n qb_action: 'error',\n status: 'error',\n message: `Duplicate but could not parse DocNumber from error.`,\n http_status: 409\n }\n }];\n }\n\n // 2. Parse the prefix and the numeric part\n const docMatch = conflictingDoc.match(/^([A-Za-z]*)(\\d+)$/);\n if (!docMatch) {\n return [{\n json: {\n ...ctx,\n qb_action: 'error',\n status: 'error',\n message: `Duplicate but could not parse prefix/number: ${conflictingDoc}`,\n http_status: 409\n }\n }];\n }\n\n const [, prefix, numStr] = docMatch;\n \n // 3. Calculate retry count statelessly using math\n const startDocMatch = (ctx.generated_invoice_number || '').match(/^([A-Za-z]*)(\\d+)$/);\n let retryCount = 0;\n if (startDocMatch) {\n retryCount = parseInt(numStr, 10) - parseInt(startDocMatch[2], 10);\n }\n\n // 4. Check limit\n if (retryCount >= MAX_RETRIES) {\n return [{\n json: {\n ...ctx,\n qb_action: 'error',\n status: 'error',\n message: `Failed after ${retryCount} attempts. Last conflict: ${conflictingDoc}`,\n http_status: 409\n }\n }];\n }\n\n // 5. Generate NEXT doc number\n const nextNum = String(parseInt(numStr, 10) + 1).padStart(numStr.length, '0');\n const nextDoc = `${prefix}${nextNum}`;\n\n // Return item back to the loop WITH the explicit retry flag\n return [{\n json: {\n ...ctx,\n qb_action: 'retry', // <--- This explicitly tells the router to loop\n current_doc_number: nextDoc,\n retry_count: retryCount + 1,\n last_error: `Duplicate: tried ${conflictingDoc}, now trying ${nextDoc}`\n }\n }];\n}\n\n// --- Case 3: Other error ---\nreturn [{\n json: {\n ...ctx,\n qb_action: 'error',\n status: 'error',\n message: `QB create failed: ${bodyText.slice(0, 500)}`,\n http_status: createResponse?.error?.statusCode || 500\n }\n}];" }, "id": "cd6ae21d-604e-448d-85c7-75a379de24d1", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2288, 2192 ], "name": "Check Result (ISHERWOOD)" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "condition-1", "leftValue": "={{ $json.qb_action }}", "rightValue": "created", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "f87b6404-b827-46aa-993c-56cc1c723892", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 2512, 2192 ], "name": "Retry Router (ISHERWOOD)" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "condition-1", "leftValue": "={{ $json.qb_action }}", "rightValue": "created", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "5b7414ac-731b-4b27-b603-26dbede9d325", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 2512, 1760 ], "name": "Retry Router (CNL)" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "cond-ish-error", "leftValue": "={{ $json.qb_action }}", "rightValue": "error", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "e8bb712f-b1a7-4104-9e8f-5c23ba646c2f", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 2736, 2192 ], "name": "Retry Router 2 (ISHERWOOD)" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "cond-cnl-error", "leftValue": "={{ $json.qb_action }}", "rightValue": "error", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "5694d736-d90f-4e79-a770-0d37679869a6", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 2736, 1920 ], "name": "Retry Router 2 (CNL)" }, { "parameters": { "url": "={{ 'https://quickbooks.api.intuit.com/v3/company/' + $json.qb_realm_id + '/invoice/' + $json.created_invoice_id + '/pdf' }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "quickBooksOAuth2Api", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Accept", "value": "application/pdf" } ] }, "options": { "response": { "response": { "responseFormat": "file" } } } }, "id": "818d23c8-a0b0-4185-966d-4adf37d85fa7", "name": "Download QBO Invoice", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ 2736, 2384 ], "credentials": { "quickBooksOAuth2Api": { "id": "qIOWSWNJQneDXGmH", "name": "ISHERWOODMINING" } } }, { "parameters": { "url": "={{ 'https://quickbooks.api.intuit.com/v3/company/' + $json.qb_realm_id + '/invoice/' + $json.created_invoice_id + '/pdf' }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "quickBooksOAuth2Api", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Accept", "value": "application/pdf" } ] }, "options": { "response": { "response": { "responseFormat": "file" } } } }, "id": "583be5ac-1569-4cbb-8dbb-6c1b11926181", "name": "Download QBO Invoice1", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ 2736, 1712 ], "credentials": { "quickBooksOAuth2Api": { "id": "BPkRunJXtL1mqASg", "name": "CNL QUICKBOOKS" } } }, { "parameters": { "jsCode": "const MAX_RETRIES = 10;\nconst ctx = $('Set Next DocNumber (CNL)').item.json;\nconst createResponse = $input.first().json;\n\n// --- Case 1: Success ---\nif (!createResponse.error && createResponse.Invoice?.Id) {\n const invoiceId = String(createResponse.Invoice.Id);\n const docNumber = String(createResponse.Invoice.DocNumber || '');\n\n return [{\n json: {\n ...ctx,\n qb_action: 'created',\n status: 'success',\n created_invoice_id: invoiceId,\n generated_invoice_number: docNumber,\n current_doc_number: docNumber,\n http_status: 200\n }\n }];\n}\n\n// --- Case 2: Duplicate Document Number ---\nconst rawBody = createResponse?.error?.response?.body\n || createResponse?.error?.body\n || createResponse?.error?.description \n || createResponse?.error?.message\n || '';\nconst bodyText = typeof rawBody === 'string' ? rawBody : JSON.stringify(rawBody);\n\nif (bodyText.includes('Duplicate Document Number Error') || bodyText.includes('code\":\"6140')) {\n \n // 1. Extract the exact DocNumber QuickBooks complained about\n const match = bodyText.match(/DocNumber=([A-Z0-9]+)/i);\n const conflictingDoc = match ? match[1] : null;\n\n if (!conflictingDoc) {\n return [{\n json: {\n ...ctx,\n qb_action: 'error',\n status: 'error',\n message: `Duplicate but could not parse DocNumber from error.`,\n http_status: 409\n }\n }];\n }\n\n // 2. Parse the prefix and the numeric part\n const docMatch = conflictingDoc.match(/^([A-Za-z]*)(\\d+)$/);\n if (!docMatch) {\n return [{\n json: {\n ...ctx,\n qb_action: 'error',\n status: 'error',\n message: `Duplicate but could not parse prefix/number: ${conflictingDoc}`,\n http_status: 409\n }\n }];\n }\n\n const [, prefix, numStr] = docMatch;\n \n // 3. Calculate retry count statelessly using math\n const startDocMatch = (ctx.generated_invoice_number || '').match(/^([A-Za-z]*)(\\d+)$/);\n let retryCount = 0;\n if (startDocMatch) {\n retryCount = parseInt(numStr, 10) - parseInt(startDocMatch[2], 10);\n }\n\n // 4. Check limit\n if (retryCount >= MAX_RETRIES) {\n return [{\n json: {\n ...ctx,\n qb_action: 'error',\n status: 'error',\n message: `Failed after ${retryCount} attempts. Last conflict: ${conflictingDoc}`,\n http_status: 409\n }\n }];\n }\n\n // 5. Generate NEXT doc number\n const nextNum = String(parseInt(numStr, 10) + 1).padStart(numStr.length, '0');\n const nextDoc = `${prefix}${nextNum}`;\n\n // Return item back to the loop WITH the explicit retry flag\n return [{\n json: {\n ...ctx,\n qb_action: 'retry', // <--- This explicitly tells the router to loop\n current_doc_number: nextDoc,\n retry_count: retryCount + 1,\n last_error: `Duplicate: tried ${conflictingDoc}, now trying ${nextDoc}`\n }\n }];\n}\n\n// --- Case 3: Other error ---\nreturn [{\n json: {\n ...ctx,\n qb_action: 'error',\n status: 'error',\n message: `QB create failed: ${bodyText.slice(0, 500)}`,\n http_status: createResponse?.error?.statusCode || 500\n }\n}];" }, "id": "9b31009f-6aff-4eac-a6bd-5fa51ccde543", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2288, 1760 ], "name": "Check Result (CNL)" }, { "parameters": { "jsCode": "const now = new Date().toISOString();\nconst ctx = $input.first()?.json ?? {};\nconst invoiceNumber = String(ctx.invoice_number ?? '').trim();\nconst invoiceId = String(ctx.invoice_id ?? '').trim();\nconst invoiceLink = String(ctx.invoice_link ?? '').trim();\nconst rawInvoicePdf = String(ctx.pdf_base64 ?? ctx.qb_invoice_pdf ?? '').trim();\nconst invoicePdf = rawInvoicePdf.startsWith('http://') || rawInvoicePdf.startsWith('https://') || rawInvoicePdf.startsWith('data:')\n ? rawInvoicePdf\n : (rawInvoicePdf ? `data:application/pdf;base64,${rawInvoicePdf}` : '');\nconst billingCompany = String(ctx.billing_company ?? '').trim();\nconst invoiceCompany = String(ctx.invoice_company ?? ctx.company ?? '').trim();\nconst sentById = String(ctx.sent_by_id ?? '').trim();\nconst sentByName = String(ctx.sent_by_name ?? '').trim();\nconst orderItemIds = Array.isArray(ctx.order_item_ids)\n ? ctx.order_item_ids.map((value) => String(value ?? '').trim()).filter(Boolean)\n : [];\nconst invoiceDate = String(ctx.qb_invoice_date ?? '').trim() || now.slice(0, 10);\n\nif (!invoiceNumber || orderItemIds.length === 0) {\n return [{\n json: {\n ...ctx,\n status: 'error',\n message: 'Missing invoice write-back fields',\n http_status: 500\n }\n }];\n}\n\nreturn orderItemIds.map((orderItemId) => {\n const pbPatch = {\n item_status: 'invoiced',\n order_item_details: {\n invoice: {\n number: invoiceNumber,\n date: invoiceDate,\n setAt: now,\n setBy: sentById,\n setByName: sentByName,\n billingCompany,\n invoiceCompany,\n qbInvoiceId: invoiceId,\n qbInvoiceLink: invoiceLink\n }\n }\n };\n\n if (invoicePdf) {\n pbPatch.qb_invoice_pdf = invoicePdf;\n }\n\n return {\n json: {\n ...ctx,\n order_item_id: orderItemId,\n pb_patch: pbPatch\n }\n };\n});" }, "id": "ea386eb1-1f77-468b-a67a-e7e8390057b7", "name": "Build PocketBase Invoice Update", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 3824, 1712 ] }, { "parameters": { "method": "PATCH", "url": "=https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io/api/collections/order_items/records/{{ $('Build PocketBase Invoice Update').item.json.order_item_id }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" }, { "name": "Authorization", "value": "=Bearer {{ $json.token }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ $('Build PocketBase Invoice Update').item.json.pb_patch }}", "options": { "timeout": 15000 } }, "id": "28ec9f65-8a1b-4d53-8a2b-dbf75409aefc", "name": "Update Order Item In PocketBase", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 4272, 1712 ], "continueOnFail": true }, { "parameters": { "jsCode": "const inputs = $input.all();\nconst sourceItems = $('Build PocketBase Invoice Update').all();\nconst firstSource = sourceItems[0]?.json ?? {};\n\nif (sourceItems.length === 1 && firstSource.status === 'error' && !firstSource.order_item_id && !firstSource.pb_patch) {\n return [{ json: firstSource }];\n}\n\nif (inputs.length === 0) {\n return [{\n json: {\n ...firstSource,\n status: 'error',\n message: 'PocketBase write-back returned no items',\n http_status: 500\n }\n }];\n}\n\nconst failures = [];\nfor (let index = 0; index < inputs.length; index += 1) {\n const payload = inputs[index]?.json ?? {};\n const source = sourceItems[index]?.json ?? firstSource;\n if (payload.error) {\n const details = payload.error?.message || payload.error?.description || payload.error?.error || JSON.stringify(payload.error);\n failures.push(`${source.order_item_id || 'unknown'}: ${String(details).slice(0, 250)}`);\n }\n}\n\nif (failures.length > 0) {\n return [{\n json: {\n ...firstSource,\n status: 'error',\n message: `PocketBase write-back failed for ${failures.length} item(s): ${failures.join('; ')}`,\n http_status: 500\n }\n }];\n}\n\nreturn [{\n json: {\n status: 'success',\n invoice_number: String(firstSource.invoice_number ?? ''),\n invoice_id: String(firstSource.invoice_id ?? ''),\n invoice_link: String(firstSource.invoice_link ?? ''),\n qb_invoice_date: String(firstSource.qb_invoice_date ?? ''),\n invoice_company: String(firstSource.invoice_company ?? firstSource.company ?? ''),\n company: String(firstSource.invoice_company ?? firstSource.company ?? ''),\n order_number: String(firstSource.order_number ?? ''),\n billing_company: String(firstSource.billing_company ?? ''),\n order_item_ids: Array.isArray(firstSource.order_item_ids) ? firstSource.order_item_ids : [],\n sent_by_id: String(firstSource.sent_by_id ?? ''),\n sent_by_name: String(firstSource.sent_by_name ?? ''),\n line_count: Number(firstSource.line_count ?? 0),\n pdf_base64: firstSource.pdf_base64 ?? null,\n pocketbase_updated_count: inputs.length\n }\n}];" }, "id": "a6eb8dde-0859-4560-8a9f-109710c559b9", "name": "Finalize PocketBase Invoice Update", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 4512, 1712 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "id": "pb-writeback-success-check", "leftValue": "={{ $json.status }}", "rightValue": "success", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "id": "dbffb28a-2c20-4711-b24d-a3fd8c37dc17", "name": "PocketBase Write-Back Is Success?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 4896, 1872 ] }, { "parameters": { "jsCode": "const pb = await this.helpers.httpRequest({\n method: 'POST',\n url: `https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io/api/collections/_superusers/auth-with-password`,\n headers: { 'Content-Type': 'application/json' },\n body: { identity: 'david.els1@outlook.com', password: '1234567890' },\n json: true,\n});\nreturn [{ json: { token: pb.token, _pb: pb } }];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 4064, 1712 ], "id": "dfe15a3c-eb7f-4d33-b2af-da49c9430547", "name": "GET PB TOKEN" } ], "pinData": { "Webhook Trigger": [ { "json": { "headers": { "host": "n8nmac.apphub.co.za", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5.2 Safari/605.1.15", "content-length": "332", "accept": "*/*", "accept-encoding": "gzip, deflate, br, zstd", "accept-language": "en-US,en;q=0.9", "content-type": "application/json", "origin": "http://localhost:4000", "priority": "u=3, i", "referer": "http://localhost:4000/", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "cross-site", "x-forwarded-for": "196.39.29.2", "x-forwarded-host": "n8nmac.apphub.co.za", "x-forwarded-port": "443", "x-forwarded-proto": "https", "x-forwarded-server": "aa6e8c40473e", "x-real-ip": "196.39.29.2" }, "params": {}, "query": {}, "body": { "order_number": "TEST COLLECION POOL", "billing_company": "CNL", "invoice_company": "Adal Engineering", "customer_id": "236", "customer_name": "Adal Engineering", "items": [ { "name": "Welding rod 3.2mm E6013 5kg pack mild steel", "qty": 1, "price": 0 } ], "order_item_ids": [ "m74xvviv2hpzz2d" ], "sent_by_id": "k045yfeze92ckx9", "sent_by_name": "David Els" }, "webhookUrl": "https://n8nmac.apphub.co.za/webhook/wfdash-quickbooks-invoice-creator", "executionMode": "production" }, "pairedItem": { "item": 0 } } ] }, "connections": { "Webhook Trigger": { "main": [ [ { "node": "Normalize Request", "type": "main", "index": 0 } ] ] }, "Normalize Request": { "main": [ [ { "node": "Is Authorized?", "type": "main", "index": 0 } ] ] }, "Is Authorized?": { "main": [ [ { "node": "Validate Payload", "type": "main", "index": 0 } ], [ { "node": "Unauthorized Response", "type": "main", "index": 0 } ] ] }, "Unauthorized Response": { "main": [ [ { "node": "Respond Error", "type": "main", "index": 0 } ] ] }, "Validate Payload": { "main": [ [ { "node": "Is Payload Valid?", "type": "main", "index": 0 } ] ] }, "Is Payload Valid?": { "main": [ [ { "node": "If2", "type": "main", "index": 0 } ], [ { "node": "Validation Error Response", "type": "main", "index": 0 } ] ] }, "Validation Error Response": { "main": [ [ { "node": "Respond Error", "type": "main", "index": 0 } ] ] }, "Mapping Error Response": { "main": [ [ { "node": "Respond Error", "type": "main", "index": 0 } ] ] }, "Set CNL Context": { "main": [ [ { "node": "QB Context", "type": "main", "index": 0 } ] ] }, "Set ISHERWOOD Context": { "main": [ [ { "node": "QB Context", "type": "main", "index": 0 } ] ] }, "QB Context": { "main": [ [ { "node": "Route Search Company", "type": "main", "index": 0 } ] ] }, "Decide Invoice Action": { "main": [ [ { "node": "Action Is Error?", "type": "main", "index": 0 } ] ] }, "Action Is Error?": { "main": [ [ { "node": "Build QB Error Response", "type": "main", "index": 0 } ], [ { "node": "Action Is Existing?", "type": "main", "index": 0 } ] ] }, "Action Is Existing?": { "main": [ [ { "node": "Build Existing Response", "type": "main", "index": 0 } ], [ { "node": "Route Create Company", "type": "main", "index": 0 } ] ] }, "Build Existing Response": { "main": [ [ { "node": "Respond Success", "type": "main", "index": 0 } ] ] }, "Build Created Response": { "main": [ [ { "node": "Created Is Success?", "type": "main", "index": 0 } ] ] }, "Created Is Success?": { "main": [ [ { "node": "Build PocketBase Invoice Update", "type": "main", "index": 0 } ], [ { "node": "Respond Error", "type": "main", "index": 0 } ] ] }, "Build QB Error Response": { "main": [ [ { "node": "Respond Error", "type": "main", "index": 0 } ] ] }, "GET ISHERWOOD CUSTOMERS1": { "main": [ [ { "node": "Fuzzy Match Customers", "type": "main", "index": 0 } ] ] }, "GET CNL CUSTOMERS1": { "main": [ [ { "node": "Fuzzy Match Customers", "type": "main", "index": 0 } ] ] }, "GET CUSTOMERS WEBHOOK": { "main": [ [ { "node": "Normalize Customer Search Request", "type": "main", "index": 0 } ] ] }, "Normalize Customer Search Request": { "main": [ [ { "node": "Is Customer Authorized?", "type": "main", "index": 0 } ] ] }, "Is Customer Authorized?": { "main": [ [ { "node": "Validate Customer Search", "type": "main", "index": 0 } ], [ { "node": "Build Customer Unauthorized", "type": "main", "index": 0 } ] ] }, "Build Customer Unauthorized": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] }, "Validate Customer Search": { "main": [ [ { "node": "Is Customer Search Valid?", "type": "main", "index": 0 } ] ] }, "Is Customer Search Valid?": { "main": [ [ { "node": "If1", "type": "main", "index": 0 } ], [ { "node": "Build Customer Validation Error", "type": "main", "index": 0 } ] ] }, "Build Customer Validation Error": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] }, "Build Customer Mapping Error": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] }, "Fuzzy Match Customers": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] }, "If1": { "main": [ [ { "node": "GET ISHERWOOD CUSTOMERS1", "type": "main", "index": 0 } ], [ { "node": "GET CNL CUSTOMERS1", "type": "main", "index": 0 } ] ] }, "If2": { "main": [ [ { "node": "Set CNL Context", "type": "main", "index": 0 } ], [ { "node": "Set ISHERWOOD Context", "type": "main", "index": 0 } ] ] }, "Route Search Company": { "main": [ [ { "node": "Search Existing Invoice (CNL)", "type": "main", "index": 0 } ], [ { "node": "Search Existing Invoice (ISHERWOOD)", "type": "main", "index": 0 } ] ] }, "Search Existing Invoice (CNL)": { "main": [ [ { "node": "Decide Invoice Action", "type": "main", "index": 0 } ] ] }, "Search Existing Invoice (ISHERWOOD)": { "main": [ [ { "node": "Decide Invoice Action", "type": "main", "index": 0 } ] ] }, "Route Create Company": { "main": [ [ { "node": "Get Last Invoices (CNL)", "type": "main", "index": 0 } ], [ { "node": "Get Last Invoices (ISHERWOOD)", "type": "main", "index": 0 } ] ] }, "Get Last Invoices (CNL)": { "main": [ [ { "node": "Set Next DocNumber (CNL)", "type": "main", "index": 0 } ] ] }, "Get Last Invoices (ISHERWOOD)": { "main": [ [ { "node": "Set Next DocNumber (ISHERWOOD)", "type": "main", "index": 0 } ] ] }, "Extract from File": { "main": [ [ { "node": "Build Created Response", "type": "main", "index": 0 } ] ] }, "Extract from File1": { "main": [ [ { "node": "Build Created Response", "type": "main", "index": 0 } ] ] }, "Set Next DocNumber (ISHERWOOD)": { "main": [ [ { "node": "Create Invoice (ISHERWOOD)", "type": "main", "index": 0 } ] ] }, "Create Invoice (ISHERWOOD)": { "main": [ [ { "node": "Check Result (ISHERWOOD)", "type": "main", "index": 0 } ] ] }, "Check Result (ISHERWOOD)": { "main": [ [ { "node": "Retry Router (ISHERWOOD)", "type": "main", "index": 0 } ] ] }, "Retry Router (ISHERWOOD)": { "main": [ [ { "node": "Download QBO Invoice", "type": "main", "index": 0 } ], [ { "node": "Retry Router 2 (ISHERWOOD)", "type": "main", "index": 0 } ] ] }, "Set Next DocNumber (CNL)": { "main": [ [ { "node": "Create Invoice (CNL)", "type": "main", "index": 0 } ] ] }, "Create Invoice (CNL)": { "main": [ [ { "node": "Check Result (CNL)", "type": "main", "index": 0 } ] ] }, "Retry Router (CNL)": { "main": [ [ { "node": "Download QBO Invoice1", "type": "main", "index": 0 } ], [ { "node": "Retry Router 2 (CNL)", "type": "main", "index": 0 } ] ] }, "Retry Router 2 (ISHERWOOD)": { "main": [ [ { "node": "Respond Error", "type": "main", "index": 0 } ], [ { "node": "Create Invoice (ISHERWOOD)", "type": "main", "index": 0 } ] ] }, "Retry Router 2 (CNL)": { "main": [ [ { "node": "Respond Error", "type": "main", "index": 0 } ], [ { "node": "Create Invoice (CNL)", "type": "main", "index": 0 } ] ] }, "Download QBO Invoice": { "main": [ [ { "node": "Extract from File", "type": "main", "index": 0 } ] ] }, "Download QBO Invoice1": { "main": [ [ { "node": "Extract from File1", "type": "main", "index": 0 } ] ] }, "Check Result (CNL)": { "main": [ [ { "node": "Retry Router (CNL)", "type": "main", "index": 0 } ] ] }, "Build PocketBase Invoice Update": { "main": [ [ { "node": "GET PB TOKEN", "type": "main", "index": 0 } ] ] }, "Update Order Item In PocketBase": { "main": [ [ { "node": "Finalize PocketBase Invoice Update", "type": "main", "index": 0 } ] ] }, "Finalize PocketBase Invoice Update": { "main": [ [ { "node": "PocketBase Write-Back Is Success?", "type": "main", "index": 0 } ] ] }, "PocketBase Write-Back Is Success?": { "main": [ [ { "node": "Respond Success", "type": "main", "index": 0 } ], [ { "node": "Build QB Error Response", "type": "main", "index": 0 } ] ] }, "GET PB TOKEN": { "main": [ [ { "node": "Update Order Item In PocketBase", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1", "binaryMode": "separate" }, "versionId": "f4ac9fe4-36c4-4491-8f31-e7c0b4003b72", "meta": { "instanceId": "375b0a7073a999c2b9437a1ec848664e8305503237ae3684ba9dca3ce53ea61b" }, "nodeGroups": [], "id": "S2GlXGj4Zdi1Zd0R", "tags": [] }