Skip to content

Commit 3f58594

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat(quickbooks): add core webhook triggers
1 parent de78c49 commit 3f58594

29 files changed

Lines changed: 1471 additions & 5 deletions

apps/docs/content/docs/en/integrations/quickbooks.mdx

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2919,3 +2919,215 @@ Download a QuickBooks file attachment through its short-lived URL
29192919
| `attachmentId` | string | Downloaded QuickBooks attachment ID |
29202920

29212921

2922+
2923+
## Triggers
2924+
2925+
A **Trigger** is a block that starts a workflow when an event happens in this service.
2926+
2927+
### QuickBooks Bill Events
2928+
2929+
Trigger when selected Bill events occur in QuickBooks
2930+
2931+
#### Configuration
2932+
2933+
| Parameter | Type | Required | Description |
2934+
| --------- | ---- | -------- | ----------- |
2935+
| `triggerCredentials` | string | Yes | QuickBooks Account |
2936+
| `eventTypes` | string | Yes | Event Types |
2937+
2938+
#### Output
2939+
2940+
| Parameter | Type | Description |
2941+
| --------- | ---- | ----------- |
2942+
| `eventId` | string | Intuit webhook event ID |
2943+
| `eventType` | string | Full Intuit CloudEvent type |
2944+
| `entityType` | string | QuickBooks entity type |
2945+
| `action` | string | QuickBooks webhook action |
2946+
| `entityId` | string | QuickBooks entity ID |
2947+
| `realmId` | string | QuickBooks company realm ID |
2948+
| `eventTime` | string | Event timestamp |
2949+
| `specVersion` | string | CloudEvents specification version |
2950+
| `source` | string | Intuit event source |
2951+
| `contentType` | string | Event content type, when provided |
2952+
| `data` | json | Optional event data supplied by Intuit |
2953+
2954+
2955+
---
2956+
2957+
### QuickBooks Bill Payment Events
2958+
2959+
Trigger when selected Bill Payment events occur in QuickBooks
2960+
2961+
#### Configuration
2962+
2963+
| Parameter | Type | Required | Description |
2964+
| --------- | ---- | -------- | ----------- |
2965+
| `triggerCredentials` | string | Yes | QuickBooks Account |
2966+
| `eventTypes` | string | Yes | Event Types |
2967+
2968+
#### Output
2969+
2970+
| Parameter | Type | Description |
2971+
| --------- | ---- | ----------- |
2972+
| `eventId` | string | Intuit webhook event ID |
2973+
| `eventType` | string | Full Intuit CloudEvent type |
2974+
| `entityType` | string | QuickBooks entity type |
2975+
| `action` | string | QuickBooks webhook action |
2976+
| `entityId` | string | QuickBooks entity ID |
2977+
| `realmId` | string | QuickBooks company realm ID |
2978+
| `eventTime` | string | Event timestamp |
2979+
| `specVersion` | string | CloudEvents specification version |
2980+
| `source` | string | Intuit event source |
2981+
| `contentType` | string | Event content type, when provided |
2982+
| `data` | json | Optional event data supplied by Intuit |
2983+
2984+
2985+
---
2986+
2987+
### QuickBooks Customer Events
2988+
2989+
Trigger when selected Customer events occur in QuickBooks
2990+
2991+
#### Configuration
2992+
2993+
| Parameter | Type | Required | Description |
2994+
| --------- | ---- | -------- | ----------- |
2995+
| `triggerCredentials` | string | Yes | QuickBooks Account |
2996+
| `eventTypes` | string | Yes | Event Types |
2997+
2998+
#### Output
2999+
3000+
| Parameter | Type | Description |
3001+
| --------- | ---- | ----------- |
3002+
| `eventId` | string | Intuit webhook event ID |
3003+
| `eventType` | string | Full Intuit CloudEvent type |
3004+
| `entityType` | string | QuickBooks entity type |
3005+
| `action` | string | QuickBooks webhook action |
3006+
| `entityId` | string | QuickBooks entity ID |
3007+
| `realmId` | string | QuickBooks company realm ID |
3008+
| `eventTime` | string | Event timestamp |
3009+
| `specVersion` | string | CloudEvents specification version |
3010+
| `source` | string | Intuit event source |
3011+
| `contentType` | string | Event content type, when provided |
3012+
| `data` | json | Optional event data supplied by Intuit |
3013+
3014+
3015+
---
3016+
3017+
### QuickBooks Invoice Events
3018+
3019+
Trigger when selected Invoice events occur in QuickBooks
3020+
3021+
#### Configuration
3022+
3023+
| Parameter | Type | Required | Description |
3024+
| --------- | ---- | -------- | ----------- |
3025+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3026+
| `eventTypes` | string | Yes | Event Types |
3027+
3028+
#### Output
3029+
3030+
| Parameter | Type | Description |
3031+
| --------- | ---- | ----------- |
3032+
| `eventId` | string | Intuit webhook event ID |
3033+
| `eventType` | string | Full Intuit CloudEvent type |
3034+
| `entityType` | string | QuickBooks entity type |
3035+
| `action` | string | QuickBooks webhook action |
3036+
| `entityId` | string | QuickBooks entity ID |
3037+
| `realmId` | string | QuickBooks company realm ID |
3038+
| `eventTime` | string | Event timestamp |
3039+
| `specVersion` | string | CloudEvents specification version |
3040+
| `source` | string | Intuit event source |
3041+
| `contentType` | string | Event content type, when provided |
3042+
| `data` | json | Optional event data supplied by Intuit |
3043+
3044+
3045+
---
3046+
3047+
### QuickBooks Payment Events
3048+
3049+
Trigger when selected Payment events occur in QuickBooks
3050+
3051+
#### Configuration
3052+
3053+
| Parameter | Type | Required | Description |
3054+
| --------- | ---- | -------- | ----------- |
3055+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3056+
| `eventTypes` | string | Yes | Event Types |
3057+
3058+
#### Output
3059+
3060+
| Parameter | Type | Description |
3061+
| --------- | ---- | ----------- |
3062+
| `eventId` | string | Intuit webhook event ID |
3063+
| `eventType` | string | Full Intuit CloudEvent type |
3064+
| `entityType` | string | QuickBooks entity type |
3065+
| `action` | string | QuickBooks webhook action |
3066+
| `entityId` | string | QuickBooks entity ID |
3067+
| `realmId` | string | QuickBooks company realm ID |
3068+
| `eventTime` | string | Event timestamp |
3069+
| `specVersion` | string | CloudEvents specification version |
3070+
| `source` | string | Intuit event source |
3071+
| `contentType` | string | Event content type, when provided |
3072+
| `data` | json | Optional event data supplied by Intuit |
3073+
3074+
3075+
---
3076+
3077+
### QuickBooks Purchase Order Events
3078+
3079+
Trigger when selected Purchase Order events occur in QuickBooks
3080+
3081+
#### Configuration
3082+
3083+
| Parameter | Type | Required | Description |
3084+
| --------- | ---- | -------- | ----------- |
3085+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3086+
| `eventTypes` | string | Yes | Event Types |
3087+
3088+
#### Output
3089+
3090+
| Parameter | Type | Description |
3091+
| --------- | ---- | ----------- |
3092+
| `eventId` | string | Intuit webhook event ID |
3093+
| `eventType` | string | Full Intuit CloudEvent type |
3094+
| `entityType` | string | QuickBooks entity type |
3095+
| `action` | string | QuickBooks webhook action |
3096+
| `entityId` | string | QuickBooks entity ID |
3097+
| `realmId` | string | QuickBooks company realm ID |
3098+
| `eventTime` | string | Event timestamp |
3099+
| `specVersion` | string | CloudEvents specification version |
3100+
| `source` | string | Intuit event source |
3101+
| `contentType` | string | Event content type, when provided |
3102+
| `data` | json | Optional event data supplied by Intuit |
3103+
3104+
3105+
---
3106+
3107+
### QuickBooks Vendor Events
3108+
3109+
Trigger when selected Vendor events occur in QuickBooks
3110+
3111+
#### Configuration
3112+
3113+
| Parameter | Type | Required | Description |
3114+
| --------- | ---- | -------- | ----------- |
3115+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3116+
| `eventTypes` | string | Yes | Event Types |
3117+
3118+
#### Output
3119+
3120+
| Parameter | Type | Description |
3121+
| --------- | ---- | ----------- |
3122+
| `eventId` | string | Intuit webhook event ID |
3123+
| `eventType` | string | Full Intuit CloudEvent type |
3124+
| `entityType` | string | QuickBooks entity type |
3125+
| `action` | string | QuickBooks webhook action |
3126+
| `entityId` | string | QuickBooks entity ID |
3127+
| `realmId` | string | QuickBooks company realm ID |
3128+
| `eventTime` | string | Event timestamp |
3129+
| `specVersion` | string | CloudEvents specification version |
3130+
| `source` | string | Intuit event source |
3131+
| `contentType` | string | Event content type, when provided |
3132+
| `data` | json | Optional event data supplied by Intuit |
3133+

