From aa81b4ed98b8ff6357513b00bb8ad686f8b0fb62 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 12:18:32 -0700 Subject: [PATCH 1/6] feat(quickbooks): add QuickBooks Online integration --- apps/docs/components/icons.tsx | 15 + apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/meta.json | 1 + .../docs/content/docs/en/tools/quickbooks.mdx | 742 ++++++++++++++++++ .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 107 +++ apps/sim/app/api/auth/[...all]/route.ts | 18 + apps/sim/app/api/auth/oauth/token/route.ts | 19 + apps/sim/blocks/blocks/quickbooks.ts | 602 ++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 15 + .../lib/api/contracts/oauth-connections.ts | 1 + apps/sim/lib/auth/auth.ts | 71 +- apps/sim/lib/core/config/env.ts | 3 + apps/sim/lib/oauth/oauth.ts | 29 + apps/sim/lib/oauth/types.ts | 2 + apps/sim/lib/oauth/utils.ts | 4 + apps/sim/tools/index.ts | 3 + apps/sim/tools/quickbooks/create_bill.ts | 134 ++++ apps/sim/tools/quickbooks/create_customer.ts | 118 +++ apps/sim/tools/quickbooks/create_invoice.ts | 153 ++++ apps/sim/tools/quickbooks/create_item.ts | 110 +++ apps/sim/tools/quickbooks/create_payment.ts | 124 +++ apps/sim/tools/quickbooks/create_vendor.ts | 111 +++ apps/sim/tools/quickbooks/get_company_info.ts | 62 ++ apps/sim/tools/quickbooks/get_customer.ts | 71 ++ apps/sim/tools/quickbooks/get_invoice.ts | 71 ++ apps/sim/tools/quickbooks/get_vendor.ts | 68 ++ apps/sim/tools/quickbooks/index.ts | 22 + apps/sim/tools/quickbooks/list_accounts.ts | 89 +++ apps/sim/tools/quickbooks/list_bills.ts | 87 ++ apps/sim/tools/quickbooks/list_customers.ts | 92 +++ apps/sim/tools/quickbooks/list_estimates.ts | 89 +++ apps/sim/tools/quickbooks/list_invoices.ts | 89 +++ apps/sim/tools/quickbooks/list_items.ts | 87 ++ apps/sim/tools/quickbooks/list_payments.ts | 89 +++ apps/sim/tools/quickbooks/list_vendors.ts | 89 +++ apps/sim/tools/quickbooks/query.ts | 76 ++ apps/sim/tools/quickbooks/send_invoice.ts | 84 ++ apps/sim/tools/quickbooks/types.ts | 429 ++++++++++ apps/sim/tools/quickbooks/update_customer.ts | 142 ++++ apps/sim/tools/quickbooks/update_invoice.ts | 111 +++ apps/sim/tools/quickbooks/utils.ts | 32 + apps/sim/tools/registry.ts | 46 ++ 44 files changed, 4312 insertions(+), 1 deletion(-) create mode 100644 apps/docs/content/docs/en/tools/quickbooks.mdx create mode 100644 apps/sim/blocks/blocks/quickbooks.ts create mode 100644 apps/sim/tools/quickbooks/create_bill.ts create mode 100644 apps/sim/tools/quickbooks/create_customer.ts create mode 100644 apps/sim/tools/quickbooks/create_invoice.ts create mode 100644 apps/sim/tools/quickbooks/create_item.ts create mode 100644 apps/sim/tools/quickbooks/create_payment.ts create mode 100644 apps/sim/tools/quickbooks/create_vendor.ts create mode 100644 apps/sim/tools/quickbooks/get_company_info.ts create mode 100644 apps/sim/tools/quickbooks/get_customer.ts create mode 100644 apps/sim/tools/quickbooks/get_invoice.ts create mode 100644 apps/sim/tools/quickbooks/get_vendor.ts create mode 100644 apps/sim/tools/quickbooks/index.ts create mode 100644 apps/sim/tools/quickbooks/list_accounts.ts create mode 100644 apps/sim/tools/quickbooks/list_bills.ts create mode 100644 apps/sim/tools/quickbooks/list_customers.ts create mode 100644 apps/sim/tools/quickbooks/list_estimates.ts create mode 100644 apps/sim/tools/quickbooks/list_invoices.ts create mode 100644 apps/sim/tools/quickbooks/list_items.ts create mode 100644 apps/sim/tools/quickbooks/list_payments.ts create mode 100644 apps/sim/tools/quickbooks/list_vendors.ts create mode 100644 apps/sim/tools/quickbooks/query.ts create mode 100644 apps/sim/tools/quickbooks/send_invoice.ts create mode 100644 apps/sim/tools/quickbooks/types.ts create mode 100644 apps/sim/tools/quickbooks/update_customer.ts create mode 100644 apps/sim/tools/quickbooks/update_invoice.ts create mode 100644 apps/sim/tools/quickbooks/utils.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index dae53828ccb..caf1068b227 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -3320,6 +3320,21 @@ export function QdrantIcon(props: SVGProps) { ) } +export function QuickBooksIcon(props: SVGProps) { + return ( + + + + + ) +} + export function QuiverIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index a3e73549b07..363b22977e9 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -143,6 +143,7 @@ import { ProfoundIcon, PulseIcon, QdrantIcon, + QuickBooksIcon, QuiverIcon, RDSIcon, RedditIcon, @@ -358,6 +359,7 @@ export const blockTypeToIconMap: Record = { pulse: PulseIcon, pulse_v2: PulseIcon, qdrant: QdrantIcon, + quickbooks: QuickBooksIcon, quiver: QuiverIcon, rds: RDSIcon, reddit: RedditIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 1f780cff3d2..24e8aa301db 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -139,6 +139,7 @@ "profound", "pulse", "qdrant", + "quickbooks", "quiver", "rds", "reddit", diff --git a/apps/docs/content/docs/en/tools/quickbooks.mdx b/apps/docs/content/docs/en/tools/quickbooks.mdx new file mode 100644 index 00000000000..062df3ffef4 --- /dev/null +++ b/apps/docs/content/docs/en/tools/quickbooks.mdx @@ -0,0 +1,742 @@ +--- +title: QuickBooks +description: Manage QuickBooks Online customers, invoices, and accounting data +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[QuickBooks Online](https://quickbooks.intuit.com/) is Intuit's cloud accounting platform used by millions of small and mid-sized businesses to manage invoicing, bills, payments, vendors, customers, and the chart of accounts. + +With QuickBooks, you can: + +- **Manage customers and vendors**: Maintain a single source of truth for the people and companies you bill and pay +- **Create and send invoices**: Generate professional invoices, email them to customers, and track payment status +- **Record payments and bills**: Apply customer payments against open invoices and capture vendor expenses +- **Query your books**: Run ad-hoc reports against the chart of accounts, items, and transactions using QuickBooks Query Language + +In Sim, the QuickBooks integration lets your agents read and write directly against QuickBooks Online. The company (realmId) is captured during OAuth sign-in, so each connected account scopes every tool call to the right books. Use it to automate invoice creation from form submissions, sync customers from your CRM, draft bills from receipts, send payment reminders, or surface live financial data inside conversational workflows. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Connect to QuickBooks Online to read and write customers, invoices, and chart-of-accounts entries, or run arbitrary QuickBooks queries. Uses Intuit OAuth 2.0; the company (realmId) is captured from the OAuth callback at sign-in time. + + + +## Tools + +### `quickbooks_create_bill` + +Create a new bill (vendor expense) in QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `vendorId` | string | Yes | Vendor ID for the bill | +| `lines` | json | Yes | Bill line items \(JSON array\). Each entry: \{ amount, accountId, description? \} | +| `txnDate` | string | No | Transaction date \(YYYY-MM-DD\) | +| `dueDate` | string | No | Due date \(YYYY-MM-DD\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bill` | object | Created bill | +| ↳ `Id` | string | QuickBooks bill ID | +| ↳ `VendorRef` | object | Vendor reference | +| ↳ `TxnDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `DueDate` | string | Due date \(YYYY-MM-DD\) | +| ↳ `TotalAmt` | number | Total bill amount | +| ↳ `Balance` | number | Outstanding balance | +| ↳ `Line` | array | Bill line items | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `billId` | string | New bill ID | + +### `quickbooks_create_customer` + +Create a new customer in QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `displayName` | string | Yes | Display name \(must be unique within the company\) | +| `companyName` | string | No | Company name | +| `givenName` | string | No | First name | +| `familyName` | string | No | Last name | +| `primaryEmail` | string | No | Primary email address | +| `primaryPhone` | string | No | Primary phone number | +| `notes` | string | No | Free-form notes | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customer` | object | Created customer | +| ↳ `Id` | string | QuickBooks customer ID | +| ↳ `DisplayName` | string | Display name shown in lists and forms | +| ↳ `CompanyName` | string | Company name | +| ↳ `GivenName` | string | First name | +| ↳ `FamilyName` | string | Last name | +| ↳ `PrimaryEmailAddr` | object | Primary email object with `Address` | +| ↳ `PrimaryPhone` | object | Primary phone object with `FreeFormNumber` | +| ↳ `Active` | boolean | Whether the customer is active | +| ↳ `Balance` | number | Open balance owed by the customer | +| ↳ `CurrencyRef` | object | Currency reference \(`value`, `name`\) | +| ↳ `Notes` | string | Free-form notes | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `customerId` | string | New customer ID | + +### `quickbooks_create_invoice` + +Create a new invoice in QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `customerId` | string | Yes | Customer ID to bill | +| `lines` | json | Yes | Invoice line items \(JSON array\). Each entry: \{ description?, amount, quantity?, itemId?, itemName? \} | +| `txnDate` | string | No | Transaction date \(YYYY-MM-DD\) | +| `dueDate` | string | No | Due date \(YYYY-MM-DD\) | +| `customerMemo` | string | No | Memo shown to the customer on the invoice | +| `billEmail` | string | No | Email address to bill \(used by Send Invoice flows\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | object | Created invoice | +| ↳ `Id` | string | QuickBooks invoice ID | +| ↳ `DocNumber` | string | Invoice document number | +| ↳ `TxnDate` | string | Invoice transaction date \(YYYY-MM-DD\) | +| ↳ `DueDate` | string | Invoice due date \(YYYY-MM-DD\) | +| ↳ `CustomerRef` | object | Reference to the customer \(`value`, `name`\) | +| ↳ `Line` | array | Invoice line items | +| ↳ `TotalAmt` | number | Total amount of the invoice | +| ↳ `Balance` | number | Outstanding balance on the invoice | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `CustomerMemo` | object | Customer-facing memo | +| ↳ `BillEmail` | object | Billing email object | +| ↳ `EmailStatus` | string | Email send status \(NotSet, NeedToSend, EmailSent\) | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `invoiceId` | string | New invoice ID | + +### `quickbooks_create_item` + +Create a new item (product or service) in QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `name` | string | Yes | Item name \(must be unique within the company\) | +| `type` | string | Yes | Item type — Service or NonInventory \(Inventory not supported in MVP\) | +| `incomeAccountId` | string | Yes | Income account ID to associate with the item | +| `description` | string | No | Item description | +| `unitPrice` | number | No | Unit price for the item | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | object | Created item | +| ↳ `Id` | string | QuickBooks item ID | +| ↳ `Name` | string | Item name | +| ↳ `Description` | string | Item description | +| ↳ `Type` | string | Item type \(Service, Inventory, NonInventory\) | +| ↳ `Active` | boolean | Whether the item is active | +| ↳ `UnitPrice` | number | Unit price | +| ↳ `IncomeAccountRef` | object | Income account reference | +| ↳ `ExpenseAccountRef` | object | Expense account reference | +| ↳ `AssetAccountRef` | object | Asset account reference | +| ↳ `QtyOnHand` | number | Quantity on hand \(Inventory only\) | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `itemId` | string | New item ID | + +### `quickbooks_create_payment` + +Record a customer payment in QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `customerId` | string | Yes | Customer ID making the payment | +| `amount` | number | Yes | Total payment amount | +| `invoiceId` | string | No | Optional invoice ID to apply the payment to | +| `paymentMethodId` | string | No | Payment method ID | +| `txnDate` | string | No | Transaction date \(YYYY-MM-DD\) | +| `paymentRefNum` | string | No | Payment reference number \(e.g., check number\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payment` | object | Created payment | +| ↳ `Id` | string | QuickBooks payment ID | +| ↳ `TxnDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `CustomerRef` | object | Customer reference | +| ↳ `TotalAmt` | number | Total payment amount | +| ↳ `UnappliedAmt` | number | Unapplied amount | +| ↳ `PaymentMethodRef` | object | Payment method reference | +| ↳ `PaymentRefNum` | string | Payment reference number | +| ↳ `Line` | array | Payment line items / linked transactions | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `paymentId` | string | New payment ID | + +### `quickbooks_create_vendor` + +Create a new vendor in QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `displayName` | string | Yes | Display name \(must be unique within the company\) | +| `companyName` | string | No | Company name | +| `givenName` | string | No | First name | +| `familyName` | string | No | Last name | +| `primaryEmail` | string | No | Primary email address | +| `primaryPhone` | string | No | Primary phone number | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `vendor` | object | Created vendor | +| ↳ `Id` | string | QuickBooks vendor ID | +| ↳ `DisplayName` | string | Display name | +| ↳ `CompanyName` | string | Company name | +| ↳ `GivenName` | string | First name | +| ↳ `FamilyName` | string | Last name | +| ↳ `PrimaryEmailAddr` | object | Primary email object with `Address` | +| ↳ `PrimaryPhone` | object | Primary phone object with `FreeFormNumber` | +| ↳ `Active` | boolean | Whether the vendor is active | +| ↳ `Balance` | number | Open balance owed to the vendor | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `vendorId` | string | New vendor ID | + +### `quickbooks_get_company_info` + +Retrieve company information for the connected QuickBooks Online company + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `companyInfo` | object | Company information record | +| ↳ `Id` | string | QuickBooks company ID \(realmId\) | +| ↳ `CompanyName` | string | Company name | +| ↳ `LegalName` | string | Legal name | +| ↳ `CompanyAddr` | object | Company address | +| ↳ `CompanyStartDate` | string | Company start date | +| ↳ `FiscalYearStartMonth` | string | Fiscal year start month | +| ↳ `Country` | string | Country code | +| ↳ `Email` | object | Primary email object | +| ↳ `WebAddr` | object | Web address | +| ↳ `SupportedLanguages` | string | Supported languages | +| ↳ `NameValue` | array | Custom name/value pairs | +| ↳ `MetaData` | object | Create/update timestamps | + +### `quickbooks_get_customer` + +Retrieve a single QuickBooks customer by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `customerId` | string | Yes | QuickBooks customer ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customer` | object | Customer record | +| ↳ `Id` | string | QuickBooks customer ID | +| ↳ `DisplayName` | string | Display name shown in lists and forms | +| ↳ `CompanyName` | string | Company name | +| ↳ `GivenName` | string | First name | +| ↳ `FamilyName` | string | Last name | +| ↳ `PrimaryEmailAddr` | object | Primary email object with `Address` | +| ↳ `PrimaryPhone` | object | Primary phone object with `FreeFormNumber` | +| ↳ `Active` | boolean | Whether the customer is active | +| ↳ `Balance` | number | Open balance owed by the customer | +| ↳ `CurrencyRef` | object | Currency reference \(`value`, `name`\) | +| ↳ `Notes` | string | Free-form notes | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `customerId` | string | Customer ID | + +### `quickbooks_get_invoice` + +Retrieve a single QuickBooks invoice by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `invoiceId` | string | Yes | QuickBooks invoice ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | object | Invoice record | +| ↳ `Id` | string | QuickBooks invoice ID | +| ↳ `DocNumber` | string | Invoice document number | +| ↳ `TxnDate` | string | Invoice transaction date \(YYYY-MM-DD\) | +| ↳ `DueDate` | string | Invoice due date \(YYYY-MM-DD\) | +| ↳ `CustomerRef` | object | Reference to the customer \(`value`, `name`\) | +| ↳ `Line` | array | Invoice line items | +| ↳ `TotalAmt` | number | Total amount of the invoice | +| ↳ `Balance` | number | Outstanding balance on the invoice | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `CustomerMemo` | object | Customer-facing memo | +| ↳ `BillEmail` | object | Billing email object | +| ↳ `EmailStatus` | string | Email send status \(NotSet, NeedToSend, EmailSent\) | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `invoiceId` | string | Invoice ID | + +### `quickbooks_get_vendor` + +Retrieve a single QuickBooks vendor by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `vendorId` | string | Yes | QuickBooks vendor ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `vendor` | object | Vendor record | +| ↳ `Id` | string | QuickBooks vendor ID | +| ↳ `DisplayName` | string | Display name | +| ↳ `CompanyName` | string | Company name | +| ↳ `GivenName` | string | First name | +| ↳ `FamilyName` | string | Last name | +| ↳ `PrimaryEmailAddr` | object | Primary email object with `Address` | +| ↳ `PrimaryPhone` | object | Primary phone object with `FreeFormNumber` | +| ↳ `Active` | boolean | Whether the vendor is active | +| ↳ `Balance` | number | Open balance owed to the vendor | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `vendorId` | string | Vendor ID | + +### `quickbooks_list_accounts` + +List chart-of-accounts entries from QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `maxResults` | number | No | Maximum number of accounts to return \(default 100, max 1000\) | +| `startPosition` | number | No | Pagination start position \(1-indexed\) | +| `where` | string | No | Optional WHERE clause \(e.g., "AccountType = \'Income\'"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `accounts` | array | Array of accounts | +| ↳ `Id` | string | QuickBooks account ID | +| ↳ `Name` | string | Account name | +| ↳ `AccountType` | string | High-level account type \(Income, Expense, etc.\) | +| ↳ `AccountSubType` | string | Account sub-type | +| ↳ `Classification` | string | Asset, Liability, Equity, Revenue, or Expense | +| ↳ `CurrentBalance` | number | Current account balance | +| ↳ `Active` | boolean | Whether the account is active | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `totalCount` | number | Number of accounts returned | + +### `quickbooks_list_bills` + +List bills from QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `maxResults` | number | No | Maximum number of bills to return \(default 100, max 1000\) | +| `startPosition` | number | No | Pagination start position \(1-indexed\) | +| `where` | string | No | Optional WHERE clause | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bills` | array | Array of bills | +| ↳ `Id` | string | QuickBooks bill ID | +| ↳ `VendorRef` | object | Vendor reference | +| ↳ `TxnDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `DueDate` | string | Due date \(YYYY-MM-DD\) | +| ↳ `TotalAmt` | number | Total bill amount | +| ↳ `Balance` | number | Outstanding balance | +| ↳ `Line` | array | Bill line items | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `totalCount` | number | Number of bills returned | + +### `quickbooks_list_customers` + +List customers from QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `maxResults` | number | No | Maximum number of customers to return \(default 100, max 1000\) | +| `startPosition` | number | No | Pagination start position \(1-indexed\) | +| `where` | string | No | Optional WHERE clause \(e.g., "Active = true AND DisplayName LIKE \'A%\'"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customers` | array | Array of QuickBooks customers | +| ↳ `Id` | string | QuickBooks customer ID | +| ↳ `DisplayName` | string | Display name shown in lists and forms | +| ↳ `CompanyName` | string | Company name | +| ↳ `GivenName` | string | First name | +| ↳ `FamilyName` | string | Last name | +| ↳ `PrimaryEmailAddr` | object | Primary email object with `Address` | +| ↳ `PrimaryPhone` | object | Primary phone object with `FreeFormNumber` | +| ↳ `Active` | boolean | Whether the customer is active | +| ↳ `Balance` | number | Open balance owed by the customer | +| ↳ `CurrencyRef` | object | Currency reference \(`value`, `name`\) | +| ↳ `Notes` | string | Free-form notes | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `totalCount` | number | Number of customers returned | + +### `quickbooks_list_estimates` + +List estimates from QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `maxResults` | number | No | Maximum number of estimates to return \(default 100, max 1000\) | +| `startPosition` | number | No | Pagination start position \(1-indexed\) | +| `where` | string | No | Optional WHERE clause | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `estimates` | array | Array of estimates | +| ↳ `Id` | string | QuickBooks estimate ID | +| ↳ `DocNumber` | string | Estimate document number | +| ↳ `TxnDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `ExpirationDate` | string | Expiration date \(YYYY-MM-DD\) | +| ↳ `CustomerRef` | object | Customer reference | +| ↳ `Line` | array | Estimate line items | +| ↳ `TotalAmt` | number | Total estimate amount | +| ↳ `TxnStatus` | string | Estimate status \(Pending, Accepted, etc.\) | +| ↳ `EmailStatus` | string | Email send status | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `totalCount` | number | Number of estimates returned | + +### `quickbooks_list_invoices` + +List invoices from QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `maxResults` | number | No | Maximum number of invoices to return \(default 100, max 1000\) | +| `startPosition` | number | No | Pagination start position \(1-indexed\) | +| `where` | string | No | Optional WHERE clause \(e.g., "Balance > \'0\'"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoices` | array | Array of invoices | +| ↳ `Id` | string | QuickBooks invoice ID | +| ↳ `DocNumber` | string | Invoice document number | +| ↳ `TxnDate` | string | Invoice transaction date \(YYYY-MM-DD\) | +| ↳ `DueDate` | string | Invoice due date \(YYYY-MM-DD\) | +| ↳ `CustomerRef` | object | Reference to the customer \(`value`, `name`\) | +| ↳ `Line` | array | Invoice line items | +| ↳ `TotalAmt` | number | Total amount of the invoice | +| ↳ `Balance` | number | Outstanding balance on the invoice | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `CustomerMemo` | object | Customer-facing memo | +| ↳ `BillEmail` | object | Billing email object | +| ↳ `EmailStatus` | string | Email send status \(NotSet, NeedToSend, EmailSent\) | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `totalCount` | number | Number of invoices returned | + +### `quickbooks_list_items` + +List items (products and services) from QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `maxResults` | number | No | Maximum number of items to return \(default 100, max 1000\) | +| `startPosition` | number | No | Pagination start position \(1-indexed\) | +| `where` | string | No | Optional WHERE clause \(e.g., "Type = \'Service\'"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of items | +| ↳ `Id` | string | QuickBooks item ID | +| ↳ `Name` | string | Item name | +| ↳ `Description` | string | Item description | +| ↳ `Type` | string | Item type \(Service, Inventory, NonInventory\) | +| ↳ `Active` | boolean | Whether the item is active | +| ↳ `UnitPrice` | number | Unit price | +| ↳ `IncomeAccountRef` | object | Income account reference | +| ↳ `ExpenseAccountRef` | object | Expense account reference | +| ↳ `AssetAccountRef` | object | Asset account reference | +| ↳ `QtyOnHand` | number | Quantity on hand \(Inventory only\) | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `totalCount` | number | Number of items returned | + +### `quickbooks_list_payments` + +List payments from QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `maxResults` | number | No | Maximum number of payments to return \(default 100, max 1000\) | +| `startPosition` | number | No | Pagination start position \(1-indexed\) | +| `where` | string | No | Optional WHERE clause | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payments` | array | Array of payments | +| ↳ `Id` | string | QuickBooks payment ID | +| ↳ `TxnDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `CustomerRef` | object | Customer reference | +| ↳ `TotalAmt` | number | Total payment amount | +| ↳ `UnappliedAmt` | number | Unapplied amount | +| ↳ `PaymentMethodRef` | object | Payment method reference | +| ↳ `PaymentRefNum` | string | Payment reference number | +| ↳ `Line` | array | Payment line items / linked transactions | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `totalCount` | number | Number of payments returned | + +### `quickbooks_list_vendors` + +List vendors from QuickBooks Online + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `maxResults` | number | No | Maximum number of vendors to return \(default 100, max 1000\) | +| `startPosition` | number | No | Pagination start position \(1-indexed\) | +| `where` | string | No | Optional WHERE clause \(e.g., "Active = true"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `vendors` | array | Array of vendors | +| ↳ `Id` | string | QuickBooks vendor ID | +| ↳ `DisplayName` | string | Display name | +| ↳ `CompanyName` | string | Company name | +| ↳ `GivenName` | string | First name | +| ↳ `FamilyName` | string | Last name | +| ↳ `PrimaryEmailAddr` | object | Primary email object with `Address` | +| ↳ `PrimaryPhone` | object | Primary phone object with `FreeFormNumber` | +| ↳ `Active` | boolean | Whether the vendor is active | +| ↳ `Balance` | number | Open balance owed to the vendor | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `totalCount` | number | Number of vendors returned | + +### `quickbooks_query` + +Run a QuickBooks Online query (SQL-like syntax, e.g., + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `query` | string | Yes | QuickBooks Query Language statement \(e.g., "SELECT * FROM Vendor WHERE Active = true MAXRESULTS 50"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Raw QueryResponse object from QuickBooks | +| `totalCount` | number | Reported total count | + +### `quickbooks_send_invoice` + +Email a QuickBooks invoice to a recipient + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `invoiceId` | string | Yes | Invoice ID to send | +| `sendTo` | string | No | Email address override; defaults to BillEmail on the invoice | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | object | Invoice after send | +| ↳ `Id` | string | QuickBooks invoice ID | +| ↳ `DocNumber` | string | Invoice document number | +| ↳ `TxnDate` | string | Invoice transaction date \(YYYY-MM-DD\) | +| ↳ `DueDate` | string | Invoice due date \(YYYY-MM-DD\) | +| ↳ `CustomerRef` | object | Reference to the customer \(`value`, `name`\) | +| ↳ `Line` | array | Invoice line items | +| ↳ `TotalAmt` | number | Total amount of the invoice | +| ↳ `Balance` | number | Outstanding balance on the invoice | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `CustomerMemo` | object | Customer-facing memo | +| ↳ `BillEmail` | object | Billing email object | +| ↳ `EmailStatus` | string | Email send status \(NotSet, NeedToSend, EmailSent\) | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `invoiceId` | string | Invoice ID | + +### `quickbooks_update_customer` + +Sparse-update an existing QuickBooks customer + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `customerId` | string | Yes | Customer ID to update | +| `syncToken` | string | Yes | Current SyncToken from the customer record \(required for updates\) | +| `displayName` | string | No | Display name | +| `companyName` | string | No | Company name | +| `givenName` | string | No | First name | +| `familyName` | string | No | Last name | +| `primaryEmail` | string | No | Primary email address | +| `primaryPhone` | string | No | Primary phone number | +| `notes` | string | No | Free-form notes | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customer` | object | Updated customer | +| ↳ `Id` | string | QuickBooks customer ID | +| ↳ `DisplayName` | string | Display name shown in lists and forms | +| ↳ `CompanyName` | string | Company name | +| ↳ `GivenName` | string | First name | +| ↳ `FamilyName` | string | Last name | +| ↳ `PrimaryEmailAddr` | object | Primary email object with `Address` | +| ↳ `PrimaryPhone` | object | Primary phone object with `FreeFormNumber` | +| ↳ `Active` | boolean | Whether the customer is active | +| ↳ `Balance` | number | Open balance owed by the customer | +| ↳ `CurrencyRef` | object | Currency reference \(`value`, `name`\) | +| ↳ `Notes` | string | Free-form notes | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `customerId` | string | Customer ID | + +### `quickbooks_update_invoice` + +Update an existing QuickBooks invoice (sparse update — date/memo/email only; line items cannot be modified) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `realmId` | string | Yes | QuickBooks company ID \(realmId\) — captured at OAuth time | +| `invoiceId` | string | Yes | Invoice ID to update | +| `syncToken` | string | Yes | Current SyncToken from the invoice record \(required for updates\) | +| `dueDate` | string | No | Due date \(YYYY-MM-DD\) | +| `customerMemo` | string | No | Customer-facing memo | +| `billEmail` | string | No | Billing email address | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | object | Updated invoice | +| ↳ `Id` | string | QuickBooks invoice ID | +| ↳ `DocNumber` | string | Invoice document number | +| ↳ `TxnDate` | string | Invoice transaction date \(YYYY-MM-DD\) | +| ↳ `DueDate` | string | Invoice due date \(YYYY-MM-DD\) | +| ↳ `CustomerRef` | object | Reference to the customer \(`value`, `name`\) | +| ↳ `Line` | array | Invoice line items | +| ↳ `TotalAmt` | number | Total amount of the invoice | +| ↳ `Balance` | number | Outstanding balance on the invoice | +| ↳ `CurrencyRef` | object | Currency reference | +| ↳ `CustomerMemo` | object | Customer-facing memo | +| ↳ `BillEmail` | object | Billing email object | +| ↳ `EmailStatus` | string | Email send status \(NotSet, NeedToSend, EmailSent\) | +| ↳ `MetaData` | object | Create/update timestamps | +| ↳ `SyncToken` | string | Optimistic concurrency token; required for sparse updates | +| `invoiceId` | string | Invoice ID | + + diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index 9d3280bd825..71c00897f47 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -143,6 +143,7 @@ import { ProfoundIcon, PulseIcon, QdrantIcon, + QuickBooksIcon, QuiverIcon, RDSIcon, RedditIcon, @@ -341,6 +342,7 @@ export const blockTypeToIconMap: Record = { profound: ProfoundIcon, pulse_v2: PulseIcon, qdrant: QdrantIcon, + quickbooks: QuickBooksIcon, quiver: QuiverIcon, rds: RDSIcon, reddit: RedditIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 346cb98feeb..8ae5e93f7c5 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -10294,6 +10294,113 @@ "integrationTypes": ["databases", "ai", "documents", "search"], "tags": ["vector-search", "knowledge-base"] }, + { + "type": "quickbooks", + "slug": "quickbooks", + "name": "QuickBooks", + "description": "Manage QuickBooks Online customers, invoices, and accounting data", + "longDescription": "Connect to QuickBooks Online to read and write customers, invoices, and chart-of-accounts entries, or run arbitrary QuickBooks queries. Uses Intuit OAuth 2.0; the company (realmId) is captured from the OAuth callback at sign-in time.", + "bgColor": "#2CA01C", + "iconName": "QuickBooksIcon", + "docsUrl": "https://docs.sim.ai/tools/quickbooks", + "operations": [ + { + "name": "List Customers", + "description": "List customers from QuickBooks Online" + }, + { + "name": "Get Customer", + "description": "Retrieve a single QuickBooks customer by ID" + }, + { + "name": "Create Customer", + "description": "Create a new customer in QuickBooks Online" + }, + { + "name": "Update Customer", + "description": "Sparse-update an existing QuickBooks customer" + }, + { + "name": "List Invoices", + "description": "List invoices from QuickBooks Online" + }, + { + "name": "Get Invoice", + "description": "Retrieve a single QuickBooks invoice by ID" + }, + { + "name": "Create Invoice", + "description": "Create a new invoice in QuickBooks Online" + }, + { + "name": "Update Invoice", + "description": "Update an existing QuickBooks invoice (sparse update — date/memo/email only; line items cannot be modified)" + }, + { + "name": "Send Invoice", + "description": "Email a QuickBooks invoice to a recipient" + }, + { + "name": "List Vendors", + "description": "List vendors from QuickBooks Online" + }, + { + "name": "Get Vendor", + "description": "Retrieve a single QuickBooks vendor by ID" + }, + { + "name": "Create Vendor", + "description": "Create a new vendor in QuickBooks Online" + }, + { + "name": "List Items", + "description": "List items (products and services) from QuickBooks Online" + }, + { + "name": "Create Item", + "description": "Create a new item (product or service) in QuickBooks Online" + }, + { + "name": "List Payments", + "description": "List payments from QuickBooks Online" + }, + { + "name": "Create Payment", + "description": "Record a customer payment in QuickBooks Online" + }, + { + "name": "List Bills", + "description": "List bills from QuickBooks Online" + }, + { + "name": "Create Bill", + "description": "Create a new bill (vendor expense) in QuickBooks Online" + }, + { + "name": "List Estimates", + "description": "List estimates from QuickBooks Online" + }, + { + "name": "List Accounts", + "description": "List chart-of-accounts entries from QuickBooks Online" + }, + { + "name": "Get Company Info", + "description": "Retrieve company information for the connected QuickBooks Online company" + }, + { + "name": "Run Query", + "description": "Run a QuickBooks Online query (SQL-like syntax, e.g., " + } + ], + "operationCount": 22, + "triggers": [], + "triggerCount": 0, + "authType": "oauth", + "category": "tools", + "integrationTypes": ["other", "ecommerce"], + "tags": ["payments"] + }, { "type": "quiver", "slug": "quiver", diff --git a/apps/sim/app/api/auth/[...all]/route.ts b/apps/sim/app/api/auth/[...all]/route.ts index 6ff9bfd6db2..c99aa4b979e 100644 --- a/apps/sim/app/api/auth/[...all]/route.ts +++ b/apps/sim/app/api/auth/[...all]/route.ts @@ -1,4 +1,5 @@ import { toNextJsHandler } from 'better-auth/next-js' +import { cookies } from 'next/headers' import { type NextRequest, NextResponse } from 'next/server' import { auth } from '@/lib/auth' import { createAnonymousSession, ensureAnonymousUserExists } from '@/lib/auth/anonymous' @@ -27,6 +28,23 @@ export const GET = withRouteHandler(async (request: NextRequest) => { return NextResponse.json(createAnonymousSession()) } + if (path === 'oauth2/callback/quickbooks') { + const realmId = request.nextUrl.searchParams.get('realmId') + const oauthError = request.nextUrl.searchParams.get('error') + const cookieStore = await cookies() + if (oauthError || !realmId) { + cookieStore.delete('qb_pending_realm') + } else { + cookieStore.set('qb_pending_realm', realmId, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 600, + }) + } + } + return betterAuthGET(request) }) diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts index 1ab26f84159..fdb153c970d 100644 --- a/apps/sim/app/api/auth/oauth/token/route.ts +++ b/apps/sim/app/api/auth/oauth/token/route.ts @@ -22,6 +22,7 @@ export const dynamic = 'force-dynamic' const logger = createLogger('OAuthTokenAPI') const SALESFORCE_INSTANCE_URL_REGEX = /__sf_instance__:([^\s]+)/ +const QUICKBOOKS_REALM_ID_REGEX = /__qb_realm__:([^\s]+)/ /** * Get an access token for a specific credential @@ -166,11 +167,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } } + let realmId: string | undefined + if (credential.providerId === 'quickbooks' && credential.scope) { + const realmMatch = credential.scope.match(QUICKBOOKS_REALM_ID_REGEX) + if (realmMatch) { + realmId = realmMatch[1] + } + } + return NextResponse.json( { accessToken, idToken: credential.idToken || undefined, ...(instanceUrl && { instanceUrl }), + ...(realmId && { realmId }), }, { status: 200 } ) @@ -249,11 +259,20 @@ export const GET = withRouteHandler(async (request: NextRequest) => { } } + let realmId: string | undefined + if (credential.providerId === 'quickbooks' && credential.scope) { + const realmMatch = credential.scope.match(QUICKBOOKS_REALM_ID_REGEX) + if (realmMatch) { + realmId = realmMatch[1] + } + } + return NextResponse.json( { accessToken, idToken: credential.idToken || undefined, ...(instanceUrl && { instanceUrl }), + ...(realmId && { realmId }), }, { status: 200 } ) diff --git a/apps/sim/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts new file mode 100644 index 00000000000..97f4d534ea2 --- /dev/null +++ b/apps/sim/blocks/blocks/quickbooks.ts @@ -0,0 +1,602 @@ +import { QuickBooksIcon } from '@/components/icons' +import { getScopesForService } from '@/lib/oauth/utils' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import type { QuickBooksResponse } from '@/tools/quickbooks/types' + +export const QuickBooksBlock: BlockConfig = { + type: 'quickbooks', + name: 'QuickBooks', + description: 'Manage QuickBooks Online customers, invoices, and accounting data', + longDescription: + 'Connect to QuickBooks Online to read and write customers, invoices, and chart-of-accounts entries, or run arbitrary QuickBooks queries. Uses Intuit OAuth 2.0; the company (realmId) is captured from the OAuth callback at sign-in time.', + authMode: AuthMode.OAuth, + docsLink: 'https://docs.sim.ai/tools/quickbooks', + category: 'tools', + integrationType: IntegrationType.Other, + tags: ['payments'], + bgColor: '#2CA01C', + icon: QuickBooksIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Customers', id: 'list_customers' }, + { label: 'Get Customer', id: 'get_customer' }, + { label: 'Create Customer', id: 'create_customer' }, + { label: 'Update Customer', id: 'update_customer' }, + { label: 'List Invoices', id: 'list_invoices' }, + { label: 'Get Invoice', id: 'get_invoice' }, + { label: 'Create Invoice', id: 'create_invoice' }, + { label: 'Update Invoice', id: 'update_invoice' }, + { label: 'Send Invoice', id: 'send_invoice' }, + { label: 'List Vendors', id: 'list_vendors' }, + { label: 'Get Vendor', id: 'get_vendor' }, + { label: 'Create Vendor', id: 'create_vendor' }, + { label: 'List Items', id: 'list_items' }, + { label: 'Create Item', id: 'create_item' }, + { label: 'List Payments', id: 'list_payments' }, + { label: 'Create Payment', id: 'create_payment' }, + { label: 'List Bills', id: 'list_bills' }, + { label: 'Create Bill', id: 'create_bill' }, + { label: 'List Estimates', id: 'list_estimates' }, + { label: 'List Accounts', id: 'list_accounts' }, + { label: 'Get Company Info', id: 'get_company_info' }, + { label: 'Run Query', id: 'query' }, + ], + value: () => 'list_customers', + }, + { + id: 'credential', + title: 'QuickBooks Account', + type: 'oauth-input', + serviceId: 'quickbooks', + requiredScopes: getScopesForService('quickbooks'), + placeholder: 'Select QuickBooks account', + required: true, + }, + + // Get/create/update IDs + { + id: 'customerId', + title: 'Customer ID', + type: 'short-input', + placeholder: 'e.g. 1234', + dependsOn: ['credential'], + condition: { + field: 'operation', + value: ['get_customer', 'create_invoice', 'update_customer', 'create_payment'], + }, + required: { + field: 'operation', + value: ['get_customer', 'create_invoice', 'update_customer', 'create_payment'], + }, + }, + { + id: 'invoiceId', + title: 'Invoice ID', + type: 'short-input', + placeholder: 'e.g. 5678', + dependsOn: ['credential'], + condition: { + field: 'operation', + value: ['get_invoice', 'update_invoice', 'send_invoice'], + }, + required: { + field: 'operation', + value: ['get_invoice', 'update_invoice', 'send_invoice'], + }, + }, + { + id: 'vendorId', + title: 'Vendor ID', + type: 'short-input', + placeholder: 'e.g. 9012', + dependsOn: ['credential'], + condition: { field: 'operation', value: ['get_vendor', 'create_bill'] }, + required: { field: 'operation', value: ['get_vendor', 'create_bill'] }, + }, + { + id: 'syncToken', + title: 'Sync Token', + type: 'short-input', + placeholder: '0', + dependsOn: ['credential'], + condition: { field: 'operation', value: ['update_customer', 'update_invoice'] }, + required: { field: 'operation', value: ['update_customer', 'update_invoice'] }, + }, + + // Create customer / update customer / create vendor (shared fields) + { + id: 'displayName', + title: 'Display Name', + type: 'short-input', + placeholder: 'Acme Corp', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer', 'create_vendor'], + }, + required: { field: 'operation', value: ['create_customer', 'create_vendor'] }, + }, + { + id: 'companyName', + title: 'Company Name', + type: 'short-input', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer', 'create_vendor'], + }, + }, + { + id: 'givenName', + title: 'First Name', + type: 'short-input', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer', 'create_vendor'], + }, + }, + { + id: 'familyName', + title: 'Last Name', + type: 'short-input', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer', 'create_vendor'], + }, + }, + { + id: 'primaryEmail', + title: 'Primary Email', + type: 'short-input', + placeholder: 'billing@acme.com', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer', 'create_vendor'], + }, + }, + { + id: 'primaryPhone', + title: 'Primary Phone', + type: 'short-input', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer', 'create_vendor'], + }, + }, + { + id: 'notes', + title: 'Notes', + type: 'long-input', + mode: 'advanced', + condition: { field: 'operation', value: ['create_customer', 'update_customer'] }, + }, + + // Create invoice + { + id: 'lines', + title: 'Line Items', + type: 'long-input', + placeholder: '[{"description":"Consulting","amount":1000,"quantity":1,"itemId":"1"}]', + condition: { field: 'operation', value: 'create_invoice' }, + required: { field: 'operation', value: 'create_invoice' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of QuickBooks invoice line items. Each entry must have a numeric `amount` and may include `description`, `quantity`, `itemId`, and `itemName`. Return ONLY the JSON array.', + generationType: 'json-object', + }, + }, + { + id: 'txnDate', + title: 'Transaction Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_invoice', 'create_bill', 'create_payment'], + }, + }, + { + id: 'dueDate', + title: 'Due Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_invoice', 'update_invoice', 'create_bill'], + }, + }, + { + id: 'customerMemo', + title: 'Customer Memo', + type: 'long-input', + mode: 'advanced', + condition: { field: 'operation', value: ['create_invoice', 'update_invoice'] }, + }, + { + id: 'billEmail', + title: 'Bill Email', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: ['create_invoice', 'update_invoice'] }, + }, + + // Send invoice + { + id: 'sendTo', + title: 'Send To', + type: 'short-input', + placeholder: 'recipient@example.com', + condition: { field: 'operation', value: 'send_invoice' }, + }, + + // Create payment + { + id: 'amount', + title: 'Amount', + type: 'short-input', + placeholder: '100.00', + condition: { field: 'operation', value: 'create_payment' }, + required: { field: 'operation', value: 'create_payment' }, + }, + { + id: 'paymentInvoiceId', + title: 'Apply to Invoice ID', + type: 'short-input', + placeholder: 'Optional — invoice to apply payment against', + mode: 'advanced', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'create_payment' }, + }, + { + id: 'paymentMethodId', + title: 'Payment Method ID', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: 'create_payment' }, + }, + { + id: 'paymentRefNum', + title: 'Payment Reference Number', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: 'create_payment' }, + }, + + // Create bill + { + id: 'billLines', + title: 'Bill Line Items', + type: 'long-input', + placeholder: '[{"amount":250,"accountId":"42","description":"Office supplies"}]', + condition: { field: 'operation', value: 'create_bill' }, + required: { field: 'operation', value: 'create_bill' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of QuickBooks bill line items. Each entry must have a numeric `amount` and a string `accountId`, and may include `description`. Return ONLY the JSON array.', + generationType: 'json-object', + }, + }, + + // Create item + { + id: 'itemName', + title: 'Item Name', + type: 'short-input', + placeholder: 'Consulting', + condition: { field: 'operation', value: 'create_item' }, + required: { field: 'operation', value: 'create_item' }, + }, + { + id: 'itemType', + title: 'Item Type', + type: 'dropdown', + options: [ + { label: 'Service', id: 'Service' }, + { label: 'Non-Inventory', id: 'NonInventory' }, + ], + condition: { field: 'operation', value: 'create_item' }, + required: { field: 'operation', value: 'create_item' }, + }, + { + id: 'incomeAccountId', + title: 'Income Account ID', + type: 'short-input', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'create_item' }, + required: { field: 'operation', value: 'create_item' }, + }, + { + id: 'itemDescription', + title: 'Item Description', + type: 'long-input', + mode: 'advanced', + condition: { field: 'operation', value: 'create_item' }, + }, + { + id: 'unitPrice', + title: 'Unit Price', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: 'create_item' }, + }, + + // List filters (shared across list_* operations) + { + id: 'where', + title: 'WHERE Clause', + type: 'short-input', + placeholder: 'Active = true', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'list_customers', + 'list_invoices', + 'list_accounts', + 'list_vendors', + 'list_items', + 'list_payments', + 'list_bills', + 'list_estimates', + ], + }, + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'short-input', + placeholder: '100', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'list_customers', + 'list_invoices', + 'list_accounts', + 'list_vendors', + 'list_items', + 'list_payments', + 'list_bills', + 'list_estimates', + ], + }, + }, + { + id: 'startPosition', + title: 'Start Position', + type: 'short-input', + placeholder: '1', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'list_customers', + 'list_invoices', + 'list_accounts', + 'list_vendors', + 'list_items', + 'list_payments', + 'list_bills', + 'list_estimates', + ], + }, + }, + + // Generic query + { + id: 'queryStatement', + title: 'Query', + type: 'long-input', + placeholder: 'SELECT * FROM Item WHERE Active = true MAXRESULTS 50', + condition: { field: 'operation', value: 'query' }, + required: { field: 'operation', value: 'query' }, + }, + ], + + tools: { + access: [ + 'quickbooks_create_bill', + 'quickbooks_create_customer', + 'quickbooks_create_invoice', + 'quickbooks_create_item', + 'quickbooks_create_payment', + 'quickbooks_create_vendor', + 'quickbooks_get_company_info', + 'quickbooks_get_customer', + 'quickbooks_get_invoice', + 'quickbooks_get_vendor', + 'quickbooks_list_accounts', + 'quickbooks_list_bills', + 'quickbooks_list_customers', + 'quickbooks_list_estimates', + 'quickbooks_list_invoices', + 'quickbooks_list_items', + 'quickbooks_list_payments', + 'quickbooks_list_vendors', + 'quickbooks_query', + 'quickbooks_send_invoice', + 'quickbooks_update_customer', + 'quickbooks_update_invoice', + ], + config: { + tool: (params) => `quickbooks_${params.operation}`, + params: (params) => { + const out: Record = { ...params } + if (params.maxResults !== undefined && params.maxResults !== '') { + out.maxResults = Number(params.maxResults) + } + if (params.startPosition !== undefined && params.startPosition !== '') { + out.startPosition = Number(params.startPosition) + } + if (params.amount !== undefined && params.amount !== '') { + out.amount = Number(params.amount) + } + if (params.unitPrice !== undefined && params.unitPrice !== '') { + out.unitPrice = Number(params.unitPrice) + } + if (params.billLines !== undefined) { + out.lines = params.billLines + out.billLines = undefined + } + if (params.paymentInvoiceId !== undefined) { + out.invoiceId = params.paymentInvoiceId + out.paymentInvoiceId = undefined + } + if (params.itemName !== undefined) { + out.name = params.itemName + out.itemName = undefined + } + if (params.itemType !== undefined) { + out.type = params.itemType + out.itemType = undefined + } + if (params.itemDescription !== undefined) { + out.description = params.itemDescription + out.itemDescription = undefined + } + if (params.queryStatement !== undefined) { + out.query = params.queryStatement + out.queryStatement = undefined + } + out.operation = undefined + return out + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'QuickBooks OAuth credential' }, + customerId: { type: 'string', description: 'QuickBooks customer ID' }, + invoiceId: { type: 'string', description: 'QuickBooks invoice ID' }, + vendorId: { type: 'string', description: 'QuickBooks vendor ID' }, + syncToken: { type: 'string', description: 'SyncToken for sparse updates' }, + displayName: { type: 'string', description: 'Display name (customer or vendor)' }, + companyName: { type: 'string', description: 'Company name' }, + givenName: { type: 'string', description: 'First name' }, + familyName: { type: 'string', description: 'Last name' }, + primaryEmail: { type: 'string', description: 'Primary email' }, + primaryPhone: { type: 'string', description: 'Primary phone' }, + notes: { type: 'string', description: 'Free-form notes' }, + lines: { type: 'json', description: 'Invoice line items (JSON array)' }, + billLines: { type: 'json', description: 'Bill line items (JSON array)' }, + txnDate: { type: 'string', description: 'Transaction date (YYYY-MM-DD)' }, + dueDate: { type: 'string', description: 'Due date (YYYY-MM-DD)' }, + customerMemo: { type: 'string', description: 'Customer-facing memo on invoice' }, + billEmail: { type: 'string', description: 'Email used for invoice billing' }, + sendTo: { type: 'string', description: 'Email recipient for send_invoice' }, + amount: { type: 'number', description: 'Payment amount' }, + paymentMethodId: { type: 'string', description: 'Payment method ID' }, + paymentRefNum: { type: 'string', description: 'Payment reference number' }, + itemName: { type: 'string', description: 'Item name' }, + itemType: { type: 'string', description: 'Item type (Service or NonInventory)' }, + incomeAccountId: { type: 'string', description: 'Income account ID for the item' }, + itemDescription: { type: 'string', description: 'Item description' }, + unitPrice: { type: 'number', description: 'Item unit price' }, + where: { type: 'string', description: 'Optional WHERE clause for list queries' }, + maxResults: { type: 'number', description: 'Maximum results returned' }, + startPosition: { type: 'number', description: 'Pagination start position (1-indexed)' }, + queryStatement: { type: 'string', description: 'QuickBooks Query Language statement' }, + }, + + outputs: { + customer: { + type: 'json', + description: + 'Customer record (Id, DisplayName, CompanyName, GivenName, FamilyName, PrimaryEmailAddr, PrimaryPhone, Active, Balance, CurrencyRef, Notes, MetaData, SyncToken)', + }, + customerId: { type: 'string', description: 'Customer ID' }, + customers: { + type: 'json', + description: + 'Array of customer records (Id, DisplayName, CompanyName, PrimaryEmailAddr, Balance, Active, SyncToken, ...)', + }, + invoice: { + type: 'json', + description: + 'Invoice record (Id, DocNumber, TxnDate, DueDate, CustomerRef, Line, TotalAmt, Balance, CurrencyRef, CustomerMemo, BillEmail, EmailStatus, MetaData, SyncToken)', + }, + invoiceId: { type: 'string', description: 'Invoice ID' }, + invoices: { + type: 'json', + description: + 'Array of invoice records (Id, DocNumber, TxnDate, DueDate, CustomerRef, TotalAmt, Balance, EmailStatus, SyncToken, ...)', + }, + accounts: { + type: 'json', + description: + 'Array of account records (Id, Name, AccountType, AccountSubType, Classification, CurrentBalance, Active, CurrencyRef, SyncToken)', + }, + vendor: { + type: 'json', + description: + 'Vendor record (Id, DisplayName, CompanyName, GivenName, FamilyName, PrimaryEmailAddr, PrimaryPhone, Active, Balance, CurrencyRef, MetaData, SyncToken)', + }, + vendorId: { type: 'string', description: 'Vendor ID' }, + vendors: { + type: 'json', + description: + 'Array of vendor records (Id, DisplayName, CompanyName, PrimaryEmailAddr, Balance, Active, SyncToken, ...)', + }, + item: { + type: 'json', + description: + 'Item record (Id, Name, Description, Type, Active, UnitPrice, IncomeAccountRef, ExpenseAccountRef, AssetAccountRef, QtyOnHand, MetaData, SyncToken)', + }, + itemId: { type: 'string', description: 'Item ID' }, + items: { + type: 'json', + description: + 'Array of item records (Id, Name, Type, UnitPrice, Active, IncomeAccountRef, SyncToken, ...)', + }, + payment: { + type: 'json', + description: + 'Payment record (Id, TxnDate, CustomerRef, TotalAmt, UnappliedAmt, PaymentMethodRef, PaymentRefNum, Line, CurrencyRef, MetaData, SyncToken)', + }, + paymentId: { type: 'string', description: 'Payment ID' }, + payments: { + type: 'json', + description: + 'Array of payment records (Id, TxnDate, CustomerRef, TotalAmt, UnappliedAmt, PaymentRefNum, SyncToken, ...)', + }, + bill: { + type: 'json', + description: + 'Bill record (Id, VendorRef, TxnDate, DueDate, TotalAmt, Balance, Line, CurrencyRef, MetaData, SyncToken)', + }, + billId: { type: 'string', description: 'Bill ID' }, + bills: { + type: 'json', + description: + 'Array of bill records (Id, VendorRef, TxnDate, DueDate, TotalAmt, Balance, SyncToken, ...)', + }, + estimates: { + type: 'json', + description: + 'Array of estimate records (Id, DocNumber, TxnDate, ExpirationDate, CustomerRef, Line, TotalAmt, TxnStatus, EmailStatus, SyncToken, ...)', + }, + companyInfo: { + type: 'json', + description: + 'Company info (Id, CompanyName, LegalName, CompanyAddr, CompanyStartDate, FiscalYearStartMonth, Country, Email, WebAddr, SupportedLanguages, NameValue, MetaData)', + }, + results: { + type: 'json', + description: + 'Raw QueryResponse object for the query operation (entity arrays keyed by entity name, plus startPosition/maxResults/totalCount)', + }, + totalCount: { type: 'number', description: 'Number of records returned' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index aacf6d49431..275ce85a6bc 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -156,6 +156,7 @@ import { PostHogBlock } from '@/blocks/blocks/posthog' import { ProfoundBlock } from '@/blocks/blocks/profound' import { PulseBlock, PulseV2Block } from '@/blocks/blocks/pulse' import { QdrantBlock } from '@/blocks/blocks/qdrant' +import { QuickBooksBlock } from '@/blocks/blocks/quickbooks' import { QuiverBlock } from '@/blocks/blocks/quiver' import { RDSBlock } from '@/blocks/blocks/rds' import { RedditBlock } from '@/blocks/blocks/reddit' @@ -406,6 +407,7 @@ export const registry: Record = { pulse: PulseBlock, pulse_v2: PulseV2Block, qdrant: QdrantBlock, + quickbooks: QuickBooksBlock, quiver: QuiverBlock, rds: RDSBlock, reddit: RedditBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index dae53828ccb..caf1068b227 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3320,6 +3320,21 @@ export function QdrantIcon(props: SVGProps) { ) } +export function QuickBooksIcon(props: SVGProps) { + return ( + + + + + ) +} + export function QuiverIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/api/contracts/oauth-connections.ts b/apps/sim/lib/api/contracts/oauth-connections.ts index ee37437e218..c40c14872ca 100644 --- a/apps/sim/lib/api/contracts/oauth-connections.ts +++ b/apps/sim/lib/api/contracts/oauth-connections.ts @@ -77,6 +77,7 @@ const oauthTokenResponseSchema = z.object({ accessToken: z.string(), idToken: z.string().optional(), instanceUrl: z.string().optional(), + realmId: z.string().optional(), }) export const oauthTokenGetContract = defineRouteContract({ diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index b446fff9727..d67d8289e9d 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -23,7 +23,7 @@ import { } from 'better-auth/plugins' import { emailHarmony } from 'better-auth-harmony' import { and, count, eq, inArray, sql } from 'drizzle-orm' -import { headers } from 'next/headers' +import { cookies, headers } from 'next/headers' import Stripe from 'stripe' import { getEmailSubject, @@ -343,6 +343,20 @@ export const auth = betterAuth({ } } + if (account.providerId === 'quickbooks') { + try { + const cookieStore = await cookies() + const realmId = cookieStore.get('qb_pending_realm')?.value + if (realmId) { + modifiedAccount.scope = `__qb_realm__:${realmId} ${account.scope ?? ''}`.trim() + } else { + logger.error('QuickBooks account.create.before: qb_pending_realm cookie missing') + } + } catch (error) { + logger.error('Failed to capture QuickBooks realmId', { error }) + } + } + if (isMicrosoftProvider(account.providerId)) { modifiedAccount.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry() } @@ -518,6 +532,21 @@ export const auth = betterAuth({ } } + if (account.providerId === 'quickbooks') { + try { + const cookieStore = await cookies() + cookieStore.delete('qb_pending_realm') + } catch (error) { + logger.error('Failed to clear qb_pending_realm cookie', { error }) + } + if (!account.accessTokenExpiresAt) { + await db + .update(schema.account) + .set({ accessTokenExpiresAt: new Date(Date.now() + 60 * 60 * 1000) }) + .where(eq(schema.account.id, account.id)) + } + } + if (isMicrosoftProvider(account.providerId)) { await db .update(schema.account) @@ -632,6 +661,7 @@ export const auth = betterAuth({ 'webflow', 'asana', 'pipedrive', + 'quickbooks', 'hubspot', 'linkedin', 'spotify', @@ -1758,6 +1788,45 @@ export const auth = betterAuth({ }, }, + // QuickBooks (Intuit) provider — realmId is captured from the OAuth + // callback URL query string at sign-in time (not via OIDC). The + // `qb_pending_realm` cookie is set by the catch-all auth route before + // Better Auth processes the callback. + { + providerId: 'quickbooks', + clientId: env.QUICKBOOKS_CLIENT_ID as string, + clientSecret: env.QUICKBOOKS_CLIENT_SECRET as string, + authorizationUrl: 'https://appcenter.intuit.com/connect/oauth2', + tokenUrl: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer', + scopes: getCanonicalScopesForProvider('quickbooks'), + responseType: 'code', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/quickbooks`, + getUserInfo: async () => { + try { + const cookieStore = await cookies() + const realmId = cookieStore.get('qb_pending_realm')?.value + if (!realmId) { + logger.error( + 'QuickBooks OAuth: qb_pending_realm cookie missing — callback interceptor did not run' + ) + return null + } + return { + id: `quickbooks-${realmId}-${generateId()}`, + name: `QuickBooks Company ${realmId}`, + email: `quickbooks-${realmId}@quickbooks.local`, + emailVerified: true, + image: undefined, + createdAt: new Date(), + updatedAt: new Date(), + } + } catch (error) { + logger.error('Error creating QuickBooks user profile:', { error }) + return null + } + }, + }, + // Salesforce provider { providerId: 'salesforce', diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 14bf33ce5d4..21da1f968ae 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -302,6 +302,9 @@ export const env = createEnv({ MICROSOFT_CLIENT_SECRET: z.string().optional(), // Microsoft OAuth client secret HUBSPOT_CLIENT_ID: z.string().optional(), // HubSpot OAuth client ID HUBSPOT_CLIENT_SECRET: z.string().optional(), // HubSpot OAuth client secret + QUICKBOOKS_CLIENT_ID: z.string().optional(), // Intuit/QuickBooks OAuth client ID + QUICKBOOKS_CLIENT_SECRET: z.string().optional(), // Intuit/QuickBooks OAuth client secret + QUICKBOOKS_ENV: z.enum(['sandbox', 'production']).optional(), // QuickBooks Online API environment (default: production) SALESFORCE_CLIENT_ID: z.string().optional(), // Salesforce OAuth client ID SALESFORCE_CLIENT_SECRET: z.string().optional(), // Salesforce OAuth client secret WEALTHBOX_CLIENT_ID: z.string().optional(), // WealthBox OAuth client ID diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index c159b94fc5d..d0cdb976f98 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -38,6 +38,7 @@ import { NotionIcon, OutlookIcon, PipedriveIcon, + QuickBooksIcon, RedditIcon, SalesforceIcon, ShopifyIcon, @@ -866,6 +867,21 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'pipedrive', }, + quickbooks: { + name: 'QuickBooks', + icon: QuickBooksIcon, + services: { + quickbooks: { + name: 'QuickBooks Online', + description: 'Manage customers, invoices, and accounting data in QuickBooks Online.', + providerId: 'quickbooks', + icon: QuickBooksIcon, + baseProviderIcon: QuickBooksIcon, + scopes: ['com.intuit.quickbooks.accounting', 'com.intuit.quickbooks.payment'], + }, + }, + defaultService: 'quickbooks', + }, hubspot: { name: 'HubSpot', icon: HubspotIcon, @@ -1391,6 +1407,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: false, } } + case 'quickbooks': { + const { clientId, clientSecret } = getCredentials( + env.QUICKBOOKS_CLIENT_ID, + env.QUICKBOOKS_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer', + clientId, + clientSecret, + useBasicAuth: true, + supportsRefreshTokenRotation: true, + } + } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index 5c39f53440a..edc6fd31f50 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -41,6 +41,7 @@ export type OAuthProvider = | 'asana' | 'attio' | 'pipedrive' + | 'quickbooks' | 'hubspot' | 'salesforce' | 'linkedin' @@ -91,6 +92,7 @@ export type OAuthService = | 'asana' | 'attio' | 'pipedrive' + | 'quickbooks' | 'hubspot' | 'salesforce' | 'linkedin' diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index 4c38f8949a0..b2b7758f471 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -80,6 +80,10 @@ export const SCOPE_DESCRIPTIONS: Record = { 'write:space.property:confluence': 'Create and manage space properties', 'read:space.permission:confluence': 'View Confluence space permissions', + // QuickBooks (Intuit) scopes + 'com.intuit.quickbooks.accounting': 'Read and write QuickBooks accounting data', + 'com.intuit.quickbooks.payment': 'Read and write QuickBooks Payments data', + // Common scopes 'read:me': 'Read profile information', offline_access: 'Access account when not using the application', diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 3bf69a56101..673eba0c04e 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -904,6 +904,9 @@ export async function executeTool( if (data.instanceUrl) { contextParams.instanceUrl = data.instanceUrl } + if (data.realmId) { + contextParams.realmId = data.realmId + } logger.info(`[${requestId}] Successfully got access token for ${toolId}`) diff --git a/apps/sim/tools/quickbooks/create_bill.ts b/apps/sim/tools/quickbooks/create_bill.ts new file mode 100644 index 00000000000..a8980b6806e --- /dev/null +++ b/apps/sim/tools/quickbooks/create_bill.ts @@ -0,0 +1,134 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksBillLine, + QuickBooksBillResponse, + QuickBooksCreateBillParams, +} from '@/tools/quickbooks/types' +import { BILL_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksCreateBill') + +function coerceLines(input: QuickBooksCreateBillParams['lines']): QuickBooksBillLine[] { + if (Array.isArray(input)) return input + if (typeof input !== 'string') { + throw new Error('Bill lines must be a JSON array') + } + const parsed = JSON.parse(input) + if (!Array.isArray(parsed)) { + throw new Error('Bill lines must be a JSON array') + } + return parsed as QuickBooksBillLine[] +} + +export const quickbooksCreateBillTool: ToolConfig< + QuickBooksCreateBillParams, + QuickBooksBillResponse +> = { + id: 'quickbooks_create_bill', + name: 'QuickBooks Create Bill', + description: 'Create a new bill (vendor expense) in QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + vendorId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Vendor ID for the bill', + }, + lines: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Bill line items (JSON array). Each entry: { amount, accountId, description? }', + }, + txnDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Transaction date (YYYY-MM-DD)', + }, + dueDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Due date (YYYY-MM-DD)', + }, + }, + + request: { + url: (params) => `${buildCompanyUrl(params.realmId, '/bill')}?minorversion=73`, + method: 'POST', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + body: (params) => { + const lines = coerceLines(params.lines) + if (lines.length === 0) { + throw new Error('At least one bill line is required') + } + + const Line = lines.map((line) => { + const amount = Number(line.amount) + if (!Number.isFinite(amount)) { + throw new Error('Each bill line requires a numeric `amount`') + } + if (!line.accountId) { + throw new Error('Each bill line requires an `accountId`') + } + const entry: Record = { + DetailType: 'AccountBasedExpenseLineDetail', + Amount: amount, + AccountBasedExpenseLineDetail: { + AccountRef: { value: line.accountId }, + }, + } + if (line.description) entry.Description = line.description + return entry + }) + + const body: Record = { + VendorRef: { value: params.vendorId }, + Line, + } + if (params.txnDate) body.TxnDate = params.txnDate + if (params.dueDate) body.DueDate = params.dueDate + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks create bill failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to create QuickBooks bill') + } + const bill = (data?.Bill ?? null) as Record | null + return { + success: true, + output: { + bill, + billId: bill ? ((bill.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + bill: { type: 'object', description: 'Created bill', properties: BILL_OUTPUT }, + billId: { type: 'string', description: 'New bill ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/create_customer.ts b/apps/sim/tools/quickbooks/create_customer.ts new file mode 100644 index 00000000000..b272267a517 --- /dev/null +++ b/apps/sim/tools/quickbooks/create_customer.ts @@ -0,0 +1,118 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksCreateCustomerParams, + QuickBooksCustomerResponse, +} from '@/tools/quickbooks/types' +import { CUSTOMER_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksCreateCustomer') + +export const quickbooksCreateCustomerTool: ToolConfig< + QuickBooksCreateCustomerParams, + QuickBooksCustomerResponse +> = { + id: 'quickbooks_create_customer', + name: 'QuickBooks Create Customer', + description: 'Create a new customer in QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + displayName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Display name (must be unique within the company)', + }, + companyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name', + }, + givenName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name', + }, + familyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name', + }, + primaryEmail: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Primary email address', + }, + primaryPhone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Primary phone number', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Free-form notes', + }, + }, + + request: { + url: (params) => `${buildCompanyUrl(params.realmId, '/customer')}?minorversion=73`, + method: 'POST', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + body: (params) => { + const body: Record = { + DisplayName: params.displayName, + } + if (params.companyName) body.CompanyName = params.companyName + if (params.givenName) body.GivenName = params.givenName + if (params.familyName) body.FamilyName = params.familyName + if (params.primaryEmail) body.PrimaryEmailAddr = { Address: params.primaryEmail } + if (params.primaryPhone) body.PrimaryPhone = { FreeFormNumber: params.primaryPhone } + if (params.notes) body.Notes = params.notes + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks create customer failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to create QuickBooks customer') + } + const customer = (data?.Customer ?? null) as Record | null + return { + success: true, + output: { + customer, + customerId: customer ? ((customer.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + customer: { type: 'object', description: 'Created customer', properties: CUSTOMER_OUTPUT }, + customerId: { type: 'string', description: 'New customer ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/create_invoice.ts b/apps/sim/tools/quickbooks/create_invoice.ts new file mode 100644 index 00000000000..c67304e014a --- /dev/null +++ b/apps/sim/tools/quickbooks/create_invoice.ts @@ -0,0 +1,153 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksCreateInvoiceParams, + QuickBooksInvoiceResponse, + QuickBooksLineItem, +} from '@/tools/quickbooks/types' +import { INVOICE_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksCreateInvoice') + +function coerceLines(input: QuickBooksCreateInvoiceParams['lines']): QuickBooksLineItem[] { + if (Array.isArray(input)) return input + if (typeof input !== 'string') { + throw new Error('Invoice lines must be a JSON array') + } + const parsed = JSON.parse(input) + if (!Array.isArray(parsed)) { + throw new Error('Invoice lines must be a JSON array') + } + return parsed as QuickBooksLineItem[] +} + +export const quickbooksCreateInvoiceTool: ToolConfig< + QuickBooksCreateInvoiceParams, + QuickBooksInvoiceResponse +> = { + id: 'quickbooks_create_invoice', + name: 'QuickBooks Create Invoice', + description: 'Create a new invoice in QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID to bill', + }, + lines: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Invoice line items (JSON array). Each entry: { description?, amount, quantity?, itemId?, itemName? }', + }, + txnDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Transaction date (YYYY-MM-DD)', + }, + dueDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Due date (YYYY-MM-DD)', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Memo shown to the customer on the invoice', + }, + billEmail: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address to bill (used by Send Invoice flows)', + }, + }, + + request: { + url: (params) => `${buildCompanyUrl(params.realmId, '/invoice')}?minorversion=73`, + method: 'POST', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + body: (params) => { + const lines = coerceLines(params.lines) + if (lines.length === 0) { + throw new Error('At least one invoice line is required') + } + + const Line = lines.map((line) => { + const amount = Number(line.amount) + if (!Number.isFinite(amount)) { + throw new Error('Each invoice line requires a numeric `amount`') + } + if (!line.itemId) { + throw new Error('Each invoice line requires `itemId`') + } + const itemRef = { + value: line.itemId, + ...(line.itemName ? { name: line.itemName } : {}), + } + const salesItemLineDetail: Record = { ItemRef: itemRef } + if (line.quantity !== undefined) salesItemLineDetail.Qty = Number(line.quantity) + + return { + DetailType: 'SalesItemLineDetail', + Amount: amount, + ...(line.description ? { Description: line.description } : {}), + SalesItemLineDetail: salesItemLineDetail, + } + }) + + const body: Record = { + CustomerRef: { value: params.customerId }, + Line, + } + if (params.txnDate) body.TxnDate = params.txnDate + if (params.dueDate) body.DueDate = params.dueDate + if (params.customerMemo) body.CustomerMemo = { value: params.customerMemo } + if (params.billEmail) body.BillEmail = { Address: params.billEmail } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks create invoice failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to create QuickBooks invoice') + } + const invoice = (data?.Invoice ?? null) as Record | null + return { + success: true, + output: { + invoice, + invoiceId: invoice ? ((invoice.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + invoice: { type: 'object', description: 'Created invoice', properties: INVOICE_OUTPUT }, + invoiceId: { type: 'string', description: 'New invoice ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/create_item.ts b/apps/sim/tools/quickbooks/create_item.ts new file mode 100644 index 00000000000..2802d27110d --- /dev/null +++ b/apps/sim/tools/quickbooks/create_item.ts @@ -0,0 +1,110 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksCreateItemParams, QuickBooksItemResponse } from '@/tools/quickbooks/types' +import { ITEM_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksCreateItem') + +const SUPPORTED_TYPES = new Set(['Service', 'NonInventory']) + +export const quickbooksCreateItemTool: ToolConfig< + QuickBooksCreateItemParams, + QuickBooksItemResponse +> = { + id: 'quickbooks_create_item', + name: 'QuickBooks Create Item', + description: 'Create a new item (product or service) in QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Item name (must be unique within the company)', + }, + type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Item type — Service or NonInventory (Inventory not supported in MVP)', + }, + incomeAccountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Income account ID to associate with the item', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Item description', + }, + unitPrice: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Unit price for the item', + }, + }, + + request: { + url: (params) => `${buildCompanyUrl(params.realmId, '/item')}?minorversion=73`, + method: 'POST', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + body: (params) => { + if (!SUPPORTED_TYPES.has(params.type)) { + throw new Error( + `QuickBooks item type '${params.type}' is not supported. Use 'Service' or 'NonInventory'. Inventory items require additional fields and are not supported in MVP.` + ) + } + const body: Record = { + Name: params.name, + Type: params.type, + IncomeAccountRef: { value: params.incomeAccountId }, + } + if (params.description) body.Description = params.description + if (params.unitPrice !== undefined && params.unitPrice !== null) { + body.UnitPrice = Number(params.unitPrice) + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks create item failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to create QuickBooks item') + } + const item = (data?.Item ?? null) as Record | null + return { + success: true, + output: { + item, + itemId: item ? ((item.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + item: { type: 'object', description: 'Created item', properties: ITEM_OUTPUT }, + itemId: { type: 'string', description: 'New item ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/create_payment.ts b/apps/sim/tools/quickbooks/create_payment.ts new file mode 100644 index 00000000000..e6a1c2f69a0 --- /dev/null +++ b/apps/sim/tools/quickbooks/create_payment.ts @@ -0,0 +1,124 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksCreatePaymentParams, + QuickBooksPaymentResponse, +} from '@/tools/quickbooks/types' +import { PAYMENT_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksCreatePayment') + +export const quickbooksCreatePaymentTool: ToolConfig< + QuickBooksCreatePaymentParams, + QuickBooksPaymentResponse +> = { + id: 'quickbooks_create_payment', + name: 'QuickBooks Create Payment', + description: 'Record a customer payment in QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID making the payment', + }, + amount: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Total payment amount', + }, + invoiceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional invoice ID to apply the payment to', + }, + paymentMethodId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment method ID', + }, + txnDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Transaction date (YYYY-MM-DD)', + }, + paymentRefNum: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment reference number (e.g., check number)', + }, + }, + + request: { + url: (params) => `${buildCompanyUrl(params.realmId, '/payment')}?minorversion=73`, + method: 'POST', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + body: (params) => { + const amount = Number(params.amount) + if (!Number.isFinite(amount)) { + throw new Error('Payment amount must be a number') + } + const body: Record = { + CustomerRef: { value: params.customerId }, + TotalAmt: amount, + } + if (params.txnDate) body.TxnDate = params.txnDate + if (params.paymentRefNum) body.PaymentRefNum = params.paymentRefNum + if (params.paymentMethodId) { + body.PaymentMethodRef = { value: params.paymentMethodId } + } + if (params.invoiceId) { + body.Line = [ + { + Amount: amount, + LinkedTxn: [{ TxnId: params.invoiceId, TxnType: 'Invoice' }], + }, + ] + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks create payment failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to create QuickBooks payment') + } + const payment = (data?.Payment ?? null) as Record | null + return { + success: true, + output: { + payment, + paymentId: payment ? ((payment.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + payment: { type: 'object', description: 'Created payment', properties: PAYMENT_OUTPUT }, + paymentId: { type: 'string', description: 'New payment ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/create_vendor.ts b/apps/sim/tools/quickbooks/create_vendor.ts new file mode 100644 index 00000000000..09d017f0f7d --- /dev/null +++ b/apps/sim/tools/quickbooks/create_vendor.ts @@ -0,0 +1,111 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksCreateVendorParams, + QuickBooksVendorResponse, +} from '@/tools/quickbooks/types' +import { VENDOR_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksCreateVendor') + +export const quickbooksCreateVendorTool: ToolConfig< + QuickBooksCreateVendorParams, + QuickBooksVendorResponse +> = { + id: 'quickbooks_create_vendor', + name: 'QuickBooks Create Vendor', + description: 'Create a new vendor in QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + displayName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Display name (must be unique within the company)', + }, + companyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name', + }, + givenName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name', + }, + familyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name', + }, + primaryEmail: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Primary email address', + }, + primaryPhone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Primary phone number', + }, + }, + + request: { + url: (params) => `${buildCompanyUrl(params.realmId, '/vendor')}?minorversion=73`, + method: 'POST', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + body: (params) => { + const body: Record = { + DisplayName: params.displayName, + } + if (params.companyName) body.CompanyName = params.companyName + if (params.givenName) body.GivenName = params.givenName + if (params.familyName) body.FamilyName = params.familyName + if (params.primaryEmail) body.PrimaryEmailAddr = { Address: params.primaryEmail } + if (params.primaryPhone) body.PrimaryPhone = { FreeFormNumber: params.primaryPhone } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks create vendor failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to create QuickBooks vendor') + } + const vendor = (data?.Vendor ?? null) as Record | null + return { + success: true, + output: { + vendor, + vendorId: vendor ? ((vendor.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + vendor: { type: 'object', description: 'Created vendor', properties: VENDOR_OUTPUT }, + vendorId: { type: 'string', description: 'New vendor ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/get_company_info.ts b/apps/sim/tools/quickbooks/get_company_info.ts new file mode 100644 index 00000000000..5957c9d9b87 --- /dev/null +++ b/apps/sim/tools/quickbooks/get_company_info.ts @@ -0,0 +1,62 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksBaseParams, QuickBooksCompanyInfoResponse } from '@/tools/quickbooks/types' +import { COMPANY_INFO_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksGetCompanyInfo') + +export const quickbooksGetCompanyInfoTool: ToolConfig< + QuickBooksBaseParams, + QuickBooksCompanyInfoResponse +> = { + id: 'quickbooks_get_company_info', + name: 'QuickBooks Get Company Info', + description: 'Retrieve company information for the connected QuickBooks Online company', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + }, + + request: { + url: (params) => + `${buildCompanyUrl(params.realmId, `/companyinfo/${encodeURIComponent(params.realmId)}`)}?minorversion=73`, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks get company info failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to get QuickBooks company info') + } + const companyInfo = (data?.CompanyInfo ?? null) as Record | null + return { + success: true, + output: { companyInfo }, + } + }, + + outputs: { + companyInfo: { + type: 'object', + description: 'Company information record', + properties: COMPANY_INFO_OUTPUT, + }, + }, +} diff --git a/apps/sim/tools/quickbooks/get_customer.ts b/apps/sim/tools/quickbooks/get_customer.ts new file mode 100644 index 00000000000..8588c3d5e05 --- /dev/null +++ b/apps/sim/tools/quickbooks/get_customer.ts @@ -0,0 +1,71 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksCustomerResponse, + QuickBooksGetCustomerParams, +} from '@/tools/quickbooks/types' +import { CUSTOMER_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksGetCustomer') + +export const quickbooksGetCustomerTool: ToolConfig< + QuickBooksGetCustomerParams, + QuickBooksCustomerResponse +> = { + id: 'quickbooks_get_customer', + name: 'QuickBooks Get Customer', + description: 'Retrieve a single QuickBooks customer by ID', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'QuickBooks customer ID', + }, + }, + + request: { + url: (params) => + `${buildCompanyUrl(params.realmId, `/customer/${encodeURIComponent(params.customerId.trim())}`)}?minorversion=73`, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks get customer failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to get QuickBooks customer') + } + const customer = (data?.Customer ?? null) as Record | null + return { + success: true, + output: { + customer, + customerId: customer ? ((customer.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + customer: { type: 'object', description: 'Customer record', properties: CUSTOMER_OUTPUT }, + customerId: { type: 'string', description: 'Customer ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/get_invoice.ts b/apps/sim/tools/quickbooks/get_invoice.ts new file mode 100644 index 00000000000..3e7ac69c763 --- /dev/null +++ b/apps/sim/tools/quickbooks/get_invoice.ts @@ -0,0 +1,71 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksGetInvoiceParams, + QuickBooksInvoiceResponse, +} from '@/tools/quickbooks/types' +import { INVOICE_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksGetInvoice') + +export const quickbooksGetInvoiceTool: ToolConfig< + QuickBooksGetInvoiceParams, + QuickBooksInvoiceResponse +> = { + id: 'quickbooks_get_invoice', + name: 'QuickBooks Get Invoice', + description: 'Retrieve a single QuickBooks invoice by ID', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + invoiceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'QuickBooks invoice ID', + }, + }, + + request: { + url: (params) => + `${buildCompanyUrl(params.realmId, `/invoice/${encodeURIComponent(params.invoiceId.trim())}`)}?minorversion=73`, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks get invoice failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to get QuickBooks invoice') + } + const invoice = (data?.Invoice ?? null) as Record | null + return { + success: true, + output: { + invoice, + invoiceId: invoice ? ((invoice.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + invoice: { type: 'object', description: 'Invoice record', properties: INVOICE_OUTPUT }, + invoiceId: { type: 'string', description: 'Invoice ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/get_vendor.ts b/apps/sim/tools/quickbooks/get_vendor.ts new file mode 100644 index 00000000000..29d6f84cf49 --- /dev/null +++ b/apps/sim/tools/quickbooks/get_vendor.ts @@ -0,0 +1,68 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksGetVendorParams, QuickBooksVendorResponse } from '@/tools/quickbooks/types' +import { VENDOR_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksGetVendor') + +export const quickbooksGetVendorTool: ToolConfig< + QuickBooksGetVendorParams, + QuickBooksVendorResponse +> = { + id: 'quickbooks_get_vendor', + name: 'QuickBooks Get Vendor', + description: 'Retrieve a single QuickBooks vendor by ID', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + vendorId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'QuickBooks vendor ID', + }, + }, + + request: { + url: (params) => + `${buildCompanyUrl(params.realmId, `/vendor/${encodeURIComponent(params.vendorId.trim())}`)}?minorversion=73`, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks get vendor failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to get QuickBooks vendor') + } + const vendor = (data?.Vendor ?? null) as Record | null + return { + success: true, + output: { + vendor, + vendorId: vendor ? ((vendor.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + vendor: { type: 'object', description: 'Vendor record', properties: VENDOR_OUTPUT }, + vendorId: { type: 'string', description: 'Vendor ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/index.ts b/apps/sim/tools/quickbooks/index.ts new file mode 100644 index 00000000000..190be418366 --- /dev/null +++ b/apps/sim/tools/quickbooks/index.ts @@ -0,0 +1,22 @@ +export { quickbooksCreateBillTool } from '@/tools/quickbooks/create_bill' +export { quickbooksCreateCustomerTool } from '@/tools/quickbooks/create_customer' +export { quickbooksCreateInvoiceTool } from '@/tools/quickbooks/create_invoice' +export { quickbooksCreateItemTool } from '@/tools/quickbooks/create_item' +export { quickbooksCreatePaymentTool } from '@/tools/quickbooks/create_payment' +export { quickbooksCreateVendorTool } from '@/tools/quickbooks/create_vendor' +export { quickbooksGetCompanyInfoTool } from '@/tools/quickbooks/get_company_info' +export { quickbooksGetCustomerTool } from '@/tools/quickbooks/get_customer' +export { quickbooksGetInvoiceTool } from '@/tools/quickbooks/get_invoice' +export { quickbooksGetVendorTool } from '@/tools/quickbooks/get_vendor' +export { quickbooksListAccountsTool } from '@/tools/quickbooks/list_accounts' +export { quickbooksListBillsTool } from '@/tools/quickbooks/list_bills' +export { quickbooksListCustomersTool } from '@/tools/quickbooks/list_customers' +export { quickbooksListEstimatesTool } from '@/tools/quickbooks/list_estimates' +export { quickbooksListInvoicesTool } from '@/tools/quickbooks/list_invoices' +export { quickbooksListItemsTool } from '@/tools/quickbooks/list_items' +export { quickbooksListPaymentsTool } from '@/tools/quickbooks/list_payments' +export { quickbooksListVendorsTool } from '@/tools/quickbooks/list_vendors' +export { quickbooksQueryTool } from '@/tools/quickbooks/query' +export { quickbooksSendInvoiceTool } from '@/tools/quickbooks/send_invoice' +export { quickbooksUpdateCustomerTool } from '@/tools/quickbooks/update_customer' +export { quickbooksUpdateInvoiceTool } from '@/tools/quickbooks/update_invoice' diff --git a/apps/sim/tools/quickbooks/list_accounts.ts b/apps/sim/tools/quickbooks/list_accounts.ts new file mode 100644 index 00000000000..7fef6a4b8c8 --- /dev/null +++ b/apps/sim/tools/quickbooks/list_accounts.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksAccountListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' +import { ACCOUNT_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksListAccounts') + +export const quickbooksListAccountsTool: ToolConfig< + QuickBooksListParams, + QuickBooksAccountListResponse +> = { + id: 'quickbooks_list_accounts', + name: 'QuickBooks List Accounts', + description: 'List chart-of-accounts entries from QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of accounts to return (default 100, max 1000)', + }, + startPosition: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination start position (1-indexed)', + }, + where: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional WHERE clause (e.g., "AccountType = \'Income\'")', + }, + }, + + request: { + url: (params) => { + const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) + const start = Math.max(Number(params.startPosition) || 1, 1) + const whereClause = params.where ? ` WHERE ${params.where}` : '' + const sql = `SELECT * FROM Account${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` + const url = buildCompanyUrl(params.realmId, '/query') + return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` + }, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks list accounts failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to list QuickBooks accounts') + } + const accounts = (data?.QueryResponse?.Account ?? []) as Record[] + const reportedTotal = data?.QueryResponse?.totalCount + const totalCount = typeof reportedTotal === 'number' ? reportedTotal : accounts.length + return { + success: true, + output: { accounts, totalCount }, + } + }, + + outputs: { + accounts: { + type: 'array', + description: 'Array of accounts', + items: { type: 'object', properties: ACCOUNT_OUTPUT }, + }, + totalCount: { type: 'number', description: 'Number of accounts returned' }, + }, +} diff --git a/apps/sim/tools/quickbooks/list_bills.ts b/apps/sim/tools/quickbooks/list_bills.ts new file mode 100644 index 00000000000..444efd29e79 --- /dev/null +++ b/apps/sim/tools/quickbooks/list_bills.ts @@ -0,0 +1,87 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksBillListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' +import { BILL_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksListBills') + +export const quickbooksListBillsTool: ToolConfig = + { + id: 'quickbooks_list_bills', + name: 'QuickBooks List Bills', + description: 'List bills from QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of bills to return (default 100, max 1000)', + }, + startPosition: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination start position (1-indexed)', + }, + where: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional WHERE clause', + }, + }, + + request: { + url: (params) => { + const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) + const start = Math.max(Number(params.startPosition) || 1, 1) + const whereClause = params.where ? ` WHERE ${params.where}` : '' + const sql = `SELECT * FROM Bill${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` + const url = buildCompanyUrl(params.realmId, '/query') + return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` + }, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks list bills failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to list QuickBooks bills') + } + const bills = (data?.QueryResponse?.Bill ?? []) as Record[] + const reportedTotal = data?.QueryResponse?.totalCount + const totalCount = typeof reportedTotal === 'number' ? reportedTotal : bills.length + return { + success: true, + output: { bills, totalCount }, + } + }, + + outputs: { + bills: { + type: 'array', + description: 'Array of bills', + items: { type: 'object', properties: BILL_OUTPUT }, + }, + totalCount: { type: 'number', description: 'Number of bills returned' }, + }, + } diff --git a/apps/sim/tools/quickbooks/list_customers.ts b/apps/sim/tools/quickbooks/list_customers.ts new file mode 100644 index 00000000000..24e906dcedb --- /dev/null +++ b/apps/sim/tools/quickbooks/list_customers.ts @@ -0,0 +1,92 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksCustomerListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' +import { CUSTOMER_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksListCustomers') + +export const quickbooksListCustomersTool: ToolConfig< + QuickBooksListParams, + QuickBooksCustomerListResponse +> = { + id: 'quickbooks_list_customers', + name: 'QuickBooks List Customers', + description: 'List customers from QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of customers to return (default 100, max 1000)', + }, + startPosition: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination start position (1-indexed)', + }, + where: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional WHERE clause (e.g., "Active = true AND DisplayName LIKE \'A%\'")', + }, + }, + + request: { + url: (params) => { + const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) + const start = Math.max(Number(params.startPosition) || 1, 1) + const whereClause = params.where ? ` WHERE ${params.where}` : '' + const sql = `SELECT * FROM Customer${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` + const url = buildCompanyUrl(params.realmId, '/query') + return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` + }, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks list customers failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to list QuickBooks customers') + } + const customers = (data?.QueryResponse?.Customer ?? []) as Record[] + const reportedTotal = data?.QueryResponse?.totalCount + const totalCount = typeof reportedTotal === 'number' ? reportedTotal : customers.length + return { + success: true, + output: { + customers, + totalCount, + }, + } + }, + + outputs: { + customers: { + type: 'array', + description: 'Array of QuickBooks customers', + items: { type: 'object', properties: CUSTOMER_OUTPUT }, + }, + totalCount: { type: 'number', description: 'Number of customers returned' }, + }, +} diff --git a/apps/sim/tools/quickbooks/list_estimates.ts b/apps/sim/tools/quickbooks/list_estimates.ts new file mode 100644 index 00000000000..e8a9042ca2d --- /dev/null +++ b/apps/sim/tools/quickbooks/list_estimates.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksEstimateListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' +import { ESTIMATE_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksListEstimates') + +export const quickbooksListEstimatesTool: ToolConfig< + QuickBooksListParams, + QuickBooksEstimateListResponse +> = { + id: 'quickbooks_list_estimates', + name: 'QuickBooks List Estimates', + description: 'List estimates from QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of estimates to return (default 100, max 1000)', + }, + startPosition: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination start position (1-indexed)', + }, + where: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional WHERE clause', + }, + }, + + request: { + url: (params) => { + const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) + const start = Math.max(Number(params.startPosition) || 1, 1) + const whereClause = params.where ? ` WHERE ${params.where}` : '' + const sql = `SELECT * FROM Estimate${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` + const url = buildCompanyUrl(params.realmId, '/query') + return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` + }, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks list estimates failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to list QuickBooks estimates') + } + const estimates = (data?.QueryResponse?.Estimate ?? []) as Record[] + const reportedTotal = data?.QueryResponse?.totalCount + const totalCount = typeof reportedTotal === 'number' ? reportedTotal : estimates.length + return { + success: true, + output: { estimates, totalCount }, + } + }, + + outputs: { + estimates: { + type: 'array', + description: 'Array of estimates', + items: { type: 'object', properties: ESTIMATE_OUTPUT }, + }, + totalCount: { type: 'number', description: 'Number of estimates returned' }, + }, +} diff --git a/apps/sim/tools/quickbooks/list_invoices.ts b/apps/sim/tools/quickbooks/list_invoices.ts new file mode 100644 index 00000000000..954f2caba71 --- /dev/null +++ b/apps/sim/tools/quickbooks/list_invoices.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksInvoiceListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' +import { INVOICE_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksListInvoices') + +export const quickbooksListInvoicesTool: ToolConfig< + QuickBooksListParams, + QuickBooksInvoiceListResponse +> = { + id: 'quickbooks_list_invoices', + name: 'QuickBooks List Invoices', + description: 'List invoices from QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of invoices to return (default 100, max 1000)', + }, + startPosition: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination start position (1-indexed)', + }, + where: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional WHERE clause (e.g., "Balance > \'0\'")', + }, + }, + + request: { + url: (params) => { + const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) + const start = Math.max(Number(params.startPosition) || 1, 1) + const whereClause = params.where ? ` WHERE ${params.where}` : '' + const sql = `SELECT * FROM Invoice${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` + const url = buildCompanyUrl(params.realmId, '/query') + return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` + }, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks list invoices failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to list QuickBooks invoices') + } + const invoices = (data?.QueryResponse?.Invoice ?? []) as Record[] + const reportedTotal = data?.QueryResponse?.totalCount + const totalCount = typeof reportedTotal === 'number' ? reportedTotal : invoices.length + return { + success: true, + output: { invoices, totalCount }, + } + }, + + outputs: { + invoices: { + type: 'array', + description: 'Array of invoices', + items: { type: 'object', properties: INVOICE_OUTPUT }, + }, + totalCount: { type: 'number', description: 'Number of invoices returned' }, + }, +} diff --git a/apps/sim/tools/quickbooks/list_items.ts b/apps/sim/tools/quickbooks/list_items.ts new file mode 100644 index 00000000000..5030f05a5aa --- /dev/null +++ b/apps/sim/tools/quickbooks/list_items.ts @@ -0,0 +1,87 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksItemListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' +import { ITEM_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksListItems') + +export const quickbooksListItemsTool: ToolConfig = + { + id: 'quickbooks_list_items', + name: 'QuickBooks List Items', + description: 'List items (products and services) from QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of items to return (default 100, max 1000)', + }, + startPosition: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination start position (1-indexed)', + }, + where: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional WHERE clause (e.g., "Type = \'Service\'")', + }, + }, + + request: { + url: (params) => { + const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) + const start = Math.max(Number(params.startPosition) || 1, 1) + const whereClause = params.where ? ` WHERE ${params.where}` : '' + const sql = `SELECT * FROM Item${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` + const url = buildCompanyUrl(params.realmId, '/query') + return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` + }, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks list items failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to list QuickBooks items') + } + const items = (data?.QueryResponse?.Item ?? []) as Record[] + const reportedTotal = data?.QueryResponse?.totalCount + const totalCount = typeof reportedTotal === 'number' ? reportedTotal : items.length + return { + success: true, + output: { items, totalCount }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of items', + items: { type: 'object', properties: ITEM_OUTPUT }, + }, + totalCount: { type: 'number', description: 'Number of items returned' }, + }, + } diff --git a/apps/sim/tools/quickbooks/list_payments.ts b/apps/sim/tools/quickbooks/list_payments.ts new file mode 100644 index 00000000000..f9ec8107e7e --- /dev/null +++ b/apps/sim/tools/quickbooks/list_payments.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksListParams, QuickBooksPaymentListResponse } from '@/tools/quickbooks/types' +import { PAYMENT_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksListPayments') + +export const quickbooksListPaymentsTool: ToolConfig< + QuickBooksListParams, + QuickBooksPaymentListResponse +> = { + id: 'quickbooks_list_payments', + name: 'QuickBooks List Payments', + description: 'List payments from QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of payments to return (default 100, max 1000)', + }, + startPosition: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination start position (1-indexed)', + }, + where: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional WHERE clause', + }, + }, + + request: { + url: (params) => { + const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) + const start = Math.max(Number(params.startPosition) || 1, 1) + const whereClause = params.where ? ` WHERE ${params.where}` : '' + const sql = `SELECT * FROM Payment${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` + const url = buildCompanyUrl(params.realmId, '/query') + return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` + }, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks list payments failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to list QuickBooks payments') + } + const payments = (data?.QueryResponse?.Payment ?? []) as Record[] + const reportedTotal = data?.QueryResponse?.totalCount + const totalCount = typeof reportedTotal === 'number' ? reportedTotal : payments.length + return { + success: true, + output: { payments, totalCount }, + } + }, + + outputs: { + payments: { + type: 'array', + description: 'Array of payments', + items: { type: 'object', properties: PAYMENT_OUTPUT }, + }, + totalCount: { type: 'number', description: 'Number of payments returned' }, + }, +} diff --git a/apps/sim/tools/quickbooks/list_vendors.ts b/apps/sim/tools/quickbooks/list_vendors.ts new file mode 100644 index 00000000000..4499da9f4e8 --- /dev/null +++ b/apps/sim/tools/quickbooks/list_vendors.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksListParams, QuickBooksVendorListResponse } from '@/tools/quickbooks/types' +import { VENDOR_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksListVendors') + +export const quickbooksListVendorsTool: ToolConfig< + QuickBooksListParams, + QuickBooksVendorListResponse +> = { + id: 'quickbooks_list_vendors', + name: 'QuickBooks List Vendors', + description: 'List vendors from QuickBooks Online', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of vendors to return (default 100, max 1000)', + }, + startPosition: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination start position (1-indexed)', + }, + where: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional WHERE clause (e.g., "Active = true")', + }, + }, + + request: { + url: (params) => { + const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) + const start = Math.max(Number(params.startPosition) || 1, 1) + const whereClause = params.where ? ` WHERE ${params.where}` : '' + const sql = `SELECT * FROM Vendor${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` + const url = buildCompanyUrl(params.realmId, '/query') + return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` + }, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks list vendors failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to list QuickBooks vendors') + } + const vendors = (data?.QueryResponse?.Vendor ?? []) as Record[] + const reportedTotal = data?.QueryResponse?.totalCount + const totalCount = typeof reportedTotal === 'number' ? reportedTotal : vendors.length + return { + success: true, + output: { vendors, totalCount }, + } + }, + + outputs: { + vendors: { + type: 'array', + description: 'Array of vendors', + items: { type: 'object', properties: VENDOR_OUTPUT }, + }, + totalCount: { type: 'number', description: 'Number of vendors returned' }, + }, +} diff --git a/apps/sim/tools/quickbooks/query.ts b/apps/sim/tools/quickbooks/query.ts new file mode 100644 index 00000000000..0a2186a559e --- /dev/null +++ b/apps/sim/tools/quickbooks/query.ts @@ -0,0 +1,76 @@ +import { createLogger } from '@sim/logger' +import type { QuickBooksQueryParams, QuickBooksQueryResponse } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksQuery') + +export const quickbooksQueryTool: ToolConfig = { + id: 'quickbooks_query', + name: 'QuickBooks Query', + description: + 'Run a QuickBooks Online query (SQL-like syntax, e.g., "SELECT * FROM Item WHERE Active = true")', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'QuickBooks Query Language statement (e.g., "SELECT * FROM Vendor WHERE Active = true MAXRESULTS 50")', + }, + }, + + request: { + url: (params) => { + const url = buildCompanyUrl(params.realmId, '/query') + return `${url}?query=${encodeURIComponent(params.query)}&minorversion=73` + }, + method: 'GET', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks query failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'QuickBooks query failed') + } + const queryResponse = (data?.QueryResponse ?? {}) as Record + const reportedTotal = queryResponse.totalCount + const totalCount = + typeof reportedTotal === 'number' + ? reportedTotal + : Object.values(queryResponse).reduce( + (sum, value) => sum + (Array.isArray(value) ? value.length : 0), + 0 + ) + return { + success: true, + output: { + results: queryResponse, + totalCount, + }, + } + }, + + outputs: { + results: { type: 'json', description: 'Raw QueryResponse object from QuickBooks' }, + totalCount: { type: 'number', description: 'Reported total count' }, + }, +} diff --git a/apps/sim/tools/quickbooks/send_invoice.ts b/apps/sim/tools/quickbooks/send_invoice.ts new file mode 100644 index 00000000000..23653b87153 --- /dev/null +++ b/apps/sim/tools/quickbooks/send_invoice.ts @@ -0,0 +1,84 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksInvoiceResponse, + QuickBooksSendInvoiceParams, +} from '@/tools/quickbooks/types' +import { INVOICE_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksSendInvoice') + +export const quickbooksSendInvoiceTool: ToolConfig< + QuickBooksSendInvoiceParams, + QuickBooksInvoiceResponse +> = { + id: 'quickbooks_send_invoice', + name: 'QuickBooks Send Invoice', + description: 'Email a QuickBooks invoice to a recipient', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + invoiceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID to send', + }, + sendTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address override; defaults to BillEmail on the invoice', + }, + }, + + request: { + url: (params) => { + const path = `/invoice/${encodeURIComponent(params.invoiceId.trim())}/send` + const base = `${buildCompanyUrl(params.realmId, path)}?minorversion=73` + return params.sendTo ? `${base}&sendTo=${encodeURIComponent(params.sendTo)}` : base + }, + method: 'POST', + headers: (params) => ({ + ...quickbooksAuthHeaders(params.accessToken), + 'Content-Type': 'application/octet-stream', + }), + body: () => ({}), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks send invoice failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to send QuickBooks invoice') + } + const invoice = (data?.Invoice ?? null) as Record | null + return { + success: true, + output: { + invoice, + invoiceId: invoice ? ((invoice.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + invoice: { type: 'object', description: 'Invoice after send', properties: INVOICE_OUTPUT }, + invoiceId: { type: 'string', description: 'Invoice ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/types.ts b/apps/sim/tools/quickbooks/types.ts new file mode 100644 index 00000000000..5a711c5e1a0 --- /dev/null +++ b/apps/sim/tools/quickbooks/types.ts @@ -0,0 +1,429 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +export interface QuickBooksBaseParams { + accessToken: string + realmId: string +} + +export interface QuickBooksListParams extends QuickBooksBaseParams { + maxResults?: number + startPosition?: number + where?: string +} + +export interface QuickBooksGetCustomerParams extends QuickBooksBaseParams { + customerId: string +} + +export interface QuickBooksCreateCustomerParams extends QuickBooksBaseParams { + displayName: string + companyName?: string + givenName?: string + familyName?: string + primaryEmail?: string + primaryPhone?: string + notes?: string +} + +export interface QuickBooksGetInvoiceParams extends QuickBooksBaseParams { + invoiceId: string +} + +export interface QuickBooksLineItem { + description?: string + amount: number + quantity?: number + itemId?: string + itemName?: string +} + +export interface QuickBooksCreateInvoiceParams extends QuickBooksBaseParams { + customerId: string + lines: QuickBooksLineItem[] | string + dueDate?: string + txnDate?: string + customerMemo?: string + billEmail?: string +} + +export interface QuickBooksQueryParams extends QuickBooksBaseParams { + query: string +} + +export interface QuickBooksResponse extends ToolResponse { + output: Record +} + +export interface QuickBooksCustomerResponse extends ToolResponse { + output: { + customer: Record | null + customerId: string | null + } +} + +export interface QuickBooksCustomerListResponse extends ToolResponse { + output: { + customers: Record[] + totalCount: number + } +} + +export interface QuickBooksInvoiceResponse extends ToolResponse { + output: { + invoice: Record | null + invoiceId: string | null + } +} + +export interface QuickBooksInvoiceListResponse extends ToolResponse { + output: { + invoices: Record[] + totalCount: number + } +} + +export interface QuickBooksAccountListResponse extends ToolResponse { + output: { + accounts: Record[] + totalCount: number + } +} + +export interface QuickBooksQueryResponse extends ToolResponse { + output: { + results: Record + totalCount: number + } +} + +/** + * Common Customer fields returned by the QuickBooks Online accounting API. + * @see https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer + */ +export const CUSTOMER_OUTPUT: Record = { + Id: { type: 'string', description: 'QuickBooks customer ID' }, + DisplayName: { type: 'string', description: 'Display name shown in lists and forms' }, + CompanyName: { type: 'string', description: 'Company name' }, + GivenName: { type: 'string', description: 'First name' }, + FamilyName: { type: 'string', description: 'Last name' }, + PrimaryEmailAddr: { type: 'object', description: 'Primary email object with `Address`' }, + PrimaryPhone: { type: 'object', description: 'Primary phone object with `FreeFormNumber`' }, + Active: { type: 'boolean', description: 'Whether the customer is active' }, + Balance: { type: 'number', description: 'Open balance owed by the customer' }, + CurrencyRef: { type: 'object', description: 'Currency reference (`value`, `name`)' }, + Notes: { type: 'string', description: 'Free-form notes' }, + MetaData: { type: 'object', description: 'Create/update timestamps' }, + SyncToken: { + type: 'string', + description: 'Optimistic concurrency token; required for sparse updates', + optional: true, + }, +} as const satisfies Record + +export const INVOICE_OUTPUT: Record = { + Id: { type: 'string', description: 'QuickBooks invoice ID' }, + DocNumber: { type: 'string', description: 'Invoice document number' }, + TxnDate: { type: 'string', description: 'Invoice transaction date (YYYY-MM-DD)' }, + DueDate: { type: 'string', description: 'Invoice due date (YYYY-MM-DD)' }, + CustomerRef: { type: 'object', description: 'Reference to the customer (`value`, `name`)' }, + Line: { type: 'array', description: 'Invoice line items' }, + TotalAmt: { type: 'number', description: 'Total amount of the invoice' }, + Balance: { type: 'number', description: 'Outstanding balance on the invoice' }, + CurrencyRef: { type: 'object', description: 'Currency reference' }, + CustomerMemo: { type: 'object', description: 'Customer-facing memo' }, + BillEmail: { type: 'object', description: 'Billing email object' }, + EmailStatus: { type: 'string', description: 'Email send status (NotSet, NeedToSend, EmailSent)' }, + MetaData: { type: 'object', description: 'Create/update timestamps' }, + SyncToken: { + type: 'string', + description: 'Optimistic concurrency token; required for sparse updates', + optional: true, + }, +} as const satisfies Record + +export const ACCOUNT_OUTPUT: Record = { + Id: { type: 'string', description: 'QuickBooks account ID' }, + Name: { type: 'string', description: 'Account name' }, + AccountType: { type: 'string', description: 'High-level account type (Income, Expense, etc.)' }, + AccountSubType: { type: 'string', description: 'Account sub-type' }, + Classification: { type: 'string', description: 'Asset, Liability, Equity, Revenue, or Expense' }, + CurrentBalance: { type: 'number', description: 'Current account balance' }, + Active: { type: 'boolean', description: 'Whether the account is active' }, + CurrencyRef: { type: 'object', description: 'Currency reference' }, + SyncToken: { + type: 'string', + description: 'Optimistic concurrency token; required for sparse updates', + optional: true, + }, +} as const satisfies Record + +export interface QuickBooksGetVendorParams extends QuickBooksBaseParams { + vendorId: string +} + +export interface QuickBooksCreateVendorParams extends QuickBooksBaseParams { + displayName: string + companyName?: string + givenName?: string + familyName?: string + primaryEmail?: string + primaryPhone?: string +} + +export interface QuickBooksVendorResponse extends ToolResponse { + output: { + vendor: Record | null + vendorId: string | null + } +} + +export interface QuickBooksVendorListResponse extends ToolResponse { + output: { + vendors: Record[] + totalCount: number + } +} + +export interface QuickBooksGetItemParams extends QuickBooksBaseParams { + itemId: string +} + +export interface QuickBooksCreateItemParams extends QuickBooksBaseParams { + name: string + type: string + incomeAccountId: string + description?: string + unitPrice?: number +} + +export interface QuickBooksItemResponse extends ToolResponse { + output: { + item: Record | null + itemId: string | null + } +} + +export interface QuickBooksItemListResponse extends ToolResponse { + output: { + items: Record[] + totalCount: number + } +} + +export interface QuickBooksGetPaymentParams extends QuickBooksBaseParams { + paymentId: string +} + +export interface QuickBooksCreatePaymentParams extends QuickBooksBaseParams { + customerId: string + amount: number + invoiceId?: string + paymentMethodId?: string + txnDate?: string + paymentRefNum?: string +} + +export interface QuickBooksPaymentResponse extends ToolResponse { + output: { + payment: Record | null + paymentId: string | null + } +} + +export interface QuickBooksPaymentListResponse extends ToolResponse { + output: { + payments: Record[] + totalCount: number + } +} + +export interface QuickBooksGetBillParams extends QuickBooksBaseParams { + billId: string +} + +export interface QuickBooksBillLine { + amount: number + accountId: string + description?: string +} + +export interface QuickBooksCreateBillParams extends QuickBooksBaseParams { + vendorId: string + lines: QuickBooksBillLine[] | string + txnDate?: string + dueDate?: string +} + +export interface QuickBooksBillResponse extends ToolResponse { + output: { + bill: Record | null + billId: string | null + } +} + +export interface QuickBooksBillListResponse extends ToolResponse { + output: { + bills: Record[] + totalCount: number + } +} + +export interface QuickBooksEstimateListResponse extends ToolResponse { + output: { + estimates: Record[] + totalCount: number + } +} + +export interface QuickBooksUpdateCustomerParams extends QuickBooksBaseParams { + customerId: string + syncToken: string + displayName?: string + companyName?: string + givenName?: string + familyName?: string + primaryEmail?: string + primaryPhone?: string + notes?: string +} + +export interface QuickBooksUpdateInvoiceParams extends QuickBooksBaseParams { + invoiceId: string + syncToken: string + dueDate?: string + customerMemo?: string + billEmail?: string +} + +export interface QuickBooksSendInvoiceParams extends QuickBooksBaseParams { + invoiceId: string + sendTo?: string +} + +export interface QuickBooksCompanyInfoResponse extends ToolResponse { + output: { + companyInfo: Record | null + } +} + +export const VENDOR_OUTPUT: Record = { + Id: { type: 'string', description: 'QuickBooks vendor ID' }, + DisplayName: { type: 'string', description: 'Display name', optional: true }, + CompanyName: { type: 'string', description: 'Company name', optional: true }, + GivenName: { type: 'string', description: 'First name', optional: true }, + FamilyName: { type: 'string', description: 'Last name', optional: true }, + PrimaryEmailAddr: { + type: 'object', + description: 'Primary email object with `Address`', + optional: true, + }, + PrimaryPhone: { + type: 'object', + description: 'Primary phone object with `FreeFormNumber`', + optional: true, + }, + Active: { type: 'boolean', description: 'Whether the vendor is active', optional: true }, + Balance: { type: 'number', description: 'Open balance owed to the vendor', optional: true }, + CurrencyRef: { type: 'object', description: 'Currency reference', optional: true }, + MetaData: { type: 'object', description: 'Create/update timestamps', optional: true }, + SyncToken: { + type: 'string', + description: 'Optimistic concurrency token; required for sparse updates', + optional: true, + }, +} as const satisfies Record + +export const ITEM_OUTPUT: Record = { + Id: { type: 'string', description: 'QuickBooks item ID' }, + Name: { type: 'string', description: 'Item name', optional: true }, + Description: { type: 'string', description: 'Item description', optional: true }, + Type: { + type: 'string', + description: 'Item type (Service, Inventory, NonInventory)', + optional: true, + }, + Active: { type: 'boolean', description: 'Whether the item is active', optional: true }, + UnitPrice: { type: 'number', description: 'Unit price', optional: true }, + IncomeAccountRef: { type: 'object', description: 'Income account reference', optional: true }, + ExpenseAccountRef: { type: 'object', description: 'Expense account reference', optional: true }, + AssetAccountRef: { type: 'object', description: 'Asset account reference', optional: true }, + QtyOnHand: { type: 'number', description: 'Quantity on hand (Inventory only)', optional: true }, + MetaData: { type: 'object', description: 'Create/update timestamps', optional: true }, + SyncToken: { + type: 'string', + description: 'Optimistic concurrency token; required for sparse updates', + optional: true, + }, +} as const satisfies Record + +export const PAYMENT_OUTPUT: Record = { + Id: { type: 'string', description: 'QuickBooks payment ID' }, + TxnDate: { type: 'string', description: 'Transaction date (YYYY-MM-DD)', optional: true }, + CustomerRef: { type: 'object', description: 'Customer reference', optional: true }, + TotalAmt: { type: 'number', description: 'Total payment amount', optional: true }, + UnappliedAmt: { type: 'number', description: 'Unapplied amount', optional: true }, + PaymentMethodRef: { type: 'object', description: 'Payment method reference', optional: true }, + PaymentRefNum: { type: 'string', description: 'Payment reference number', optional: true }, + Line: { type: 'array', description: 'Payment line items / linked transactions', optional: true }, + CurrencyRef: { type: 'object', description: 'Currency reference', optional: true }, + MetaData: { type: 'object', description: 'Create/update timestamps', optional: true }, + SyncToken: { + type: 'string', + description: 'Optimistic concurrency token; required for sparse updates', + optional: true, + }, +} as const satisfies Record + +export const BILL_OUTPUT: Record = { + Id: { type: 'string', description: 'QuickBooks bill ID' }, + VendorRef: { type: 'object', description: 'Vendor reference', optional: true }, + TxnDate: { type: 'string', description: 'Transaction date (YYYY-MM-DD)', optional: true }, + DueDate: { type: 'string', description: 'Due date (YYYY-MM-DD)', optional: true }, + TotalAmt: { type: 'number', description: 'Total bill amount', optional: true }, + Balance: { type: 'number', description: 'Outstanding balance', optional: true }, + Line: { type: 'array', description: 'Bill line items', optional: true }, + CurrencyRef: { type: 'object', description: 'Currency reference', optional: true }, + MetaData: { type: 'object', description: 'Create/update timestamps', optional: true }, + SyncToken: { + type: 'string', + description: 'Optimistic concurrency token; required for sparse updates', + optional: true, + }, +} as const satisfies Record + +export const ESTIMATE_OUTPUT: Record = { + Id: { type: 'string', description: 'QuickBooks estimate ID' }, + DocNumber: { type: 'string', description: 'Estimate document number', optional: true }, + TxnDate: { type: 'string', description: 'Transaction date (YYYY-MM-DD)', optional: true }, + ExpirationDate: { type: 'string', description: 'Expiration date (YYYY-MM-DD)', optional: true }, + CustomerRef: { type: 'object', description: 'Customer reference', optional: true }, + Line: { type: 'array', description: 'Estimate line items', optional: true }, + TotalAmt: { type: 'number', description: 'Total estimate amount', optional: true }, + TxnStatus: { + type: 'string', + description: 'Estimate status (Pending, Accepted, etc.)', + optional: true, + }, + EmailStatus: { type: 'string', description: 'Email send status', optional: true }, + MetaData: { type: 'object', description: 'Create/update timestamps', optional: true }, + SyncToken: { + type: 'string', + description: 'Optimistic concurrency token; required for sparse updates', + optional: true, + }, +} as const satisfies Record + +export const COMPANY_INFO_OUTPUT: Record = { + Id: { type: 'string', description: 'QuickBooks company ID (realmId)' }, + CompanyName: { type: 'string', description: 'Company name', optional: true }, + LegalName: { type: 'string', description: 'Legal name', optional: true }, + CompanyAddr: { type: 'object', description: 'Company address', optional: true }, + CompanyStartDate: { type: 'string', description: 'Company start date', optional: true }, + FiscalYearStartMonth: { type: 'string', description: 'Fiscal year start month', optional: true }, + Country: { type: 'string', description: 'Country code', optional: true }, + Email: { type: 'object', description: 'Primary email object', optional: true }, + WebAddr: { type: 'object', description: 'Web address', optional: true }, + SupportedLanguages: { type: 'string', description: 'Supported languages', optional: true }, + NameValue: { type: 'array', description: 'Custom name/value pairs', optional: true }, + MetaData: { type: 'object', description: 'Create/update timestamps', optional: true }, +} as const satisfies Record diff --git a/apps/sim/tools/quickbooks/update_customer.ts b/apps/sim/tools/quickbooks/update_customer.ts new file mode 100644 index 00000000000..b1cf1f18174 --- /dev/null +++ b/apps/sim/tools/quickbooks/update_customer.ts @@ -0,0 +1,142 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksCustomerResponse, + QuickBooksUpdateCustomerParams, +} from '@/tools/quickbooks/types' +import { CUSTOMER_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksUpdateCustomer') + +export const quickbooksUpdateCustomerTool: ToolConfig< + QuickBooksUpdateCustomerParams, + QuickBooksCustomerResponse +> = { + id: 'quickbooks_update_customer', + name: 'QuickBooks Update Customer', + description: 'Sparse-update an existing QuickBooks customer', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID to update', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current SyncToken from the customer record (required for updates)', + }, + displayName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Display name', + }, + companyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name', + }, + givenName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name', + }, + familyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name', + }, + primaryEmail: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Primary email address', + }, + primaryPhone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Primary phone number', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Free-form notes', + }, + }, + + request: { + url: (params) => `${buildCompanyUrl(params.realmId, '/customer')}?minorversion=73`, + method: 'POST', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + body: (params) => { + const body: Record = { + Id: params.customerId, + SyncToken: params.syncToken, + sparse: true, + } + if (params.displayName !== undefined) body.DisplayName = params.displayName + if (params.companyName !== undefined) body.CompanyName = params.companyName + if (params.givenName !== undefined) body.GivenName = params.givenName + if (params.familyName !== undefined) body.FamilyName = params.familyName + if (params.primaryEmail !== undefined) { + body.PrimaryEmailAddr = { Address: params.primaryEmail } + } + if (params.primaryPhone !== undefined) { + body.PrimaryPhone = { FreeFormNumber: params.primaryPhone } + } + if (params.notes !== undefined) body.Notes = params.notes + if (Object.keys(body).length <= 3) { + throw new Error( + 'update_customer requires at least one field to update (e.g., displayName, primaryEmail, notes)' + ) + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks update customer failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to update QuickBooks customer') + } + const customer = (data?.Customer ?? null) as Record | null + return { + success: true, + output: { + customer, + customerId: customer ? ((customer.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + customer: { type: 'object', description: 'Updated customer', properties: CUSTOMER_OUTPUT }, + customerId: { type: 'string', description: 'Customer ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/update_invoice.ts b/apps/sim/tools/quickbooks/update_invoice.ts new file mode 100644 index 00000000000..70ab41e1e39 --- /dev/null +++ b/apps/sim/tools/quickbooks/update_invoice.ts @@ -0,0 +1,111 @@ +import { createLogger } from '@sim/logger' +import type { + QuickBooksInvoiceResponse, + QuickBooksUpdateInvoiceParams, +} from '@/tools/quickbooks/types' +import { INVOICE_OUTPUT } from '@/tools/quickbooks/types' +import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('QuickBooksUpdateInvoice') + +export const quickbooksUpdateInvoiceTool: ToolConfig< + QuickBooksUpdateInvoiceParams, + QuickBooksInvoiceResponse +> = { + id: 'quickbooks_update_invoice', + name: 'QuickBooks Update Invoice', + description: + 'Update an existing QuickBooks invoice (sparse update — date/memo/email only; line items cannot be modified)', + version: '1.0.0', + + oauth: { required: true, provider: 'quickbooks' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks OAuth access token', + }, + realmId: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'QuickBooks company ID (realmId) — captured at OAuth time', + }, + invoiceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID to update', + }, + syncToken: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current SyncToken from the invoice record (required for updates)', + }, + dueDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Due date (YYYY-MM-DD)', + }, + customerMemo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer-facing memo', + }, + billEmail: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Billing email address', + }, + }, + + request: { + url: (params) => `${buildCompanyUrl(params.realmId, '/invoice')}?minorversion=73`, + method: 'POST', + headers: (params) => quickbooksAuthHeaders(params.accessToken), + body: (params) => { + const body: Record = { + Id: params.invoiceId, + SyncToken: params.syncToken, + sparse: true, + } + if (params.dueDate !== undefined) body.DueDate = params.dueDate + if (params.customerMemo !== undefined) body.CustomerMemo = { value: params.customerMemo } + if (params.billEmail !== undefined) body.BillEmail = { Address: params.billEmail } + if (Object.keys(body).length <= 3) { + throw new Error( + 'update_invoice requires at least one field to update (dueDate, customerMemo, or billEmail)' + ) + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + logger.error('QuickBooks update invoice failed', { status: response.status, data }) + throw new Error(data?.Fault?.Error?.[0]?.Message || 'Failed to update QuickBooks invoice') + } + const invoice = (data?.Invoice ?? null) as Record | null + return { + success: true, + output: { + invoice, + invoiceId: invoice ? ((invoice.Id as string) ?? null) : null, + }, + } + }, + + outputs: { + invoice: { type: 'object', description: 'Updated invoice', properties: INVOICE_OUTPUT }, + invoiceId: { type: 'string', description: 'Invoice ID' }, + }, +} diff --git a/apps/sim/tools/quickbooks/utils.ts b/apps/sim/tools/quickbooks/utils.ts new file mode 100644 index 00000000000..9d2f0ebaeaa --- /dev/null +++ b/apps/sim/tools/quickbooks/utils.ts @@ -0,0 +1,32 @@ +import { env } from '@/lib/core/config/env' + +/** + * Build a QuickBooks Online API URL for a specific company (realmId). + * realmId is captured from the OAuth callback query string at sign-in time + * and surfaced to tools via the access-token route. + */ +export function getQuickBooksApiBaseUrl(): string { + return env.QUICKBOOKS_ENV === 'sandbox' + ? 'https://sandbox-quickbooks.api.intuit.com' + : 'https://quickbooks.api.intuit.com' +} + +export function buildCompanyUrl(realmId: string | undefined, path: string): string { + if (!realmId) { + throw new Error('QuickBooks realmId missing — reconnect the QuickBooks account') + } + const base = getQuickBooksApiBaseUrl() + const trimmed = path.startsWith('/') ? path : `/${path}` + return `${base}/v3/company/${realmId}${trimmed}` +} + +export function quickbooksAuthHeaders(accessToken: string | undefined): Record { + if (!accessToken) { + throw new Error('Missing QuickBooks access token') + } + return { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 9130ac52dee..8c5de8c488f 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2017,6 +2017,30 @@ import { } from '@/tools/profound' import { pulseParserTool, pulseParserV2Tool } from '@/tools/pulse' import { qdrantFetchTool, qdrantSearchTool, qdrantUpsertTool } from '@/tools/qdrant' +import { + quickbooksCreateBillTool, + quickbooksCreateCustomerTool, + quickbooksCreateInvoiceTool, + quickbooksCreateItemTool, + quickbooksCreatePaymentTool, + quickbooksCreateVendorTool, + quickbooksGetCompanyInfoTool, + quickbooksGetCustomerTool, + quickbooksGetInvoiceTool, + quickbooksGetVendorTool, + quickbooksListAccountsTool, + quickbooksListBillsTool, + quickbooksListCustomersTool, + quickbooksListEstimatesTool, + quickbooksListInvoicesTool, + quickbooksListItemsTool, + quickbooksListPaymentsTool, + quickbooksListVendorsTool, + quickbooksQueryTool, + quickbooksSendInvoiceTool, + quickbooksUpdateCustomerTool, + quickbooksUpdateInvoiceTool, +} from '@/tools/quickbooks' import { quiverImageToSvgTool, quiverListModelsTool, quiverTextToSvgTool } from '@/tools/quiver' import { rdsDeleteTool, @@ -4349,6 +4373,28 @@ export const tools: Record = { profound_visibility_report: profoundVisibilityReportTool, pulse_parser: pulseParserTool, pulse_parser_v2: pulseParserV2Tool, + quickbooks_create_bill: quickbooksCreateBillTool, + quickbooks_create_customer: quickbooksCreateCustomerTool, + quickbooks_create_invoice: quickbooksCreateInvoiceTool, + quickbooks_create_item: quickbooksCreateItemTool, + quickbooks_create_payment: quickbooksCreatePaymentTool, + quickbooks_create_vendor: quickbooksCreateVendorTool, + quickbooks_get_company_info: quickbooksGetCompanyInfoTool, + quickbooks_get_customer: quickbooksGetCustomerTool, + quickbooks_get_invoice: quickbooksGetInvoiceTool, + quickbooks_get_vendor: quickbooksGetVendorTool, + quickbooks_list_accounts: quickbooksListAccountsTool, + quickbooks_list_bills: quickbooksListBillsTool, + quickbooks_list_customers: quickbooksListCustomersTool, + quickbooks_list_estimates: quickbooksListEstimatesTool, + quickbooks_list_invoices: quickbooksListInvoicesTool, + quickbooks_list_items: quickbooksListItemsTool, + quickbooks_list_payments: quickbooksListPaymentsTool, + quickbooks_list_vendors: quickbooksListVendorsTool, + quickbooks_query: quickbooksQueryTool, + quickbooks_send_invoice: quickbooksSendInvoiceTool, + quickbooks_update_customer: quickbooksUpdateCustomerTool, + quickbooks_update_invoice: quickbooksUpdateInvoiceTool, quiver_image_to_svg: quiverImageToSvgTool, quiver_list_models: quiverListModelsTool, quiver_text_to_svg: quiverTextToSvgTool, From 425616e7e6fce9ec3ac3f6d3a28cfdd91df89610 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 12:31:32 -0700 Subject: [PATCH 2/6] fix(quickbooks): address PR review (stable account ID, where sanitization, send body, query description) --- .../docs/content/docs/en/tools/quickbooks.mdx | 2 +- .../integrations/data/integrations.json | 2 +- apps/sim/lib/auth/auth.ts | 2 +- apps/sim/tools/quickbooks/list_accounts.ts | 9 ++++++-- apps/sim/tools/quickbooks/list_bills.ts | 9 ++++++-- apps/sim/tools/quickbooks/list_customers.ts | 9 ++++++-- apps/sim/tools/quickbooks/list_estimates.ts | 9 ++++++-- apps/sim/tools/quickbooks/list_invoices.ts | 9 ++++++-- apps/sim/tools/quickbooks/list_items.ts | 9 ++++++-- apps/sim/tools/quickbooks/list_payments.ts | 9 ++++++-- apps/sim/tools/quickbooks/list_vendors.ts | 9 ++++++-- apps/sim/tools/quickbooks/query.ts | 2 +- apps/sim/tools/quickbooks/send_invoice.ts | 1 - apps/sim/tools/quickbooks/utils.ts | 21 +++++++++++++++++++ 14 files changed, 81 insertions(+), 21 deletions(-) diff --git a/apps/docs/content/docs/en/tools/quickbooks.mdx b/apps/docs/content/docs/en/tools/quickbooks.mdx index 062df3ffef4..8fe84903fb6 100644 --- a/apps/docs/content/docs/en/tools/quickbooks.mdx +++ b/apps/docs/content/docs/en/tools/quickbooks.mdx @@ -615,7 +615,7 @@ List vendors from QuickBooks Online ### `quickbooks_query` -Run a QuickBooks Online query (SQL-like syntax, e.g., +Run a QuickBooks Online query using SQL-like syntax (example: SELECT * FROM Item WHERE Active = true) #### Input diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 8ae5e93f7c5..a5a3afaf415 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -10390,7 +10390,7 @@ }, { "name": "Run Query", - "description": "Run a QuickBooks Online query (SQL-like syntax, e.g., " + "description": "Run a QuickBooks Online query using SQL-like syntax (example: SELECT * FROM Item WHERE Active = true)" } ], "operationCount": 22, diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index d67d8289e9d..f4d5d901583 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1812,7 +1812,7 @@ export const auth = betterAuth({ return null } return { - id: `quickbooks-${realmId}-${generateId()}`, + id: `quickbooks-${realmId}`, name: `QuickBooks Company ${realmId}`, email: `quickbooks-${realmId}@quickbooks.local`, emailVerified: true, diff --git a/apps/sim/tools/quickbooks/list_accounts.ts b/apps/sim/tools/quickbooks/list_accounts.ts index 7fef6a4b8c8..9cdcae7523b 100644 --- a/apps/sim/tools/quickbooks/list_accounts.ts +++ b/apps/sim/tools/quickbooks/list_accounts.ts @@ -1,7 +1,11 @@ import { createLogger } from '@sim/logger' import type { QuickBooksAccountListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' import { ACCOUNT_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { + buildCompanyUrl, + quickbooksAuthHeaders, + sanitizeWhereClause, +} from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksListAccounts') @@ -54,7 +58,8 @@ export const quickbooksListAccountsTool: ToolConfig< url: (params) => { const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) const start = Math.max(Number(params.startPosition) || 1, 1) - const whereClause = params.where ? ` WHERE ${params.where}` : '' + const safeWhere = sanitizeWhereClause(params.where) + const whereClause = safeWhere ? ` WHERE ${safeWhere}` : '' const sql = `SELECT * FROM Account${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` const url = buildCompanyUrl(params.realmId, '/query') return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` diff --git a/apps/sim/tools/quickbooks/list_bills.ts b/apps/sim/tools/quickbooks/list_bills.ts index 444efd29e79..e2cb679b911 100644 --- a/apps/sim/tools/quickbooks/list_bills.ts +++ b/apps/sim/tools/quickbooks/list_bills.ts @@ -1,7 +1,11 @@ import { createLogger } from '@sim/logger' import type { QuickBooksBillListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' import { BILL_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { + buildCompanyUrl, + quickbooksAuthHeaders, + sanitizeWhereClause, +} from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksListBills') @@ -52,7 +56,8 @@ export const quickbooksListBillsTool: ToolConfig { const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) const start = Math.max(Number(params.startPosition) || 1, 1) - const whereClause = params.where ? ` WHERE ${params.where}` : '' + const safeWhere = sanitizeWhereClause(params.where) + const whereClause = safeWhere ? ` WHERE ${safeWhere}` : '' const sql = `SELECT * FROM Bill${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` const url = buildCompanyUrl(params.realmId, '/query') return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` diff --git a/apps/sim/tools/quickbooks/list_customers.ts b/apps/sim/tools/quickbooks/list_customers.ts index 24e906dcedb..0aefc7948d2 100644 --- a/apps/sim/tools/quickbooks/list_customers.ts +++ b/apps/sim/tools/quickbooks/list_customers.ts @@ -1,7 +1,11 @@ import { createLogger } from '@sim/logger' import type { QuickBooksCustomerListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' import { CUSTOMER_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { + buildCompanyUrl, + quickbooksAuthHeaders, + sanitizeWhereClause, +} from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksListCustomers') @@ -54,7 +58,8 @@ export const quickbooksListCustomersTool: ToolConfig< url: (params) => { const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) const start = Math.max(Number(params.startPosition) || 1, 1) - const whereClause = params.where ? ` WHERE ${params.where}` : '' + const safeWhere = sanitizeWhereClause(params.where) + const whereClause = safeWhere ? ` WHERE ${safeWhere}` : '' const sql = `SELECT * FROM Customer${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` const url = buildCompanyUrl(params.realmId, '/query') return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` diff --git a/apps/sim/tools/quickbooks/list_estimates.ts b/apps/sim/tools/quickbooks/list_estimates.ts index e8a9042ca2d..ee7d21d3521 100644 --- a/apps/sim/tools/quickbooks/list_estimates.ts +++ b/apps/sim/tools/quickbooks/list_estimates.ts @@ -1,7 +1,11 @@ import { createLogger } from '@sim/logger' import type { QuickBooksEstimateListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' import { ESTIMATE_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { + buildCompanyUrl, + quickbooksAuthHeaders, + sanitizeWhereClause, +} from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksListEstimates') @@ -54,7 +58,8 @@ export const quickbooksListEstimatesTool: ToolConfig< url: (params) => { const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) const start = Math.max(Number(params.startPosition) || 1, 1) - const whereClause = params.where ? ` WHERE ${params.where}` : '' + const safeWhere = sanitizeWhereClause(params.where) + const whereClause = safeWhere ? ` WHERE ${safeWhere}` : '' const sql = `SELECT * FROM Estimate${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` const url = buildCompanyUrl(params.realmId, '/query') return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` diff --git a/apps/sim/tools/quickbooks/list_invoices.ts b/apps/sim/tools/quickbooks/list_invoices.ts index 954f2caba71..0a96029c8aa 100644 --- a/apps/sim/tools/quickbooks/list_invoices.ts +++ b/apps/sim/tools/quickbooks/list_invoices.ts @@ -1,7 +1,11 @@ import { createLogger } from '@sim/logger' import type { QuickBooksInvoiceListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' import { INVOICE_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { + buildCompanyUrl, + quickbooksAuthHeaders, + sanitizeWhereClause, +} from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksListInvoices') @@ -54,7 +58,8 @@ export const quickbooksListInvoicesTool: ToolConfig< url: (params) => { const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) const start = Math.max(Number(params.startPosition) || 1, 1) - const whereClause = params.where ? ` WHERE ${params.where}` : '' + const safeWhere = sanitizeWhereClause(params.where) + const whereClause = safeWhere ? ` WHERE ${safeWhere}` : '' const sql = `SELECT * FROM Invoice${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` const url = buildCompanyUrl(params.realmId, '/query') return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` diff --git a/apps/sim/tools/quickbooks/list_items.ts b/apps/sim/tools/quickbooks/list_items.ts index 5030f05a5aa..6ecdfa5c86a 100644 --- a/apps/sim/tools/quickbooks/list_items.ts +++ b/apps/sim/tools/quickbooks/list_items.ts @@ -1,7 +1,11 @@ import { createLogger } from '@sim/logger' import type { QuickBooksItemListResponse, QuickBooksListParams } from '@/tools/quickbooks/types' import { ITEM_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { + buildCompanyUrl, + quickbooksAuthHeaders, + sanitizeWhereClause, +} from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksListItems') @@ -52,7 +56,8 @@ export const quickbooksListItemsTool: ToolConfig { const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) const start = Math.max(Number(params.startPosition) || 1, 1) - const whereClause = params.where ? ` WHERE ${params.where}` : '' + const safeWhere = sanitizeWhereClause(params.where) + const whereClause = safeWhere ? ` WHERE ${safeWhere}` : '' const sql = `SELECT * FROM Item${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` const url = buildCompanyUrl(params.realmId, '/query') return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` diff --git a/apps/sim/tools/quickbooks/list_payments.ts b/apps/sim/tools/quickbooks/list_payments.ts index f9ec8107e7e..e818c52bc74 100644 --- a/apps/sim/tools/quickbooks/list_payments.ts +++ b/apps/sim/tools/quickbooks/list_payments.ts @@ -1,7 +1,11 @@ import { createLogger } from '@sim/logger' import type { QuickBooksListParams, QuickBooksPaymentListResponse } from '@/tools/quickbooks/types' import { PAYMENT_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { + buildCompanyUrl, + quickbooksAuthHeaders, + sanitizeWhereClause, +} from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksListPayments') @@ -54,7 +58,8 @@ export const quickbooksListPaymentsTool: ToolConfig< url: (params) => { const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) const start = Math.max(Number(params.startPosition) || 1, 1) - const whereClause = params.where ? ` WHERE ${params.where}` : '' + const safeWhere = sanitizeWhereClause(params.where) + const whereClause = safeWhere ? ` WHERE ${safeWhere}` : '' const sql = `SELECT * FROM Payment${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` const url = buildCompanyUrl(params.realmId, '/query') return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` diff --git a/apps/sim/tools/quickbooks/list_vendors.ts b/apps/sim/tools/quickbooks/list_vendors.ts index 4499da9f4e8..fa41ad99ba2 100644 --- a/apps/sim/tools/quickbooks/list_vendors.ts +++ b/apps/sim/tools/quickbooks/list_vendors.ts @@ -1,7 +1,11 @@ import { createLogger } from '@sim/logger' import type { QuickBooksListParams, QuickBooksVendorListResponse } from '@/tools/quickbooks/types' import { VENDOR_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { + buildCompanyUrl, + quickbooksAuthHeaders, + sanitizeWhereClause, +} from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksListVendors') @@ -54,7 +58,8 @@ export const quickbooksListVendorsTool: ToolConfig< url: (params) => { const max = Math.min(Math.max(Number(params.maxResults) || 100, 1), 1000) const start = Math.max(Number(params.startPosition) || 1, 1) - const whereClause = params.where ? ` WHERE ${params.where}` : '' + const safeWhere = sanitizeWhereClause(params.where) + const whereClause = safeWhere ? ` WHERE ${safeWhere}` : '' const sql = `SELECT * FROM Vendor${whereClause} STARTPOSITION ${start} MAXRESULTS ${max}` const url = buildCompanyUrl(params.realmId, '/query') return `${url}?query=${encodeURIComponent(sql)}&minorversion=73` diff --git a/apps/sim/tools/quickbooks/query.ts b/apps/sim/tools/quickbooks/query.ts index 0a2186a559e..0783d22fbe4 100644 --- a/apps/sim/tools/quickbooks/query.ts +++ b/apps/sim/tools/quickbooks/query.ts @@ -9,7 +9,7 @@ export const quickbooksQueryTool: ToolConfig ({}), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/quickbooks/utils.ts b/apps/sim/tools/quickbooks/utils.ts index 9d2f0ebaeaa..63febb6ced5 100644 --- a/apps/sim/tools/quickbooks/utils.ts +++ b/apps/sim/tools/quickbooks/utils.ts @@ -20,6 +20,27 @@ export function buildCompanyUrl(realmId: string | undefined, path: string): stri return `${base}/v3/company/${realmId}${trimmed}` } +/** + * Reject `where` clause values that contain QQL keywords other than predicates. + * Prevents callers (or LLMs) from escaping the per-tool entity scope by + * appending `MAXRESULTS`, `STARTPOSITION`, `ORDERBY`, additional `SELECT` + * statements, etc. + */ +const FORBIDDEN_WHERE_KEYWORDS = + /\b(MAXRESULTS|STARTPOSITION|ORDER\s*BY|SELECT|FROM|GROUP\s*BY|HAVING)\b/i + +export function sanitizeWhereClause(where: string | undefined): string | undefined { + if (!where) return undefined + const trimmed = where.trim() + if (!trimmed) return undefined + if (FORBIDDEN_WHERE_KEYWORDS.test(trimmed)) { + throw new Error( + 'where clause may only contain predicate expressions — keywords like MAXRESULTS, STARTPOSITION, ORDER BY, SELECT, and FROM are not allowed' + ) + } + return trimmed +} + export function quickbooksAuthHeaders(accessToken: string | undefined): Record { if (!accessToken) { throw new Error('Missing QuickBooks access token') From fbb16fe2e11dff90e768a4c5dd5729efd89e0b06 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 12:44:30 -0700 Subject: [PATCH 3/6] fix(quickbooks): url-encode realmId, mark invoice line itemId as required in description --- apps/sim/tools/quickbooks/create_invoice.ts | 2 +- apps/sim/tools/quickbooks/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/quickbooks/create_invoice.ts b/apps/sim/tools/quickbooks/create_invoice.ts index c67304e014a..a3d617956bc 100644 --- a/apps/sim/tools/quickbooks/create_invoice.ts +++ b/apps/sim/tools/quickbooks/create_invoice.ts @@ -57,7 +57,7 @@ export const quickbooksCreateInvoiceTool: ToolConfig< required: true, visibility: 'user-or-llm', description: - 'Invoice line items (JSON array). Each entry: { description?, amount, quantity?, itemId?, itemName? }', + 'Invoice line items (JSON array). Each entry: { itemId, amount, quantity?, description?, itemName? }. itemId is required (use list_items to look up valid IDs).', }, txnDate: { type: 'string', diff --git a/apps/sim/tools/quickbooks/utils.ts b/apps/sim/tools/quickbooks/utils.ts index 63febb6ced5..91775147b79 100644 --- a/apps/sim/tools/quickbooks/utils.ts +++ b/apps/sim/tools/quickbooks/utils.ts @@ -17,7 +17,7 @@ export function buildCompanyUrl(realmId: string | undefined, path: string): stri } const base = getQuickBooksApiBaseUrl() const trimmed = path.startsWith('/') ? path : `/${path}` - return `${base}/v3/company/${realmId}${trimmed}` + return `${base}/v3/company/${encodeURIComponent(realmId)}${trimmed}` } /** From 28b40bd4fcb6c17f5510e5e972afa38030b9ffff Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 13:00:00 -0700 Subject: [PATCH 4/6] fix(quickbooks): wand prompt requires itemId, extract coerceJsonArray helper --- apps/sim/blocks/blocks/quickbooks.ts | 2 +- apps/sim/tools/quickbooks/create_bill.ts | 15 +++------------ apps/sim/tools/quickbooks/create_invoice.ts | 15 +++------------ apps/sim/tools/quickbooks/utils.ts | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/apps/sim/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts index 97f4d534ea2..f3e00e5c5ef 100644 --- a/apps/sim/blocks/blocks/quickbooks.ts +++ b/apps/sim/blocks/blocks/quickbooks.ts @@ -189,7 +189,7 @@ export const QuickBooksBlock: BlockConfig = { wandConfig: { enabled: true, prompt: - 'Generate a JSON array of QuickBooks invoice line items. Each entry must have a numeric `amount` and may include `description`, `quantity`, `itemId`, and `itemName`. Return ONLY the JSON array.', + 'Generate a JSON array of QuickBooks invoice line items. Each entry must have a numeric `amount` and a string `itemId` (the QuickBooks Item ID); may include `description`, `quantity`, and `itemName`. Return ONLY the JSON array.', generationType: 'json-object', }, }, diff --git a/apps/sim/tools/quickbooks/create_bill.ts b/apps/sim/tools/quickbooks/create_bill.ts index a8980b6806e..e13f5a76f50 100644 --- a/apps/sim/tools/quickbooks/create_bill.ts +++ b/apps/sim/tools/quickbooks/create_bill.ts @@ -5,22 +5,13 @@ import type { QuickBooksCreateBillParams, } from '@/tools/quickbooks/types' import { BILL_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { buildCompanyUrl, coerceJsonArray, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksCreateBill') -function coerceLines(input: QuickBooksCreateBillParams['lines']): QuickBooksBillLine[] { - if (Array.isArray(input)) return input - if (typeof input !== 'string') { - throw new Error('Bill lines must be a JSON array') - } - const parsed = JSON.parse(input) - if (!Array.isArray(parsed)) { - throw new Error('Bill lines must be a JSON array') - } - return parsed as QuickBooksBillLine[] -} +const coerceLines = (input: QuickBooksCreateBillParams['lines']): QuickBooksBillLine[] => + coerceJsonArray(input, 'Bill lines') export const quickbooksCreateBillTool: ToolConfig< QuickBooksCreateBillParams, diff --git a/apps/sim/tools/quickbooks/create_invoice.ts b/apps/sim/tools/quickbooks/create_invoice.ts index a3d617956bc..bb07bb48506 100644 --- a/apps/sim/tools/quickbooks/create_invoice.ts +++ b/apps/sim/tools/quickbooks/create_invoice.ts @@ -5,22 +5,13 @@ import type { QuickBooksLineItem, } from '@/tools/quickbooks/types' import { INVOICE_OUTPUT } from '@/tools/quickbooks/types' -import { buildCompanyUrl, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' +import { buildCompanyUrl, coerceJsonArray, quickbooksAuthHeaders } from '@/tools/quickbooks/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('QuickBooksCreateInvoice') -function coerceLines(input: QuickBooksCreateInvoiceParams['lines']): QuickBooksLineItem[] { - if (Array.isArray(input)) return input - if (typeof input !== 'string') { - throw new Error('Invoice lines must be a JSON array') - } - const parsed = JSON.parse(input) - if (!Array.isArray(parsed)) { - throw new Error('Invoice lines must be a JSON array') - } - return parsed as QuickBooksLineItem[] -} +const coerceLines = (input: QuickBooksCreateInvoiceParams['lines']): QuickBooksLineItem[] => + coerceJsonArray(input, 'Invoice lines') export const quickbooksCreateInvoiceTool: ToolConfig< QuickBooksCreateInvoiceParams, diff --git a/apps/sim/tools/quickbooks/utils.ts b/apps/sim/tools/quickbooks/utils.ts index 91775147b79..1625732c725 100644 --- a/apps/sim/tools/quickbooks/utils.ts +++ b/apps/sim/tools/quickbooks/utils.ts @@ -41,6 +41,23 @@ export function sanitizeWhereClause(where: string | undefined): string | undefin return trimmed } +/** + * Coerce a `lines` parameter (already an array, or a JSON-encoded string from + * an LLM/short-input) into a typed array. Throws with a contextual label when + * the value is missing or not an array. + */ +export function coerceJsonArray(input: unknown, label: string): T[] { + if (Array.isArray(input)) return input as T[] + if (typeof input !== 'string') { + throw new Error(`${label} must be a JSON array`) + } + const parsed = JSON.parse(input) + if (!Array.isArray(parsed)) { + throw new Error(`${label} must be a JSON array`) + } + return parsed as T[] +} + export function quickbooksAuthHeaders(accessToken: string | undefined): Record { if (!accessToken) { throw new Error('Missing QuickBooks access token') From aef9205082fe3d020c0081b998bee4f699becc7c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 13:38:23 -0700 Subject: [PATCH 5/6] fix(quickbooks): persist realm scope on re-auth, type itemId as required Co-Authored-By: Claude Opus 4.7 --- apps/sim/lib/auth/auth.ts | 23 ++++++++++++++++++----- apps/sim/tools/quickbooks/types.ts | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index f4d5d901583..82c8bc19485 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -533,17 +533,30 @@ export const auth = betterAuth({ } if (account.providerId === 'quickbooks') { + const updates: { + accessTokenExpiresAt?: Date + scope?: string + } = {} + + let realmId: string | undefined try { const cookieStore = await cookies() + realmId = cookieStore.get('qb_pending_realm')?.value cookieStore.delete('qb_pending_realm') } catch (error) { - logger.error('Failed to clear qb_pending_realm cookie', { error }) + logger.error('Failed to read/clear qb_pending_realm cookie', { error }) } + if (!account.accessTokenExpiresAt) { - await db - .update(schema.account) - .set({ accessTokenExpiresAt: new Date(Date.now() + 60 * 60 * 1000) }) - .where(eq(schema.account.id, account.id)) + updates.accessTokenExpiresAt = new Date(Date.now() + 60 * 60 * 1000) + } + + if (realmId && !account.scope?.includes('__qb_realm__:')) { + updates.scope = `__qb_realm__:${realmId} ${account.scope ?? ''}`.trim() + } + + if (Object.keys(updates).length > 0) { + await db.update(schema.account).set(updates).where(eq(schema.account.id, account.id)) } } diff --git a/apps/sim/tools/quickbooks/types.ts b/apps/sim/tools/quickbooks/types.ts index 5a711c5e1a0..02b1c3cbdba 100644 --- a/apps/sim/tools/quickbooks/types.ts +++ b/apps/sim/tools/quickbooks/types.ts @@ -33,7 +33,7 @@ export interface QuickBooksLineItem { description?: string amount: number quantity?: number - itemId?: string + itemId: string itemName?: string } From d026d581d32d3ba7d73b0ef2efd7a7e3bed9c1e8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 13:49:59 -0700 Subject: [PATCH 6/6] fix(quickbooks): strip string literals before WHERE keyword sanitization Co-Authored-By: Claude Opus 4.7 --- apps/sim/tools/quickbooks/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/tools/quickbooks/utils.ts b/apps/sim/tools/quickbooks/utils.ts index 1625732c725..66c56b37b27 100644 --- a/apps/sim/tools/quickbooks/utils.ts +++ b/apps/sim/tools/quickbooks/utils.ts @@ -33,7 +33,11 @@ export function sanitizeWhereClause(where: string | undefined): string | undefin if (!where) return undefined const trimmed = where.trim() if (!trimmed) return undefined - if (FORBIDDEN_WHERE_KEYWORDS.test(trimmed)) { + // Strip single-quoted string literals (QQL escapes a quote by doubling it: '') + // before checking for forbidden keywords, so values like 'From Scratch Inc' + // don't trigger false positives on FROM/SELECT/etc. + const withoutLiterals = trimmed.replace(/'(?:''|[^'])*'/g, "''") + if (FORBIDDEN_WHERE_KEYWORDS.test(withoutLiterals)) { throw new Error( 'where clause may only contain predicate expressions — keywords like MAXRESULTS, STARTPOSITION, ORDER BY, SELECT, and FROM are not allowed' )