From 93ba836a7fb11238329a2f401f411d434e14c9b0 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 31 Jul 2026 12:17:04 -0700 Subject: [PATCH 1/8] feat(quickbooks): add bounded sales transaction reads --- .../quickbooks/read_sales_transactions.ts | 157 ++++++++++++ apps/sim/tools/quickbooks/types.ts | 223 +++++++++++++++++- apps/sim/tools/quickbooks/utils.ts | 39 ++- 3 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 apps/sim/tools/quickbooks/read_sales_transactions.ts diff --git a/apps/sim/tools/quickbooks/read_sales_transactions.ts b/apps/sim/tools/quickbooks/read_sales_transactions.ts new file mode 100644 index 00000000000..c056136a3b6 --- /dev/null +++ b/apps/sim/tools/quickbooks/read_sales_transactions.ts @@ -0,0 +1,157 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import type { + QuickBooksReadSalesTransactionsParams, + QuickBooksReadSalesTransactionsResponse, + QuickBooksSalesTransaction, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_LIST_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + buildQuickBooksQueryUrl, + getQuickBooksSalesEntity, + getQuickBooksToolHeaders, + transformQuickBooksEntityResponse, + transformQuickBooksListResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksReadSalesTransactionsTool: ToolConfig< + QuickBooksReadSalesTransactionsParams, + QuickBooksReadSalesTransactionsResponse +> = { + id: 'quickbooks_read_sales_transactions', + name: 'QuickBooks Read Sales Transactions', + description: + 'List or read one estimate, invoice, sales receipt, payment, credit memo, or refund receipt', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + transactionType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Sales transaction type to read', + }, + readMode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Whether to list transactions or read one transaction by ID', + }, + transactionId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'QuickBooks transaction ID, required for by-ID reads', + }, + startPosition: { + type: 'number', + required: false, + visibility: 'user-or-llm', + default: 1, + description: 'One-based position of the first list record to return', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + default: 25, + description: 'Number of list records to request (1–100)', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => { + const config = getQuickBooksSalesEntity(params.transactionType) + if (params.readMode === 'list') { + return buildQuickBooksQueryUrl( + params.realmId, + config.entity, + params.startPosition ?? 1, + params.maxResults ?? 25 + ).toString() + } + if (params.readMode === 'by_id') { + if (!params.transactionId?.trim()) { + throw new Error('QuickBooks transaction ID is required for by-ID reads') + } + return buildQuickBooksEntityUrl( + params.realmId, + config.resource, + params.transactionId + ).toString() + } + throw new Error(`Unsupported QuickBooks sales read mode: ${String(params.readMode)}`) + }, + method: 'GET', + headers: (params) => getQuickBooksToolHeaders(params.accessToken), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: async (response, params) => { + if (!params) throw new Error('QuickBooks sales transaction parameters are required') + const config = getQuickBooksSalesEntity(params.transactionType) + if (params.readMode === 'list') { + const result = await transformQuickBooksListResponse( + response, + { + ...params, + startPosition: params.startPosition ?? 1, + maxResults: params.maxResults ?? 25, + }, + config.entity + ) + return { + success: true, + output: { transactionType: params.transactionType, ...result.output }, + } + } + if (params.readMode === 'by_id') { + const result = await transformQuickBooksEntityResponse( + response, + config.entity + ) + return { + success: true, + output: { transactionType: params.transactionType, item: result.item, time: result.time }, + } + } + throw new Error(`Unsupported QuickBooks sales read mode: ${String(params.readMode)}`) + }, + outputs: { + transactionType: { type: 'string', description: 'Sales transaction type returned' }, + item: { + type: 'json', + description: 'Single native QuickBooks sales transaction', + optional: true, + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + items: { + type: 'array', + description: 'Native QuickBooks sales transactions', + optional: true, + items: { type: 'json', properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES }, + }, + ...QUICKBOOKS_LIST_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/types.ts b/apps/sim/tools/quickbooks/types.ts index 6815def7e90..7ef7857c9b0 100644 --- a/apps/sim/tools/quickbooks/types.ts +++ b/apps/sim/tools/quickbooks/types.ts @@ -185,14 +185,23 @@ export interface QuickBooksTransaction { DocNumber?: string TxnDate?: string DueDate?: string + ExpirationDate?: string + CustomerRef?: QuickBooksReference + CustomerMemo?: { value?: string } VendorRef?: QuickBooksReference APAccountRef?: QuickBooksReference + DepositToAccountRef?: QuickBooksReference + PaymentMethodRef?: QuickBooksReference + PaymentRefNum?: string CurrencyRef?: QuickBooksReference ExchangeRate?: number Line?: Array> + LinkedTxn?: Array<{ TxnId?: string; TxnType?: string }> TotalAmt?: number Balance?: number + UnappliedAmt?: number PrivateNote?: string + TxnStatus?: string TxnTaxDetail?: Record MetaData?: QuickBooksMetaData sparse?: boolean @@ -201,6 +210,7 @@ export interface QuickBooksTransaction { export type QuickBooksPurchaseOrder = QuickBooksTransaction export type QuickBooksBill = QuickBooksTransaction +export type QuickBooksSalesTransaction = QuickBooksTransaction export interface QuickBooksAuthParams { accessToken: string @@ -231,6 +241,123 @@ export interface QuickBooksReadMasterDataParams extends QuickBooksAuthParams { maxResults?: number } +export type QuickBooksSalesTransactionType = + | 'estimate' + | 'invoice' + | 'sales_receipt' + | 'payment' + | 'credit_memo' + | 'refund_receipt' + +export interface QuickBooksReadSalesTransactionsParams extends QuickBooksAuthParams { + transactionType: QuickBooksSalesTransactionType + readMode: QuickBooksMasterDataReadMode + transactionId?: string + startPosition?: number + maxResults?: number +} + +export type QuickBooksSalesLineType = 'item' | 'description' + +export interface QuickBooksSalesLineInput { + lineType: QuickBooksSalesLineType + amount?: number + itemId?: string + description?: string + quantity?: number + unitPrice?: number + serviceDate?: string +} + +export interface QuickBooksCreateSalesDocumentParams extends QuickBooksAuthParams { + customerId: string + lines: QuickBooksSalesLineInput[] + transactionDate?: string + documentNumber?: string + privateNote?: string + customerMemo?: string + dueDate?: string + expirationDate?: string + paymentMethodId?: string + paymentReferenceNumber?: string + depositAccountId?: string + requestId?: string +} + +export interface QuickBooksUpdateSalesDocumentParams + extends Omit { + transactionId: string + syncToken: string + customerId?: string + lines?: QuickBooksSalesLineInput[] +} + +type QuickBooksNonReceiptCreateParams = Omit< + QuickBooksCreateSalesDocumentParams, + 'paymentMethodId' | 'paymentReferenceNumber' | 'depositAccountId' +> +type QuickBooksNonReceiptUpdateParams = Omit< + QuickBooksUpdateSalesDocumentParams, + 'paymentMethodId' | 'paymentReferenceNumber' | 'depositAccountId' +> + +export type QuickBooksCreateEstimateParams = Omit +export type QuickBooksUpdateEstimateParams = Omit +export type QuickBooksCreateInvoiceParams = Omit +export type QuickBooksUpdateInvoiceParams = Omit +export type QuickBooksCreateSalesReceiptParams = Omit< + QuickBooksCreateSalesDocumentParams, + 'dueDate' | 'expirationDate' +> +export type QuickBooksUpdateSalesReceiptParams = Omit< + QuickBooksUpdateSalesDocumentParams, + 'dueDate' | 'expirationDate' +> +export type QuickBooksCreateCreditMemoParams = Omit< + QuickBooksNonReceiptCreateParams, + 'dueDate' | 'expirationDate' +> +export type QuickBooksUpdateCreditMemoParams = Omit< + QuickBooksNonReceiptUpdateParams, + 'dueDate' | 'expirationDate' +> +export type QuickBooksCreateRefundReceiptParams = Omit< + QuickBooksCreateSalesReceiptParams, + 'depositAccountId' +> & { depositAccountId: string } +export type QuickBooksUpdateRefundReceiptParams = QuickBooksUpdateSalesReceiptParams + +export interface QuickBooksInvoiceAllocationInput { + invoiceId: string + amount: number +} + +export interface QuickBooksCreateCustomerPaymentParams extends QuickBooksAuthParams { + customerId: string + totalAmount: number + transactionDate?: string + privateNote?: string + paymentReferenceNumber?: string + paymentMethodId?: string + depositAccountId?: string + invoiceAllocations?: QuickBooksInvoiceAllocationInput[] + requestId?: string +} + +export interface QuickBooksUpdateCustomerPaymentParams + extends Omit { + paymentId: string + syncToken: string + customerId?: string + totalAmount?: number +} + +export interface QuickBooksVoidTransactionParams extends QuickBooksAuthParams { + transactionId: string + syncToken: string + confirmVoid: boolean +} + export type QuickBooksActiveStatus = 'unchanged' | 'active' | 'inactive' export interface QuickBooksCreateCustomerParams extends QuickBooksAuthParams { @@ -328,7 +455,20 @@ export interface QuickBooksReadMasterDataResponse extends ToolResponse { } } -export interface QuickBooksMutationResponse +export interface QuickBooksReadSalesTransactionsResponse extends ToolResponse { + output: { + transactionType: QuickBooksSalesTransactionType + item?: QuickBooksSalesTransaction + items?: QuickBooksSalesTransaction[] + startPosition?: number + maxResults?: number + nextStartPosition?: number + hasMore?: boolean + time: string | null + } +} + +export interface QuickBooksMutationResponse extends ToolResponse { output: { record: T @@ -338,11 +478,24 @@ export interface QuickBooksMutationResponse | QuickBooksReadMasterDataResponse + | QuickBooksReadSalesTransactionsResponse | QuickBooksMutationResponse + | QuickBooksMutationResponse + | QuickBooksVoidResponse export const QUICKBOOKS_REFERENCE_PROPERTIES: Record = { value: { type: 'string', description: 'QuickBooks entity ID', optional: true }, @@ -621,6 +774,69 @@ export const QUICKBOOKS_MASTER_DATA_PROPERTIES: Record = ...QUICKBOOKS_EMPLOYEE_PROPERTIES, } +export const QUICKBOOKS_SALES_TRANSACTION_PROPERTIES: Record = { + Id: { type: 'string', description: 'QuickBooks sales transaction ID' }, + SyncToken: { type: 'string', description: 'Current transaction sync token', optional: true }, + DocNumber: { type: 'string', description: 'Transaction document number', optional: true }, + TxnDate: { type: 'string', description: 'Transaction date', optional: true }, + DueDate: { type: 'string', description: 'Invoice due date', optional: true }, + ExpirationDate: { type: 'string', description: 'Estimate expiration date', optional: true }, + CustomerRef: { + type: 'json', + description: 'Customer reference', + optional: true, + properties: QUICKBOOKS_REFERENCE_PROPERTIES, + }, + CustomerMemo: { type: 'json', description: 'Customer-facing memo', optional: true }, + DepositToAccountRef: { + type: 'json', + description: 'Deposit account reference', + optional: true, + properties: QUICKBOOKS_REFERENCE_PROPERTIES, + }, + PaymentMethodRef: { + type: 'json', + description: 'Payment method reference', + optional: true, + properties: QUICKBOOKS_REFERENCE_PROPERTIES, + }, + PaymentRefNum: { + type: 'string', + description: 'Customer payment reference number', + optional: true, + }, + CurrencyRef: { + type: 'json', + description: 'Transaction currency reference', + optional: true, + properties: QUICKBOOKS_REFERENCE_PROPERTIES, + }, + Line: { + type: 'array', + description: 'Native QuickBooks transaction lines', + optional: true, + items: { type: 'json' }, + }, + LinkedTxn: { + type: 'array', + description: 'Transactions linked by QuickBooks', + optional: true, + items: { type: 'json' }, + }, + TotalAmt: { type: 'number', description: 'Transaction total amount', optional: true }, + Balance: { type: 'number', description: 'Remaining transaction balance', optional: true }, + UnappliedAmt: { type: 'number', description: 'Unapplied payment amount', optional: true }, + PrivateNote: { type: 'string', description: 'Internal transaction note', optional: true }, + TxnStatus: { type: 'string', description: 'Transaction status', optional: true }, + TxnTaxDetail: { type: 'json', description: 'Calculated tax details', optional: true }, + MetaData: { + type: 'json', + description: 'Transaction creation and update timestamps', + optional: true, + properties: QUICKBOOKS_METADATA_PROPERTIES, + }, +} + export const QUICKBOOKS_MUTATION_OUTPUTS: Record = { recordId: { type: 'string', description: 'ID of the created or updated QuickBooks entity' }, syncToken: { @@ -634,3 +850,8 @@ export const QUICKBOOKS_MUTATION_OUTPUTS: Record = { nullable: true, }, } + +export const QUICKBOOKS_VOID_OUTPUTS: Record = { + ...QUICKBOOKS_MUTATION_OUTPUTS, + voided: { type: 'boolean', description: 'Whether QuickBooks voided the transaction' }, +} diff --git a/apps/sim/tools/quickbooks/utils.ts b/apps/sim/tools/quickbooks/utils.ts index cd29ba27845..173c5c2e20b 100644 --- a/apps/sim/tools/quickbooks/utils.ts +++ b/apps/sim/tools/quickbooks/utils.ts @@ -11,11 +11,11 @@ import type { QuickBooksActiveStatus, QuickBooksAddress, QuickBooksListResponse, - QuickBooksMasterDataRecord, QuickBooksMasterDataRecordType, QuickBooksMutationResponse, QuickBooksPaginationParams, QuickBooksReference, + QuickBooksSalesTransactionType, QuickBooksVendor, QuickBooksWritableItemType, } from '@/tools/quickbooks/types' @@ -23,10 +23,16 @@ import type { export type QuickBooksQueryEntity = | 'Account' | 'Bill' + | 'CreditMemo' | 'Customer' | 'Employee' + | 'Estimate' + | 'Invoice' | 'Item' + | 'Payment' | 'PurchaseOrder' + | 'RefundReceipt' + | 'SalesReceipt' | 'Vendor' interface QuickBooksQueryResponse { @@ -48,6 +54,18 @@ export const QUICKBOOKS_MASTER_DATA_ENTITIES = { { entity: QuickBooksQueryEntity; resource: string } > +export const QUICKBOOKS_SALES_ENTITIES = { + credit_memo: { entity: 'CreditMemo', resource: 'creditmemo' }, + estimate: { entity: 'Estimate', resource: 'estimate' }, + invoice: { entity: 'Invoice', resource: 'invoice' }, + payment: { entity: 'Payment', resource: 'payment' }, + refund_receipt: { entity: 'RefundReceipt', resource: 'refundreceipt' }, + sales_receipt: { entity: 'SalesReceipt', resource: 'salesreceipt' }, +} as const satisfies Record< + QuickBooksSalesTransactionType, + { entity: QuickBooksQueryEntity; resource: string } +> + export function validateQuickBooksPagination( startPosition: number, maxResults: number @@ -84,6 +102,14 @@ export function getQuickBooksMasterDataEntity(recordType: QuickBooksMasterDataRe return config } +export function getQuickBooksSalesEntity(transactionType: QuickBooksSalesTransactionType) { + const config = QUICKBOOKS_SALES_ENTITIES[transactionType] + if (!config) { + throw new Error(`Unsupported QuickBooks sales transaction type: ${String(transactionType)}`) + } + return config +} + export function buildQuickBooksEntityUrl( realmId: string, resource: string, @@ -167,10 +193,9 @@ export async function transformQuickBooksListResponse( } } -export async function transformQuickBooksEntityResponse( - response: Response, - entity: QuickBooksQueryEntity -): Promise<{ item: T; time: string | null }> { +export async function transformQuickBooksEntityResponse< + T extends { Id: string; SyncToken?: string }, +>(response: Response, entity: QuickBooksQueryEntity): Promise<{ item: T; time: string | null }> { const data = await parseQuickBooksJson & { time?: string }>( response, `QuickBooks ${entity} response` @@ -185,7 +210,9 @@ export async function transformQuickBooksEntityResponse( +export async function transformQuickBooksMutationResponse< + T extends { Id: string; SyncToken?: string }, +>( response: Response, entity: QuickBooksQueryEntity, sanitize: (item: T) => T = (item) => item From ebc8a6f0c453494a8a17a4b2c481bdb054497900 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 31 Jul 2026 12:17:12 -0700 Subject: [PATCH 2/8] feat(quickbooks): add sales and receivables mutations --- .../tools/quickbooks/create_credit_memo.ts | 115 ++++ .../quickbooks/create_customer_payment.ts | 127 +++++ apps/sim/tools/quickbooks/create_estimate.ts | 121 +++++ apps/sim/tools/quickbooks/create_invoice.ts | 121 +++++ .../tools/quickbooks/create_refund_receipt.ts | 134 +++++ .../tools/quickbooks/create_sales_receipt.ts | 133 +++++ apps/sim/tools/quickbooks/sales.test.ts | 493 ++++++++++++++++++ apps/sim/tools/quickbooks/sales_utils.ts | 320 ++++++++++++ .../tools/quickbooks/update_credit_memo.ts | 114 ++++ .../quickbooks/update_customer_payment.ts | 126 +++++ apps/sim/tools/quickbooks/update_estimate.ts | 120 +++++ apps/sim/tools/quickbooks/update_invoice.ts | 120 +++++ .../tools/quickbooks/update_refund_receipt.ts | 132 +++++ .../tools/quickbooks/update_sales_receipt.ts | 132 +++++ .../tools/quickbooks/void_customer_payment.ts | 101 ++++ apps/sim/tools/quickbooks/void_invoice.ts | 99 ++++ 16 files changed, 2508 insertions(+) create mode 100644 apps/sim/tools/quickbooks/create_credit_memo.ts create mode 100644 apps/sim/tools/quickbooks/create_customer_payment.ts create mode 100644 apps/sim/tools/quickbooks/create_estimate.ts create mode 100644 apps/sim/tools/quickbooks/create_invoice.ts create mode 100644 apps/sim/tools/quickbooks/create_refund_receipt.ts create mode 100644 apps/sim/tools/quickbooks/create_sales_receipt.ts create mode 100644 apps/sim/tools/quickbooks/sales.test.ts create mode 100644 apps/sim/tools/quickbooks/sales_utils.ts create mode 100644 apps/sim/tools/quickbooks/update_credit_memo.ts create mode 100644 apps/sim/tools/quickbooks/update_customer_payment.ts create mode 100644 apps/sim/tools/quickbooks/update_estimate.ts create mode 100644 apps/sim/tools/quickbooks/update_invoice.ts create mode 100644 apps/sim/tools/quickbooks/update_refund_receipt.ts create mode 100644 apps/sim/tools/quickbooks/update_sales_receipt.ts create mode 100644 apps/sim/tools/quickbooks/void_customer_payment.ts create mode 100644 apps/sim/tools/quickbooks/void_invoice.ts diff --git a/apps/sim/tools/quickbooks/create_credit_memo.ts b/apps/sim/tools/quickbooks/create_credit_memo.ts new file mode 100644 index 00000000000..919b30a0dea --- /dev/null +++ b/apps/sim/tools/quickbooks/create_credit_memo.ts @@ -0,0 +1,115 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { + addQuickBooksRequestId, + buildQuickBooksCreateSalesDocumentBody, +} from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksCreateCreditMemoParams, + QuickBooksMutationResponse, + QuickBooksSalesTransaction, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksCreateCreditMemoTool: ToolConfig< + QuickBooksCreateCreditMemoParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_create_credit_memo', + name: 'QuickBooks Create Credit Memo', + description: 'Create a customer credit memo with bounded sales lines', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer receiving the credit memo', + }, + lines: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Bounded item and description lines', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Credit memo date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional credit memo number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Internal credit memo note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer-facing credit memo memo', + }, + requestId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Intuit idempotency request ID, up to 50 characters', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => + addQuickBooksRequestId( + buildQuickBooksEntityUrl(params.realmId, 'creditmemo'), + params.requestId + ).toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksCreateSalesDocumentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'CreditMemo'), + outputs: { + record: { + type: 'json', + description: 'Created native QuickBooks CreditMemo', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/create_customer_payment.ts b/apps/sim/tools/quickbooks/create_customer_payment.ts new file mode 100644 index 00000000000..1172863d09a --- /dev/null +++ b/apps/sim/tools/quickbooks/create_customer_payment.ts @@ -0,0 +1,127 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { + addQuickBooksRequestId, + buildQuickBooksCreatePaymentBody, +} from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksCreateCustomerPaymentParams, + QuickBooksMutationResponse, + QuickBooksSalesTransaction, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksCreateCustomerPaymentTool: ToolConfig< + QuickBooksCreateCustomerPaymentParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_create_customer_payment', + name: 'QuickBooks Create Customer Payment', + description: 'Record a customer payment with optional bounded invoice allocations', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer making the payment', + }, + totalAmount: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Positive total payment amount', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment date in YYYY-MM-DD format', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Internal payment note', + }, + paymentReferenceNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment reference number such as a check number', + }, + paymentMethodId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'QuickBooks payment method ID', + }, + depositAccountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'QuickBooks deposit account ID', + }, + invoiceAllocations: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Up to 100 invoice allocations with invoiceId and positive amount', + }, + requestId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Intuit idempotency request ID, up to 50 characters', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => + addQuickBooksRequestId( + buildQuickBooksEntityUrl(params.realmId, 'payment'), + params.requestId + ).toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksCreatePaymentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'Payment'), + outputs: { + record: { + type: 'json', + description: 'Created native QuickBooks Payment', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/create_estimate.ts b/apps/sim/tools/quickbooks/create_estimate.ts new file mode 100644 index 00000000000..a73a23dd485 --- /dev/null +++ b/apps/sim/tools/quickbooks/create_estimate.ts @@ -0,0 +1,121 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { + addQuickBooksRequestId, + buildQuickBooksCreateSalesDocumentBody, +} from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksCreateEstimateParams, + QuickBooksMutationResponse, + QuickBooksSalesTransaction, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksCreateEstimateTool: ToolConfig< + QuickBooksCreateEstimateParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_create_estimate', + name: 'QuickBooks Create Estimate', + description: 'Create an estimate with bounded item and description lines', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer receiving the estimate', + }, + lines: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Bounded item and description lines', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Estimate date in YYYY-MM-DD format', + }, + expirationDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Estimate expiration date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional estimate number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Internal estimate note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer-facing estimate memo', + }, + requestId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Intuit idempotency request ID, up to 50 characters', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => + addQuickBooksRequestId( + buildQuickBooksEntityUrl(params.realmId, 'estimate'), + params.requestId + ).toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksCreateSalesDocumentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'Estimate'), + outputs: { + record: { + type: 'json', + description: 'Created native QuickBooks Estimate', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/create_invoice.ts b/apps/sim/tools/quickbooks/create_invoice.ts new file mode 100644 index 00000000000..7e6dcb9cb98 --- /dev/null +++ b/apps/sim/tools/quickbooks/create_invoice.ts @@ -0,0 +1,121 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { + addQuickBooksRequestId, + buildQuickBooksCreateSalesDocumentBody, +} from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksCreateInvoiceParams, + QuickBooksMutationResponse, + QuickBooksSalesTransaction, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksCreateInvoiceTool: ToolConfig< + QuickBooksCreateInvoiceParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_create_invoice', + name: 'QuickBooks Create Invoice', + description: 'Create an invoice without emailing or collecting payment', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer receiving the invoice', + }, + lines: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Bounded item and description lines', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Invoice date in YYYY-MM-DD format', + }, + dueDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Invoice due date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional invoice number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Internal invoice note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer-facing invoice memo', + }, + requestId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Intuit idempotency request ID, up to 50 characters', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => + addQuickBooksRequestId( + buildQuickBooksEntityUrl(params.realmId, 'invoice'), + params.requestId + ).toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksCreateSalesDocumentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'Invoice'), + outputs: { + record: { + type: 'json', + description: 'Created native QuickBooks Invoice', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/create_refund_receipt.ts b/apps/sim/tools/quickbooks/create_refund_receipt.ts new file mode 100644 index 00000000000..42743328088 --- /dev/null +++ b/apps/sim/tools/quickbooks/create_refund_receipt.ts @@ -0,0 +1,134 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { + addQuickBooksRequestId, + buildQuickBooksCreateSalesDocumentBody, +} from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksCreateRefundReceiptParams, + QuickBooksMutationResponse, + QuickBooksSalesTransaction, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksCreateRefundReceiptTool: ToolConfig< + QuickBooksCreateRefundReceiptParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_create_refund_receipt', + name: 'QuickBooks Create Refund Receipt', + description: 'Create a customer refund receipt against a required deposit account', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer receiving the refund', + }, + lines: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Bounded item and description lines', + }, + depositAccountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'QuickBooks bank account funding the refund', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Refund receipt date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional refund receipt number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Internal refund receipt note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer-facing refund memo', + }, + paymentMethodId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'QuickBooks payment method ID', + }, + paymentReferenceNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Refund payment reference number', + }, + requestId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Intuit idempotency request ID, up to 50 characters', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => + addQuickBooksRequestId( + buildQuickBooksEntityUrl(params.realmId, 'refundreceipt'), + params.requestId + ).toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => + buildQuickBooksCreateSalesDocumentBody(params, { requireDepositAccount: true }), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'RefundReceipt'), + outputs: { + record: { + type: 'json', + description: 'Created native QuickBooks RefundReceipt', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/create_sales_receipt.ts b/apps/sim/tools/quickbooks/create_sales_receipt.ts new file mode 100644 index 00000000000..e46a32265ed --- /dev/null +++ b/apps/sim/tools/quickbooks/create_sales_receipt.ts @@ -0,0 +1,133 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { + addQuickBooksRequestId, + buildQuickBooksCreateSalesDocumentBody, +} from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksCreateSalesReceiptParams, + QuickBooksMutationResponse, + QuickBooksSalesTransaction, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksCreateSalesReceiptTool: ToolConfig< + QuickBooksCreateSalesReceiptParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_create_sales_receipt', + name: 'QuickBooks Create Sales Receipt', + description: 'Create a sales receipt for a completed customer sale', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer for the sales receipt', + }, + lines: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Bounded item and description lines', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sales receipt date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional sales receipt number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Internal sales receipt note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer-facing sales receipt memo', + }, + paymentMethodId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'QuickBooks payment method ID', + }, + paymentReferenceNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment reference number', + }, + depositAccountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'QuickBooks deposit account ID', + }, + requestId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Intuit idempotency request ID, up to 50 characters', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => + addQuickBooksRequestId( + buildQuickBooksEntityUrl(params.realmId, 'salesreceipt'), + params.requestId + ).toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksCreateSalesDocumentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'SalesReceipt'), + outputs: { + record: { + type: 'json', + description: 'Created native QuickBooks SalesReceipt', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/sales.test.ts b/apps/sim/tools/quickbooks/sales.test.ts new file mode 100644 index 00000000000..c827d3b2b0f --- /dev/null +++ b/apps/sim/tools/quickbooks/sales.test.ts @@ -0,0 +1,493 @@ +import { resetEnvMock, setEnv } from '@sim/testing' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { QuickBooksBlock } from '@/blocks/blocks/quickbooks' +import { + quickbooksCreateCreditMemoTool, + quickbooksCreateCustomerPaymentTool, + quickbooksCreateEstimateTool, + quickbooksCreateInvoiceTool, + quickbooksCreateRefundReceiptTool, + quickbooksCreateSalesReceiptTool, + quickbooksReadSalesTransactionsTool, + quickbooksUpdateCreditMemoTool, + quickbooksUpdateCustomerPaymentTool, + quickbooksUpdateEstimateTool, + quickbooksUpdateInvoiceTool, + quickbooksUpdateRefundReceiptTool, + quickbooksUpdateSalesReceiptTool, + quickbooksVoidCustomerPaymentTool, + quickbooksVoidInvoiceTool, +} from '@/tools/quickbooks' +import { + buildQuickBooksCreatePaymentBody, + buildQuickBooksCreateSalesDocumentBody, + buildQuickBooksUpdatePaymentBody, + buildQuickBooksUpdateSalesDocumentBody, + parseQuickBooksInvoiceAllocations, + parseQuickBooksSalesLines, +} from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksCreateCustomerPaymentParams, + QuickBooksCreateSalesDocumentParams, + QuickBooksReadSalesTransactionsParams, + QuickBooksUpdateCustomerPaymentParams, + QuickBooksUpdateSalesDocumentParams, + QuickBooksVoidTransactionParams, +} from '@/tools/quickbooks/types' + +const authParams = { accessToken: 'access-token', realmId: '123456789' } + +const itemLine = { + lineType: 'item' as const, + amount: 125.5, + itemId: '7', + description: 'Sanitized consulting', + quantity: 2, + unitPrice: 62.75, + serviceDate: '2026-07-31', +} + +beforeEach(() => setEnv({ QUICKBOOKS_ENV: 'sandbox' })) +afterEach(resetEnvMock) + +describe('QuickBooks sales reader', () => { + const listParams: QuickBooksReadSalesTransactionsParams = { + ...authParams, + transactionType: 'invoice', + readMode: 'list', + startPosition: 3, + maxResults: 25, + } + + it.each([ + ['estimate', 'Estimate', 'estimate'], + ['invoice', 'Invoice', 'invoice'], + ['sales_receipt', 'SalesReceipt', 'salesreceipt'], + ['payment', 'Payment', 'payment'], + ['credit_memo', 'CreditMemo', 'creditmemo'], + ['refund_receipt', 'RefundReceipt', 'refundreceipt'], + ] as const)('maps %s to fixed list and by-ID contracts', (transactionType, entity, resource) => { + const requestUrl = quickbooksReadSalesTransactionsTool.request.url as ( + params: QuickBooksReadSalesTransactionsParams + ) => string + const listUrl = new URL(requestUrl({ ...listParams, transactionType })) + expect(listUrl.pathname).toBe('/v3/company/123456789/query') + expect(listUrl.searchParams.get('query')).toBe( + `SELECT * FROM ${entity} STARTPOSITION 3 MAXRESULTS 25` + ) + expect(listUrl.searchParams.get('minorversion')).toBe('75') + + const byIdUrl = new URL( + requestUrl({ + ...listParams, + transactionType, + readMode: 'by_id', + transactionId: ' A/B ', + }) + ) + expect(byIdUrl.pathname).toBe(`/v3/company/123456789/${resource}/A%2FB`) + }) + + it('preserves native list and by-ID records', async () => { + await expect( + quickbooksReadSalesTransactionsTool.transformResponse!( + Response.json({ + QueryResponse: { + Invoice: [{ Id: '12', SyncToken: '1', DocNumber: 'SANITIZED-12' }], + startPosition: 3, + maxResults: 1, + }, + time: 'test-time', + }), + listParams + ) + ).resolves.toEqual({ + success: true, + output: { + transactionType: 'invoice', + items: [{ Id: '12', SyncToken: '1', DocNumber: 'SANITIZED-12' }], + startPosition: 3, + maxResults: 1, + nextStartPosition: 4, + hasMore: false, + time: 'test-time', + }, + }) + + await expect( + quickbooksReadSalesTransactionsTool.transformResponse!( + Response.json({ Invoice: { Id: 'A/B', SyncToken: '2' }, time: 'test-time' }), + { ...listParams, readMode: 'by_id', transactionId: 'A/B' } + ) + ).resolves.toEqual({ + success: true, + output: { + transactionType: 'invoice', + item: { Id: 'A/B', SyncToken: '2' }, + time: 'test-time', + }, + }) + }) + + it('rejects unknown types, modes, and missing by-ID values before a request', () => { + const requestUrl = quickbooksReadSalesTransactionsTool.request.url as ( + params: QuickBooksReadSalesTransactionsParams + ) => string + expect(() => requestUrl({ ...listParams, readMode: 'by_id' })).toThrow('transaction ID') + expect(() => + requestUrl({ + ...listParams, + transactionType: 'unsupported' as QuickBooksReadSalesTransactionsParams['transactionType'], + }) + ).toThrow('transaction type') + expect(() => + requestUrl({ + ...listParams, + readMode: 'unsupported' as QuickBooksReadSalesTransactionsParams['readMode'], + }) + ).toThrow('read mode') + }) +}) + +describe('QuickBooks sales document validation and bodies', () => { + it('parses item and description lines and rejects unbounded or unknown input', () => { + expect( + parseQuickBooksSalesLines( + JSON.stringify([itemLine, { lineType: 'description', description: 'Sanitized note' }]) + ) + ).toEqual([itemLine, { lineType: 'description', description: 'Sanitized note' }]) + expect(() => parseQuickBooksSalesLines('[]')).toThrow('at least one line') + expect(() => + parseQuickBooksSalesLines('[{"lineType":"item","amount":1,"itemId":"7","raw":true}]') + ).toThrow('unsupported field') + expect(() => + parseQuickBooksSalesLines('[{"lineType":"item","amount":0,"itemId":"7"}]') + ).toThrow('positive finite') + expect(() => parseQuickBooksSalesLines('[{"lineType":"item","amount":1,"itemId":7}]')).toThrow( + 'itemId must be a string' + ) + expect(() => + parseQuickBooksSalesLines( + '[{"lineType":"item","amount":1,"itemId":"7","serviceDate":"2026-02-30"}]' + ) + ).toThrow('valid date') + expect(() => parseQuickBooksSalesLines(Array.from({ length: 101 }, () => itemLine))).toThrow( + 'more than 100' + ) + }) + + it('builds the bounded common create body without raw fragments', () => { + const params: QuickBooksCreateSalesDocumentParams = { + ...authParams, + customerId: ' 2 ', + lines: [itemLine, { lineType: 'description', description: 'Sanitized note' }], + transactionDate: '2026-07-31', + documentNumber: 'SANITIZED-1', + privateNote: 'Internal note', + customerMemo: 'Customer memo', + dueDate: '2026-08-31', + expirationDate: '2026-08-15', + paymentMethodId: '3', + paymentReferenceNumber: 'CHECK-1', + depositAccountId: '4', + } + expect(buildQuickBooksCreateSalesDocumentBody(params)).toEqual({ + CustomerRef: { value: '2' }, + Line: [ + { + Amount: 125.5, + Description: 'Sanitized consulting', + DetailType: 'SalesItemLineDetail', + SalesItemLineDetail: { + ItemRef: { value: '7' }, + Qty: 2, + UnitPrice: 62.75, + ServiceDate: '2026-07-31', + }, + }, + { DetailType: 'DescriptionOnly', Description: 'Sanitized note' }, + ], + TxnDate: '2026-07-31', + DocNumber: 'SANITIZED-1', + PrivateNote: 'Internal note', + CustomerMemo: { value: 'Customer memo' }, + DueDate: '2026-08-31', + ExpirationDate: '2026-08-15', + PaymentMethodRef: { value: '3' }, + PaymentRefNum: 'CHECK-1', + DepositToAccountRef: { value: '4' }, + }) + }) + + it('requires a deposit account for refund-receipt creation', () => { + expect(() => + buildQuickBooksCreateSalesDocumentBody( + { ...authParams, customerId: '2', lines: [itemLine] }, + { requireDepositAccount: true } + ) + ).toThrow('depositAccountId') + }) + + it('builds sparse updates and rejects an empty update', () => { + const params: QuickBooksUpdateSalesDocumentParams = { + ...authParams, + transactionId: '12', + syncToken: '3', + privateNote: 'Replacement note', + } + expect(buildQuickBooksUpdateSalesDocumentBody(params)).toEqual({ + Id: '12', + SyncToken: '3', + sparse: true, + PrivateNote: 'Replacement note', + }) + expect(() => + buildQuickBooksUpdateSalesDocumentBody({ + ...authParams, + transactionId: '12', + syncToken: '3', + }) + ).toThrow('at least one field') + }) + + it.each([ + [quickbooksCreateEstimateTool, 'estimate', 'Estimate'], + [quickbooksCreateInvoiceTool, 'invoice', 'Invoice'], + [quickbooksCreateSalesReceiptTool, 'salesreceipt', 'SalesReceipt'], + [quickbooksCreateCreditMemoTool, 'creditmemo', 'CreditMemo'], + [quickbooksCreateRefundReceiptTool, 'refundreceipt', 'RefundReceipt'], + ] as const)( + 'uses one fixed %s create endpoint and verified wrapper', + async (tool, resource, wrapper) => { + const requestUrl = tool.request.url as (params: QuickBooksCreateSalesDocumentParams) => string + const url = new URL( + requestUrl({ ...authParams, customerId: '2', lines: [itemLine], requestId: 'request-1' }) + ) + expect(url.pathname).toBe(`/v3/company/123456789/${resource}`) + expect(url.searchParams.get('requestid')).toBe('request-1') + expect(tool.postProcess).toBeUndefined() + await expect( + tool.transformResponse!( + Response.json({ [wrapper]: { Id: '12', SyncToken: '0' }, time: 'test-time' }) + ) + ).resolves.toMatchObject({ + success: true, + output: { recordId: '12', syncToken: '0', time: 'test-time' }, + }) + } + ) + + it.each([ + [quickbooksUpdateEstimateTool, 'estimate'], + [quickbooksUpdateInvoiceTool, 'invoice'], + [quickbooksUpdateSalesReceiptTool, 'salesreceipt'], + [quickbooksUpdateCreditMemoTool, 'creditmemo'], + [quickbooksUpdateRefundReceiptTool, 'refundreceipt'], + ] as const)('uses one fixed %s sparse-update endpoint', (tool, resource) => { + const requestUrl = tool.request.url as (params: QuickBooksUpdateSalesDocumentParams) => string + expect( + new URL( + requestUrl({ + ...authParams, + transactionId: '12', + syncToken: '1', + privateNote: 'Replacement', + }) + ).pathname + ).toBe(`/v3/company/123456789/${resource}`) + expect(tool.postProcess).toBeUndefined() + }) +}) + +describe('QuickBooks customer payments and voids', () => { + it('parses bounded allocations and enforces payment totals', () => { + expect(parseQuickBooksInvoiceAllocations('[{"invoiceId":"12","amount":75}]')).toEqual([ + { invoiceId: '12', amount: 75 }, + ]) + expect(() => + parseQuickBooksInvoiceAllocations('[{"invoiceId":"12","amount":75,"raw":true}]') + ).toThrow('unsupported field') + expect(() => parseQuickBooksInvoiceAllocations('[{"invoiceId":12,"amount":75}]')).toThrow( + 'invoiceId must be a string' + ) + + const params: QuickBooksCreateCustomerPaymentParams = { + ...authParams, + customerId: '2', + totalAmount: 100, + invoiceAllocations: [{ invoiceId: '12', amount: 75 }], + } + expect(buildQuickBooksCreatePaymentBody(params)).toEqual({ + CustomerRef: { value: '2' }, + TotalAmt: 100, + Line: [{ Amount: 75, LinkedTxn: [{ TxnId: '12', TxnType: 'Invoice' }] }], + }) + expect(() => + buildQuickBooksCreatePaymentBody({ + ...params, + invoiceAllocations: [{ invoiceId: '12', amount: 101 }], + }) + ).toThrow('cannot exceed') + }) + + it('builds sparse payment updates and rejects allocations without a replacement total', () => { + const params: QuickBooksUpdateCustomerPaymentParams = { + ...authParams, + paymentId: '15', + syncToken: '2', + totalAmount: 50, + invoiceAllocations: [{ invoiceId: '12', amount: 25 }], + } + expect(buildQuickBooksUpdatePaymentBody(params)).toEqual({ + Id: '15', + SyncToken: '2', + sparse: true, + TotalAmt: 50, + Line: [{ Amount: 25, LinkedTxn: [{ TxnId: '12', TxnType: 'Invoice' }] }], + }) + expect(() => + buildQuickBooksUpdatePaymentBody({ + ...authParams, + paymentId: '15', + syncToken: '2', + invoiceAllocations: [{ invoiceId: '12', amount: 25 }], + }) + ).toThrow('totalAmount is required') + }) + + it('uses the verified payment endpoints', () => { + const createUrl = quickbooksCreateCustomerPaymentTool.request.url as ( + params: QuickBooksCreateCustomerPaymentParams + ) => string + expect( + new URL( + createUrl({ ...authParams, customerId: '2', totalAmount: 10, requestId: 'payment-1' }) + ).searchParams.get('requestid') + ).toBe('payment-1') + expect( + new URL( + ( + quickbooksUpdateCustomerPaymentTool.request.url as ( + params: QuickBooksUpdateCustomerPaymentParams + ) => string + )({ ...authParams, paymentId: '15', syncToken: '2', privateNote: 'Updated' }) + ).pathname + ).toBe('/v3/company/123456789/payment') + }) + + it.each([ + [quickbooksVoidInvoiceTool, 'invoice', 'void', null], + [quickbooksVoidCustomerPaymentTool, 'payment', 'update', 'void'], + ] as const)( + 'requires confirmation and uses the verified %s void contract', + (tool, resource, operation, include) => { + const params: QuickBooksVoidTransactionParams = { + ...authParams, + transactionId: '20', + syncToken: '3', + confirmVoid: true, + } + const url = new URL( + (tool.request.url as (value: QuickBooksVoidTransactionParams) => string)(params) + ) + expect(url.pathname).toBe(`/v3/company/123456789/${resource}`) + expect(url.searchParams.get('operation')).toBe(operation) + expect(url.searchParams.get('include')).toBe(include) + expect(tool.request.body!(params)).toMatchObject({ Id: '20', SyncToken: '3' }) + expect(() => tool.request.body!({ ...params, confirmVoid: false })).toThrow('Confirm void') + } + ) +}) + +describe('QuickBooks sales block coercion', () => { + it('parses dynamic JSON and numeric values only in block parameter mapping', () => { + expect( + QuickBooksBlock.tools.config!.params!({ + operation: 'quickbooks_create_invoice', + oauthCredential: 'credential-id', + customerId: '2', + lines: JSON.stringify([itemLine]), + requestId: 'request-1', + }) + ).toMatchObject({ credential: 'credential-id', customerId: '2', lines: [itemLine] }) + + expect( + QuickBooksBlock.tools.config!.params!({ + operation: 'quickbooks_create_customer_payment', + oauthCredential: 'credential-id', + customerId: '2', + totalAmount: '100.50', + invoiceAllocations: '[{"invoiceId":"12","amount":75}]', + }) + ).toMatchObject({ + credential: 'credential-id', + totalAmount: 100.5, + invoiceAllocations: [{ invoiceId: '12', amount: 75 }], + }) + }) + + it('drops stale hidden fields when the selected sales operation changes', () => { + expect( + QuickBooksBlock.tools.config!.params!({ + operation: 'quickbooks_create_estimate', + oauthCredential: 'credential-id', + customerId: '2', + lines: JSON.stringify([itemLine]), + dueDate: '2026-08-31', + paymentMethodId: 'stale-payment-method', + depositAccountId: 'stale-deposit-account', + transactionId: 'stale-id', + syncToken: 'stale-token', + }) + ).toMatchObject({ + dueDate: undefined, + paymentMethodId: undefined, + depositAccountId: undefined, + transactionId: undefined, + syncToken: undefined, + }) + + expect( + QuickBooksBlock.tools.config!.params!({ + operation: 'quickbooks_update_customer_payment', + oauthCredential: 'credential-id', + transactionId: '20', + syncToken: '3', + requestId: 'stale-request-id', + }) + ).toMatchObject({ paymentId: '20', syncToken: '3', requestId: undefined }) + }) + + it('maps read and void UI values directly to bounded tool parameters', () => { + expect( + QuickBooksBlock.tools.config!.params!({ + operation: 'quickbooks_read_sales_transactions', + oauthCredential: 'credential-id', + transactionType: 'invoice', + readMode: 'list', + startPosition: '1', + maxResults: '25', + }) + ).toEqual({ + credential: 'credential-id', + transactionType: 'invoice', + readMode: 'list', + startPosition: 1, + maxResults: 25, + }) + expect( + QuickBooksBlock.tools.config!.params!({ + operation: 'quickbooks_void_invoice', + oauthCredential: 'credential-id', + transactionId: '20', + syncToken: '3', + confirmVoid: 'yes', + }) + ).toEqual({ + credential: 'credential-id', + transactionId: '20', + syncToken: '3', + confirmVoid: true, + }) + }) +}) diff --git a/apps/sim/tools/quickbooks/sales_utils.ts b/apps/sim/tools/quickbooks/sales_utils.ts new file mode 100644 index 00000000000..641f80429e8 --- /dev/null +++ b/apps/sim/tools/quickbooks/sales_utils.ts @@ -0,0 +1,320 @@ +import { filterUndefined } from '@sim/utils/object' +import type { + QuickBooksCreateCustomerPaymentParams, + QuickBooksCreateSalesDocumentParams, + QuickBooksInvoiceAllocationInput, + QuickBooksSalesLineInput, + QuickBooksUpdateCustomerPaymentParams, + QuickBooksUpdateSalesDocumentParams, +} from '@/tools/quickbooks/types' +import { + assertQuickBooksSparseUpdate, + optionalQuickBooksString, + quickBooksReference, + requiredQuickBooksString, +} from '@/tools/quickbooks/utils' + +const MAX_SALES_LINES = 100 +const MAX_PAYMENT_ALLOCATIONS = 100 +const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/ +const ITEM_LINE_KEYS = new Set([ + 'lineType', + 'amount', + 'itemId', + 'description', + 'quantity', + 'unitPrice', + 'serviceDate', +]) +const DESCRIPTION_LINE_KEYS = new Set(['lineType', 'description']) +const PAYMENT_ALLOCATION_KEYS = new Set(['invoiceId', 'amount']) + +function parseJsonArray(value: unknown, fieldName: string): unknown[] | undefined { + if (value == null || value === '') return undefined + let parsed = value + if (typeof value === 'string') { + try { + parsed = JSON.parse(value) + } catch { + throw new Error(`${fieldName} must be valid JSON`) + } + } + if (!Array.isArray(parsed)) throw new Error(`${fieldName} must be a JSON array`) + return parsed +} + +function assertObject(value: unknown, fieldName: string): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`${fieldName} must be a JSON object`) + } + return value as Record +} + +function assertAllowedKeys( + value: Record, + allowed: Set, + fieldName: string +): void { + const unknownKey = Object.keys(value).find((key) => !allowed.has(key)) + if (unknownKey) throw new Error(`${fieldName} contains unsupported field "${unknownKey}"`) +} + +function requiredPositiveNumber(value: unknown, fieldName: string): number { + const parsed = typeof value === 'number' ? value : Number(value) + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`${fieldName} must be a positive finite number`) + } + return parsed +} + +function optionalFiniteNumber(value: unknown, fieldName: string): number | undefined { + if (value == null || value === '') return undefined + const parsed = typeof value === 'number' ? value : Number(value) + if (!Number.isFinite(parsed)) throw new Error(`${fieldName} must be a finite number`) + return parsed +} + +function requiredStringValue(value: unknown, fieldName: string): string { + if (typeof value !== 'string') throw new Error(`${fieldName} must be a string`) + return requiredQuickBooksString(value, fieldName) +} + +function optionalStringValue(value: unknown, fieldName: string): string | undefined { + if (value === undefined) return undefined + if (typeof value !== 'string') throw new Error(`${fieldName} must be a string`) + return optionalQuickBooksString(value) +} + +export function validateQuickBooksDate( + value: string | undefined, + fieldName: string +): string | undefined { + const normalized = optionalQuickBooksString(value) + if (!normalized) return undefined + if (!DATE_PATTERN.test(normalized)) throw new Error(`${fieldName} must use YYYY-MM-DD`) + const date = new Date(`${normalized}T00:00:00Z`) + if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== normalized) { + throw new Error(`${fieldName} must be a valid date`) + } + return normalized +} + +export function parseQuickBooksSalesLines( + value: unknown, + fieldName = 'lines' +): QuickBooksSalesLineInput[] | undefined { + const parsed = parseJsonArray(value, fieldName) + if (!parsed) return undefined + if (parsed.length === 0) throw new Error(`${fieldName} must contain at least one line`) + if (parsed.length > MAX_SALES_LINES) { + throw new Error(`${fieldName} cannot contain more than ${MAX_SALES_LINES} lines`) + } + + return parsed.map((rawLine, index) => { + const itemName = `${fieldName}[${index}]` + const line = assertObject(rawLine, itemName) + if (line.lineType === 'description') { + assertAllowedKeys(line, DESCRIPTION_LINE_KEYS, itemName) + return { + lineType: 'description', + description: requiredStringValue(line.description, `${itemName}.description`), + } + } + if (line.lineType !== 'item') { + throw new Error(`${itemName}.lineType must be item or description`) + } + assertAllowedKeys(line, ITEM_LINE_KEYS, itemName) + const description = optionalStringValue(line.description, `${itemName}.description`) + return { + lineType: 'item', + amount: requiredPositiveNumber(line.amount, `${itemName}.amount`), + itemId: requiredStringValue(line.itemId, `${itemName}.itemId`), + description, + quantity: optionalFiniteNumber(line.quantity, `${itemName}.quantity`), + unitPrice: optionalFiniteNumber(line.unitPrice, `${itemName}.unitPrice`), + serviceDate: validateQuickBooksDate( + optionalStringValue(line.serviceDate, `${itemName}.serviceDate`), + `${itemName}.serviceDate` + ), + } + }) +} + +export function buildQuickBooksSalesLines(lines: QuickBooksSalesLineInput[]): unknown[] { + const validated = parseQuickBooksSalesLines(lines) + if (!validated) throw new Error('lines are required') + return validated.map((line) => { + if (line.lineType === 'description') { + return { DetailType: 'DescriptionOnly', Description: line.description } + } + return filterUndefined({ + Amount: line.amount, + Description: line.description, + DetailType: 'SalesItemLineDetail', + SalesItemLineDetail: filterUndefined({ + ItemRef: quickBooksReference(line.itemId!, 'itemId'), + Qty: line.quantity, + UnitPrice: line.unitPrice, + ServiceDate: line.serviceDate, + }), + }) + }) +} + +export function parseQuickBooksInvoiceAllocations( + value: unknown, + fieldName = 'invoiceAllocations' +): QuickBooksInvoiceAllocationInput[] | undefined { + const parsed = parseJsonArray(value, fieldName) + if (!parsed) return undefined + if (parsed.length === 0) throw new Error(`${fieldName} must contain at least one allocation`) + if (parsed.length > MAX_PAYMENT_ALLOCATIONS) { + throw new Error(`${fieldName} cannot contain more than ${MAX_PAYMENT_ALLOCATIONS} allocations`) + } + return parsed.map((rawAllocation, index) => { + const itemName = `${fieldName}[${index}]` + const allocation = assertObject(rawAllocation, itemName) + assertAllowedKeys(allocation, PAYMENT_ALLOCATION_KEYS, itemName) + return { + invoiceId: requiredStringValue(allocation.invoiceId, `${itemName}.invoiceId`), + amount: requiredPositiveNumber(allocation.amount, `${itemName}.amount`), + } + }) +} + +function buildPaymentLines( + allocations: QuickBooksInvoiceAllocationInput[] | undefined, + totalAmount: number | undefined +): unknown[] | undefined { + if (!allocations) return undefined + if (totalAmount === undefined) { + throw new Error('totalAmount is required when invoice allocations are supplied') + } + const allocationTotal = allocations.reduce((sum, allocation) => sum + allocation.amount, 0) + if (allocationTotal > totalAmount) { + throw new Error('Invoice allocation amounts cannot exceed totalAmount') + } + return allocations.map((allocation) => ({ + Amount: allocation.amount, + LinkedTxn: [{ TxnId: allocation.invoiceId, TxnType: 'Invoice' }], + })) +} + +export function addQuickBooksRequestId(url: URL, requestId?: string): URL { + const normalized = optionalQuickBooksString(requestId) + if (!normalized) return url + if (normalized.length > 50) throw new Error('requestId cannot exceed 50 characters') + url.searchParams.set('requestid', normalized) + return url +} + +export function buildQuickBooksCreateSalesDocumentBody( + params: QuickBooksCreateSalesDocumentParams, + options: { requireDepositAccount?: boolean } = {} +): Record { + if (options.requireDepositAccount && !params.depositAccountId?.trim()) { + throw new Error('depositAccountId is required to create a refund receipt') + } + return filterUndefined({ + CustomerRef: quickBooksReference(params.customerId, 'customerId'), + Line: buildQuickBooksSalesLines(params.lines), + TxnDate: validateQuickBooksDate(params.transactionDate, 'transactionDate'), + DocNumber: optionalQuickBooksString(params.documentNumber), + PrivateNote: optionalQuickBooksString(params.privateNote), + CustomerMemo: optionalQuickBooksString(params.customerMemo) + ? { value: optionalQuickBooksString(params.customerMemo) } + : undefined, + DueDate: validateQuickBooksDate(params.dueDate, 'dueDate'), + ExpirationDate: validateQuickBooksDate(params.expirationDate, 'expirationDate'), + PaymentMethodRef: params.paymentMethodId + ? quickBooksReference(params.paymentMethodId, 'paymentMethodId') + : undefined, + PaymentRefNum: optionalQuickBooksString(params.paymentReferenceNumber), + DepositToAccountRef: params.depositAccountId + ? quickBooksReference(params.depositAccountId, 'depositAccountId') + : undefined, + }) +} + +export function buildQuickBooksUpdateSalesDocumentBody( + params: QuickBooksUpdateSalesDocumentParams +): Record { + const body = filterUndefined({ + Id: requiredQuickBooksString(params.transactionId, 'transactionId'), + SyncToken: requiredQuickBooksString(params.syncToken, 'syncToken'), + sparse: true, + CustomerRef: params.customerId + ? quickBooksReference(params.customerId, 'customerId') + : undefined, + Line: params.lines ? buildQuickBooksSalesLines(params.lines) : undefined, + TxnDate: validateQuickBooksDate(params.transactionDate, 'transactionDate'), + DocNumber: optionalQuickBooksString(params.documentNumber), + PrivateNote: optionalQuickBooksString(params.privateNote), + CustomerMemo: optionalQuickBooksString(params.customerMemo) + ? { value: optionalQuickBooksString(params.customerMemo) } + : undefined, + DueDate: validateQuickBooksDate(params.dueDate, 'dueDate'), + ExpirationDate: validateQuickBooksDate(params.expirationDate, 'expirationDate'), + PaymentMethodRef: params.paymentMethodId + ? quickBooksReference(params.paymentMethodId, 'paymentMethodId') + : undefined, + PaymentRefNum: optionalQuickBooksString(params.paymentReferenceNumber), + DepositToAccountRef: params.depositAccountId + ? quickBooksReference(params.depositAccountId, 'depositAccountId') + : undefined, + }) as Record + assertQuickBooksSparseUpdate(body) + return body +} + +export function buildQuickBooksCreatePaymentBody( + params: QuickBooksCreateCustomerPaymentParams +): Record { + const totalAmount = requiredPositiveNumber(params.totalAmount, 'totalAmount') + const allocations = parseQuickBooksInvoiceAllocations(params.invoiceAllocations) + return filterUndefined({ + CustomerRef: quickBooksReference(params.customerId, 'customerId'), + TotalAmt: totalAmount, + TxnDate: validateQuickBooksDate(params.transactionDate, 'transactionDate'), + PrivateNote: optionalQuickBooksString(params.privateNote), + PaymentRefNum: optionalQuickBooksString(params.paymentReferenceNumber), + PaymentMethodRef: params.paymentMethodId + ? quickBooksReference(params.paymentMethodId, 'paymentMethodId') + : undefined, + DepositToAccountRef: params.depositAccountId + ? quickBooksReference(params.depositAccountId, 'depositAccountId') + : undefined, + Line: buildPaymentLines(allocations, totalAmount), + }) +} + +export function buildQuickBooksUpdatePaymentBody( + params: QuickBooksUpdateCustomerPaymentParams +): Record { + const totalAmount = + params.totalAmount === undefined + ? undefined + : requiredPositiveNumber(params.totalAmount, 'totalAmount') + const allocations = parseQuickBooksInvoiceAllocations(params.invoiceAllocations) + const body = filterUndefined({ + Id: requiredQuickBooksString(params.paymentId, 'paymentId'), + SyncToken: requiredQuickBooksString(params.syncToken, 'syncToken'), + sparse: true, + CustomerRef: params.customerId + ? quickBooksReference(params.customerId, 'customerId') + : undefined, + TotalAmt: totalAmount, + TxnDate: validateQuickBooksDate(params.transactionDate, 'transactionDate'), + PrivateNote: optionalQuickBooksString(params.privateNote), + PaymentRefNum: optionalQuickBooksString(params.paymentReferenceNumber), + PaymentMethodRef: params.paymentMethodId + ? quickBooksReference(params.paymentMethodId, 'paymentMethodId') + : undefined, + DepositToAccountRef: params.depositAccountId + ? quickBooksReference(params.depositAccountId, 'depositAccountId') + : undefined, + Line: buildPaymentLines(allocations, totalAmount), + }) as Record + assertQuickBooksSparseUpdate(body) + return body +} diff --git a/apps/sim/tools/quickbooks/update_credit_memo.ts b/apps/sim/tools/quickbooks/update_credit_memo.ts new file mode 100644 index 00000000000..d6c6df79258 --- /dev/null +++ b/apps/sim/tools/quickbooks/update_credit_memo.ts @@ -0,0 +1,114 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { buildQuickBooksUpdateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksMutationResponse, + QuickBooksSalesTransaction, + QuickBooksUpdateCreditMemoParams, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksUpdateCreditMemoTool: ToolConfig< + QuickBooksUpdateCreditMemoParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_update_credit_memo', + name: 'QuickBooks Update Credit Memo', + description: 'Sparse-update a credit memo using its current sync token', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + transactionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Credit memo ID to update', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current credit memo sync token', + }, + customerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer ID', + }, + lines: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Replacement bounded credit memo lines', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement credit memo date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement credit memo number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement internal note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer-facing memo', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => buildQuickBooksEntityUrl(params.realmId, 'creditmemo').toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksUpdateSalesDocumentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'CreditMemo'), + outputs: { + record: { + type: 'json', + description: 'Updated native QuickBooks CreditMemo', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/update_customer_payment.ts b/apps/sim/tools/quickbooks/update_customer_payment.ts new file mode 100644 index 00000000000..a2316f04e0f --- /dev/null +++ b/apps/sim/tools/quickbooks/update_customer_payment.ts @@ -0,0 +1,126 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { buildQuickBooksUpdatePaymentBody } from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksMutationResponse, + QuickBooksSalesTransaction, + QuickBooksUpdateCustomerPaymentParams, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksUpdateCustomerPaymentTool: ToolConfig< + QuickBooksUpdateCustomerPaymentParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_update_customer_payment', + name: 'QuickBooks Update Customer Payment', + description: 'Sparse-update a customer payment using its current sync token', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + paymentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Payment ID to update', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current payment sync token', + }, + customerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer ID', + }, + totalAmount: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Replacement positive payment total', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement payment date in YYYY-MM-DD format', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement internal note', + }, + paymentReferenceNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement payment reference number', + }, + paymentMethodId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement payment method ID', + }, + depositAccountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement deposit account ID', + }, + invoiceAllocations: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Replacement bounded invoice allocations', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => buildQuickBooksEntityUrl(params.realmId, 'payment').toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksUpdatePaymentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'Payment'), + outputs: { + record: { + type: 'json', + description: 'Updated native QuickBooks Payment', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/update_estimate.ts b/apps/sim/tools/quickbooks/update_estimate.ts new file mode 100644 index 00000000000..4b25d80dd30 --- /dev/null +++ b/apps/sim/tools/quickbooks/update_estimate.ts @@ -0,0 +1,120 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { buildQuickBooksUpdateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksMutationResponse, + QuickBooksSalesTransaction, + QuickBooksUpdateEstimateParams, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksUpdateEstimateTool: ToolConfig< + QuickBooksUpdateEstimateParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_update_estimate', + name: 'QuickBooks Update Estimate', + description: 'Sparse-update an estimate using its current sync token', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + transactionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Estimate ID to update', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current estimate sync token', + }, + customerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer ID', + }, + lines: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Replacement bounded estimate lines', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement estimate date in YYYY-MM-DD format', + }, + expirationDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement expiration date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement estimate number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement internal note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer-facing memo', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => buildQuickBooksEntityUrl(params.realmId, 'estimate').toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksUpdateSalesDocumentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'Estimate'), + outputs: { + record: { + type: 'json', + description: 'Updated native QuickBooks Estimate', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/update_invoice.ts b/apps/sim/tools/quickbooks/update_invoice.ts new file mode 100644 index 00000000000..2f45adcfb13 --- /dev/null +++ b/apps/sim/tools/quickbooks/update_invoice.ts @@ -0,0 +1,120 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { buildQuickBooksUpdateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksMutationResponse, + QuickBooksSalesTransaction, + QuickBooksUpdateInvoiceParams, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksUpdateInvoiceTool: ToolConfig< + QuickBooksUpdateInvoiceParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_update_invoice', + name: 'QuickBooks Update Invoice', + description: 'Sparse-update an invoice using its current sync token', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + transactionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID to update', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current invoice sync token', + }, + customerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer ID', + }, + lines: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Replacement bounded invoice lines', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement invoice date in YYYY-MM-DD format', + }, + dueDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement due date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement invoice number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement internal note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer-facing memo', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => buildQuickBooksEntityUrl(params.realmId, 'invoice').toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksUpdateSalesDocumentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'Invoice'), + outputs: { + record: { + type: 'json', + description: 'Updated native QuickBooks Invoice', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/update_refund_receipt.ts b/apps/sim/tools/quickbooks/update_refund_receipt.ts new file mode 100644 index 00000000000..ce684335378 --- /dev/null +++ b/apps/sim/tools/quickbooks/update_refund_receipt.ts @@ -0,0 +1,132 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { buildQuickBooksUpdateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksMutationResponse, + QuickBooksSalesTransaction, + QuickBooksUpdateRefundReceiptParams, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksUpdateRefundReceiptTool: ToolConfig< + QuickBooksUpdateRefundReceiptParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_update_refund_receipt', + name: 'QuickBooks Update Refund Receipt', + description: 'Sparse-update a refund receipt using its current sync token', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + transactionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Refund receipt ID to update', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current refund receipt sync token', + }, + customerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer ID', + }, + lines: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Replacement bounded refund lines', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement refund date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement refund receipt number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement internal note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer-facing memo', + }, + paymentMethodId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement payment method ID', + }, + paymentReferenceNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement payment reference number', + }, + depositAccountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement deposit account ID', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => buildQuickBooksEntityUrl(params.realmId, 'refundreceipt').toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksUpdateSalesDocumentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'RefundReceipt'), + outputs: { + record: { + type: 'json', + description: 'Updated native QuickBooks RefundReceipt', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/update_sales_receipt.ts b/apps/sim/tools/quickbooks/update_sales_receipt.ts new file mode 100644 index 00000000000..5a19c1703de --- /dev/null +++ b/apps/sim/tools/quickbooks/update_sales_receipt.ts @@ -0,0 +1,132 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import { buildQuickBooksUpdateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' +import type { + QuickBooksMutationResponse, + QuickBooksSalesTransaction, + QuickBooksUpdateSalesReceiptParams, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_MUTATION_OUTPUTS, + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksUpdateSalesReceiptTool: ToolConfig< + QuickBooksUpdateSalesReceiptParams, + QuickBooksMutationResponse +> = { + id: 'quickbooks_update_sales_receipt', + name: 'QuickBooks Update Sales Receipt', + description: 'Sparse-update a sales receipt using its current sync token', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + transactionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Sales receipt ID to update', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current sales receipt sync token', + }, + customerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer ID', + }, + lines: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Replacement bounded sales receipt lines', + }, + transactionDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement receipt date in YYYY-MM-DD format', + }, + documentNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement sales receipt number', + }, + privateNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement internal note', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement customer-facing memo', + }, + paymentMethodId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement payment method ID', + }, + paymentReferenceNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement payment reference number', + }, + depositAccountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Replacement deposit account ID', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => buildQuickBooksEntityUrl(params.realmId, 'salesreceipt').toString(), + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => buildQuickBooksUpdateSalesDocumentBody(params), + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: (response) => + transformQuickBooksMutationResponse(response, 'SalesReceipt'), + outputs: { + record: { + type: 'json', + description: 'Updated native QuickBooks SalesReceipt', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_MUTATION_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/void_customer_payment.ts b/apps/sim/tools/quickbooks/void_customer_payment.ts new file mode 100644 index 00000000000..4005848362a --- /dev/null +++ b/apps/sim/tools/quickbooks/void_customer_payment.ts @@ -0,0 +1,101 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import type { + QuickBooksSalesTransaction, + QuickBooksVoidResponse, + QuickBooksVoidTransactionParams, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + QUICKBOOKS_VOID_OUTPUTS, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + requiredQuickBooksString, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksVoidCustomerPaymentTool: ToolConfig< + QuickBooksVoidTransactionParams, + QuickBooksVoidResponse +> = { + id: 'quickbooks_void_customer_payment', + name: 'QuickBooks Void Customer Payment', + description: 'Void a customer payment after explicit confirmation', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + transactionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Payment ID to void', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current payment sync token', + }, + confirmVoid: { + type: 'boolean', + required: true, + visibility: 'user-or-llm', + description: 'Explicit confirmation that the payment should be voided', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => { + const url = buildQuickBooksEntityUrl(params.realmId, 'payment') + url.searchParams.set('operation', 'update') + url.searchParams.set('include', 'void') + return url.toString() + }, + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => { + if (params.confirmVoid !== true) throw new Error('Confirm void before voiding the payment') + return { + Id: requiredQuickBooksString(params.transactionId, 'transactionId'), + SyncToken: requiredQuickBooksString(params.syncToken, 'syncToken'), + sparse: true, + } + }, + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: async (response) => { + const result = await transformQuickBooksMutationResponse( + response, + 'Payment' + ) + return { success: true, output: { ...result.output, voided: true } } + }, + outputs: { + record: { + type: 'json', + description: 'Voided native QuickBooks Payment', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_VOID_OUTPUTS, + }, +} diff --git a/apps/sim/tools/quickbooks/void_invoice.ts b/apps/sim/tools/quickbooks/void_invoice.ts new file mode 100644 index 00000000000..5805585b2b2 --- /dev/null +++ b/apps/sim/tools/quickbooks/void_invoice.ts @@ -0,0 +1,99 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' +import type { + QuickBooksSalesTransaction, + QuickBooksVoidResponse, + QuickBooksVoidTransactionParams, +} from '@/tools/quickbooks/types' +import { + QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + QUICKBOOKS_VOID_OUTPUTS, +} from '@/tools/quickbooks/types' +import { + buildQuickBooksEntityUrl, + getQuickBooksToolHeaders, + requiredQuickBooksString, + transformQuickBooksMutationResponse, +} from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +export const quickbooksVoidInvoiceTool: ToolConfig< + QuickBooksVoidTransactionParams, + QuickBooksVoidResponse +> = { + id: 'quickbooks_void_invoice', + name: 'QuickBooks Void Invoice', + description: 'Void an invoice after explicit confirmation', + version: '1.0.0', + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID derived from the connected credential', + }, + transactionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID to void', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current invoice sync token', + }, + confirmVoid: { + type: 'boolean', + required: true, + visibility: 'user-or-llm', + description: 'Explicit confirmation that the invoice should be voided', + }, + }, + oauth: { + required: true, + provider: 'quickbooks', + requiredScopes: ['com.intuit.quickbooks.accounting'], + }, + errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, + request: { + url: (params) => { + const url = buildQuickBooksEntityUrl(params.realmId, 'invoice') + url.searchParams.set('operation', 'void') + return url.toString() + }, + method: 'POST', + headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), + body: (params) => { + if (params.confirmVoid !== true) throw new Error('Confirm void before voiding the invoice') + return { + Id: requiredQuickBooksString(params.transactionId, 'transactionId'), + SyncToken: requiredQuickBooksString(params.syncToken, 'syncToken'), + } + }, + retry: { enabled: false }, + maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES, + }, + transformResponse: async (response) => { + const result = await transformQuickBooksMutationResponse( + response, + 'Invoice' + ) + return { success: true, output: { ...result.output, voided: true } } + }, + outputs: { + record: { + type: 'json', + description: 'Voided native QuickBooks Invoice', + properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, + }, + ...QUICKBOOKS_VOID_OUTPUTS, + }, +} From c166427f8e18d8e706448d851c8b1374c806d40f Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 31 Jul 2026 12:17:18 -0700 Subject: [PATCH 3/8] feat(quickbooks): expose sales operations in the block --- .../docs/en/integrations/quickbooks.mdx | 897 +++++++++++++++++- apps/sim/blocks/blocks/quickbooks.ts | 509 +++++++++- apps/sim/lib/integrations/integrations.json | 66 +- apps/sim/tools/quickbooks/index.ts | 15 + apps/sim/tools/quickbooks/quickbooks.test.ts | 66 +- apps/sim/tools/registry.ts | 30 + 6 files changed, 1549 insertions(+), 34 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/quickbooks.mdx b/apps/docs/content/docs/en/integrations/quickbooks.mdx index af3a7b02c8d..476d1e4dfec 100644 --- a/apps/docs/content/docs/en/integrations/quickbooks.mdx +++ b/apps/docs/content/docs/en/integrations/quickbooks.mdx @@ -1,6 +1,6 @@ --- title: QuickBooks -description: Read and manage QuickBooks Online company and master data +description: Manage QuickBooks Online company, master data, sales, and receivables --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -12,7 +12,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" ## Usage Instructions -Connect one QuickBooks Online company to read company, master-data, purchase-order, and bill records and to create or update customers, vendors, and basic products or services. +Connect one QuickBooks Online company to manage master data and bounded sales and receivables workflows, while preserving existing purchase-order and bill reads. @@ -489,6 +489,899 @@ Sparse-update supported fields on an item without changing its type | ↳ `value` | string | QuickBooks entity ID | | ↳ `name` | string | QuickBooks entity display name | +### `quickbooks_read_sales_transactions` + +List or read one estimate, invoice, sales receipt, payment, credit memo, or refund receipt + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `transactionType` | string | Yes | Sales transaction type to read | +| `readMode` | string | Yes | Whether to list transactions or read one transaction by ID | +| `transactionId` | string | No | QuickBooks transaction ID, required for by-ID reads | +| `startPosition` | number | No | One-based position of the first list record to return | +| `maxResults` | number | No | Number of list records to request \(1–100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `startPosition` | number | One-based position of the first item in this response | +| `maxResults` | number | Actual number of items reported for this response | +| `nextStartPosition` | number | Position to use when explicitly requesting the next page | +| `hasMore` | boolean | Conservative indication that another page may exist | +| `time` | string | QuickBooks response timestamp | +| `transactionType` | string | Sales transaction type returned | +| `item` | json | Single native QuickBooks sales transaction | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | +| `items` | array | Native QuickBooks sales transactions | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_create_estimate` + +Create an estimate with bounded item and description lines + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Customer receiving the estimate | +| `lines` | json | Yes | Bounded item and description lines | +| `transactionDate` | string | No | Estimate date in YYYY-MM-DD format | +| `expirationDate` | string | No | Estimate expiration date in YYYY-MM-DD format | +| `documentNumber` | string | No | Optional estimate number | +| `privateNote` | string | No | Internal estimate note | +| `customerMemo` | string | No | Customer-facing estimate memo | +| `requestId` | string | No | Optional Intuit idempotency request ID, up to 50 characters | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Created native QuickBooks Estimate | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_update_estimate` + +Sparse-update an estimate using its current sync token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `transactionId` | string | Yes | Estimate ID to update | +| `syncToken` | string | Yes | Current estimate sync token | +| `customerId` | string | No | Replacement customer ID | +| `lines` | json | No | Replacement bounded estimate lines | +| `transactionDate` | string | No | Replacement estimate date in YYYY-MM-DD format | +| `expirationDate` | string | No | Replacement expiration date in YYYY-MM-DD format | +| `documentNumber` | string | No | Replacement estimate number | +| `privateNote` | string | No | Replacement internal note | +| `customerMemo` | string | No | Replacement customer-facing memo | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Updated native QuickBooks Estimate | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_create_invoice` + +Create an invoice without emailing or collecting payment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Customer receiving the invoice | +| `lines` | json | Yes | Bounded item and description lines | +| `transactionDate` | string | No | Invoice date in YYYY-MM-DD format | +| `dueDate` | string | No | Invoice due date in YYYY-MM-DD format | +| `documentNumber` | string | No | Optional invoice number | +| `privateNote` | string | No | Internal invoice note | +| `customerMemo` | string | No | Customer-facing invoice memo | +| `requestId` | string | No | Optional Intuit idempotency request ID, up to 50 characters | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Created native QuickBooks Invoice | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_update_invoice` + +Sparse-update an invoice using its current sync token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `transactionId` | string | Yes | Invoice ID to update | +| `syncToken` | string | Yes | Current invoice sync token | +| `customerId` | string | No | Replacement customer ID | +| `lines` | json | No | Replacement bounded invoice lines | +| `transactionDate` | string | No | Replacement invoice date in YYYY-MM-DD format | +| `dueDate` | string | No | Replacement due date in YYYY-MM-DD format | +| `documentNumber` | string | No | Replacement invoice number | +| `privateNote` | string | No | Replacement internal note | +| `customerMemo` | string | No | Replacement customer-facing memo | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Updated native QuickBooks Invoice | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_void_invoice` + +Void an invoice after explicit confirmation + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `transactionId` | string | Yes | Invoice ID to void | +| `syncToken` | string | Yes | Current invoice sync token | +| `confirmVoid` | boolean | Yes | Explicit confirmation that the invoice should be voided | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `voided` | boolean | Whether QuickBooks voided the transaction | +| `record` | json | Voided native QuickBooks Invoice | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_create_sales_receipt` + +Create a sales receipt for a completed customer sale + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Customer for the sales receipt | +| `lines` | json | Yes | Bounded item and description lines | +| `transactionDate` | string | No | Sales receipt date in YYYY-MM-DD format | +| `documentNumber` | string | No | Optional sales receipt number | +| `privateNote` | string | No | Internal sales receipt note | +| `customerMemo` | string | No | Customer-facing sales receipt memo | +| `paymentMethodId` | string | No | QuickBooks payment method ID | +| `paymentReferenceNumber` | string | No | Payment reference number | +| `depositAccountId` | string | No | QuickBooks deposit account ID | +| `requestId` | string | No | Optional Intuit idempotency request ID, up to 50 characters | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Created native QuickBooks SalesReceipt | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_update_sales_receipt` + +Sparse-update a sales receipt using its current sync token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `transactionId` | string | Yes | Sales receipt ID to update | +| `syncToken` | string | Yes | Current sales receipt sync token | +| `customerId` | string | No | Replacement customer ID | +| `lines` | json | No | Replacement bounded sales receipt lines | +| `transactionDate` | string | No | Replacement receipt date in YYYY-MM-DD format | +| `documentNumber` | string | No | Replacement sales receipt number | +| `privateNote` | string | No | Replacement internal note | +| `customerMemo` | string | No | Replacement customer-facing memo | +| `paymentMethodId` | string | No | Replacement payment method ID | +| `paymentReferenceNumber` | string | No | Replacement payment reference number | +| `depositAccountId` | string | No | Replacement deposit account ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Updated native QuickBooks SalesReceipt | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_create_customer_payment` + +Record a customer payment with optional bounded invoice allocations + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Customer making the payment | +| `totalAmount` | number | Yes | Positive total payment amount | +| `transactionDate` | string | No | Payment date in YYYY-MM-DD format | +| `privateNote` | string | No | Internal payment note | +| `paymentReferenceNumber` | string | No | Payment reference number such as a check number | +| `paymentMethodId` | string | No | QuickBooks payment method ID | +| `depositAccountId` | string | No | QuickBooks deposit account ID | +| `invoiceAllocations` | json | No | Up to 100 invoice allocations with invoiceId and positive amount | +| `requestId` | string | No | Optional Intuit idempotency request ID, up to 50 characters | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Created native QuickBooks Payment | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_update_customer_payment` + +Sparse-update a customer payment using its current sync token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `paymentId` | string | Yes | Payment ID to update | +| `syncToken` | string | Yes | Current payment sync token | +| `customerId` | string | No | Replacement customer ID | +| `totalAmount` | number | No | Replacement positive payment total | +| `transactionDate` | string | No | Replacement payment date in YYYY-MM-DD format | +| `privateNote` | string | No | Replacement internal note | +| `paymentReferenceNumber` | string | No | Replacement payment reference number | +| `paymentMethodId` | string | No | Replacement payment method ID | +| `depositAccountId` | string | No | Replacement deposit account ID | +| `invoiceAllocations` | json | No | Replacement bounded invoice allocations | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Updated native QuickBooks Payment | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_void_customer_payment` + +Void a customer payment after explicit confirmation + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `transactionId` | string | Yes | Payment ID to void | +| `syncToken` | string | Yes | Current payment sync token | +| `confirmVoid` | boolean | Yes | Explicit confirmation that the payment should be voided | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `voided` | boolean | Whether QuickBooks voided the transaction | +| `record` | json | Voided native QuickBooks Payment | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_create_credit_memo` + +Create a customer credit memo with bounded sales lines + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Customer receiving the credit memo | +| `lines` | json | Yes | Bounded item and description lines | +| `transactionDate` | string | No | Credit memo date in YYYY-MM-DD format | +| `documentNumber` | string | No | Optional credit memo number | +| `privateNote` | string | No | Internal credit memo note | +| `customerMemo` | string | No | Customer-facing credit memo memo | +| `requestId` | string | No | Optional Intuit idempotency request ID, up to 50 characters | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Created native QuickBooks CreditMemo | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_update_credit_memo` + +Sparse-update a credit memo using its current sync token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `transactionId` | string | Yes | Credit memo ID to update | +| `syncToken` | string | Yes | Current credit memo sync token | +| `customerId` | string | No | Replacement customer ID | +| `lines` | json | No | Replacement bounded credit memo lines | +| `transactionDate` | string | No | Replacement credit memo date in YYYY-MM-DD format | +| `documentNumber` | string | No | Replacement credit memo number | +| `privateNote` | string | No | Replacement internal note | +| `customerMemo` | string | No | Replacement customer-facing memo | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Updated native QuickBooks CreditMemo | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_create_refund_receipt` + +Create a customer refund receipt against a required deposit account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Customer receiving the refund | +| `lines` | json | Yes | Bounded item and description lines | +| `depositAccountId` | string | Yes | QuickBooks bank account funding the refund | +| `transactionDate` | string | No | Refund receipt date in YYYY-MM-DD format | +| `documentNumber` | string | No | Optional refund receipt number | +| `privateNote` | string | No | Internal refund receipt note | +| `customerMemo` | string | No | Customer-facing refund memo | +| `paymentMethodId` | string | No | QuickBooks payment method ID | +| `paymentReferenceNumber` | string | No | Refund payment reference number | +| `requestId` | string | No | Optional Intuit idempotency request ID, up to 50 characters | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Created native QuickBooks RefundReceipt | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + +### `quickbooks_update_refund_receipt` + +Sparse-update a refund receipt using its current sync token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `transactionId` | string | Yes | Refund receipt ID to update | +| `syncToken` | string | Yes | Current refund receipt sync token | +| `customerId` | string | No | Replacement customer ID | +| `lines` | json | No | Replacement bounded refund lines | +| `transactionDate` | string | No | Replacement refund date in YYYY-MM-DD format | +| `documentNumber` | string | No | Replacement refund receipt number | +| `privateNote` | string | No | Replacement internal note | +| `customerMemo` | string | No | Replacement customer-facing memo | +| `paymentMethodId` | string | No | Replacement payment method ID | +| `paymentReferenceNumber` | string | No | Replacement payment reference number | +| `depositAccountId` | string | No | Replacement deposit account ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordId` | string | ID of the created or updated QuickBooks entity | +| `syncToken` | string | Latest sync token required for a subsequent update | +| `time` | string | QuickBooks response timestamp | +| `record` | json | Updated native QuickBooks RefundReceipt | +| ↳ `Id` | string | QuickBooks sales transaction ID | +| ↳ `SyncToken` | string | Current transaction sync token | +| ↳ `DocNumber` | string | Transaction document number | +| ↳ `TxnDate` | string | Transaction date | +| ↳ `DueDate` | string | Invoice due date | +| ↳ `ExpirationDate` | string | Estimate expiration date | +| ↳ `CustomerRef` | json | Customer reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `CustomerMemo` | json | Customer-facing memo | +| ↳ `DepositToAccountRef` | json | Deposit account reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentMethodRef` | json | Payment method reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `PaymentRefNum` | string | Customer payment reference number | +| ↳ `CurrencyRef` | json | Transaction currency reference | +| ↳ `value` | string | QuickBooks entity ID | +| ↳ `name` | string | QuickBooks entity display name | +| ↳ `Line` | array | Native QuickBooks transaction lines | +| ↳ `LinkedTxn` | array | Transactions linked by QuickBooks | +| ↳ `TotalAmt` | number | Transaction total amount | +| ↳ `Balance` | number | Remaining transaction balance | +| ↳ `UnappliedAmt` | number | Unapplied payment amount | +| ↳ `PrivateNote` | string | Internal transaction note | +| ↳ `TxnStatus` | string | Transaction status | +| ↳ `TxnTaxDetail` | json | Calculated tax details | +| ↳ `MetaData` | json | Transaction creation and update timestamps | +| ↳ `CreateTime` | string | Entity creation timestamp | +| ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | + ### `quickbooks_list_purchase_orders` List purchase orders in the connected QuickBooks Online company diff --git a/apps/sim/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts index 7c531c8dc2e..d4bc71ade6f 100644 --- a/apps/sim/blocks/blocks/quickbooks.ts +++ b/apps/sim/blocks/blocks/quickbooks.ts @@ -2,25 +2,76 @@ import { QuickBooksIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { + parseQuickBooksInvoiceAllocations, + parseQuickBooksSalesLines, +} from '@/tools/quickbooks/sales_utils' import type { QuickBooksResponse } from '@/tools/quickbooks/types' import { parseQuickBooksAddress } from '@/tools/quickbooks/utils' const MASTER_DATA_OPERATION = 'quickbooks_read_master_data' +const SALES_READ_OPERATION = 'quickbooks_read_sales_transactions' const CUSTOMER_OPERATIONS = ['quickbooks_create_customer', 'quickbooks_update_customer'] as const const VENDOR_OPERATIONS = ['quickbooks_create_vendor', 'quickbooks_update_vendor'] as const const ITEM_OPERATIONS = ['quickbooks_create_item', 'quickbooks_update_item'] as const -const UPDATE_OPERATIONS = [ +const SALES_DOCUMENT_CREATE_OPERATIONS = [ + 'quickbooks_create_estimate', + 'quickbooks_create_invoice', + 'quickbooks_create_sales_receipt', + 'quickbooks_create_credit_memo', + 'quickbooks_create_refund_receipt', +] as const +const SALES_DOCUMENT_UPDATE_OPERATIONS = [ + 'quickbooks_update_estimate', + 'quickbooks_update_invoice', + 'quickbooks_update_sales_receipt', + 'quickbooks_update_credit_memo', + 'quickbooks_update_refund_receipt', +] as const +const SALES_DOCUMENT_OPERATIONS = [ + ...SALES_DOCUMENT_CREATE_OPERATIONS, + ...SALES_DOCUMENT_UPDATE_OPERATIONS, +] as const +const PAYMENT_OPERATIONS = [ + 'quickbooks_create_customer_payment', + 'quickbooks_update_customer_payment', +] as const +const SALES_CREATE_OPERATIONS = [ + ...SALES_DOCUMENT_CREATE_OPERATIONS, + 'quickbooks_create_customer_payment', +] as const +const SALES_UPDATE_OPERATIONS = [ + ...SALES_DOCUMENT_UPDATE_OPERATIONS, + 'quickbooks_update_customer_payment', +] as const +const SALES_VOID_OPERATIONS = [ + 'quickbooks_void_invoice', + 'quickbooks_void_customer_payment', +] as const +const MASTER_DATA_UPDATE_OPERATIONS = [ 'quickbooks_update_customer', 'quickbooks_update_item', 'quickbooks_update_vendor', ] as const +const SALES_MUTATION_OPERATIONS = [ + ...SALES_CREATE_OPERATIONS, + ...SALES_UPDATE_OPERATIONS, + ...SALES_VOID_OPERATIONS, +] as const +const UPDATE_OPERATIONS = [ + ...MASTER_DATA_UPDATE_OPERATIONS, + ...SALES_UPDATE_OPERATIONS, + ...SALES_VOID_OPERATIONS, +] as const const MUTATION_OPERATIONS = [ ...CUSTOMER_OPERATIONS, ...ITEM_OPERATIONS, ...VENDOR_OPERATIONS, + ...SALES_MUTATION_OPERATIONS, ] as const const PAGINATED_OPERATIONS = [ MASTER_DATA_OPERATION, + SALES_READ_OPERATION, 'quickbooks_list_purchase_orders', 'quickbooks_list_bills', ] as const @@ -31,6 +82,7 @@ const TRANSACTION_LIST_OPERATIONS = [ const QUICKBOOKS_OPERATIONS = [ 'quickbooks_get_company_info', MASTER_DATA_OPERATION, + SALES_READ_OPERATION, ...MUTATION_OPERATIONS, 'quickbooks_list_purchase_orders', 'quickbooks_list_bills', @@ -75,16 +127,32 @@ function paginationCondition(values?: Record) { if (values?.operation === MASTER_DATA_OPERATION) { return { field: 'readMode', value: 'list' } } + if (values?.operation === SALES_READ_OPERATION) { + return { field: 'readMode', value: 'list' } + } return { field: 'operation', value: [...TRANSACTION_LIST_OPERATIONS] } } +function salesTransactionIdCondition(values?: Record) { + if (values?.operation === SALES_READ_OPERATION) { + return { field: 'readMode', value: 'by_id' } + } + return { field: 'operation', value: [...SALES_UPDATE_OPERATIONS, ...SALES_VOID_OPERATIONS] } +} + +function parseConfirmation(value: unknown): boolean { + if (value === true || value === 'yes') return true + if (value === false || value === 'no' || value == null || value === '') return false + throw new Error('confirmVoid must be yes or no') +} + export const QuickBooksBlock: BlockConfig = { type: 'quickbooks', name: 'QuickBooks', - description: 'Read and manage QuickBooks Online company and master data', + description: 'Manage QuickBooks Online company, master data, sales, and receivables', authMode: AuthMode.OAuth, longDescription: - 'Connect one QuickBooks Online company to read company, master-data, purchase-order, and bill records and to create or update customers, vendors, and basic products or services.', + 'Connect one QuickBooks Online company to manage master data and bounded sales and receivables workflows, while preserving existing purchase-order and bill reads.', docsLink: 'https://docs.sim.ai/integrations/quickbooks', category: 'tools', integrationType: IntegrationType.Commerce, @@ -104,6 +172,30 @@ export const QuickBooksBlock: BlockConfig = { { label: 'Vendors: Update', id: 'quickbooks_update_vendor' }, { label: 'Items: Create', id: 'quickbooks_create_item' }, { label: 'Items: Update', id: 'quickbooks_update_item' }, + { label: 'Sales: Read Transactions', id: 'quickbooks_read_sales_transactions' }, + { label: 'Estimates: Create', id: 'quickbooks_create_estimate' }, + { label: 'Estimates: Update', id: 'quickbooks_update_estimate' }, + { label: 'Invoices: Create', id: 'quickbooks_create_invoice' }, + { label: 'Invoices: Update', id: 'quickbooks_update_invoice' }, + { label: 'Invoices: Void', id: 'quickbooks_void_invoice' }, + { label: 'Sales Receipts: Create', id: 'quickbooks_create_sales_receipt' }, + { label: 'Sales Receipts: Update', id: 'quickbooks_update_sales_receipt' }, + { + label: 'Customer Payments: Create', + id: 'quickbooks_create_customer_payment', + }, + { + label: 'Customer Payments: Update', + id: 'quickbooks_update_customer_payment', + }, + { + label: 'Customer Payments: Void', + id: 'quickbooks_void_customer_payment', + }, + { label: 'Credit Memos: Create', id: 'quickbooks_create_credit_memo' }, + { label: 'Credit Memos: Update', id: 'quickbooks_update_credit_memo' }, + { label: 'Refund Receipts: Create', id: 'quickbooks_create_refund_receipt' }, + { label: 'Refund Receipts: Update', id: 'quickbooks_update_refund_receipt' }, { label: 'Purchasing: List Purchase Orders', id: 'quickbooks_list_purchase_orders' }, { label: 'Payables: List Bills', id: 'quickbooks_list_bills' }, ], @@ -142,8 +234,8 @@ export const QuickBooksBlock: BlockConfig = { { label: 'List', id: 'list' }, { label: 'By ID', id: 'by_id' }, ], - condition: { field: 'operation', value: MASTER_DATA_OPERATION }, - required: { field: 'operation', value: MASTER_DATA_OPERATION }, + condition: { field: 'operation', value: [MASTER_DATA_OPERATION, SALES_READ_OPERATION] }, + required: { field: 'operation', value: [MASTER_DATA_OPERATION, SALES_READ_OPERATION] }, value: () => 'list', }, { @@ -162,6 +254,30 @@ export const QuickBooksBlock: BlockConfig = { and: { field: 'readMode', value: 'by_id' }, }, }, + { + id: 'transactionType', + title: 'Transaction Type', + type: 'dropdown', + options: [ + { label: 'Estimate', id: 'estimate' }, + { label: 'Invoice', id: 'invoice' }, + { label: 'Sales Receipt', id: 'sales_receipt' }, + { label: 'Customer Payment', id: 'payment' }, + { label: 'Credit Memo', id: 'credit_memo' }, + { label: 'Refund Receipt', id: 'refund_receipt' }, + ], + condition: { field: 'operation', value: SALES_READ_OPERATION }, + required: { field: 'operation', value: SALES_READ_OPERATION }, + value: () => 'invoice', + }, + { + id: 'transactionId', + title: 'Transaction ID', + type: 'short-input', + placeholder: 'QuickBooks transaction ID', + condition: salesTransactionIdCondition, + required: salesTransactionIdCondition, + }, { id: 'startPosition', title: 'Start Position', @@ -185,8 +301,14 @@ export const QuickBooksBlock: BlockConfig = { title: 'Customer ID', type: 'short-input', placeholder: 'QuickBooks customer ID', - condition: { field: 'operation', value: 'quickbooks_update_customer' }, - required: { field: 'operation', value: 'quickbooks_update_customer' }, + condition: { + field: 'operation', + value: ['quickbooks_update_customer', ...SALES_DOCUMENT_OPERATIONS, ...PAYMENT_OPERATIONS], + }, + required: { + field: 'operation', + value: ['quickbooks_update_customer', ...SALES_CREATE_OPERATIONS], + }, }, { id: 'vendorId', @@ -431,9 +553,178 @@ export const QuickBooksBlock: BlockConfig = { { label: 'Active', id: 'active' }, { label: 'Inactive', id: 'inactive' }, ], - condition: { field: 'operation', value: [...UPDATE_OPERATIONS] }, + condition: { field: 'operation', value: [...MASTER_DATA_UPDATE_OPERATIONS] }, value: () => 'unchanged', }, + { + id: 'lines', + title: 'Lines (JSON)', + type: 'code', + language: 'json', + placeholder: '[{"lineType":"item","amount":100,"itemId":"7","description":"Consulting"}]', + condition: { field: 'operation', value: [...SALES_DOCUMENT_OPERATIONS] }, + required: { field: 'operation', value: [...SALES_DOCUMENT_CREATE_OPERATIONS] }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of QuickBooks sales lines. Use item lines with lineType, positive amount, itemId, and optional description, quantity, unitPrice, and serviceDate; or description lines with lineType and description. Return ONLY the JSON array - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + { + id: 'totalAmount', + title: 'Total Amount', + type: 'short-input', + placeholder: '100.00', + condition: { field: 'operation', value: [...PAYMENT_OPERATIONS] }, + required: { field: 'operation', value: 'quickbooks_create_customer_payment' }, + }, + { + id: 'transactionDate', + title: 'Transaction Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { + field: 'operation', + value: [...SALES_CREATE_OPERATIONS, ...SALES_UPDATE_OPERATIONS], + }, + mode: 'advanced', + }, + { + id: 'dueDate', + title: 'Due Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { + field: 'operation', + value: ['quickbooks_create_invoice', 'quickbooks_update_invoice'], + }, + mode: 'advanced', + }, + { + id: 'expirationDate', + title: 'Expiration Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { + field: 'operation', + value: ['quickbooks_create_estimate', 'quickbooks_update_estimate'], + }, + mode: 'advanced', + }, + { + id: 'documentNumber', + title: 'Document Number', + type: 'short-input', + placeholder: 'Optional QuickBooks document number', + condition: { field: 'operation', value: [...SALES_DOCUMENT_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'privateNote', + title: 'Private Note', + type: 'long-input', + placeholder: 'Internal note', + condition: { + field: 'operation', + value: [...SALES_CREATE_OPERATIONS, ...SALES_UPDATE_OPERATIONS], + }, + mode: 'advanced', + }, + { + id: 'customerMemo', + title: 'Customer Memo', + type: 'long-input', + placeholder: 'Customer-facing memo', + condition: { field: 'operation', value: [...SALES_DOCUMENT_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'paymentMethodId', + title: 'Payment Method ID', + type: 'short-input', + placeholder: 'QuickBooks payment method ID', + condition: { + field: 'operation', + value: [ + 'quickbooks_create_sales_receipt', + 'quickbooks_update_sales_receipt', + 'quickbooks_create_refund_receipt', + 'quickbooks_update_refund_receipt', + ...PAYMENT_OPERATIONS, + ], + }, + mode: 'advanced', + }, + { + id: 'paymentReferenceNumber', + title: 'Payment Reference Number', + type: 'short-input', + placeholder: 'Check or payment reference', + condition: { + field: 'operation', + value: [ + 'quickbooks_create_sales_receipt', + 'quickbooks_update_sales_receipt', + 'quickbooks_create_refund_receipt', + 'quickbooks_update_refund_receipt', + ...PAYMENT_OPERATIONS, + ], + }, + mode: 'advanced', + }, + { + id: 'depositAccountId', + title: 'Deposit Account ID', + type: 'short-input', + placeholder: 'QuickBooks deposit account ID', + condition: { + field: 'operation', + value: [ + 'quickbooks_create_sales_receipt', + 'quickbooks_update_sales_receipt', + 'quickbooks_create_refund_receipt', + 'quickbooks_update_refund_receipt', + ...PAYMENT_OPERATIONS, + ], + }, + required: { field: 'operation', value: 'quickbooks_create_refund_receipt' }, + }, + { + id: 'invoiceAllocations', + title: 'Invoice Allocations (JSON)', + type: 'code', + language: 'json', + placeholder: '[{"invoiceId":"42","amount":75}]', + condition: { field: 'operation', value: [...PAYMENT_OPERATIONS] }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of QuickBooks invoice allocations using only invoiceId and a positive amount. Return ONLY the JSON array - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + { + id: 'requestId', + title: 'Request ID', + type: 'short-input', + placeholder: 'Optional idempotency key (max 50 characters)', + condition: { field: 'operation', value: [...SALES_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'confirmVoid', + title: 'Confirm Void', + type: 'dropdown', + options: [ + { label: 'No', id: 'no' }, + { label: 'Yes', id: 'yes' }, + ], + condition: { field: 'operation', value: [...SALES_VOID_OPERATIONS] }, + required: { field: 'operation', value: [...SALES_VOID_OPERATIONS] }, + value: () => 'no', + }, ], tools: { access: [ @@ -445,6 +736,21 @@ export const QuickBooksBlock: BlockConfig = { 'quickbooks_update_vendor', 'quickbooks_create_item', 'quickbooks_update_item', + 'quickbooks_read_sales_transactions', + 'quickbooks_create_estimate', + 'quickbooks_update_estimate', + 'quickbooks_create_invoice', + 'quickbooks_update_invoice', + 'quickbooks_void_invoice', + 'quickbooks_create_sales_receipt', + 'quickbooks_update_sales_receipt', + 'quickbooks_create_customer_payment', + 'quickbooks_update_customer_payment', + 'quickbooks_void_customer_payment', + 'quickbooks_create_credit_memo', + 'quickbooks_update_credit_memo', + 'quickbooks_create_refund_receipt', + 'quickbooks_update_refund_receipt', 'quickbooks_list_purchase_orders', 'quickbooks_list_bills', ], @@ -477,6 +783,85 @@ export const QuickBooksBlock: BlockConfig = { maxResults: parsePaginationInteger(params.maxResults, 'maxResults', 25), } } + if (operation === SALES_READ_OPERATION) { + if (params.readMode === 'by_id') { + return { + credential: oauthCredentialValue, + transactionType: params.transactionType, + readMode: params.readMode, + transactionId: optionalValue(params.transactionId), + } + } + return { + credential: oauthCredentialValue, + transactionType: params.transactionType, + readMode: params.readMode, + startPosition: parsePaginationInteger(params.startPosition, 'startPosition', 1), + maxResults: parsePaginationInteger(params.maxResults, 'maxResults', 25), + } + } + if (SALES_VOID_OPERATIONS.includes(operation as (typeof SALES_VOID_OPERATIONS)[number])) { + return { + credential: oauthCredentialValue, + transactionId: optionalValue(params.transactionId), + syncToken: optionalValue(params.syncToken), + confirmVoid: parseConfirmation(params.confirmVoid), + } + } + if ( + SALES_DOCUMENT_OPERATIONS.includes( + operation as (typeof SALES_DOCUMENT_OPERATIONS)[number] + ) + ) { + const isCreate = SALES_DOCUMENT_CREATE_OPERATIONS.includes( + operation as (typeof SALES_DOCUMENT_CREATE_OPERATIONS)[number] + ) + const isInvoice = + operation === 'quickbooks_create_invoice' || operation === 'quickbooks_update_invoice' + const isEstimate = + operation === 'quickbooks_create_estimate' || operation === 'quickbooks_update_estimate' + const isReceipt = + operation === 'quickbooks_create_sales_receipt' || + operation === 'quickbooks_update_sales_receipt' || + operation === 'quickbooks_create_refund_receipt' || + operation === 'quickbooks_update_refund_receipt' + return { + credential: oauthCredentialValue, + transactionId: isCreate ? undefined : optionalValue(params.transactionId), + syncToken: isCreate ? undefined : optionalValue(params.syncToken), + customerId: optionalValue(params.customerId), + lines: parseQuickBooksSalesLines(params.lines), + transactionDate: optionalValue(params.transactionDate), + dueDate: isInvoice ? optionalValue(params.dueDate) : undefined, + expirationDate: isEstimate ? optionalValue(params.expirationDate) : undefined, + documentNumber: optionalValue(params.documentNumber), + privateNote: optionalValue(params.privateNote), + customerMemo: optionalValue(params.customerMemo), + paymentMethodId: isReceipt ? optionalValue(params.paymentMethodId) : undefined, + paymentReferenceNumber: isReceipt + ? optionalValue(params.paymentReferenceNumber) + : undefined, + depositAccountId: isReceipt ? optionalValue(params.depositAccountId) : undefined, + requestId: isCreate ? optionalValue(params.requestId) : undefined, + } + } + if (PAYMENT_OPERATIONS.includes(operation as (typeof PAYMENT_OPERATIONS)[number])) { + const isCreate = operation === 'quickbooks_create_customer_payment' + return { + credential: oauthCredentialValue, + paymentId: isCreate ? undefined : optionalValue(params.transactionId), + syncToken: isCreate ? undefined : optionalValue(params.syncToken), + customerId: optionalValue(params.customerId), + totalAmount: parseOptionalNumber(params.totalAmount, 'totalAmount'), + transactionDate: optionalValue(params.transactionDate), + privateNote: optionalValue(params.privateNote), + paymentReferenceNumber: optionalValue(params.paymentReferenceNumber), + paymentMethodId: optionalValue(params.paymentMethodId), + depositAccountId: optionalValue(params.depositAccountId), + invoiceAllocations: parseQuickBooksInvoiceAllocations(params.invoiceAllocations), + requestId: isCreate ? optionalValue(params.requestId) : undefined, + } + } if ( operation === 'quickbooks_list_purchase_orders' || operation === 'quickbooks_list_bills' @@ -556,6 +941,8 @@ export const QuickBooksBlock: BlockConfig = { recordType: { type: 'string', description: 'Master-data entity type' }, readMode: { type: 'string', description: 'List or by-ID read mode' }, recordId: { type: 'string', description: 'Master-data record ID' }, + transactionType: { type: 'string', description: 'Sales transaction entity type' }, + transactionId: { type: 'string', description: 'Sales transaction ID' }, startPosition: { type: 'number', description: 'One-based position of the first list item to request', @@ -564,7 +951,7 @@ export const QuickBooksBlock: BlockConfig = { type: 'number', description: 'Number of list items to request, from 1 through 100', }, - customerId: { type: 'string', description: 'Customer ID for an update' }, + customerId: { type: 'string', description: 'QuickBooks customer ID' }, vendorId: { type: 'string', description: 'Vendor ID for an update' }, itemId: { type: 'string', description: 'Item ID for an update' }, syncToken: { type: 'string', description: 'Current entity sync token' }, @@ -589,6 +976,26 @@ export const QuickBooksBlock: BlockConfig = { purchaseCost: { type: 'number', description: 'Item purchase cost' }, expenseAccountId: { type: 'string', description: 'Item expense account ID' }, activeStatus: { type: 'string', description: 'Entity active-status change' }, + lines: { type: 'json', description: 'Bounded item and description sales lines' }, + totalAmount: { type: 'number', description: 'Customer payment total' }, + transactionDate: { type: 'string', description: 'Transaction date in YYYY-MM-DD format' }, + dueDate: { type: 'string', description: 'Invoice due date in YYYY-MM-DD format' }, + expirationDate: { + type: 'string', + description: 'Estimate expiration date in YYYY-MM-DD format', + }, + documentNumber: { type: 'string', description: 'QuickBooks document number' }, + privateNote: { type: 'string', description: 'Internal transaction note' }, + customerMemo: { type: 'string', description: 'Customer-facing transaction memo' }, + paymentMethodId: { type: 'string', description: 'QuickBooks payment method ID' }, + paymentReferenceNumber: { type: 'string', description: 'Payment reference number' }, + depositAccountId: { type: 'string', description: 'QuickBooks deposit account ID' }, + invoiceAllocations: { + type: 'json', + description: 'Bounded customer-payment allocations to invoices', + }, + requestId: { type: 'string', description: 'Optional Intuit idempotency request ID' }, + confirmVoid: { type: 'boolean', description: 'Explicit confirmation for a void operation' }, }, outputs: { company: { @@ -602,20 +1009,24 @@ export const QuickBooksBlock: BlockConfig = { description: 'Master-data record type returned by the read', condition: { field: 'operation', value: MASTER_DATA_OPERATION }, }, + transactionType: { + type: 'string', + description: 'Sales transaction type returned by the read', + condition: { field: 'operation', value: SALES_READ_OPERATION }, + }, item: { type: 'json', - description: - 'Single Account, Customer, Vendor, Item, or Employee with native QuickBooks fields', + description: 'Single master-data or sales transaction record with native QuickBooks fields', condition: { field: 'operation', - value: MASTER_DATA_OPERATION, + value: [MASTER_DATA_OPERATION, SALES_READ_OPERATION], and: { field: 'readMode', value: 'by_id' }, }, }, items: { type: 'array', description: - 'Account, Customer, Vendor, Item, Employee, PurchaseOrder, or Bill objects with native QuickBooks fields', + 'Master-data, sales transaction, PurchaseOrder, or Bill objects with native QuickBooks fields', condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, }, startPosition: { @@ -640,12 +1051,13 @@ export const QuickBooksBlock: BlockConfig = { }, record: { type: 'json', - description: 'Created or updated Customer, Vendor, or Item with native QuickBooks fields', + description: + 'Created, updated, or voided master-data or sales record with native QuickBooks fields', condition: { field: 'operation', value: [...MUTATION_OPERATIONS] }, }, recordId: { type: 'string', - description: 'ID of the created or updated QuickBooks record', + description: 'ID of the created, updated, or voided QuickBooks record', condition: { field: 'operation', value: [...MUTATION_OPERATIONS] }, }, syncToken: { @@ -653,6 +1065,11 @@ export const QuickBooksBlock: BlockConfig = { description: 'Latest QuickBooks sync token for a subsequent update', condition: { field: 'operation', value: [...MUTATION_OPERATIONS] }, }, + voided: { + type: 'boolean', + description: 'True when QuickBooks successfully voided the transaction', + condition: { field: 'operation', value: [...SALES_VOID_OPERATIONS] }, + }, time: { type: 'string', description: 'QuickBooks response timestamp', @@ -700,6 +1117,42 @@ export const QuickBooksBlockMeta = { category: 'operations', tags: ['finance', 'audit', 'data-quality'], }, + { + icon: QuickBooksIcon, + title: 'QuickBooks estimate preparation', + prompt: + 'Build a workflow that receives an approved customer quote and line items, creates a QuickBooks estimate, and stores its ID and sync token for controlled revisions.', + modules: ['tables', 'agent', 'workflows'], + category: 'operations', + tags: ['finance', 'estimates', 'sales'], + }, + { + icon: QuickBooksIcon, + title: 'QuickBooks invoice creation', + prompt: + 'Create a workflow that validates approved customer and item IDs, creates a QuickBooks invoice without emailing it, and stores the returned ID and sync token.', + modules: ['tables', 'agent', 'workflows'], + category: 'operations', + tags: ['finance', 'invoices', 'receivables'], + }, + { + icon: QuickBooksIcon, + title: 'QuickBooks partial-payment application', + prompt: + 'Build a workflow that records a customer payment, applies bounded amounts to approved QuickBooks invoice IDs, and reports any unapplied remainder.', + modules: ['tables', 'agent', 'workflows'], + category: 'operations', + tags: ['finance', 'payments', 'receivables'], + }, + { + icon: QuickBooksIcon, + title: 'QuickBooks credit and refund review', + prompt: + 'Build a controlled workflow that creates approved QuickBooks credit memos or refund receipts, stores their IDs and sync tokens, and sends the resulting records for review.', + modules: ['tables', 'agent', 'workflows'], + category: 'operations', + tags: ['finance', 'credits', 'refunds'], + }, { icon: QuickBooksIcon, title: 'QuickBooks purchase order digest', @@ -756,24 +1209,28 @@ export const QuickBooksBlockMeta = { '# Audit QuickBooks Master Data\n\n## Steps\n1. Use Master Data: Read in List mode for the required record types.\n2. Continue only with explicit `nextStartPosition` values while `hasMore` is true.\n3. Report incomplete or inconsistent records with their QuickBooks IDs.\n\n## Output\nReturn a read-only audit with source IDs and supporting values.', }, { - name: 'reconcile-pos-to-bills', - description: - 'Compare QuickBooks purchase orders and bills and flag likely reconciliation exceptions.', + name: 'prepare-quickbooks-estimates', + description: 'Create and revise bounded QuickBooks estimates from approved quote details.', content: - '# Reconcile QuickBooks Purchase Orders to Bills\n\n## Steps\n1. List Purchase Orders and Bills for the required explicit pages.\n2. Match records using vendor references, document numbers, dates, lines, and amounts.\n3. Flag missing matches, amount differences, and duplicate candidates.\n\n## Output\nReturn a reconciliation report with QuickBooks IDs. Do not modify bills or close purchase orders.', + '# Prepare QuickBooks Estimates\n\n## Steps\n1. Validate the customer, item IDs, amounts, and dates.\n2. Use Estimates: Create with bounded item or description lines.\n3. For a revision, use the estimate ID and latest `syncToken` with Estimates: Update.\n\n## Output\nReturn the native Estimate, ID, and latest sync token. Do not claim to email or accept the estimate.', }, { - name: 'report-unpaid-bills', - description: 'Report QuickBooks bills that retain an unpaid balance.', + name: 'create-quickbooks-invoices', + description: 'Create approved QuickBooks invoices and retain identifiers for later updates.', content: - '# Report Unpaid QuickBooks Bills\n\n## Steps\n1. List Bills page by page as needed.\n2. Keep bills whose populated `Balance` is greater than zero.\n3. Group by vendor, currency, and due date while retaining bill IDs.\n\n## Output\nReturn unpaid bill details and subtotals. Do not mark bills paid or change due dates.', + '# Create QuickBooks Invoices\n\n## Steps\n1. Validate the approved customer, item IDs, positive amounts, and optional dates.\n2. Use Invoices: Create with at least one bounded line.\n3. Store the returned `recordId` and `syncToken`.\n\n## Output\nReturn the native Invoice and identifiers. Do not claim to email the invoice or collect payment automatically.', }, { - name: 'reactivate-master-data-records', - description: - 'Reactivate a known customer, vendor, or supported item using its current token.', + name: 'apply-quickbooks-customer-payments', + description: 'Record customer payments with bounded optional invoice allocations.', + content: + '# Apply QuickBooks Customer Payments\n\n## Steps\n1. Validate the customer ID and positive payment total.\n2. Optionally allocate positive amounts to known invoice IDs without exceeding the payment total.\n3. Use Customer Payments: Create and store the returned ID and sync token.\n\n## Output\nReturn the native Payment and any unapplied remainder reported by QuickBooks.', + }, + { + name: 'manage-quickbooks-credits-and-refunds', + description: 'Create or update bounded customer credit memos and refund receipts.', content: - '# Reactivate QuickBooks Master Data\n\n## Steps\n1. Read the inactive record by ID to obtain its current `SyncToken`.\n2. Use the matching Update operation with Active Status set to Active.\n3. Retain the returned latest sync token.\n\n## Output\nReturn the reactivated record and identifiers. Account and employee administration is unsupported.', + '# Manage QuickBooks Credits and Refunds\n\n## Steps\n1. Validate the approved customer, line items, and account references.\n2. Use Credit Memos or Refund Receipts Create; provide a deposit account for a refund receipt.\n3. Use the returned ID and current sync token for any approved update.\n\n## Output\nReturn the native transaction and identifiers. Do not claim to administer taxes or payment collection.', }, ], } as const satisfies BlockMeta diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 8c9764537bb..1e0ba684393 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -14519,8 +14519,8 @@ "type": "quickbooks", "slug": "quickbooks", "name": "QuickBooks", - "description": "Read and manage QuickBooks Online company and master data", - "longDescription": "Connect one QuickBooks Online company to read company, master-data, purchase-order, and bill records and to create or update customers, vendors, and basic products or services.", + "description": "Manage QuickBooks Online company, master data, sales, and receivables", + "longDescription": "Connect one QuickBooks Online company to manage master data and bounded sales and receivables workflows, while preserving existing purchase-order and bill reads.", "bgColor": "#2CA01C", "iconName": "QuickBooksIcon", "docsUrl": "https://docs.sim.ai/integrations/quickbooks", @@ -14557,6 +14557,66 @@ "name": "Items: Update", "description": "Sparse-update supported fields on an item without changing its type" }, + { + "name": "Sales: Read Transactions", + "description": "List or read one estimate, invoice, sales receipt, payment, credit memo, or refund receipt" + }, + { + "name": "Estimates: Create", + "description": "Create an estimate with bounded item and description lines" + }, + { + "name": "Estimates: Update", + "description": "Sparse-update an estimate using its current sync token" + }, + { + "name": "Invoices: Create", + "description": "Create an invoice without emailing or collecting payment" + }, + { + "name": "Invoices: Update", + "description": "Sparse-update an invoice using its current sync token" + }, + { + "name": "Invoices: Void", + "description": "Void an invoice after explicit confirmation" + }, + { + "name": "Sales Receipts: Create", + "description": "Create a sales receipt for a completed customer sale" + }, + { + "name": "Sales Receipts: Update", + "description": "Sparse-update a sales receipt using its current sync token" + }, + { + "name": "Customer Payments: Create", + "description": "Record a customer payment with optional bounded invoice allocations" + }, + { + "name": "Customer Payments: Update", + "description": "Sparse-update a customer payment using its current sync token" + }, + { + "name": "Customer Payments: Void", + "description": "Void a customer payment after explicit confirmation" + }, + { + "name": "Credit Memos: Create", + "description": "Create a customer credit memo with bounded sales lines" + }, + { + "name": "Credit Memos: Update", + "description": "Sparse-update a credit memo using its current sync token" + }, + { + "name": "Refund Receipts: Create", + "description": "Create a customer refund receipt against a required deposit account" + }, + { + "name": "Refund Receipts: Update", + "description": "Sparse-update a refund receipt using its current sync token" + }, { "name": "Purchasing: List Purchase Orders", "description": "List purchase orders in the connected QuickBooks Online company" @@ -14566,7 +14626,7 @@ "description": "List bills in the connected QuickBooks Online company" } ], - "operationCount": 10, + "operationCount": 25, "triggers": [], "triggerCount": 0, "authType": "oauth", diff --git a/apps/sim/tools/quickbooks/index.ts b/apps/sim/tools/quickbooks/index.ts index 822bde714c1..736e94701ac 100644 --- a/apps/sim/tools/quickbooks/index.ts +++ b/apps/sim/tools/quickbooks/index.ts @@ -1,11 +1,26 @@ +export { quickbooksCreateCreditMemoTool } from '@/tools/quickbooks/create_credit_memo' export { quickbooksCreateCustomerTool } from '@/tools/quickbooks/create_customer' +export { quickbooksCreateCustomerPaymentTool } from '@/tools/quickbooks/create_customer_payment' +export { quickbooksCreateEstimateTool } from '@/tools/quickbooks/create_estimate' +export { quickbooksCreateInvoiceTool } from '@/tools/quickbooks/create_invoice' export { quickbooksCreateItemTool } from '@/tools/quickbooks/create_item' +export { quickbooksCreateRefundReceiptTool } from '@/tools/quickbooks/create_refund_receipt' +export { quickbooksCreateSalesReceiptTool } from '@/tools/quickbooks/create_sales_receipt' export { quickbooksCreateVendorTool } from '@/tools/quickbooks/create_vendor' export { quickbooksGetCompanyInfoTool } from '@/tools/quickbooks/get_company_info' export { quickbooksListBillsTool } from '@/tools/quickbooks/list_bills' export { quickbooksListPurchaseOrdersTool } from '@/tools/quickbooks/list_purchase_orders' export { quickbooksReadMasterDataTool } from '@/tools/quickbooks/read_master_data' +export { quickbooksReadSalesTransactionsTool } from '@/tools/quickbooks/read_sales_transactions' export * from '@/tools/quickbooks/types' +export { quickbooksUpdateCreditMemoTool } from '@/tools/quickbooks/update_credit_memo' export { quickbooksUpdateCustomerTool } from '@/tools/quickbooks/update_customer' +export { quickbooksUpdateCustomerPaymentTool } from '@/tools/quickbooks/update_customer_payment' +export { quickbooksUpdateEstimateTool } from '@/tools/quickbooks/update_estimate' +export { quickbooksUpdateInvoiceTool } from '@/tools/quickbooks/update_invoice' export { quickbooksUpdateItemTool } from '@/tools/quickbooks/update_item' +export { quickbooksUpdateRefundReceiptTool } from '@/tools/quickbooks/update_refund_receipt' +export { quickbooksUpdateSalesReceiptTool } from '@/tools/quickbooks/update_sales_receipt' export { quickbooksUpdateVendorTool } from '@/tools/quickbooks/update_vendor' +export { quickbooksVoidCustomerPaymentTool } from '@/tools/quickbooks/void_customer_payment' +export { quickbooksVoidInvoiceTool } from '@/tools/quickbooks/void_invoice' diff --git a/apps/sim/tools/quickbooks/quickbooks.test.ts b/apps/sim/tools/quickbooks/quickbooks.test.ts index 4a4f07ced8a..0973ab248d8 100644 --- a/apps/sim/tools/quickbooks/quickbooks.test.ts +++ b/apps/sim/tools/quickbooks/quickbooks.test.ts @@ -8,6 +8,23 @@ import { getQuickBooksUserInfoUrl, QUICKBOOKS_MAX_RESPONSE_BYTES, } from '@/tools/quickbooks/client' +import { + quickbooksCreateCreditMemoTool, + quickbooksCreateCustomerPaymentTool, + quickbooksCreateEstimateTool, + quickbooksCreateInvoiceTool, + quickbooksCreateRefundReceiptTool, + quickbooksCreateSalesReceiptTool, + quickbooksReadSalesTransactionsTool, + quickbooksUpdateCreditMemoTool, + quickbooksUpdateCustomerPaymentTool, + quickbooksUpdateEstimateTool, + quickbooksUpdateInvoiceTool, + quickbooksUpdateRefundReceiptTool, + quickbooksUpdateSalesReceiptTool, + quickbooksVoidCustomerPaymentTool, + quickbooksVoidInvoiceTool, +} from '@/tools/quickbooks' import { quickbooksCreateCustomerTool } from '@/tools/quickbooks/create_customer' import { quickbooksCreateItemTool } from '@/tools/quickbooks/create_item' import { quickbooksCreateVendorTool } from '@/tools/quickbooks/create_vendor' @@ -654,19 +671,34 @@ describe('QuickBooks item mutations', () => { describe('QuickBooks tool and block boundaries', () => { const tools = [ + quickbooksCreateCreditMemoTool, quickbooksCreateCustomerTool, + quickbooksCreateCustomerPaymentTool, + quickbooksCreateEstimateTool, quickbooksCreateItemTool, + quickbooksCreateInvoiceTool, + quickbooksCreateRefundReceiptTool, + quickbooksCreateSalesReceiptTool, quickbooksCreateVendorTool, quickbooksGetCompanyInfoTool, quickbooksListBillsTool, quickbooksListPurchaseOrdersTool, quickbooksReadMasterDataTool, + quickbooksReadSalesTransactionsTool, + quickbooksUpdateCreditMemoTool, quickbooksUpdateCustomerTool, + quickbooksUpdateCustomerPaymentTool, + quickbooksUpdateEstimateTool, quickbooksUpdateItemTool, + quickbooksUpdateInvoiceTool, + quickbooksUpdateRefundReceiptTool, + quickbooksUpdateSalesReceiptTool, quickbooksUpdateVendorTool, + quickbooksVoidCustomerPaymentTool, + quickbooksVoidInvoiceTool, ] - it('declares exactly ten bounded tools with hidden company credentials and no retries', () => { + it('declares exactly 25 bounded tools with hidden company credentials and no retries', () => { expect(tools.map((tool) => tool.id).sort()).toEqual([...QuickBooksBlock.tools.access].sort()) for (const tool of tools) { expect(tool.params.accessToken).toMatchObject({ required: true, visibility: 'hidden' }) @@ -677,7 +709,7 @@ describe('QuickBooks tool and block boundaries', () => { } }) - it('exposes the ten compact operations and unique subblock IDs', () => { + it('exposes the 25 compact operations and unique subblock IDs', () => { const operation = QuickBooksBlock.subBlocks.find((subBlock) => subBlock.id === 'operation') expect(operation?.options).toEqual([ { label: 'Company: Get Info', id: 'quickbooks_get_company_info' }, @@ -688,6 +720,21 @@ describe('QuickBooks tool and block boundaries', () => { { label: 'Vendors: Update', id: 'quickbooks_update_vendor' }, { label: 'Items: Create', id: 'quickbooks_create_item' }, { label: 'Items: Update', id: 'quickbooks_update_item' }, + { label: 'Sales: Read Transactions', id: 'quickbooks_read_sales_transactions' }, + { label: 'Estimates: Create', id: 'quickbooks_create_estimate' }, + { label: 'Estimates: Update', id: 'quickbooks_update_estimate' }, + { label: 'Invoices: Create', id: 'quickbooks_create_invoice' }, + { label: 'Invoices: Update', id: 'quickbooks_update_invoice' }, + { label: 'Invoices: Void', id: 'quickbooks_void_invoice' }, + { label: 'Sales Receipts: Create', id: 'quickbooks_create_sales_receipt' }, + { label: 'Sales Receipts: Update', id: 'quickbooks_update_sales_receipt' }, + { label: 'Customer Payments: Create', id: 'quickbooks_create_customer_payment' }, + { label: 'Customer Payments: Update', id: 'quickbooks_update_customer_payment' }, + { label: 'Customer Payments: Void', id: 'quickbooks_void_customer_payment' }, + { label: 'Credit Memos: Create', id: 'quickbooks_create_credit_memo' }, + { label: 'Credit Memos: Update', id: 'quickbooks_update_credit_memo' }, + { label: 'Refund Receipts: Create', id: 'quickbooks_create_refund_receipt' }, + { label: 'Refund Receipts: Update', id: 'quickbooks_update_refund_receipt' }, { label: 'Purchasing: List Purchase Orders', id: 'quickbooks_list_purchase_orders', @@ -820,13 +867,26 @@ describe('QuickBooks tool and block boundaries', () => { field: 'operation', value: [ 'quickbooks_read_master_data', + 'quickbooks_read_sales_transactions', 'quickbooks_list_purchase_orders', 'quickbooks_list_bills', ], }) expect(subBlocks.syncToken.condition).toEqual({ field: 'operation', - value: ['quickbooks_update_customer', 'quickbooks_update_item', 'quickbooks_update_vendor'], + value: [ + 'quickbooks_update_customer', + 'quickbooks_update_item', + 'quickbooks_update_vendor', + 'quickbooks_update_estimate', + 'quickbooks_update_invoice', + 'quickbooks_update_sales_receipt', + 'quickbooks_update_credit_memo', + 'quickbooks_update_refund_receipt', + 'quickbooks_update_customer_payment', + 'quickbooks_void_invoice', + 'quickbooks_void_customer_payment', + ], }) expect(subBlocks.itemType.condition).toEqual({ field: 'operation', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 70b19f39892..aa94114d8b7 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2982,16 +2982,31 @@ import { quartrListTranscriptsTool, } from '@/tools/quartr' import { + quickbooksCreateCreditMemoTool, + quickbooksCreateCustomerPaymentTool, quickbooksCreateCustomerTool, + quickbooksCreateEstimateTool, + quickbooksCreateInvoiceTool, quickbooksCreateItemTool, + quickbooksCreateRefundReceiptTool, + quickbooksCreateSalesReceiptTool, quickbooksCreateVendorTool, quickbooksGetCompanyInfoTool, quickbooksListBillsTool, quickbooksListPurchaseOrdersTool, quickbooksReadMasterDataTool, + quickbooksReadSalesTransactionsTool, + quickbooksUpdateCreditMemoTool, + quickbooksUpdateCustomerPaymentTool, quickbooksUpdateCustomerTool, + quickbooksUpdateEstimateTool, + quickbooksUpdateInvoiceTool, quickbooksUpdateItemTool, + quickbooksUpdateRefundReceiptTool, + quickbooksUpdateSalesReceiptTool, quickbooksUpdateVendorTool, + quickbooksVoidCustomerPaymentTool, + quickbooksVoidInvoiceTool, } from '@/tools/quickbooks' import { quiverImageToSvgTool, quiverListModelsTool, quiverTextToSvgTool } from '@/tools/quiver' import { @@ -6034,15 +6049,30 @@ export const tools: Record = { postgresql_execute: postgresExecuteTool, postgresql_introspect: postgresIntrospectTool, quickbooks_create_customer: quickbooksCreateCustomerTool, + quickbooks_create_customer_payment: quickbooksCreateCustomerPaymentTool, + quickbooks_create_credit_memo: quickbooksCreateCreditMemoTool, + quickbooks_create_estimate: quickbooksCreateEstimateTool, quickbooks_create_item: quickbooksCreateItemTool, + quickbooks_create_invoice: quickbooksCreateInvoiceTool, + quickbooks_create_refund_receipt: quickbooksCreateRefundReceiptTool, + quickbooks_create_sales_receipt: quickbooksCreateSalesReceiptTool, quickbooks_create_vendor: quickbooksCreateVendorTool, quickbooks_get_company_info: quickbooksGetCompanyInfoTool, quickbooks_list_bills: quickbooksListBillsTool, quickbooks_list_purchase_orders: quickbooksListPurchaseOrdersTool, quickbooks_read_master_data: quickbooksReadMasterDataTool, + quickbooks_read_sales_transactions: quickbooksReadSalesTransactionsTool, quickbooks_update_customer: quickbooksUpdateCustomerTool, + quickbooks_update_customer_payment: quickbooksUpdateCustomerPaymentTool, + quickbooks_update_credit_memo: quickbooksUpdateCreditMemoTool, + quickbooks_update_estimate: quickbooksUpdateEstimateTool, quickbooks_update_item: quickbooksUpdateItemTool, + quickbooks_update_invoice: quickbooksUpdateInvoiceTool, + quickbooks_update_refund_receipt: quickbooksUpdateRefundReceiptTool, + quickbooks_update_sales_receipt: quickbooksUpdateSalesReceiptTool, quickbooks_update_vendor: quickbooksUpdateVendorTool, + quickbooks_void_customer_payment: quickbooksVoidCustomerPaymentTool, + quickbooks_void_invoice: quickbooksVoidInvoiceTool, rds_query: rdsQueryTool, rds_insert: rdsInsertTool, rds_update: rdsUpdateTool, From e334bbc27c7c22c03a7bbbb25e250fdb2157ba05 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 31 Jul 2026 12:38:30 -0700 Subject: [PATCH 4/8] fix(quickbooks): address independent sales review --- .../docs/en/integrations/quickbooks.mdx | 13 ++++++ apps/sim/blocks/blocks/quickbooks.ts | 24 +++++------ .../sim/lib/workflows/blocks/block-outputs.ts | 6 ++- apps/sim/tools/quickbooks/quickbooks.test.ts | 32 ++++++++++++--- apps/sim/tools/quickbooks/sales.test.ts | 41 +++++++++++++++++++ apps/sim/tools/quickbooks/sales_utils.ts | 8 +++- apps/sim/tools/quickbooks/utils.ts | 17 ++++++-- packages/workflow-types/src/blocks.ts | 1 + 8 files changed, 118 insertions(+), 24 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/quickbooks.mdx b/apps/docs/content/docs/en/integrations/quickbooks.mdx index 476d1e4dfec..1a823ba7f53 100644 --- a/apps/docs/content/docs/en/integrations/quickbooks.mdx +++ b/apps/docs/content/docs/en/integrations/quickbooks.mdx @@ -10,6 +10,19 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#2CA01C" /> +{/* MANUAL-CONTENT-START:intro */} +Connect one QuickBooks Online company per credential. During OAuth, choose the company that the workflow should access; Sim binds that company to the credential automatically, so you do not enter a realm ID or API host. + +Master Data and Sales transaction reads support **List** and **By ID** modes. List actions return at most one page. Use `nextStartPosition` in another workflow step when `hasMore` is true. Sim does not paginate, retry, or fetch related records automatically. + +QuickBooks updates are sparse: provide the record ID, its current `SyncToken`, and only the fields you want to change. Use the latest `SyncToken` returned by a read or mutation. Voiding keeps the transaction in QuickBooks with a zeroed financial effect; it is not deletion and requires explicit confirmation. Create actions accept an optional `requestId` that QuickBooks uses for idempotency when the same request may be submitted again. + +Sandbox credentials call only Intuit's sandbox API and are suitable for disposable test data. Production credentials call the production API and affect the selected live company. + +Reports and attachments are not exposed by this version of the block, so report date-range and attachment-size controls do not apply here. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions Connect one QuickBooks Online company to manage master data and bounded sales and receivables workflows, while preserving existing purchase-order and bill reads. diff --git a/apps/sim/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts index d4bc71ade6f..c2cde7d76a1 100644 --- a/apps/sim/blocks/blocks/quickbooks.ts +++ b/apps/sim/blocks/blocks/quickbooks.ts @@ -1,6 +1,6 @@ import { QuickBooksIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' -import type { BlockConfig, BlockMeta } from '@/blocks/types' +import type { BlockConfig, BlockMeta, OutputCondition } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import { parseQuickBooksInvoiceAllocations, @@ -69,16 +69,16 @@ const MUTATION_OPERATIONS = [ ...VENDOR_OPERATIONS, ...SALES_MUTATION_OPERATIONS, ] as const -const PAGINATED_OPERATIONS = [ - MASTER_DATA_OPERATION, - SALES_READ_OPERATION, - 'quickbooks_list_purchase_orders', - 'quickbooks_list_bills', -] as const const TRANSACTION_LIST_OPERATIONS = [ 'quickbooks_list_purchase_orders', 'quickbooks_list_bills', ] as const +const LIST_OUTPUT_CONDITION: OutputCondition = { + field: 'operation', + value: [MASTER_DATA_OPERATION, SALES_READ_OPERATION], + and: { field: 'readMode', value: 'list' }, + or: { field: 'operation', value: [...TRANSACTION_LIST_OPERATIONS] }, +} const QUICKBOOKS_OPERATIONS = [ 'quickbooks_get_company_info', MASTER_DATA_OPERATION, @@ -1027,27 +1027,27 @@ export const QuickBooksBlock: BlockConfig = { type: 'array', description: 'Master-data, sales transaction, PurchaseOrder, or Bill objects with native QuickBooks fields', - condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, + condition: LIST_OUTPUT_CONDITION, }, startPosition: { type: 'number', description: 'One-based position of the first returned list item', - condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, + condition: LIST_OUTPUT_CONDITION, }, maxResults: { type: 'number', description: 'Actual number of items reported for the list response', - condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, + condition: LIST_OUTPUT_CONDITION, }, nextStartPosition: { type: 'number', description: 'Position to pass into an explicit next-page request', - condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, + condition: LIST_OUTPUT_CONDITION, }, hasMore: { type: 'boolean', description: 'Conservative indication that another list page may exist', - condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, + condition: LIST_OUTPUT_CONDITION, }, record: { type: 'json', diff --git a/apps/sim/lib/workflows/blocks/block-outputs.ts b/apps/sim/lib/workflows/blocks/block-outputs.ts index aa28b50d447..5d509fa7c15 100644 --- a/apps/sim/lib/workflows/blocks/block-outputs.ts +++ b/apps/sim/lib/workflows/blocks/block-outputs.ts @@ -52,7 +52,7 @@ function isConditionPrimitive(value: unknown): value is ConditionValue { * Evaluates an output condition against subBlock values. * Returns true if the condition is met and the output should be shown. */ -function evaluateOutputCondition( +export function evaluateOutputCondition( condition: OutputCondition, subBlocks: Record | undefined ): boolean { @@ -93,6 +93,10 @@ function evaluateOutputCondition( matches = matches && andMatches } + if (condition.or) { + matches = matches || evaluateOutputCondition(condition.or, subBlocks) + } + return matches } diff --git a/apps/sim/tools/quickbooks/quickbooks.test.ts b/apps/sim/tools/quickbooks/quickbooks.test.ts index 0973ab248d8..e220813cb9d 100644 --- a/apps/sim/tools/quickbooks/quickbooks.test.ts +++ b/apps/sim/tools/quickbooks/quickbooks.test.ts @@ -1,5 +1,6 @@ import { resetEnvMock, setEnv } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { evaluateOutputCondition } from '@/lib/workflows/blocks/block-outputs' import { evaluateSubBlockCondition } from '@/lib/workflows/subblocks/visibility' import { QuickBooksBlock } from '@/blocks/blocks/quickbooks' import { @@ -865,13 +866,32 @@ describe('QuickBooks tool and block boundaries', () => { ).toBe(true) expect(QuickBooksBlock.outputs.items.condition).toEqual({ field: 'operation', - value: [ - 'quickbooks_read_master_data', - 'quickbooks_read_sales_transactions', - 'quickbooks_list_purchase_orders', - 'quickbooks_list_bills', - ], + value: ['quickbooks_read_master_data', 'quickbooks_read_sales_transactions'], + and: { field: 'readMode', value: 'list' }, + or: { + field: 'operation', + value: ['quickbooks_list_purchase_orders', 'quickbooks_list_bills'], + }, }) + const listOutputCondition = QuickBooksBlock.outputs.items.condition! + expect( + evaluateOutputCondition(listOutputCondition, { + operation: { value: 'quickbooks_read_sales_transactions' }, + readMode: { value: 'by_id' }, + }) + ).toBe(false) + expect( + evaluateOutputCondition(listOutputCondition, { + operation: { value: 'quickbooks_read_sales_transactions' }, + readMode: { value: 'list' }, + }) + ).toBe(true) + expect( + evaluateOutputCondition(listOutputCondition, { + operation: { value: 'quickbooks_list_bills' }, + readMode: { value: 'by_id' }, + }) + ).toBe(true) expect(subBlocks.syncToken.condition).toEqual({ field: 'operation', value: [ diff --git a/apps/sim/tools/quickbooks/sales.test.ts b/apps/sim/tools/quickbooks/sales.test.ts index c827d3b2b0f..1235635e741 100644 --- a/apps/sim/tools/quickbooks/sales.test.ts +++ b/apps/sim/tools/quickbooks/sales.test.ts @@ -129,6 +129,23 @@ describe('QuickBooks sales reader', () => { }) }) + it('rejects malformed records without usable QuickBooks IDs', async () => { + await expect( + quickbooksReadSalesTransactionsTool.transformResponse!( + Response.json({ QueryResponse: { Invoice: [null] } }), + listParams + ) + ).rejects.toThrow('malformed Invoice record') + + await expect( + quickbooksReadSalesTransactionsTool.transformResponse!(Response.json({ Invoice: {} }), { + ...listParams, + readMode: 'by_id', + transactionId: '12', + }) + ).rejects.toThrow('without an Id') + }) + it('rejects unknown types, modes, and missing by-ID values before a request', () => { const requestUrl = quickbooksReadSalesTransactionsTool.request.url as ( params: QuickBooksReadSalesTransactionsParams @@ -328,6 +345,17 @@ describe('QuickBooks customer payments and voids', () => { invoiceAllocations: [{ invoiceId: '12', amount: 101 }], }) ).toThrow('cannot exceed') + + expect( + buildQuickBooksCreatePaymentBody({ + ...params, + totalAmount: 0.06, + invoiceAllocations: [ + { invoiceId: '12', amount: 0.01 }, + { invoiceId: '13', amount: 0.05 }, + ], + }) + ).toMatchObject({ TotalAmt: 0.06 }) }) it('builds sparse payment updates and rejects allocations without a replacement total', () => { @@ -353,6 +381,19 @@ describe('QuickBooks customer payments and voids', () => { invoiceAllocations: [{ invoiceId: '12', amount: 25 }], }) ).toThrow('totalAmount is required') + + expect( + buildQuickBooksUpdatePaymentBody({ + ...authParams, + paymentId: '15', + syncToken: '2', + totalAmount: 0.06, + invoiceAllocations: [ + { invoiceId: '12', amount: 0.01 }, + { invoiceId: '13', amount: 0.05 }, + ], + }) + ).toMatchObject({ TotalAmt: 0.06 }) }) it('uses the verified payment endpoints', () => { diff --git a/apps/sim/tools/quickbooks/sales_utils.ts b/apps/sim/tools/quickbooks/sales_utils.ts index 641f80429e8..39eba86f258 100644 --- a/apps/sim/tools/quickbooks/sales_utils.ts +++ b/apps/sim/tools/quickbooks/sales_utils.ts @@ -1,4 +1,5 @@ import { filterUndefined } from '@sim/utils/object' +import Decimal from 'decimal.js' import type { QuickBooksCreateCustomerPaymentParams, QuickBooksCreateSalesDocumentParams, @@ -190,8 +191,11 @@ function buildPaymentLines( if (totalAmount === undefined) { throw new Error('totalAmount is required when invoice allocations are supplied') } - const allocationTotal = allocations.reduce((sum, allocation) => sum + allocation.amount, 0) - if (allocationTotal > totalAmount) { + const allocationTotal = allocations.reduce( + (sum, allocation) => sum.plus(allocation.amount), + new Decimal(0) + ) + if (allocationTotal.greaterThan(totalAmount)) { throw new Error('Invoice allocation amounts cannot exceed totalAmount') } return allocations.map((allocation) => ({ diff --git a/apps/sim/tools/quickbooks/utils.ts b/apps/sim/tools/quickbooks/utils.ts index 173c5c2e20b..de0a08a7316 100644 --- a/apps/sim/tools/quickbooks/utils.ts +++ b/apps/sim/tools/quickbooks/utils.ts @@ -43,6 +43,17 @@ interface QuickBooksQueryResponse { time?: string } +function assertQuickBooksEntity(candidate: unknown, entity: QuickBooksQueryEntity): T { + if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) { + throw new Error(`QuickBooks ${entity} response contains a malformed ${entity} record`) + } + const recordId = (candidate as { Id?: unknown }).Id + if (typeof recordId !== 'string' || !recordId.trim()) { + throw new Error(`QuickBooks ${entity} response contains a record without an Id`) + } + return candidate as T +} + export const QUICKBOOKS_MASTER_DATA_ENTITIES = { account: { entity: 'Account', resource: 'account' }, customer: { entity: 'Customer', resource: 'customer' }, @@ -172,7 +183,7 @@ export async function transformQuickBooksListResponse( throw new Error(`QuickBooks ${entity} response contains a malformed entity list`) } - const items = (candidate ?? []) as T[] + const items = (candidate ?? []).map((item) => assertQuickBooksEntity(item, entity)) const startPosition = Number.isInteger(queryResponse.startPosition) ? (queryResponse.startPosition as number) : params.startPosition @@ -201,11 +212,11 @@ export async function transformQuickBooksEntityResponse< `QuickBooks ${entity} response` ) const candidate = data[entity] - if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) { + if (!candidate) { throw new Error(`QuickBooks ${entity} response is missing ${entity}`) } return { - item: candidate as T, + item: assertQuickBooksEntity(candidate, entity), time: typeof data.time === 'string' ? data.time : null, } } diff --git a/packages/workflow-types/src/blocks.ts b/packages/workflow-types/src/blocks.ts index 860b4c892ad..c426e40e07f 100644 --- a/packages/workflow-types/src/blocks.ts +++ b/packages/workflow-types/src/blocks.ts @@ -71,6 +71,7 @@ export interface OutputCondition { | null not?: boolean } + or?: OutputCondition } export type OutputFieldDefinition = From 187e7d2414f62e68f167cd2710593aba4b7fd463 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 31 Jul 2026 13:32:31 -0700 Subject: [PATCH 5/8] fix(quickbooks): address final integration review --- .../docs/en/integrations/quickbooks.mdx | 11 +- apps/sim/blocks/blocks/quickbooks.ts | 14 ++- apps/sim/tools/quickbooks/client.ts | 15 ++- apps/sim/tools/quickbooks/create_customer.ts | 13 ++- apps/sim/tools/quickbooks/create_item.ts | 13 ++- apps/sim/tools/quickbooks/create_vendor.ts | 13 ++- apps/sim/tools/quickbooks/get_company_info.ts | 7 +- apps/sim/tools/quickbooks/quickbooks.test.ts | 107 ++++++++++++++++++ apps/sim/tools/quickbooks/sales_utils.ts | 8 -- apps/sim/tools/quickbooks/types.ts | 3 + apps/sim/tools/quickbooks/utils.ts | 8 ++ 11 files changed, 189 insertions(+), 23 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/quickbooks.mdx b/apps/docs/content/docs/en/integrations/quickbooks.mdx index 1a823ba7f53..33e46334482 100644 --- a/apps/docs/content/docs/en/integrations/quickbooks.mdx +++ b/apps/docs/content/docs/en/integrations/quickbooks.mdx @@ -188,8 +188,8 @@ List or read one account, customer, vendor, item, or employee | ↳ `InvStartDate` | string | Inventory tracking start date | | ↳ `PrimaryAddr` | json | Employee primary address | | ↳ `BillableTime` | boolean | Whether employee time is billable | -| `startPosition` | number | One-based position of the first record in this page | -| `maxResults` | number | Actual number of records returned in this page | +| `startPosition` | number | One-based position of the first item in this response | +| `maxResults` | number | Actual number of items reported for this response | | `nextStartPosition` | number | Position to use when explicitly requesting the next page | | `hasMore` | boolean | Conservative indication that another page may exist | | `time` | string | QuickBooks response timestamp | @@ -211,6 +211,7 @@ Create a customer in the connected QuickBooks Online company | `billingAddress` | json | No | Customer billing address | | `shippingAddress` | json | No | Customer shipping address | | `taxable` | boolean | No | Whether sales to this customer are taxable | +| `requestId` | string | No | Optional Intuit idempotency request ID, up to 50 characters | #### Output @@ -307,6 +308,7 @@ Create a vendor in the connected QuickBooks Online company | `printOnCheckName` | string | No | Name to print on checks | | `accountNumber` | string | No | Vendor account number | | `vendor1099` | boolean | No | Whether the vendor is tracked for 1099 reporting | +| `requestId` | string | No | Optional Intuit idempotency request ID, up to 50 characters | #### Output @@ -405,6 +407,7 @@ Create a Service or Non-inventory item in QuickBooks Online | `purchaseCost` | number | No | Purchase cost per unit | | `expenseAccountId` | string | No | Expense account ID used when the item is purchased | | `taxable` | boolean | No | Whether the item is taxable | +| `requestId` | string | No | Optional Intuit idempotency request ID, up to 50 characters | #### Output @@ -446,7 +449,7 @@ Create a Service or Non-inventory item in QuickBooks Online ### `quickbooks_update_item` -Sparse-update supported fields on an item without changing its type +Sparse-update a Service or Non-inventory item in QuickBooks Online #### Input @@ -454,6 +457,7 @@ Sparse-update supported fields on an item without changing its type | --------- | ---- | -------- | ----------- | | `itemId` | string | Yes | ID of the item to update | | `syncToken` | string | Yes | Current item sync token | +| `itemType` | string | Yes | Current item type: service or non_inventory | | `name` | string | No | Replacement item name | | `incomeAccountId` | string | No | Replacement income account ID | | `description` | string | No | Replacement sales description | @@ -590,7 +594,6 @@ List or read one estimate, invoice, sales receipt, payment, credit memo, or refu | ↳ `MetaData` | json | Transaction creation and update timestamps | | ↳ `CreateTime` | string | Entity creation timestamp | | ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | - ### `quickbooks_create_estimate` Create an estimate with bounded item and description lines diff --git a/apps/sim/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts index c2cde7d76a1..aae1143478e 100644 --- a/apps/sim/blocks/blocks/quickbooks.ts +++ b/apps/sim/blocks/blocks/quickbooks.ts @@ -14,6 +14,11 @@ const SALES_READ_OPERATION = 'quickbooks_read_sales_transactions' const CUSTOMER_OPERATIONS = ['quickbooks_create_customer', 'quickbooks_update_customer'] as const const VENDOR_OPERATIONS = ['quickbooks_create_vendor', 'quickbooks_update_vendor'] as const const ITEM_OPERATIONS = ['quickbooks_create_item', 'quickbooks_update_item'] as const +const MASTER_DATA_CREATE_OPERATIONS = [ + 'quickbooks_create_customer', + 'quickbooks_create_item', + 'quickbooks_create_vendor', +] as const const SALES_DOCUMENT_CREATE_OPERATIONS = [ 'quickbooks_create_estimate', 'quickbooks_create_invoice', @@ -40,6 +45,7 @@ const SALES_CREATE_OPERATIONS = [ ...SALES_DOCUMENT_CREATE_OPERATIONS, 'quickbooks_create_customer_payment', ] as const +const CREATE_OPERATIONS = [...MASTER_DATA_CREATE_OPERATIONS, ...SALES_CREATE_OPERATIONS] as const const SALES_UPDATE_OPERATIONS = [ ...SALES_DOCUMENT_UPDATE_OPERATIONS, 'quickbooks_update_customer_payment', @@ -710,7 +716,7 @@ export const QuickBooksBlock: BlockConfig = { title: 'Request ID', type: 'short-input', placeholder: 'Optional idempotency key (max 50 characters)', - condition: { field: 'operation', value: [...SALES_CREATE_OPERATIONS] }, + condition: { field: 'operation', value: [...CREATE_OPERATIONS] }, mode: 'advanced', }, { @@ -876,6 +882,7 @@ export const QuickBooksBlock: BlockConfig = { operation === 'quickbooks_create_customer' || operation === 'quickbooks_update_customer' ) { + const isCreate = operation === 'quickbooks_create_customer' return { credential: oauthCredentialValue, customerId: optionalValue(params.customerId), @@ -890,9 +897,11 @@ export const QuickBooksBlock: BlockConfig = { shippingAddress: parseQuickBooksAddress(params.shippingAddress, 'shippingAddress'), taxable: parseTriStateBoolean(params.taxable, 'taxable'), activeStatus: params.activeStatus ?? 'unchanged', + requestId: isCreate ? optionalValue(params.requestId) : undefined, } } if (operation === 'quickbooks_create_vendor' || operation === 'quickbooks_update_vendor') { + const isCreate = operation === 'quickbooks_create_vendor' return { credential: oauthCredentialValue, vendorId: optionalValue(params.vendorId), @@ -908,9 +917,11 @@ export const QuickBooksBlock: BlockConfig = { accountNumber: optionalValue(params.accountNumber), vendor1099: parseTriStateBoolean(params.vendor1099, 'vendor1099'), activeStatus: params.activeStatus ?? 'unchanged', + requestId: isCreate ? optionalValue(params.requestId) : undefined, } } if (operation === 'quickbooks_create_item' || operation === 'quickbooks_update_item') { + const isCreate = operation === 'quickbooks_create_item' return { credential: oauthCredentialValue, itemId: optionalValue(params.itemId), @@ -926,6 +937,7 @@ export const QuickBooksBlock: BlockConfig = { expenseAccountId: optionalValue(params.expenseAccountId), taxable: parseTriStateBoolean(params.taxable, 'taxable'), activeStatus: params.activeStatus ?? 'unchanged', + requestId: isCreate ? optionalValue(params.requestId) : undefined, } } return { credential: oauthCredentialValue } diff --git a/apps/sim/tools/quickbooks/client.ts b/apps/sim/tools/quickbooks/client.ts index 20ccfd384d5..634b2022e29 100644 --- a/apps/sim/tools/quickbooks/client.ts +++ b/apps/sim/tools/quickbooks/client.ts @@ -77,6 +77,17 @@ export interface QuickBooksCompanyInfoEnvelope { time?: string } +export function assertQuickBooksCompanyInfo(candidate: unknown): T { + if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) { + throw new Error('QuickBooks CompanyInfo response is missing a valid CompanyInfo object') + } + const id = (candidate as { Id?: unknown }).Id + if (typeof id !== 'string' || !id.trim()) { + throw new Error('QuickBooks CompanyInfo response is missing a valid company Id') + } + return candidate as T +} + function getQuickBooksTrackingId(headers: Headers): string | null { return ( headers.get('intuit_tid') ?? headers.get('intuit-tid') ?? headers.get('x-request-id') ?? null @@ -167,9 +178,7 @@ export async function fetchValidatedQuickBooksCompanyInfo( ) } - if (!data.CompanyInfo || typeof data.CompanyInfo !== 'object') { - throw new Error('QuickBooks company validation response is missing CompanyInfo') - } + assertQuickBooksCompanyInfo(data.CompanyInfo) return data } diff --git a/apps/sim/tools/quickbooks/create_customer.ts b/apps/sim/tools/quickbooks/create_customer.ts index 989169f89e0..22c1486031c 100644 --- a/apps/sim/tools/quickbooks/create_customer.ts +++ b/apps/sim/tools/quickbooks/create_customer.ts @@ -11,6 +11,7 @@ import { QUICKBOOKS_MUTATION_OUTPUTS, } from '@/tools/quickbooks/types' import { + addQuickBooksRequestId, buildQuickBooksEntityUrl, getQuickBooksToolHeaders, optionalQuickBooksString, @@ -96,6 +97,12 @@ export const quickbooksCreateCustomerTool: ToolConfig< visibility: 'user-or-llm', description: 'Whether sales to this customer are taxable', }, + requestId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Intuit idempotency request ID, up to 50 characters', + }, }, oauth: { required: true, @@ -104,7 +111,11 @@ export const quickbooksCreateCustomerTool: ToolConfig< }, errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, request: { - url: (params) => buildQuickBooksEntityUrl(params.realmId, 'customer').toString(), + url: (params) => + addQuickBooksRequestId( + buildQuickBooksEntityUrl(params.realmId, 'customer'), + params.requestId + ).toString(), method: 'POST', headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), body: (params) => diff --git a/apps/sim/tools/quickbooks/create_item.ts b/apps/sim/tools/quickbooks/create_item.ts index 87066f9647b..b36a126021e 100644 --- a/apps/sim/tools/quickbooks/create_item.ts +++ b/apps/sim/tools/quickbooks/create_item.ts @@ -8,6 +8,7 @@ import type { } from '@/tools/quickbooks/types' import { QUICKBOOKS_ITEM_PROPERTIES, QUICKBOOKS_MUTATION_OUTPUTS } from '@/tools/quickbooks/types' import { + addQuickBooksRequestId, buildQuickBooksEntityUrl, getQuickBooksToolHeaders, optionalQuickBooksString, @@ -94,6 +95,12 @@ export const quickbooksCreateItemTool: ToolConfig< visibility: 'user-or-llm', description: 'Whether the item is taxable', }, + requestId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Intuit idempotency request ID, up to 50 characters', + }, }, oauth: { required: true, @@ -102,7 +109,11 @@ export const quickbooksCreateItemTool: ToolConfig< }, errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, request: { - url: (params) => buildQuickBooksEntityUrl(params.realmId, 'item').toString(), + url: (params) => + addQuickBooksRequestId( + buildQuickBooksEntityUrl(params.realmId, 'item'), + params.requestId + ).toString(), method: 'POST', headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), body: (params) => { diff --git a/apps/sim/tools/quickbooks/create_vendor.ts b/apps/sim/tools/quickbooks/create_vendor.ts index 00ff1155fa4..73f2780d447 100644 --- a/apps/sim/tools/quickbooks/create_vendor.ts +++ b/apps/sim/tools/quickbooks/create_vendor.ts @@ -8,6 +8,7 @@ import type { } from '@/tools/quickbooks/types' import { QUICKBOOKS_MUTATION_OUTPUTS, QUICKBOOKS_VENDOR_PROPERTIES } from '@/tools/quickbooks/types' import { + addQuickBooksRequestId, buildQuickBooksEntityUrl, getQuickBooksToolHeaders, optionalQuickBooksString, @@ -100,6 +101,12 @@ export const quickbooksCreateVendorTool: ToolConfig< visibility: 'user-or-llm', description: 'Whether the vendor is tracked for 1099 reporting', }, + requestId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Intuit idempotency request ID, up to 50 characters', + }, }, oauth: { required: true, @@ -108,7 +115,11 @@ export const quickbooksCreateVendorTool: ToolConfig< }, errorExtractor: ErrorExtractorId.QUICKBOOKS_FAULT, request: { - url: (params) => buildQuickBooksEntityUrl(params.realmId, 'vendor').toString(), + url: (params) => + addQuickBooksRequestId( + buildQuickBooksEntityUrl(params.realmId, 'vendor'), + params.requestId + ).toString(), method: 'POST', headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), body: (params) => diff --git a/apps/sim/tools/quickbooks/get_company_info.ts b/apps/sim/tools/quickbooks/get_company_info.ts index 2a5f514d712..4c3f77bd340 100644 --- a/apps/sim/tools/quickbooks/get_company_info.ts +++ b/apps/sim/tools/quickbooks/get_company_info.ts @@ -1,5 +1,6 @@ import { ErrorExtractorId } from '@/tools/error-extractors' import { + assertQuickBooksCompanyInfo, buildQuickBooksCompanyUrl, normalizeQuickBooksRealmId, QUICKBOOKS_MAX_RESPONSE_BYTES, @@ -65,13 +66,11 @@ export const quickbooksGetCompanyInfoTool: ToolConfig< response, 'QuickBooks CompanyInfo response' ) - if (!data.CompanyInfo || typeof data.CompanyInfo !== 'object') { - throw new Error('QuickBooks CompanyInfo response is missing CompanyInfo') - } + const company = assertQuickBooksCompanyInfo(data.CompanyInfo) return { success: true, output: { - company: data.CompanyInfo, + company, time: typeof data.time === 'string' ? data.time : null, }, } diff --git a/apps/sim/tools/quickbooks/quickbooks.test.ts b/apps/sim/tools/quickbooks/quickbooks.test.ts index e220813cb9d..0415a4c2080 100644 --- a/apps/sim/tools/quickbooks/quickbooks.test.ts +++ b/apps/sim/tools/quickbooks/quickbooks.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs' import { resetEnvMock, setEnv } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { evaluateOutputCondition } from '@/lib/workflows/blocks/block-outputs' @@ -152,6 +153,37 @@ describe('QuickBooks response contracts', () => { } }) + it.each([ + [{}, 'company Id'], + [[], 'valid CompanyInfo object'], + [{ Id: '' }, 'company Id'], + [{ Id: ' ' }, 'company Id'], + ])('rejects malformed CompanyInfo during OAuth validation', async (companyInfo, message) => { + const originalFetch = global.fetch + global.fetch = async () => Response.json({ CompanyInfo: companyInfo }) + + try { + await expect( + fetchValidatedQuickBooksCompanyInfo('access-token', '123456789') + ).rejects.toThrow(message) + } finally { + global.fetch = originalFetch + } + }) + + it.each([ + [{}, 'company Id'], + [[], 'valid CompanyInfo object'], + [{ Id: '' }, 'company Id'], + ])('rejects malformed CompanyInfo tool responses', async (companyInfo, message) => { + await expect( + quickbooksGetCompanyInfoTool.transformResponse!( + Response.json({ CompanyInfo: companyInfo }), + authParams + ) + ).rejects.toThrow(message) + }) + it('preserves sanitized fault guidance for failed company validation', async () => { const originalFetch = global.fetch global.fetch = async () => @@ -476,6 +508,44 @@ describe('QuickBooks customer and vendor mutations', () => { }) }) + it('adds bounded idempotency request IDs to all master-data creates', () => { + const customerUrl = quickbooksCreateCustomerTool.request.url as ( + params: QuickBooksCreateCustomerParams + ) => string + const vendorUrl = quickbooksCreateVendorTool.request.url as ( + params: QuickBooksCreateVendorParams + ) => string + const itemUrl = quickbooksCreateItemTool.request.url as ( + params: QuickBooksCreateItemParams + ) => string + + expect( + new URL( + customerUrl({ ...authParams, displayName: 'Customer', requestId: 'customer-request' }) + ).searchParams.get('requestid') + ).toBe('customer-request') + expect( + new URL( + vendorUrl({ ...authParams, displayName: 'Vendor', requestId: 'vendor-request' }) + ).searchParams.get('requestid') + ).toBe('vendor-request') + expect( + new URL( + itemUrl({ + ...authParams, + name: 'Item', + itemType: 'service', + incomeAccountId: '79', + requestId: 'item-request', + }) + ).searchParams.get('requestid') + ).toBe('item-request') + + expect(() => + customerUrl({ ...authParams, displayName: 'Customer', requestId: 'x'.repeat(51) }) + ).toThrow('cannot exceed 50 characters') + }) + it('builds sparse customer updates and rejects an empty update', () => { const update: QuickBooksUpdateCustomerParams = { ...authParams, @@ -790,12 +860,14 @@ describe('QuickBooks tool and block boundaries', () => { displayName: 'Sanitized Customer', billingAddress: '{"line1":"123 Main St"}', taxable: 'no', + requestId: 'customer-request', }) ).toMatchObject({ credential: 'credential-id', displayName: 'Sanitized Customer', billingAddress: { Line1: '123 Main St' }, taxable: false, + requestId: 'customer-request', }) expect( @@ -912,5 +984,40 @@ describe('QuickBooks tool and block boundaries', () => { field: 'operation', value: 'quickbooks_create_item', }) + expect( + evaluateSubBlockCondition(subBlocks.requestId.condition, { + operation: 'quickbooks_create_customer', + }) + ).toBe(true) + expect( + evaluateSubBlockCondition(subBlocks.requestId.condition, { + operation: 'quickbooks_update_customer', + }) + ).toBe(false) + }) + + it('documents master-data identity, sync, pagination, and time outputs', () => { + const docs = readFileSync( + new URL('../../../docs/content/docs/en/integrations/quickbooks.mdx', import.meta.url), + 'utf8' + ) + const section = docs + .split('### `quickbooks_read_master_data`')[1] + ?.split('### `quickbooks_create_customer`')[0] + + expect(section).toBeDefined() + for (const output of [ + 'Id', + 'SyncToken', + 'Active', + 'MetaData', + 'startPosition', + 'maxResults', + 'nextStartPosition', + 'hasMore', + 'time', + ]) { + expect(section).toContain(`\`${output}\``) + } }) }) diff --git a/apps/sim/tools/quickbooks/sales_utils.ts b/apps/sim/tools/quickbooks/sales_utils.ts index 39eba86f258..029568078b0 100644 --- a/apps/sim/tools/quickbooks/sales_utils.ts +++ b/apps/sim/tools/quickbooks/sales_utils.ts @@ -204,14 +204,6 @@ function buildPaymentLines( })) } -export function addQuickBooksRequestId(url: URL, requestId?: string): URL { - const normalized = optionalQuickBooksString(requestId) - if (!normalized) return url - if (normalized.length > 50) throw new Error('requestId cannot exceed 50 characters') - url.searchParams.set('requestid', normalized) - return url -} - export function buildQuickBooksCreateSalesDocumentBody( params: QuickBooksCreateSalesDocumentParams, options: { requireDepositAccount?: boolean } = {} diff --git a/apps/sim/tools/quickbooks/types.ts b/apps/sim/tools/quickbooks/types.ts index 7ef7857c9b0..f0f09bc9c60 100644 --- a/apps/sim/tools/quickbooks/types.ts +++ b/apps/sim/tools/quickbooks/types.ts @@ -362,6 +362,7 @@ export type QuickBooksActiveStatus = 'unchanged' | 'active' | 'inactive' export interface QuickBooksCreateCustomerParams extends QuickBooksAuthParams { displayName: string + requestId?: string companyName?: string givenName?: string familyName?: string @@ -382,6 +383,7 @@ export interface QuickBooksUpdateCustomerParams export interface QuickBooksCreateVendorParams extends QuickBooksAuthParams { displayName: string + requestId?: string companyName?: string givenName?: string familyName?: string @@ -407,6 +409,7 @@ export interface QuickBooksCreateItemParams extends QuickBooksAuthParams { name: string itemType: QuickBooksWritableItemType incomeAccountId: string + requestId?: string description?: string unitPrice?: number purchaseDescription?: string diff --git a/apps/sim/tools/quickbooks/utils.ts b/apps/sim/tools/quickbooks/utils.ts index de0a08a7316..bc3e8117d30 100644 --- a/apps/sim/tools/quickbooks/utils.ts +++ b/apps/sim/tools/quickbooks/utils.ts @@ -140,6 +140,14 @@ export function buildQuickBooksEntityUrl( ) } +export function addQuickBooksRequestId(url: URL, requestId?: string): URL { + const normalized = optionalQuickBooksString(requestId) + if (!normalized) return url + if (normalized.length > 50) throw new Error('requestId cannot exceed 50 characters') + url.searchParams.set('requestid', normalized) + return url +} + export async function parseQuickBooksJson(response: Response, label: string): Promise { if (!response.ok) { throw new Error(`QuickBooks request failed with HTTP ${response.status}`) From ddb0053d70b2ed80f11a325aa29a5aa3081d62a5 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 31 Jul 2026 14:07:33 -0700 Subject: [PATCH 6/8] fix(quickbooks): clarify master data output metadata --- .../docs/en/integrations/quickbooks.mdx | 44 +++++++-------- apps/sim/tools/quickbooks/quickbooks.test.ts | 20 +++++++ apps/sim/tools/quickbooks/types.ts | 53 +++++++++++++++++++ 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/quickbooks.mdx b/apps/docs/content/docs/en/integrations/quickbooks.mdx index 33e46334482..b93e8eadd12 100644 --- a/apps/docs/content/docs/en/integrations/quickbooks.mdx +++ b/apps/docs/content/docs/en/integrations/quickbooks.mdx @@ -91,29 +91,29 @@ List or read one account, customer, vendor, item, or employee | ↳ `MetaData` | json | Entity creation and update timestamps | | ↳ `CreateTime` | string | Entity creation timestamp | | ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | -| ↳ `Name` | string | Item name | +| ↳ `Name` | string | Account or item name | | ↳ `SubAccount` | boolean | Whether this is a subaccount | -| ↳ `ParentRef` | json | Parent item or category reference | +| ↳ `ParentRef` | json | Parent account, item, or category reference | | ↳ `value` | string | QuickBooks entity ID | | ↳ `name` | string | QuickBooks entity display name | -| ↳ `FullyQualifiedName` | string | Hierarchical qualified item name | +| ↳ `FullyQualifiedName` | string | Hierarchical qualified account or item name | | ↳ `Classification` | string | Account classification | | ↳ `AccountType` | string | Account type | | ↳ `AccountSubType` | string | Account subtype | | ↳ `CurrentBalance` | number | Account current balance | -| ↳ `CurrencyRef` | json | Vendor currency reference | +| ↳ `CurrencyRef` | json | Account, customer, or vendor currency reference | | ↳ `value` | string | QuickBooks entity ID | | ↳ `name` | string | QuickBooks entity display name | -| ↳ `DisplayName` | string | Employee display name | -| ↳ `CompanyName` | string | Vendor company name | +| ↳ `DisplayName` | string | Customer, vendor, or employee display name | +| ↳ `CompanyName` | string | Customer or vendor company name | | ↳ `GivenName` | string | Given name | | ↳ `FamilyName` | string | Family name | -| ↳ `Taxable` | boolean | Whether the item is taxable | -| ↳ `PrimaryEmailAddr` | json | Employee primary email address | -| ↳ `PrimaryPhone` | json | Employee primary phone number | -| ↳ `BillAddr` | json | Vendor billing address | +| ↳ `Taxable` | boolean | Taxable status for the customer or item | +| ↳ `PrimaryEmailAddr` | json | Customer, vendor, or employee primary email address | +| ↳ `PrimaryPhone` | json | Customer, vendor, or employee primary phone number | +| ↳ `BillAddr` | json | Customer or vendor billing address | | ↳ `ShipAddr` | json | Customer shipping address | -| ↳ `Balance` | number | Vendor balance | +| ↳ `Balance` | number | Customer or vendor balance | | ↳ `PrintOnCheckName` | string | Name printed on checks | | ↳ `Vendor1099` | boolean | Whether the vendor is tracked for 1099 reporting | | ↳ `AcctNum` | string | Vendor account number | @@ -143,29 +143,29 @@ List or read one account, customer, vendor, item, or employee | ↳ `MetaData` | json | Entity creation and update timestamps | | ↳ `CreateTime` | string | Entity creation timestamp | | ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | -| ↳ `Name` | string | Item name | +| ↳ `Name` | string | Account or item name | | ↳ `SubAccount` | boolean | Whether this is a subaccount | -| ↳ `ParentRef` | json | Parent item or category reference | +| ↳ `ParentRef` | json | Parent account, item, or category reference | | ↳ `value` | string | QuickBooks entity ID | | ↳ `name` | string | QuickBooks entity display name | -| ↳ `FullyQualifiedName` | string | Hierarchical qualified item name | +| ↳ `FullyQualifiedName` | string | Hierarchical qualified account or item name | | ↳ `Classification` | string | Account classification | | ↳ `AccountType` | string | Account type | | ↳ `AccountSubType` | string | Account subtype | | ↳ `CurrentBalance` | number | Account current balance | -| ↳ `CurrencyRef` | json | Vendor currency reference | +| ↳ `CurrencyRef` | json | Account, customer, or vendor currency reference | | ↳ `value` | string | QuickBooks entity ID | | ↳ `name` | string | QuickBooks entity display name | -| ↳ `DisplayName` | string | Employee display name | -| ↳ `CompanyName` | string | Vendor company name | +| ↳ `DisplayName` | string | Customer, vendor, or employee display name | +| ↳ `CompanyName` | string | Customer or vendor company name | | ↳ `GivenName` | string | Given name | | ↳ `FamilyName` | string | Family name | -| ↳ `Taxable` | boolean | Whether the item is taxable | -| ↳ `PrimaryEmailAddr` | json | Employee primary email address | -| ↳ `PrimaryPhone` | json | Employee primary phone number | -| ↳ `BillAddr` | json | Vendor billing address | +| ↳ `Taxable` | boolean | Taxable status for the customer or item | +| ↳ `PrimaryEmailAddr` | json | Customer, vendor, or employee primary email address | +| ↳ `PrimaryPhone` | json | Customer, vendor, or employee primary phone number | +| ↳ `BillAddr` | json | Customer or vendor billing address | | ↳ `ShipAddr` | json | Customer shipping address | -| ↳ `Balance` | number | Vendor balance | +| ↳ `Balance` | number | Customer or vendor balance | | ↳ `PrintOnCheckName` | string | Name printed on checks | | ↳ `Vendor1099` | boolean | Whether the vendor is tracked for 1099 reporting | | ↳ `AcctNum` | string | Vendor account number | diff --git a/apps/sim/tools/quickbooks/quickbooks.test.ts b/apps/sim/tools/quickbooks/quickbooks.test.ts index 0415a4c2080..8f3200bbd8c 100644 --- a/apps/sim/tools/quickbooks/quickbooks.test.ts +++ b/apps/sim/tools/quickbooks/quickbooks.test.ts @@ -43,6 +43,7 @@ import type { QuickBooksUpdateItemParams, QuickBooksUpdateVendorParams, } from '@/tools/quickbooks/types' +import { QUICKBOOKS_MASTER_DATA_PROPERTIES } from '@/tools/quickbooks/types' import { quickbooksUpdateCustomerTool } from '@/tools/quickbooks/update_customer' import { quickbooksUpdateItemTool } from '@/tools/quickbooks/update_item' import { quickbooksUpdateVendorTool } from '@/tools/quickbooks/update_vendor' @@ -1019,5 +1020,24 @@ describe('QuickBooks tool and block boundaries', () => { ]) { expect(section).toContain(`\`${output}\``) } + + const neutralDescriptions = { + Name: 'Account or item name', + ParentRef: 'Parent account, item, or category reference', + FullyQualifiedName: 'Hierarchical qualified account or item name', + CurrencyRef: 'Account, customer, or vendor currency reference', + DisplayName: 'Customer, vendor, or employee display name', + CompanyName: 'Customer or vendor company name', + Taxable: 'Taxable status for the customer or item', + PrimaryEmailAddr: 'Customer, vendor, or employee primary email address', + PrimaryPhone: 'Customer, vendor, or employee primary phone number', + BillAddr: 'Customer or vendor billing address', + Balance: 'Customer or vendor balance', + } as const + + for (const [field, description] of Object.entries(neutralDescriptions)) { + expect(QUICKBOOKS_MASTER_DATA_PROPERTIES[field]?.description).toBe(description) + expect(section).toContain(description) + } }) }) diff --git a/apps/sim/tools/quickbooks/types.ts b/apps/sim/tools/quickbooks/types.ts index f0f09bc9c60..04ed9f5f40d 100644 --- a/apps/sim/tools/quickbooks/types.ts +++ b/apps/sim/tools/quickbooks/types.ts @@ -775,6 +775,59 @@ export const QUICKBOOKS_MASTER_DATA_PROPERTIES: Record = ...QUICKBOOKS_VENDOR_PROPERTIES, ...QUICKBOOKS_ITEM_PROPERTIES, ...QUICKBOOKS_EMPLOYEE_PROPERTIES, + Name: { type: 'string', description: 'Account or item name', optional: true }, + ParentRef: { + type: 'json', + description: 'Parent account, item, or category reference', + optional: true, + properties: QUICKBOOKS_REFERENCE_PROPERTIES, + }, + FullyQualifiedName: { + type: 'string', + description: 'Hierarchical qualified account or item name', + optional: true, + }, + CurrencyRef: { + type: 'json', + description: 'Account, customer, or vendor currency reference', + optional: true, + properties: QUICKBOOKS_REFERENCE_PROPERTIES, + }, + DisplayName: { + type: 'string', + description: 'Customer, vendor, or employee display name', + optional: true, + }, + CompanyName: { + type: 'string', + description: 'Customer or vendor company name', + optional: true, + }, + Taxable: { + type: 'boolean', + description: 'Taxable status for the customer or item', + optional: true, + }, + PrimaryEmailAddr: { + type: 'json', + description: 'Customer, vendor, or employee primary email address', + optional: true, + }, + PrimaryPhone: { + type: 'json', + description: 'Customer, vendor, or employee primary phone number', + optional: true, + }, + BillAddr: { + type: 'json', + description: 'Customer or vendor billing address', + optional: true, + }, + Balance: { + type: 'number', + description: 'Customer or vendor balance', + optional: true, + }, } export const QUICKBOOKS_SALES_TRANSACTION_PROPERTIES: Record = { From 6e8f041c2a8f46423673cbe41f519098ec593de9 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 31 Jul 2026 14:31:51 -0700 Subject: [PATCH 7/8] fix(quickbooks): mark sales pagination outputs optional --- .../docs/en/integrations/quickbooks.mdx | 11 ++++--- .../quickbooks/read_sales_transactions.ts | 32 ++++++++++++++++--- apps/sim/tools/quickbooks/sales.test.ts | 8 +++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/quickbooks.mdx b/apps/docs/content/docs/en/integrations/quickbooks.mdx index b93e8eadd12..75827fa5900 100644 --- a/apps/docs/content/docs/en/integrations/quickbooks.mdx +++ b/apps/docs/content/docs/en/integrations/quickbooks.mdx @@ -524,11 +524,6 @@ List or read one estimate, invoice, sales receipt, payment, credit memo, or refu | Parameter | Type | Description | | --------- | ---- | ----------- | -| `startPosition` | number | One-based position of the first item in this response | -| `maxResults` | number | Actual number of items reported for this response | -| `nextStartPosition` | number | Position to use when explicitly requesting the next page | -| `hasMore` | boolean | Conservative indication that another page may exist | -| `time` | string | QuickBooks response timestamp | | `transactionType` | string | Sales transaction type returned | | `item` | json | Single native QuickBooks sales transaction | | ↳ `Id` | string | QuickBooks sales transaction ID | @@ -594,6 +589,12 @@ List or read one estimate, invoice, sales receipt, payment, credit memo, or refu | ↳ `MetaData` | json | Transaction creation and update timestamps | | ↳ `CreateTime` | string | Entity creation timestamp | | ↳ `LastUpdatedTime` | string | Entity last-updated timestamp | +| `startPosition` | number | One-based position of the first item in this response | +| `maxResults` | number | Actual number of items reported for this response | +| `nextStartPosition` | number | Position to use when explicitly requesting the next page | +| `hasMore` | boolean | Conservative indication that another page may exist | +| `time` | string | QuickBooks response timestamp | + ### `quickbooks_create_estimate` Create an estimate with bounded item and description lines diff --git a/apps/sim/tools/quickbooks/read_sales_transactions.ts b/apps/sim/tools/quickbooks/read_sales_transactions.ts index c056136a3b6..3b3de0b1aef 100644 --- a/apps/sim/tools/quickbooks/read_sales_transactions.ts +++ b/apps/sim/tools/quickbooks/read_sales_transactions.ts @@ -5,10 +5,7 @@ import type { QuickBooksReadSalesTransactionsResponse, QuickBooksSalesTransaction, } from '@/tools/quickbooks/types' -import { - QUICKBOOKS_LIST_OUTPUTS, - QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, -} from '@/tools/quickbooks/types' +import { QUICKBOOKS_SALES_TRANSACTION_PROPERTIES } from '@/tools/quickbooks/types' import { buildQuickBooksEntityUrl, buildQuickBooksQueryUrl, @@ -152,6 +149,31 @@ export const quickbooksReadSalesTransactionsTool: ToolConfig< optional: true, items: { type: 'json', properties: QUICKBOOKS_SALES_TRANSACTION_PROPERTIES }, }, - ...QUICKBOOKS_LIST_OUTPUTS, + startPosition: { + type: 'number', + description: 'One-based position of the first item in this response', + optional: true, + }, + maxResults: { + type: 'number', + description: 'Actual number of items reported for this response', + optional: true, + }, + nextStartPosition: { + type: 'number', + description: 'Position to use when explicitly requesting the next page', + optional: true, + }, + hasMore: { + type: 'boolean', + description: 'Conservative indication that another page may exist', + optional: true, + }, + time: { + type: 'string', + description: 'QuickBooks response timestamp', + optional: true, + nullable: true, + }, }, } diff --git a/apps/sim/tools/quickbooks/sales.test.ts b/apps/sim/tools/quickbooks/sales.test.ts index 1235635e741..fabfdb8266b 100644 --- a/apps/sim/tools/quickbooks/sales.test.ts +++ b/apps/sim/tools/quickbooks/sales.test.ts @@ -129,6 +129,14 @@ describe('QuickBooks sales reader', () => { }) }) + it('marks mode-specific sales read outputs as optional', () => { + expect(quickbooksReadSalesTransactionsTool.outputs.item?.optional).toBe(true) + expect(quickbooksReadSalesTransactionsTool.outputs.items?.optional).toBe(true) + for (const output of ['startPosition', 'maxResults', 'nextStartPosition', 'hasMore']) { + expect(quickbooksReadSalesTransactionsTool.outputs[output]?.optional).toBe(true) + } + }) + it('rejects malformed records without usable QuickBooks IDs', async () => { await expect( quickbooksReadSalesTransactionsTool.transformResponse!( From a0f1c65d8f91e673e050e27fedfc519701f2720d Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 31 Jul 2026 14:43:24 -0700 Subject: [PATCH 8/8] fix(quickbooks): validate calculated sales amounts --- .../docs/en/integrations/quickbooks.mdx | 7 +++-- apps/sim/blocks/blocks/quickbooks.ts | 2 +- .../tools/quickbooks/create_credit_memo.ts | 6 ++--- .../quickbooks/create_customer_payment.ts | 6 ++--- apps/sim/tools/quickbooks/create_estimate.ts | 6 ++--- apps/sim/tools/quickbooks/create_invoice.ts | 6 ++--- .../tools/quickbooks/create_refund_receipt.ts | 6 ++--- .../tools/quickbooks/create_sales_receipt.ts | 6 ++--- apps/sim/tools/quickbooks/quickbooks.test.ts | 12 ++++----- apps/sim/tools/quickbooks/sales.test.ts | 26 +++++++++++++++++++ apps/sim/tools/quickbooks/sales_utils.ts | 22 ++++++++++++---- 11 files changed, 65 insertions(+), 40 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/quickbooks.mdx b/apps/docs/content/docs/en/integrations/quickbooks.mdx index 75827fa5900..057f740e2b3 100644 --- a/apps/docs/content/docs/en/integrations/quickbooks.mdx +++ b/apps/docs/content/docs/en/integrations/quickbooks.mdx @@ -188,8 +188,8 @@ List or read one account, customer, vendor, item, or employee | ↳ `InvStartDate` | string | Inventory tracking start date | | ↳ `PrimaryAddr` | json | Employee primary address | | ↳ `BillableTime` | boolean | Whether employee time is billable | -| `startPosition` | number | One-based position of the first item in this response | -| `maxResults` | number | Actual number of items reported for this response | +| `startPosition` | number | One-based position of the first record in this page | +| `maxResults` | number | Actual number of records returned in this page | | `nextStartPosition` | number | Position to use when explicitly requesting the next page | | `hasMore` | boolean | Conservative indication that another page may exist | | `time` | string | QuickBooks response timestamp | @@ -449,7 +449,7 @@ Create a Service or Non-inventory item in QuickBooks Online ### `quickbooks_update_item` -Sparse-update a Service or Non-inventory item in QuickBooks Online +Sparse-update supported fields on an item without changing its type #### Input @@ -457,7 +457,6 @@ Sparse-update a Service or Non-inventory item in QuickBooks Online | --------- | ---- | -------- | ----------- | | `itemId` | string | Yes | ID of the item to update | | `syncToken` | string | Yes | Current item sync token | -| `itemType` | string | Yes | Current item type: service or non_inventory | | `name` | string | No | Replacement item name | | `incomeAccountId` | string | No | Replacement income account ID | | `description` | string | No | Replacement sales description | diff --git a/apps/sim/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts index aae1143478e..6d95c412a2c 100644 --- a/apps/sim/blocks/blocks/quickbooks.ts +++ b/apps/sim/blocks/blocks/quickbooks.ts @@ -573,7 +573,7 @@ export const QuickBooksBlock: BlockConfig = { wandConfig: { enabled: true, prompt: - 'Generate a JSON array of QuickBooks sales lines. Use item lines with lineType, positive amount, itemId, and optional description, quantity, unitPrice, and serviceDate; or description lines with lineType and description. Return ONLY the JSON array - no explanations, no extra text.', + 'Generate a JSON array of QuickBooks sales lines. Use item lines with lineType, positive amount, itemId, and optional description, positive quantity, positive unitPrice, and serviceDate. When quantity and unitPrice are both present, amount must equal quantity multiplied by unitPrice. Use description lines with lineType and description. Return ONLY the JSON array - no explanations, no extra text.', generationType: 'json-object', }, }, diff --git a/apps/sim/tools/quickbooks/create_credit_memo.ts b/apps/sim/tools/quickbooks/create_credit_memo.ts index 919b30a0dea..309e674500b 100644 --- a/apps/sim/tools/quickbooks/create_credit_memo.ts +++ b/apps/sim/tools/quickbooks/create_credit_memo.ts @@ -1,9 +1,6 @@ import { ErrorExtractorId } from '@/tools/error-extractors' import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' -import { - addQuickBooksRequestId, - buildQuickBooksCreateSalesDocumentBody, -} from '@/tools/quickbooks/sales_utils' +import { buildQuickBooksCreateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' import type { QuickBooksCreateCreditMemoParams, QuickBooksMutationResponse, @@ -14,6 +11,7 @@ import { QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, } from '@/tools/quickbooks/types' import { + addQuickBooksRequestId, buildQuickBooksEntityUrl, getQuickBooksToolHeaders, transformQuickBooksMutationResponse, diff --git a/apps/sim/tools/quickbooks/create_customer_payment.ts b/apps/sim/tools/quickbooks/create_customer_payment.ts index 1172863d09a..5fd516acbe0 100644 --- a/apps/sim/tools/quickbooks/create_customer_payment.ts +++ b/apps/sim/tools/quickbooks/create_customer_payment.ts @@ -1,9 +1,6 @@ import { ErrorExtractorId } from '@/tools/error-extractors' import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' -import { - addQuickBooksRequestId, - buildQuickBooksCreatePaymentBody, -} from '@/tools/quickbooks/sales_utils' +import { buildQuickBooksCreatePaymentBody } from '@/tools/quickbooks/sales_utils' import type { QuickBooksCreateCustomerPaymentParams, QuickBooksMutationResponse, @@ -14,6 +11,7 @@ import { QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, } from '@/tools/quickbooks/types' import { + addQuickBooksRequestId, buildQuickBooksEntityUrl, getQuickBooksToolHeaders, transformQuickBooksMutationResponse, diff --git a/apps/sim/tools/quickbooks/create_estimate.ts b/apps/sim/tools/quickbooks/create_estimate.ts index a73a23dd485..b64e5d1b7fa 100644 --- a/apps/sim/tools/quickbooks/create_estimate.ts +++ b/apps/sim/tools/quickbooks/create_estimate.ts @@ -1,9 +1,6 @@ import { ErrorExtractorId } from '@/tools/error-extractors' import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' -import { - addQuickBooksRequestId, - buildQuickBooksCreateSalesDocumentBody, -} from '@/tools/quickbooks/sales_utils' +import { buildQuickBooksCreateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' import type { QuickBooksCreateEstimateParams, QuickBooksMutationResponse, @@ -14,6 +11,7 @@ import { QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, } from '@/tools/quickbooks/types' import { + addQuickBooksRequestId, buildQuickBooksEntityUrl, getQuickBooksToolHeaders, transformQuickBooksMutationResponse, diff --git a/apps/sim/tools/quickbooks/create_invoice.ts b/apps/sim/tools/quickbooks/create_invoice.ts index 7e6dcb9cb98..33a4ed68681 100644 --- a/apps/sim/tools/quickbooks/create_invoice.ts +++ b/apps/sim/tools/quickbooks/create_invoice.ts @@ -1,9 +1,6 @@ import { ErrorExtractorId } from '@/tools/error-extractors' import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' -import { - addQuickBooksRequestId, - buildQuickBooksCreateSalesDocumentBody, -} from '@/tools/quickbooks/sales_utils' +import { buildQuickBooksCreateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' import type { QuickBooksCreateInvoiceParams, QuickBooksMutationResponse, @@ -14,6 +11,7 @@ import { QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, } from '@/tools/quickbooks/types' import { + addQuickBooksRequestId, buildQuickBooksEntityUrl, getQuickBooksToolHeaders, transformQuickBooksMutationResponse, diff --git a/apps/sim/tools/quickbooks/create_refund_receipt.ts b/apps/sim/tools/quickbooks/create_refund_receipt.ts index 42743328088..359545563c7 100644 --- a/apps/sim/tools/quickbooks/create_refund_receipt.ts +++ b/apps/sim/tools/quickbooks/create_refund_receipt.ts @@ -1,9 +1,6 @@ import { ErrorExtractorId } from '@/tools/error-extractors' import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' -import { - addQuickBooksRequestId, - buildQuickBooksCreateSalesDocumentBody, -} from '@/tools/quickbooks/sales_utils' +import { buildQuickBooksCreateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' import type { QuickBooksCreateRefundReceiptParams, QuickBooksMutationResponse, @@ -14,6 +11,7 @@ import { QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, } from '@/tools/quickbooks/types' import { + addQuickBooksRequestId, buildQuickBooksEntityUrl, getQuickBooksToolHeaders, transformQuickBooksMutationResponse, diff --git a/apps/sim/tools/quickbooks/create_sales_receipt.ts b/apps/sim/tools/quickbooks/create_sales_receipt.ts index e46a32265ed..8a66f6d3bef 100644 --- a/apps/sim/tools/quickbooks/create_sales_receipt.ts +++ b/apps/sim/tools/quickbooks/create_sales_receipt.ts @@ -1,9 +1,6 @@ import { ErrorExtractorId } from '@/tools/error-extractors' import { QUICKBOOKS_MAX_RESPONSE_BYTES } from '@/tools/quickbooks/client' -import { - addQuickBooksRequestId, - buildQuickBooksCreateSalesDocumentBody, -} from '@/tools/quickbooks/sales_utils' +import { buildQuickBooksCreateSalesDocumentBody } from '@/tools/quickbooks/sales_utils' import type { QuickBooksCreateSalesReceiptParams, QuickBooksMutationResponse, @@ -14,6 +11,7 @@ import { QUICKBOOKS_SALES_TRANSACTION_PROPERTIES, } from '@/tools/quickbooks/types' import { + addQuickBooksRequestId, buildQuickBooksEntityUrl, getQuickBooksToolHeaders, transformQuickBooksMutationResponse, diff --git a/apps/sim/tools/quickbooks/quickbooks.test.ts b/apps/sim/tools/quickbooks/quickbooks.test.ts index 8f3200bbd8c..d76d1108dba 100644 --- a/apps/sim/tools/quickbooks/quickbooks.test.ts +++ b/apps/sim/tools/quickbooks/quickbooks.test.ts @@ -4,12 +4,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { evaluateOutputCondition } from '@/lib/workflows/blocks/block-outputs' import { evaluateSubBlockCondition } from '@/lib/workflows/subblocks/visibility' import { QuickBooksBlock } from '@/blocks/blocks/quickbooks' -import { - fetchValidatedQuickBooksCompanyInfo, - getQuickBooksEnvironment, - getQuickBooksUserInfoUrl, - QUICKBOOKS_MAX_RESPONSE_BYTES, -} from '@/tools/quickbooks/client' import { quickbooksCreateCreditMemoTool, quickbooksCreateCustomerPaymentTool, @@ -27,6 +21,12 @@ import { quickbooksVoidCustomerPaymentTool, quickbooksVoidInvoiceTool, } from '@/tools/quickbooks' +import { + fetchValidatedQuickBooksCompanyInfo, + getQuickBooksEnvironment, + getQuickBooksUserInfoUrl, + QUICKBOOKS_MAX_RESPONSE_BYTES, +} from '@/tools/quickbooks/client' import { quickbooksCreateCustomerTool } from '@/tools/quickbooks/create_customer' import { quickbooksCreateItemTool } from '@/tools/quickbooks/create_item' import { quickbooksCreateVendorTool } from '@/tools/quickbooks/create_vendor' diff --git a/apps/sim/tools/quickbooks/sales.test.ts b/apps/sim/tools/quickbooks/sales.test.ts index fabfdb8266b..3e9cd93e936 100644 --- a/apps/sim/tools/quickbooks/sales.test.ts +++ b/apps/sim/tools/quickbooks/sales.test.ts @@ -191,6 +191,32 @@ describe('QuickBooks sales document validation and bodies', () => { expect(() => parseQuickBooksSalesLines('[{"lineType":"item","amount":1,"itemId":7}]')).toThrow( 'itemId must be a string' ) + expect(() => + parseQuickBooksSalesLines('[{"lineType":"item","amount":10,"itemId":"7","quantity":0}]') + ).toThrow('quantity must be a positive finite number') + expect(() => + parseQuickBooksSalesLines('[{"lineType":"item","amount":10,"itemId":"7","unitPrice":-1}]') + ).toThrow('unitPrice must be a positive finite number') + expect(() => + parseQuickBooksSalesLines( + '[{"lineType":"item","amount":10,"itemId":"7","quantity":2,"unitPrice":6}]' + ) + ).toThrow('amount must equal quantity multiplied by unitPrice') + expect( + parseQuickBooksSalesLines( + '[{"lineType":"item","amount":0.02,"itemId":"7","quantity":0.1,"unitPrice":0.2}]' + ) + ).toEqual([ + { + lineType: 'item', + amount: 0.02, + itemId: '7', + description: undefined, + quantity: 0.1, + unitPrice: 0.2, + serviceDate: undefined, + }, + ]) expect(() => parseQuickBooksSalesLines( '[{"lineType":"item","amount":1,"itemId":"7","serviceDate":"2026-02-30"}]' diff --git a/apps/sim/tools/quickbooks/sales_utils.ts b/apps/sim/tools/quickbooks/sales_utils.ts index 029568078b0..ffa28cd1338 100644 --- a/apps/sim/tools/quickbooks/sales_utils.ts +++ b/apps/sim/tools/quickbooks/sales_utils.ts @@ -68,10 +68,12 @@ function requiredPositiveNumber(value: unknown, fieldName: string): number { return parsed } -function optionalFiniteNumber(value: unknown, fieldName: string): number | undefined { +function optionalPositiveNumber(value: unknown, fieldName: string): number | undefined { if (value == null || value === '') return undefined const parsed = typeof value === 'number' ? value : Number(value) - if (!Number.isFinite(parsed)) throw new Error(`${fieldName} must be a finite number`) + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`${fieldName} must be a positive finite number`) + } return parsed } @@ -126,13 +128,23 @@ export function parseQuickBooksSalesLines( } assertAllowedKeys(line, ITEM_LINE_KEYS, itemName) const description = optionalStringValue(line.description, `${itemName}.description`) + const amount = requiredPositiveNumber(line.amount, `${itemName}.amount`) + const quantity = optionalPositiveNumber(line.quantity, `${itemName}.quantity`) + const unitPrice = optionalPositiveNumber(line.unitPrice, `${itemName}.unitPrice`) + if ( + quantity !== undefined && + unitPrice !== undefined && + !new Decimal(quantity).times(unitPrice).toDecimalPlaces(2).equals(new Decimal(amount)) + ) { + throw new Error(`${itemName}.amount must equal quantity multiplied by unitPrice`) + } return { lineType: 'item', - amount: requiredPositiveNumber(line.amount, `${itemName}.amount`), + amount, itemId: requiredStringValue(line.itemId, `${itemName}.itemId`), description, - quantity: optionalFiniteNumber(line.quantity, `${itemName}.quantity`), - unitPrice: optionalFiniteNumber(line.unitPrice, `${itemName}.unitPrice`), + quantity, + unitPrice, serviceDate: validateQuickBooksDate( optionalStringValue(line.serviceDate, `${itemName}.serviceDate`), `${itemName}.serviceDate`