From 48e0962dbc3b47e99dafbea7daa22cc71f1c2d08 Mon Sep 17 00:00:00 2001 From: Hassan_Wari Date: Tue, 17 Sep 2024 19:15:07 +0300 Subject: [PATCH] feat(integration-templates): add quickbooks actions & sycns --- .../integration-templates/quickbooks.mdx | 21 ++ docs-v2/mint.json | 1 + .../quickbooks/actions/create-account.ts | 46 +++ .../quickbooks/actions/create-credit-memo.ts | 73 ++++ .../quickbooks/actions/create-customer.ts | 46 +++ .../quickbooks/actions/create-invoice.ts | 73 ++++ .../quickbooks/actions/create-item.ts | 46 +++ .../quickbooks/actions/create-payment.ts | 52 +++ .../quickbooks/actions/update-account.ts | 46 +++ .../quickbooks/actions/update-credit-memo.ts | 46 +++ .../quickbooks/actions/update-customer.ts | 46 +++ .../quickbooks/actions/update-invoice.ts | 46 +++ .../quickbooks/actions/update-item.ts | 46 +++ .../quickbooks/fixtures/create-account.json | 7 + .../fixtures/create-credit-memo.json | 25 ++ .../quickbooks/fixtures/create-customer.json | 20 + .../quickbooks/fixtures/create-invoice.json | 40 ++ .../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 | 118 ++++++ .../quickbooks/mappers/toAccount.ts | 63 ++++ .../quickbooks/mappers/toCreditMemo.ts | 113 ++++++ .../quickbooks/mappers/toCustomer.ts | 136 +++++++ .../quickbooks/mappers/toInvoice.ts | 122 +++++++ .../quickbooks/mappers/toItem.ts | 109 ++++++ .../quickbooks/mappers/toPayment.ts | 60 +++ integration-templates/quickbooks/nango.yaml | 276 ++++++++++++++ .../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 | 306 ++++++++++++++++ .../quickbooks/utils/getCompany.ts | 19 + .../quickbooks/utils/toDate.ts | 14 + packages/shared/flows.yaml | 342 ++++++++++++++++++ 41 files changed, 2667 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/toDate.ts 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 5793b99e45a..7a2c7b96c9c 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..b822aff335b --- /dev/null +++ b/integration-templates/quickbooks/actions/create-account.ts @@ -0,0 +1,46 @@ +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 { + const companyId = await getCompany(nango); + + // 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)}` + }); + } + + // Map the account input to the QuickBooks account structure + const quickBooksAccount = toQuickBooksAccount(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..756f1f775c9 --- /dev/null +++ b/integration-templates/quickbooks/actions/create-credit-memo.ts @@ -0,0 +1,73 @@ +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 { + const companyId = await getCompany(nango); + + // 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)}` + }); + } + } + + // Map the credit memo input to the QuickBooks credit memo structure + const quickBooksInvoice = toQuickBooksCreditMemo(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..b0b1556693e --- /dev/null +++ b/integration-templates/quickbooks/actions/create-customer.ts @@ -0,0 +1,46 @@ +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 { + const companyId = await getCompany(nango); + + // 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)}` + }); + } + + // Map the customer input to the QuickBooks customer structure + const quickBooksCustomer = toQuickBooksCustomer(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..2600ffb35f2 --- /dev/null +++ b/integration-templates/quickbooks/actions/create-invoice.ts @@ -0,0 +1,73 @@ +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 { + const companyId = await getCompany(nango); + + // 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)}` + }); + } + } + + // Map the invoice input to the QuickBooks invoice structure + const quickBooksInvoice = toQuickBooksInvoice(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..1cbf282e47c --- /dev/null +++ b/integration-templates/quickbooks/actions/create-item.ts @@ -0,0 +1,46 @@ +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 { + const companyId = await getCompany(nango); + + // 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)}` + }); + } + + // Map the item input to the QuickBooks item structure + const quickBooksItem = toQuickBooksItem(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..1b24415bcc4 --- /dev/null +++ b/integration-templates/quickbooks/actions/create-payment.ts @@ -0,0 +1,52 @@ +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 { + const companyId = await getCompany(nango); + + // 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)}` + }); + } + + // Map the invoice input to the QuickBooks invoice structure + const quickBooksPayment = toQuickBooksPayment(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..096bfd945f1 --- /dev/null +++ b/integration-templates/quickbooks/actions/update-account.ts @@ -0,0 +1,46 @@ +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 { + const companyId = await getCompany(nango); + + // 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)}` + }); + } + + // Map the account input to the QuickBooks account structure + const quickBooksAccount = toQuickBooksAccount(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..ba6012271b3 --- /dev/null +++ b/integration-templates/quickbooks/actions/update-credit-memo.ts @@ -0,0 +1,46 @@ +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 { + const companyId = await getCompany(nango); + + // 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.` + }); + } + + // Map the credit memo input to the QuickBooks credit memo structure + const quickBooksInvoice = toQuickBooksCreditMemo(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..f0759d3eec0 --- /dev/null +++ b/integration-templates/quickbooks/actions/update-customer.ts @@ -0,0 +1,46 @@ +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 { + const companyId = await getCompany(nango); + + // 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)}` + }); + } + + // Map the customer input to the QuickBooks customer structure + const quickBooksCustomer = toQuickBooksCustomer(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..9a7dba65a27 --- /dev/null +++ b/integration-templates/quickbooks/actions/update-invoice.ts @@ -0,0 +1,46 @@ +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 { + const companyId = await getCompany(nango); + + // 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.` + }); + } + + // Map the invoice input to the QuickBooks invoice structure + const quickBooksInvoice = toQuickBooksInvoice(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..19d57370a8b --- /dev/null +++ b/integration-templates/quickbooks/actions/update-item.ts @@ -0,0 +1,46 @@ +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 { + const companyId = await getCompany(nango); + + // 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)}` + }); + } + + // Map the customer input to the QuickBooks customer structure + const quickBooksItem = toQuickBooksItem(input); + + const config: ProxyConfiguration = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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..13f13233cee --- /dev/null +++ b/integration-templates/quickbooks/fixtures/create-account.json @@ -0,0 +1,7 @@ +{ + "name": "Updated Account Name 6", + "account_type": "Accounts Receivable", + "account_sub_type": "AccountsPayable", + "acct_num": "567890000", + "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..397414b94a8 --- /dev/null +++ b/integration-templates/quickbooks/fixtures/create-credit-memo.json @@ -0,0 +1,25 @@ +{ + "customer_ref": { + "value": "73", + "name": "Mrps" + }, + "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..106e8d6b12c --- /dev/null +++ b/integration-templates/quickbooks/fixtures/create-customer.json @@ -0,0 +1,20 @@ +{ + "display_name": "John Doe - 6", + "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..995ecaed3dd --- /dev/null +++ b/integration-templates/quickbooks/fixtures/create-invoice.json @@ -0,0 +1,40 @@ +{ + "customer_ref": { + "value": "1", + "name": "John Doe" + }, + "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, + "description": "Purchase of Widget B" + } + ], + "due_date": "2024-10-01", + "currency_ref": { + "value": "USD", + "name": "US Dollar" + } +} diff --git a/integration-templates/quickbooks/fixtures/create-item.json b/integration-templates/quickbooks/fixtures/create-item.json new file mode 100644 index 00000000000..fdcc5846e9e --- /dev/null +++ b/integration-templates/quickbooks/fixtures/create-item.json @@ -0,0 +1,21 @@ +{ + "track_qty_onHand": true, + "name": "Timber Test 1", + "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..7866b360c6c --- /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", + "acct_num": "1111222333444222", + "id": "100", + "sync_token": "2" +} 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..d827d51d0d5 --- /dev/null +++ b/integration-templates/quickbooks/fixtures/update-credit-memo.json @@ -0,0 +1,23 @@ +{ + "sync_token": "2", + "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 A" + } + ], + "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..268eefaf162 --- /dev/null +++ b/integration-templates/quickbooks/fixtures/update-customer.json @@ -0,0 +1,12 @@ +{ + "display_name": "John Doe 2", + "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..1162ae09d85 --- /dev/null +++ b/integration-templates/quickbooks/fixtures/update-invoice.json @@ -0,0 +1,34 @@ +{ + "sync_token": "1", + "id": "167", + "due_date": "2015-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..1c45d808f36 --- /dev/null +++ b/integration-templates/quickbooks/fixtures/update-item.json @@ -0,0 +1,7 @@ +{ + "sync_token": "0", + "name": "Tree Test 1", + "id": "32", + "unit_price_cents": 1001, + "purchase_cost_cents": 901 +} diff --git a/integration-templates/quickbooks/helpers/paginate.ts b/integration-templates/quickbooks/helpers/paginate.ts new file mode 100644 index 00000000000..1ef345fecef --- /dev/null +++ b/integration-templates/quickbooks/helpers/paginate.ts @@ -0,0 +1,118 @@ +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. + */ +async function* paginate( + nango: NangoSync, + { model, initialPage = 1, maxResults = 10, 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 = { + baseUrlOverride: 'https://sandbox-quickbooks.api.intuit.com', + 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}'`; +} + +export default paginate; diff --git a/integration-templates/quickbooks/mappers/toAccount.ts b/integration-templates/quickbooks/mappers/toAccount.ts new file mode 100644 index 00000000000..c8aa6d962ff --- /dev/null +++ b/integration-templates/quickbooks/mappers/toAccount.ts @@ -0,0 +1,63 @@ +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: any = {}; + + 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; + } + + return quickBooksAccount; +} diff --git a/integration-templates/quickbooks/mappers/toCreditMemo.ts b/integration-templates/quickbooks/mappers/toCreditMemo.ts new file mode 100644 index 00000000000..abc7fab012c --- /dev/null +++ b/integration-templates/quickbooks/mappers/toCreditMemo.ts @@ -0,0 +1,113 @@ +import type { CreditMemo, CreateCreditMemo, UpdateCreditMemo } from '../../models'; +import type { QuickBooksCreditMemo, LineInvoice } from '../types'; + +/** + * 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: any = {}; + + // 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; + } + + if (creditMemo.customer_ref) { + quickBooksCreditMemo.CustomerRef = { + value: creditMemo.customer_ref.value, + name: creditMemo.customer_ref.name + }; + } + + if (creditMemo.line) { + quickBooksCreditMemo.Line = creditMemo.line.map((line) => { + const qbLine: any = {}; + 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; + }); + } + + if (creditMemo.currency_ref) { + quickBooksCreditMemo.CurrencyRef = { + value: creditMemo.currency_ref.value, + name: creditMemo.currency_ref.name + }; + } + + if (creditMemo.project_ref) { + quickBooksCreditMemo.ProjectRef = { + value: creditMemo.project_ref.value, + name: creditMemo.project_ref.name + }; + } + + return quickBooksCreditMemo; +} diff --git a/integration-templates/quickbooks/mappers/toCustomer.ts b/integration-templates/quickbooks/mappers/toCustomer.ts new file mode 100644 index 00000000000..44ad8d2173b --- /dev/null +++ b/integration-templates/quickbooks/mappers/toCustomer.ts @@ -0,0 +1,136 @@ +import type { Customer, CreateCustomer, UpdateCustomer } from '../../models'; +import type { QuickBooksCustomer } 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): QuickBooksCustomer { + const quickBooksCustomer: any = {}; + + if ('id' in customer && 'sync_token' in customer) { + const updateCustomer = customer; + quickBooksCustomer.Id = updateCustomer.id; + quickBooksCustomer.SyncToken = updateCustomer.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 = {}; + if (customer.bill_address.line1) { + quickBooksCustomer.BillAddr.Line1 = customer.bill_address.line1; + } + if (customer.bill_address.line2) { + quickBooksCustomer.BillAddr.Line2 = customer.bill_address.line2; + } + if (customer.bill_address.city) { + quickBooksCustomer.BillAddr.City = customer.bill_address.city; + } + if (customer.bill_address.postal_code) { + quickBooksCustomer.BillAddr.PostalCode = customer.bill_address.postal_code; + } + if (customer.bill_address.country) { + quickBooksCustomer.BillAddr.Country = customer.bill_address.country; + } + } + + if (customer.ship_address) { + quickBooksCustomer.ShipAddr = {}; + if (customer.ship_address.line1) { + quickBooksCustomer.ShipAddr.Line1 = customer.ship_address.line1; + } + if (customer.ship_address.line2) { + quickBooksCustomer.ShipAddr.Line2 = customer.ship_address.line2; + } + if (customer.ship_address.city) { + quickBooksCustomer.ShipAddr.City = customer.ship_address.city; + } + if (customer.ship_address.postal_code) { + quickBooksCustomer.ShipAddr.PostalCode = customer.ship_address.postal_code; + } + if (customer.ship_address.country) { + quickBooksCustomer.ShipAddr.Country = customer.ship_address.country; + } + } + + if (customer.notes) { + quickBooksCustomer.Notes = customer.notes; + } + + return quickBooksCustomer; +} diff --git a/integration-templates/quickbooks/mappers/toInvoice.ts b/integration-templates/quickbooks/mappers/toInvoice.ts new file mode 100644 index 00000000000..adc787bcddc --- /dev/null +++ b/integration-templates/quickbooks/mappers/toInvoice.ts @@ -0,0 +1,122 @@ +import type { Invoice, CreateInvoice, UpdateInvoice } from '../../models'; +import type { QuickBooksInvoice, LineInvoice } from '../types'; +import { toDate } from '../utils/toDate.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: any = {}; + + // 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; + } + + if (invoice.customer_ref) { + quickBooksInvoice.CustomerRef = { + value: invoice.customer_ref.value, + name: invoice.customer_ref.name + }; + } + + if (invoice.due_date) { + quickBooksInvoice.DueDate = toDate(invoice.due_date); + } + + if (invoice.line) { + quickBooksInvoice.Line = invoice.line.map((line) => { + const qbLine: any = {}; + 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; + }); + } + + if (invoice.currency_ref) { + quickBooksInvoice.CurrencyRef = { + value: invoice.currency_ref.value, + name: invoice.currency_ref.name + }; + } + + if (invoice.project_ref) { + quickBooksInvoice.ProjectRef = { + value: invoice.project_ref.value, + name: invoice.project_ref.name + }; + } + + return quickBooksInvoice; +} diff --git a/integration-templates/quickbooks/mappers/toItem.ts b/integration-templates/quickbooks/mappers/toItem.ts new file mode 100644 index 00000000000..bd3a1355acc --- /dev/null +++ b/integration-templates/quickbooks/mappers/toItem.ts @@ -0,0 +1,109 @@ +import type { Item, CreateItem, UpdateItem } from '../../models'; +import type { QuickBooksItem } from '../types'; +import { toDate } from '../utils/toDate.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: any = {}; + + 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; + } + + if (item.income_accountRef) { + quickBooksItem.IncomeAccountRef = {}; + if (item.income_accountRef.name) { + quickBooksItem.IncomeAccountRef.name = item.income_accountRef.name; + } + if (item.income_accountRef.value) { + quickBooksItem.IncomeAccountRef.value = item.income_accountRef.value; + } + } + + if (item.asset_accountRef) { + quickBooksItem.AssetAccountRef = {}; + if (item.asset_accountRef.name) { + quickBooksItem.AssetAccountRef.name = item.asset_accountRef.name; + } + if (item.asset_accountRef.value) { + quickBooksItem.AssetAccountRef.value = item.asset_accountRef.value; + } + } + + if (item.expense_accountRef) { + quickBooksItem.ExpenseAccountRef = {}; + if (item.expense_accountRef.name) { + quickBooksItem.ExpenseAccountRef.name = item.expense_accountRef.name; + } + if (item.expense_accountRef.value) { + quickBooksItem.ExpenseAccountRef.value = item.expense_accountRef.value; + } + } + + return quickBooksItem; +} diff --git a/integration-templates/quickbooks/mappers/toPayment.ts b/integration-templates/quickbooks/mappers/toPayment.ts new file mode 100644 index 00000000000..7e2a82f00d5 --- /dev/null +++ b/integration-templates/quickbooks/mappers/toPayment.ts @@ -0,0 +1,60 @@ +import type { Payment, CreatePayment } from '../../models'; +import type { QuickBooksPayment } from '../types'; + +/** + * 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: any = {}; + + if (payment.customer_ref) { + quickBooksPayment.CustomerRef = { + value: payment.customer_ref.value, + name: payment.customer_ref.name + }; + } + + if (payment.total_amount_cents) { + quickBooksPayment.TotalAmt = payment.total_amount_cents / 100; + } + + if (payment.currency_ref) { + quickBooksPayment.CurrencyRef = { + value: payment.currency_ref.value, + name: payment.currency_ref.name + }; + } + + if (payment.project_ref) { + quickBooksPayment.ProjectRef = { + value: payment.project_ref.value, + name: payment.project_ref.name + }; + } + + return quickBooksPayment; +} diff --git a/integration-templates/quickbooks/nango.yaml b/integration-templates/quickbooks/nango.yaml new file mode 100644 index 00000000000..27c3ae8ee2c --- /dev/null +++ b/integration-templates/quickbooks/nango.yaml @@ -0,0 +1,276 @@ +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 + track_deletes: true + 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 + track_deletes: true + 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 + track_deletes: true + 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 + track_deletes: true + 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 + track_deletes: true + 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..5d64d7e4253 --- /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..b19a525db4a --- /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..e87dc260e4c --- /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..3f7a4cbdc0f --- /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..930fad4cf07 --- /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..9101e893de0 --- /dev/null +++ b/integration-templates/quickbooks/types.ts @@ -0,0 +1,306 @@ +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; +} + +interface ReferenceType { + value: string; + name?: string; +} + +interface MetaData { + CreateTime: string; + LastUpdatedTime: string; +} + +interface EmailAddress { + Address: string; +} + +interface TelephoneNumber { + FreeFormNumber?: string; +} + +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; + 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/toDate.ts b/integration-templates/quickbooks/utils/toDate.ts new file mode 100644 index 00000000000..b21c819ce95 --- /dev/null +++ b/integration-templates/quickbooks/utils/toDate.ts @@ -0,0 +1,14 @@ +/** + * 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. + */ +export function toDate(dateStr: string): string { + const date = new Date(dateStr); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} diff --git a/packages/shared/flows.yaml b/packages/shared/flows.yaml index ce927b0f767..6c9dd434df7 100644 --- a/packages/shared/flows.yaml +++ b/packages/shared/flows.yaml @@ -3423,6 +3423,348 @@ 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 + track_deletes: true + 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 + track_deletes: true + 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 + track_deletes: true + 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 + track_deletes: true + 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 + track_deletes: true + 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: