Add complete end-to-end QuickBooks invoicing workflow for the invoicing page: - Add `jspdf` dependency for client-side PDF generation - Implement updated item status handling with fallback for new invoice statuses - Add server-side proxy API for secure QuickBooks customer lookup - Build utilities to combine QuickBooks invoices and picking slips into a single PDF - Add invoicing view models for row grouping, payload construction, and status checks - Add full test coverage for all new components, APIs, and utilities - Include feature design documentation and sample n8n workflow config
2541 lines
85 KiB
JSON
2541 lines
85 KiB
JSON
{
|
|
"name": "1 - WFDASH QUICKBOOKS TEST_NEXTJS",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": "wfdash-quickbooks-invoice-creator",
|
|
"responseMode": "responseNode",
|
|
"options": {}
|
|
},
|
|
"id": "webhook-trigger",
|
|
"name": "Webhook Trigger",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 2.1,
|
|
"position": [
|
|
-2880,
|
|
-1168
|
|
],
|
|
"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": "normalize-request",
|
|
"name": "Normalize Request",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-2656,
|
|
-1168
|
|
]
|
|
},
|
|
{
|
|
"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": "is-authorized",
|
|
"name": "Is Authorized?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-2432,
|
|
-1168
|
|
]
|
|
},
|
|
{
|
|
"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": "unauthorized-response",
|
|
"name": "Unauthorized Response",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
2048,
|
|
-464
|
|
]
|
|
},
|
|
{
|
|
"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": "validate-payload",
|
|
"name": "Validate Payload",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-2208,
|
|
-1264
|
|
]
|
|
},
|
|
{
|
|
"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": "is-payload-valid",
|
|
"name": "Is Payload Valid?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1984,
|
|
-1264
|
|
]
|
|
},
|
|
{
|
|
"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": "validation-response",
|
|
"name": "Validation Error Response",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
2048,
|
|
-656
|
|
]
|
|
},
|
|
{
|
|
"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": "set-cnl-context",
|
|
"name": "Set CNL Context",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
-1536,
|
|
-1456
|
|
]
|
|
},
|
|
{
|
|
"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": "set-isherwood-context",
|
|
"name": "Set ISHERWOOD Context",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
-1536,
|
|
-832
|
|
]
|
|
},
|
|
{
|
|
"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": "mapping-error-response",
|
|
"name": "Mapping Error Response",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
2048,
|
|
-272
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "return [{ json: { ...$json } }];"
|
|
},
|
|
"id": "qb-context",
|
|
"name": "QB Context",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1312,
|
|
-1360
|
|
]
|
|
},
|
|
{
|
|
"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": "search-existing-invoice",
|
|
"name": "Search Existing Invoice (CNL)",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
-864,
|
|
-1456
|
|
],
|
|
"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) + '\u2026' : 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": "decide-invoice-action",
|
|
"name": "Decide Invoice Action",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-640,
|
|
-1360
|
|
]
|
|
},
|
|
{
|
|
"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": "is-action-error",
|
|
"name": "Action Is Error?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-416,
|
|
-1360
|
|
]
|
|
},
|
|
{
|
|
"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": "is-action-existing",
|
|
"name": "Action Is Existing?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-192,
|
|
-1520
|
|
]
|
|
},
|
|
{
|
|
"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": "create-invoice",
|
|
"name": "Create Invoice (CNL)",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
704,
|
|
-1600
|
|
],
|
|
"credentials": {
|
|
"quickBooksOAuth2Api": {
|
|
"id": "BPkRunJXtL1mqASg",
|
|
"name": "CNL QUICKBOOKS"
|
|
}
|
|
},
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const ctx = $('QB Context').item.json;\nconst sourceItem = $input.first();\nconst sourceBinary = sourceItem?.binary ?? {};\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// Keep the base64 copy for compatibility/debugging and preserve the binary PDF for PocketBase upload.\nconst pdfBase64 = $json.pdf_base64 || null;\n\nconsole.log(JSON.stringify({\n event: 'wfdash.invoice.created',\n order_number: ctx.order_number,\n invoice_id: invoiceId,\n has_pdf: !!pdfBase64,\n has_binary_pdf: Object.keys(sourceBinary).length > 0\n}));\n\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 binary: sourceBinary\n}];"
|
|
},
|
|
"id": "build-created-response",
|
|
"name": "Build Created Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1824,
|
|
-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": "build-existing-response",
|
|
"name": "Build Existing Response",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
2048,
|
|
-1904
|
|
]
|
|
},
|
|
{
|
|
"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": "build-qb-error-response",
|
|
"name": "Build QB Error Response",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
2048,
|
|
-848
|
|
]
|
|
},
|
|
{
|
|
"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": "is-created-success",
|
|
"name": "Created Is Success?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2048,
|
|
-1712
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "json",
|
|
"responseBody": "={{ $json }}",
|
|
"options": {
|
|
"responseCode": 200
|
|
}
|
|
},
|
|
"id": "respond-success",
|
|
"name": "Respond Success",
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
2272,
|
|
-1856
|
|
]
|
|
},
|
|
{
|
|
"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": "respond-error",
|
|
"name": "Respond Error",
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
2272,
|
|
-848
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "getAll",
|
|
"returnAll": true,
|
|
"filters": {}
|
|
},
|
|
"type": "n8n-nodes-base.quickbooks",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1600,
|
|
-3376
|
|
],
|
|
"id": "5fa878f4-506c-429a-8d24-0c28c009dcfb",
|
|
"name": "GET ISHERWOOD CUSTOMERS1",
|
|
"credentials": {
|
|
"quickBooksOAuth2Api": {
|
|
"id": "qIOWSWNJQneDXGmH",
|
|
"name": "ISHERWOODMINING"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "getAll",
|
|
"returnAll": true,
|
|
"filters": {}
|
|
},
|
|
"type": "n8n-nodes-base.quickbooks",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1600,
|
|
-3184
|
|
],
|
|
"id": "94459bb4-08be-40e6-8956-22f78bbb98c2",
|
|
"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": [
|
|
-2944,
|
|
-3088
|
|
],
|
|
"id": "d8ceb8b9-5b9e-4b7a-a74b-1ffc28eaec58",
|
|
"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": [
|
|
-1152,
|
|
-2912
|
|
],
|
|
"id": "07dad867-382e-4764-b486-36868a3dae99",
|
|
"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": "normalize-customer-search-request",
|
|
"name": "Normalize Customer Search Request",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-2720,
|
|
-3088
|
|
]
|
|
},
|
|
{
|
|
"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": "is-customer-authorized",
|
|
"name": "Is Customer Authorized?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-2496,
|
|
-3088
|
|
]
|
|
},
|
|
{
|
|
"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": "build-customer-unauthorized",
|
|
"name": "Build Customer Unauthorized",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
-1376,
|
|
-2816
|
|
]
|
|
},
|
|
{
|
|
"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": "validate-customer-search",
|
|
"name": "Validate Customer Search",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-2272,
|
|
-3184
|
|
]
|
|
},
|
|
{
|
|
"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": "is-customer-search-valid",
|
|
"name": "Is Customer Search Valid?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-2048,
|
|
-3184
|
|
]
|
|
},
|
|
{
|
|
"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": "build-customer-validation-error",
|
|
"name": "Build Customer Validation Error",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
-1376,
|
|
-3008
|
|
]
|
|
},
|
|
{
|
|
"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": "build-customer-mapping-error",
|
|
"name": "Build Customer Mapping Error",
|
|
"type": "n8n-nodes-base.set",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
-1376,
|
|
-2624
|
|
]
|
|
},
|
|
{
|
|
"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": "fuzzy-match-customers",
|
|
"name": "Fuzzy Match Customers",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1376,
|
|
-3280
|
|
]
|
|
},
|
|
{
|
|
"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": [
|
|
-1824,
|
|
-3280
|
|
],
|
|
"id": "31148170-810d-482a-baa0-f1cc40f18833",
|
|
"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": [
|
|
-1760,
|
|
-1360
|
|
],
|
|
"id": "39fb2218-75d4-4009-8528-3700814d63f9",
|
|
"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": "route-search-company",
|
|
"name": "Route Search Company",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1088,
|
|
-1360
|
|
]
|
|
},
|
|
{
|
|
"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": "search-existing-invoice-isherwood",
|
|
"name": "Search Existing Invoice (ISHERWOOD)",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
-864,
|
|
-832
|
|
],
|
|
"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": "route-create-company",
|
|
"name": "Route Create Company",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
32,
|
|
-1424
|
|
]
|
|
},
|
|
{
|
|
"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": "get-last-invoices-cnl",
|
|
"name": "Get Last Invoices (CNL)",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
256,
|
|
-1600
|
|
],
|
|
"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": "set-next-docnumber-cnl",
|
|
"name": "Set Next DocNumber (CNL)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
480,
|
|
-1600
|
|
]
|
|
},
|
|
{
|
|
"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": "get-last-invoices-isherwood",
|
|
"name": "Get Last Invoices (ISHERWOOD)",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
256,
|
|
-1216
|
|
],
|
|
"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": "set-next-docnumber-isherwood",
|
|
"name": "Set Next DocNumber (ISHERWOOD)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
480,
|
|
-1216
|
|
]
|
|
},
|
|
{
|
|
"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": "create-invoice-isherwood",
|
|
"name": "Create Invoice (ISHERWOOD)",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
704,
|
|
-1216
|
|
],
|
|
"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": [
|
|
1600,
|
|
-1040
|
|
],
|
|
"id": "94d429f7-f060-4a61-9bd5-94f6886844cb",
|
|
"name": "Extract from File"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "binaryToPropery",
|
|
"destinationKey": "pdf_base64",
|
|
"options": {
|
|
"encoding": "base64",
|
|
"keepSource": "both"
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1600,
|
|
-1712
|
|
],
|
|
"id": "c6147990-98d5-4a6f-9e1d-3d01af6b6b69",
|
|
"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": "check-result-ish-new",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
928,
|
|
-1232
|
|
],
|
|
"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": "if-retry-ish",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1152,
|
|
-1232
|
|
],
|
|
"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": "if-retry-cnl",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1152,
|
|
-1664
|
|
],
|
|
"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": "retry-router-ish-2",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1376,
|
|
-1232
|
|
],
|
|
"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": "retry-router-cnl-2",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1376,
|
|
-1504
|
|
],
|
|
"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": "a3b9568a-07b6-42f3-a941-d074be451af1",
|
|
"name": "Download QBO Invoice",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
1376,
|
|
-1040
|
|
],
|
|
"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": "bef1d237-4268-40be-943d-5d3c2995db6c",
|
|
"name": "Download QBO Invoice1",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
1376,
|
|
-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": "51458779-0364-4938-97a8-a3eeb08e0aaf",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
928,
|
|
-1664
|
|
],
|
|
"name": "Check Result (CNL)"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"mode": "runOnceForAllItems",
|
|
"jsCode": "const now = new Date().toISOString();\nconst sourceItem = $input.first();\nconst ctx = sourceItem?.json ?? {};\nconst sourceBinary = sourceItem?.binary ?? {};\nconst invoiceNumber = String(ctx.invoice_number ?? '').trim();\nconst invoiceId = String(ctx.invoice_id ?? '').trim();\nconst invoiceLink = String(ctx.invoice_link ?? '').trim();\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\nconst binaryKeys = Object.keys(sourceBinary);\nconst rawBinary = sourceBinary.qb_invoice_pdf ?? sourceBinary.data ?? (binaryKeys.length > 0 ? sourceBinary[binaryKeys[0]] : null);\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\nif (!rawBinary?.data) {\n return [{\n json: {\n ...ctx,\n status: 'error',\n message: 'Missing invoice PDF binary for PocketBase upload',\n http_status: 500\n }\n }];\n}\n\nconst uploadBinary = {\n ...rawBinary,\n mimeType: rawBinary.mimeType || 'application/pdf',\n fileName: `invoice_${invoiceNumber || 'invoice'}.pdf`\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 return {\n json: {\n ...ctx,\n order_item_id: orderItemId,\n pb_patch: pbPatch\n },\n binary: {\n qb_invoice_pdf: uploadBinary\n }\n };\n});"
|
|
},
|
|
"id": "build-pocketbase-invoice-update",
|
|
"name": "Build PocketBase Invoice Update",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2272,
|
|
-1600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "PATCH",
|
|
"url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/collections/order_items/records/{{ $json.order_item_id }}",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{ $env.PB_ADMIN_TOKEN }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ $json.pb_patch }}",
|
|
"options": {
|
|
"timeout": 15000
|
|
}
|
|
},
|
|
"id": "update-order-item-pocketbase",
|
|
"name": "Update Order Item In PocketBase",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2496,
|
|
-1600
|
|
],
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"mode": "runOnceForAllItems",
|
|
"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": "finalize-pocketbase-invoice-update",
|
|
"name": "Finalize PocketBase Invoice Update",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2720,
|
|
-1600
|
|
]
|
|
},
|
|
{
|
|
"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": "is-pocketbase-writeback-success",
|
|
"name": "PocketBase Write-Back Is Success?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2944,
|
|
-1600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "PATCH",
|
|
"url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/collections/order_items/records/{{ $json.order_item_id }}",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{ $env.PB_ADMIN_TOKEN }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"contentType": "multipart-form-data",
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "qb_invoice_pdf",
|
|
"value": "={{ $binary.qb_invoice_pdf }}"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"timeout": 15000,
|
|
"response": {
|
|
"response": {
|
|
"responseFormat": "json"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"id": "upload-invoice-pdf-pocketbase",
|
|
"name": "Upload Invoice PDF To PocketBase",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.4,
|
|
"position": [
|
|
2496,
|
|
-1824
|
|
],
|
|
"continueOnFail": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"mode": "runOnceForAllItems",
|
|
"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 invoice PDF upload 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 invoice PDF upload failed for ${failures.length} item(s): ${failures.join('; ')}`,\n http_status: 500\n }\n }];\n}\n\nreturn sourceItems.map((item) => ({\n json: item.json\n}));"
|
|
},
|
|
"id": "finalize-invoice-pdf-upload",
|
|
"name": "Finalize Invoice PDF Upload",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2720,
|
|
-1824
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 1
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "invoice-upload-success-check",
|
|
"leftValue": "={{ $json.status }}",
|
|
"rightValue": "success",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "is-invoice-pdf-upload-success",
|
|
"name": "Invoice PDF Upload Is Success?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2944,
|
|
-1824
|
|
]
|
|
}
|
|
],
|
|
"pinData": {
|
|
"Webhook Trigger": [
|
|
{
|
|
"json": {
|
|
"headers": {
|
|
"host": "n8nmac.apphub.co.za",
|
|
"user-agent": "python-requests/2.31.0",
|
|
"content-length": "240",
|
|
"accept": "*/*",
|
|
"accept-encoding": "gzip, deflate",
|
|
"content-type": "application/json",
|
|
"x-forwarded-for": "196.39.59.39",
|
|
"x-forwarded-host": "n8nmac.apphub.co.za",
|
|
"x-forwarded-port": "443",
|
|
"x-forwarded-proto": "https",
|
|
"x-forwarded-server": "aa6e8c40473e",
|
|
"x-real-ip": "196.39.59.39",
|
|
"x-webhook-token": "mPo6cK0lU1j3+1a07Ike3UTfDQ8TjsufJhccH3hsq3Y="
|
|
},
|
|
"params": {},
|
|
"query": {},
|
|
"body": {
|
|
"company": "Cash Sales",
|
|
"billing_company": "CNL",
|
|
"order_number": "test221111ZZZ",
|
|
"customer_id": "19",
|
|
"customer_name": "Cash Sales",
|
|
"customer_email": "",
|
|
"items": [
|
|
{
|
|
"code": "ITEM-13069",
|
|
"name": "eewrewr",
|
|
"qty": 22,
|
|
"price": 23
|
|
}
|
|
]
|
|
},
|
|
"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": "Upload Invoice PDF To PocketBase",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Upload Invoice PDF To PocketBase": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Finalize Invoice PDF Upload",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Finalize Invoice PDF Upload": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Invoice PDF Upload Is Success?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Invoice PDF Upload Is Success?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Update Order Item In PocketBase",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build QB Error Response",
|
|
"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
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"active": true,
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"binaryMode": "separate",
|
|
"callerPolicy": "workflowsFromSameOwner",
|
|
"availableInMCP": false,
|
|
"timeSavedMode": "fixed",
|
|
"errorWorkflow": "3Q0XR2ltc4XRzeAi"
|
|
},
|
|
"versionId": "4f9a2b85-4a41-47b3-bcd7-191ca61dce30",
|
|
"meta": {
|
|
"templateCredsSetupCompleted": true,
|
|
"instanceId": "375b0a7073a999c2b9437a1ec848664e8305503237ae3684ba9dca3ce53ea61b"
|
|
},
|
|
"nodeGroups": [],
|
|
"id": "DD7q1li7faeFGgVn",
|
|
"tags": [
|
|
{
|
|
"updatedAt": "2026-02-15T18:08:23.959Z",
|
|
"createdAt": "2026-02-15T18:08:23.959Z",
|
|
"id": "hrH7nvLe4XMvZpEw",
|
|
"name": "the one"
|
|
}
|
|
]
|
|
}
|