Skip to content

Commit c332dda

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat(quickbooks): add read-only procurement integration
1 parent 6647808 commit c332dda

39 files changed

Lines changed: 2960 additions & 69 deletions

apps/docs/components/icons.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2494,6 +2494,25 @@ export function BrexIcon(props: SVGProps<SVGSVGElement>) {
24942494
)
24952495
}
24962496

2497+
/**
2498+
* Official QuickBooks circular mark, cropped from the user-supplied
2499+
* Intuit_QuickBooks_logo.svg wordmark.
2500+
*/
2501+
export function QuickBooksIcon(props: SVGProps<SVGSVGElement>) {
2502+
return (
2503+
<svg {...props} viewBox='0 0 61.54 61.54' fill='none' xmlns='http://www.w3.org/2000/svg'>
2504+
<path
2505+
fill='#2CA01C'
2506+
d='M30.77 61.54c16.99 0 30.77-13.78 30.77-30.77S47.76 0 30.77 0 0 13.78 0 30.77s13.77 30.77 30.77 30.77Z'
2507+
/>
2508+
<path
2509+
fill='#FFF'
2510+
d='M20.51 18.8c-6.61 0-11.97 5.36-11.97 11.97s5.35 11.96 11.97 11.96h1.71v-4.44h-1.71c-4.15 0-7.52-3.37-7.52-7.52 0-4.15 3.37-7.52 7.52-7.52h4.11V46.5c0 2.45 1.99 4.44 4.44 4.44V18.8h-8.55Zm20.52 23.93c6.61 0 11.97-5.36 11.97-11.96S47.65 18.81 41.03 18.81h-1.71v4.44h1.71c4.15 0 7.52 3.37 7.52 7.52s-3.37 7.52-7.52 7.52h-4.11V15.04c0-2.45-1.99-4.44-4.44-4.44v32.13h8.55Z'
2511+
/>
2512+
</svg>
2513+
)
2514+
}
2515+
24972516
export function BrightDataIcon(props: SVGProps<SVGSVGElement>) {
24982517
return (
24992518
<svg

apps/docs/components/ui/icon-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ import {
176176
PulseIcon,
177177
QdrantIcon,
178178
QuartrIcon,
179+
QuickBooksIcon,
179180
QuiverIcon,
180181
RailwayIcon,
181182
RB2BIcon,
@@ -455,6 +456,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
455456
pulse_v2: PulseIcon,
456457
qdrant: QdrantIcon,
457458
quartr: QuartrIcon,
459+
quickbooks: QuickBooksIcon,
458460
quiver: QuiverIcon,
459461
railway: RailwayIcon,
460462
rb2b: RB2BIcon,

apps/docs/content/docs/en/integrations/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
"pulse",
188188
"qdrant",
189189
"quartr",
190+
"quickbooks",
190191
"quiver",
191192
"railway",
192193
"rb2b",
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
title: QuickBooks
3+
description: Read procurement data from QuickBooks Online
4+
---
5+
6+
import { BlockInfoCard } from "@/components/ui/block-info-card"
7+
8+
<BlockInfoCard
9+
type="quickbooks"
10+
color="#2CA01C"
11+
/>
12+
13+
## Usage Instructions
14+
15+
Connect one QuickBooks Online company and read its company profile, vendors, purchase orders, and bills. The connected company is selected during OAuth and cannot be overridden by workflow input.
16+
17+
18+
19+
## Actions
20+
21+
### `quickbooks_get_company_info`
22+
23+
Get information about the connected QuickBooks Online company
24+
25+
#### Input
26+
27+
| Parameter | Type | Required | Description |
28+
| --------- | ---- | -------- | ----------- |
29+
30+
#### Output
31+
32+
| Parameter | Type | Description |
33+
| --------- | ---- | ----------- |
34+
| `company` | json | Verified QuickBooks CompanyInfo object, including Id, CompanyName, LegalName, addresses, contact details, NameValue settings, and MetaData when populated |
35+
|`Id` | string | Connected QuickBooks company ID |
36+
|`SyncToken` | string | CompanyInfo sync token |
37+
|`CompanyName` | string | Company display name |
38+
|`LegalName` | string | Company legal name |
39+
|`CompanyAddr` | json | Company address |
40+
|`CustomerCommunicationAddr` | json | Customer communication address |
41+
|`LegalAddr` | json | Company legal address |
42+
|`PrimaryPhone` | json | Primary phone details |
43+
|`Email` | json | Company email details |
44+
|`WebAddr` | json | Company website details |
45+
|`CompanyStartDate` | string | Company start date |
46+
|`Country` | string | Company country code |
47+
|`FiscalYearStartMonth` | string | Fiscal year starting month |
48+
|`DefaultTimeZone` | string | Company default time zone |
49+
|`NameValue` | array | QuickBooks company settings represented as name/value entries |
50+
|`MetaData` | json | CompanyInfo creation and update timestamps |
51+
|`CreateTime` | string | Entity creation timestamp |
52+
|`LastUpdatedTime` | string | Entity last-updated timestamp |
53+
| `time` | string | QuickBooks response timestamp |
54+
55+
### `quickbooks_list_bills`
56+
57+
List bills in the connected QuickBooks Online company
58+
59+
#### Input
60+
61+
| Parameter | Type | Required | Description |
62+
| --------- | ---- | -------- | ----------- |
63+
| `startPosition` | number | Yes | One-based position of the first bill to return |
64+
| `maxResults` | number | Yes | Number of bills to request \(1–100\) |
65+
66+
#### Output
67+
68+
| Parameter | Type | Description |
69+
| --------- | ---- | ----------- |
70+
| `startPosition` | number | One-based position of the first item in this response |
71+
| `maxResults` | number | Actual number of items reported for this response |
72+
| `nextStartPosition` | number | Position to use when explicitly requesting the next page |
73+
| `hasMore` | boolean | Conservative indication that another page may exist |
74+
| `time` | string | QuickBooks response timestamp |
75+
| `items` | array | Bill objects returned by QuickBooks |
76+
|`Id` | string | Bill ID |
77+
|`SyncToken` | string | Bill sync token |
78+
|`DocNumber` | string | Bill reference number |
79+
|`TxnDate` | string | Bill transaction date |
80+
|`DueDate` | string | Bill due date |
81+
|`VendorRef` | json | Bill vendor reference |
82+
|`value` | string | QuickBooks entity ID |
83+
|`name` | string | QuickBooks entity display name |
84+
|`APAccountRef` | json | Accounts payable account reference |
85+
|`value` | string | QuickBooks entity ID |
86+
|`name` | string | QuickBooks entity display name |
87+
|`CurrencyRef` | json | Bill currency reference |
88+
|`value` | string | QuickBooks entity ID |
89+
|`name` | string | QuickBooks entity display name |
90+
|`ExchangeRate` | number | Bill exchange rate |
91+
|`Line` | array | Bill line items and their detail objects |
92+
|`TotalAmt` | number | Bill total amount |
93+
|`Balance` | number | Unpaid bill balance |
94+
|`PrivateNote` | string | Private bill note |
95+
|`MetaData` | json | Bill creation and update timestamps |
96+
|`CreateTime` | string | Entity creation timestamp |
97+
|`LastUpdatedTime` | string | Entity last-updated timestamp |
98+
99+
### `quickbooks_list_purchase_orders`
100+
101+
List purchase orders in the connected QuickBooks Online company
102+
103+
#### Input
104+
105+
| Parameter | Type | Required | Description |
106+
| --------- | ---- | -------- | ----------- |
107+
| `startPosition` | number | Yes | One-based position of the first purchase order to return |
108+
| `maxResults` | number | Yes | Number of purchase orders to request \(1–100\) |
109+
110+
#### Output
111+
112+
| Parameter | Type | Description |
113+
| --------- | ---- | ----------- |
114+
| `startPosition` | number | One-based position of the first item in this response |
115+
| `maxResults` | number | Actual number of items reported for this response |
116+
| `nextStartPosition` | number | Position to use when explicitly requesting the next page |
117+
| `hasMore` | boolean | Conservative indication that another page may exist |
118+
| `time` | string | QuickBooks response timestamp |
119+
| `items` | array | PurchaseOrder objects returned by QuickBooks |
120+
|`Id` | string | Purchase order ID |
121+
|`SyncToken` | string | Purchase order sync token |
122+
|`DocNumber` | string | Purchase order number |
123+
|`TxnDate` | string | Purchase order date |
124+
|`VendorRef` | json | Purchase order vendor reference |
125+
|`value` | string | QuickBooks entity ID |
126+
|`name` | string | QuickBooks entity display name |
127+
|`CurrencyRef` | json | Purchase order currency reference |
128+
|`value` | string | QuickBooks entity ID |
129+
|`name` | string | QuickBooks entity display name |
130+
|`ExchangeRate` | number | Purchase order exchange rate |
131+
|`Line` | array | Purchase order line items and their detail objects |
132+
|`TotalAmt` | number | Purchase order total amount |
133+
|`PrivateNote` | string | Private purchase order note |
134+
|`MetaData` | json | Purchase order creation and update timestamps |
135+
|`CreateTime` | string | Entity creation timestamp |
136+
|`LastUpdatedTime` | string | Entity last-updated timestamp |
137+
138+
### `quickbooks_list_vendors`
139+
140+
List vendors in the connected QuickBooks Online company
141+
142+
#### Input
143+
144+
| Parameter | Type | Required | Description |
145+
| --------- | ---- | -------- | ----------- |
146+
| `startPosition` | number | Yes | One-based position of the first vendor to return |
147+
| `maxResults` | number | Yes | Number of vendors to request \(1–100\) |
148+
149+
#### Output
150+
151+
| Parameter | Type | Description |
152+
| --------- | ---- | ----------- |
153+
| `startPosition` | number | One-based position of the first item in this response |
154+
| `maxResults` | number | Actual number of items reported for this response |
155+
| `nextStartPosition` | number | Position to use when explicitly requesting the next page |
156+
| `hasMore` | boolean | Conservative indication that another page may exist |
157+
| `time` | string | QuickBooks response timestamp |
158+
| `items` | array | Vendor objects returned by QuickBooks |
159+
|`Id` | string | Vendor ID |
160+
|`SyncToken` | string | Vendor sync token |
161+
|`DisplayName` | string | Vendor display name |
162+
|`CompanyName` | string | Vendor company name |
163+
|`GivenName` | string | Vendor given name |
164+
|`FamilyName` | string | Vendor family name |
165+
|`PrintOnCheckName` | string | Name printed on checks |
166+
|`Active` | boolean | Whether the vendor is active |
167+
|`Vendor1099` | boolean | Whether the vendor is tracked for 1099 reporting |
168+
|`BillAddr` | json | Vendor billing address |
169+
|`PrimaryPhone` | json | Vendor primary phone details |
170+
|`PrimaryEmailAddr` | json | Vendor primary email details |
171+
|`Balance` | number | Vendor balance |
172+
|`CurrencyRef` | json | Vendor currency reference |
173+
|`value` | string | QuickBooks entity ID |
174+
|`name` | string | QuickBooks entity display name |
175+
|`MetaData` | json | Vendor creation and update timestamps |
176+
|`CreateTime` | string | Entity creation timestamp |
177+
|`LastUpdatedTime` | string | Entity last-updated timestamp |

apps/sim/app/api/auth/oauth/token/route.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,74 @@ describe('OAuth Token API Routes', () => {
6767

6868
expect(response.status).toBe(200)
6969
expect(data).toHaveProperty('accessToken', 'fresh-token')
70+
expect(data).not.toHaveProperty('realmId')
7071

7172
expect(mockAuthorizeCredentialUse).toHaveBeenCalled()
7273
expect(authOAuthUtilsMockFns.mockGetCredential).toHaveBeenCalled()
7374
expect(authOAuthUtilsMockFns.mockRefreshTokenIfNeeded).toHaveBeenCalled()
7475
})
7576

77+
it('returns realmId only for QuickBooks credentials', async () => {
78+
mockAuthorizeCredentialUse.mockResolvedValueOnce({
79+
ok: true,
80+
authType: 'session',
81+
requesterUserId: 'test-user-id',
82+
credentialOwnerUserId: 'owner-user-id',
83+
})
84+
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({
85+
id: 'credential-id',
86+
accountId: 'quickbooks:123456789:intuit-subject-01234567-89ab-4def-8abc-0123456789ab',
87+
accessToken: 'test-token',
88+
refreshToken: 'refresh-token',
89+
accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
90+
providerId: 'quickbooks',
91+
})
92+
authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({
93+
accessToken: 'fresh-token',
94+
refreshed: false,
95+
})
96+
97+
const response = await POST(
98+
createMockRequest('POST', {
99+
credentialId: 'credential-id',
100+
})
101+
)
102+
103+
expect(response.status).toBe(200)
104+
expect(await response.json()).toEqual({
105+
accessToken: 'fresh-token',
106+
realmId: '123456789',
107+
})
108+
})
109+
110+
it('rejects a malformed QuickBooks company identity with reconnect guidance', async () => {
111+
mockAuthorizeCredentialUse.mockResolvedValueOnce({
112+
ok: true,
113+
authType: 'session',
114+
requesterUserId: 'test-user-id',
115+
credentialOwnerUserId: 'owner-user-id',
116+
})
117+
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({
118+
id: 'credential-id',
119+
accountId: 'malformed',
120+
accessToken: 'test-token',
121+
refreshToken: 'refresh-token',
122+
accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
123+
providerId: 'quickbooks',
124+
})
125+
126+
const response = await POST(
127+
createMockRequest('POST', {
128+
credentialId: 'credential-id',
129+
})
130+
)
131+
const data = await response.json()
132+
133+
expect(response.status).toBe(401)
134+
expect(data.error).toMatch(/Reconnect the QuickBooks credential/)
135+
expect(authOAuthUtilsMockFns.mockRefreshTokenIfNeeded).not.toHaveBeenCalled()
136+
})
137+
76138
it('should handle workflowId for server-side authentication', async () => {
77139
mockAuthorizeCredentialUse.mockResolvedValueOnce({
78140
ok: true,

apps/sim/app/api/auth/oauth/token/route.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
1212
import { generateRequestId } from '@/lib/core/utils/request'
1313
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1414
import { TokenServiceAccountValidationError } from '@/lib/credentials/token-service-accounts/errors'
15+
import { parseQuickBooksAccountId } from '@/lib/oauth/quickbooks'
1516
import { captureServerEvent } from '@/lib/posthog/server'
1617
import {
1718
getCredential,
@@ -249,6 +250,23 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
249250
const oauthActorId = authz.requesterUserId
250251
const oauthWorkspaceId = authz.workspaceId ?? null
251252

253+
let realmId: string | undefined
254+
if (credential.providerId === 'quickbooks') {
255+
try {
256+
realmId = parseQuickBooksAccountId(credential.accountId).realmId
257+
} catch (error) {
258+
return NextResponse.json(
259+
{
260+
error: getErrorMessage(
261+
error,
262+
'QuickBooks company identity is invalid. Reconnect the QuickBooks credential.'
263+
),
264+
},
265+
{ status: 401 }
266+
)
267+
}
268+
}
269+
252270
try {
253271
const { accessToken } = await refreshTokenIfNeeded(
254272
requestId,
@@ -295,6 +313,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
295313
accessToken,
296314
idToken: credential.idToken || undefined,
297315
...(instanceUrl && { instanceUrl }),
316+
...(realmId && { realmId }),
298317
},
299318
{ status: 200 }
300319
)
@@ -360,6 +379,23 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
360379
const actorId = authz.requesterUserId
361380
const workspaceId = authz.workspaceId ?? null
362381

382+
let realmId: string | undefined
383+
if (credential.providerId === 'quickbooks') {
384+
try {
385+
realmId = parseQuickBooksAccountId(credential.accountId).realmId
386+
} catch (error) {
387+
return NextResponse.json(
388+
{
389+
error: getErrorMessage(
390+
error,
391+
'QuickBooks company identity is invalid. Reconnect the QuickBooks credential.'
392+
),
393+
},
394+
{ status: 401 }
395+
)
396+
}
397+
}
398+
363399
try {
364400
const { accessToken } = await refreshTokenIfNeeded(
365401
requestId,
@@ -407,6 +443,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
407443
accessToken,
408444
idToken: credential.idToken || undefined,
409445
...(instanceUrl && { instanceUrl }),
446+
...(realmId && { realmId }),
410447
},
411448
{ status: 200 }
412449
)

0 commit comments

Comments
 (0)