Skip to content

Commit 6a99c79

Browse files
Bill Leoutsakoswaleedlatif1
authored andcommitted
feat(quickbooks): add core webhook triggers
1 parent 29450bd commit 6a99c79

27 files changed

Lines changed: 1481 additions & 3 deletions

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

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2959,3 +2959,215 @@ Download a QuickBooks file attachment as a stored Sim file
29592959
| `attachmentId` | string | Downloaded QuickBooks attachment ID |
29602960

29612961

2962+
2963+
## Triggers
2964+
2965+
A **Trigger** is a block that starts a workflow when an event happens in this service.
2966+
2967+
### QuickBooks Bill Events
2968+
2969+
Trigger when selected Bill events occur in QuickBooks
2970+
2971+
#### Configuration
2972+
2973+
| Parameter | Type | Required | Description |
2974+
| --------- | ---- | -------- | ----------- |
2975+
| `triggerCredentials` | string | Yes | QuickBooks Account |
2976+
| `eventTypes` | string | Yes | Event Types |
2977+
2978+
#### Output
2979+
2980+
| Parameter | Type | Description |
2981+
| --------- | ---- | ----------- |
2982+
| `eventId` | string | Intuit webhook event ID |
2983+
| `eventType` | string | Full Intuit CloudEvent type |
2984+
| `entityType` | string | QuickBooks entity type |
2985+
| `action` | string | QuickBooks webhook action |
2986+
| `entityId` | string | QuickBooks entity ID |
2987+
| `realmId` | string | QuickBooks company realm ID |
2988+
| `eventTime` | string | Event timestamp |
2989+
| `specVersion` | string | CloudEvents specification version |
2990+
| `source` | string | Intuit event source |
2991+
| `contentType` | string | Event content type, when provided |
2992+
| `data` | json | Optional event data supplied by Intuit |
2993+
2994+
2995+
---
2996+
2997+
### QuickBooks Bill Payment Events
2998+
2999+
Trigger when selected Bill Payment events occur in QuickBooks
3000+
3001+
#### Configuration
3002+
3003+
| Parameter | Type | Required | Description |
3004+
| --------- | ---- | -------- | ----------- |
3005+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3006+
| `eventTypes` | string | Yes | Event Types |
3007+
3008+
#### Output
3009+
3010+
| Parameter | Type | Description |
3011+
| --------- | ---- | ----------- |
3012+
| `eventId` | string | Intuit webhook event ID |
3013+
| `eventType` | string | Full Intuit CloudEvent type |
3014+
| `entityType` | string | QuickBooks entity type |
3015+
| `action` | string | QuickBooks webhook action |
3016+
| `entityId` | string | QuickBooks entity ID |
3017+
| `realmId` | string | QuickBooks company realm ID |
3018+
| `eventTime` | string | Event timestamp |
3019+
| `specVersion` | string | CloudEvents specification version |
3020+
| `source` | string | Intuit event source |
3021+
| `contentType` | string | Event content type, when provided |
3022+
| `data` | json | Optional event data supplied by Intuit |
3023+
3024+
3025+
---
3026+
3027+
### QuickBooks Customer Events
3028+
3029+
Trigger when selected Customer events occur in QuickBooks
3030+
3031+
#### Configuration
3032+
3033+
| Parameter | Type | Required | Description |
3034+
| --------- | ---- | -------- | ----------- |
3035+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3036+
| `eventTypes` | string | Yes | Event Types |
3037+
3038+
#### Output
3039+
3040+
| Parameter | Type | Description |
3041+
| --------- | ---- | ----------- |
3042+
| `eventId` | string | Intuit webhook event ID |
3043+
| `eventType` | string | Full Intuit CloudEvent type |
3044+
| `entityType` | string | QuickBooks entity type |
3045+
| `action` | string | QuickBooks webhook action |
3046+
| `entityId` | string | QuickBooks entity ID |
3047+
| `realmId` | string | QuickBooks company realm ID |
3048+
| `eventTime` | string | Event timestamp |
3049+
| `specVersion` | string | CloudEvents specification version |
3050+
| `source` | string | Intuit event source |
3051+
| `contentType` | string | Event content type, when provided |
3052+
| `data` | json | Optional event data supplied by Intuit |
3053+
3054+
3055+
---
3056+
3057+
### QuickBooks Invoice Events
3058+
3059+
Trigger when selected Invoice events occur in QuickBooks
3060+
3061+
#### Configuration
3062+
3063+
| Parameter | Type | Required | Description |
3064+
| --------- | ---- | -------- | ----------- |
3065+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3066+
| `eventTypes` | string | Yes | Event Types |
3067+
3068+
#### Output
3069+
3070+
| Parameter | Type | Description |
3071+
| --------- | ---- | ----------- |
3072+
| `eventId` | string | Intuit webhook event ID |
3073+
| `eventType` | string | Full Intuit CloudEvent type |
3074+
| `entityType` | string | QuickBooks entity type |
3075+
| `action` | string | QuickBooks webhook action |
3076+
| `entityId` | string | QuickBooks entity ID |
3077+
| `realmId` | string | QuickBooks company realm ID |
3078+
| `eventTime` | string | Event timestamp |
3079+
| `specVersion` | string | CloudEvents specification version |
3080+
| `source` | string | Intuit event source |
3081+
| `contentType` | string | Event content type, when provided |
3082+
| `data` | json | Optional event data supplied by Intuit |
3083+
3084+
3085+
---
3086+
3087+
### QuickBooks Payment Events
3088+
3089+
Trigger when selected Payment events occur in QuickBooks
3090+
3091+
#### Configuration
3092+
3093+
| Parameter | Type | Required | Description |
3094+
| --------- | ---- | -------- | ----------- |
3095+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3096+
| `eventTypes` | string | Yes | Event Types |
3097+
3098+
#### Output
3099+
3100+
| Parameter | Type | Description |
3101+
| --------- | ---- | ----------- |
3102+
| `eventId` | string | Intuit webhook event ID |
3103+
| `eventType` | string | Full Intuit CloudEvent type |
3104+
| `entityType` | string | QuickBooks entity type |
3105+
| `action` | string | QuickBooks webhook action |
3106+
| `entityId` | string | QuickBooks entity ID |
3107+
| `realmId` | string | QuickBooks company realm ID |
3108+
| `eventTime` | string | Event timestamp |
3109+
| `specVersion` | string | CloudEvents specification version |
3110+
| `source` | string | Intuit event source |
3111+
| `contentType` | string | Event content type, when provided |
3112+
| `data` | json | Optional event data supplied by Intuit |
3113+
3114+
3115+
---
3116+
3117+
### QuickBooks Purchase Order Events
3118+
3119+
Trigger when selected Purchase Order events occur in QuickBooks
3120+
3121+
#### Configuration
3122+
3123+
| Parameter | Type | Required | Description |
3124+
| --------- | ---- | -------- | ----------- |
3125+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3126+
| `eventTypes` | string | Yes | Event Types |
3127+
3128+
#### Output
3129+
3130+
| Parameter | Type | Description |
3131+
| --------- | ---- | ----------- |
3132+
| `eventId` | string | Intuit webhook event ID |
3133+
| `eventType` | string | Full Intuit CloudEvent type |
3134+
| `entityType` | string | QuickBooks entity type |
3135+
| `action` | string | QuickBooks webhook action |
3136+
| `entityId` | string | QuickBooks entity ID |
3137+
| `realmId` | string | QuickBooks company realm ID |
3138+
| `eventTime` | string | Event timestamp |
3139+
| `specVersion` | string | CloudEvents specification version |
3140+
| `source` | string | Intuit event source |
3141+
| `contentType` | string | Event content type, when provided |
3142+
| `data` | json | Optional event data supplied by Intuit |
3143+
3144+
3145+
---
3146+
3147+
### QuickBooks Vendor Events
3148+
3149+
Trigger when selected Vendor events occur in QuickBooks
3150+
3151+
#### Configuration
3152+
3153+
| Parameter | Type | Required | Description |
3154+
| --------- | ---- | -------- | ----------- |
3155+
| `triggerCredentials` | string | Yes | QuickBooks Account |
3156+
| `eventTypes` | string | Yes | Event Types |
3157+
3158+
#### Output
3159+
3160+
| Parameter | Type | Description |
3161+
| --------- | ---- | ----------- |
3162+
| `eventId` | string | Intuit webhook event ID |
3163+
| `eventType` | string | Full Intuit CloudEvent type |
3164+
| `entityType` | string | QuickBooks entity type |
3165+
| `action` | string | QuickBooks webhook action |
3166+
| `entityId` | string | QuickBooks entity ID |
3167+
| `realmId` | string | QuickBooks company realm ID |
3168+
| `eventTime` | string | Event timestamp |
3169+
| `specVersion` | string | CloudEvents specification version |
3170+
| `source` | string | Intuit event source |
3171+
| `contentType` | string | Event content type, when provided |
3172+
| `data` | json | Optional event data supplied by Intuit |
3173+

‎apps/sim/.env.example‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
153153
# TIKTOK_CLIENT_ID=
154154
# TIKTOK_CLIENT_SECRET=
155155

156-
# Azure Blob Storage
156+
# Azure Blob Storage takes precedence over S3 if both are configured
157157
# AZURE_ACCOUNT_NAME= # Azure storage account name
158158
# AZURE_ACCOUNT_KEY= # Azure storage account key
159159
# AZURE_CONNECTION_STRING= # Alternative to account name/key
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)