apps/sim/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
125125
# QUICKBOOKS_CLIENT_ID=
126126
# QUICKBOOKS_CLIENT_SECRET=
127127
# QUICKBOOKS_ENV=sandbox # Required when QuickBooks is configured: sandbox or production
128+
# QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN= # Verifier token from the Intuit webhook configuration
128129

129130
# Azure Blob Storage takes precedence over S3 if both are configured
130131
# AZURE_ACCOUNT_NAME= # Azure storage account name
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/** @vitest-environment node */
2+
3+
import crypto from 'node:crypto'
4+
import { requestUtilsMockFns, resetEnvMock, setEnv } from '@sim/testing'
5+
import { NextRequest } from 'next/server'
6+
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const { mockEnqueue, mockRelease } = vi.hoisted(() => ({
9+
mockEnqueue: vi.fn(),
10+
mockRelease: vi.fn(),
11+
}))
12+
13+
vi.mock('@/background/quickbooks-webhook-ingress', () => ({
14+
enqueueQuickBooksWebhookIngress: mockEnqueue,
15+
}))
16+
vi.mock('@/lib/core/admission/gate', () => ({
17+
admissionRejectedResponse: vi.fn(() => new Response(null, { status: 503 })),
18+
tryAdmit: vi.fn(() => ({ release: mockRelease })),
19+
}))
20+
vi.mock('@/lib/core/utils/with-route-handler', () => ({
21+
withRouteHandler:
22+
(handler: (request: NextRequest) => Promise<Response>) => (request: NextRequest) =>
23+
handler(request),
24+
}))
25+
26+
import { POST } from '@/app/api/webhooks/quickbooks/route'
27+
28+
const validEvent = {
29+
specversion: '1.0',
30+
id: 'event-1',
31+
source: 'quickbooks-online',
32+
type: 'qbo.invoice.created.v1',
33+
time: '2026-08-03T12:00:00Z',
34+
intuitentityid: '123',
35+
intuitaccountid: '456',
36+
}
37+
38+
function request(body: string, signature?: string): NextRequest {
39+
return new NextRequest('http://localhost/api/webhooks/quickbooks', {
40+
method: 'POST',
41+
headers: {
42+
'content-type': 'application/json',
43+
...(signature ? { 'intuit-signature': signature } : {}),
44+
},
45+
body,
46+
})
47+
}
48+
49+
function signedRequest(value: unknown): NextRequest {
50+
const body = JSON.stringify(value)
51+
const signature = crypto.createHmac('sha256', 'verifier').update(body).digest('base64')
52+
return request(body, signature)
53+
}
54+
55+
describe('QuickBooks webhook ingress route', () => {
56+
beforeEach(() => {
57+
vi.clearAllMocks()
58+
setEnv({ QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN: 'verifier' })
59+
requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('request-1')
60+
mockEnqueue.mockResolvedValue('job-1')
61+
})
62+
afterAll(() => {
63+
resetEnvMock()
64+
requestUtilsMockFns.mockGenerateRequestId.mockReset()
65+
})
66+
67+
it('accepts a signed multi-company batch only after durable enqueue', async () => {
68+
const response = await POST(
69+
signedRequest([validEvent, { ...validEvent, id: 'event-2', intuitaccountid: '789' }])
70+
)
71+
expect(response.status).toBe(200)
72+
expect(mockEnqueue).toHaveBeenCalledWith(
73+
expect.objectContaining({
74+
events: [validEvent, { ...validEvent, id: 'event-2', intuitaccountid: '789' }],
75+
requestId: 'request-1',
76+
})
77+
)
78+
expect(mockRelease).toHaveBeenCalledOnce()
79+
})
80+
81+
it('rejects missing signatures and malformed signed payloads before enqueue', async () => {
82+
expect((await POST(request(JSON.stringify([validEvent])))).status).toBe(401)
83+
expect((await POST(signedRequest({ invalid: true }))).status).toBe(400)
84+
expect(mockEnqueue).not.toHaveBeenCalled()
85+
})
86+
87+
it('rejects batches over the 1,000 event bound', async () => {
88+
const events = Array.from({ length: 1001 }, (_, index) => ({
89+
...validEvent,
90+
id: `event-${index}`,
91+
}))
92+
expect((await POST(signedRequest(events))).status).toBe(400)
93+
expect(mockEnqueue).not.toHaveBeenCalled()
94+
})
95+
96+
it('returns 503 when durable acceptance fails', async () => {
97+
mockEnqueue.mockRejectedValue(new Error('queue unavailable'))
98+
expect((await POST(signedRequest([validEvent]))).status).toBe(503)
99+
})
100+
})

0 commit comments

Comments
 (0)