{
  "name": "Turn a Won Deal into a Draft Invoice | Uptune",
  "nodes": [
    {
      "parameters": {
        "content": "## Turn a Won Deal into a Draft Invoice\nConvert the agreed products on a won Pipedrive deal into a Xero draft and send a finance reviewer the approval request.\n\nRun **Run with sample data** first. **Setup** defaults to dry run and uses the sample fixture without contacting connected apps. Configure the accounts and destinations in README.md before switching modes.",
        "width": 450,
        "height": 235
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -390
      ],
      "id": "26b8e4bb-d92c-5e1e-9eaa-ef9f6ffa3fc9",
      "name": "Start here"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -720,
        80
      ],
      "id": "a5b9acf1-5530-519b-b4b4-61e0fa710eff",
      "name": "Run with sample data"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nreturn [{json: {sampleRun: true, body: {\"dealId\": \"1042\", \"xeroContactId\": \"c52ed9ef-953c-4ba3-881d-86be2d6d6d89\"}}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -480,
        80
      ],
      "id": "b0e9f276-309f-57d7-9bb3-aea1f07ba945",
      "name": "Sample input"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "uptune-won-deal-draft-invoice",
        "authentication": "headerAuth",
        "responseMode": "onReceived",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -480,
        300
      ],
      "id": "beff172f-d1e8-5839-81d2-53a26c44578b",
      "name": "Receive event",
      "webhookId": "beff172f-d1e8-5839-81d2-53a26c44578b"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst config = {\n  \"dryRun\": true,\n  \"pipedriveSubdomain\": \"\",\n  \"xeroTenantId\": \"\",\n  \"xeroAccountCode\": \"\",\n  \"xeroTaxType\": \"\",\n  \"taxBasis\": \"exclusive\",\n  \"invoicePrefix\": \"UPT-PD\",\n  \"paymentTermsDays\": 30,\n  \"financeReviewerId\": \"\",\n  \"authorizeAfterApproval\": false,\n  \"emailAfterAuthorization\": false\n};\nreturn [{json: {...input, config}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        150
      ],
      "id": "5bf91550-09ed-5dec-8c4c-6f903f61ec5b",
      "name": "Setup"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst body = input.body || {};\nconst cfg = input.config;\nconst dealId = String(body.dealId ?? '').trim();\nconst contactId = String(body.xeroContactId ?? '').trim().toLowerCase();\nconst preview = input.config.dryRun !== false || input.sampleRun === true;\nif (!/^[1-9]\\d{0,14}$/.test(dealId) || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(contactId)) throw new Error('Supply a Pipedrive deal ID and a mapped Xero contact UUID.');\nif (!/^[A-Z][A-Z0-9-]{1,19}$/.test(cfg.invoicePrefix) || !Number.isInteger(cfg.paymentTermsDays) || cfg.paymentTermsDays < 0 || cfg.paymentTermsDays > 365 ||\n    !['exclusive','inclusive','none'].includes(cfg.taxBasis)) throw new Error('Check the invoice prefix, payment terms and tax basis.');\nif (!preview && (!/^[a-z0-9-]{2,63}$/.test(cfg.pipedriveSubdomain) || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(cfg.xeroTenantId) ||\n    !cfg.xeroAccountCode || !cfg.xeroTaxType || !/^[UW][A-Z0-9]{5,20}$/.test(cfg.financeReviewerId))) throw new Error('Complete the Pipedrive, Xero and Slack fields in Setup.');\nreturn [{json:{dealId,contactId,preview,invoiceNumber:cfg.invoicePrefix+'-'+dealId}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -75,
        150
      ],
      "id": "f5f66044-983f-5d87-b4ed-9f4e1af90fad",
      "name": "Validate invoice intake"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "3a9cb054-6bc8-5868-a4de-73c0dd0fb431",
              "leftValue": "={{ !$json.preview }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        160,
        150
      ],
      "id": "770b5cd1-06a0-581e-82c4-a42e9f6e0155",
      "name": "Read live invoice sources?"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://'+$(\"Setup\").first().json.config.pipedriveSubdomain+'.pipedrive.com/api/v2/deals/'+encodeURIComponent($(\"Validate invoice intake\").first().json.dealId) }}",
        "authentication": "genericCredentialType",
        "options": {
          "timeout": 30000
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        395,
        150
      ],
      "id": "355df7e2-a9aa-5a06-8c1f-31650bba6cd1",
      "name": "Read Pipedrive deal"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst req = $(\"Validate invoice intake\").first().json;\nconst deal = req.preview ? {\"id\": 1042, \"status\": \"won\", \"title\": \"Northstar onboarding\", \"currency\": \"USD\"} : input.data;\nif ((!req.preview && input.success !== true) || !deal || String(deal.id) !== req.dealId || deal.status !== 'won') throw new Error('Pipedrive did not return the requested won deal.');\nconst currency = String(deal.currency || '');\nif (!/^[A-Z]{3}$/.test(currency)) throw new Error('The won deal needs a valid currency.');\nreturn [{json:{...req,currency,title:String(deal.title || '').slice(0,180)}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        630,
        150
      ],
      "id": "42bbf5d3-cbd3-5a0f-aeab-bbc912c03316",
      "name": "Verify deal is won"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "385cb9b0-9484-53e1-bb85-1d2b4f96cb2d",
              "leftValue": "={{ !$json.preview }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        865,
        150
      ],
      "id": "6c29962e-7fe1-55ff-9088-d1b333dc3e49",
      "name": "Read live deal products?"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://'+$(\"Setup\").first().json.config.pipedriveSubdomain+'.pipedrive.com/api/v2/deals/'+encodeURIComponent($(\"Validate invoice intake\").first().json.dealId)+'/products?limit=100' }}",
        "authentication": "genericCredentialType",
        "options": {
          "timeout": 30000
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1100,
        150
      ],
      "id": "4c661783-e0ef-5444-8b6a-873e1e7cc570",
      "name": "Read Pipedrive deal products"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst cfg = $(\"Setup\").first().json.config;\nconst deal = $(\"Verify deal is won\").first().json;\nconst source = deal.preview ? [{\"id\": 1, \"name\": \"Implementation\", \"item_price\": 1200, \"quantity\": 2, \"currency\": \"USD\", \"tax_method\": \"exclusive\", \"discount\": 10, \"discount_type\": \"percentage\", \"is_enabled\": true, \"billing_frequency\": \"one-time\"}] : input.data;\nif ((!deal.preview && (input.success !== true || input.additional_data?.next_cursor)) || !Array.isArray(source)) {\n  throw new Error('Pipedrive must return the complete deal product list within one page.');\n}\nconst products = source.filter(row=>row.is_enabled !== false);\nif (!products.length || products.length > 100) throw new Error('Add between 1 and 100 enabled deal products.');\nconst lines = products.map(row=>{\n  const discount = row.discount ?? 0;\n  if ((row.billing_frequency && row.billing_frequency !== 'one-time') ||\n      (discount !== 0 && row.discount_type !== 'percentage') || !Number.isFinite(discount) || discount < 0 || discount > 100 ||\n      row.currency !== deal.currency || row.tax_method !== cfg.taxBasis || !Number.isFinite(row.item_price) || row.item_price < 0 ||\n      !Number.isFinite(row.quantity) || row.quantity <= 0) throw new Error('Unsupported deal product. Check recurrence, currency, tax basis, price, quantity and discount.');\n  const description = String(row.name || row.product_name || row.comments || '').trim().slice(0,500);\n  if (!description) throw new Error('A deal product has no description.');\n  return {Description:description,Quantity:row.quantity,UnitAmount:row.item_price,DiscountRate:discount,\n    AccountCode:cfg.xeroAccountCode || '200',TaxType:cfg.xeroTaxType || 'OUTPUT'};\n});\nif (lines.every(line=>line.UnitAmount === 0 || line.DiscountRate === 100)) throw new Error('Review zero-value invoices in Xero.');\nconst now = Date.now();\nconst date = new Date(now).toISOString().slice(0,10);\nconst due = new Date(now + cfg.paymentTermsDays*86400000).toISOString().slice(0,10);\nreturn [{json:{preview:deal.preview,invoiceNumber:deal.invoiceNumber,contactId:deal.contactId,\n  payload:{Invoices:[{Type:'ACCREC',Status:'DRAFT',Contact:{ContactID:deal.contactId},InvoiceNumber:deal.invoiceNumber,\n  Reference:'Pipedrive deal '+deal.dealId,Date:date,DueDate:due,CurrencyCode:deal.currency,\n  LineAmountTypes:{exclusive:'Exclusive',inclusive:'Inclusive',none:'NoTax'}[cfg.taxBasis],LineItems:lines}]}}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1335,
        150
      ],
      "id": "5d5c5713-d2aa-5d81-8364-2458480ceb4b",
      "name": "Build Xero invoice"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "5c928f58-090f-5fa2-b556-a08c8d209406",
              "leftValue": "={{ !$json.preview }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1570,
        150
      ],
      "id": "e1fd9118-06c5-5dd5-b3fe-ad6903ac9a7e",
      "name": "Create live Xero draft?"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.xero.com/api.xro/2.0/Contacts/\" + encodeURIComponent($(\"Build Xero invoice\").first().json.contactId) }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "xeroOAuth2Api",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Xero-tenant-id",
              "value": "={{ $(\"Setup\").first().json.config.xeroTenantId }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1805,
        150
      ],
      "id": "1bd87b41-0f68-55cd-b26c-0dabefb0e363",
      "name": "Read mapped Xero contact"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst contact = input.Contacts?.[0];\nif (input.Contacts?.length !== 1 || String(contact?.ContactID || '').toLowerCase() !== $(\"Build Xero invoice\").first().json.contactId || contact.ContactStatus !== 'ACTIVE') {\n  throw new Error('The mapped Xero contact is missing or inactive.');\n}\nreturn [{json:{email:String(contact.EmailAddress || '').trim().toLowerCase()}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2040,
        150
      ],
      "id": "57cd35b4-f8d9-50b1-9211-1a8d7e09d5a5",
      "name": "Verify Xero contact"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.xero.com/api.xro/2.0/Invoices?where=\" + encodeURIComponent('InvoiceNumber==\"' + $(\"Build Xero invoice\").first().json.invoiceNumber + '\"') }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "xeroOAuth2Api",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Xero-tenant-id",
              "value": "={{ $(\"Setup\").first().json.config.xeroTenantId }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        2275,
        150
      ],
      "id": "a2982eb9-6e00-5921-8f1c-772e6d2adcd8",
      "name": "Find invoice by number"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nif (!Array.isArray(input.Invoices) || input.Invoices.some(inv=>inv.InvoiceNumber !== $(\"Build Xero invoice\").first().json.invoiceNumber)) throw new Error('Xero returned an unexpected invoice result.');\nreturn [{json:{exists:input.Invoices.length > 0}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2510,
        150
      ],
      "id": "234dfa43-ba2f-50dd-9870-f5c80e16866a",
      "name": "Check invoice replay"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "d95cfa85-cd8d-5728-9c35-c45c7335372a",
              "leftValue": "={{ !$json.exists }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2745,
        150
      ],
      "id": "ada6bd4d-0451-583b-a167-04e5f4887498",
      "name": "Is this a new invoice?"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "https://api.xero.com/api.xro/2.0/Invoices",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "xeroOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($(\"Build Xero invoice\").first().json.payload) }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Xero-tenant-id",
              "value": "={{ $(\"Setup\").first().json.config.xeroTenantId }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            },
            {
              "name": "Idempotency-Key",
              "value": "={{ 'draft-'+ $(\"Build Xero invoice\").first().json.invoiceNumber }}"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        2980,
        150
      ],
      "id": "fda4fc3e-a71d-558e-ba8e-cfc17636d565",
      "name": "Create Xero draft invoice"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst p = $(\"Build Xero invoice\").first().json;\nconst inv = input.Invoices?.[0];\nif (input.Invoices?.length !== 1 || !inv?.InvoiceID || inv.Status !== 'DRAFT' || inv.HasErrors ||\n    inv.InvoiceNumber !== p.invoiceNumber || String(inv.Contact?.ContactID || '').toLowerCase() !== p.contactId ||\n    inv.CurrencyCode !== p.payload.Invoices[0].CurrencyCode || !Number.isFinite(inv.Total) || !inv.UpdatedDateUTC) {\n  throw new Error('Xero did not confirm the expected draft. Check Xero by invoice number before retrying.');\n}\nreturn [{json:{invoiceId:inv.InvoiceID,invoiceNumber:inv.InvoiceNumber,updated:inv.UpdatedDateUTC,total:inv.Total,\n  currency:inv.CurrencyCode,contactId:p.contactId,email:$(\"Verify Xero contact\").first().json.email}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3215,
        150
      ],
      "id": "fd8a9044-8432-5352-a48d-1b485076c402",
      "name": "Capture draft for review"
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "sendAndWait",
        "select": "user",
        "user": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.financeReviewerId }}",
          "mode": "id"
        },
        "message": "={{ 'Review Xero draft '+$json.invoiceNumber+' for '+$json.currency+' '+$json.total+'. Check line items, tax, contact and payment terms in Xero. https://go.xero.com/AccountsReceivable/View.aspx?InvoiceID='+$json.invoiceId }}",
        "responseType": "approval",
        "approvalOptions": {
          "values": {
            "approvalType": "double",
            "approveLabel": "Approve",
            "disapproveLabel": "Decline"
          }
        },
        "captureResponder": true,
        "approvers": "={{ [$(\"Setup\").first().json.config.financeReviewerId] }}",
        "options": {
          "limitWaitTime": {
            "values": {
              "limitType": "afterTimeInterval",
              "resumeAmount": 2,
              "resumeUnit": "days"
            }
          }
        }
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.7,
      "position": [
        3450,
        150
      ],
      "id": "b9c1f19d-1a6d-5b1f-9818-8504449e2c7f",
      "name": "Ask finance in Slack"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "04e01641-ec85-5be5-8ac4-88b63e448bf4",
              "leftValue": "={{ $json.data?.approved === true && $json.data?.responder?.id === $(\"Setup\").first().json.config.financeReviewerId }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3685,
        150
      ],
      "id": "38dbeb0d-6cef-5c07-8195-470bf7e65792",
      "name": "Finance approved this draft?"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "d3ab5991-e7e8-551d-9eef-4c34586595b4",
              "leftValue": "={{ $(\"Setup\").first().json.config.authorizeAfterApproval === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3920,
        150
      ],
      "id": "34e20b8c-0d0d-586f-a798-4cf5b9e60652",
      "name": "Authorize approved invoice?"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://'+$(\"Setup\").first().json.config.pipedriveSubdomain+'.pipedrive.com/api/v2/deals/'+encodeURIComponent($(\"Validate invoice intake\").first().json.dealId) }}",
        "authentication": "genericCredentialType",
        "options": {
          "timeout": 30000
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        4155,
        150
      ],
      "id": "7a088e07-9518-5381-a221-3f3874d89bdb",
      "name": "Read deal after approval"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst original = $(\"Verify deal is won\").first().json;\nif (input.success !== true || String(input.data?.id) !== original.dealId ||\n    input.data?.status !== 'won' || input.data?.currency !== original.currency) {\n  throw new Error('The approved deal is no longer won in the same currency.');\n}\nreturn [{json:{dealId:original.dealId}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4390,
        150
      ],
      "id": "d0ee5dcc-b67a-5e68-b0f2-e12aa2563b45",
      "name": "Recheck won deal"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.xero.com/api.xro/2.0/Invoices/\" + encodeURIComponent($(\"Capture draft for review\").first().json.invoiceId) }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "xeroOAuth2Api",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Xero-tenant-id",
              "value": "={{ $(\"Setup\").first().json.config.xeroTenantId }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        4625,
        150
      ],
      "id": "10053ed2-da2a-5338-9b7e-44dad6abc9a7",
      "name": "Re-read Xero draft"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst draft = $(\"Capture draft for review\").first().json;\nconst invoice = input.Invoices?.[0];\nif (input.Invoices?.length !== 1 || invoice?.InvoiceID !== draft.invoiceId || invoice.Status !== 'DRAFT' ||\n    invoice.InvoiceNumber !== draft.invoiceNumber || invoice.UpdatedDateUTC !== draft.updated || invoice.Total !== draft.total ||\n    invoice.CurrencyCode !== draft.currency || String(invoice.Contact?.ContactID || '').toLowerCase() !== draft.contactId) {\n  throw new Error('The invoice changed after the approval request. Review the current draft in Xero.');\n}\nreturn [{json:{invoiceId:draft.invoiceId}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4860,
        150
      ],
      "id": "6c880ea4-46d1-54a2-846c-6d5f0f20dd66",
      "name": "Confirm invoice still matches"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ \"https://api.xero.com/api.xro/2.0/Invoices/\" + encodeURIComponent($(\"Capture draft for review\").first().json.invoiceId) }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "xeroOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({Invoices:[{InvoiceID:$(\"Capture draft for review\").first().json.invoiceId,Status:'AUTHORISED'}]}) }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Xero-tenant-id",
              "value": "={{ $(\"Setup\").first().json.config.xeroTenantId }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            },
            {
              "name": "Idempotency-Key",
              "value": "={{ 'authorize-'+ $(\"Capture draft for review\").first().json.invoiceNumber }}"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        5095,
        150
      ],
      "id": "2ef50aba-4c94-5f59-889e-97bff7974de3",
      "name": "Authorize invoice in Xero"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst draft = $(\"Capture draft for review\").first().json;\nconst inv = input.Invoices?.[0];\nif (input.Invoices?.length !== 1 || inv?.InvoiceID !== draft.invoiceId || inv.Status !== 'AUTHORISED' || inv.HasErrors) throw new Error('Xero did not confirm authorization.');\nreturn [{json:{invoiceId:draft.invoiceId}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5330,
        150
      ],
      "id": "9d46c762-b32c-5f8d-8b9c-760d3593c1d5",
      "name": "Check authorized invoice"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "3e41b1dc-377f-5958-bd47-d2612195b0a2",
              "leftValue": "={{ $(\"Setup\").first().json.config.emailAfterAuthorization === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5565,
        150
      ],
      "id": "69a99fbf-8b7d-5e6a-abf6-f28f40ea1b82",
      "name": "Email authorized invoice?"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.xero.com/api.xro/2.0/Contacts/\" + encodeURIComponent($(\"Capture draft for review\").first().json.contactId) }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "xeroOAuth2Api",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Xero-tenant-id",
              "value": "={{ $(\"Setup\").first().json.config.xeroTenantId }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        5800,
        150
      ],
      "id": "09abc9b8-f892-5fa6-833e-293b1d03afef",
      "name": "Recheck invoice email contact"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst draft = $(\"Capture draft for review\").first().json;\nconst person = input.Contacts?.[0];\nconst email = String(person?.EmailAddress || '').trim().toLowerCase();\nif (input.Contacts?.length !== 1 || String(person?.ContactID || '').toLowerCase() !== draft.contactId ||\n    person.ContactStatus !== 'ACTIVE' || email !== draft.email || !/^[^\\s@<>,\";]+@[^\\s@<>,\";]+\\.[^\\s@<>,\";]+$/.test(email) ||\n    (person.ContactPersons || []).some(entry=>entry.IncludeInEmails === true)) throw new Error('The Xero contact email changed, is invalid, or has additional recipients. Send manually after review.');\nreturn [{json:{invoiceId:draft.invoiceId}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6035,
        150
      ],
      "id": "aaf5853f-bcc9-590c-8bfc-13d7753cadf8",
      "name": "Confirm invoice recipient"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ \"https://api.xero.com/api.xro/2.0/Invoices/\" + encodeURIComponent($(\"Capture draft for review\").first().json.invoiceId) + '/Email' }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "xeroOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({}) }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Xero-tenant-id",
              "value": "={{ $(\"Setup\").first().json.config.xeroTenantId }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            },
            {
              "name": "Idempotency-Key",
              "value": "={{ 'email-'+ $(\"Capture draft for review\").first().json.invoiceNumber }}"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        6270,
        150
      ],
      "id": "791f3fa6-cacb-5e4a-9073-31f31889f252",
      "name": "Email invoice from Xero"
    }
  ],
  "connections": {
    "Run with sample data": {
      "main": [
        [
          {
            "node": "Sample input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample input": {
      "main": [
        [
          {
            "node": "Setup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive event": {
      "main": [
        [
          {
            "node": "Setup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup": {
      "main": [
        [
          {
            "node": "Validate invoice intake",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate invoice intake": {
      "main": [
        [
          {
            "node": "Read live invoice sources?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read live invoice sources?": {
      "main": [
        [
          {
            "node": "Read Pipedrive deal",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Verify deal is won",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Pipedrive deal": {
      "main": [
        [
          {
            "node": "Verify deal is won",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify deal is won": {
      "main": [
        [
          {
            "node": "Read live deal products?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read live deal products?": {
      "main": [
        [
          {
            "node": "Read Pipedrive deal products",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Xero invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Pipedrive deal products": {
      "main": [
        [
          {
            "node": "Build Xero invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Xero invoice": {
      "main": [
        [
          {
            "node": "Create live Xero draft?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create live Xero draft?": {
      "main": [
        [
          {
            "node": "Read mapped Xero contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read mapped Xero contact": {
      "main": [
        [
          {
            "node": "Verify Xero contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Xero contact": {
      "main": [
        [
          {
            "node": "Find invoice by number",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find invoice by number": {
      "main": [
        [
          {
            "node": "Check invoice replay",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check invoice replay": {
      "main": [
        [
          {
            "node": "Is this a new invoice?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is this a new invoice?": {
      "main": [
        [
          {
            "node": "Create Xero draft invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Xero draft invoice": {
      "main": [
        [
          {
            "node": "Capture draft for review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Capture draft for review": {
      "main": [
        [
          {
            "node": "Ask finance in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask finance in Slack": {
      "main": [
        [
          {
            "node": "Finance approved this draft?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Finance approved this draft?": {
      "main": [
        [
          {
            "node": "Authorize approved invoice?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Authorize approved invoice?": {
      "main": [
        [
          {
            "node": "Read deal after approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read deal after approval": {
      "main": [
        [
          {
            "node": "Recheck won deal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recheck won deal": {
      "main": [
        [
          {
            "node": "Re-read Xero draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Re-read Xero draft": {
      "main": [
        [
          {
            "node": "Confirm invoice still matches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Confirm invoice still matches": {
      "main": [
        [
          {
            "node": "Authorize invoice in Xero",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Authorize invoice in Xero": {
      "main": [
        [
          {
            "node": "Check authorized invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check authorized invoice": {
      "main": [
        [
          {
            "node": "Email authorized invoice?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email authorized invoice?": {
      "main": [
        [
          {
            "node": "Recheck invoice email contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recheck invoice email contact": {
      "main": [
        [
          {
            "node": "Confirm invoice recipient",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Confirm invoice recipient": {
      "main": [
        [
          {
            "node": "Email invoice from Xero",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "all"
  },
  "pinData": {},
  "tags": []
}
