From a7578b8f37de94518df2587950e79f6477ff96fd Mon Sep 17 00:00:00 2001
From: Hassan_Wari <85742599+hassan254-prog@users.noreply.github.com>
Date: Fri, 20 Sep 2024 11:07:33 +0300
Subject: [PATCH 1/3] feat(integration-templates): add quickbooks actions &
syncs (#2741)
## Describe your changes
- Add quickbooks actions & sycns
## Issue ticket number and link
[EXT-143](https://linear.app/nango/issue/EXT-143/quickbooks-syncs-and-actions-similar-to-xero)
---
docs-v2/integrations/all/quickbooks.mdx | 1 +
.../integration-templates/quickbooks.mdx | 21 ++
docs-v2/mint.json | 1 +
.../quickbooks/actions/create-account.ts | 44 +++
.../quickbooks/actions/create-credit-memo.ts | 71 ++++
.../quickbooks/actions/create-customer.ts | 44 +++
.../quickbooks/actions/create-invoice.ts | 70 ++++
.../quickbooks/actions/create-item.ts | 44 +++
.../quickbooks/actions/create-payment.ts | 50 +++
.../quickbooks/actions/update-account.ts | 44 +++
.../quickbooks/actions/update-credit-memo.ts | 44 +++
.../quickbooks/actions/update-customer.ts | 44 +++
.../quickbooks/actions/update-invoice.ts | 44 +++
.../quickbooks/actions/update-item.ts | 44 +++
.../quickbooks/fixtures/create-account.json | 7 +
.../fixtures/create-credit-memo.json | 25 ++
.../quickbooks/fixtures/create-customer.json | 20 ++
.../quickbooks/fixtures/create-invoice.json | 39 ++
.../quickbooks/fixtures/create-item.json | 21 ++
.../quickbooks/fixtures/create-payment.json | 6 +
.../quickbooks/fixtures/update-account.json | 8 +
.../fixtures/update-credit-memo.json | 23 ++
.../quickbooks/fixtures/update-customer.json | 12 +
.../quickbooks/fixtures/update-invoice.json | 34 ++
.../quickbooks/fixtures/update-item.json | 7 +
.../quickbooks/helpers/paginate.ts | 115 ++++++
.../quickbooks/mappers/toAccount.ts | 67 ++++
.../quickbooks/mappers/toCreditMemo.ts | 108 ++++++
.../quickbooks/mappers/toCustomer.ts | 121 +++++++
.../quickbooks/mappers/toInvoice.ts | 117 ++++++
.../quickbooks/mappers/toItem.ts | 95 +++++
.../quickbooks/mappers/toPayment.ts | 55 +++
integration-templates/quickbooks/nango.yaml | 271 ++++++++++++++
.../quickbooks/syncs/accounts.ts | 40 +++
.../quickbooks/syncs/customers.ts | 40 +++
.../quickbooks/syncs/invoices.ts | 39 ++
.../quickbooks/syncs/items.ts | 40 +++
.../quickbooks/syncs/payments.ts | 39 ++
integration-templates/quickbooks/types.ts | 314 ++++++++++++++++
.../quickbooks/utils/getCompany.ts | 19 +
.../quickbooks/utils/mapRefrence.ts | 10 +
.../quickbooks/utils/toDate.ts | 22 ++
packages/shared/flows.yaml | 337 ++++++++++++++++++
43 files changed, 2617 insertions(+)
create mode 100644 docs-v2/integrations/integration-templates/quickbooks.mdx
create mode 100644 integration-templates/quickbooks/actions/create-account.ts
create mode 100644 integration-templates/quickbooks/actions/create-credit-memo.ts
create mode 100644 integration-templates/quickbooks/actions/create-customer.ts
create mode 100644 integration-templates/quickbooks/actions/create-invoice.ts
create mode 100644 integration-templates/quickbooks/actions/create-item.ts
create mode 100644 integration-templates/quickbooks/actions/create-payment.ts
create mode 100644 integration-templates/quickbooks/actions/update-account.ts
create mode 100644 integration-templates/quickbooks/actions/update-credit-memo.ts
create mode 100644 integration-templates/quickbooks/actions/update-customer.ts
create mode 100644 integration-templates/quickbooks/actions/update-invoice.ts
create mode 100644 integration-templates/quickbooks/actions/update-item.ts
create mode 100644 integration-templates/quickbooks/fixtures/create-account.json
create mode 100644 integration-templates/quickbooks/fixtures/create-credit-memo.json
create mode 100644 integration-templates/quickbooks/fixtures/create-customer.json
create mode 100644 integration-templates/quickbooks/fixtures/create-invoice.json
create mode 100644 integration-templates/quickbooks/fixtures/create-item.json
create mode 100644 integration-templates/quickbooks/fixtures/create-payment.json
create mode 100644 integration-templates/quickbooks/fixtures/update-account.json
create mode 100644 integration-templates/quickbooks/fixtures/update-credit-memo.json
create mode 100644 integration-templates/quickbooks/fixtures/update-customer.json
create mode 100644 integration-templates/quickbooks/fixtures/update-invoice.json
create mode 100644 integration-templates/quickbooks/fixtures/update-item.json
create mode 100644 integration-templates/quickbooks/helpers/paginate.ts
create mode 100644 integration-templates/quickbooks/mappers/toAccount.ts
create mode 100644 integration-templates/quickbooks/mappers/toCreditMemo.ts
create mode 100644 integration-templates/quickbooks/mappers/toCustomer.ts
create mode 100644 integration-templates/quickbooks/mappers/toInvoice.ts
create mode 100644 integration-templates/quickbooks/mappers/toItem.ts
create mode 100644 integration-templates/quickbooks/mappers/toPayment.ts
create mode 100644 integration-templates/quickbooks/nango.yaml
create mode 100644 integration-templates/quickbooks/syncs/accounts.ts
create mode 100644 integration-templates/quickbooks/syncs/customers.ts
create mode 100644 integration-templates/quickbooks/syncs/invoices.ts
create mode 100644 integration-templates/quickbooks/syncs/items.ts
create mode 100644 integration-templates/quickbooks/syncs/payments.ts
create mode 100644 integration-templates/quickbooks/types.ts
create mode 100644 integration-templates/quickbooks/utils/getCompany.ts
create mode 100644 integration-templates/quickbooks/utils/mapRefrence.ts
create mode 100644 integration-templates/quickbooks/utils/toDate.ts
diff --git a/docs-v2/integrations/all/quickbooks.mdx b/docs-v2/integrations/all/quickbooks.mdx
index d58b24f5d8b..6ca98c146c6 100644
--- a/docs-v2/integrations/all/quickbooks.mdx
+++ b/docs-v2/integrations/all/quickbooks.mdx
@@ -22,6 +22,7 @@ API configuration: [`quickbooks`](https://nango.dev/providers.yaml)
- [How to register an Application](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0#create-an-app)
- [OAuth-related docs](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization)
- [List of OAuth scopes](https://developer.intuit.com/app/developer/qbo/docs/learn/scopes#current-scopes)
+- [Quickbooks API docs](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account)
Need help getting started? Get help in the [community](https://nango.dev/slack).
diff --git a/docs-v2/integrations/integration-templates/quickbooks.mdx b/docs-v2/integrations/integration-templates/quickbooks.mdx
new file mode 100644
index 00000000000..f61124d1396
--- /dev/null
+++ b/docs-v2/integrations/integration-templates/quickbooks.mdx
@@ -0,0 +1,21 @@
+---
+title: 'Quickbooks API Integration Template'
+sidebarTitle: 'Quickbooks'
+---
+
+## Get started with the Quickbooks template
+
+
+ Learn how to use integration templates in Nango
+
+
+
+ Get the latest version of the Quickbooks integration template from GitHub
+
+
+## Need help with the template?
+Please reach out in the [Slack community](https://nango.dev/slack), we are very active there and happy to help!
\ No newline at end of file
diff --git a/docs-v2/mint.json b/docs-v2/mint.json
index 7ef4fd36745..8e8836d0333 100644
--- a/docs-v2/mint.json
+++ b/docs-v2/mint.json
@@ -302,6 +302,7 @@
"integrations/integration-templates/woocommerce",
"integrations/integration-templates/workable",
"integrations/integration-templates/xero",
+ "integrations/integration-templates/quickbooks",
"integrations/integration-templates/zendesk",
"integrations/integration-templates/zoho-crm",
"integrations/integration-templates/zoho-mail"
diff --git a/integration-templates/quickbooks/actions/create-account.ts b/integration-templates/quickbooks/actions/create-account.ts
new file mode 100644
index 00000000000..c3183f53e47
--- /dev/null
+++ b/integration-templates/quickbooks/actions/create-account.ts
@@ -0,0 +1,44 @@
+import type { NangoAction, CreateAccount, Account, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksAccount, toAccount } from '../mappers/toAccount.js';
+
+/**
+ * This function handles the creation of a account in QuickBooks via the Nango action.
+ * It validates the input account data, maps it to the appropriate QuickBooks account structure,
+ * and sends a request to create the account in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#create-an-account
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {CreateAccount} input - The account data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise} - Returns the created account object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: CreateAccount): Promise {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input account object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Ensure that required fields are present for QuickBooks
+ if (!input.name || (!input.account_type && !input.account_sub_type)) {
+ throw new nango.ActionError({
+ message: `Please provide a 'name' and at least one of the following: account_type or account_sub_type. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the account input to the QuickBooks account structure
+ const quickBooksAccount = toQuickBooksAccount(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/account`,
+ data: quickBooksAccount
+ };
+
+ const response = await nango.post(config);
+
+ return toAccount(response.data['Account']);
+}
diff --git a/integration-templates/quickbooks/actions/create-credit-memo.ts b/integration-templates/quickbooks/actions/create-credit-memo.ts
new file mode 100644
index 00000000000..72946725355
--- /dev/null
+++ b/integration-templates/quickbooks/actions/create-credit-memo.ts
@@ -0,0 +1,71 @@
+import type { NangoAction, CreateCreditMemo, CreditMemo, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksCreditMemo, toCreditMemo } from '../mappers/toCreditMemo.js';
+
+/**
+ * This function handles the creation of a credit memo in QuickBooks via the Nango action.
+ * It validates the input credit memo data, maps it to the appropriate QuickBooks credit memo structure,
+ * and sends a request to create the credit memo in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/creditmemo#create-a-credit-memo
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {CreateCreditMemo} input - The credit memo data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise} - Returns the created credit memo object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: CreateCreditMemo): Promise {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input credit memo object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Validate required fields
+ if (!input.customer_ref || !input.customer_ref.value) {
+ throw new nango.ActionError({
+ message: `CustomerRef is required and must include a value. Received: ${JSON.stringify(input.customer_ref)}`
+ });
+ }
+
+ if (!input.line || input.line.length === 0) {
+ throw new nango.ActionError({
+ message: `At least one line item is required. Received: ${JSON.stringify(input.line)}`
+ });
+ }
+
+ // Validate each line item
+ for (const line of input.line) {
+ if (!line.detail_type) {
+ throw new nango.ActionError({
+ message: `DetailType is required for each line item. Received: ${JSON.stringify(line)}`
+ });
+ }
+
+ if (line.amount_cents === undefined) {
+ throw new nango.ActionError({
+ message: `amount_cents is required for each line item. Received: ${JSON.stringify(line)}`
+ });
+ }
+
+ if (!line.sales_item_line_detail || !line.sales_item_line_detail.item_ref) {
+ throw new nango.ActionError({
+ message: `SalesItemLineDetail with item_ref is required for each line item. Received: ${JSON.stringify(line.sales_item_line_detail)}`
+ });
+ }
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the credit memo input to the QuickBooks credit memo structure
+ const quickBooksInvoice = toQuickBooksCreditMemo(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/creditmemo`,
+ data: quickBooksInvoice
+ };
+
+ const response = await nango.post(config);
+
+ return toCreditMemo(response.data['CreditMemo']);
+}
diff --git a/integration-templates/quickbooks/actions/create-customer.ts b/integration-templates/quickbooks/actions/create-customer.ts
new file mode 100644
index 00000000000..5f5c09f1ca6
--- /dev/null
+++ b/integration-templates/quickbooks/actions/create-customer.ts
@@ -0,0 +1,44 @@
+import type { NangoAction, CreateCustomer, Customer, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksCustomer, toCustomer } from '../mappers/toCustomer.js';
+
+/**
+ * This function handles the creation of a customer in QuickBooks via the Nango action.
+ * It validates the input customer data, maps it to the appropriate QuickBooks customer structure,
+ * and sends a request to create the customer in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#create-a-customer
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {CreateCustomer} input - The customer data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise} - Returns the created customer object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: CreateCustomer): Promise {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input customer object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Ensure that required fields are present for QuickBooks
+ if (!input.title && !input.given_name && !input.display_name && !input.suffix) {
+ throw new nango.ActionError({
+ message: `Please provide at least one of the following fields: title, given_name, display_name, or suffix. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the customer input to the QuickBooks customer structure
+ const quickBooksCustomer = toQuickBooksCustomer(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/customer`,
+ data: quickBooksCustomer
+ };
+
+ const response = await nango.post(config);
+
+ return toCustomer(response.data['Customer']);
+}
diff --git a/integration-templates/quickbooks/actions/create-invoice.ts b/integration-templates/quickbooks/actions/create-invoice.ts
new file mode 100644
index 00000000000..07df9bea329
--- /dev/null
+++ b/integration-templates/quickbooks/actions/create-invoice.ts
@@ -0,0 +1,70 @@
+import type { NangoAction, CreateInvoice, Invoice, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksInvoice, toInvoice } from '../mappers/toInvoice.js';
+
+/**
+ * This function handles the creation of an invoice in QuickBooks via the Nango action.
+ * It validates the input invoice data, maps it to the appropriate QuickBooks invoice structure,
+ * and sends a request to create the invoice in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#create-an-invoice
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {CreateInvoice} input - The invoice data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise} - Returns the created invoice object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: CreateInvoice): Promise {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input invoice object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Validate required fields
+ if (!input.customer_ref || !input.customer_ref.value) {
+ throw new nango.ActionError({
+ message: `CustomerRef is required and must include a value. Received: ${JSON.stringify(input.customer_ref)}`
+ });
+ }
+
+ if (!input.line || input.line.length === 0) {
+ throw new nango.ActionError({
+ message: `At least one line item is required. Received: ${JSON.stringify(input.line)}`
+ });
+ }
+
+ // Validate each line item
+ for (const line of input.line) {
+ if (!line.detail_type) {
+ throw new nango.ActionError({
+ message: `DetailType is required for each line item. Received: ${JSON.stringify(line)}`
+ });
+ }
+
+ if (line.amount_cents === undefined) {
+ throw new nango.ActionError({
+ message: `Amount_cents is required for each line item. Received: ${JSON.stringify(line)}`
+ });
+ }
+
+ if (!line.sales_item_line_detail || !line.sales_item_line_detail.item_ref) {
+ throw new nango.ActionError({
+ message: `SalesItemLineDetail with item_ref is required for each line item. Received: ${JSON.stringify(line.sales_item_line_detail)}`
+ });
+ }
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the invoice input to the QuickBooks invoice structure
+ const quickBooksInvoice = toQuickBooksInvoice(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/invoice`,
+ data: quickBooksInvoice
+ };
+ const response = await nango.post(config);
+
+ return toInvoice(response.data['Invoice']);
+}
diff --git a/integration-templates/quickbooks/actions/create-item.ts b/integration-templates/quickbooks/actions/create-item.ts
new file mode 100644
index 00000000000..a18a9026153
--- /dev/null
+++ b/integration-templates/quickbooks/actions/create-item.ts
@@ -0,0 +1,44 @@
+import type { NangoAction, CreateItem, Item, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksItem, toItem } from '../mappers/toItem.js';
+
+/**
+ * This function handles the creation of an item in QuickBooks via the Nango action.
+ * It validates the input item data, maps it to the appropriate QuickBooks item structure,
+ * and sends a request to create the item in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {CreateItem} input - The item data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise- } - Returns the created item object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: CreateItem): Promise
- {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input item object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Ensure that required fields are present for QuickBooks
+ if (!input.name || (!input.expense_accountRef && !input.income_accountRef)) {
+ throw new nango.ActionError({
+ message: `Please provide a 'name' and at least one of the following: 'expense_accountRef' or 'income_accountRef'. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the item input to the QuickBooks item structure
+ const quickBooksItem = toQuickBooksItem(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/item`,
+ data: quickBooksItem
+ };
+
+ const response = await nango.post(config);
+
+ return toItem(response.data['Item']);
+}
diff --git a/integration-templates/quickbooks/actions/create-payment.ts b/integration-templates/quickbooks/actions/create-payment.ts
new file mode 100644
index 00000000000..2b9cb7c05b7
--- /dev/null
+++ b/integration-templates/quickbooks/actions/create-payment.ts
@@ -0,0 +1,50 @@
+import type { NangoAction, CreatePayment, Payment, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksPayment, toPayment } from '../mappers/toPayment.js';
+
+/**
+ * This function handles the creation of an invoice in QuickBooks via the Nango action.
+ * It validates the input invoice data, maps it to the appropriate QuickBooks invoice structure,
+ * and sends a request to create the invoice in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#create-a-payment
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {CreatePayment} input - The invoice data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise} - Returns the created invoice object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: CreatePayment): Promise {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input invoice object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Validate required fields
+ if (!input.customer_ref || !input.customer_ref.value) {
+ throw new nango.ActionError({
+ message: `CustomerRef is required and must include a value. Received: ${JSON.stringify(input.customer_ref)}`
+ });
+ }
+
+ if (!input.total_amount_cents) {
+ throw new nango.ActionError({
+ message: `Amount_cents is required for the payment is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the invoice input to the QuickBooks invoice structure
+ const quickBooksPayment = toQuickBooksPayment(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/payment`,
+ data: quickBooksPayment
+ };
+
+ const response = await nango.post(config);
+
+ return toPayment(response.data['Payment']);
+}
diff --git a/integration-templates/quickbooks/actions/update-account.ts b/integration-templates/quickbooks/actions/update-account.ts
new file mode 100644
index 00000000000..fd91f794f9d
--- /dev/null
+++ b/integration-templates/quickbooks/actions/update-account.ts
@@ -0,0 +1,44 @@
+import type { NangoAction, UpdateAccount, Account, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksAccount, toAccount } from '../mappers/toAccount.js';
+
+/**
+ * This function handles the partial update of a account in QuickBooks via the Nango action.
+ * It validates the input account data, maps it to the appropriate QuickBooks account structure,
+ * and sends a request to sparse update the account in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#sparse-update-a-account
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {UpdateAccount} input - The account data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise} - Returns the created account object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: UpdateAccount): Promise {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input account object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Ensure that required fields are present for QuickBooks
+ if (!input.id || !input.sync_token || (!input.account_type && !input.account_sub_type)) {
+ throw new nango.ActionError({
+ message: `Both 'id' and 'sync_token' must be provided, and at least one of; account_type, account_sub_type. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the account input to the QuickBooks account structure
+ const quickBooksAccount = toQuickBooksAccount(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/account`,
+ data: quickBooksAccount
+ };
+
+ const response = await nango.post(config);
+
+ return toAccount(response.data['Account']);
+}
diff --git a/integration-templates/quickbooks/actions/update-credit-memo.ts b/integration-templates/quickbooks/actions/update-credit-memo.ts
new file mode 100644
index 00000000000..9ef62f065ee
--- /dev/null
+++ b/integration-templates/quickbooks/actions/update-credit-memo.ts
@@ -0,0 +1,44 @@
+import type { NangoAction, UpdateCreditMemo, CreditMemo, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksCreditMemo, toCreditMemo } from '../mappers/toCreditMemo.js';
+
+/**
+ * This function handles the partial update of a credit memo in QuickBooks via the Nango action.
+ * It validates the input credit memo data, maps it to the appropriate QuickBooks credit memo structure,
+ * and sends a request to sparse update the credit memo in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/creditmemo#full-update-a-credit-memo
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {UpdateCreditMemo} input - The credit memo data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise} - Returns the created credit memo object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: UpdateCreditMemo): Promise {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input credit memo object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Ensure that required fields are present for QuickBooks
+ if (!input.id || !input.sync_token) {
+ throw new nango.ActionError({
+ message: `No id or sync_token is provided.`
+ });
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the credit memo input to the QuickBooks credit memo structure
+ const quickBooksInvoice = toQuickBooksCreditMemo(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/creditmemo`,
+ data: quickBooksInvoice
+ };
+
+ const response = await nango.post(config);
+
+ return toCreditMemo(response.data['CreditMemo']);
+}
diff --git a/integration-templates/quickbooks/actions/update-customer.ts b/integration-templates/quickbooks/actions/update-customer.ts
new file mode 100644
index 00000000000..d675380d945
--- /dev/null
+++ b/integration-templates/quickbooks/actions/update-customer.ts
@@ -0,0 +1,44 @@
+import type { NangoAction, UpdateCustomer, Customer, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksCustomer, toCustomer } from '../mappers/toCustomer.js';
+
+/**
+ * This function handles the partial update of a customer in QuickBooks via the Nango action.
+ * It validates the input customer data, maps it to the appropriate QuickBooks customer structure,
+ * and sends a request to sparse update the customer in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#sparse-update-a-customer
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {UpdateCustomer} input - The customer data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise} - Returns the created customer object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: UpdateCustomer): Promise {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input customer object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Ensure that required fields are present for QuickBooks
+ if (!input.id || !input.sync_token || (!input.title && !input.given_name && !input.display_name && !input.suffix)) {
+ throw new nango.ActionError({
+ message: `Both 'id' and 'sync_token' must be provided, and at least one of 'title', 'given_name', 'display_name', or 'suffix' must be non-empty. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the customer input to the QuickBooks customer structure
+ const quickBooksCustomer = toQuickBooksCustomer(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/customer`,
+ data: quickBooksCustomer
+ };
+
+ const response = await nango.post(config);
+
+ return toCustomer(response.data['Customer']);
+}
diff --git a/integration-templates/quickbooks/actions/update-invoice.ts b/integration-templates/quickbooks/actions/update-invoice.ts
new file mode 100644
index 00000000000..75cb67b0dbf
--- /dev/null
+++ b/integration-templates/quickbooks/actions/update-invoice.ts
@@ -0,0 +1,44 @@
+import type { NangoAction, UpdateInvoice, Invoice, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksInvoice, toInvoice } from '../mappers/toInvoice.js';
+
+/**
+ * This function handles the partial update of a invoice in QuickBooks via the Nango action.
+ * It validates the input invoice data, maps it to the appropriate QuickBooks invoice structure,
+ * and sends a request to sparse update the invoice in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#sparse-update-an-invoice
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {UpdateInvoice} input - The invoice data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise} - Returns the created invoice object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: UpdateInvoice): Promise {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input invoice object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Ensure that required fields are present for QuickBooks
+ if (!input.id || !input.sync_token) {
+ throw new nango.ActionError({
+ message: `No id or sync_token is provided.`
+ });
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the invoice input to the QuickBooks invoice structure
+ const quickBooksInvoice = toQuickBooksInvoice(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/invoice`,
+ data: quickBooksInvoice
+ };
+
+ const response = await nango.post(config);
+
+ return toInvoice(response.data['Invoice']);
+}
diff --git a/integration-templates/quickbooks/actions/update-item.ts b/integration-templates/quickbooks/actions/update-item.ts
new file mode 100644
index 00000000000..cc653d5cd59
--- /dev/null
+++ b/integration-templates/quickbooks/actions/update-item.ts
@@ -0,0 +1,44 @@
+import type { NangoAction, UpdateItem, Item, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+import { toQuickBooksItem, toItem } from '../mappers/toItem.js';
+
+/**
+ * This function handles the partial update of a customer in QuickBooks via the Nango action.
+ * It validates the input customer data, maps it to the appropriate QuickBooks customer structure,
+ * and sends a request to sparse update the customer in the QuickBooks API.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#sparse-update-a-customer
+ *
+ * @param {NangoAction} nango - The Nango action instance to handle API requests.
+ * @param {UpdateItem} input - The customer data input that will be sent to QuickBooks.
+ * @throws {nango.ActionError} - Throws an error if the input is missing or lacks required fields.
+ * @returns {Promise
- } - Returns the created customer object from QuickBooks.
+ */
+export default async function runAction(nango: NangoAction, input: UpdateItem): Promise
- {
+ // Validate if input is present
+ if (!input) {
+ throw new nango.ActionError({
+ message: `Input customer object is required. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ // Ensure that required fields are present for QuickBooks
+ if (!input.id || !input.sync_token) {
+ throw new nango.ActionError({
+ message: `Both 'id' and 'sync_token' must be provided. Received: ${JSON.stringify(input)}`
+ });
+ }
+
+ const companyId = await getCompany(nango);
+ // Map the customer input to the QuickBooks customer structure
+ const quickBooksItem = toQuickBooksItem(input);
+
+ const config: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/item`,
+ data: quickBooksItem
+ };
+
+ const response = await nango.post(config);
+
+ return toItem(response.data['Item']);
+}
diff --git a/integration-templates/quickbooks/fixtures/create-account.json b/integration-templates/quickbooks/fixtures/create-account.json
new file mode 100644
index 00000000000..467eddee94e
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/create-account.json
@@ -0,0 +1,7 @@
+{
+ "name": "Updated Account Name 9",
+ "account_type": "Accounts Receivable",
+ "account_sub_type": "AccountsPayable",
+ "acct_num": "5678900009p",
+ "description": "Updated description for the account"
+}
diff --git a/integration-templates/quickbooks/fixtures/create-credit-memo.json b/integration-templates/quickbooks/fixtures/create-credit-memo.json
new file mode 100644
index 00000000000..2d902ed9c72
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/create-credit-memo.json
@@ -0,0 +1,25 @@
+{
+ "customer_ref": {
+ "value": "73",
+ "name": "John Doe"
+ },
+ "line": [
+ {
+ "detail_type": "SalesItemLineDetail",
+ "amount_cents": 5000,
+ "sales_item_line_detail": {
+ "item_ref": {
+ "value": "9",
+ "name": "Widget A"
+ }
+ },
+ "quantity": 2,
+ "unit_price_cents": 2500,
+ "description": "Purchase of Widget A"
+ }
+ ],
+ "currency_ref": {
+ "value": "USD",
+ "name": "US Dollar"
+ }
+}
diff --git a/integration-templates/quickbooks/fixtures/create-customer.json b/integration-templates/quickbooks/fixtures/create-customer.json
new file mode 100644
index 00000000000..d7e9eb7ea2a
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/create-customer.json
@@ -0,0 +1,20 @@
+{
+ "display_name": "John Doe - 7",
+ "company_name": "Doe Enterprises 5",
+ "given_name": "John Care-3",
+ "primary_email": "john.doe@example.com",
+ "primary_phone": "123-456-7890",
+ "bill_address": {
+ "line2": "Suite 100",
+ "city": "Somewhere",
+ "country": "USA"
+ },
+ "ship_address": {
+ "line1": "456 Market St",
+ "line2": "Apt 12",
+ "city": "Anywhere",
+ "postal_code": "10001",
+ "country": "USA"
+ },
+ "notes": "Preferred customer, handle with care."
+}
diff --git a/integration-templates/quickbooks/fixtures/create-invoice.json b/integration-templates/quickbooks/fixtures/create-invoice.json
new file mode 100644
index 00000000000..e0db265b814
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/create-invoice.json
@@ -0,0 +1,39 @@
+{
+ "customer_ref": {
+ "value": "1"
+ },
+ "line": [
+ {
+ "detail_type": "SalesItemLineDetail",
+ "amount_cents": 5000,
+ "sales_item_line_detail": {
+ "item_ref": {
+ "value": "10",
+ "name": "Widget A"
+ }
+ },
+ "quantity": 2,
+ "unit_price_cents": 2500,
+ "discount_rate": 10,
+ "description": "Purchase of Widget A"
+ },
+ {
+ "detail_type": "SalesItemLineDetail",
+ "amount_cents": 1500,
+ "sales_item_line_detail": {
+ "item_ref": {
+ "value": "9",
+ "name": "Widget B"
+ }
+ },
+ "quantity": 1,
+ "unit_price_cents": 1500,
+ "discount_rate": 10,
+ "description": "Purchase of Widget B"
+ }
+ ],
+ "due_date": "2024-10-01",
+ "currency_ref": {
+ "value": "USD"
+ }
+}
diff --git a/integration-templates/quickbooks/fixtures/create-item.json b/integration-templates/quickbooks/fixtures/create-item.json
new file mode 100644
index 00000000000..06192cff9db
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/create-item.json
@@ -0,0 +1,21 @@
+{
+ "track_qty_onHand": true,
+ "name": "Timber Test 4",
+ "qty_on_hand": 10,
+ "income_accountRef": {
+ "name": "Sales of Product Income",
+ "value": "79"
+ },
+ "asset_accountRef": {
+ "name": "Inventory Asset",
+ "value": "81"
+ },
+ "inv_start_date": "2015-01-01",
+ "type": "Inventory",
+ "unit_price_cents": 10000,
+ "purchase_cost_cents": 9000,
+ "expense_accountRef": {
+ "name": "Cost of Goods Sold",
+ "value": "80"
+ }
+}
diff --git a/integration-templates/quickbooks/fixtures/create-payment.json b/integration-templates/quickbooks/fixtures/create-payment.json
new file mode 100644
index 00000000000..33beb8d8154
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/create-payment.json
@@ -0,0 +1,6 @@
+{
+ "customer_ref": {
+ "value": "1"
+ },
+ "total_amount_cents": 100
+}
diff --git a/integration-templates/quickbooks/fixtures/update-account.json b/integration-templates/quickbooks/fixtures/update-account.json
new file mode 100644
index 00000000000..89877fad3ad
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/update-account.json
@@ -0,0 +1,8 @@
+{
+ "name": "My test 9",
+ "account_type": "Accounts Receivable",
+ "description": "this is an a test account 2",
+ "acct_num": "1111222333444222",
+ "id": "100",
+ "sync_token": "3"
+}
diff --git a/integration-templates/quickbooks/fixtures/update-credit-memo.json b/integration-templates/quickbooks/fixtures/update-credit-memo.json
new file mode 100644
index 00000000000..04706ecbfd5
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/update-credit-memo.json
@@ -0,0 +1,23 @@
+{
+ "sync_token": "4",
+ "id": "170",
+ "line": [
+ {
+ "detail_type": "SalesItemLineDetail",
+ "amount_cents": 50,
+ "sales_item_line_detail": {
+ "item_ref": {
+ "value": "9",
+ "name": "Widget A"
+ }
+ },
+ "quantity": 2,
+ "unit_price_cents": 25,
+ "description": "Purchase of Widget Ab"
+ }
+ ],
+ "customer_ref": {
+ "value": "2",
+ "name": "John Doe"
+ }
+}
diff --git a/integration-templates/quickbooks/fixtures/update-customer.json b/integration-templates/quickbooks/fixtures/update-customer.json
new file mode 100644
index 00000000000..81e581fe29f
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/update-customer.json
@@ -0,0 +1,12 @@
+{
+ "display_name": "John Doe 3",
+ "id": "74",
+ "sync_token": "8",
+ "ship_address": {
+ "line1": "456 Market St",
+ "line2": "Apt 12",
+ "city": "Anywhere",
+ "postal_code": "10005",
+ "country": "USA"
+ }
+}
diff --git a/integration-templates/quickbooks/fixtures/update-invoice.json b/integration-templates/quickbooks/fixtures/update-invoice.json
new file mode 100644
index 00000000000..7689142c128
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/update-invoice.json
@@ -0,0 +1,34 @@
+{
+ "sync_token": "2",
+ "id": "181",
+ "due_date": "2016-09-30",
+ "line": [
+ {
+ "detail_type": "SalesItemLineDetail",
+ "amount_cents": 50000,
+ "sales_item_line_detail": {
+ "item_ref": {
+ "value": "10",
+ "name": "Widget A"
+ }
+ },
+ "quantity": 2,
+ "unit_price_cents": 25000,
+ "discount_rate": 10,
+ "description": "Purchase of Widget A"
+ },
+ {
+ "detail_type": "SalesItemLineDetail",
+ "amount_cents": 15000,
+ "sales_item_line_detail": {
+ "item_ref": {
+ "value": "9",
+ "name": "Widget B"
+ }
+ },
+ "quantity": 1,
+ "unit_price_cents": 15000,
+ "description": "Purchase of Widget B"
+ }
+ ]
+}
diff --git a/integration-templates/quickbooks/fixtures/update-item.json b/integration-templates/quickbooks/fixtures/update-item.json
new file mode 100644
index 00000000000..ff6e526db0a
--- /dev/null
+++ b/integration-templates/quickbooks/fixtures/update-item.json
@@ -0,0 +1,7 @@
+{
+ "sync_token": "1",
+ "name": "Tree Test 8",
+ "id": "32",
+ "unit_price_cents": 1006,
+ "purchase_cost_cents": 991
+}
diff --git a/integration-templates/quickbooks/helpers/paginate.ts b/integration-templates/quickbooks/helpers/paginate.ts
new file mode 100644
index 00000000000..98d1389ad40
--- /dev/null
+++ b/integration-templates/quickbooks/helpers/paginate.ts
@@ -0,0 +1,115 @@
+import type { NangoSync, ProxyConfiguration } from '../../models';
+import { getCompany } from '../utils/getCompany.js';
+
+export interface PaginationParams {
+ model: string;
+ initialPage?: number;
+ maxResults?: number;
+ additionalFilter?: string;
+}
+
+/**
+ * Asynchronous generator function for paginating through API results.
+ *
+ * This function handles pagination by making repeated requests to the specified API endpoint.
+ * It yields arrays of results for each page until no more data is available.
+ *
+ * @param nango The NangoSync instance used for making API calls.
+ * @param params Configuration parameters for pagination, including the model, pagination params, and additional filters.
+ * @returns An async generator that yields arrays of results from each page.
+ */
+export async function* paginate(
+ nango: NangoSync,
+ { model, initialPage = 1, maxResults = 100, additionalFilter = '' }: PaginationParams
+): AsyncGenerator {
+ if (!model) {
+ throw new Error("'model' parameter is required.");
+ }
+
+ let startPosition = initialPage;
+ const responseKey = 'QueryResponse'; // Constant across all
+
+ const companyId = await getCompany(nango);
+
+ while (true) {
+ const query = buildBaseQuery(model, nango.lastSyncDate, additionalFilter);
+ const queryWithPagination = buildQueryWithPagination(query, startPosition, maxResults);
+
+ await nango.log('Syncing records using the following query:', { queryWithPagination });
+
+ const payload: ProxyConfiguration = {
+ endpoint: `/v3/company/${companyId}/query`,
+ params: { query: queryWithPagination },
+ retries: 10
+ };
+
+ const response = await nango.get>(payload);
+
+ if (!response || !response.data || !response.data[responseKey]) {
+ await nango.log('No data found in response, exiting pagination.');
+ break;
+ }
+
+ let responseData = response.data[responseKey];
+
+ if (model && responseData) {
+ responseData = model.split('.').reduce((obj, key) => obj?.[key], responseData);
+ }
+
+ const results = (responseData as T[]) || [];
+
+ if (results.length === 0) {
+ break;
+ }
+
+ yield results;
+
+ startPosition += maxResults;
+ }
+}
+
+/**
+ * Builds the base SQL-like query string.
+ *
+ * @param model The model to query.
+ * @param lastSyncDate The last sync date for incremental sync.
+ * @param additionalFilter Additional filter to be applied to the query.
+ * @returns The base query string with optional filters.
+ */
+function buildBaseQuery(model: string, lastSyncDate?: Date, additionalFilter?: string): string {
+ let query = `SELECT * FROM ${model}`;
+
+ if (lastSyncDate) {
+ query = addIncrementalFilter(query, lastSyncDate);
+ }
+
+ if (additionalFilter) {
+ query += lastSyncDate ? ` AND ${additionalFilter}` : ` WHERE ${additionalFilter}`;
+ }
+
+ return query;
+}
+
+/**
+ * Creates a query string with pagination parameters.
+ *
+ * @param query The base SQL-like query string.
+ * @param startPosition The position to start fetching results from.
+ * @param maxResults The maximum number of results to fetch.
+ * @returns The query string with pagination parameters.
+ */
+function buildQueryWithPagination(query: string, startPosition: number, maxResults: number): string {
+ return `${query} STARTPOSITION ${startPosition} MAXRESULTS ${maxResults}`;
+}
+
+/**
+ * Adds an incremental filter to the SQL-like query based on the last sync date.
+ *
+ * @param query The base SQL-like query string.
+ * @param lastSyncDate The date to filter records based on creation time.
+ * @returns The query string with incremental filter.
+ */
+function addIncrementalFilter(query: string, lastSyncDate: Date): string {
+ const formattedDate = lastSyncDate.toISOString();
+ return `${query} WHERE Metadata.LastUpdatedTime > '${formattedDate}'`;
+}
diff --git a/integration-templates/quickbooks/mappers/toAccount.ts b/integration-templates/quickbooks/mappers/toAccount.ts
new file mode 100644
index 00000000000..ec5e7e9d090
--- /dev/null
+++ b/integration-templates/quickbooks/mappers/toAccount.ts
@@ -0,0 +1,67 @@
+import type { Account, CreateAccount, UpdateAccount } from '../../models';
+import type { QuickBooksAccount } from '../types';
+
+/**
+ * Converts a QuickBooksAccount object to an Account object.
+ * Only includes essential properties mapped from QuickBooksAccount.
+ * @param account The QuickBooksAccount object to convert.
+ * @returns Account object representing QuickBooks account information.
+ */
+export function toAccount(account: QuickBooksAccount): Account {
+ return {
+ id: account.Id,
+ fully_qualified_name: account.FullyQualifiedName,
+ name: account.Name,
+ account_type: account.AccountType,
+ account_sub_type: account.AccountSubType,
+ classification: account.Classification,
+ current_balance_cents: account.CurrentBalance * 100,
+ active: account.Active,
+ sub_account: account.SubAccount,
+ description: account.Description ?? null,
+ acct_num: account.AcctNum ?? null,
+ created_at: new Date(account.MetaData.CreateTime).toISOString(),
+ updated_at: new Date(account.MetaData.LastUpdatedTime).toISOString()
+ };
+}
+
+/**
+ * Maps the account data from the input format to the QuickBooks account structure.
+ * This function checks for the presence of various fields in the account object and maps them
+ * to the corresponding fields expected by QuickBooks.
+ *
+ * @param {CreateAccount | UpdateAccount} account - The account data input object that needs to be mapped.
+ * @returns {QuickBooksAccount} - The mapped QuickBooks account object.
+ */
+export function toQuickBooksAccount(account: CreateAccount | UpdateAccount): QuickBooksAccount {
+ const quickBooksAccount: Partial = {};
+
+ if ('id' in account && 'sync_token' in account) {
+ const updateItem = account as UpdateAccount;
+ quickBooksAccount.Id = updateItem.id;
+ quickBooksAccount.SyncToken = updateItem.sync_token;
+ quickBooksAccount.sparse = true;
+ }
+
+ if (account.name) {
+ quickBooksAccount.Name = account.name;
+ }
+
+ if (account.account_type) {
+ quickBooksAccount.AccountType = account.account_type;
+ }
+
+ if (account.account_sub_type) {
+ quickBooksAccount.AccountSubType = account.account_sub_type;
+ }
+
+ if (account.acct_num) {
+ quickBooksAccount.AcctNum = account.acct_num;
+ }
+
+ if (account.description) {
+ quickBooksAccount.Description = account.description;
+ }
+
+ return quickBooksAccount as QuickBooksAccount;
+}
diff --git a/integration-templates/quickbooks/mappers/toCreditMemo.ts b/integration-templates/quickbooks/mappers/toCreditMemo.ts
new file mode 100644
index 00000000000..01826929568
--- /dev/null
+++ b/integration-templates/quickbooks/mappers/toCreditMemo.ts
@@ -0,0 +1,108 @@
+import type { CreditMemo, CreateCreditMemo, UpdateCreditMemo } from '../../models';
+import type { QuickBooksCreditMemo, LineInvoice } from '../types';
+import { mapReference } from '../utils/mapRefrence.js';
+
+/**
+ * Converts a QuickBooksCreditMemo object to a CreditMemo object.
+ * Only includes essential properties mapped from QuickBooksCreditMemo.
+ * @param creditMemo The QuickBooksCreditMemo object to convert.
+ * @returns CreditMemo object representing QuickBooks creditMemo information.
+ */
+export function toCreditMemo(creditMemo: QuickBooksCreditMemo): CreditMemo {
+ return {
+ created_at: new Date(creditMemo.MetaData?.CreateTime).toISOString(),
+ updated_at: new Date(creditMemo.MetaData?.LastUpdatedTime).toISOString(),
+ id: creditMemo.Id,
+ txn_date: creditMemo.TxnDate,
+ remaining_credit: creditMemo.RemainingCredit,
+ balance_cents: (creditMemo.Balance || 0) * 100,
+ total_amt_cents: (creditMemo.TotalAmt || 0) * 100,
+ customer_name: creditMemo.CustomerRef.name ?? null,
+ bill_address: creditMemo.BillAddr
+ ? {
+ city: creditMemo.BillAddr.City ?? null,
+ line1: creditMemo.BillAddr.Line1 ?? null,
+ postal_code: creditMemo.BillAddr.PostalCode ?? null,
+ country: creditMemo.BillAddr.Country ?? null,
+ id: creditMemo.BillAddr.Id
+ }
+ : null,
+ items: (creditMemo.Line || [])
+ .filter((line: LineInvoice) => line.DetailType === 'SalesItemLineDetail')
+ .map((line: LineInvoice) => ({
+ id: line.Id,
+ description: line.Description ?? null,
+ qty: line.SalesItemLineDetail?.Qty ?? 0,
+ unit_price_cents: (line.SalesItemLineDetail?.UnitPrice || 0) * 100,
+ amount_cents: (line.Amount || 0) * 100
+ }))
+ };
+}
+
+/**
+ * Maps the creditMemo data from the input format to the QuickBooks credit Memo structure.
+ * This function checks for the presence of various fields in the creditMemo object and maps them
+ * to the corresponding fields expected by QuickBooks.
+ *
+ * @param {CreateCreditMemo} creditMemo - The creditMemo data input object that needs to be mapped.
+ * @returns {QuickBooksCreditMemo} - The mapped QuickBooks creditMemo object.
+ */
+export function toQuickBooksCreditMemo(creditMemo: CreateCreditMemo): QuickBooksCreditMemo {
+ const quickBooksCreditMemo: Partial = {};
+
+ // Handle update scenarios if applicable
+ if ('id' in creditMemo && 'sync_token' in creditMemo) {
+ const updateInvoice = creditMemo as UpdateCreditMemo;
+ quickBooksCreditMemo.Id = updateInvoice.id;
+ quickBooksCreditMemo.SyncToken = updateInvoice.sync_token;
+ quickBooksCreditMemo.sparse = true;
+ }
+
+ const customerRef = mapReference(creditMemo.customer_ref);
+ if (customerRef) {
+ quickBooksCreditMemo.CustomerRef = customerRef;
+ }
+
+ if (creditMemo.line) {
+ quickBooksCreditMemo.Line = creditMemo.line.map((line) => {
+ const qbLine: Partial = {};
+ qbLine.DetailType = line.detail_type;
+ qbLine.Amount = line.amount_cents / 100;
+
+ if (line.sales_item_line_detail) {
+ qbLine.SalesItemLineDetail = {
+ ItemRef: {
+ value: line.sales_item_line_detail.item_ref.value,
+ name: line.sales_item_line_detail.item_ref.name ?? ''
+ }
+ };
+
+ if (line.quantity) {
+ qbLine.SalesItemLineDetail.Qty = line.quantity;
+ }
+
+ if (line.unit_price_cents) {
+ qbLine.SalesItemLineDetail.UnitPrice = line.unit_price_cents / 100;
+ }
+ }
+
+ if (line.description) {
+ qbLine.Description = line.description;
+ }
+
+ return qbLine as LineInvoice;
+ });
+ }
+
+ const currencyRef = mapReference(creditMemo.currency_ref);
+ if (currencyRef) {
+ quickBooksCreditMemo.CurrencyRef = currencyRef;
+ }
+
+ const projectRef = mapReference(creditMemo.project_ref);
+ if (projectRef) {
+ quickBooksCreditMemo.ProjectRef = projectRef;
+ }
+
+ return quickBooksCreditMemo as QuickBooksCreditMemo;
+}
diff --git a/integration-templates/quickbooks/mappers/toCustomer.ts b/integration-templates/quickbooks/mappers/toCustomer.ts
new file mode 100644
index 00000000000..e461004fb67
--- /dev/null
+++ b/integration-templates/quickbooks/mappers/toCustomer.ts
@@ -0,0 +1,121 @@
+import type { Customer, CreateCustomer, UpdateCustomer } from '../../models';
+import type { QuickBooksCustomer, CreateQuickbooksCustomer, PhysicalAddressCreation } from '../types';
+
+/**
+ * Converts a QuickBooksCustomer object to a Customer object.
+ * Only includes essential properties mapped from QuickBooksCustomer.
+ * @param customer The QuickBooksCustomer object to convert.
+ * @returns Customer object representing QuickBooks customer information.
+ */
+export function toCustomer(customer: QuickBooksCustomer): Customer {
+ return {
+ id: customer.Id,
+ given_name: customer.GivenName ?? null,
+ display_name: customer.DisplayName,
+ active: customer.Active,
+ balance_cents: customer.Balance * 100,
+ taxable: customer.Taxable,
+ primary_email: customer.PrimaryEmailAddr?.Address ?? null,
+ primary_phone: customer.PrimaryPhone?.FreeFormNumber ?? null,
+ bill_address: customer.BillAddr
+ ? {
+ city: customer.BillAddr.City ?? null,
+ line1: customer.BillAddr.Line1 ?? null,
+ postal_code: customer.BillAddr.PostalCode ?? null,
+ country: customer.BillAddr.Country ?? null,
+ id: customer.BillAddr.Id
+ }
+ : null,
+ ship_address: customer.ShipAddr
+ ? {
+ city: customer.ShipAddr.City ?? null,
+ line1: customer.ShipAddr.Line1 ?? null,
+ postal_code: customer.ShipAddr.PostalCode ?? null,
+ country: customer.ShipAddr.Country ?? null,
+ id: customer.ShipAddr.Id
+ }
+ : null,
+ created_at: new Date(customer.MetaData.CreateTime).toISOString(),
+ updated_at: new Date(customer.MetaData.LastUpdatedTime).toISOString()
+ };
+}
+
+/**
+ * Maps the customer data from the input format to the QuickBooks customer structure.
+ * This function checks for the presence of various fields in the customer object and maps them
+ * to the corresponding fields expected by QuickBooks.
+ *
+ * @param {CreateCustomer | UpdateCustomer} customer - The customer data input object that needs to be mapped.
+ * @returns {QuickBooksCustomer} - The mapped QuickBooks customer object.
+ */
+export function toQuickBooksCustomer(customer: CreateCustomer | UpdateCustomer): CreateQuickbooksCustomer {
+ const quickBooksCustomer: Partial = {};
+
+ // Map fields for update customer
+ if ('id' in customer && 'sync_token' in customer) {
+ quickBooksCustomer.Id = customer.id;
+ quickBooksCustomer.SyncToken = customer.sync_token;
+ quickBooksCustomer.sparse = true;
+ }
+
+ if (customer.display_name) {
+ quickBooksCustomer.DisplayName = customer.display_name;
+ }
+
+ if (customer.company_name) {
+ quickBooksCustomer.CompanyName = customer.company_name;
+ }
+
+ if (customer.title) {
+ quickBooksCustomer.Title = customer.title;
+ }
+
+ if (customer.given_name) {
+ quickBooksCustomer.GivenName = customer.given_name;
+ }
+
+ if (customer.suffix) {
+ quickBooksCustomer.Suffix = customer.suffix;
+ }
+
+ if (customer.primary_email) {
+ quickBooksCustomer.PrimaryEmailAddr = { Address: customer.primary_email };
+ }
+
+ if (customer.primary_phone) {
+ quickBooksCustomer.PrimaryPhone = { FreeFormNumber: customer.primary_phone };
+ }
+
+ if (customer.bill_address) {
+ quickBooksCustomer.BillAddr = mapAddress(customer.bill_address);
+ }
+
+ if (customer.ship_address) {
+ quickBooksCustomer.ShipAddr = mapAddress(customer.ship_address);
+ }
+
+ if (customer.notes) {
+ quickBooksCustomer.Notes = customer.notes;
+ }
+
+ return quickBooksCustomer as CreateQuickbooksCustomer;
+}
+
+/**
+ * Maps a simplified address object to a `PhysicalAddressCreation` type,
+ * including only properties that are defined and omitting `undefined` values.
+ *
+ * @param address - The simplified address object with optional properties.
+ * @returns A `PhysicalAddressCreation` object with only defined address properties.
+ */
+function mapAddress(address: { line1?: string; line2?: string; city?: string; postal_code?: string; country?: string }): PhysicalAddressCreation {
+ const result: Partial = {};
+
+ if (address.line1) result.Line1 = address.line1;
+ if (address.line2) result.Line2 = address.line2;
+ if (address.city) result.City = address.city;
+ if (address.postal_code) result.PostalCode = address.postal_code;
+ if (address.country) result.Country = address.country;
+
+ return result as PhysicalAddressCreation;
+}
diff --git a/integration-templates/quickbooks/mappers/toInvoice.ts b/integration-templates/quickbooks/mappers/toInvoice.ts
new file mode 100644
index 00000000000..39ef1df28b3
--- /dev/null
+++ b/integration-templates/quickbooks/mappers/toInvoice.ts
@@ -0,0 +1,117 @@
+import type { Invoice, CreateInvoice, UpdateInvoice } from '../../models';
+import type { QuickBooksInvoice, LineInvoice } from '../types';
+import { toDate } from '../utils/toDate.js';
+import { mapReference } from '../utils/mapRefrence.js';
+
+/**
+ * Converts a QuickBooksInvoice object to an Invoice object.
+ * Only includes essential properties mapped from QuickBooksInvoice.
+ * @param invoice The QuickBooksInvoice object to convert.
+ * @returns Invoice object representing QuickBooks invoice information.
+ */
+export function toInvoice(invoice: QuickBooksInvoice): Invoice {
+ return {
+ created_at: new Date(invoice.MetaData?.CreateTime).toISOString(),
+ updated_at: new Date(invoice.MetaData?.LastUpdatedTime).toISOString(),
+ id: invoice.Id,
+ txn_date: invoice.TxnDate,
+ due_date: invoice.DueDate,
+ balance_cents: invoice.Balance * 100,
+ total_amt_cents: invoice.TotalAmt * 100,
+ deposit_cents: (invoice.Deposit || 0) * 100,
+ bill_address: invoice.BillAddr
+ ? {
+ city: invoice.BillAddr.City ?? null,
+ line1: invoice.BillAddr.Line1 ?? null,
+ postal_code: invoice.BillAddr.PostalCode ?? null,
+ country: invoice.BillAddr.Country ?? null,
+ id: invoice.BillAddr.Id
+ }
+ : null,
+ items: (invoice.Line || [])
+ .filter((line: LineInvoice) => line.DetailType === 'SalesItemLineDetail')
+ .map((line: LineInvoice) => ({
+ id: line.Id,
+ description: line.Description ?? null,
+ qty: line.SalesItemLineDetail?.Qty ?? 0,
+ unit_price_cents: (line.SalesItemLineDetail?.UnitPrice || 0) * 100,
+ amount_cents: line.Amount * 100
+ }))
+ };
+}
+
+/**
+ * Maps the invoice data from the input format to the QuickBooks invoice structure.
+ * This function checks for the presence of various fields in the invoice object and maps them
+ * to the corresponding fields expected by QuickBooks.
+ *
+ * @param {CreateInvoice} invoice - The invoice data input object that needs to be mapped.
+ * @returns {QuickBooksInvoice} - The mapped QuickBooks invoice object.
+ */
+export function toQuickBooksInvoice(invoice: CreateInvoice): QuickBooksInvoice {
+ const quickBooksInvoice: Partial = {};
+
+ // Handle update scenarios if applicable
+ if ('id' in invoice && 'sync_token' in invoice) {
+ const updateInvoice = invoice as UpdateInvoice;
+ quickBooksInvoice.Id = updateInvoice.id;
+ quickBooksInvoice.SyncToken = updateInvoice.sync_token;
+ quickBooksInvoice.sparse = true;
+ }
+
+ const customerRef = mapReference(invoice.customer_ref);
+ if (customerRef) {
+ quickBooksInvoice.CustomerRef = customerRef;
+ }
+
+ if (invoice.due_date) {
+ quickBooksInvoice.DueDate = toDate(invoice.due_date);
+ }
+
+ if (invoice.line) {
+ quickBooksInvoice.Line = invoice.line.map((line) => {
+ const qbLine: Partial = {};
+ qbLine.DetailType = line.detail_type;
+ qbLine.Amount = line.amount_cents / 100;
+
+ if (line.sales_item_line_detail) {
+ qbLine.SalesItemLineDetail = {
+ ItemRef: {
+ value: line.sales_item_line_detail.item_ref.value,
+ name: line.sales_item_line_detail.item_ref.name ?? ''
+ }
+ };
+
+ if (line.quantity) {
+ qbLine.SalesItemLineDetail.Qty = line.quantity;
+ }
+
+ if (line.unit_price_cents) {
+ qbLine.SalesItemLineDetail.UnitPrice = line.unit_price_cents / 100;
+ }
+
+ if (line.discount_rate) {
+ qbLine.SalesItemLineDetail.DiscountRate = line.discount_rate;
+ }
+ }
+
+ if (line.description) {
+ qbLine.Description = line.description;
+ }
+
+ return qbLine as LineInvoice;
+ });
+ }
+
+ const currencyRef = mapReference(invoice.currency_ref);
+ if (currencyRef) {
+ quickBooksInvoice.CurrencyRef = currencyRef;
+ }
+
+ const projectRef = mapReference(invoice.project_ref);
+ if (projectRef) {
+ quickBooksInvoice.ProjectRef = projectRef;
+ }
+
+ return quickBooksInvoice as QuickBooksInvoice;
+}
diff --git a/integration-templates/quickbooks/mappers/toItem.ts b/integration-templates/quickbooks/mappers/toItem.ts
new file mode 100644
index 00000000000..030366a5261
--- /dev/null
+++ b/integration-templates/quickbooks/mappers/toItem.ts
@@ -0,0 +1,95 @@
+import type { Item, CreateItem, UpdateItem } from '../../models';
+import type { QuickBooksItem } from '../types';
+import { toDate } from '../utils/toDate.js';
+import { mapReference } from '../utils/mapRefrence.js';
+
+/**
+ * Converts a QuickBooksItem object to a Item object.
+ * Only includes essential properties mapped from QuickBooksItem.
+ * @param item The QuickBooksItem object to convert.
+ * @returns Item object representing QuickBooks item information.
+ */
+export function toItem(item: QuickBooksItem): Item {
+ return {
+ id: item.Id,
+ name: item.Name,
+ active: item.Active,
+ type: item.Type,
+ unit_price_cents: item.UnitPrice * 100,
+ purchase_cost_cents: item.PurchaseCost * 100,
+ qty_on_hand: item.QtyOnHand ?? null,
+ inv_start_date: item.InvStartDate ? new Date(item.InvStartDate).toISOString() : null,
+ track_qty_onHand: item.TrackQtyOnHand,
+ description: item.Description ?? null,
+ created_at: new Date(item.MetaData.CreateTime).toISOString(),
+ updated_at: new Date(item.MetaData.LastUpdatedTime).toISOString()
+ };
+}
+
+/**
+ * Maps the item data from the input format to the QuickBooks item structure.
+ * This function checks for the presence of various fields in the item object and maps them
+ * to the corresponding fields expected by QuickBooks.
+ *
+ * @param {CreateItem | UpdateItem} item - The item data input object that needs to be mapped.
+ * @returns {QuickBooksItem} - The mapped QuickBooks item object.
+ */
+export function toQuickBooksItem(item: CreateItem | UpdateItem): QuickBooksItem {
+ const quickBooksItem: Partial = {};
+
+ if ('id' in item && 'sync_token' in item) {
+ const updateItem = item;
+ quickBooksItem.Id = updateItem.id;
+ quickBooksItem.SyncToken = updateItem.sync_token;
+ quickBooksItem.sparse = true;
+ }
+
+ if (item.name) {
+ quickBooksItem.Name = item.name;
+ }
+
+ if (item.type) {
+ quickBooksItem.Type = item.type;
+ }
+
+ if (item.track_qty_onHand) {
+ quickBooksItem.TrackQtyOnHand = item.track_qty_onHand;
+ }
+
+ if (item.qty_on_hand) {
+ quickBooksItem.QtyOnHand = item.qty_on_hand;
+ }
+
+ if (item.inv_start_date) {
+ quickBooksItem.InvStartDate = toDate(item.inv_start_date);
+ }
+
+ if (item.unit_price_cents) {
+ quickBooksItem.UnitPrice = item.unit_price_cents / 100;
+ }
+
+ if (item.purchase_cost_cents) {
+ quickBooksItem.PurchaseCost = item.purchase_cost_cents / 100;
+ }
+
+ if (item.qty_on_hand) {
+ quickBooksItem.QtyOnHand = item.qty_on_hand;
+ }
+
+ const incomeAccountRef = mapReference(item.income_accountRef);
+ if (incomeAccountRef) {
+ quickBooksItem.IncomeAccountRef = incomeAccountRef;
+ }
+
+ const assetAccountRef = mapReference(item.asset_accountRef);
+ if (assetAccountRef) {
+ quickBooksItem.AssetAccountRef = assetAccountRef;
+ }
+
+ const expenseAccountRef = mapReference(item.expense_accountRef);
+ if (expenseAccountRef) {
+ quickBooksItem.ExpenseAccountRef = expenseAccountRef;
+ }
+
+ return quickBooksItem as QuickBooksItem;
+}
diff --git a/integration-templates/quickbooks/mappers/toPayment.ts b/integration-templates/quickbooks/mappers/toPayment.ts
new file mode 100644
index 00000000000..f5e73e55eb4
--- /dev/null
+++ b/integration-templates/quickbooks/mappers/toPayment.ts
@@ -0,0 +1,55 @@
+import type { Payment, CreatePayment } from '../../models';
+import type { QuickBooksPayment } from '../types';
+import { mapReference } from '../utils/mapRefrence.js';
+
+/**
+ * Converts a QuickBooksPayment object to a Payment object.
+ * Only includes essential properties mapped from QuickBooksPayment.
+ * @param customer The QuickBooksPayment object to convert.
+ * @returns Payment object representing QuickBooks payment information.
+ */
+export function toPayment(quickBooksPayment: QuickBooksPayment): Payment {
+ const payment: Payment = {
+ id: quickBooksPayment.Id,
+ amount_cents: Math.round(quickBooksPayment.TotalAmt * 100),
+ customer_name: quickBooksPayment.CustomerRef.name ?? null,
+ txn_date: quickBooksPayment.TxnDate,
+ created_at: new Date(quickBooksPayment.MetaData.CreateTime).toISOString(),
+ updated_at: new Date(quickBooksPayment.MetaData.LastUpdatedTime).toISOString()
+ };
+
+ return payment;
+}
+
+/**
+ * Maps the payment data from the input format to the QuickBooks payment structure.
+ * This function checks for the presence of various fields in the payment object and maps them
+ * to the corresponding fields expected by QuickBooks.
+ *
+ * @param {CreatePayment} payment - The payment data input object that needs to be mapped.
+ * @returns {QuickBooksPayment} - The mapped QuickBooks payment object.
+ */
+export function toQuickBooksPayment(payment: CreatePayment): QuickBooksPayment {
+ const quickBooksPayment: Partial = {};
+
+ const customerRef = mapReference(payment.customer_ref);
+ if (customerRef) {
+ quickBooksPayment.CustomerRef = customerRef;
+ }
+
+ if (payment.total_amount_cents) {
+ quickBooksPayment.TotalAmt = payment.total_amount_cents / 100;
+ }
+
+ const currencyRef = mapReference(payment.currency_ref);
+ if (currencyRef) {
+ quickBooksPayment.CurrencyRef = currencyRef;
+ }
+
+ const projectRef = mapReference(payment.project_ref);
+ if (projectRef) {
+ quickBooksPayment.ProjectRef = projectRef;
+ }
+
+ return quickBooksPayment as QuickBooksPayment;
+}
diff --git a/integration-templates/quickbooks/nango.yaml b/integration-templates/quickbooks/nango.yaml
new file mode 100644
index 00000000000..fc7f050f81d
--- /dev/null
+++ b/integration-templates/quickbooks/nango.yaml
@@ -0,0 +1,271 @@
+integrations:
+ quickbooks:
+ syncs:
+ customers:
+ description: |
+ Fetches all QuickBooks customers. Handles both active and archived customers, saving or deleting them based on their status.
+ runs: every hour
+ output: Customer
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/customers
+ accounts:
+ description: |
+ Fetches all accounts in QuickBooks. Handles both active and archived accounts, saving or deleting them based on their status.
+ runs: every hour
+ output: Account
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/accounts
+ payments:
+ description: |
+ Fetches all payments in QuickBooks. Handles both active and voided payments, saving or deleting them based on their status.
+ runs: every hour
+ output: Payment
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/payments
+ items:
+ description: |
+ Fetches all items in QuickBooks. Handles both active and archived items, saving or deleting them based on their status.
+ runs: every hour
+ output: Item
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/items
+ invoices:
+ description: |
+ Fetches all invoices in QuickBooks. Handles both active and voided invoices, saving or deleting them based on their status.
+ runs: every hour
+ output: Invoice
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/invoices
+ actions:
+ create-customer:
+ description: |
+ Creates a single customer in QuickBooks.
+ input: CreateCustomer
+ scopes: com.intuit.quickbooks.accounting
+ output: Customer
+ endpoint: POST /quickbooks/customer
+ update-customer:
+ description: |
+ Update a single customer in QuickBooks.
+ input: UpdateCustomer
+ scopes: com.intuit.quickbooks.accounting
+ output: Customer
+ endpoint: PUT /quickbooks/customer
+ create-item:
+ description: |
+ Creates a single item in QuickBooks.
+ input: CreateItem
+ scopes: com.intuit.quickbooks.accounting
+ output: Item
+ endpoint: POST /quickbooks/item
+ update-item:
+ description: |
+ Update a single item in QuickBooks.
+ input: UpdateItem
+ scopes: com.intuit.quickbooks.accounting
+ output: Item
+ endpoint: PUT /quickbooks/item
+ create-account:
+ description: |
+ Creates a single account in QuickBooks.
+ input: CreateAccount
+ scopes: com.intuit.quickbooks.accounting
+ output: Account
+ endpoint: POST /quickbooks/account
+ update-account:
+ description: |
+ Updates a single account in QuickBooks.
+ input: UpdateAccount
+ scopes: com.intuit.quickbooks.accounting
+ output: Account
+ endpoint: PUT /quickbooks/account
+ create-invoice:
+ description: |
+ Creates a single invoice in QuickBooks.
+ input: CreateInvoice
+ scopes: com.intuit.quickbooks.accounting
+ output: Invoice
+ endpoint: POST /quickbooks/invoice
+ update-invoice:
+ description: |
+ Updates a single invoice in QuickBooks.
+ input: UpdateInvoice
+ scopes: com.intuit.quickbooks.accounting
+ output: Invoice
+ endpoint: PUT /quickbooks/invoice
+ create-credit-memo:
+ description: |
+ Creates a single credit memo in QuickBooks.
+ input: CreateCreditMemo
+ scopes: com.intuit.quickbooks.accounting
+ output: CreditMemo
+ endpoint: POST /quickbooks/credit-memo
+ update-credit-memo:
+ description: |
+ Updates a single credit memo in QuickBooks.
+ input: UpdateCreditMemo
+ scopes: com.intuit.quickbooks.accounting
+ output: CreditMemo
+ endpoint: PUT /quickbooks/credit-memo
+ create-payment:
+ description: |
+ Creates a single payment in QuickBooks.
+ input: CreatePayment
+ scopes: com.intuit.quickbooks.accounting
+ output: Payment
+ endpoint: POST /quickbooks/payment
+
+models:
+ Updates:
+ id: string
+ sync_token: string
+ BaseInvoice:
+ __extends: Metadata
+ id: string
+ txn_date: string
+ balance_cents: number
+ total_amt_cents: number
+ bill_address: BillAddr | null
+ items: InvoiceItem[]
+ Metadata:
+ created_at: string
+ updated_at: string
+ Reference:
+ name?: string
+ value: string
+ BillAddr:
+ city: string | null
+ line1: string | null
+ postal_code: string | null
+ country: string | null
+ id: string
+ Customer:
+ __extends: Metadata
+ id: string
+ given_name: string | null
+ display_name: string | null
+ active: boolean
+ balance_cents: number
+ taxable: boolean
+ primary_email: string | null
+ primary_phone: string | null
+ bill_address: BillAddr | null
+ ship_address: BillAddr | null
+ Account:
+ __extends: Metadata
+ id: string
+ fully_qualified_name: string
+ name: string
+ account_type: string
+ account_sub_type: string
+ classification: string
+ current_balance_cents: number
+ active: boolean
+ description: string | null
+ acct_num: string | null
+ sub_account: boolean
+ Payment:
+ __extends: Metadata
+ id: string
+ amount_cents: number
+ customer_name: string | null
+ txn_date: string
+ Item:
+ __extends: Metadata
+ id: string
+ name: string
+ active: boolean
+ type: string
+ unit_price_cents: number
+ purchase_cost_cents: number
+ qty_on_hand: number | null
+ inv_start_date: string | null
+ description: string | null
+ track_qty_onHand: boolean
+ Invoice:
+ __extends: BaseInvoice
+ due_date: string
+ deposit_cents: number
+ InvoiceItem:
+ id: string
+ description: string | null
+ qty: number
+ unit_price_cents: number
+ amount_cents: number
+ Address:
+ line1?: string
+ line2?: string
+ city?: string
+ postal_code?: string
+ country?: string
+ lat?: string
+ long?: string
+ CreateCustomer:
+ display_name?: string
+ suffix?: string
+ title?: string
+ given_name?: string
+ company_name?: string
+ notes?: string
+ primary_email?: string
+ primary_phone?: string
+ bill_address?: Address
+ ship_address?: Address
+ UpdateCustomer:
+ __extends: CreateCustomer,Updates
+ CreateItem:
+ track_qty_onHand?: boolean
+ qty_on_hand?: number
+ name: string
+ expense_accountRef?: Reference
+ income_accountRef?: Reference
+ asset_accountRef?: Reference
+ inv_start_date?: string
+ unit_price_cents?: number
+ purchase_cost_cents?: number
+ type?: string
+ UpdateItem:
+ __extends: CreateItem,Updates
+ CreateAccount:
+ name: string
+ account_type?: string
+ account_sub_type?: string
+ description?: string
+ acct_num?: string
+ UpdateAccount:
+ __extends: CreateAccount,Updates
+ CreateInvoice:
+ customer_ref?: Reference
+ line?: Line[]
+ due_date?: string
+ currency_ref?: Reference
+ project_ref?: Reference
+ Line:
+ detail_type: string
+ amount_cents: number
+ sales_item_line_detail:
+ item_ref: Reference
+ quantity?: number
+ unit_price_cents?: number
+ discount_rate?: number
+ description?: string
+ UpdateInvoice:
+ __extends: CreateInvoice,Updates
+ CreateCreditMemo:
+ __extends: CreateInvoice
+ UpdateCreditMemo:
+ __extends: UpdateInvoice
+ CreditMemo:
+ __extends: BaseInvoice
+ remaining_credit: number
+ customer_name: string | null
+ CreatePayment:
+ total_amount_cents: number
+ customer_ref: Reference
+ currency_ref?: Reference
+ project_ref?: Reference
diff --git a/integration-templates/quickbooks/syncs/accounts.ts b/integration-templates/quickbooks/syncs/accounts.ts
new file mode 100644
index 00000000000..2d4e839d87f
--- /dev/null
+++ b/integration-templates/quickbooks/syncs/accounts.ts
@@ -0,0 +1,40 @@
+import type { NangoSync, Account } from '../../models';
+import type { QuickBooksAccount } from '../types';
+import { paginate } from '../helpers/paginate.js';
+import { toAccount } from '../mappers/toAccount.js';
+import type { PaginationParams } from '../helpers/paginate';
+
+/**
+ * Fetches account data from QuickBooks API and saves it in batch.
+ * Handles both active and archived accounts, saving or deleting them based on their status.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#query-an-account
+ *
+ * @param nango The NangoSync instance used for making API calls and saving data.
+ * @returns A promise that resolves when the data has been successfully fetched and saved.
+ */
+export default async function fetchData(nango: NangoSync): Promise {
+ const config: PaginationParams = {
+ model: 'Account',
+ additionalFilter: 'Active IN (true, false)'
+ };
+
+ let allAccounts: QuickBooksAccount[] = [];
+
+ // Fetch all accounts with pagination
+ for await (const accounts of paginate(nango, config)) {
+ allAccounts = [...allAccounts, ...accounts];
+ }
+
+ // Filter and process active accounts
+ const activeAccounts = allAccounts.filter((account) => account.Active);
+ const mappedActiveAccounts = activeAccounts.map(toAccount);
+ await nango.batchSave(mappedActiveAccounts, 'Account');
+
+ // Handle archived accounts only if it's an incremental refresh
+ if (nango.lastSyncDate) {
+ const archivedAccounts = allAccounts.filter((account) => !account.Active);
+ const mappedArchivedAccounts = archivedAccounts.map(toAccount);
+ await nango.batchDelete(mappedArchivedAccounts, 'Account');
+ }
+}
diff --git a/integration-templates/quickbooks/syncs/customers.ts b/integration-templates/quickbooks/syncs/customers.ts
new file mode 100644
index 00000000000..24497f414ae
--- /dev/null
+++ b/integration-templates/quickbooks/syncs/customers.ts
@@ -0,0 +1,40 @@
+import type { NangoSync, Customer } from '../../models';
+import type { QuickBooksCustomer } from '../types';
+import { paginate } from '../helpers/paginate.js';
+import { toCustomer } from '../mappers/toCustomer.js';
+import type { PaginationParams } from '../helpers/paginate';
+
+/**
+ * Fetches customer data from QuickBooks API and saves it in batch.
+ * Handles both active and archived customers, saving or deleting them based on their status.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#query-a-customer
+ *
+ * @param nango The NangoSync instance used for making API calls and saving data.
+ * @returns A promise that resolves when the data has been successfully fetched and saved.
+ */
+export default async function fetchData(nango: NangoSync): Promise {
+ const config: PaginationParams = {
+ model: 'Customer',
+ additionalFilter: 'Active IN (true, false)'
+ };
+
+ let allCustomers: QuickBooksCustomer[] = [];
+
+ // Fetch all customers with pagination
+ for await (const customers of paginate(nango, config)) {
+ allCustomers = [...allCustomers, ...customers];
+ }
+
+ // Filter and process active customers
+ const activeCustomers = allCustomers.filter((customer) => customer.Active);
+ const mappedActiveCustomers = activeCustomers.map(toCustomer);
+ await nango.batchSave(mappedActiveCustomers, 'Customer');
+
+ // Handle archived customers only if it's an incremental refresh
+ if (nango.lastSyncDate) {
+ const archivedCustomers = allCustomers.filter((customer) => !customer.Active);
+ const mappedArchivedCustomers = archivedCustomers.map(toCustomer);
+ await nango.batchDelete(mappedArchivedCustomers, 'Customer');
+ }
+}
diff --git a/integration-templates/quickbooks/syncs/invoices.ts b/integration-templates/quickbooks/syncs/invoices.ts
new file mode 100644
index 00000000000..6b9e7887a8a
--- /dev/null
+++ b/integration-templates/quickbooks/syncs/invoices.ts
@@ -0,0 +1,39 @@
+import type { NangoSync, Invoice } from '../../models';
+import type { QuickBooksInvoice } from '../types';
+import { paginate } from '../helpers/paginate.js';
+import { toInvoice } from '../mappers/toInvoice.js';
+import type { PaginationParams } from '../helpers/paginate';
+
+/**
+ * Fetches invoice data from QuickBooks API and saves it in batch.
+ * Handles both active and voided invoices, saving or deleting them based on their status.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#query-an-invoice
+ *
+ * @param nango The NangoSync instance used for making API calls and saving data.
+ * @returns A promise that resolves when the data has been successfully fetched and saved.
+ */
+export default async function fetchData(nango: NangoSync): Promise {
+ const config: PaginationParams = {
+ model: 'Invoice'
+ };
+
+ let allPayments: QuickBooksInvoice[] = [];
+
+ // Fetch all invoices with pagination
+ for await (const invoices of paginate(nango, config)) {
+ allPayments = [...allPayments, ...invoices];
+ }
+
+ // Filter and process invoices that are not voided (i.e., active invoices)
+ const activeInvoices = allPayments.filter((invoice) => !invoice.PrivateNote?.includes('Voided'));
+ const mappedActiveInvoices = activeInvoices.map(toInvoice);
+ await nango.batchSave(mappedActiveInvoices, 'Invoice');
+
+ // Handle voided invoices only if it's an incremental refresh
+ if (nango.lastSyncDate) {
+ const voidedPayments = allPayments.filter((invoice) => invoice.PrivateNote?.includes('Voided'));
+ const mappedVoidedPayments = voidedPayments.map(toInvoice);
+ await nango.batchDelete(mappedVoidedPayments, 'Invoice');
+ }
+}
diff --git a/integration-templates/quickbooks/syncs/items.ts b/integration-templates/quickbooks/syncs/items.ts
new file mode 100644
index 00000000000..67576282766
--- /dev/null
+++ b/integration-templates/quickbooks/syncs/items.ts
@@ -0,0 +1,40 @@
+import type { NangoSync, Item } from '../../models';
+import type { QuickBooksItem } from '../types';
+import { paginate } from '../helpers/paginate.js';
+import { toItem } from '../mappers/toItem.js';
+import type { PaginationParams } from '../helpers/paginate';
+
+/**
+ * Fetches item data from QuickBooks API and saves it in batch.
+ * Handles both active and archived items, saving or deleting them based on their status.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#query-an-item
+ *
+ * @param nango The NangoSync instance used for making API calls and saving data.
+ * @returns A promise that resolves when the data has been successfully fetched and saved.
+ */
+export default async function fetchData(nango: NangoSync): Promise {
+ const config: PaginationParams = {
+ model: 'Item',
+ additionalFilter: 'Active IN (true, false)'
+ };
+
+ let allItems: QuickBooksItem[] = [];
+
+ // Fetch all items with pagination
+ for await (const items of paginate(nango, config)) {
+ allItems = [...allItems, ...items];
+ }
+
+ // Filter and process active items
+ const activeItems = allItems.filter((item) => item.Active);
+ const mappedActiveItems = activeItems.map(toItem);
+ await nango.batchSave
- (mappedActiveItems, 'Item');
+
+ // Handle archived items only if it's an incremental refresh
+ if (nango.lastSyncDate) {
+ const archivedItems = allItems.filter((item) => !item.Active);
+ const mappedArchivedItems = archivedItems.map(toItem);
+ await nango.batchDelete
- (mappedArchivedItems, 'Item');
+ }
+}
diff --git a/integration-templates/quickbooks/syncs/payments.ts b/integration-templates/quickbooks/syncs/payments.ts
new file mode 100644
index 00000000000..9a59b292c04
--- /dev/null
+++ b/integration-templates/quickbooks/syncs/payments.ts
@@ -0,0 +1,39 @@
+import type { NangoSync, Payment } from '../../models';
+import type { QuickBooksPayment } from '../types';
+import { paginate } from '../helpers/paginate.js';
+import { toPayment } from '../mappers/toPayment.js';
+import type { PaginationParams } from '../helpers/paginate';
+
+/**
+ * Fetches payment data from QuickBooks API and saves it in batch.
+ * Handles both active and voided payments, saving or deleting them based on their status.
+ * For detailed endpoint documentation, refer to:
+ * https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#query-a-payment
+ *
+ * @param nango The NangoSync instance used for making API calls and saving data.
+ * @returns A promise that resolves when the data has been successfully fetched and saved.
+ */
+export default async function fetchData(nango: NangoSync): Promise {
+ const config: PaginationParams = {
+ model: 'Payment'
+ };
+
+ let allPayments: QuickBooksPayment[] = [];
+
+ // Fetch all payments with pagination
+ for await (const payments of paginate(nango, config)) {
+ allPayments = [...allPayments, ...payments];
+ }
+
+ // Filter and process payments that are not voided (i.e., active payments)
+ const activePayments = allPayments.filter((payment) => !payment.PrivateNote?.includes('Voided'));
+ const mappedActivePayments = activePayments.map(toPayment);
+ await nango.batchSave(mappedActivePayments, 'Payment');
+
+ // Handle voided payments only if it's an incremental refresh
+ if (nango.lastSyncDate) {
+ const voidedPayments = allPayments.filter((payment) => payment.PrivateNote?.includes('Voided'));
+ const mappedVoidedPayments = voidedPayments.map(toPayment);
+ await nango.batchDelete(mappedVoidedPayments, 'Payment');
+ }
+}
diff --git a/integration-templates/quickbooks/types.ts b/integration-templates/quickbooks/types.ts
new file mode 100644
index 00000000000..01120e53a9b
--- /dev/null
+++ b/integration-templates/quickbooks/types.ts
@@ -0,0 +1,314 @@
+export interface QuickBooksAccount {
+ FullyQualifiedName: string;
+ domain: string;
+ Name: string;
+ Classification: string;
+ AccountSubType: string;
+ CurrencyRef: ReferenceType;
+ CurrentBalanceWithSubAccounts: number;
+ sparse: boolean;
+ MetaData: MetaData;
+ AccountType: string;
+ CurrentBalance: number;
+ Active: boolean;
+ Description?: string;
+ SyncToken: string;
+ Id: string;
+ AcctNum?: string;
+ SubAccount: boolean;
+}
+
+export interface QuickBooksCustomer {
+ Id: string;
+ SyncToken: string;
+ DisplayName: string;
+ Title?: string;
+ sparse: boolean;
+ domain: string;
+ GivenName?: string;
+ MiddleName?: string;
+ Suffix?: string;
+ FamilyName?: string;
+ PrimaryEmailAddr?: EmailAddress;
+ ResaleNum?: string;
+ SecondaryTaxIdentifier?: string;
+ ARAccountRef?: ReferenceType;
+ DefaultTaxCodeRef: ReferenceType;
+ PreferredDeliveryMethod: 'Print' | 'Email' | 'None';
+ GSTIN?: string;
+ SalesTermRef?: ReferenceType;
+ CustomerTypeRef?: ReferenceType;
+ Fax?: TelephoneNumber;
+ FreeFormNumber?: string;
+ BusinessNumber?: string;
+ BillWithParent: boolean;
+ CurrencyRef?: ReferenceType;
+ Mobile?: TelephoneNumber;
+ Job: boolean;
+ BalanceWithJobs: number;
+ PrimaryPhone?: TelephoneNumber;
+ OpenBalanceDate?: string;
+ Taxable: boolean;
+ AlternatePhone?: TelephoneNumber;
+ MetaData: MetaData;
+ ParentRef?: ReferenceType;
+ Notes?: string;
+ WebAddr?: string;
+ URI?: string;
+ Active: boolean;
+ CompanyName?: string;
+ Balance: number;
+ ShipAddr?: PhysicalAddress;
+ PaymentMethodRef?: ReferenceType;
+ IsProject?: boolean;
+ Source?: string;
+ PrimaryTaxIdentifier?: string;
+ GSTRegistrationType?: 'GST_REG_REG' | 'GST_REG_COMP' | 'GST_UNREG' | 'CONSUMER' | 'OVERSEAS' | 'SEZ' | 'DEEMED';
+ PrintOnCheckName: string;
+ BillAddr?: PhysicalAddress;
+ FullyQualifiedName: string;
+ Level?: number;
+ TaxExemptionReasonId?: number;
+}
+
+export interface ReferenceType {
+ value: string;
+ name?: string;
+}
+
+interface MetaData {
+ CreateTime: string;
+ LastUpdatedTime: string;
+}
+
+interface EmailAddress {
+ Address: string;
+}
+
+interface TelephoneNumber {
+ FreeFormNumber?: string;
+}
+
+export type PhysicalAddressCreation = Omit;
+
+export interface CreateQuickbooksCustomer extends Omit {
+ BillAddr?: PhysicalAddressCreation;
+ ShipAddr?: PhysicalAddressCreation;
+}
+
+export interface PhysicalAddress {
+ Line1?: string;
+ Line2?: string;
+ Line3?: string;
+ Line4?: string;
+ Line5?: string;
+ City?: string;
+ SubDivisionCode?: string;
+ PostalCode?: string;
+ Country?: string;
+ CountrySubDivisionCode?: string;
+ Lat?: string;
+ Long?: string;
+ Id: string;
+}
+
+interface LinkedTxn {
+ TxnId: string;
+ TxnType: string;
+}
+
+interface TaxLineDetail {
+ TaxRateRef: ReferenceType;
+ PercentBased: boolean;
+ TaxPercent: number;
+ NetAmountTaxable: number;
+}
+
+interface TaxLine {
+ Amount: number;
+ DetailType: string;
+ TaxLineDetail: TaxLineDetail;
+}
+
+interface TxnTaxDetail {
+ TxnTaxCodeRef?: ReferenceType;
+ TotalTax: number;
+ TaxLine?: TaxLine[];
+}
+
+interface CustomerMemo {
+ value: string;
+}
+
+interface LinePayment {
+ Amount: number;
+ LinkedTxn?: {
+ TxnId: string;
+ TxnType: string;
+ TxnLineId?: string;
+ }[];
+}
+
+interface CreditChargeResponse {
+ Status?: string;
+ AuthCode?: string;
+ TxnAuthorizationTime?: string;
+ CCTransId?: string;
+}
+
+interface CreditChargeInfo {
+ CcExpiryMonth?: number;
+ CcExpiryYear?: number;
+ NameOnAcct?: string;
+ Type?: string;
+ BillAddrStreet?: string;
+ Amount?: number;
+ PostalCode?: string;
+ ProcessPayment?: boolean;
+}
+
+export interface QuickBooksPayment {
+ Id: string;
+ domain: string;
+ TotalAmt: number;
+ CustomerRef: ReferenceType;
+ SyncToken: string;
+ CurrencyRef?: ReferenceType;
+ ProjectRef?: ReferenceType;
+ PrivateNote?: string;
+ PaymentMethodRef?: ReferenceType;
+ UnappliedAmt?: number;
+ DepositToAccountRef?: ReferenceType;
+ ExchangeRate?: number;
+ Line?: LinePayment[];
+ TxnSource?: string;
+ TxnDate: string;
+ CreditCardPayment?: {
+ CreditChargeResponse?: CreditChargeResponse;
+ CreditChargeInfo?: CreditChargeInfo;
+ };
+ TransactionLocationType: string;
+ Status?: 'Completed' | 'Unknown';
+ PaymentRefNum?: string;
+ TaxExemptionRef?: ReferenceType;
+ MetaData: MetaData;
+}
+
+interface QuickBooksItemGroupLine {
+ Qty: number;
+ ItemRef: ReferenceType;
+}
+
+interface QuickBooksItemGroupDetail {
+ ItemGroupLine: QuickBooksItemGroupLine[];
+}
+
+export interface QuickBooksItem {
+ FullyQualifiedName: string;
+ Sku?: string;
+ ItemCategoryType?: string;
+ domain: string;
+ Name: string;
+ TrackQtyOnHand: boolean;
+ Type: string;
+ PurchaseCost: number;
+ QtyOnHand?: number;
+ InvStartDate?: string;
+ Taxable: boolean;
+ ExpenseAccountRef?: ReferenceType;
+ AssetAccountRef?: ReferenceType;
+ IncomeAccountRef?: ReferenceType;
+ TaxClassificationRef?: ReferenceType;
+ ClassRef?: ReferenceType;
+ SalesTaxCodeRef?: ReferenceType;
+ SalesTaxIncluded?: boolean;
+ ItemGroupDetail?: QuickBooksItemGroupDetail;
+ sparse: boolean;
+ Active: boolean;
+ PrintGroupedItems?: boolean;
+ SyncToken: string;
+ UnitPrice: number;
+ Id: string;
+ Description?: string;
+ PurchaseDesc?: string;
+ UQCDisplayText?: string;
+ Source?: string;
+ MetaData: MetaData;
+}
+
+interface SalesItemLineDetail {
+ TaxCodeRef?: ReferenceType;
+ Qty?: number;
+ UnitPrice?: number;
+ DiscountRate?: number;
+ ItemRef: ReferenceType;
+}
+
+export interface LineInvoice {
+ Description?: string;
+ DetailType: string;
+ SalesItemLineDetail?: SalesItemLineDetail;
+ LinkedTxn?: LinkedTxn[];
+ SubTotalLineDetail?: object;
+ Amount: number;
+ LineNum: number;
+ Id: string;
+}
+
+export interface QuickBooksInvoice {
+ AllowIPNPayment: boolean;
+ AllowOnlinePayment: boolean;
+ AllowOnlineCreditCardPayment: boolean;
+ AllowOnlineACHPayment: boolean;
+ domain: string;
+ sparse: boolean;
+ Id: string;
+ SyncToken: string;
+ MetaData: MetaData;
+ CustomField: any[];
+ DocNumber: string;
+ TxnDate: string;
+ CurrencyRef: ReferenceType;
+ LinkedTxn: LinkedTxn[];
+ Line: LineInvoice[];
+ TxnTaxDetail: TxnTaxDetail;
+ CustomerRef: ReferenceType;
+ CustomerMemo: CustomerMemo;
+ BillAddr: PhysicalAddress;
+ ShipAddr: PhysicalAddress;
+ SalesTermRef: ReferenceType;
+ DueDate: string;
+ TotalAmt: number;
+ ApplyTaxAfterDiscount: boolean;
+ PrintStatus: string;
+ EmailStatus: string;
+ BillEmail: EmailAddress;
+ Balance: number;
+ PrivateNote?: string;
+ ProjectRef?: ReferenceType;
+ Deposit?: number;
+}
+
+export interface QuickBooksCreditMemo {
+ RemainingCredit: number;
+ domain: string;
+ sparse: boolean;
+ Id: string;
+ SyncToken: string;
+ MetaData: MetaData;
+ CustomField: any[];
+ DocNumber: string;
+ TxnDate: string;
+ CurrencyRef: ReferenceType;
+ Line: LineInvoice[];
+ TxnTaxDetail: TxnTaxDetail;
+ CustomerRef: ReferenceType;
+ ProjectRef?: ReferenceType;
+ BillAddr: PhysicalAddress;
+ ShipAddr?: PhysicalAddress;
+ TotalAmt: number;
+ ApplyTaxAfterDiscount: boolean;
+ PrintStatus: string;
+ EmailStatus: string;
+ Balance: number;
+}
diff --git a/integration-templates/quickbooks/utils/getCompany.ts b/integration-templates/quickbooks/utils/getCompany.ts
new file mode 100644
index 00000000000..befc7660a8f
--- /dev/null
+++ b/integration-templates/quickbooks/utils/getCompany.ts
@@ -0,0 +1,19 @@
+import type { NangoAction } from '../../models';
+
+/**
+ * Retrieves the realmId for the QuickBooks instance from the Nango connection configuration.
+ *
+ * @param {NangoAction} nango - The NangoAction instance for handling the get Connection task.
+ * @returns {Promise} - The realmId for the QuickBooks instance.
+ * @throws {Error} - Throws an error if the realmId cannot be retrieved.
+ */
+export async function getCompany(nango: NangoAction): Promise {
+ const connection = await nango.getConnection();
+ const realmId = connection.connection_config['realmId'];
+
+ if (realmId) {
+ return realmId;
+ }
+
+ throw new Error('realmId not found in the connection configuration. Please reauthenticate to set the realmId');
+}
diff --git a/integration-templates/quickbooks/utils/mapRefrence.ts b/integration-templates/quickbooks/utils/mapRefrence.ts
new file mode 100644
index 00000000000..2775c5f92a9
--- /dev/null
+++ b/integration-templates/quickbooks/utils/mapRefrence.ts
@@ -0,0 +1,10 @@
+/**
+ * Maps an optional reference object to a new object with default values.
+ * If the input reference is undefined, returns undefined.
+ *
+ * @param ref - The reference object to map.
+ * @returns A new object with 'value' and 'name' properties, or undefined.
+ */
+export function mapReference(ref?: { value: string; name?: string }): { value: string; name: string } | undefined {
+ return ref ? { value: ref.value, name: ref.name ?? '' } : undefined;
+}
diff --git a/integration-templates/quickbooks/utils/toDate.ts b/integration-templates/quickbooks/utils/toDate.ts
new file mode 100644
index 00000000000..648ade68955
--- /dev/null
+++ b/integration-templates/quickbooks/utils/toDate.ts
@@ -0,0 +1,22 @@
+/**
+ * Converts a date string to a formatted date string in the 'YYYY-MM-DD' format.
+ *
+ * @param {string} dateStr - The input date string to be converted. Should be a valid date string format.
+ * @returns {string} - The formatted date string in 'YYYY-MM-DD' format.
+ * @throws {Error} - If the date string is invalid or conversion fails.
+ */
+export function toDate(dateStr: string): string {
+ const date = new Date(dateStr);
+
+ if (isNaN(date.getTime())) {
+ throw new Error('Invalid date format');
+ }
+
+ const formattedDate = date.toISOString().split('T')[0];
+
+ if (formattedDate === undefined) {
+ throw new Error('Date formatting failed');
+ }
+
+ return formattedDate;
+}
diff --git a/packages/shared/flows.yaml b/packages/shared/flows.yaml
index ce927b0f767..85041bf7ca9 100644
--- a/packages/shared/flows.yaml
+++ b/packages/shared/flows.yaml
@@ -3423,6 +3423,343 @@ integrations:
picture_id: integer
label: integer
cc_email: string
+ quickbooks:
+ syncs:
+ customers:
+ description: >
+ Fetches all QuickBooks customers. Handles both active and archived
+ customers, saving or deleting them based on their status.
+ runs: every hour
+ output: Customer
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/customers
+ accounts:
+ description: >
+ Fetches all accounts in QuickBooks. Handles both active and archived
+ accounts, saving or deleting them based on their status.
+ runs: every hour
+ output: Account
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/accounts
+ payments:
+ description: >
+ Fetches all payments in QuickBooks. Handles both active and voided
+ payments, saving or deleting them based on their status.
+ runs: every hour
+ output: Payment
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/payments
+ items:
+ description: >
+ Fetches all items in QuickBooks. Handles both active and archived
+ items, saving or deleting them based on their status.
+ runs: every hour
+ output: Item
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/items
+ invoices:
+ description: >
+ Fetches all invoices in QuickBooks. Handles both active and voided
+ invoices, saving or deleting them based on their status.
+ runs: every hour
+ output: Invoice
+ sync_type: incremental
+ scopes: com.intuit.quickbooks.accounting
+ endpoint: GET /quickbooks/invoices
+ actions:
+ create-customer:
+ description: |
+ Creates a single customer in QuickBooks.
+ input: CreateCustomer
+ scopes: com.intuit.quickbooks.accounting
+ output: Customer
+ endpoint: POST /quickbooks/customer
+ update-customer:
+ description: |
+ Update a single customer in QuickBooks.
+ input: UpdateCustomer
+ scopes: com.intuit.quickbooks.accounting
+ output: Customer
+ endpoint: PUT /quickbooks/customer
+ create-item:
+ description: |
+ Creates a single item in QuickBooks.
+ input: CreateItem
+ scopes: com.intuit.quickbooks.accounting
+ output: Item
+ endpoint: POST /quickbooks/item
+ update-item:
+ description: |
+ Update a single item in QuickBooks.
+ input: UpdateItem
+ scopes: com.intuit.quickbooks.accounting
+ output: Item
+ endpoint: PUT /quickbooks/item
+ create-account:
+ description: |
+ Creates a single account in QuickBooks.
+ input: CreateAccount
+ scopes: com.intuit.quickbooks.accounting
+ output: Account
+ endpoint: POST /quickbooks/account
+ update-account:
+ description: |
+ Updates a single account in QuickBooks.
+ input: UpdateAccount
+ scopes: com.intuit.quickbooks.accounting
+ output: Account
+ endpoint: PUT /quickbooks/account
+ create-invoice:
+ description: |
+ Creates a single invoice in QuickBooks.
+ input: CreateInvoice
+ scopes: com.intuit.quickbooks.accounting
+ output: Invoice
+ endpoint: POST /quickbooks/invoice
+ update-invoice:
+ description: |
+ Updates a single invoice in QuickBooks.
+ input: UpdateInvoice
+ scopes: com.intuit.quickbooks.accounting
+ output: Invoice
+ endpoint: PUT /quickbooks/invoice
+ create-credit-memo:
+ description: |
+ Creates a single credit memo in QuickBooks.
+ input: CreateCreditMemo
+ scopes: com.intuit.quickbooks.accounting
+ output: CreditMemo
+ endpoint: POST /quickbooks/credit-memo
+ update-credit-memo:
+ description: |
+ Updates a single credit memo in QuickBooks.
+ input: UpdateCreditMemo
+ scopes: com.intuit.quickbooks.accounting
+ output: CreditMemo
+ endpoint: PUT /quickbooks/credit-memo
+ create-payment:
+ description: |
+ Creates a single payment in QuickBooks.
+ input: CreatePayment
+ scopes: com.intuit.quickbooks.accounting
+ output: Payment
+ endpoint: POST /quickbooks/payment
+ models:
+ Updates:
+ id: string
+ sync_token: string
+ BaseInvoice:
+ created_at: string
+ updated_at: string
+ id: string
+ txn_date: string
+ balance_cents: number
+ total_amt_cents: number
+ bill_address: BillAddr | null
+ items: InvoiceItem[]
+ Metadata:
+ created_at: string
+ updated_at: string
+ Reference:
+ name?: string
+ value: string
+ BillAddr:
+ city: string | null
+ line1: string | null
+ postal_code: string | null
+ country: string | null
+ id: string
+ Customer:
+ created_at: string
+ updated_at: string
+ id: string
+ given_name: string | null
+ display_name: string | null
+ active: boolean
+ balance_cents: number
+ taxable: boolean
+ primary_email: string | null
+ primary_phone: string | null
+ bill_address: BillAddr | null
+ ship_address: BillAddr | null
+ Account:
+ created_at: string
+ updated_at: string
+ id: string
+ fully_qualified_name: string
+ name: string
+ account_type: string
+ account_sub_type: string
+ classification: string
+ current_balance_cents: number
+ active: boolean
+ description: string | null
+ acct_num: string | null
+ sub_account: boolean
+ Payment:
+ created_at: string
+ updated_at: string
+ id: string
+ amount_cents: number
+ customer_name: string | null
+ txn_date: string
+ Item:
+ created_at: string
+ updated_at: string
+ id: string
+ name: string
+ active: boolean
+ type: string
+ unit_price_cents: number
+ purchase_cost_cents: number
+ qty_on_hand: number | null
+ inv_start_date: string | null
+ description: string | null
+ track_qty_onHand: boolean
+ Invoice:
+ created_at: string
+ updated_at: string
+ id: string
+ txn_date: string
+ balance_cents: number
+ total_amt_cents: number
+ bill_address: BillAddr | null
+ items: InvoiceItem[]
+ due_date: string
+ deposit_cents: number
+ InvoiceItem:
+ id: string
+ description: string | null
+ qty: number
+ unit_price_cents: number
+ amount_cents: number
+ Address:
+ line1?: string
+ line2?: string
+ city?: string
+ postal_code?: string
+ country?: string
+ lat?: string
+ long?: string
+ CreateCustomer:
+ display_name?: string
+ suffix?: string
+ title?: string
+ given_name?: string
+ company_name?: string
+ notes?: string
+ primary_email?: string
+ primary_phone?: string
+ bill_address?: Address
+ ship_address?: Address
+ UpdateCustomer:
+ id: string
+ sync_token: string
+ display_name?: string
+ suffix?: string
+ title?: string
+ given_name?: string
+ company_name?: string
+ notes?: string
+ primary_email?: string
+ primary_phone?: string
+ bill_address?: Address
+ ship_address?: Address
+ CreateItem:
+ track_qty_onHand?: boolean
+ qty_on_hand?: number
+ name: string
+ expense_accountRef?: Reference
+ income_accountRef?: Reference
+ asset_accountRef?: Reference
+ inv_start_date?: string
+ unit_price_cents?: number
+ purchase_cost_cents?: number
+ type?: string
+ UpdateItem:
+ id: string
+ sync_token: string
+ track_qty_onHand?: boolean
+ qty_on_hand?: number
+ name: string
+ expense_accountRef?: Reference
+ income_accountRef?: Reference
+ asset_accountRef?: Reference
+ inv_start_date?: string
+ unit_price_cents?: number
+ purchase_cost_cents?: number
+ type?: string
+ CreateAccount:
+ name: string
+ account_type?: string
+ account_sub_type?: string
+ description?: string
+ acct_num?: string
+ UpdateAccount:
+ id: string
+ sync_token: string
+ name: string
+ account_type?: string
+ account_sub_type?: string
+ description?: string
+ acct_num?: string
+ CreateInvoice:
+ customer_ref?: Reference
+ line?: Line[]
+ due_date?: string
+ currency_ref?: Reference
+ project_ref?: Reference
+ Line:
+ detail_type: string
+ amount_cents: number
+ sales_item_line_detail:
+ item_ref: Reference
+ quantity?: number
+ unit_price_cents?: number
+ discount_rate?: number
+ description?: string
+ UpdateInvoice:
+ id: string
+ sync_token: string
+ customer_ref?: Reference
+ line?: Line[]
+ due_date?: string
+ currency_ref?: Reference
+ project_ref?: Reference
+ CreateCreditMemo:
+ customer_ref?: Reference
+ line?: Line[]
+ due_date?: string
+ currency_ref?: Reference
+ project_ref?: Reference
+ UpdateCreditMemo:
+ id: string
+ sync_token: string
+ customer_ref?: Reference
+ line?: Line[]
+ due_date?: string
+ currency_ref?: Reference
+ project_ref?: Reference
+ CreditMemo:
+ created_at: string
+ updated_at: string
+ id: string
+ txn_date: string
+ balance_cents: number
+ total_amt_cents: number
+ bill_address: BillAddr | null
+ items: InvoiceItem[]
+ remaining_credit: number
+ customer_name: string | null
+ CreatePayment:
+ total_amount_cents: number
+ customer_ref: Reference
+ currency_ref?: Reference
+ project_ref?: Reference
salesforce:
actions:
fetch-fields:
From 5fafbe4a262ae30ce2a428e2d24776a08204d387 Mon Sep 17 00:00:00 2001
From: Robin Guldener
Date: Fri, 20 Sep 2024 12:38:30 +0200
Subject: [PATCH 2/3] feat(docs): Update cloud vs self-hosted page (#2748)
## Describe your changes
## Issue ticket number and link
## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [ ] I added tests, otherwise the reason is:
- [ ] I added observability, otherwise the reason is:
- [ ] I added analytics, otherwise the reason is:
---
docs-v2/host/cloud.mdx | 76 ++++++++++++++++++++++--------------------
1 file changed, 40 insertions(+), 36 deletions(-)
diff --git a/docs-v2/host/cloud.mdx b/docs-v2/host/cloud.mdx
index db212ea5726..14cbb70bbc0 100644
--- a/docs-v2/host/cloud.mdx
+++ b/docs-v2/host/cloud.mdx
@@ -1,59 +1,63 @@
---
title: 'Cloud vs. self-hosting'
sidebarTitle: 'Cloud vs. self-hosting'
-description: 'Overview of cloud & self-hosting options.'
+description: 'Overview of the different Nango versions (cloud or self-hosted)'
---
-| Features | Free Self-Hosting | Cloud | Enterprise Self-Hosting |
-|-|-|-|-|
-| OAuth w/ custom callback URL | ✅ | ✅ (free) | ✅ |
-| Fully white-label integrations | ✅ | ✅ (free) | ✅ |
-| API Key auth | ✅| ✅ (free) | ✅ |
-| Basic auth | ✅ | ✅ (free) | ✅ |
-| Request proxying | ✅ | ✅ (free) | ✅ |
-| Management dashboard | ✅ | ✅ (free) | ✅ |
-| Logs & monitoring | ✅ | ✅ (free) | ✅ |
-| Encryption at rest | ✅ | ✅ (free) | ✅ |
-| Programmatic API & SDKs | ✅ | ✅ (free) | ✅ |
-| Continuous data syncs | ❌ | ✅ | ✅ |
-| 2-way syncs & write backs (Actions) | ❌ | ✅ | ✅ |
-| Integration templates | ❌ | ✅ | ✅ |
-| Custom Syncs | ❌ | ✅ | ✅ |
-| Custom Actions | ❌ | ✅ | ✅ |
-| Webhooks from Nango | ❌ | ✅ | ✅ |
-| Webhooks from external APIs | ❌ | ✅ | ✅ |
-| Field mappings | ❌ | ✅ | ✅ |
-| Customization of integration per user | ❌ | ✅ | ✅ |
-| Rate-limit handling | ❌ | ✅ | ✅ |
-| Data deletion detection | ❌ | ✅ | ✅ |
-| API pagination handling | ❌ | ✅ | ✅ |
-| API response validation | ❌ | ✅ | ✅ |
-| Auto-scaling | ❌ | ✅ | ✅ |
-| Auto-upgrades | ❌ | ✅ | ✅ |
-| Multi-user & team management | ❌ | ✅ | ✅ |
-| Production-grade support | ❌ | ✅ | ✅ |
+| Features | Free (self-hosted or cloud) | Paid (self-hosted or cloud) |
+|-|-|-|
+| Best for | (O)Auth for 250+ APIs | 2-way data syncs with 250+ APIs |
+| OAuth w/ custom callback URL | ✅ | ✅ |
+| Fully white-label integrations | ✅ | ✅ |
+| API Key auth | ✅ | ✅ |
+| Basic auth | ✅ | ✅ |
+| Request proxying | ✅ | ✅ |
+| Management dashboard | ✅ | ✅ |
+| Logs & monitoring | ✅ | ✅ |
+| Encryption at rest | ✅ | ✅ |
+| Programmatic API & SDKs | ✅ | ✅ |
+| Continuous data syncs | ❌ | ✅ |
+| 2-way syncs & write backs (Actions) | ❌ | ✅ |
+| Integration templates | ❌ | ✅ |
+| Custom Syncs | ❌ | ✅ |
+| Custom Actions | ❌ | ✅ |
+| Webhooks from Nango | ❌ | ✅ |
+| Webhooks from external APIs | ❌ | ✅ |
+| Field mappings | ❌ | ✅ |
+| Customization of integration per user | ❌ | ✅ |
+| Rate-limit handling | ❌ | ✅ |
+| Data deletion detection | ❌ | ✅ |
+| API pagination handling | ❌ | ✅ |
+| API response validation | ❌ | ✅ |
+| Auto-scaling | ❌ | ✅ |
+| Auto-upgrades | ❌ | ✅ |
+| Multi-user & team management | ❌ | ✅ |
+| Production-grade support | ❌ | ✅ |
## Nango Cloud
Nango Cloud is free to use for features related to authorization and request proxying, without limitations.
-Other features around consuming & syncing data with APIs are paid, with a free tier (cf. [pricing](https://nango.dev/pricing)).
+Other features for the 2-way data syncs with APIs are paid, with a free tier (cf. [pricing](https://nango.dev/pricing)).
Sign up for a free Cloud account:
[![Try Nango Cloud](/images/nango-deploy-button.svg)](https://app.nango.dev/signup)
+
## Free Self-Hosting
-Free self-hosting features have been designed with easy self-hostability in mind, ensuring that you can deploy them independently in your own environment.
+The same features that are free on Cloud, (O)Auth for all APIs Nango supports, are also available for free self-hosted instances.
+
+Check the sidebar for guides on setting up your self-hosted Nango instance on [AWS](/host/self-host/aws), [GCP](/host/self-host/gcp) or [locally](/host/self-host/local)
-These features are widely adopted by many companies in production. They simplify the process of obtaining, storing, and refreshing credentials to access any API. The same features are available for free without limitations on Nango Cloud.
+## Paid Self-Hosting
-## Enterprise Self-Hosting
+All paid features available on Nango Cloud are also available on the paid self-hosted version. A [Scale plan](https://www.nango.dev/pricing) subscription is required.
-Enterprise self-hosting requires subscribing to a Scale plan. Instructions for Enterprise self-hosting can be communicated upon request.
+Paid self-hosting is optimized for large, regulated Enterprises.
-If you are interested in Enterprise self-hosting, please get in touch with us in the [community](https://nango.dev/slack).
+If you are interested in the paid self-hosting version, please get in touch with us in the [community](https://nango.dev/slack) or [book a demo](https://nango.dev/chat).
**Questions, problems, feedback?** Please reach out in the [Slack community](https://nango.dev/slack).
-
\ No newline at end of file
+
From 0e229c5d19cbdca27282be108e5bf6e3711ea43c Mon Sep 17 00:00:00 2001
From: Thomas Bonnin <233326+TBonnin@users.noreply.github.com>
Date: Fri, 20 Sep 2024 10:32:02 -0400
Subject: [PATCH 3/3] feat: make connectionId optional when creating a new
connection (#2746)
## Describe your changes
Making connectionId optional when creating a connection. If connectionId
is not provided a uuid is generated.
I haven't updated the documentation which is still relevant. I will do
it once we decide not passing the connectionId is the default way. With
this PR we are making it possible without promoting it yet.
## Issue ticket number and link
https://linear.app/nango/issue/NAN-1747/make-connectionid-optional-when-creating-a-new-connection
## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [ ] I added tests, otherwise the reason is:
- [ ] I added observability, otherwise the reason is:
- [ ] I added analytics, otherwise the reason is:
---
packages/frontend/lib/index.ts | 46 +++++++++++-----
.../lib/controllers/apiAuth.controller.ts | 32 +++++------
.../lib/controllers/appAuth.controller.ts | 15 ++----
.../controllers/appStoreAuth.controller.ts | 16 +++---
.../lib/controllers/auth/postTableau.ts | 12 +++--
.../server/lib/controllers/auth/postTba.ts | 8 +--
.../controllers/auth/postUnauthenticated.ts | 10 ++--
.../lib/controllers/connection.controller.ts | 28 +++++-----
.../lib/controllers/oauth.controller.ts | 53 ++++++++-----------
packages/server/lib/utils/hmac.ts | 4 +-
.../shared/lib/services/connection.service.ts | 5 ++
packages/shared/lib/services/hmac.service.ts | 5 +-
packages/types/lib/auth/http.api.ts | 6 +--
.../webapp/src/pages/Connection/Create.tsx | 11 ++--
14 files changed, 130 insertions(+), 121 deletions(-)
diff --git a/packages/frontend/lib/index.ts b/packages/frontend/lib/index.ts
index dae20d77c42..fa749200f85 100644
--- a/packages/frontend/lib/index.ts
+++ b/packages/frontend/lib/index.ts
@@ -27,9 +27,9 @@ export interface AuthResult {
isPending?: boolean;
}
-interface AuthOptions {
+type AuthOptions = {
detectClosedAuthWindow?: boolean; // If true, `nango.auth()` would fail if the login window is closed before the authorization flow is completed
-}
+} & (ConnectionConfig | OAuth2ClientCredentials | OAuthCredentialsOverride | BasicApiCredentials | ApiKeyCredentials | AppStoreCredentials);
export default class Nango {
private hostBaseUrl: string;
@@ -82,11 +82,24 @@ export default class Nango {
/**
* Creates a new unauthenticated connection using the specified provider configuration key and connection ID
* @param providerConfigKey - The key identifying the provider configuration on Nango
- * @param connectionId - The ID of the connection
+ * @param connectionId - Optional. The ID of the connection
* @param connectionConfig - Optional. Additional configuration for the connection
* @returns A promise that resolves with the authentication result
*/
- public async create(providerConfigKey: string, connectionId: string, connectionConfig?: ConnectionConfig): Promise {
+ public async create(providerConfigKey: string, connectionConfig?: ConnectionConfig): Promise;
+ public async create(providerConfigKey: string, connectionId: string, connectionConfig?: ConnectionConfig): Promise;
+ public async create(
+ providerConfigKey: string,
+ connectionIdOrConnectionConfig?: string | ConnectionConfig,
+ moreConnectionConfig?: ConnectionConfig
+ ): Promise {
+ let connectionId: string | null = null;
+ let connectionConfig: ConnectionConfig | undefined = moreConnectionConfig;
+ if (typeof connectionIdOrConnectionConfig === 'string') {
+ connectionId = connectionIdOrConnectionConfig;
+ } else {
+ connectionConfig = connectionIdOrConnectionConfig;
+ }
const url = this.hostBaseUrl + `/auth/unauthenticated/${providerConfigKey}${this.toQueryString(connectionId, connectionConfig)}`;
const res = await fetch(url, {
@@ -107,16 +120,23 @@ export default class Nango {
/**
* Initiates the authorization process for a connection
* @param providerConfigKey - The key identifying the provider configuration on Nango
- * @param connectionId - The ID of the connection for which to authorize
+ * @param connectionId - Optional. The ID of the connection for which to authorize
* @param options - Optional. Additional options for authorization
* @returns A promise that resolves with the authorization result
*/
- public auth(
- providerConfigKey: string,
- connectionId: string,
- options?: (ConnectionConfig | OAuth2ClientCredentials | OAuthCredentialsOverride | BasicApiCredentials | ApiKeyCredentials | AppStoreCredentials) &
- AuthOptions
- ): Promise {
+ public auth(providerConfigKey: string, options?: AuthOptions): Promise;
+ public auth(providerConfigKey: string, connectionId: string, options?: AuthOptions): Promise;
+ public auth(providerConfigKey: string, connectionIdOrOptions?: string | AuthOptions, moreOptions?: AuthOptions): Promise {
+ let connectionId: string | null = null;
+ let options: AuthOptions | undefined = moreOptions;
+ if (typeof connectionIdOrOptions === 'string') {
+ connectionId = connectionIdOrOptions;
+ } else {
+ options = {
+ ...options,
+ ...connectionIdOrOptions
+ };
+ }
if (
options &&
'credentials' in options &&
@@ -286,7 +306,7 @@ export default class Nango {
*/
private async customAuth(
providerConfigKey: string,
- connectionId: string,
+ connectionId: string | null,
connectionConfigWithCredentials: ConnectionConfig,
connectionConfig?: ConnectionConfig
): Promise {
@@ -430,7 +450,7 @@ export default class Nango {
* @param connectionConfig - Optional. Additional configuration for the connection
* @returns The generated query string
*/
- private toQueryString(connectionId: string, connectionConfig?: ConnectionConfig): string {
+ private toQueryString(connectionId: string | null, connectionConfig?: ConnectionConfig): string {
const query: string[] = [];
if (connectionId) {
diff --git a/packages/server/lib/controllers/apiAuth.controller.ts b/packages/server/lib/controllers/apiAuth.controller.ts
index 412d1cbf32f..db424a95a25 100644
--- a/packages/server/lib/controllers/apiAuth.controller.ts
+++ b/packages/server/lib/controllers/apiAuth.controller.ts
@@ -27,7 +27,7 @@ class ApiAuthController {
async apiKey(req: Request, res: Response>, next: NextFunction) {
const { account, environment } = res.locals;
const { providerConfigKey } = req.params;
- const connectionId = req.query['connection_id'] as string | undefined;
+ const receivedConnectionId = req.query['connection_id'] as string | undefined;
const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {};
let logCtx: LogContext | undefined;
@@ -48,12 +48,6 @@ class ApiAuthController {
return;
}
- if (!connectionId) {
- errorManager.errRes(res, 'missing_connection_id');
-
- return;
- }
-
const hmacEnabled = await hmacService.isEnabled(environment.id);
if (hmacEnabled) {
const hmac = req.query['hmac'] as string | undefined;
@@ -65,7 +59,7 @@ class ApiAuthController {
return;
}
- const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, connectionId);
+ const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, receivedConnectionId);
if (!verified) {
await logCtx.error('Invalid HMAC');
await logCtx.failed();
@@ -76,6 +70,8 @@ class ApiAuthController {
}
}
+ const connectionId = receivedConnectionId || connectionService.generateConnectionId();
+
const config = await configService.getProviderConfig(providerConfigKey, environment.id);
if (config == null) {
@@ -176,7 +172,7 @@ class ApiAuthController {
if (logCtx) {
void connectionCreationFailedHook(
{
- connection: { connection_id: connectionId!, provider_config_key: providerConfigKey! },
+ connection: { connection_id: receivedConnectionId!, provider_config_key: providerConfigKey! },
environment,
account,
auth_mode: 'API_KEY',
@@ -199,7 +195,7 @@ class ApiAuthController {
environmentId: environment.id,
metadata: {
providerConfigKey,
- connectionId
+ receivedConnectionId
}
});
@@ -210,7 +206,7 @@ class ApiAuthController {
async basic(req: Request, res: Response>, next: NextFunction) {
const { account, environment } = res.locals;
const { providerConfigKey } = req.params;
- const connectionId = req.query['connection_id'] as string | undefined;
+ const receivedConnectionId = req.query['connection_id'] as string | undefined;
const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {};
let logCtx: LogContext | undefined;
@@ -232,12 +228,6 @@ class ApiAuthController {
return;
}
- if (!connectionId) {
- errorManager.errRes(res, 'missing_connection_id');
-
- return;
- }
-
const hmacEnabled = await hmacService.isEnabled(environment.id);
if (hmacEnabled) {
const hmac = req.query['hmac'] as string | undefined;
@@ -249,7 +239,7 @@ class ApiAuthController {
return;
}
- const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, connectionId);
+ const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, receivedConnectionId);
if (!verified) {
await logCtx.error('Invalid HMAC');
await logCtx.failed();
@@ -259,6 +249,8 @@ class ApiAuthController {
}
}
+ const connectionId = receivedConnectionId || connectionService.generateConnectionId();
+
const { username = '', password = '' } = req.body;
const config = await configService.getProviderConfig(providerConfigKey, environment.id);
@@ -354,7 +346,7 @@ class ApiAuthController {
if (logCtx) {
void connectionCreationFailedHook(
{
- connection: { connection_id: connectionId!, provider_config_key: providerConfigKey! },
+ connection: { connection_id: receivedConnectionId!, provider_config_key: providerConfigKey! },
environment,
account,
auth_mode: 'API_KEY',
@@ -377,7 +369,7 @@ class ApiAuthController {
environmentId: environment.id,
metadata: {
providerConfigKey,
- connectionId
+ connectionId: receivedConnectionId
}
});
diff --git a/packages/server/lib/controllers/appAuth.controller.ts b/packages/server/lib/controllers/appAuth.controller.ts
index 83a318ea81d..6cc011bfe55 100644
--- a/packages/server/lib/controllers/appAuth.controller.ts
+++ b/packages/server/lib/controllers/appAuth.controller.ts
@@ -58,21 +58,16 @@ class AppAuthController {
void analytics.track(AnalyticsTypes.PRE_APP_AUTH, account.id);
- const { providerConfigKey, connectionId, webSocketClientId: wsClientId } = session;
+ const { providerConfigKey, connectionId: receivedConnectionId, webSocketClientId: wsClientId } = session;
const logCtx = await logContextGetter.get({ id: session.activityLogId });
try {
if (!providerConfigKey) {
errorManager.errRes(res, 'missing_connection');
-
return;
}
- if (!connectionId) {
- errorManager.errRes(res, 'missing_connection_id');
-
- return;
- }
+ const connectionId = receivedConnectionId || connectionService.generateConnectionId();
const config = await configService.getProviderConfig(providerConfigKey, environment.id);
@@ -226,12 +221,12 @@ class AppAuthController {
await telemetry.log(LogTypes.AUTH_TOKEN_REQUEST_FAILURE, `App auth request process failed ${content}`, LogActionEnum.AUTH, {
environmentId: String(environment.id),
providerConfigKey: String(providerConfigKey),
- connectionId: String(connectionId)
+ connectionId: String(receivedConnectionId)
});
void connectionCreationFailedHook(
{
- connection: { connection_id: connectionId, provider_config_key: providerConfigKey },
+ connection: { connection_id: receivedConnectionId, provider_config_key: providerConfigKey },
environment,
account,
auth_mode: 'APP',
@@ -245,7 +240,7 @@ class AppAuthController {
logCtx
);
- return publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, WSErrBuilder.UnknownError(prettyError));
+ return publisher.notifyErr(res, wsClientId, providerConfigKey, receivedConnectionId, WSErrBuilder.UnknownError(prettyError));
}
}
}
diff --git a/packages/server/lib/controllers/appStoreAuth.controller.ts b/packages/server/lib/controllers/appStoreAuth.controller.ts
index b0fc621fcc9..c006f86e1f6 100644
--- a/packages/server/lib/controllers/appStoreAuth.controller.ts
+++ b/packages/server/lib/controllers/appStoreAuth.controller.ts
@@ -21,7 +21,7 @@ class AppStoreAuthController {
async auth(req: Request, res: Response>, next: NextFunction) {
const { environment, account } = res.locals;
const { providerConfigKey } = req.params;
- const connectionId = req.query['connection_id'] as string | undefined;
+ const receivedConnectionId = req.query['connection_id'] as string | undefined;
let logCtx: LogContext | undefined;
@@ -42,12 +42,6 @@ class AppStoreAuthController {
return;
}
- if (!connectionId) {
- errorManager.errRes(res, 'missing_connection_id');
-
- return;
- }
-
const hmacEnabled = await hmacService.isEnabled(environment.id);
if (hmacEnabled) {
const hmac = req.query['hmac'] as string | undefined;
@@ -59,7 +53,7 @@ class AppStoreAuthController {
return;
}
- const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, connectionId);
+ const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, receivedConnectionId);
if (!verified) {
await logCtx.error('Invalid HMAC');
await logCtx.failed();
@@ -70,6 +64,8 @@ class AppStoreAuthController {
}
}
+ const connectionId = receivedConnectionId || connectionService.generateConnectionId();
+
const config = await configService.getProviderConfig(providerConfigKey, environment.id);
if (config == null) {
@@ -185,7 +181,7 @@ class AppStoreAuthController {
void connectionCreationFailedHook(
{
- connection: { connection_id: connectionId!, provider_config_key: providerConfigKey! },
+ connection: { connection_id: receivedConnectionId!, provider_config_key: providerConfigKey! },
environment,
account,
auth_mode: 'APP_STORE',
@@ -209,7 +205,7 @@ class AppStoreAuthController {
environmentId: environment.id,
metadata: {
providerConfigKey,
- connectionId
+ connectionId: receivedConnectionId
}
});
diff --git a/packages/server/lib/controllers/auth/postTableau.ts b/packages/server/lib/controllers/auth/postTableau.ts
index 7e524f06498..39a9522656e 100644
--- a/packages/server/lib/controllers/auth/postTableau.ts
+++ b/packages/server/lib/controllers/auth/postTableau.ts
@@ -30,7 +30,7 @@ const bodyValidation = z
const queryStringValidation = z
.object({
- connection_id: connectionIdSchema,
+ connection_id: connectionIdSchema.optional(),
params: z.record(z.any()).optional(),
public_key: z.string().uuid(),
hmac: z.string().optional()
@@ -70,7 +70,7 @@ export const postPublicTableauAuthorization = asyncWrapper>, next: NextFunction) {
try {
const { environment, account } = res.locals;
- const { connection_id, provider_config_key, metadata, connection_config } = req.body;
+ const { provider_config_key, metadata, connection_config } = req.body;
- if (!connection_id) {
- errorManager.errRes(res, 'missing_connection');
- return;
- }
+ const connectionId = (req.body['connection_id'] as string) || connectionService.generateConnectionId();
if (!provider_config_key) {
errorManager.errRes(res, 'missing_provider_config');
@@ -359,7 +356,7 @@ class ConnectionController {
};
const [imported] = await connectionService.importOAuthConnection({
- connectionId: connection_id,
+ connectionId,
providerConfigKey: provider_config_key,
provider: providerName,
metadata,
@@ -422,7 +419,7 @@ class ConnectionController {
};
const [imported] = await connectionService.importOAuthConnection({
- connectionId: connection_id,
+ connectionId,
providerConfigKey: provider_config_key,
provider: providerName,
metadata,
@@ -471,7 +468,7 @@ class ConnectionController {
};
const [imported] = await connectionService.importOAuthConnection({
- connectionId: connection_id,
+ connectionId,
providerConfigKey: provider_config_key,
provider: providerName,
metadata,
@@ -513,7 +510,7 @@ class ConnectionController {
);
};
const [imported] = await connectionService.importApiAuthConnection({
- connectionId: connection_id,
+ connectionId,
providerConfigKey: provider_config_key,
provider: providerName,
metadata,
@@ -555,7 +552,7 @@ class ConnectionController {
};
const [imported] = await connectionService.importApiAuthConnection({
- connectionId: connection_id,
+ connectionId,
providerConfigKey: provider_config_key,
provider: providerName,
metadata,
@@ -602,7 +599,7 @@ class ConnectionController {
}
const [imported] = await connectionService.upsertConnection({
- connectionId: connection_id,
+ connectionId,
providerConfigKey: provider_config_key,
provider: providerName,
parsedRawCredentials: credentials as unknown as AuthCredentials,
@@ -650,7 +647,7 @@ class ConnectionController {
}
const [imported] = await connectionService.upsertTbaConnection({
- connectionId: connection_id,
+ connectionId,
providerConfigKey: provider_config_key,
credentials: tbaCredentials,
connectionConfig: {
@@ -670,7 +667,7 @@ class ConnectionController {
}
} else if (provider.auth_mode === 'NONE') {
const [imported] = await connectionService.upsertUnauthConnection({
- connectionId: connection_id,
+ connectionId,
providerConfigKey: provider_config_key,
provider: providerName,
environment,
@@ -702,7 +699,10 @@ class ConnectionController {
);
}
- res.status(201).send(req.body);
+ res.status(201).send({
+ ...req.body,
+ connection_id: connectionId
+ });
} catch (err) {
next(err);
}
diff --git a/packages/server/lib/controllers/oauth.controller.ts b/packages/server/lib/controllers/oauth.controller.ts
index 4631a82ce2c..c8e46ef97bc 100644
--- a/packages/server/lib/controllers/oauth.controller.ts
+++ b/packages/server/lib/controllers/oauth.controller.ts
@@ -54,7 +54,7 @@ class OAuthController {
const accountId = account.id;
const environmentId = environment.id;
const { providerConfigKey } = req.params;
- let connectionId = req.query['connection_id'] as string | undefined;
+ const receivedConnectionId = req.query['connection_id'] as string | undefined;
const wsClientId = req.query['ws_client_id'] as string | undefined;
const userScope = req.query['user_scope'] as string | undefined;
@@ -77,7 +77,7 @@ class OAuthController {
environmentId: String(environmentId),
accountId: String(accountId),
providerConfigKey: String(providerConfigKey),
- connectionId: String(connectionId)
+ connectionId: String(receivedConnectionId)
});
const callbackUrl = await getOauthCallbackUrl(environmentId);
@@ -85,22 +85,13 @@ class OAuthController {
const authorizationParams = req.query['authorization_params'] != null ? getAdditionalAuthorizationParams(req.query['authorization_params']) : {};
const overrideCredentials = req.query['credentials'] != null ? getAdditionalAuthorizationParams(req.query['credentials']) : {};
- if (connectionId == null) {
- const error = WSErrBuilder.MissingConnectionId();
- await logCtx.error(error.message);
- await logCtx.failed();
-
- return publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, error);
- } else if (providerConfigKey == null) {
+ if (providerConfigKey == null) {
const error = WSErrBuilder.MissingProviderConfigKey();
await logCtx.error(error.message);
await logCtx.failed();
- return publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, error);
+ return publisher.notifyErr(res, wsClientId, providerConfigKey, receivedConnectionId, error);
}
-
- connectionId = connectionId.toString();
-
const hmacEnabled = await hmacService.isEnabled(environmentId);
if (hmacEnabled) {
const hmac = req.query['hmac'] as string | undefined;
@@ -109,19 +100,21 @@ class OAuthController {
await logCtx.error(error.message);
await logCtx.failed();
- return publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, error);
+ return publisher.notifyErr(res, wsClientId, providerConfigKey, receivedConnectionId, error);
}
- const verified = await hmacService.verify(hmac, environmentId, providerConfigKey, connectionId);
+ const verified = await hmacService.verify(hmac, environmentId, providerConfigKey, receivedConnectionId);
if (!verified) {
const error = WSErrBuilder.InvalidHmac();
await logCtx.error(error.message);
await logCtx.failed();
- return publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, error);
+ return publisher.notifyErr(res, wsClientId, providerConfigKey, receivedConnectionId, error);
}
}
+ const connectionId = receivedConnectionId || connectionService.generateConnectionId();
+
await logCtx.info('Authorization URL request from the client');
const config = await configService.getProviderConfig(providerConfigKey, environmentId);
@@ -240,18 +233,18 @@ class OAuthController {
environmentId,
metadata: {
providerConfigKey,
- connectionId
+ connectionId: receivedConnectionId
}
});
- return publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, WSErrBuilder.UnknownError(prettyError));
+ return publisher.notifyErr(res, wsClientId, providerConfigKey, receivedConnectionId, WSErrBuilder.UnknownError(prettyError));
}
}
public async oauth2RequestCC(req: Request, res: Response>, next: NextFunction) {
const { environment, account } = res.locals;
const { providerConfigKey } = req.params;
- const connectionId = req.query['connection_id'] as string | undefined;
+ const receivedConnectionId = req.query['connection_id'] as string | undefined;
const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {};
const body = req.body;
@@ -288,12 +281,6 @@ class OAuthController {
return;
}
- if (!connectionId) {
- errorManager.errRes(res, 'missing_connection_id');
-
- return;
- }
-
const hmacEnabled = await hmacService.isEnabled(environment.id);
if (hmacEnabled) {
const hmac = req.query['hmac'] as string | undefined;
@@ -305,7 +292,7 @@ class OAuthController {
return;
}
- const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, connectionId);
+ const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, receivedConnectionId);
if (!verified) {
await logCtx.error('Invalid HMAC');
await logCtx.failed();
@@ -316,6 +303,8 @@ class OAuthController {
}
}
+ const connectionId = receivedConnectionId || connectionService.generateConnectionId();
+
const config = await configService.getProviderConfig(providerConfigKey, environment.id);
if (!config) {
@@ -408,7 +397,7 @@ class OAuthController {
void connectionCreationFailedHook(
{
- connection: { connection_id: connectionId!, provider_config_key: providerConfigKey! },
+ connection: { connection_id: receivedConnectionId!, provider_config_key: providerConfigKey! },
environment,
account,
auth_mode: 'OAUTH2_CC',
@@ -432,7 +421,7 @@ class OAuthController {
environmentId: environment.id,
metadata: {
providerConfigKey,
- connectionId
+ connectionId: receivedConnectionId
}
});
@@ -712,8 +701,8 @@ class OAuthController {
const redirectUrl = oAuth1Client.getAuthorizationURL(tokenResult, oAuth1CallbackURL);
await logCtx.info('Successfully requested token. Redirecting...', {
- providerConfigKey: session.providerConfigKey,
- connectionId: session.connectionId,
+ providerConfigKey,
+ connectionId,
redirectUrl
});
@@ -1158,8 +1147,8 @@ class OAuthController {
operation: LogActionEnum.AUTH,
environmentId: session.environmentId,
metadata: {
- providerConfigKey: session.providerConfigKey,
- connectionId: session.connectionId
+ providerConfigKey,
+ connectionId
}
});
diff --git a/packages/server/lib/utils/hmac.ts b/packages/server/lib/utils/hmac.ts
index 95a5ada4304..24efb0ecb1c 100644
--- a/packages/server/lib/utils/hmac.ts
+++ b/packages/server/lib/utils/hmac.ts
@@ -14,7 +14,7 @@ export async function hmacCheck({
environment: DBEnvironment;
logCtx: LogContext;
providerConfigKey: string;
- connectionId: string;
+ connectionId: string | undefined;
hmac: string | undefined;
res: Response;
}) {
@@ -28,7 +28,7 @@ export async function hmacCheck({
return;
}
- const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, connectionId);
+ const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, ...(connectionId ? [connectionId] : []));
if (!verified) {
await logCtx.error('Invalid HMAC');
await logCtx.failed();
diff --git a/packages/shared/lib/services/connection.service.ts b/packages/shared/lib/services/connection.service.ts
index c5e991c23e2..8e0b596432e 100644
--- a/packages/shared/lib/services/connection.service.ts
+++ b/packages/shared/lib/services/connection.service.ts
@@ -55,6 +55,7 @@ import { CONNECTIONS_WITH_SCRIPTS_CAP_LIMIT } from '../constants.js';
import type { Orchestrator } from '../clients/orchestrator.js';
import { SlackService } from './notification/slack.service.js';
import { getProvider } from './providers.js';
+import { v4 as uuidv4 } from 'uuid';
const logger = getLogger('Connection');
const ACTIVE_LOG_TABLE = dbNamespace + 'active_logs';
@@ -69,6 +70,10 @@ class ConnectionService {
this.locking = locking;
}
+ public generateConnectionId(): string {
+ return uuidv4();
+ }
+
public async upsertConnection({
connectionId,
providerConfigKey,
diff --git a/packages/shared/lib/services/hmac.service.ts b/packages/shared/lib/services/hmac.service.ts
index a2a190151d4..5b2cf6f8c6b 100644
--- a/packages/shared/lib/services/hmac.service.ts
+++ b/packages/shared/lib/services/hmac.service.ts
@@ -21,8 +21,9 @@ class HmacService {
return key;
}
- async verify(expectedDigest: string, id: number, ...values: string[]): Promise {
- const actualDigest = await this.digest(id, ...values);
+ async verify(expectedDigest: string, id: number, ...values: (string | undefined)[]): Promise {
+ const definedValues: string[] = values.flatMap((v) => (v === undefined ? [] : [v]));
+ const actualDigest = await this.digest(id, ...definedValues);
return expectedDigest === actualDigest;
}
diff --git a/packages/types/lib/auth/http.api.ts b/packages/types/lib/auth/http.api.ts
index 27c9b1f4629..9b8048b2310 100644
--- a/packages/types/lib/auth/http.api.ts
+++ b/packages/types/lib/auth/http.api.ts
@@ -9,7 +9,7 @@ export type PostPublicTbaAuthorization = Endpoint<{
oauth_client_secret_override?: string | undefined;
};
Querystring: {
- connection_id: string;
+ connection_id?: string | undefined;
public_key: string;
params?: Record | undefined;
hmac?: string | undefined;
@@ -39,7 +39,7 @@ export type PostPublicTableauAuthorization = Endpoint<{
content_url?: string | undefined;
};
Querystring: {
- connection_id: string;
+ connection_id?: string | undefined;
public_key: string;
params?: Record | undefined;
hmac?: string | undefined;
@@ -64,7 +64,7 @@ export type PostPublicTableauAuthorization = Endpoint<{
export type PostPublicUnauthenticatedAuthorization = Endpoint<{
Method: 'POST';
Querystring: {
- connection_id: string;
+ connection_id?: string | undefined;
hmac?: string | undefined;
};
Params: {
diff --git a/packages/webapp/src/pages/Connection/Create.tsx b/packages/webapp/src/pages/Connection/Create.tsx
index 9d658722d39..8005e713c57 100644
--- a/packages/webapp/src/pages/Connection/Create.tsx
+++ b/packages/webapp/src/pages/Connection/Create.tsx
@@ -205,14 +205,19 @@ export default function IntegrationCreate() {
content_url: contentUrl
};
}
-
- nango[authMode === 'NONE' ? 'create' : 'auth'](target.integration_unique_key.value, target.connection_id.value, {
+ const connectionConfig = {
user_scope: authMode === 'NONE' ? undefined : selectedScopes || [],
params,
authorization_params: authorizationParams || {},
hmac: hmacDigest || '',
credentials
- })
+ };
+ const getConnection =
+ authMode === 'NONE'
+ ? nango.create(target.integration_unique_key.value, target.connection_id.value, connectionConfig)
+ : nango.auth(target.integration_unique_key.value, target.connection_id.value, connectionConfig);
+
+ getConnection
.then(() => {
toast.success('Connection created!', { position: toast.POSITION.BOTTOM_CENTER });
analyticsTrack('web:connection_created', { provider: integration?.provider || 'unknown' });