From 3f585944f66ae0c07ba9a674fb77c87a95f807b2 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 4 Aug 2026 00:00:27 -0700 Subject: [PATCH 1/9] feat(quickbooks): add core webhook triggers --- .../docs/en/integrations/quickbooks.mdx | 212 ++++++++++++++++++ apps/sim/.env.example | 1 + .../app/api/webhooks/quickbooks/route.test.ts | 100 +++++++++ apps/sim/app/api/webhooks/quickbooks/route.ts | 97 ++++++++ .../quickbooks-webhook-ingress.test.ts | 101 +++++++++ .../background/quickbooks-webhook-ingress.ts | 130 +++++++++++ .../quickbooks-webhook-targets.test.ts | 98 ++++++++ .../background/quickbooks-webhook-targets.ts | 112 +++++++++ apps/sim/blocks/blocks/quickbooks.ts | 21 ++ apps/sim/lib/api/contracts/webhooks.ts | 29 +++ .../core/async-jobs/backends/trigger-dev.ts | 1 + apps/sim/lib/core/async-jobs/types.ts | 1 + apps/sim/lib/core/config/env.ts | 1 + apps/sim/lib/integrations/integrations.json | 42 +++- .../lib/webhooks/providers/quickbooks.test.ts | 76 +++++++ apps/sim/lib/webhooks/providers/quickbooks.ts | 87 +++++++ apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/triggers/quickbooks/bill_events.ts | 19 ++ .../quickbooks/bill_payment_events.ts | 19 ++ .../triggers/quickbooks/customer_events.ts | 19 ++ apps/sim/triggers/quickbooks/index.ts | 7 + .../sim/triggers/quickbooks/invoice_events.ts | 31 +++ .../sim/triggers/quickbooks/payment_events.ts | 19 ++ .../quickbooks/purchase_order_events.ts | 19 ++ .../triggers/quickbooks/quickbooks.test.ts | 30 +++ apps/sim/triggers/quickbooks/utils.ts | 163 ++++++++++++++ apps/sim/triggers/quickbooks/vendor_events.ts | 19 ++ apps/sim/triggers/registry.ts | 16 ++ scripts/check-api-validation-contracts.ts | 4 +- 29 files changed, 1471 insertions(+), 5 deletions(-) create mode 100644 apps/sim/app/api/webhooks/quickbooks/route.test.ts create mode 100644 apps/sim/app/api/webhooks/quickbooks/route.ts create mode 100644 apps/sim/background/quickbooks-webhook-ingress.test.ts create mode 100644 apps/sim/background/quickbooks-webhook-ingress.ts create mode 100644 apps/sim/background/quickbooks-webhook-targets.test.ts create mode 100644 apps/sim/background/quickbooks-webhook-targets.ts create mode 100644 apps/sim/lib/webhooks/providers/quickbooks.test.ts create mode 100644 apps/sim/lib/webhooks/providers/quickbooks.ts create mode 100644 apps/sim/triggers/quickbooks/bill_events.ts create mode 100644 apps/sim/triggers/quickbooks/bill_payment_events.ts create mode 100644 apps/sim/triggers/quickbooks/customer_events.ts create mode 100644 apps/sim/triggers/quickbooks/index.ts create mode 100644 apps/sim/triggers/quickbooks/invoice_events.ts create mode 100644 apps/sim/triggers/quickbooks/payment_events.ts create mode 100644 apps/sim/triggers/quickbooks/purchase_order_events.ts create mode 100644 apps/sim/triggers/quickbooks/quickbooks.test.ts create mode 100644 apps/sim/triggers/quickbooks/utils.ts create mode 100644 apps/sim/triggers/quickbooks/vendor_events.ts diff --git a/apps/docs/content/docs/en/integrations/quickbooks.mdx b/apps/docs/content/docs/en/integrations/quickbooks.mdx index cf2222d54c4..f56e15d647d 100644 --- a/apps/docs/content/docs/en/integrations/quickbooks.mdx +++ b/apps/docs/content/docs/en/integrations/quickbooks.mdx @@ -2919,3 +2919,215 @@ Download a QuickBooks file attachment through its short-lived URL | `attachmentId` | string | Downloaded QuickBooks attachment ID | + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### QuickBooks Bill Events + +Trigger when selected Bill events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Bill Payment Events + +Trigger when selected Bill Payment events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Customer Events + +Trigger when selected Customer events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Invoice Events + +Trigger when selected Invoice events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Payment Events + +Trigger when selected Payment events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Purchase Order Events + +Trigger when selected Purchase Order events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Vendor Events + +Trigger when selected Vendor events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + diff --git a/apps/sim/.env.example b/apps/sim/.env.example index fc275846d34..a31be15a207 100644 --- a/apps/sim/.env.example +++ b/apps/sim/.env.example @@ -125,6 +125,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic # QUICKBOOKS_CLIENT_ID= # QUICKBOOKS_CLIENT_SECRET= # QUICKBOOKS_ENV=sandbox # Required when QuickBooks is configured: sandbox or production +# QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN= # Verifier token from the Intuit webhook configuration # Azure Blob Storage takes precedence over S3 if both are configured # AZURE_ACCOUNT_NAME= # Azure storage account name diff --git a/apps/sim/app/api/webhooks/quickbooks/route.test.ts b/apps/sim/app/api/webhooks/quickbooks/route.test.ts new file mode 100644 index 00000000000..9719f288acd --- /dev/null +++ b/apps/sim/app/api/webhooks/quickbooks/route.test.ts @@ -0,0 +1,100 @@ +/** @vitest-environment node */ + +import crypto from 'node:crypto' +import { requestUtilsMockFns, resetEnvMock, setEnv } from '@sim/testing' +import { NextRequest } from 'next/server' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockEnqueue, mockRelease } = vi.hoisted(() => ({ + mockEnqueue: vi.fn(), + mockRelease: vi.fn(), +})) + +vi.mock('@/background/quickbooks-webhook-ingress', () => ({ + enqueueQuickBooksWebhookIngress: mockEnqueue, +})) +vi.mock('@/lib/core/admission/gate', () => ({ + admissionRejectedResponse: vi.fn(() => new Response(null, { status: 503 })), + tryAdmit: vi.fn(() => ({ release: mockRelease })), +})) +vi.mock('@/lib/core/utils/with-route-handler', () => ({ + withRouteHandler: + (handler: (request: NextRequest) => Promise) => (request: NextRequest) => + handler(request), +})) + +import { POST } from '@/app/api/webhooks/quickbooks/route' + +const validEvent = { + specversion: '1.0', + id: 'event-1', + source: 'quickbooks-online', + type: 'qbo.invoice.created.v1', + time: '2026-08-03T12:00:00Z', + intuitentityid: '123', + intuitaccountid: '456', +} + +function request(body: string, signature?: string): NextRequest { + return new NextRequest('http://localhost/api/webhooks/quickbooks', { + method: 'POST', + headers: { + 'content-type': 'application/json', + ...(signature ? { 'intuit-signature': signature } : {}), + }, + body, + }) +} + +function signedRequest(value: unknown): NextRequest { + const body = JSON.stringify(value) + const signature = crypto.createHmac('sha256', 'verifier').update(body).digest('base64') + return request(body, signature) +} + +describe('QuickBooks webhook ingress route', () => { + beforeEach(() => { + vi.clearAllMocks() + setEnv({ QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN: 'verifier' }) + requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('request-1') + mockEnqueue.mockResolvedValue('job-1') + }) + afterAll(() => { + resetEnvMock() + requestUtilsMockFns.mockGenerateRequestId.mockReset() + }) + + it('accepts a signed multi-company batch only after durable enqueue', async () => { + const response = await POST( + signedRequest([validEvent, { ...validEvent, id: 'event-2', intuitaccountid: '789' }]) + ) + expect(response.status).toBe(200) + expect(mockEnqueue).toHaveBeenCalledWith( + expect.objectContaining({ + events: [validEvent, { ...validEvent, id: 'event-2', intuitaccountid: '789' }], + requestId: 'request-1', + }) + ) + expect(mockRelease).toHaveBeenCalledOnce() + }) + + it('rejects missing signatures and malformed signed payloads before enqueue', async () => { + expect((await POST(request(JSON.stringify([validEvent])))).status).toBe(401) + expect((await POST(signedRequest({ invalid: true }))).status).toBe(400) + expect(mockEnqueue).not.toHaveBeenCalled() + }) + + it('rejects batches over the 1,000 event bound', async () => { + const events = Array.from({ length: 1001 }, (_, index) => ({ + ...validEvent, + id: `event-${index}`, + })) + expect((await POST(signedRequest(events))).status).toBe(400) + expect(mockEnqueue).not.toHaveBeenCalled() + }) + + it('returns 503 when durable acceptance fails', async () => { + mockEnqueue.mockRejectedValue(new Error('queue unavailable')) + expect((await POST(signedRequest([validEvent]))).status).toBe(503) + }) +}) diff --git a/apps/sim/app/api/webhooks/quickbooks/route.ts b/apps/sim/app/api/webhooks/quickbooks/route.ts new file mode 100644 index 00000000000..fd79e800d46 --- /dev/null +++ b/apps/sim/app/api/webhooks/quickbooks/route.ts @@ -0,0 +1,97 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { quickBooksWebhookEventsSchema } from '@/lib/api/contracts/webhooks' +import { admissionRejectedResponse, tryAdmit } from '@/lib/core/admission/gate' +import { generateRequestId } from '@/lib/core/utils/request' +import { + assertContentLengthWithinLimit, + isPayloadSizeLimitError, + readStreamToBufferWithLimit, +} from '@/lib/core/utils/stream-limits' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { WEBHOOK_MAX_BODY_BYTES } from '@/lib/webhooks/constants' +import { verifyQuickBooksSignature } from '@/lib/webhooks/providers/quickbooks' +import { + enqueueQuickBooksWebhookIngress, + type QuickBooksWebhookIngressPayload, +} from '@/background/quickbooks-webhook-ingress' + +const logger = createLogger('QuickBooksWebhookIngress') +const BODY_LABEL = 'QuickBooks webhook body' + +export const dynamic = 'force-dynamic' +export const runtime = 'nodejs' +export const maxDuration = 60 + +async function readBody(request: Request): Promise { + assertContentLengthWithinLimit(request.headers, WEBHOOK_MAX_BODY_BYTES, BODY_LABEL) + const buffer = await readStreamToBufferWithLimit(request.body, { + maxBytes: WEBHOOK_MAX_BODY_BYTES, + label: BODY_LABEL, + }) + return new TextDecoder().decode(buffer) +} + +/** App-level Intuit callback. Verifies raw bytes and durably accepts before fanout. */ +export const POST = withRouteHandler(async (request: NextRequest) => { + const ticket = tryAdmit() + if (!ticket) return admissionRejectedResponse() + + const requestId = generateRequestId() + const receivedAt = Date.now() + try { + let rawBody: string + try { + rawBody = await readBody(request) + } catch (error) { + if (isPayloadSizeLimitError(error)) { + return NextResponse.json({ error: 'Request body too large' }, { status: 413 }) + } + throw error + } + + const authError = verifyQuickBooksSignature( + rawBody, + request.headers.get('intuit-signature'), + requestId + ) + if (authError) return authError + + let json: unknown + try { + json = JSON.parse(rawBody) + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) + } + const parsed = quickBooksWebhookEventsSchema.safeParse(json) + if (!parsed.success) { + logger.warn(`[${requestId}] Invalid QuickBooks webhook envelope`, { + issues: parsed.error.issues, + }) + return NextResponse.json({ error: 'Invalid webhook envelope' }, { status: 400 }) + } + + const payload: QuickBooksWebhookIngressPayload = { + events: parsed.data, + headers: { + 'content-type': request.headers.get('content-type') ?? 'application/json', + }, + requestId, + receivedAt, + } + const jobId = await enqueueQuickBooksWebhookIngress(payload) + logger.info(`[${requestId}] Accepted QuickBooks webhook delivery`, { + eventCount: parsed.data.length, + jobId, + }) + return NextResponse.json({ ok: true }) + } catch (error) { + logger.error(`[${requestId}] QuickBooks webhook ingress error`, { + error: getErrorMessage(error, 'Unknown error'), + }) + return NextResponse.json({ error: 'Temporarily unable to accept webhook' }, { status: 503 }) + } finally { + ticket.release() + } +}) diff --git a/apps/sim/background/quickbooks-webhook-ingress.test.ts b/apps/sim/background/quickbooks-webhook-ingress.test.ts new file mode 100644 index 00000000000..1127bbb2d66 --- /dev/null +++ b/apps/sim/background/quickbooks-webhook-ingress.test.ts @@ -0,0 +1,101 @@ +/** @vitest-environment node */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockDispatch, mockEnqueue, mockFindPage } = vi.hoisted(() => ({ + mockDispatch: vi.fn(), + mockEnqueue: vi.fn(), + mockFindPage: vi.fn(), +})) +vi.mock('@trigger.dev/sdk', () => ({ task: vi.fn((config: unknown) => config) })) +vi.mock('@/lib/webhooks/processor', () => ({ dispatchResolvedWebhookTarget: mockDispatch })) +vi.mock('@/background/quickbooks-webhook-targets', () => ({ + findQuickBooksWebhookTargetPage: mockFindPage, +})) +vi.mock('@/lib/core/async-jobs', () => ({ + getJobQueue: vi.fn(async () => ({ enqueue: mockEnqueue })), +})) + +import { + enqueueQuickBooksWebhookIngress, + executeQuickBooksWebhookIngress, + type QuickBooksWebhookIngressPayload, +} from '@/background/quickbooks-webhook-ingress' + +const event = { + specversion: '1.0', + id: 'event-1', + source: 'quickbooks-online', + type: 'qbo.invoice.created.v1', + time: '2026-08-03T12:00:00Z', + intuitentityid: '123', + intuitaccountid: '456', +} +const payload: QuickBooksWebhookIngressPayload = { + events: [event, { ...event, id: 'event-2', intuitaccountid: '789' }], + headers: { 'content-type': 'application/json' }, + requestId: 'request-1', + receivedAt: 1, +} + +describe('QuickBooks webhook ingress job', () => { + beforeEach(() => { + vi.clearAllMocks() + mockEnqueue.mockResolvedValue('job-1') + }) + + it('routes one event to one company and dispatches targets sequentially', async () => { + const order: string[] = [] + mockFindPage.mockResolvedValue({ + hasMore: false, + nextCursor: null, + targets: [ + { webhook: { id: 'w1' }, workflow: { id: 'wf1' } }, + { webhook: { id: 'w2' }, workflow: { id: 'wf2' } }, + ], + }) + mockDispatch.mockImplementation(async (webhook: { id: string }) => { + order.push(webhook.id) + return { outcome: 'queued' } + }) + await expect(executeQuickBooksWebhookIngress(payload)).resolves.toEqual({ + ignored: 0, + processed: 2, + targetCount: 2, + }) + expect(mockFindPage).toHaveBeenCalledWith('456', 'request-1', undefined) + expect(order).toEqual(['w1', 'w2']) + }) + + it('continues target pages before advancing to the next event', async () => { + mockFindPage.mockResolvedValue({ + hasMore: true, + nextCursor: 'webhook-100', + targets: [], + }) + await enqueueQuickBooksWebhookIngress(payload) + const options = mockEnqueue.mock.calls[0][2] as { runner: () => Promise } + await options.runner() + expect(mockEnqueue).toHaveBeenNthCalledWith( + 2, + 'quickbooks-webhook-ingress', + expect.objectContaining({ afterWebhookId: 'webhook-100' }), + expect.objectContaining({ + jobId: 'quickbooks-webhook-ingress:request-1:0:webhook-100', + }) + ) + }) + + it('advances to the next event only after the current event finishes', async () => { + mockFindPage.mockResolvedValue({ hasMore: false, nextCursor: null, targets: [] }) + await enqueueQuickBooksWebhookIngress(payload) + const options = mockEnqueue.mock.calls[0][2] as { runner: () => Promise } + await options.runner() + expect(mockEnqueue).toHaveBeenNthCalledWith( + 2, + 'quickbooks-webhook-ingress', + expect.objectContaining({ eventIndex: 1, afterWebhookId: undefined }), + expect.objectContaining({ jobId: 'quickbooks-webhook-ingress:request-1:1:root' }) + ) + }) +}) diff --git a/apps/sim/background/quickbooks-webhook-ingress.ts b/apps/sim/background/quickbooks-webhook-ingress.ts new file mode 100644 index 00000000000..5c0ac6e8a98 --- /dev/null +++ b/apps/sim/background/quickbooks-webhook-ingress.ts @@ -0,0 +1,130 @@ +import { createLogger } from '@sim/logger' +import { task } from '@trigger.dev/sdk' +import { NextRequest } from 'next/server' +import type { QuickBooksWebhookEvent } from '@/lib/api/contracts/webhooks' +import { getJobQueue } from '@/lib/core/async-jobs' +import { dispatchResolvedWebhookTarget } from '@/lib/webhooks/processor' +import { findQuickBooksWebhookTargetPage } from '@/background/quickbooks-webhook-targets' + +const logger = createLogger('QuickBooksWebhookIngressTask') + +export const QUICKBOOKS_WEBHOOK_INGRESS_CONCURRENCY_LIMIT = 50 +export const QUICKBOOKS_WEBHOOK_INGRESS_MAX_ATTEMPTS = 3 + +export interface QuickBooksWebhookIngressPayload { + afterWebhookId?: string + eventIndex?: number + events: QuickBooksWebhookEvent[] + headers: { 'content-type': string } + requestId: string + receivedAt: number +} + +export interface QuickBooksWebhookIngressResult { + ignored: number + nextCursor?: string + processed: number + targetCount: number +} + +/** Process one event against one bounded target page. */ +export async function executeQuickBooksWebhookIngress( + payload: QuickBooksWebhookIngressPayload +): Promise { + const eventIndex = payload.eventIndex ?? 0 + const event = payload.events[eventIndex] + if (!event) return { ignored: 0, processed: 0, targetCount: 0 } + + const request = new NextRequest('http://internal/api/webhooks/quickbooks', { + method: 'POST', + headers: payload.headers, + body: JSON.stringify(event), + }) + const page = await findQuickBooksWebhookTargetPage( + event.intuitaccountid, + payload.requestId, + payload.afterWebhookId + ) + const nextCursor = page.hasMore ? page.nextCursor : null + if (page.hasMore && (!nextCursor || nextCursor === payload.afterWebhookId)) { + throw new Error('QuickBooks webhook target pagination did not advance') + } + + let ignored = 0 + let processed = 0 + let failed = 0 + for (const { webhook, workflow } of page.targets) { + const result = await dispatchResolvedWebhookTarget(webhook, workflow, event, request, { + requestId: payload.requestId, + path: webhook.path ?? undefined, + receivedAt: payload.receivedAt, + triggerTimestampMs: Date.parse(event.time), + }) + if (result.outcome === 'queued') processed += 1 + else if (result.outcome === 'ignored') ignored += 1 + else failed += 1 + } + + if (failed > 0) { + throw new Error(`Failed to dispatch ${failed} of ${page.targets.length} QuickBooks targets`) + } + + logger.info(`[${payload.requestId}] QuickBooks webhook page completed`, { + eventId: event.id, + eventIndex, + ignored, + processed, + targetCount: page.targets.length, + }) + return { + ignored, + processed, + targetCount: page.targets.length, + ...(nextCursor ? { nextCursor } : {}), + } +} + +async function runQuickBooksWebhookIngressJob( + payload: QuickBooksWebhookIngressPayload +): Promise { + const eventIndex = payload.eventIndex ?? 0 + const result = await executeQuickBooksWebhookIngress(payload) + if (result.nextCursor) { + await enqueueQuickBooksWebhookIngress({ ...payload, afterWebhookId: result.nextCursor }) + return + } + if (eventIndex + 1 < payload.events.length) { + await enqueueQuickBooksWebhookIngress({ + ...payload, + eventIndex: eventIndex + 1, + afterWebhookId: undefined, + }) + } +} + +export async function enqueueQuickBooksWebhookIngress( + payload: QuickBooksWebhookIngressPayload +): Promise { + const jobQueue = await getJobQueue() + const eventIndex = payload.eventIndex ?? 0 + return jobQueue.enqueue('quickbooks-webhook-ingress', payload, { + jobId: `quickbooks-webhook-ingress:${payload.requestId}:${eventIndex}:${payload.afterWebhookId ?? 'root'}`, + maxAttempts: QUICKBOOKS_WEBHOOK_INGRESS_MAX_ATTEMPTS, + concurrencyKey: 'quickbooks-webhook-ingress', + concurrencyLimit: QUICKBOOKS_WEBHOOK_INGRESS_CONCURRENCY_LIMIT, + runner: async () => runQuickBooksWebhookIngressJob(payload), + }) +} + +export const quickBooksWebhookIngressTask = task({ + id: 'quickbooks-webhook-ingress', + machine: 'small-1x', + retry: { + maxAttempts: QUICKBOOKS_WEBHOOK_INGRESS_MAX_ATTEMPTS, + factor: 2, + minTimeoutInMs: 1000, + maxTimeoutInMs: 10_000, + }, + queue: { concurrencyLimit: QUICKBOOKS_WEBHOOK_INGRESS_CONCURRENCY_LIMIT }, + run: async (payload: QuickBooksWebhookIngressPayload) => runQuickBooksWebhookIngressJob(payload), +}) diff --git a/apps/sim/background/quickbooks-webhook-targets.test.ts b/apps/sim/background/quickbooks-webhook-targets.test.ts new file mode 100644 index 00000000000..af0e24c64c4 --- /dev/null +++ b/apps/sim/background/quickbooks-webhook-targets.test.ts @@ -0,0 +1,98 @@ +/** @vitest-environment node */ + +import { dbChainMock, dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockCredentialExpression, mockEq, mockGt, tables } = vi.hoisted(() => ({ + mockCredentialExpression: vi.fn(() => 'webhook.credentialId'), + mockEq: vi.fn((left: unknown, right: unknown) => ({ left, right })), + mockGt: vi.fn((left: unknown, right: unknown) => ({ gt: [left, right] })), + tables: { + account: { id: 'account.id', accountId: 'account.accountId', providerId: 'account.providerId' }, + credential: { + id: 'credential.id', + accountId: 'credential.accountId', + providerId: 'credential.providerId', + type: 'credential.type', + workspaceId: 'credential.workspaceId', + }, + webhook: { + deploymentVersionId: 'webhook.deploymentVersionId', + isActive: 'webhook.isActive', + id: 'webhook.id', + archivedAt: 'webhook.archivedAt', + provider: 'webhook.provider', + providerConfig: 'webhook.providerConfig', + workflowId: 'webhook.workflowId', + }, + workflow: { + id: 'workflow.id', + workspaceId: 'workflow.workspaceId', + archivedAt: 'workflow.archivedAt', + }, + workflowDeploymentVersion: { + id: 'workflowDeploymentVersion.id', + workflowId: 'workflowDeploymentVersion.workflowId', + isActive: 'workflowDeploymentVersion.isActive', + }, + }, +})) + +vi.mock('@sim/db', () => ({ + ...dbChainMock, + ...tables, + webhookCredentialIdExpression: mockCredentialExpression, +})) +vi.mock('drizzle-orm', () => ({ + and: vi.fn((...conditions: unknown[]) => conditions), + asc: vi.fn((value: unknown) => ({ asc: value })), + eq: mockEq, + gt: mockGt, + isNull: vi.fn((value: unknown) => ({ isNull: value })), + like: vi.fn((left: unknown, right: unknown) => ({ left, right })), + or: vi.fn((...conditions: unknown[]) => conditions), +})) + +import { + findQuickBooksWebhookTargetPage, + QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE, +} from '@/background/quickbooks-webhook-targets' + +const UUID = '11111111-2222-4333-8444-555555555555' + +describe('findQuickBooksWebhookTargetPage', () => { + afterAll(resetDbChainMock) + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('returns only the exact company identity and enforces provider/workspace bindings', async () => { + queueTableRows(tables.account, [ + { + accountId: `quickbooks:456:user-${UUID}`, + webhookId: 'webhook-1', + webhook: { id: 'webhook-1' }, + workflow: { id: 'workflow-1' }, + }, + { + accountId: `quickbooks:789:user-${UUID}`, + webhookId: 'webhook-2', + webhook: { id: 'webhook-2' }, + workflow: { id: 'workflow-2' }, + }, + ]) + + const page = await findQuickBooksWebhookTargetPage('456', 'request-1') + expect(page.targets).toEqual([{ webhook: { id: 'webhook-1' }, workflow: { id: 'workflow-1' } }]) + expect(mockEq).toHaveBeenCalledWith('credential.providerId', 'quickbooks') + expect(mockEq).toHaveBeenCalledWith('webhook.provider', 'quickbooks') + expect(mockEq).toHaveBeenCalledWith('workflow.workspaceId', 'credential.workspaceId') + }) + + it('uses a fixed 100-target keyset page', async () => { + await findQuickBooksWebhookTargetPage('456', 'request-2', 'webhook-100') + expect(mockGt).toHaveBeenCalledWith('webhook.id', 'webhook-100') + expect(dbChainMockFns.limit).toHaveBeenCalledWith(QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE) + }) +}) diff --git a/apps/sim/background/quickbooks-webhook-targets.ts b/apps/sim/background/quickbooks-webhook-targets.ts new file mode 100644 index 00000000000..646ef8f3862 --- /dev/null +++ b/apps/sim/background/quickbooks-webhook-targets.ts @@ -0,0 +1,112 @@ +import { + account, + credential, + db, + webhook, + webhookCredentialIdExpression, + workflow, + workflowDeploymentVersion, +} from '@sim/db' +import { createLogger } from '@sim/logger' +import { and, asc, eq, gt, isNull, like, or } from 'drizzle-orm' +import { parseQuickBooksAccountId } from '@/lib/oauth/quickbooks' +import { deliverableWebhookPredicate } from '@/lib/webhooks/delivery-predicate' + +const logger = createLogger('QuickBooksWebhookTargets') +const ACCOUNT_ID_UUID_LIKE_SUFFIX = '________-____-____-____-____________' + +export const QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE = 100 + +export interface QuickBooksWebhookTargetPage { + hasMore: boolean + nextCursor: string | null + targets: Array<{ + webhook: typeof webhook.$inferSelect + workflow: typeof workflow.$inferSelect + }> +} + +function escapeLikePattern(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_') +} + +/** Resolve one bounded page of active trigger targets for exactly one QuickBooks company. */ +export async function findQuickBooksWebhookTargetPage( + realmId: string, + requestId: string, + afterWebhookId?: string +): Promise { + if (!realmId) return { hasMore: false, nextCursor: null, targets: [] } + + const rows = await db + .select({ accountId: account.accountId, webhookId: webhook.id, webhook, workflow }) + .from(account) + .innerJoin( + credential, + and( + eq(credential.accountId, account.id), + eq(credential.type, 'oauth'), + eq(credential.providerId, 'quickbooks') + ) + ) + .innerJoin( + webhook, + and( + eq(webhookCredentialIdExpression(webhook.providerConfig), credential.id), + eq(webhook.provider, 'quickbooks'), + deliverableWebhookPredicate(webhook) + ) + ) + .innerJoin( + workflow, + and( + eq(workflow.id, webhook.workflowId), + eq(workflow.workspaceId, credential.workspaceId), + isNull(workflow.archivedAt) + ) + ) + .leftJoin( + workflowDeploymentVersion, + and( + eq(workflowDeploymentVersion.workflowId, workflow.id), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + .where( + and( + eq(account.providerId, 'quickbooks'), + like( + account.accountId, + `quickbooks:${escapeLikePattern(realmId)}:%-${ACCOUNT_ID_UUID_LIKE_SUFFIX}` + ), + or( + eq(webhook.deploymentVersionId, workflowDeploymentVersion.id), + and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId)) + ), + afterWebhookId ? gt(webhook.id, afterWebhookId) : undefined + ) + ) + .orderBy(asc(webhook.id)) + .limit(QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE) + + const targets = rows + .filter((row) => { + try { + return parseQuickBooksAccountId(row.accountId).realmId === realmId + } catch { + return false + } + }) + .map(({ webhook: webhookRecord, workflow: workflowRecord }) => ({ + webhook: webhookRecord, + workflow: workflowRecord, + })) + const nextCursor = rows.at(-1)?.webhookId ?? null + const hasMore = rows.length === QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE + + logger.info(`[${requestId}] Resolved QuickBooks webhook target page`, { + hasMore, + targetCount: targets.length, + }) + return { hasMore, nextCursor, targets } +} diff --git a/apps/sim/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts index 9f60068370c..af89c89bc63 100644 --- a/apps/sim/blocks/blocks/quickbooks.ts +++ b/apps/sim/blocks/blocks/quickbooks.ts @@ -26,6 +26,7 @@ import { } from '@/tools/quickbooks/sales_utils' import type { QuickBooksReportType, QuickBooksResponse } from '@/tools/quickbooks/types' import { parseQuickBooksAddress } from '@/tools/quickbooks/values' +import { getTrigger } from '@/triggers' const MASTER_DATA_OPERATION = 'quickbooks_read_master_data' const SALES_READ_OPERATION = 'quickbooks_read_sales_transactions' @@ -386,6 +387,7 @@ export const QuickBooksBlock: BlockConfig = { integrationType: IntegrationType.Commerce, bgColor: '#2CA01C', icon: QuickBooksIcon, + triggerAllowed: true, subBlocks: [ { id: 'operation', @@ -1965,7 +1967,26 @@ export const QuickBooksBlock: BlockConfig = { }, value: () => 'no', }, + ...getTrigger('quickbooks_invoice_events').subBlocks, + ...getTrigger('quickbooks_customer_events').subBlocks, + ...getTrigger('quickbooks_payment_events').subBlocks, + ...getTrigger('quickbooks_vendor_events').subBlocks, + ...getTrigger('quickbooks_bill_events').subBlocks, + ...getTrigger('quickbooks_bill_payment_events').subBlocks, + ...getTrigger('quickbooks_purchase_order_events').subBlocks, ], + triggers: { + enabled: true, + available: [ + 'quickbooks_customer_events', + 'quickbooks_invoice_events', + 'quickbooks_payment_events', + 'quickbooks_vendor_events', + 'quickbooks_bill_events', + 'quickbooks_bill_payment_events', + 'quickbooks_purchase_order_events', + ], + }, tools: { access: [ 'quickbooks_get_company_info', diff --git a/apps/sim/lib/api/contracts/webhooks.ts b/apps/sim/lib/api/contracts/webhooks.ts index 04c2d3ad5ba..85f3a53372d 100644 --- a/apps/sim/lib/api/contracts/webhooks.ts +++ b/apps/sim/lib/api/contracts/webhooks.ts @@ -293,3 +293,32 @@ export const tiktokWebhookContract = defineRouteContract({ schema: tiktokWebhookResponseSchema, }, }) + +/** Intuit's app-level QuickBooks CloudEvent envelope. */ +export const quickBooksWebhookEventSchema = z.object({ + specversion: z.string().min(1).max(32), + id: z.string().min(1).max(255), + source: z.string().min(1).max(2048), + type: z.string().min(1).max(255), + datacontenttype: z.string().min(1).max(255).optional(), + time: z.string().datetime({ offset: true }), + intuitentityid: z.string().min(1).max(255), + intuitaccountid: z.string().min(1).max(255), + data: z.unknown().optional(), +}) + +export const quickBooksWebhookEventsSchema = z.array(quickBooksWebhookEventSchema).min(1).max(1000) + +export type QuickBooksWebhookEvent = z.input + +export const quickBooksWebhookContract = defineRouteContract({ + method: 'POST', + path: '/api/webhooks/quickbooks', + headers: z.object({ 'intuit-signature': z.string().min(1) }), + // Body is validated after HMAC verification against the raw payload. + body: quickBooksWebhookEventsSchema, + response: { + mode: 'json', + schema: z.union([z.object({ ok: z.literal(true) }), z.object({ error: z.string().min(1) })]), + }, +}) diff --git a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts index 12f9f15bc88..363ca9d2c1a 100644 --- a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts +++ b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts @@ -39,6 +39,7 @@ const JOB_TYPE_TO_TASK_ID: Record = { 'workflow-execution': 'workflow-execution', 'schedule-execution': 'schedule-execution', 'webhook-execution': 'webhook-execution', + 'quickbooks-webhook-ingress': 'quickbooks-webhook-ingress', 'tiktok-webhook-ingress': 'tiktok-webhook-ingress', 'resume-execution': 'resume-execution', 'workflow-group-cell': 'workflow-group-cell', diff --git a/apps/sim/lib/core/async-jobs/types.ts b/apps/sim/lib/core/async-jobs/types.ts index 9a1ee04aefa..d38ef28ecc3 100644 --- a/apps/sim/lib/core/async-jobs/types.ts +++ b/apps/sim/lib/core/async-jobs/types.ts @@ -24,6 +24,7 @@ export type JobType = | 'workflow-execution' | 'schedule-execution' | 'webhook-execution' + | 'quickbooks-webhook-ingress' | 'tiktok-webhook-ingress' | 'resume-execution' | 'workflow-group-cell' diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 90a974f51fa..ba3968c185e 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -420,6 +420,7 @@ export const env = createEnv({ QUICKBOOKS_CLIENT_ID: z.string().optional(), // QuickBooks Online OAuth client ID QUICKBOOKS_CLIENT_SECRET: z.string().optional(), // QuickBooks Online OAuth client secret QUICKBOOKS_ENV: z.enum(['sandbox', 'production']).optional(), // QuickBooks Online API environment (must be configured explicitly) + QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN: z.string().optional(), // Intuit app-level webhook verifier token LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID LINEAR_CLIENT_SECRET: z.string().optional(), // Linear OAuth client secret CLICKUP_CLIENT_ID: z.string().optional(), // ClickUp OAuth client ID diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index ea5064ee1b3..7e3500901ad 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-08-03", + "updatedAt": "2026-08-04", "integrations": [ { "type": "onepassword", @@ -14760,8 +14760,44 @@ } ], "operationCount": 47, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "quickbooks_customer_events", + "name": "QuickBooks Customer Events", + "description": "Trigger when selected Customer events occur in QuickBooks" + }, + { + "id": "quickbooks_invoice_events", + "name": "QuickBooks Invoice Events", + "description": "Trigger when selected Invoice events occur in QuickBooks" + }, + { + "id": "quickbooks_payment_events", + "name": "QuickBooks Payment Events", + "description": "Trigger when selected Payment events occur in QuickBooks" + }, + { + "id": "quickbooks_vendor_events", + "name": "QuickBooks Vendor Events", + "description": "Trigger when selected Vendor events occur in QuickBooks" + }, + { + "id": "quickbooks_bill_events", + "name": "QuickBooks Bill Events", + "description": "Trigger when selected Bill events occur in QuickBooks" + }, + { + "id": "quickbooks_bill_payment_events", + "name": "QuickBooks Bill Payment Events", + "description": "Trigger when selected Bill Payment events occur in QuickBooks" + }, + { + "id": "quickbooks_purchase_order_events", + "name": "QuickBooks Purchase Order Events", + "description": "Trigger when selected Purchase Order events occur in QuickBooks" + } + ], + "triggerCount": 7, "authType": "oauth", "oauthServiceId": "quickbooks", "category": "tools", diff --git a/apps/sim/lib/webhooks/providers/quickbooks.test.ts b/apps/sim/lib/webhooks/providers/quickbooks.test.ts new file mode 100644 index 00000000000..76b22c34329 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/quickbooks.test.ts @@ -0,0 +1,76 @@ +import crypto from 'node:crypto' +import { NextRequest } from 'next/server' +import { describe, expect, it } from 'vitest' +import { quickBooksHandler, verifyQuickBooksSignature } from '@/lib/webhooks/providers/quickbooks' +import { isQuickBooksEventMatch } from '@/triggers/quickbooks/utils' + +const event = { + specversion: '1.0', + id: 'event-1', + source: 'quickbooks-online', + type: 'qbo.invoice.updated.v1', + datacontenttype: 'application/json', + time: '2026-08-03T12:00:00Z', + intuitentityid: '123', + intuitaccountid: '456', + data: { changedFields: ['Balance'] }, +} + +describe('QuickBooks webhook provider', () => { + it('verifies the documented base64 HMAC over the raw body', () => { + const body = JSON.stringify([event]) + const signature = crypto.createHmac('sha256', 'verifier').update(body).digest('base64') + expect(verifyQuickBooksSignature(body, signature, 'request-1', 'verifier')).toBeNull() + expect(verifyQuickBooksSignature(body, 'invalid', 'request-2', 'verifier')?.status).toBe(401) + expect(verifyQuickBooksSignature(body, null, 'request-3', 'verifier')?.status).toBe(401) + }) + + it('matches only configured actions for the selected entity', () => { + expect( + isQuickBooksEventMatch('quickbooks_invoice_events', event.type, ['created', 'updated']) + ).toBe(true) + expect(isQuickBooksEventMatch('quickbooks_invoice_events', event.type, ['created'])).toBe(false) + expect(isQuickBooksEventMatch('quickbooks_bill_events', event.type, ['updated'])).toBe(false) + }) + + it('formats only the common verified event fields', async () => { + const result = await quickBooksHandler.formatInput!({ + body: event, + webhook: {}, + workflow: { id: 'workflow-1', userId: 'user-1' }, + headers: {}, + requestId: 'request-4', + }) + expect(result.input).toEqual({ + eventId: 'event-1', + eventType: 'qbo.invoice.updated.v1', + entityType: 'invoice', + action: 'updated', + entityId: '123', + realmId: '456', + eventTime: '2026-08-03T12:00:00Z', + specVersion: '1.0', + source: 'quickbooks-online', + contentType: 'application/json', + data: { changedFields: ['Balance'] }, + }) + expect(quickBooksHandler.extractIdempotencyId!(event)).toBe('event-1') + }) + + it('uses the provider-local ingress and durable queue modes', async () => { + expect(quickBooksHandler.ingressMode).toBe('provider') + expect(quickBooksHandler.executionMode).toBe('queue') + const matched = await quickBooksHandler.matchEvent!({ + body: event, + request: new NextRequest('http://localhost'), + requestId: 'request-5', + providerConfig: { + triggerId: 'quickbooks_invoice_events', + eventTypes: ['updated'], + }, + webhook: {}, + workflow: {}, + }) + expect(matched).toBe(true) + }) +}) diff --git a/apps/sim/lib/webhooks/providers/quickbooks.ts b/apps/sim/lib/webhooks/providers/quickbooks.ts new file mode 100644 index 00000000000..9b57f5d0303 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/quickbooks.ts @@ -0,0 +1,87 @@ +import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Base64 } from '@sim/security/hmac' +import { NextResponse } from 'next/server' +import { env } from '@/lib/core/config/env' +import type { + AuthContext, + EventMatchContext, + FormatInputContext, + FormatInputResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' + +const logger = createLogger('WebhookProvider:QuickBooks') + +export function verifyQuickBooksSignature( + rawBody: string, + signature: string | null, + requestId: string, + verifierToken: string | undefined = env.QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN +): NextResponse | null { + if (!verifierToken) { + logger.warn(`[${requestId}] QuickBooks webhook verifier token is not configured`) + return new NextResponse('Unauthorized', { status: 401 }) + } + if (!signature) { + logger.warn(`[${requestId}] QuickBooks webhook is missing intuit-signature`) + return new NextResponse('Unauthorized', { status: 401 }) + } + + const expected = hmacSha256Base64(rawBody, verifierToken) + if (!safeCompare(expected, signature.trim())) { + logger.warn(`[${requestId}] QuickBooks webhook signature verification failed`) + return new NextResponse('Unauthorized', { status: 401 }) + } + return null +} + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null + return value as Record +} + +export const quickBooksHandler: WebhookProviderHandler = { + ingressMode: 'provider', + executionMode: 'queue', + + verifyAuth({ request, rawBody, requestId }: AuthContext) { + return verifyQuickBooksSignature(rawBody, request.headers.get('intuit-signature'), requestId) + }, + + async matchEvent({ body, providerConfig }: EventMatchContext) { + const event = asRecord(body) + const triggerId = typeof providerConfig.triggerId === 'string' ? providerConfig.triggerId : '' + const eventType = typeof event?.type === 'string' ? event.type : '' + const { isQuickBooksEventMatch } = await import('@/triggers/quickbooks/utils') + return isQuickBooksEventMatch(triggerId, eventType, providerConfig.eventTypes) + }, + + async formatInput({ body }: FormatInputContext): Promise { + const event = asRecord(body) ?? {} + const eventType = typeof event.type === 'string' ? event.type : '' + const { parseQuickBooksWebhookType } = await import('@/triggers/quickbooks/utils') + const parsed = parseQuickBooksWebhookType(eventType) + + return { + input: { + eventId: typeof event.id === 'string' ? event.id : '', + eventType, + entityType: parsed?.entity ?? '', + action: parsed?.action ?? '', + entityId: typeof event.intuitentityid === 'string' ? event.intuitentityid : '', + realmId: typeof event.intuitaccountid === 'string' ? event.intuitaccountid : '', + eventTime: typeof event.time === 'string' ? event.time : '', + specVersion: typeof event.specversion === 'string' ? event.specversion : '', + source: typeof event.source === 'string' ? event.source : '', + contentType: typeof event.datacontenttype === 'string' ? event.datacontenttype : null, + data: event.data ?? null, + }, + } + }, + + extractIdempotencyId(body: unknown) { + const event = asRecord(body) + return typeof event?.id === 'string' ? event.id : null + }, +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 085280c9d55..83f01a3ae09 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -36,6 +36,7 @@ import { mondayHandler } from '@/lib/webhooks/providers/monday' import { notionHandler } from '@/lib/webhooks/providers/notion' import { outlookHandler } from '@/lib/webhooks/providers/outlook' import { pagerdutyHandler } from '@/lib/webhooks/providers/pagerduty' +import { quickBooksHandler } from '@/lib/webhooks/providers/quickbooks' import { resendHandler } from '@/lib/webhooks/providers/resend' import { revenueCatHandler } from '@/lib/webhooks/providers/revenuecat' import { rootlyHandler } from '@/lib/webhooks/providers/rootly' @@ -104,6 +105,7 @@ const PROVIDER_HANDLERS: Record = { notion: notionHandler, outlook: outlookHandler, pagerduty: pagerdutyHandler, + quickbooks: quickBooksHandler, rss: rssHandler, salesforce: salesforceHandler, sendblue: sendblueHandler, diff --git a/apps/sim/triggers/quickbooks/bill_events.ts b/apps/sim/triggers/quickbooks/bill_events.ts new file mode 100644 index 00000000000..dc6f7ab5e40 --- /dev/null +++ b/apps/sim/triggers/quickbooks/bill_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksBillEventsTrigger: TriggerConfig = { + id: 'quickbooks_bill_events', + name: 'QuickBooks Bill Events', + provider: 'quickbooks', + description: 'Trigger when selected Bill events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_bill_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/bill_payment_events.ts b/apps/sim/triggers/quickbooks/bill_payment_events.ts new file mode 100644 index 00000000000..01e874b6548 --- /dev/null +++ b/apps/sim/triggers/quickbooks/bill_payment_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksBillPaymentEventsTrigger: TriggerConfig = { + id: 'quickbooks_bill_payment_events', + name: 'QuickBooks Bill Payment Events', + provider: 'quickbooks', + description: 'Trigger when selected Bill Payment events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_bill_payment_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/customer_events.ts b/apps/sim/triggers/quickbooks/customer_events.ts new file mode 100644 index 00000000000..bace42baa51 --- /dev/null +++ b/apps/sim/triggers/quickbooks/customer_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksCustomerEventsTrigger: TriggerConfig = { + id: 'quickbooks_customer_events', + name: 'QuickBooks Customer Events', + provider: 'quickbooks', + description: 'Trigger when selected Customer events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_customer_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/index.ts b/apps/sim/triggers/quickbooks/index.ts new file mode 100644 index 00000000000..a5651b1d2d3 --- /dev/null +++ b/apps/sim/triggers/quickbooks/index.ts @@ -0,0 +1,7 @@ +export { quickBooksBillEventsTrigger } from '@/triggers/quickbooks/bill_events' +export { quickBooksBillPaymentEventsTrigger } from '@/triggers/quickbooks/bill_payment_events' +export { quickBooksCustomerEventsTrigger } from '@/triggers/quickbooks/customer_events' +export { quickBooksInvoiceEventsTrigger } from '@/triggers/quickbooks/invoice_events' +export { quickBooksPaymentEventsTrigger } from '@/triggers/quickbooks/payment_events' +export { quickBooksPurchaseOrderEventsTrigger } from '@/triggers/quickbooks/purchase_order_events' +export { quickBooksVendorEventsTrigger } from '@/triggers/quickbooks/vendor_events' diff --git a/apps/sim/triggers/quickbooks/invoice_events.ts b/apps/sim/triggers/quickbooks/invoice_events.ts new file mode 100644 index 00000000000..07e247c7684 --- /dev/null +++ b/apps/sim/triggers/quickbooks/invoice_events.ts @@ -0,0 +1,31 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, + quickBooksTriggerOptions, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksInvoiceEventsTrigger: TriggerConfig = { + id: 'quickbooks_invoice_events', + name: 'QuickBooks Invoice Events', + provider: 'quickbooks', + description: 'Trigger when selected Invoice events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: [ + { + id: 'selectedTriggerId', + title: 'Trigger Type', + type: 'dropdown', + mode: 'trigger', + options: quickBooksTriggerOptions, + value: () => 'quickbooks_invoice_events', + required: true, + }, + ...buildQuickBooksTriggerSubBlocks('quickbooks_invoice_events'), + ], + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/payment_events.ts b/apps/sim/triggers/quickbooks/payment_events.ts new file mode 100644 index 00000000000..b8139edcd41 --- /dev/null +++ b/apps/sim/triggers/quickbooks/payment_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksPaymentEventsTrigger: TriggerConfig = { + id: 'quickbooks_payment_events', + name: 'QuickBooks Payment Events', + provider: 'quickbooks', + description: 'Trigger when selected Payment events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_payment_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/purchase_order_events.ts b/apps/sim/triggers/quickbooks/purchase_order_events.ts new file mode 100644 index 00000000000..fa2d04745c3 --- /dev/null +++ b/apps/sim/triggers/quickbooks/purchase_order_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksPurchaseOrderEventsTrigger: TriggerConfig = { + id: 'quickbooks_purchase_order_events', + name: 'QuickBooks Purchase Order Events', + provider: 'quickbooks', + description: 'Trigger when selected Purchase Order events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_purchase_order_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/quickbooks.test.ts b/apps/sim/triggers/quickbooks/quickbooks.test.ts new file mode 100644 index 00000000000..9cc99e2a52b --- /dev/null +++ b/apps/sim/triggers/quickbooks/quickbooks.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import { QuickBooksBlock } from '@/blocks/blocks/quickbooks' +import { getTrigger } from '@/triggers' +import { + QUICKBOOKS_TRIGGER_DEFINITIONS, + quickBooksTriggerOptions, +} from '@/triggers/quickbooks/utils' + +describe('QuickBooks triggers', () => { + it('keeps trigger definitions, dropdown options, and block access in parity', () => { + const ids = QUICKBOOKS_TRIGGER_DEFINITIONS.map(({ id }) => id) + expect(quickBooksTriggerOptions.map(({ id }) => id)).toEqual(ids) + expect(QuickBooksBlock.triggers?.available).toEqual(ids) + for (const id of ids) expect(getTrigger(id).id).toBe(id) + }) + + it('uses one primary trigger dropdown and entity-specific event selectors', () => { + expect( + getTrigger('quickbooks_invoice_events').subBlocks.filter( + (subBlock) => subBlock.id === 'selectedTriggerId' + ) + ).toHaveLength(1) + for (const definition of QUICKBOOKS_TRIGGER_DEFINITIONS) { + const trigger = getTrigger(definition.id) + const eventTypes = trigger.subBlocks.find((subBlock) => subBlock.id === 'eventTypes') + expect(eventTypes?.options?.map((option) => option.id)).toEqual(definition.actions) + expect(trigger.outputs).toEqual(getTrigger('quickbooks_invoice_events').outputs) + } + }) +}) diff --git a/apps/sim/triggers/quickbooks/utils.ts b/apps/sim/triggers/quickbooks/utils.ts new file mode 100644 index 00000000000..b7cbeb4db7d --- /dev/null +++ b/apps/sim/triggers/quickbooks/utils.ts @@ -0,0 +1,163 @@ +import { getScopesForService } from '@/lib/oauth/utils' +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +export type QuickBooksWebhookAction = + | 'created' + | 'updated' + | 'deleted' + | 'merged' + | 'voided' + | 'emailed' + +export interface QuickBooksTriggerDefinition { + actions: readonly QuickBooksWebhookAction[] + entity: string + entityType: string + group: string + id: string + label: string +} + +export const QUICKBOOKS_TRIGGER_DEFINITIONS: readonly QuickBooksTriggerDefinition[] = [ + { + id: 'quickbooks_customer_events', + label: 'Customer Events', + group: 'Sales and Receivables', + entity: 'customer', + entityType: 'Customer', + actions: ['created', 'updated', 'deleted', 'merged'], + }, + { + id: 'quickbooks_invoice_events', + label: 'Invoice Events', + group: 'Sales and Receivables', + entity: 'invoice', + entityType: 'Invoice', + actions: ['created', 'updated', 'deleted', 'voided', 'emailed'], + }, + { + id: 'quickbooks_payment_events', + label: 'Payment Events', + group: 'Sales and Receivables', + entity: 'payment', + entityType: 'Payment', + actions: ['created', 'updated', 'deleted', 'voided', 'emailed'], + }, + { + id: 'quickbooks_vendor_events', + label: 'Vendor Events', + group: 'Purchasing and Payables', + entity: 'vendor', + entityType: 'Vendor', + actions: ['created', 'updated', 'deleted', 'merged'], + }, + { + id: 'quickbooks_bill_events', + label: 'Bill Events', + group: 'Purchasing and Payables', + entity: 'bill', + entityType: 'Bill', + actions: ['created', 'updated', 'deleted'], + }, + { + id: 'quickbooks_bill_payment_events', + label: 'Bill Payment Events', + group: 'Purchasing and Payables', + entity: 'billpayment', + entityType: 'BillPayment', + actions: ['created', 'updated', 'deleted', 'voided'], + }, + { + id: 'quickbooks_purchase_order_events', + label: 'Purchase Order Events', + group: 'Purchasing and Payables', + entity: 'purchaseorder', + entityType: 'PurchaseOrder', + actions: ['created', 'updated', 'deleted', 'emailed'], + }, +] as const + +export const quickBooksTriggerOptions = QUICKBOOKS_TRIGGER_DEFINITIONS.map((definition) => ({ + label: definition.label, + id: definition.id, + group: definition.group, +})) + +export const QUICKBOOKS_WEBHOOK_HEADERS = { + 'Content-Type': 'application/json', + 'intuit-signature': '', +} as const + +export function getQuickBooksTriggerDefinition( + triggerId: string +): QuickBooksTriggerDefinition | undefined { + return QUICKBOOKS_TRIGGER_DEFINITIONS.find((definition) => definition.id === triggerId) +} + +export function parseQuickBooksWebhookType(type: string): { + action: string + entity: string +} | null { + const match = /^qbo\.([a-z]+)\.([a-z]+)\.v1$/.exec(type) + return match ? { entity: match[1], action: match[2] } : null +} + +export function isQuickBooksEventMatch( + triggerId: string, + eventType: string, + selectedActions: unknown +): boolean { + const definition = getQuickBooksTriggerDefinition(triggerId) + const parsed = parseQuickBooksWebhookType(eventType) + if (!definition || !parsed || parsed.entity !== definition.entity) return false + if (!definition.actions.includes(parsed.action as QuickBooksWebhookAction)) return false + return Array.isArray(selectedActions) && selectedActions.includes(parsed.action) +} + +export function buildQuickBooksTriggerSubBlocks(triggerId: string): SubBlockConfig[] { + const definition = getQuickBooksTriggerDefinition(triggerId) + if (!definition) throw new Error(`Unknown QuickBooks trigger: ${triggerId}`) + + return [ + { + id: 'triggerCredentials', + title: 'QuickBooks Account', + type: 'oauth-input', + serviceId: 'quickbooks', + requiredScopes: getScopesForService('quickbooks'), + mode: 'trigger', + required: true, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'eventTypes', + title: 'Event Types', + type: 'dropdown', + multiSelect: true, + options: definition.actions.map((action) => ({ + label: action.charAt(0).toUpperCase() + action.slice(1), + id: action, + })), + mode: 'trigger', + required: true, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +export function buildQuickBooksTriggerOutputs(): Record { + return { + eventId: { type: 'string', description: 'Intuit webhook event ID' }, + eventType: { type: 'string', description: 'Full Intuit CloudEvent type' }, + entityType: { type: 'string', description: 'QuickBooks entity type' }, + action: { type: 'string', description: 'QuickBooks webhook action' }, + entityId: { type: 'string', description: 'QuickBooks entity ID' }, + realmId: { type: 'string', description: 'QuickBooks company realm ID' }, + eventTime: { type: 'string', description: 'Event timestamp' }, + specVersion: { type: 'string', description: 'CloudEvents specification version' }, + source: { type: 'string', description: 'Intuit event source' }, + contentType: { type: 'string', description: 'Event content type, when provided' }, + data: { type: 'json', description: 'Optional event data supplied by Intuit' }, + } +} diff --git a/apps/sim/triggers/quickbooks/vendor_events.ts b/apps/sim/triggers/quickbooks/vendor_events.ts new file mode 100644 index 00000000000..aa06333cad1 --- /dev/null +++ b/apps/sim/triggers/quickbooks/vendor_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksVendorEventsTrigger: TriggerConfig = { + id: 'quickbooks_vendor_events', + name: 'QuickBooks Vendor Events', + provider: 'quickbooks', + description: 'Trigger when selected Vendor events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_vendor_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index c9c55455f44..ff39129cc0f 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -369,6 +369,15 @@ import { pagerdutyIncidentTriggeredTrigger, pagerdutyWebhookTrigger, } from '@/triggers/pagerduty' +import { + quickBooksBillEventsTrigger, + quickBooksBillPaymentEventsTrigger, + quickBooksCustomerEventsTrigger, + quickBooksInvoiceEventsTrigger, + quickBooksPaymentEventsTrigger, + quickBooksPurchaseOrderEventsTrigger, + quickBooksVendorEventsTrigger, +} from '@/triggers/quickbooks' import { resendEmailBouncedTrigger, resendEmailClickedTrigger, @@ -738,6 +747,13 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { pagerduty_incident_escalated: pagerdutyIncidentEscalatedTrigger, pagerduty_incident_reassigned: pagerdutyIncidentReassignedTrigger, pagerduty_webhook: pagerdutyWebhookTrigger, + quickbooks_bill_events: quickBooksBillEventsTrigger, + quickbooks_bill_payment_events: quickBooksBillPaymentEventsTrigger, + quickbooks_customer_events: quickBooksCustomerEventsTrigger, + quickbooks_invoice_events: quickBooksInvoiceEventsTrigger, + quickbooks_payment_events: quickBooksPaymentEventsTrigger, + quickbooks_purchase_order_events: quickBooksPurchaseOrderEventsTrigger, + quickbooks_vendor_events: quickBooksVendorEventsTrigger, resend_email_sent: resendEmailSentTrigger, resend_email_delivered: resendEmailDeliveredTrigger, resend_email_bounced: resendEmailBouncedTrigger, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 4bed6a4e7d5..65c47d16de5 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 1009, - zodRoutes: 1009, + totalRoutes: 1010, + zodRoutes: 1010, nonZodRoutes: 0, } as const From b1be51dcb03666ade3eb3a7ec3eb47692838986c Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 4 Aug 2026 00:13:46 -0700 Subject: [PATCH 2/9] docs(quickbooks): add webhook setup guidance --- apps/sim/triggers/quickbooks/utils.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/sim/triggers/quickbooks/utils.ts b/apps/sim/triggers/quickbooks/utils.ts index b7cbeb4db7d..fc7a5560561 100644 --- a/apps/sim/triggers/quickbooks/utils.ts +++ b/apps/sim/triggers/quickbooks/utils.ts @@ -115,6 +115,22 @@ export function isQuickBooksEventMatch( return Array.isArray(selectedActions) && selectedActions.includes(parsed.action) } +function quickBooksSetupInstructions(entityLabel: string): string { + const instructions = [ + 'App setup: A Sim operator must register https://<your-sim-domain>/api/webhooks/quickbooks as the app-level endpoint in the Intuit Developer Portal and configure the server-only QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN.', + 'Connect the QuickBooks account whose company should receive these events.', + `Select the ${entityLabel} actions this workflow should handle.`, + 'Deploy the workflow to activate the trigger.', + ] + + return instructions + .map( + (instruction, index) => + `
${index === 0 ? instruction : `${index}. ${instruction}`}
` + ) + .join('') +} + export function buildQuickBooksTriggerSubBlocks(triggerId: string): SubBlockConfig[] { const definition = getQuickBooksTriggerDefinition(triggerId) if (!definition) throw new Error(`Unknown QuickBooks trigger: ${triggerId}`) @@ -143,6 +159,15 @@ export function buildQuickBooksTriggerSubBlocks(triggerId: string): SubBlockConf required: true, condition: { field: 'selectedTriggerId', value: triggerId }, }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: quickBooksSetupInstructions(definition.label), + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, ] } From 937ec788b0b24f6cfd7caaa8debe1c83a6e5a778 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 4 Aug 2026 00:23:47 -0700 Subject: [PATCH 3/9] fix(quickbooks): continue webhook batches after target failures --- .../quickbooks-webhook-ingress.test.ts | 20 ++++++++++++ .../background/quickbooks-webhook-ingress.ts | 32 ++++++++++++------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/apps/sim/background/quickbooks-webhook-ingress.test.ts b/apps/sim/background/quickbooks-webhook-ingress.test.ts index 1127bbb2d66..a4bd4b9d2c7 100644 --- a/apps/sim/background/quickbooks-webhook-ingress.test.ts +++ b/apps/sim/background/quickbooks-webhook-ingress.test.ts @@ -59,6 +59,7 @@ describe('QuickBooks webhook ingress job', () => { return { outcome: 'queued' } }) await expect(executeQuickBooksWebhookIngress(payload)).resolves.toEqual({ + failed: 0, ignored: 0, processed: 2, targetCount: 2, @@ -98,4 +99,23 @@ describe('QuickBooks webhook ingress job', () => { expect.objectContaining({ jobId: 'quickbooks-webhook-ingress:request-1:1:root' }) ) }) + + it('durably continues the batch before retrying a failed target page', async () => { + mockFindPage.mockResolvedValue({ + hasMore: false, + nextCursor: null, + targets: [{ webhook: { id: 'w1' }, workflow: { id: 'wf1' } }], + }) + mockDispatch.mockResolvedValue({ outcome: 'failed' }) + + await enqueueQuickBooksWebhookIngress(payload) + const options = mockEnqueue.mock.calls[0][2] as { runner: () => Promise } + await expect(options.runner()).rejects.toThrow('Failed to dispatch 1 of 1 QuickBooks targets') + expect(mockEnqueue).toHaveBeenNthCalledWith( + 2, + 'quickbooks-webhook-ingress', + expect.objectContaining({ eventIndex: 1, afterWebhookId: undefined }), + expect.objectContaining({ jobId: 'quickbooks-webhook-ingress:request-1:1:root' }) + ) + }) }) diff --git a/apps/sim/background/quickbooks-webhook-ingress.ts b/apps/sim/background/quickbooks-webhook-ingress.ts index 5c0ac6e8a98..3449422b159 100644 --- a/apps/sim/background/quickbooks-webhook-ingress.ts +++ b/apps/sim/background/quickbooks-webhook-ingress.ts @@ -21,6 +21,7 @@ export interface QuickBooksWebhookIngressPayload { } export interface QuickBooksWebhookIngressResult { + failed: number ignored: number nextCursor?: string processed: number @@ -33,7 +34,7 @@ export async function executeQuickBooksWebhookIngress( ): Promise { const eventIndex = payload.eventIndex ?? 0 const event = payload.events[eventIndex] - if (!event) return { ignored: 0, processed: 0, targetCount: 0 } + if (!event) return { failed: 0, ignored: 0, processed: 0, targetCount: 0 } const request = new NextRequest('http://internal/api/webhooks/quickbooks', { method: 'POST', @@ -65,10 +66,6 @@ export async function executeQuickBooksWebhookIngress( else failed += 1 } - if (failed > 0) { - throw new Error(`Failed to dispatch ${failed} of ${page.targets.length} QuickBooks targets`) - } - logger.info(`[${payload.requestId}] QuickBooks webhook page completed`, { eventId: event.id, eventIndex, @@ -77,6 +74,7 @@ export async function executeQuickBooksWebhookIngress( targetCount: page.targets.length, }) return { + failed, ignored, processed, targetCount: page.targets.length, @@ -84,16 +82,14 @@ export async function executeQuickBooksWebhookIngress( } } -async function runQuickBooksWebhookIngressJob( - payload: QuickBooksWebhookIngressPayload +async function enqueueQuickBooksWebhookContinuation( + payload: QuickBooksWebhookIngressPayload, + result: QuickBooksWebhookIngressResult ): Promise { const eventIndex = payload.eventIndex ?? 0 - const result = await executeQuickBooksWebhookIngress(payload) if (result.nextCursor) { await enqueueQuickBooksWebhookIngress({ ...payload, afterWebhookId: result.nextCursor }) - return - } - if (eventIndex + 1 < payload.events.length) { + } else if (eventIndex + 1 < payload.events.length) { await enqueueQuickBooksWebhookIngress({ ...payload, eventIndex: eventIndex + 1, @@ -102,6 +98,20 @@ async function runQuickBooksWebhookIngressJob( } } +async function runQuickBooksWebhookIngressJob( + payload: QuickBooksWebhookIngressPayload +): Promise { + const result = await executeQuickBooksWebhookIngress(payload) + // The continuation has a deterministic job id, so retries cannot duplicate it. Enqueue it + // before retrying this page to avoid stranding later events in an already-acknowledged batch. + await enqueueQuickBooksWebhookContinuation(payload, result) + if (result.failed > 0) { + throw new Error( + `Failed to dispatch ${result.failed} of ${result.targetCount} QuickBooks targets` + ) + } +} + export async function enqueueQuickBooksWebhookIngress( payload: QuickBooksWebhookIngressPayload ): Promise { From 29824665523dfc2af1ba677e25e246fb2a2c6267 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 4 Aug 2026 00:32:44 -0700 Subject: [PATCH 4/9] fix(quickbooks): align primary webhook trigger fallback --- apps/sim/blocks/blocks/quickbooks.ts | 2 +- apps/sim/triggers/quickbooks/quickbooks.test.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/sim/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts index af89c89bc63..6b75472815b 100644 --- a/apps/sim/blocks/blocks/quickbooks.ts +++ b/apps/sim/blocks/blocks/quickbooks.ts @@ -1978,8 +1978,8 @@ export const QuickBooksBlock: BlockConfig = { triggers: { enabled: true, available: [ - 'quickbooks_customer_events', 'quickbooks_invoice_events', + 'quickbooks_customer_events', 'quickbooks_payment_events', 'quickbooks_vendor_events', 'quickbooks_bill_events', diff --git a/apps/sim/triggers/quickbooks/quickbooks.test.ts b/apps/sim/triggers/quickbooks/quickbooks.test.ts index 9cc99e2a52b..66fe7a6fdb0 100644 --- a/apps/sim/triggers/quickbooks/quickbooks.test.ts +++ b/apps/sim/triggers/quickbooks/quickbooks.test.ts @@ -10,7 +10,10 @@ describe('QuickBooks triggers', () => { it('keeps trigger definitions, dropdown options, and block access in parity', () => { const ids = QUICKBOOKS_TRIGGER_DEFINITIONS.map(({ id }) => id) expect(quickBooksTriggerOptions.map(({ id }) => id)).toEqual(ids) - expect(QuickBooksBlock.triggers?.available).toEqual(ids) + expect(QuickBooksBlock.triggers?.available).toEqual([ + 'quickbooks_invoice_events', + ...ids.filter((id) => id !== 'quickbooks_invoice_events'), + ]) for (const id of ids) expect(getTrigger(id).id).toBe(id) }) From 28c04b1d3504572ccf06f0197bad1f5a111cfbfb Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Tue, 4 Aug 2026 01:21:17 -0700 Subject: [PATCH 5/9] fix(quickbooks): bound webhook ingress jobs --- .../quickbooks-webhook-ingress.test.ts | 113 ++++++++------ .../background/quickbooks-webhook-ingress.ts | 138 +++++++++--------- 2 files changed, 138 insertions(+), 113 deletions(-) diff --git a/apps/sim/background/quickbooks-webhook-ingress.test.ts b/apps/sim/background/quickbooks-webhook-ingress.test.ts index a4bd4b9d2c7..2380197b3f9 100644 --- a/apps/sim/background/quickbooks-webhook-ingress.test.ts +++ b/apps/sim/background/quickbooks-webhook-ingress.test.ts @@ -44,16 +44,27 @@ describe('QuickBooks webhook ingress job', () => { mockEnqueue.mockResolvedValue('job-1') }) - it('routes one event to one company and dispatches targets sequentially', async () => { + it('routes the batch by company and dispatches targets sequentially', async () => { const order: string[] = [] - mockFindPage.mockResolvedValue({ - hasMore: false, - nextCursor: null, - targets: [ - { webhook: { id: 'w1' }, workflow: { id: 'wf1' } }, - { webhook: { id: 'w2' }, workflow: { id: 'wf2' } }, - ], - }) + mockFindPage + .mockResolvedValueOnce({ + hasMore: true, + nextCursor: 'w2', + targets: [ + { webhook: { id: 'w1' }, workflow: { id: 'wf1' } }, + { webhook: { id: 'w2' }, workflow: { id: 'wf2' } }, + ], + }) + .mockResolvedValueOnce({ + hasMore: false, + nextCursor: null, + targets: [{ webhook: { id: 'w3' }, workflow: { id: 'wf3' } }], + }) + .mockResolvedValueOnce({ + hasMore: false, + nextCursor: null, + targets: [{ webhook: { id: 'w4' }, workflow: { id: 'wf4' } }], + }) mockDispatch.mockImplementation(async (webhook: { id: string }) => { order.push(webhook.id) return { outcome: 'queued' } @@ -61,61 +72,71 @@ describe('QuickBooks webhook ingress job', () => { await expect(executeQuickBooksWebhookIngress(payload)).resolves.toEqual({ failed: 0, ignored: 0, - processed: 2, - targetCount: 2, + processed: 4, + targetCount: 4, }) expect(mockFindPage).toHaveBeenCalledWith('456', 'request-1', undefined) - expect(order).toEqual(['w1', 'w2']) + expect(mockFindPage).toHaveBeenCalledWith('456', 'request-1', 'w2') + expect(mockFindPage).toHaveBeenCalledWith('789', 'request-1', undefined) + expect(order).toEqual(['w1', 'w2', 'w3', 'w4']) }) - it('continues target pages before advancing to the next event', async () => { - mockFindPage.mockResolvedValue({ - hasMore: true, - nextCursor: 'webhook-100', - targets: [], - }) + it('enqueues the bounded delivery once without copying it into continuation jobs', async () => { + mockFindPage.mockResolvedValue({ hasMore: false, nextCursor: null, targets: [] }) await enqueueQuickBooksWebhookIngress(payload) const options = mockEnqueue.mock.calls[0][2] as { runner: () => Promise } await options.runner() - expect(mockEnqueue).toHaveBeenNthCalledWith( - 2, + expect(mockEnqueue).toHaveBeenCalledOnce() + expect(mockEnqueue).toHaveBeenCalledWith( 'quickbooks-webhook-ingress', - expect.objectContaining({ afterWebhookId: 'webhook-100' }), + payload, expect.objectContaining({ - jobId: 'quickbooks-webhook-ingress:request-1:0:webhook-100', + jobId: 'quickbooks-webhook-ingress:request-1', }) ) }) - it('advances to the next event only after the current event finishes', async () => { - mockFindPage.mockResolvedValue({ hasMore: false, nextCursor: null, targets: [] }) + it('continues later events before retrying a delivery with failed targets', async () => { + mockFindPage + .mockResolvedValueOnce({ + hasMore: false, + nextCursor: null, + targets: [ + { webhook: { id: 'w1' }, workflow: { id: 'wf1' } }, + { webhook: { id: 'w2' }, workflow: { id: 'wf2' } }, + ], + }) + .mockResolvedValueOnce({ + hasMore: false, + nextCursor: null, + targets: [{ webhook: { id: 'w3' }, workflow: { id: 'wf3' } }], + }) + mockDispatch + .mockRejectedValueOnce(new Error('dispatch unavailable')) + .mockResolvedValueOnce({ outcome: 'failed' }) + .mockResolvedValueOnce({ outcome: 'queued' }) + await enqueueQuickBooksWebhookIngress(payload) const options = mockEnqueue.mock.calls[0][2] as { runner: () => Promise } - await options.runner() - expect(mockEnqueue).toHaveBeenNthCalledWith( - 2, - 'quickbooks-webhook-ingress', - expect.objectContaining({ eventIndex: 1, afterWebhookId: undefined }), - expect.objectContaining({ jobId: 'quickbooks-webhook-ingress:request-1:1:root' }) + await expect(options.runner()).rejects.toThrow( + 'QuickBooks webhook delivery completed with 2 failures' ) + expect(mockFindPage).toHaveBeenCalledWith('789', 'request-1', undefined) + expect(mockDispatch).toHaveBeenCalledTimes(3) + expect(mockEnqueue).toHaveBeenCalledOnce() }) - it('durably continues the batch before retrying a failed target page', async () => { - mockFindPage.mockResolvedValue({ - hasMore: false, - nextCursor: null, - targets: [{ webhook: { id: 'w1' }, workflow: { id: 'wf1' } }], - }) - mockDispatch.mockResolvedValue({ outcome: 'failed' }) + it('continues later events when a target page cannot be resolved', async () => { + mockFindPage + .mockRejectedValueOnce(new Error('database unavailable')) + .mockResolvedValueOnce({ hasMore: false, nextCursor: null, targets: [] }) - await enqueueQuickBooksWebhookIngress(payload) - const options = mockEnqueue.mock.calls[0][2] as { runner: () => Promise } - await expect(options.runner()).rejects.toThrow('Failed to dispatch 1 of 1 QuickBooks targets') - expect(mockEnqueue).toHaveBeenNthCalledWith( - 2, - 'quickbooks-webhook-ingress', - expect.objectContaining({ eventIndex: 1, afterWebhookId: undefined }), - expect.objectContaining({ jobId: 'quickbooks-webhook-ingress:request-1:1:root' }) - ) + await expect(executeQuickBooksWebhookIngress(payload)).resolves.toEqual({ + failed: 1, + ignored: 0, + processed: 0, + targetCount: 0, + }) + expect(mockFindPage).toHaveBeenCalledWith('789', 'request-1', undefined) }) }) diff --git a/apps/sim/background/quickbooks-webhook-ingress.ts b/apps/sim/background/quickbooks-webhook-ingress.ts index 3449422b159..b091225fdcd 100644 --- a/apps/sim/background/quickbooks-webhook-ingress.ts +++ b/apps/sim/background/quickbooks-webhook-ingress.ts @@ -12,8 +12,6 @@ export const QUICKBOOKS_WEBHOOK_INGRESS_CONCURRENCY_LIMIT = 50 export const QUICKBOOKS_WEBHOOK_INGRESS_MAX_ATTEMPTS = 3 export interface QuickBooksWebhookIngressPayload { - afterWebhookId?: string - eventIndex?: number events: QuickBooksWebhookEvent[] headers: { 'content-type': string } requestId: string @@ -23,92 +21,99 @@ export interface QuickBooksWebhookIngressPayload { export interface QuickBooksWebhookIngressResult { failed: number ignored: number - nextCursor?: string processed: number targetCount: number } -/** Process one event against one bounded target page. */ +/** Process the bounded delivery sequentially, retaining at most one target page at a time. */ export async function executeQuickBooksWebhookIngress( payload: QuickBooksWebhookIngressPayload ): Promise { - const eventIndex = payload.eventIndex ?? 0 - const event = payload.events[eventIndex] - if (!event) return { failed: 0, ignored: 0, processed: 0, targetCount: 0 } - - const request = new NextRequest('http://internal/api/webhooks/quickbooks', { - method: 'POST', - headers: payload.headers, - body: JSON.stringify(event), - }) - const page = await findQuickBooksWebhookTargetPage( - event.intuitaccountid, - payload.requestId, - payload.afterWebhookId - ) - const nextCursor = page.hasMore ? page.nextCursor : null - if (page.hasMore && (!nextCursor || nextCursor === payload.afterWebhookId)) { - throw new Error('QuickBooks webhook target pagination did not advance') - } - let ignored = 0 let processed = 0 let failed = 0 - for (const { webhook, workflow } of page.targets) { - const result = await dispatchResolvedWebhookTarget(webhook, workflow, event, request, { - requestId: payload.requestId, - path: webhook.path ?? undefined, - receivedAt: payload.receivedAt, - triggerTimestampMs: Date.parse(event.time), + let targetCount = 0 + + for (const [eventIndex, event] of payload.events.entries()) { + const request = new NextRequest('http://internal/api/webhooks/quickbooks', { + method: 'POST', + headers: payload.headers, + body: JSON.stringify(event), }) - if (result.outcome === 'queued') processed += 1 - else if (result.outcome === 'ignored') ignored += 1 - else failed += 1 + let afterWebhookId: string | undefined + + while (true) { + try { + const page = await findQuickBooksWebhookTargetPage( + event.intuitaccountid, + payload.requestId, + afterWebhookId + ) + const nextCursor = page.hasMore ? page.nextCursor : null + if (page.hasMore && (!nextCursor || nextCursor === afterWebhookId)) { + throw new Error('QuickBooks webhook target pagination did not advance') + } + + targetCount += page.targets.length + for (const { webhook, workflow } of page.targets) { + try { + const result = await dispatchResolvedWebhookTarget(webhook, workflow, event, request, { + requestId: payload.requestId, + path: webhook.path ?? undefined, + receivedAt: payload.receivedAt, + triggerTimestampMs: Date.parse(event.time), + }) + if (result.outcome === 'queued') processed += 1 + else if (result.outcome === 'ignored') ignored += 1 + else failed += 1 + } catch (error) { + failed += 1 + logger.error(`[${payload.requestId}] QuickBooks webhook target dispatch failed`, { + error, + eventId: event.id, + eventIndex, + webhookId: webhook.id, + }) + } + } + + logger.info(`[${payload.requestId}] QuickBooks webhook page completed`, { + eventId: event.id, + eventIndex, + ignored, + processed, + targetCount: page.targets.length, + }) + if (!nextCursor) break + afterWebhookId = nextCursor + } catch (error) { + failed += 1 + logger.error(`[${payload.requestId}] QuickBooks webhook event page failed`, { + error, + eventId: event.id, + eventIndex, + }) + break + } + } } - logger.info(`[${payload.requestId}] QuickBooks webhook page completed`, { - eventId: event.id, - eventIndex, - ignored, - processed, - targetCount: page.targets.length, - }) - return { + logger.info(`[${payload.requestId}] QuickBooks webhook delivery completed`, { + eventCount: payload.events.length, failed, ignored, processed, - targetCount: page.targets.length, - ...(nextCursor ? { nextCursor } : {}), - } -} - -async function enqueueQuickBooksWebhookContinuation( - payload: QuickBooksWebhookIngressPayload, - result: QuickBooksWebhookIngressResult -): Promise { - const eventIndex = payload.eventIndex ?? 0 - if (result.nextCursor) { - await enqueueQuickBooksWebhookIngress({ ...payload, afterWebhookId: result.nextCursor }) - } else if (eventIndex + 1 < payload.events.length) { - await enqueueQuickBooksWebhookIngress({ - ...payload, - eventIndex: eventIndex + 1, - afterWebhookId: undefined, - }) - } + targetCount, + }) + return { failed, ignored, processed, targetCount } } async function runQuickBooksWebhookIngressJob( payload: QuickBooksWebhookIngressPayload ): Promise { const result = await executeQuickBooksWebhookIngress(payload) - // The continuation has a deterministic job id, so retries cannot duplicate it. Enqueue it - // before retrying this page to avoid stranding later events in an already-acknowledged batch. - await enqueueQuickBooksWebhookContinuation(payload, result) if (result.failed > 0) { - throw new Error( - `Failed to dispatch ${result.failed} of ${result.targetCount} QuickBooks targets` - ) + throw new Error(`QuickBooks webhook delivery completed with ${result.failed} failures`) } } @@ -116,9 +121,8 @@ export async function enqueueQuickBooksWebhookIngress( payload: QuickBooksWebhookIngressPayload ): Promise { const jobQueue = await getJobQueue() - const eventIndex = payload.eventIndex ?? 0 return jobQueue.enqueue('quickbooks-webhook-ingress', payload, { - jobId: `quickbooks-webhook-ingress:${payload.requestId}:${eventIndex}:${payload.afterWebhookId ?? 'root'}`, + jobId: `quickbooks-webhook-ingress:${payload.requestId}`, maxAttempts: QUICKBOOKS_WEBHOOK_INGRESS_MAX_ATTEMPTS, concurrencyKey: 'quickbooks-webhook-ingress', concurrencyLimit: QUICKBOOKS_WEBHOOK_INGRESS_CONCURRENCY_LIMIT, From 71e6591bc8f8c84355906f1364347224825e5a15 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos <157128530+BillLeoutsakosvl346@users.noreply.github.com> Date: Wed, 5 Aug 2026 00:49:03 -0700 Subject: [PATCH 6/9] feat(quickbooks): complete webhook trigger matrix (#6248) * feat(quickbooks): complete webhook trigger matrix * fix(quickbooks): document webhook event selectors * fix(quickbooks): clarify webhook setup --------- Co-authored-by: Bill Leoutsakos --- .../docs/en/integrations/quickbooks.mdx | 667 +++++++++++++++++- apps/sim/blocks/blocks/quickbooks.ts | 44 ++ apps/sim/lib/integrations/integrations.json | 118 +++- .../sim/triggers/quickbooks/account_events.ts | 19 + apps/sim/triggers/quickbooks/budget_events.ts | 19 + apps/sim/triggers/quickbooks/class_events.ts | 19 + .../triggers/quickbooks/credit_memo_events.ts | 19 + .../triggers/quickbooks/currency_events.ts | 19 + .../triggers/quickbooks/department_events.ts | 19 + .../sim/triggers/quickbooks/deposit_events.ts | 19 + .../triggers/quickbooks/employee_events.ts | 19 + .../triggers/quickbooks/estimate_events.ts | 19 + apps/sim/triggers/quickbooks/index.ts | 22 + apps/sim/triggers/quickbooks/item_events.ts | 19 + .../quickbooks/journal_code_events.ts | 19 + .../quickbooks/journal_entry_events.ts | 19 + .../quickbooks/payment_method_events.ts | 19 + .../quickbooks/preferences_updated.ts | 19 + .../triggers/quickbooks/purchase_events.ts | 19 + .../triggers/quickbooks/quickbooks.test.ts | 49 +- .../quickbooks/refund_receipt_events.ts | 19 + .../quickbooks/sales_receipt_events.ts | 19 + .../triggers/quickbooks/tax_agency_events.ts | 19 + apps/sim/triggers/quickbooks/term_events.ts | 19 + .../quickbooks/time_activity_events.ts | 19 + .../triggers/quickbooks/transfer_events.ts | 19 + apps/sim/triggers/quickbooks/utils.ts | 212 +++++- .../quickbooks/vendor_credit_events.ts | 19 + apps/sim/triggers/registry.ts | 44 ++ 29 files changed, 1562 insertions(+), 12 deletions(-) create mode 100644 apps/sim/triggers/quickbooks/account_events.ts create mode 100644 apps/sim/triggers/quickbooks/budget_events.ts create mode 100644 apps/sim/triggers/quickbooks/class_events.ts create mode 100644 apps/sim/triggers/quickbooks/credit_memo_events.ts create mode 100644 apps/sim/triggers/quickbooks/currency_events.ts create mode 100644 apps/sim/triggers/quickbooks/department_events.ts create mode 100644 apps/sim/triggers/quickbooks/deposit_events.ts create mode 100644 apps/sim/triggers/quickbooks/employee_events.ts create mode 100644 apps/sim/triggers/quickbooks/estimate_events.ts create mode 100644 apps/sim/triggers/quickbooks/item_events.ts create mode 100644 apps/sim/triggers/quickbooks/journal_code_events.ts create mode 100644 apps/sim/triggers/quickbooks/journal_entry_events.ts create mode 100644 apps/sim/triggers/quickbooks/payment_method_events.ts create mode 100644 apps/sim/triggers/quickbooks/preferences_updated.ts create mode 100644 apps/sim/triggers/quickbooks/purchase_events.ts create mode 100644 apps/sim/triggers/quickbooks/refund_receipt_events.ts create mode 100644 apps/sim/triggers/quickbooks/sales_receipt_events.ts create mode 100644 apps/sim/triggers/quickbooks/tax_agency_events.ts create mode 100644 apps/sim/triggers/quickbooks/term_events.ts create mode 100644 apps/sim/triggers/quickbooks/time_activity_events.ts create mode 100644 apps/sim/triggers/quickbooks/transfer_events.ts create mode 100644 apps/sim/triggers/quickbooks/vendor_credit_events.ts diff --git a/apps/docs/content/docs/en/integrations/quickbooks.mdx b/apps/docs/content/docs/en/integrations/quickbooks.mdx index f56e15d647d..3efa2c08bdd 100644 --- a/apps/docs/content/docs/en/integrations/quickbooks.mdx +++ b/apps/docs/content/docs/en/integrations/quickbooks.mdx @@ -2924,6 +2924,36 @@ Download a QuickBooks file attachment through its short-lived URL A **Trigger** is a block that starts a workflow when an event happens in this service. +### QuickBooks Account Events + +Trigger when selected Account events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + ### QuickBooks Bill Events Trigger when selected Bill events occur in QuickBooks @@ -2982,6 +3012,126 @@ Trigger when selected Bill Payment events occur in QuickBooks | `data` | json | Optional event data supplied by Intuit | +--- + +### QuickBooks Budget Events + +Trigger when selected Budget events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Class Events + +Trigger when selected Class events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Credit Memo Events + +Trigger when selected Credit Memo events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Currency Events + +Trigger when selected Currency events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + --- ### QuickBooks Customer Events @@ -3012,6 +3162,126 @@ Trigger when selected Customer events occur in QuickBooks | `data` | json | Optional event data supplied by Intuit | +--- + +### QuickBooks Department Events + +Trigger when selected Department events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Deposit Events + +Trigger when selected Deposit events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Employee Events + +Trigger when selected Employee events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Estimate Events + +Trigger when selected Estimate events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + --- ### QuickBooks Invoice Events @@ -3044,9 +3314,9 @@ Trigger when selected Invoice events occur in QuickBooks --- -### QuickBooks Payment Events +### QuickBooks Item Events -Trigger when selected Payment events occur in QuickBooks +Trigger when selected Item events occur in QuickBooks #### Configuration @@ -3074,9 +3344,398 @@ Trigger when selected Payment events occur in QuickBooks --- -### QuickBooks Purchase Order Events +### QuickBooks Journal Code Events -Trigger when selected Purchase Order events occur in QuickBooks +Trigger when selected Journal Code events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Journal Entry Events + +Trigger when selected Journal Entry events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Payment Events + +Trigger when selected Payment events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Payment Method Events + +Trigger when selected Payment Method events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Preferences Updated + +Trigger when QuickBooks Preferences are updated + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Purchase Events + +Trigger when selected Purchase events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Purchase Order Events + +Trigger when selected Purchase Order events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Refund Receipt Events + +Trigger when selected Refund Receipt events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Sales Receipt Events + +Trigger when selected Sales Receipt events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Tax Agency Events + +Trigger when selected Tax Agency events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Term Events + +Trigger when selected Term events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Time Activity Events + +Trigger when selected Time Activity events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Transfer Events + +Trigger when selected Transfer events occur in QuickBooks + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | QuickBooks Account | +| `eventTypes` | string | Yes | Event Types | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Intuit webhook event ID | +| `eventType` | string | Full Intuit CloudEvent type | +| `entityType` | string | QuickBooks entity type | +| `action` | string | QuickBooks webhook action | +| `entityId` | string | QuickBooks entity ID | +| `realmId` | string | QuickBooks company realm ID | +| `eventTime` | string | Event timestamp | +| `specVersion` | string | CloudEvents specification version | +| `source` | string | Intuit event source | +| `contentType` | string | Event content type, when provided | +| `data` | json | Optional event data supplied by Intuit | + + +--- + +### QuickBooks Vendor Credit Events + +Trigger when selected Vendor Credit events occur in QuickBooks #### Configuration diff --git a/apps/sim/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts index 6b75472815b..ab5883ca0a1 100644 --- a/apps/sim/blocks/blocks/quickbooks.ts +++ b/apps/sim/blocks/blocks/quickbooks.ts @@ -1969,22 +1969,66 @@ export const QuickBooksBlock: BlockConfig = { }, ...getTrigger('quickbooks_invoice_events').subBlocks, ...getTrigger('quickbooks_customer_events').subBlocks, + ...getTrigger('quickbooks_estimate_events').subBlocks, ...getTrigger('quickbooks_payment_events').subBlocks, + ...getTrigger('quickbooks_credit_memo_events').subBlocks, + ...getTrigger('quickbooks_refund_receipt_events').subBlocks, + ...getTrigger('quickbooks_sales_receipt_events').subBlocks, ...getTrigger('quickbooks_vendor_events').subBlocks, ...getTrigger('quickbooks_bill_events').subBlocks, ...getTrigger('quickbooks_bill_payment_events').subBlocks, ...getTrigger('quickbooks_purchase_order_events').subBlocks, + ...getTrigger('quickbooks_purchase_events').subBlocks, + ...getTrigger('quickbooks_vendor_credit_events').subBlocks, + ...getTrigger('quickbooks_deposit_events').subBlocks, + ...getTrigger('quickbooks_journal_entry_events').subBlocks, + ...getTrigger('quickbooks_transfer_events').subBlocks, + ...getTrigger('quickbooks_item_events').subBlocks, + ...getTrigger('quickbooks_employee_events').subBlocks, + ...getTrigger('quickbooks_time_activity_events').subBlocks, + ...getTrigger('quickbooks_account_events').subBlocks, + ...getTrigger('quickbooks_budget_events').subBlocks, + ...getTrigger('quickbooks_class_events').subBlocks, + ...getTrigger('quickbooks_currency_events').subBlocks, + ...getTrigger('quickbooks_department_events').subBlocks, + ...getTrigger('quickbooks_journal_code_events').subBlocks, + ...getTrigger('quickbooks_payment_method_events').subBlocks, + ...getTrigger('quickbooks_preferences_updated').subBlocks, + ...getTrigger('quickbooks_tax_agency_events').subBlocks, + ...getTrigger('quickbooks_term_events').subBlocks, ], triggers: { enabled: true, available: [ 'quickbooks_invoice_events', 'quickbooks_customer_events', + 'quickbooks_estimate_events', 'quickbooks_payment_events', + 'quickbooks_credit_memo_events', + 'quickbooks_refund_receipt_events', + 'quickbooks_sales_receipt_events', 'quickbooks_vendor_events', 'quickbooks_bill_events', 'quickbooks_bill_payment_events', 'quickbooks_purchase_order_events', + 'quickbooks_purchase_events', + 'quickbooks_vendor_credit_events', + 'quickbooks_deposit_events', + 'quickbooks_journal_entry_events', + 'quickbooks_transfer_events', + 'quickbooks_item_events', + 'quickbooks_employee_events', + 'quickbooks_time_activity_events', + 'quickbooks_account_events', + 'quickbooks_budget_events', + 'quickbooks_class_events', + 'quickbooks_currency_events', + 'quickbooks_department_events', + 'quickbooks_journal_code_events', + 'quickbooks_payment_method_events', + 'quickbooks_preferences_updated', + 'quickbooks_tax_agency_events', + 'quickbooks_term_events', ], }, tools: { diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 7e3500901ad..e5b28eae763 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -14761,21 +14761,41 @@ ], "operationCount": 47, "triggers": [ + { + "id": "quickbooks_invoice_events", + "name": "QuickBooks Invoice Events", + "description": "Trigger when selected Invoice events occur in QuickBooks" + }, { "id": "quickbooks_customer_events", "name": "QuickBooks Customer Events", "description": "Trigger when selected Customer events occur in QuickBooks" }, { - "id": "quickbooks_invoice_events", - "name": "QuickBooks Invoice Events", - "description": "Trigger when selected Invoice events occur in QuickBooks" + "id": "quickbooks_estimate_events", + "name": "QuickBooks Estimate Events", + "description": "Trigger when selected Estimate events occur in QuickBooks" }, { "id": "quickbooks_payment_events", "name": "QuickBooks Payment Events", "description": "Trigger when selected Payment events occur in QuickBooks" }, + { + "id": "quickbooks_credit_memo_events", + "name": "QuickBooks Credit Memo Events", + "description": "Trigger when selected Credit Memo events occur in QuickBooks" + }, + { + "id": "quickbooks_refund_receipt_events", + "name": "QuickBooks Refund Receipt Events", + "description": "Trigger when selected Refund Receipt events occur in QuickBooks" + }, + { + "id": "quickbooks_sales_receipt_events", + "name": "QuickBooks Sales Receipt Events", + "description": "Trigger when selected Sales Receipt events occur in QuickBooks" + }, { "id": "quickbooks_vendor_events", "name": "QuickBooks Vendor Events", @@ -14795,9 +14815,99 @@ "id": "quickbooks_purchase_order_events", "name": "QuickBooks Purchase Order Events", "description": "Trigger when selected Purchase Order events occur in QuickBooks" + }, + { + "id": "quickbooks_purchase_events", + "name": "QuickBooks Purchase Events", + "description": "Trigger when selected Purchase events occur in QuickBooks" + }, + { + "id": "quickbooks_vendor_credit_events", + "name": "QuickBooks Vendor Credit Events", + "description": "Trigger when selected Vendor Credit events occur in QuickBooks" + }, + { + "id": "quickbooks_deposit_events", + "name": "QuickBooks Deposit Events", + "description": "Trigger when selected Deposit events occur in QuickBooks" + }, + { + "id": "quickbooks_journal_entry_events", + "name": "QuickBooks Journal Entry Events", + "description": "Trigger when selected Journal Entry events occur in QuickBooks" + }, + { + "id": "quickbooks_transfer_events", + "name": "QuickBooks Transfer Events", + "description": "Trigger when selected Transfer events occur in QuickBooks" + }, + { + "id": "quickbooks_item_events", + "name": "QuickBooks Item Events", + "description": "Trigger when selected Item events occur in QuickBooks" + }, + { + "id": "quickbooks_employee_events", + "name": "QuickBooks Employee Events", + "description": "Trigger when selected Employee events occur in QuickBooks" + }, + { + "id": "quickbooks_time_activity_events", + "name": "QuickBooks Time Activity Events", + "description": "Trigger when selected Time Activity events occur in QuickBooks" + }, + { + "id": "quickbooks_account_events", + "name": "QuickBooks Account Events", + "description": "Trigger when selected Account events occur in QuickBooks" + }, + { + "id": "quickbooks_budget_events", + "name": "QuickBooks Budget Events", + "description": "Trigger when selected Budget events occur in QuickBooks" + }, + { + "id": "quickbooks_class_events", + "name": "QuickBooks Class Events", + "description": "Trigger when selected Class events occur in QuickBooks" + }, + { + "id": "quickbooks_currency_events", + "name": "QuickBooks Currency Events", + "description": "Trigger when selected Currency events occur in QuickBooks" + }, + { + "id": "quickbooks_department_events", + "name": "QuickBooks Department Events", + "description": "Trigger when selected Department events occur in QuickBooks" + }, + { + "id": "quickbooks_journal_code_events", + "name": "QuickBooks Journal Code Events", + "description": "Trigger when selected Journal Code events occur in QuickBooks" + }, + { + "id": "quickbooks_payment_method_events", + "name": "QuickBooks Payment Method Events", + "description": "Trigger when selected Payment Method events occur in QuickBooks" + }, + { + "id": "quickbooks_preferences_updated", + "name": "QuickBooks Preferences Updated", + "description": "Trigger when QuickBooks Preferences are updated" + }, + { + "id": "quickbooks_tax_agency_events", + "name": "QuickBooks Tax Agency Events", + "description": "Trigger when selected Tax Agency events occur in QuickBooks" + }, + { + "id": "quickbooks_term_events", + "name": "QuickBooks Term Events", + "description": "Trigger when selected Term events occur in QuickBooks" } ], - "triggerCount": 7, + "triggerCount": 29, "authType": "oauth", "oauthServiceId": "quickbooks", "category": "tools", diff --git a/apps/sim/triggers/quickbooks/account_events.ts b/apps/sim/triggers/quickbooks/account_events.ts new file mode 100644 index 00000000000..7f8e80c3510 --- /dev/null +++ b/apps/sim/triggers/quickbooks/account_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksAccountEventsTrigger: TriggerConfig = { + id: 'quickbooks_account_events', + name: 'QuickBooks Account Events', + provider: 'quickbooks', + description: 'Trigger when selected Account events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_account_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/budget_events.ts b/apps/sim/triggers/quickbooks/budget_events.ts new file mode 100644 index 00000000000..811ca36b9c3 --- /dev/null +++ b/apps/sim/triggers/quickbooks/budget_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksBudgetEventsTrigger: TriggerConfig = { + id: 'quickbooks_budget_events', + name: 'QuickBooks Budget Events', + provider: 'quickbooks', + description: 'Trigger when selected Budget events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_budget_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/class_events.ts b/apps/sim/triggers/quickbooks/class_events.ts new file mode 100644 index 00000000000..0bad9dfd867 --- /dev/null +++ b/apps/sim/triggers/quickbooks/class_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksClassEventsTrigger: TriggerConfig = { + id: 'quickbooks_class_events', + name: 'QuickBooks Class Events', + provider: 'quickbooks', + description: 'Trigger when selected Class events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_class_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/credit_memo_events.ts b/apps/sim/triggers/quickbooks/credit_memo_events.ts new file mode 100644 index 00000000000..fd86973add3 --- /dev/null +++ b/apps/sim/triggers/quickbooks/credit_memo_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksCreditMemoEventsTrigger: TriggerConfig = { + id: 'quickbooks_credit_memo_events', + name: 'QuickBooks Credit Memo Events', + provider: 'quickbooks', + description: 'Trigger when selected Credit Memo events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_credit_memo_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/currency_events.ts b/apps/sim/triggers/quickbooks/currency_events.ts new file mode 100644 index 00000000000..f9fb4876fa1 --- /dev/null +++ b/apps/sim/triggers/quickbooks/currency_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksCurrencyEventsTrigger: TriggerConfig = { + id: 'quickbooks_currency_events', + name: 'QuickBooks Currency Events', + provider: 'quickbooks', + description: 'Trigger when selected Currency events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_currency_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/department_events.ts b/apps/sim/triggers/quickbooks/department_events.ts new file mode 100644 index 00000000000..4816234b708 --- /dev/null +++ b/apps/sim/triggers/quickbooks/department_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksDepartmentEventsTrigger: TriggerConfig = { + id: 'quickbooks_department_events', + name: 'QuickBooks Department Events', + provider: 'quickbooks', + description: 'Trigger when selected Department events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_department_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/deposit_events.ts b/apps/sim/triggers/quickbooks/deposit_events.ts new file mode 100644 index 00000000000..979c8b9476d --- /dev/null +++ b/apps/sim/triggers/quickbooks/deposit_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksDepositEventsTrigger: TriggerConfig = { + id: 'quickbooks_deposit_events', + name: 'QuickBooks Deposit Events', + provider: 'quickbooks', + description: 'Trigger when selected Deposit events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_deposit_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/employee_events.ts b/apps/sim/triggers/quickbooks/employee_events.ts new file mode 100644 index 00000000000..2b6f46b910f --- /dev/null +++ b/apps/sim/triggers/quickbooks/employee_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksEmployeeEventsTrigger: TriggerConfig = { + id: 'quickbooks_employee_events', + name: 'QuickBooks Employee Events', + provider: 'quickbooks', + description: 'Trigger when selected Employee events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_employee_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/estimate_events.ts b/apps/sim/triggers/quickbooks/estimate_events.ts new file mode 100644 index 00000000000..3056bdef66a --- /dev/null +++ b/apps/sim/triggers/quickbooks/estimate_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksEstimateEventsTrigger: TriggerConfig = { + id: 'quickbooks_estimate_events', + name: 'QuickBooks Estimate Events', + provider: 'quickbooks', + description: 'Trigger when selected Estimate events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_estimate_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/index.ts b/apps/sim/triggers/quickbooks/index.ts index a5651b1d2d3..6bec911cdca 100644 --- a/apps/sim/triggers/quickbooks/index.ts +++ b/apps/sim/triggers/quickbooks/index.ts @@ -1,7 +1,29 @@ +export { quickBooksAccountEventsTrigger } from '@/triggers/quickbooks/account_events' export { quickBooksBillEventsTrigger } from '@/triggers/quickbooks/bill_events' export { quickBooksBillPaymentEventsTrigger } from '@/triggers/quickbooks/bill_payment_events' +export { quickBooksBudgetEventsTrigger } from '@/triggers/quickbooks/budget_events' +export { quickBooksClassEventsTrigger } from '@/triggers/quickbooks/class_events' +export { quickBooksCreditMemoEventsTrigger } from '@/triggers/quickbooks/credit_memo_events' +export { quickBooksCurrencyEventsTrigger } from '@/triggers/quickbooks/currency_events' export { quickBooksCustomerEventsTrigger } from '@/triggers/quickbooks/customer_events' +export { quickBooksDepartmentEventsTrigger } from '@/triggers/quickbooks/department_events' +export { quickBooksDepositEventsTrigger } from '@/triggers/quickbooks/deposit_events' +export { quickBooksEmployeeEventsTrigger } from '@/triggers/quickbooks/employee_events' +export { quickBooksEstimateEventsTrigger } from '@/triggers/quickbooks/estimate_events' export { quickBooksInvoiceEventsTrigger } from '@/triggers/quickbooks/invoice_events' +export { quickBooksItemEventsTrigger } from '@/triggers/quickbooks/item_events' +export { quickBooksJournalCodeEventsTrigger } from '@/triggers/quickbooks/journal_code_events' +export { quickBooksJournalEntryEventsTrigger } from '@/triggers/quickbooks/journal_entry_events' export { quickBooksPaymentEventsTrigger } from '@/triggers/quickbooks/payment_events' +export { quickBooksPaymentMethodEventsTrigger } from '@/triggers/quickbooks/payment_method_events' +export { quickBooksPreferencesUpdatedTrigger } from '@/triggers/quickbooks/preferences_updated' +export { quickBooksPurchaseEventsTrigger } from '@/triggers/quickbooks/purchase_events' export { quickBooksPurchaseOrderEventsTrigger } from '@/triggers/quickbooks/purchase_order_events' +export { quickBooksRefundReceiptEventsTrigger } from '@/triggers/quickbooks/refund_receipt_events' +export { quickBooksSalesReceiptEventsTrigger } from '@/triggers/quickbooks/sales_receipt_events' +export { quickBooksTaxAgencyEventsTrigger } from '@/triggers/quickbooks/tax_agency_events' +export { quickBooksTermEventsTrigger } from '@/triggers/quickbooks/term_events' +export { quickBooksTimeActivityEventsTrigger } from '@/triggers/quickbooks/time_activity_events' +export { quickBooksTransferEventsTrigger } from '@/triggers/quickbooks/transfer_events' +export { quickBooksVendorCreditEventsTrigger } from '@/triggers/quickbooks/vendor_credit_events' export { quickBooksVendorEventsTrigger } from '@/triggers/quickbooks/vendor_events' diff --git a/apps/sim/triggers/quickbooks/item_events.ts b/apps/sim/triggers/quickbooks/item_events.ts new file mode 100644 index 00000000000..0f138f84e06 --- /dev/null +++ b/apps/sim/triggers/quickbooks/item_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksItemEventsTrigger: TriggerConfig = { + id: 'quickbooks_item_events', + name: 'QuickBooks Item Events', + provider: 'quickbooks', + description: 'Trigger when selected Item events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_item_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/journal_code_events.ts b/apps/sim/triggers/quickbooks/journal_code_events.ts new file mode 100644 index 00000000000..c5fabf22b52 --- /dev/null +++ b/apps/sim/triggers/quickbooks/journal_code_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksJournalCodeEventsTrigger: TriggerConfig = { + id: 'quickbooks_journal_code_events', + name: 'QuickBooks Journal Code Events', + provider: 'quickbooks', + description: 'Trigger when selected Journal Code events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_journal_code_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/journal_entry_events.ts b/apps/sim/triggers/quickbooks/journal_entry_events.ts new file mode 100644 index 00000000000..606d4f1e7d4 --- /dev/null +++ b/apps/sim/triggers/quickbooks/journal_entry_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksJournalEntryEventsTrigger: TriggerConfig = { + id: 'quickbooks_journal_entry_events', + name: 'QuickBooks Journal Entry Events', + provider: 'quickbooks', + description: 'Trigger when selected Journal Entry events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_journal_entry_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/payment_method_events.ts b/apps/sim/triggers/quickbooks/payment_method_events.ts new file mode 100644 index 00000000000..d59d89e5398 --- /dev/null +++ b/apps/sim/triggers/quickbooks/payment_method_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksPaymentMethodEventsTrigger: TriggerConfig = { + id: 'quickbooks_payment_method_events', + name: 'QuickBooks Payment Method Events', + provider: 'quickbooks', + description: 'Trigger when selected Payment Method events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_payment_method_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/preferences_updated.ts b/apps/sim/triggers/quickbooks/preferences_updated.ts new file mode 100644 index 00000000000..56165222d05 --- /dev/null +++ b/apps/sim/triggers/quickbooks/preferences_updated.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksSingleEventTriggerSubBlocks, + buildQuickBooksTriggerOutputs, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksPreferencesUpdatedTrigger: TriggerConfig = { + id: 'quickbooks_preferences_updated', + name: 'QuickBooks Preferences Updated', + provider: 'quickbooks', + description: 'Trigger when QuickBooks Preferences are updated', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksSingleEventTriggerSubBlocks('quickbooks_preferences_updated'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/purchase_events.ts b/apps/sim/triggers/quickbooks/purchase_events.ts new file mode 100644 index 00000000000..198d0a4259b --- /dev/null +++ b/apps/sim/triggers/quickbooks/purchase_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksPurchaseEventsTrigger: TriggerConfig = { + id: 'quickbooks_purchase_events', + name: 'QuickBooks Purchase Events', + provider: 'quickbooks', + description: 'Trigger when selected Purchase events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_purchase_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/quickbooks.test.ts b/apps/sim/triggers/quickbooks/quickbooks.test.ts index 66fe7a6fdb0..1f93cfb3842 100644 --- a/apps/sim/triggers/quickbooks/quickbooks.test.ts +++ b/apps/sim/triggers/quickbooks/quickbooks.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { QuickBooksBlock } from '@/blocks/blocks/quickbooks' import { getTrigger } from '@/triggers' import { + isQuickBooksEventMatch, QUICKBOOKS_TRIGGER_DEFINITIONS, quickBooksTriggerOptions, } from '@/triggers/quickbooks/utils' @@ -14,6 +15,12 @@ describe('QuickBooks triggers', () => { 'quickbooks_invoice_events', ...ids.filter((id) => id !== 'quickbooks_invoice_events'), ]) + expect(ids).toHaveLength(29) + expect( + QUICKBOOKS_TRIGGER_DEFINITIONS.reduce((total, definition) => { + return total + definition.actions.length + }, 0) + ).toBe(101) for (const id of ids) expect(getTrigger(id).id).toBe(id) }) @@ -26,8 +33,48 @@ describe('QuickBooks triggers', () => { for (const definition of QUICKBOOKS_TRIGGER_DEFINITIONS) { const trigger = getTrigger(definition.id) const eventTypes = trigger.subBlocks.find((subBlock) => subBlock.id === 'eventTypes') - expect(eventTypes?.options?.map((option) => option.id)).toEqual(definition.actions) + if (definition.id === 'quickbooks_preferences_updated') { + expect(eventTypes).toBeUndefined() + } else { + expect(eventTypes?.options?.map((option) => option.id)).toEqual(definition.actions) + } + const instructions = trigger.subBlocks.find((subBlock) => + subBlock.id.startsWith('triggerInstructions') + )?.defaultValue + expect(instructions).toContain('enable webhook notifications') + if (definition.id === 'quickbooks_preferences_updated') { + expect(instructions).not.toContain('Select the Preferences Updated') + } expect(trigger.outputs).toEqual(getTrigger('quickbooks_invoice_events').outputs) } }) + + it('matches all 101 supported combinations and rejects unsupported actions', () => { + for (const definition of QUICKBOOKS_TRIGGER_DEFINITIONS) { + for (const action of definition.actions) { + expect( + isQuickBooksEventMatch( + definition.id, + `qbo.${definition.entity}.${action}.v1`, + definition.actions + ) + ).toBe(true) + } + expect( + isQuickBooksEventMatch(definition.id, `qbo.${definition.entity}.unsupported.v1`, [ + 'unsupported', + ]) + ).toBe(false) + expect( + isQuickBooksEventMatch( + definition.id, + `qbo.other.${definition.actions[0]}.v1`, + definition.actions + ) + ).toBe(false) + } + expect( + isQuickBooksEventMatch('quickbooks_preferences_updated', 'qbo.preferences.updated.v1', null) + ).toBe(true) + }) }) diff --git a/apps/sim/triggers/quickbooks/refund_receipt_events.ts b/apps/sim/triggers/quickbooks/refund_receipt_events.ts new file mode 100644 index 00000000000..01828070af2 --- /dev/null +++ b/apps/sim/triggers/quickbooks/refund_receipt_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksRefundReceiptEventsTrigger: TriggerConfig = { + id: 'quickbooks_refund_receipt_events', + name: 'QuickBooks Refund Receipt Events', + provider: 'quickbooks', + description: 'Trigger when selected Refund Receipt events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_refund_receipt_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/sales_receipt_events.ts b/apps/sim/triggers/quickbooks/sales_receipt_events.ts new file mode 100644 index 00000000000..7e9d52daaed --- /dev/null +++ b/apps/sim/triggers/quickbooks/sales_receipt_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksSalesReceiptEventsTrigger: TriggerConfig = { + id: 'quickbooks_sales_receipt_events', + name: 'QuickBooks Sales Receipt Events', + provider: 'quickbooks', + description: 'Trigger when selected Sales Receipt events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_sales_receipt_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/tax_agency_events.ts b/apps/sim/triggers/quickbooks/tax_agency_events.ts new file mode 100644 index 00000000000..b95d42be716 --- /dev/null +++ b/apps/sim/triggers/quickbooks/tax_agency_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksTaxAgencyEventsTrigger: TriggerConfig = { + id: 'quickbooks_tax_agency_events', + name: 'QuickBooks Tax Agency Events', + provider: 'quickbooks', + description: 'Trigger when selected Tax Agency events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_tax_agency_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/term_events.ts b/apps/sim/triggers/quickbooks/term_events.ts new file mode 100644 index 00000000000..ccc092875ac --- /dev/null +++ b/apps/sim/triggers/quickbooks/term_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksTermEventsTrigger: TriggerConfig = { + id: 'quickbooks_term_events', + name: 'QuickBooks Term Events', + provider: 'quickbooks', + description: 'Trigger when selected Term events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_term_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/time_activity_events.ts b/apps/sim/triggers/quickbooks/time_activity_events.ts new file mode 100644 index 00000000000..48c83bb924b --- /dev/null +++ b/apps/sim/triggers/quickbooks/time_activity_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksTimeActivityEventsTrigger: TriggerConfig = { + id: 'quickbooks_time_activity_events', + name: 'QuickBooks Time Activity Events', + provider: 'quickbooks', + description: 'Trigger when selected Time Activity events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_time_activity_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/transfer_events.ts b/apps/sim/triggers/quickbooks/transfer_events.ts new file mode 100644 index 00000000000..ef2cbd4fd03 --- /dev/null +++ b/apps/sim/triggers/quickbooks/transfer_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksTransferEventsTrigger: TriggerConfig = { + id: 'quickbooks_transfer_events', + name: 'QuickBooks Transfer Events', + provider: 'quickbooks', + description: 'Trigger when selected Transfer events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_transfer_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/quickbooks/utils.ts b/apps/sim/triggers/quickbooks/utils.ts index fc7a5560561..4dc854ef470 100644 --- a/apps/sim/triggers/quickbooks/utils.ts +++ b/apps/sim/triggers/quickbooks/utils.ts @@ -28,6 +28,14 @@ export const QUICKBOOKS_TRIGGER_DEFINITIONS: readonly QuickBooksTriggerDefinitio entityType: 'Customer', actions: ['created', 'updated', 'deleted', 'merged'], }, + { + id: 'quickbooks_estimate_events', + label: 'Estimate Events', + group: 'Sales and Receivables', + entity: 'estimate', + entityType: 'Estimate', + actions: ['created', 'updated', 'deleted', 'emailed'], + }, { id: 'quickbooks_invoice_events', label: 'Invoice Events', @@ -44,6 +52,30 @@ export const QUICKBOOKS_TRIGGER_DEFINITIONS: readonly QuickBooksTriggerDefinitio entityType: 'Payment', actions: ['created', 'updated', 'deleted', 'voided', 'emailed'], }, + { + id: 'quickbooks_credit_memo_events', + label: 'Credit Memo Events', + group: 'Sales and Receivables', + entity: 'creditmemo', + entityType: 'CreditMemo', + actions: ['created', 'updated', 'deleted', 'voided', 'emailed'], + }, + { + id: 'quickbooks_refund_receipt_events', + label: 'Refund Receipt Events', + group: 'Sales and Receivables', + entity: 'refundreceipt', + entityType: 'RefundReceipt', + actions: ['created', 'updated', 'deleted', 'voided', 'emailed'], + }, + { + id: 'quickbooks_sales_receipt_events', + label: 'Sales Receipt Events', + group: 'Sales and Receivables', + entity: 'salesreceipt', + entityType: 'SalesReceipt', + actions: ['created', 'updated', 'deleted', 'voided', 'emailed'], + }, { id: 'quickbooks_vendor_events', label: 'Vendor Events', @@ -76,6 +108,150 @@ export const QUICKBOOKS_TRIGGER_DEFINITIONS: readonly QuickBooksTriggerDefinitio entityType: 'PurchaseOrder', actions: ['created', 'updated', 'deleted', 'emailed'], }, + { + id: 'quickbooks_purchase_events', + label: 'Purchase Events', + group: 'Purchasing and Payables', + entity: 'purchase', + entityType: 'Purchase', + actions: ['created', 'updated', 'deleted', 'voided'], + }, + { + id: 'quickbooks_vendor_credit_events', + label: 'Vendor Credit Events', + group: 'Purchasing and Payables', + entity: 'vendorcredit', + entityType: 'VendorCredit', + actions: ['created', 'updated', 'deleted'], + }, + { + id: 'quickbooks_deposit_events', + label: 'Deposit Events', + group: 'Accounting', + entity: 'deposit', + entityType: 'Deposit', + actions: ['created', 'updated', 'deleted'], + }, + { + id: 'quickbooks_journal_entry_events', + label: 'Journal Entry Events', + group: 'Accounting', + entity: 'journalentry', + entityType: 'JournalEntry', + actions: ['created', 'updated', 'deleted'], + }, + { + id: 'quickbooks_transfer_events', + label: 'Transfer Events', + group: 'Accounting', + entity: 'transfer', + entityType: 'Transfer', + actions: ['created', 'updated', 'deleted', 'voided'], + }, + { + id: 'quickbooks_item_events', + label: 'Item Events', + group: 'Products and People', + entity: 'item', + entityType: 'Item', + actions: ['created', 'updated', 'deleted', 'merged'], + }, + { + id: 'quickbooks_employee_events', + label: 'Employee Events', + group: 'Products and People', + entity: 'employee', + entityType: 'Employee', + actions: ['created', 'updated', 'deleted', 'merged'], + }, + { + id: 'quickbooks_time_activity_events', + label: 'Time Activity Events', + group: 'Products and People', + entity: 'timeactivity', + entityType: 'TimeActivity', + actions: ['created', 'updated', 'deleted'], + }, + { + id: 'quickbooks_account_events', + label: 'Account Events', + group: 'Company and Setup', + entity: 'account', + entityType: 'Account', + actions: ['created', 'updated', 'deleted', 'merged'], + }, + { + id: 'quickbooks_budget_events', + label: 'Budget Events', + group: 'Company and Setup', + entity: 'budget', + entityType: 'Budget', + actions: ['created', 'updated'], + }, + { + id: 'quickbooks_class_events', + label: 'Class Events', + group: 'Company and Setup', + entity: 'class', + entityType: 'Class', + actions: ['created', 'updated', 'deleted', 'merged'], + }, + { + id: 'quickbooks_currency_events', + label: 'Currency Events', + group: 'Company and Setup', + entity: 'currency', + entityType: 'Currency', + actions: ['created', 'updated'], + }, + { + id: 'quickbooks_department_events', + label: 'Department Events', + group: 'Company and Setup', + entity: 'department', + entityType: 'Department', + actions: ['created', 'updated', 'merged'], + }, + { + id: 'quickbooks_journal_code_events', + label: 'Journal Code Events', + group: 'Company and Setup', + entity: 'journalcode', + entityType: 'JournalCode', + actions: ['created', 'updated'], + }, + { + id: 'quickbooks_payment_method_events', + label: 'Payment Method Events', + group: 'Company and Setup', + entity: 'paymentmethod', + entityType: 'PaymentMethod', + actions: ['created', 'updated', 'merged'], + }, + { + id: 'quickbooks_preferences_updated', + label: 'Preferences Updated', + group: 'Company and Setup', + entity: 'preferences', + entityType: 'Preferences', + actions: ['updated'], + }, + { + id: 'quickbooks_tax_agency_events', + label: 'Tax Agency Events', + group: 'Company and Setup', + entity: 'taxagency', + entityType: 'TaxAgency', + actions: ['created', 'updated'], + }, + { + id: 'quickbooks_term_events', + label: 'Term Events', + group: 'Company and Setup', + entity: 'term', + entityType: 'Term', + actions: ['created', 'updated'], + }, ] as const export const quickBooksTriggerOptions = QUICKBOOKS_TRIGGER_DEFINITIONS.map((definition) => ({ @@ -112,14 +288,17 @@ export function isQuickBooksEventMatch( const parsed = parseQuickBooksWebhookType(eventType) if (!definition || !parsed || parsed.entity !== definition.entity) return false if (!definition.actions.includes(parsed.action as QuickBooksWebhookAction)) return false + if (definition.actions.length === 1) return true return Array.isArray(selectedActions) && selectedActions.includes(parsed.action) } -function quickBooksSetupInstructions(entityLabel: string): string { +function quickBooksSetupInstructions(entityLabel: string, includeActionSelection = true): string { const instructions = [ - 'App setup: A Sim operator must register https://<your-sim-domain>/api/webhooks/quickbooks as the app-level endpoint in the Intuit Developer Portal and configure the server-only QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN.', + 'App setup: A Sim operator must register https://<your-sim-domain>/api/webhooks/quickbooks as the app-level endpoint, enable webhook notifications for the supported QuickBooks entities and actions in the Intuit Developer Portal, and configure the server-only QUICKBOOKS_WEBHOOK_VERIFIER_TOKEN.', 'Connect the QuickBooks account whose company should receive these events.', - `Select the ${entityLabel} actions this workflow should handle.`, + ...(includeActionSelection + ? [`Select the ${entityLabel} actions this workflow should handle.`] + : []), 'Deploy the workflow to activate the trigger.', ] @@ -171,6 +350,33 @@ export function buildQuickBooksTriggerSubBlocks(triggerId: string): SubBlockConf ] } +export function buildQuickBooksSingleEventTriggerSubBlocks(triggerId: string): SubBlockConfig[] { + const definition = getQuickBooksTriggerDefinition(triggerId) + if (!definition) throw new Error(`Unknown QuickBooks trigger: ${triggerId}`) + + return [ + { + id: 'triggerCredentials', + title: 'QuickBooks Account', + type: 'oauth-input', + serviceId: 'quickbooks', + requiredScopes: getScopesForService('quickbooks'), + mode: 'trigger', + required: true, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: quickBooksSetupInstructions(definition.label, false), + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + export function buildQuickBooksTriggerOutputs(): Record { return { eventId: { type: 'string', description: 'Intuit webhook event ID' }, diff --git a/apps/sim/triggers/quickbooks/vendor_credit_events.ts b/apps/sim/triggers/quickbooks/vendor_credit_events.ts new file mode 100644 index 00000000000..e359a09cc75 --- /dev/null +++ b/apps/sim/triggers/quickbooks/vendor_credit_events.ts @@ -0,0 +1,19 @@ +import { QuickBooksIcon } from '@/components/icons' +import { + buildQuickBooksTriggerOutputs, + buildQuickBooksTriggerSubBlocks, + QUICKBOOKS_WEBHOOK_HEADERS, +} from '@/triggers/quickbooks/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const quickBooksVendorCreditEventsTrigger: TriggerConfig = { + id: 'quickbooks_vendor_credit_events', + name: 'QuickBooks Vendor Credit Events', + provider: 'quickbooks', + description: 'Trigger when selected Vendor Credit events occur in QuickBooks', + version: '1.0.0', + icon: QuickBooksIcon, + subBlocks: buildQuickBooksTriggerSubBlocks('quickbooks_vendor_credit_events'), + outputs: buildQuickBooksTriggerOutputs(), + webhook: { method: 'POST', headers: { ...QUICKBOOKS_WEBHOOK_HEADERS } }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index ff39129cc0f..0cf598f3b2b 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -370,12 +370,34 @@ import { pagerdutyWebhookTrigger, } from '@/triggers/pagerduty' import { + quickBooksAccountEventsTrigger, quickBooksBillEventsTrigger, quickBooksBillPaymentEventsTrigger, + quickBooksBudgetEventsTrigger, + quickBooksClassEventsTrigger, + quickBooksCreditMemoEventsTrigger, + quickBooksCurrencyEventsTrigger, quickBooksCustomerEventsTrigger, + quickBooksDepartmentEventsTrigger, + quickBooksDepositEventsTrigger, + quickBooksEmployeeEventsTrigger, + quickBooksEstimateEventsTrigger, quickBooksInvoiceEventsTrigger, + quickBooksItemEventsTrigger, + quickBooksJournalCodeEventsTrigger, + quickBooksJournalEntryEventsTrigger, quickBooksPaymentEventsTrigger, + quickBooksPaymentMethodEventsTrigger, + quickBooksPreferencesUpdatedTrigger, + quickBooksPurchaseEventsTrigger, quickBooksPurchaseOrderEventsTrigger, + quickBooksRefundReceiptEventsTrigger, + quickBooksSalesReceiptEventsTrigger, + quickBooksTaxAgencyEventsTrigger, + quickBooksTermEventsTrigger, + quickBooksTimeActivityEventsTrigger, + quickBooksTransferEventsTrigger, + quickBooksVendorCreditEventsTrigger, quickBooksVendorEventsTrigger, } from '@/triggers/quickbooks' import { @@ -747,12 +769,34 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { pagerduty_incident_escalated: pagerdutyIncidentEscalatedTrigger, pagerduty_incident_reassigned: pagerdutyIncidentReassignedTrigger, pagerduty_webhook: pagerdutyWebhookTrigger, + quickbooks_account_events: quickBooksAccountEventsTrigger, quickbooks_bill_events: quickBooksBillEventsTrigger, quickbooks_bill_payment_events: quickBooksBillPaymentEventsTrigger, + quickbooks_budget_events: quickBooksBudgetEventsTrigger, + quickbooks_class_events: quickBooksClassEventsTrigger, + quickbooks_credit_memo_events: quickBooksCreditMemoEventsTrigger, + quickbooks_currency_events: quickBooksCurrencyEventsTrigger, quickbooks_customer_events: quickBooksCustomerEventsTrigger, + quickbooks_department_events: quickBooksDepartmentEventsTrigger, + quickbooks_deposit_events: quickBooksDepositEventsTrigger, + quickbooks_employee_events: quickBooksEmployeeEventsTrigger, + quickbooks_estimate_events: quickBooksEstimateEventsTrigger, quickbooks_invoice_events: quickBooksInvoiceEventsTrigger, + quickbooks_item_events: quickBooksItemEventsTrigger, + quickbooks_journal_code_events: quickBooksJournalCodeEventsTrigger, + quickbooks_journal_entry_events: quickBooksJournalEntryEventsTrigger, quickbooks_payment_events: quickBooksPaymentEventsTrigger, + quickbooks_payment_method_events: quickBooksPaymentMethodEventsTrigger, + quickbooks_preferences_updated: quickBooksPreferencesUpdatedTrigger, + quickbooks_purchase_events: quickBooksPurchaseEventsTrigger, quickbooks_purchase_order_events: quickBooksPurchaseOrderEventsTrigger, + quickbooks_refund_receipt_events: quickBooksRefundReceiptEventsTrigger, + quickbooks_sales_receipt_events: quickBooksSalesReceiptEventsTrigger, + quickbooks_tax_agency_events: quickBooksTaxAgencyEventsTrigger, + quickbooks_term_events: quickBooksTermEventsTrigger, + quickbooks_time_activity_events: quickBooksTimeActivityEventsTrigger, + quickbooks_transfer_events: quickBooksTransferEventsTrigger, + quickbooks_vendor_credit_events: quickBooksVendorCreditEventsTrigger, quickbooks_vendor_events: quickBooksVendorEventsTrigger, resend_email_sent: resendEmailSentTrigger, resend_email_delivered: resendEmailDeliveredTrigger, From eff8d3dc9a91a8894d0241170e227e6bc90a0485 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Wed, 5 Aug 2026 01:14:53 -0700 Subject: [PATCH 7/9] refactor(quickbooks): align webhook trigger routing --- .../quickbooks-webhook-ingress.test.ts | 90 ++++++-------- .../background/quickbooks-webhook-ingress.ts | 97 +++++++-------- .../quickbooks-webhook-targets.test.ts | 98 --------------- .../background/quickbooks-webhook-targets.ts | 112 ------------------ .../__tests__/webhook-deactivation.test.ts | 28 +++++ apps/sim/lib/credentials/deletion.ts | 18 +-- apps/sim/lib/webhooks/deploy.test.ts | 74 ++++++++++++ apps/sim/lib/webhooks/deploy.ts | 48 +++++++- 8 files changed, 239 insertions(+), 326 deletions(-) delete mode 100644 apps/sim/background/quickbooks-webhook-targets.test.ts delete mode 100644 apps/sim/background/quickbooks-webhook-targets.ts create mode 100644 apps/sim/lib/credentials/__tests__/webhook-deactivation.test.ts diff --git a/apps/sim/background/quickbooks-webhook-ingress.test.ts b/apps/sim/background/quickbooks-webhook-ingress.test.ts index 2380197b3f9..2fddf4d77c3 100644 --- a/apps/sim/background/quickbooks-webhook-ingress.test.ts +++ b/apps/sim/background/quickbooks-webhook-ingress.test.ts @@ -2,15 +2,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockDispatch, mockEnqueue, mockFindPage } = vi.hoisted(() => ({ +const { mockDispatch, mockEnqueue, mockFindWebhooks } = vi.hoisted(() => ({ mockDispatch: vi.fn(), mockEnqueue: vi.fn(), - mockFindPage: vi.fn(), + mockFindWebhooks: vi.fn(), })) -vi.mock('@trigger.dev/sdk', () => ({ task: vi.fn((config: unknown) => config) })) -vi.mock('@/lib/webhooks/processor', () => ({ dispatchResolvedWebhookTarget: mockDispatch })) -vi.mock('@/background/quickbooks-webhook-targets', () => ({ - findQuickBooksWebhookTargetPage: mockFindPage, +vi.mock('@trigger.dev/sdk', () => ({ + task: vi.fn((config: unknown) => config), +})) +vi.mock('@/lib/webhooks/processor', () => ({ + dispatchResolvedWebhookTarget: mockDispatch, + findWebhooksByRoutingKey: mockFindWebhooks, })) vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: vi.fn(async () => ({ enqueue: mockEnqueue })), @@ -46,25 +48,12 @@ describe('QuickBooks webhook ingress job', () => { it('routes the batch by company and dispatches targets sequentially', async () => { const order: string[] = [] - mockFindPage - .mockResolvedValueOnce({ - hasMore: true, - nextCursor: 'w2', - targets: [ - { webhook: { id: 'w1' }, workflow: { id: 'wf1' } }, - { webhook: { id: 'w2' }, workflow: { id: 'wf2' } }, - ], - }) - .mockResolvedValueOnce({ - hasMore: false, - nextCursor: null, - targets: [{ webhook: { id: 'w3' }, workflow: { id: 'wf3' } }], - }) - .mockResolvedValueOnce({ - hasMore: false, - nextCursor: null, - targets: [{ webhook: { id: 'w4' }, workflow: { id: 'wf4' } }], - }) + mockFindWebhooks + .mockResolvedValueOnce([ + { webhook: { id: 'w1' }, workflow: { id: 'wf1' } }, + { webhook: { id: 'w2' }, workflow: { id: 'wf2' } }, + ]) + .mockResolvedValueOnce([{ webhook: { id: 'w3' }, workflow: { id: 'wf3' } }]) mockDispatch.mockImplementation(async (webhook: { id: string }) => { order.push(webhook.id) return { outcome: 'queued' } @@ -72,19 +61,20 @@ describe('QuickBooks webhook ingress job', () => { await expect(executeQuickBooksWebhookIngress(payload)).resolves.toEqual({ failed: 0, ignored: 0, - processed: 4, - targetCount: 4, + processed: 3, + targetCount: 3, }) - expect(mockFindPage).toHaveBeenCalledWith('456', 'request-1', undefined) - expect(mockFindPage).toHaveBeenCalledWith('456', 'request-1', 'w2') - expect(mockFindPage).toHaveBeenCalledWith('789', 'request-1', undefined) - expect(order).toEqual(['w1', 'w2', 'w3', 'w4']) + expect(mockFindWebhooks).toHaveBeenNthCalledWith(1, '456', 'request-1', 'quickbooks') + expect(mockFindWebhooks).toHaveBeenNthCalledWith(2, '789', 'request-1', 'quickbooks') + expect(order).toEqual(['w1', 'w2', 'w3']) }) it('enqueues the bounded delivery once without copying it into continuation jobs', async () => { - mockFindPage.mockResolvedValue({ hasMore: false, nextCursor: null, targets: [] }) + mockFindWebhooks.mockResolvedValue([]) await enqueueQuickBooksWebhookIngress(payload) - const options = mockEnqueue.mock.calls[0][2] as { runner: () => Promise } + const options = mockEnqueue.mock.calls[0][2] as { + runner: () => Promise + } await options.runner() expect(mockEnqueue).toHaveBeenCalledOnce() expect(mockEnqueue).toHaveBeenCalledWith( @@ -97,39 +87,33 @@ describe('QuickBooks webhook ingress job', () => { }) it('continues later events before retrying a delivery with failed targets', async () => { - mockFindPage - .mockResolvedValueOnce({ - hasMore: false, - nextCursor: null, - targets: [ - { webhook: { id: 'w1' }, workflow: { id: 'wf1' } }, - { webhook: { id: 'w2' }, workflow: { id: 'wf2' } }, - ], - }) - .mockResolvedValueOnce({ - hasMore: false, - nextCursor: null, - targets: [{ webhook: { id: 'w3' }, workflow: { id: 'wf3' } }], - }) + mockFindWebhooks + .mockResolvedValueOnce([ + { webhook: { id: 'w1' }, workflow: { id: 'wf1' } }, + { webhook: { id: 'w2' }, workflow: { id: 'wf2' } }, + ]) + .mockResolvedValueOnce([{ webhook: { id: 'w3' }, workflow: { id: 'wf3' } }]) mockDispatch .mockRejectedValueOnce(new Error('dispatch unavailable')) .mockResolvedValueOnce({ outcome: 'failed' }) .mockResolvedValueOnce({ outcome: 'queued' }) await enqueueQuickBooksWebhookIngress(payload) - const options = mockEnqueue.mock.calls[0][2] as { runner: () => Promise } + const options = mockEnqueue.mock.calls[0][2] as { + runner: () => Promise + } await expect(options.runner()).rejects.toThrow( 'QuickBooks webhook delivery completed with 2 failures' ) - expect(mockFindPage).toHaveBeenCalledWith('789', 'request-1', undefined) + expect(mockFindWebhooks).toHaveBeenCalledWith('789', 'request-1', 'quickbooks') expect(mockDispatch).toHaveBeenCalledTimes(3) expect(mockEnqueue).toHaveBeenCalledOnce() }) - it('continues later events when a target page cannot be resolved', async () => { - mockFindPage + it('continues later events when targets cannot be resolved', async () => { + mockFindWebhooks .mockRejectedValueOnce(new Error('database unavailable')) - .mockResolvedValueOnce({ hasMore: false, nextCursor: null, targets: [] }) + .mockResolvedValueOnce([]) await expect(executeQuickBooksWebhookIngress(payload)).resolves.toEqual({ failed: 1, @@ -137,6 +121,6 @@ describe('QuickBooks webhook ingress job', () => { processed: 0, targetCount: 0, }) - expect(mockFindPage).toHaveBeenCalledWith('789', 'request-1', undefined) + expect(mockFindWebhooks).toHaveBeenCalledWith('789', 'request-1', 'quickbooks') }) }) diff --git a/apps/sim/background/quickbooks-webhook-ingress.ts b/apps/sim/background/quickbooks-webhook-ingress.ts index b091225fdcd..4f93f9fd359 100644 --- a/apps/sim/background/quickbooks-webhook-ingress.ts +++ b/apps/sim/background/quickbooks-webhook-ingress.ts @@ -3,8 +3,7 @@ import { task } from '@trigger.dev/sdk' import { NextRequest } from 'next/server' import type { QuickBooksWebhookEvent } from '@/lib/api/contracts/webhooks' import { getJobQueue } from '@/lib/core/async-jobs' -import { dispatchResolvedWebhookTarget } from '@/lib/webhooks/processor' -import { findQuickBooksWebhookTargetPage } from '@/background/quickbooks-webhook-targets' +import { dispatchResolvedWebhookTarget, findWebhooksByRoutingKey } from '@/lib/webhooks/processor' const logger = createLogger('QuickBooksWebhookIngressTask') @@ -25,7 +24,7 @@ export interface QuickBooksWebhookIngressResult { targetCount: number } -/** Process the bounded delivery sequentially, retaining at most one target page at a time. */ +/** Process the bounded delivery sequentially through the shared routing-key dispatcher. */ export async function executeQuickBooksWebhookIngress( payload: QuickBooksWebhookIngressPayload ): Promise { @@ -40,61 +39,51 @@ export async function executeQuickBooksWebhookIngress( headers: payload.headers, body: JSON.stringify(event), }) - let afterWebhookId: string | undefined - while (true) { - try { - const page = await findQuickBooksWebhookTargetPage( - event.intuitaccountid, - payload.requestId, - afterWebhookId - ) - const nextCursor = page.hasMore ? page.nextCursor : null - if (page.hasMore && (!nextCursor || nextCursor === afterWebhookId)) { - throw new Error('QuickBooks webhook target pagination did not advance') - } + try { + const targets = await findWebhooksByRoutingKey( + event.intuitaccountid, + payload.requestId, + 'quickbooks' + ) + targetCount += targets.length - targetCount += page.targets.length - for (const { webhook, workflow } of page.targets) { - try { - const result = await dispatchResolvedWebhookTarget(webhook, workflow, event, request, { - requestId: payload.requestId, - path: webhook.path ?? undefined, - receivedAt: payload.receivedAt, - triggerTimestampMs: Date.parse(event.time), - }) - if (result.outcome === 'queued') processed += 1 - else if (result.outcome === 'ignored') ignored += 1 - else failed += 1 - } catch (error) { - failed += 1 - logger.error(`[${payload.requestId}] QuickBooks webhook target dispatch failed`, { - error, - eventId: event.id, - eventIndex, - webhookId: webhook.id, - }) - } + for (const { webhook, workflow } of targets) { + try { + const result = await dispatchResolvedWebhookTarget(webhook, workflow, event, request, { + requestId: payload.requestId, + path: webhook.path ?? undefined, + receivedAt: payload.receivedAt, + triggerTimestampMs: Date.parse(event.time), + }) + if (result.outcome === 'queued') processed += 1 + else if (result.outcome === 'ignored') ignored += 1 + else failed += 1 + } catch (error) { + failed += 1 + logger.error(`[${payload.requestId}] QuickBooks webhook target dispatch failed`, { + error, + eventId: event.id, + eventIndex, + webhookId: webhook.id, + }) } - - logger.info(`[${payload.requestId}] QuickBooks webhook page completed`, { - eventId: event.id, - eventIndex, - ignored, - processed, - targetCount: page.targets.length, - }) - if (!nextCursor) break - afterWebhookId = nextCursor - } catch (error) { - failed += 1 - logger.error(`[${payload.requestId}] QuickBooks webhook event page failed`, { - error, - eventId: event.id, - eventIndex, - }) - break } + + logger.info(`[${payload.requestId}] QuickBooks webhook event completed`, { + eventId: event.id, + eventIndex, + ignored, + processed, + targetCount: targets.length, + }) + } catch (error) { + failed += 1 + logger.error(`[${payload.requestId}] QuickBooks webhook target lookup failed`, { + error, + eventId: event.id, + eventIndex, + }) } } diff --git a/apps/sim/background/quickbooks-webhook-targets.test.ts b/apps/sim/background/quickbooks-webhook-targets.test.ts deleted file mode 100644 index af0e24c64c4..00000000000 --- a/apps/sim/background/quickbooks-webhook-targets.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** @vitest-environment node */ - -import { dbChainMock, dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' - -const { mockCredentialExpression, mockEq, mockGt, tables } = vi.hoisted(() => ({ - mockCredentialExpression: vi.fn(() => 'webhook.credentialId'), - mockEq: vi.fn((left: unknown, right: unknown) => ({ left, right })), - mockGt: vi.fn((left: unknown, right: unknown) => ({ gt: [left, right] })), - tables: { - account: { id: 'account.id', accountId: 'account.accountId', providerId: 'account.providerId' }, - credential: { - id: 'credential.id', - accountId: 'credential.accountId', - providerId: 'credential.providerId', - type: 'credential.type', - workspaceId: 'credential.workspaceId', - }, - webhook: { - deploymentVersionId: 'webhook.deploymentVersionId', - isActive: 'webhook.isActive', - id: 'webhook.id', - archivedAt: 'webhook.archivedAt', - provider: 'webhook.provider', - providerConfig: 'webhook.providerConfig', - workflowId: 'webhook.workflowId', - }, - workflow: { - id: 'workflow.id', - workspaceId: 'workflow.workspaceId', - archivedAt: 'workflow.archivedAt', - }, - workflowDeploymentVersion: { - id: 'workflowDeploymentVersion.id', - workflowId: 'workflowDeploymentVersion.workflowId', - isActive: 'workflowDeploymentVersion.isActive', - }, - }, -})) - -vi.mock('@sim/db', () => ({ - ...dbChainMock, - ...tables, - webhookCredentialIdExpression: mockCredentialExpression, -})) -vi.mock('drizzle-orm', () => ({ - and: vi.fn((...conditions: unknown[]) => conditions), - asc: vi.fn((value: unknown) => ({ asc: value })), - eq: mockEq, - gt: mockGt, - isNull: vi.fn((value: unknown) => ({ isNull: value })), - like: vi.fn((left: unknown, right: unknown) => ({ left, right })), - or: vi.fn((...conditions: unknown[]) => conditions), -})) - -import { - findQuickBooksWebhookTargetPage, - QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE, -} from '@/background/quickbooks-webhook-targets' - -const UUID = '11111111-2222-4333-8444-555555555555' - -describe('findQuickBooksWebhookTargetPage', () => { - afterAll(resetDbChainMock) - beforeEach(() => { - vi.clearAllMocks() - resetDbChainMock() - }) - - it('returns only the exact company identity and enforces provider/workspace bindings', async () => { - queueTableRows(tables.account, [ - { - accountId: `quickbooks:456:user-${UUID}`, - webhookId: 'webhook-1', - webhook: { id: 'webhook-1' }, - workflow: { id: 'workflow-1' }, - }, - { - accountId: `quickbooks:789:user-${UUID}`, - webhookId: 'webhook-2', - webhook: { id: 'webhook-2' }, - workflow: { id: 'workflow-2' }, - }, - ]) - - const page = await findQuickBooksWebhookTargetPage('456', 'request-1') - expect(page.targets).toEqual([{ webhook: { id: 'webhook-1' }, workflow: { id: 'workflow-1' } }]) - expect(mockEq).toHaveBeenCalledWith('credential.providerId', 'quickbooks') - expect(mockEq).toHaveBeenCalledWith('webhook.provider', 'quickbooks') - expect(mockEq).toHaveBeenCalledWith('workflow.workspaceId', 'credential.workspaceId') - }) - - it('uses a fixed 100-target keyset page', async () => { - await findQuickBooksWebhookTargetPage('456', 'request-2', 'webhook-100') - expect(mockGt).toHaveBeenCalledWith('webhook.id', 'webhook-100') - expect(dbChainMockFns.limit).toHaveBeenCalledWith(QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE) - }) -}) diff --git a/apps/sim/background/quickbooks-webhook-targets.ts b/apps/sim/background/quickbooks-webhook-targets.ts deleted file mode 100644 index 646ef8f3862..00000000000 --- a/apps/sim/background/quickbooks-webhook-targets.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - account, - credential, - db, - webhook, - webhookCredentialIdExpression, - workflow, - workflowDeploymentVersion, -} from '@sim/db' -import { createLogger } from '@sim/logger' -import { and, asc, eq, gt, isNull, like, or } from 'drizzle-orm' -import { parseQuickBooksAccountId } from '@/lib/oauth/quickbooks' -import { deliverableWebhookPredicate } from '@/lib/webhooks/delivery-predicate' - -const logger = createLogger('QuickBooksWebhookTargets') -const ACCOUNT_ID_UUID_LIKE_SUFFIX = '________-____-____-____-____________' - -export const QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE = 100 - -export interface QuickBooksWebhookTargetPage { - hasMore: boolean - nextCursor: string | null - targets: Array<{ - webhook: typeof webhook.$inferSelect - workflow: typeof workflow.$inferSelect - }> -} - -function escapeLikePattern(value: string): string { - return value.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_') -} - -/** Resolve one bounded page of active trigger targets for exactly one QuickBooks company. */ -export async function findQuickBooksWebhookTargetPage( - realmId: string, - requestId: string, - afterWebhookId?: string -): Promise { - if (!realmId) return { hasMore: false, nextCursor: null, targets: [] } - - const rows = await db - .select({ accountId: account.accountId, webhookId: webhook.id, webhook, workflow }) - .from(account) - .innerJoin( - credential, - and( - eq(credential.accountId, account.id), - eq(credential.type, 'oauth'), - eq(credential.providerId, 'quickbooks') - ) - ) - .innerJoin( - webhook, - and( - eq(webhookCredentialIdExpression(webhook.providerConfig), credential.id), - eq(webhook.provider, 'quickbooks'), - deliverableWebhookPredicate(webhook) - ) - ) - .innerJoin( - workflow, - and( - eq(workflow.id, webhook.workflowId), - eq(workflow.workspaceId, credential.workspaceId), - isNull(workflow.archivedAt) - ) - ) - .leftJoin( - workflowDeploymentVersion, - and( - eq(workflowDeploymentVersion.workflowId, workflow.id), - eq(workflowDeploymentVersion.isActive, true) - ) - ) - .where( - and( - eq(account.providerId, 'quickbooks'), - like( - account.accountId, - `quickbooks:${escapeLikePattern(realmId)}:%-${ACCOUNT_ID_UUID_LIKE_SUFFIX}` - ), - or( - eq(webhook.deploymentVersionId, workflowDeploymentVersion.id), - and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId)) - ), - afterWebhookId ? gt(webhook.id, afterWebhookId) : undefined - ) - ) - .orderBy(asc(webhook.id)) - .limit(QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE) - - const targets = rows - .filter((row) => { - try { - return parseQuickBooksAccountId(row.accountId).realmId === realmId - } catch { - return false - } - }) - .map(({ webhook: webhookRecord, workflow: workflowRecord }) => ({ - webhook: webhookRecord, - workflow: workflowRecord, - })) - const nextCursor = rows.at(-1)?.webhookId ?? null - const hasMore = rows.length === QUICKBOOKS_WEBHOOK_TARGET_PAGE_SIZE - - logger.info(`[${requestId}] Resolved QuickBooks webhook target page`, { - hasMore, - targetCount: targets.length, - }) - return { hasMore, nextCursor, targets } -} diff --git a/apps/sim/lib/credentials/__tests__/webhook-deactivation.test.ts b/apps/sim/lib/credentials/__tests__/webhook-deactivation.test.ts new file mode 100644 index 00000000000..7ce73cd112c --- /dev/null +++ b/apps/sim/lib/credentials/__tests__/webhook-deactivation.test.ts @@ -0,0 +1,28 @@ +/** + * @vitest-environment node + */ + +import { dbChainMockFns, drizzleOrmMock, resetDbChainMock, schemaMock } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { clearCredentialRefs } from '@/lib/credentials/deletion' + +describe('credential-bound webhook deactivation', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + afterAll(resetDbChainMock) + + it('deactivates QuickBooks and Slack webhook registrations when a credential is removed', async () => { + await clearCredentialRefs('credential-1', 'workspace-1') + + expect(dbChainMockFns.update).toHaveBeenCalledWith(schemaMock.webhook) + expect(dbChainMockFns.set).toHaveBeenCalledWith( + expect.objectContaining({ isActive: false, updatedAt: expect.any(Date) }) + ) + expect(drizzleOrmMock.eq).toHaveBeenCalledWith(schemaMock.webhook.provider, 'quickbooks') + expect(drizzleOrmMock.eq).toHaveBeenCalledWith(schemaMock.webhook.provider, 'slack_app') + expect(drizzleOrmMock.eq).toHaveBeenCalledWith(schemaMock.webhook.provider, 'slack') + }) +}) diff --git a/apps/sim/lib/credentials/deletion.ts b/apps/sim/lib/credentials/deletion.ts index 1ea84e8d1c5..c1d43ff3c14 100644 --- a/apps/sim/lib/credentials/deletion.ts +++ b/apps/sim/lib/credentials/deletion.ts @@ -90,18 +90,18 @@ export async function clearCredentialRefs( clearInPausedExecutions(credentialId, workspaceId, needle), clearInWorkflowCheckpoints(credentialId, workspaceId, needle), clearInKnowledgeConnectors(credentialId), - deactivateSlackAppWebhooks(credentialId), + deactivateCredentialBoundWebhooks(credentialId), ]) } /** - * Deactivates Slack trigger webhooks bound to this credential so inbound - * events stop routing once the account is disconnected: native (`slack_app`) - * rows reference it via `providerConfig.credentialId`, custom-bot (`slack`) - * rows via `routingKey` = the bot credential id. Neither is a foreign key, so - * neither is covered by CASCADE. + * Deactivates app-level trigger webhooks bound to this credential so inbound + * events stop routing once the account is disconnected. Native Slack and + * QuickBooks rows reference it via `providerConfig.credentialId`; custom-bot + * Slack rows use `routingKey` = the bot credential id. Neither is a foreign + * key, so neither is covered by CASCADE. */ -async function deactivateSlackAppWebhooks(credentialId: string): Promise { +async function deactivateCredentialBoundWebhooks(credentialId: string): Promise { await db .update(schema.webhook) .set({ isActive: false, updatedAt: new Date() }) @@ -113,6 +113,10 @@ async function deactivateSlackAppWebhooks(credentialId: string): Promise { eq(schema.webhook.provider, 'slack_app'), sql`${schema.webhook.providerConfig}->>'credentialId' = ${credentialId}` ), + and( + eq(schema.webhook.provider, 'quickbooks'), + sql`${schema.webhook.providerConfig}->>'credentialId' = ${credentialId}` + ), and(eq(schema.webhook.provider, 'slack'), eq(schema.webhook.routingKey, credentialId)) ) ) diff --git a/apps/sim/lib/webhooks/deploy.test.ts b/apps/sim/lib/webhooks/deploy.test.ts index da0f6f4a4f0..95be0fd454c 100644 --- a/apps/sim/lib/webhooks/deploy.test.ts +++ b/apps/sim/lib/webhooks/deploy.test.ts @@ -359,3 +359,77 @@ describe('resolveWebhookConfigForBlock — slack_oauth routing', () => { expect(mockFetchSlackTeamId).not.toHaveBeenCalled() }) }) + +describe('resolveWebhookConfigForBlock — QuickBooks routing', () => { + const quickBooksTriggerDef = { + provider: 'quickbooks', + name: 'QuickBooks Invoice Events', + subBlocks: [ + { + id: 'triggerCredentials', + mode: 'trigger', + serviceId: 'quickbooks', + required: true, + }, + ], + } + + function resolveQuickBooks( + credentialReference: string, + workflow: Record = { workspaceId: 'ws-1' } + ) { + ;(getBlock as unknown as Mock).mockReturnValue({ category: 'tools' }) + ;(getTrigger as unknown as Mock).mockReturnValue(quickBooksTriggerDef) + const block = makeBlock('quickbooks', { + selectedTriggerId: 'quickbooks_invoice_events', + triggerCredentials: credentialReference, + }) + block.triggerMode = true + return resolveWebhookConfigForBlock({ + block, + workflow, + userId: 'deployer-1', + requestId: 'req-1', + }) + } + + it('routes a workspace-owned credential by its stored QuickBooks realm ID', async () => { + queueTableRows(credential, [{ id: 'cred-qb-1' }]) + mockResolveOAuthAccountId.mockResolvedValue({ accountId: 'account-row-1' }) + queueTableRows(account, [ + { + accountId: 'quickbooks:9341456000000000:user-123e4567-e89b-12d3-a456-426614174000', + }, + ]) + + const result = await resolveQuickBooks('cred-qb-1') + + expect(result?.success).toBe(true) + if (!result?.success) throw new Error('expected success') + expect(result.config.provider).toBe('quickbooks') + expect(result.config.routingKey).toBe('9341456000000000') + expect(result.config.triggerPath).toBeNull() + expect(result.config.providerConfig.credentialId).toBe('cred-qb-1') + }) + + it('rejects a QuickBooks credential outside the workflow workspace', async () => { + const result = await resolveQuickBooks('cred-foreign') + + expect(result?.success).toBe(false) + if (result?.success) throw new Error('expected failure') + expect(result?.error?.message).toContain('not available in this workspace') + expect(mockResolveOAuthAccountId).not.toHaveBeenCalled() + }) + + it('rejects a malformed stored QuickBooks company identity', async () => { + queueTableRows(credential, [{ id: 'cred-qb-1' }]) + mockResolveOAuthAccountId.mockResolvedValue({ accountId: 'account-row-1' }) + queueTableRows(account, [{ accountId: 'invalid-quickbooks-account' }]) + + const result = await resolveQuickBooks('cred-qb-1') + + expect(result?.success).toBe(false) + if (result?.success) throw new Error('expected failure') + expect(result?.error?.message).toContain('Reconnect it and try again') + }) +}) diff --git a/apps/sim/lib/webhooks/deploy.ts b/apps/sim/lib/webhooks/deploy.ts index b5e04b99df2..87223b1438c 100644 --- a/apps/sim/lib/webhooks/deploy.ts +++ b/apps/sim/lib/webhooks/deploy.ts @@ -6,6 +6,7 @@ import { generateShortId } from '@sim/utils/id' import { and, eq, inArray, isNull, or } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { getProviderIdFromServiceId } from '@/lib/oauth' +import { parseQuickBooksAccountId } from '@/lib/oauth/quickbooks' import { WebhookPathClaimConflictError } from '@/lib/webhooks/path-claims' import { PendingWebhookVerificationTracker } from '@/lib/webhooks/pending-verification' import { @@ -363,8 +364,8 @@ export async function resolveTriggerCredentialId( /** * Resolves a trigger block to its persisted webhook config, including the - * Slack-specific routing branch. Exported for unit testing that branch; not part - * of the public deploy API. + * provider-specific routing branches. Exported for unit testing those branches; + * not part of the public deploy API. */ export async function resolveWebhookConfigForBlock(input: { block: BlockState @@ -565,6 +566,49 @@ export async function resolveWebhookConfigForBlock(input: { // (`slack_app`) rows on providerConfig.credentialId. providerConfig.credentialId = resolvedCredentialId } + } else if (triggerDef.provider === 'quickbooks') { + if (!credentialId) { + return { + success: false, + error: { + message: 'Select a QuickBooks account for the trigger.', + status: 400, + }, + } + } + + const resolvedAccount = await resolveOAuthAccountId(credentialId) + const [quickBooksAccount] = resolvedAccount?.accountId + ? await db + .select({ accountId: account.accountId }) + .from(account) + .where( + and(eq(account.id, resolvedAccount.accountId), eq(account.providerId, 'quickbooks')) + ) + .limit(1) + : [] + + let realmId: string | undefined + try { + realmId = quickBooksAccount?.accountId + ? parseQuickBooksAccountId(quickBooksAccount.accountId).realmId + : undefined + } catch { + realmId = undefined + } + + if (!realmId) { + return { + success: false, + error: { + message: 'Could not verify the connected QuickBooks company. Reconnect it and try again.', + status: 400, + }, + } + } + + effectivePath = null + routingKey = realmId } return { From afb7009b8e92e4b215226fa6db3749630a8e8068 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Wed, 5 Aug 2026 14:53:56 -0700 Subject: [PATCH 8/9] fix(quickbooks): align webhook event selection --- .../workflow/edit-workflow/validation.test.ts | 31 +++++++++++++++++++ .../workflow/edit-workflow/validation.ts | 3 +- apps/sim/lib/copilot/vfs/serializers.test.ts | 14 +++++++++ apps/sim/lib/copilot/vfs/serializers.ts | 1 + .../lib/webhooks/providers/quickbooks.test.ts | 26 ++++++++++++++-- apps/sim/lib/webhooks/providers/quickbooks.ts | 10 ++++-- .../triggers/quickbooks/quickbooks.test.ts | 15 +++++++-- apps/sim/triggers/quickbooks/utils.ts | 9 ++++-- 8 files changed, 100 insertions(+), 9 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts index 8ae05a41465..0c99d871827 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts @@ -251,6 +251,7 @@ import { collectUnresolvedReferences, preValidateCredentialInputs, validateInputsForBlock, + validateValueForSubBlockType, validateWorkflowSelectorIds, } from './validation' @@ -264,6 +265,36 @@ describe('validateInputsForBlock', () => { mockValidateSelectorIds.mockResolvedValue({ valid: [], invalid: [] }) }) + it('validates every value in a multi-select dropdown array', () => { + const config = { + id: 'eventTypes', + type: 'dropdown' as const, + multiSelect: true, + options: [ + { id: 'created', label: 'Created' }, + { id: 'updated', label: 'Updated' }, + ], + } + expect( + validateValueForSubBlockType( + config, + ['created', 'updated'], + 'eventTypes', + 'quickbooks', + 'quickbooks-1' + ).valid + ).toBe(true) + expect( + validateValueForSubBlockType( + config, + ['created', 'deleted'], + 'eventTypes', + 'quickbooks', + 'quickbooks-1' + ).valid + ).toBe(false) + }) + it('accepts condition-input arrays with arbitrary item ids', () => { const result = validateInputsForBlock( 'condition', diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts index 48d44f21dbc..c5caa677c82 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts @@ -297,7 +297,8 @@ export function validateValueForSubBlockType( : subBlockConfig.options if (options && Array.isArray(options)) { const validIds = options.map((opt) => opt.id) - if (!validIds.includes(value)) { + const values = subBlockConfig.multiSelect && Array.isArray(value) ? value : [value] + if (values.some((item) => !validIds.includes(item))) { return { valid: false, error: { diff --git a/apps/sim/lib/copilot/vfs/serializers.test.ts b/apps/sim/lib/copilot/vfs/serializers.test.ts index 4394b746e0b..facaf068fe7 100644 --- a/apps/sim/lib/copilot/vfs/serializers.test.ts +++ b/apps/sim/lib/copilot/vfs/serializers.test.ts @@ -111,6 +111,20 @@ describe('VFS metadata serializers', () => { }) describe('hosted-key VFS metadata', () => { + it('preserves multi-select dropdown behavior in block schemas', () => { + const block = { + type: 'events', + name: 'Events', + subBlocks: [{ id: 'eventTypes', type: 'dropdown', multiSelect: true }], + tools: { access: [] }, + inputs: {}, + outputs: {}, + } as unknown as BlockConfig + + const schema = JSON.parse(serializeBlockSchema(block)) + expect(schema.subBlocks[0].multiSelect).toBe(true) + }) + it('indexes hosted and conditional-hosted operations for every configured service', () => { const metadata = JSON.parse( serializeApiKeyIntegrations( diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index 5dd5959a86e..6a737c03b8a 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -549,6 +549,7 @@ function serializeSubBlock(sb: SubBlockConfig): Record { if (sb.required === true) result.required = true if (sb.defaultValue !== undefined) result.defaultValue = sb.defaultValue if (sb.mode) result.mode = sb.mode + if (sb.multiSelect) result.multiSelect = true if (sb.canonicalParamId) result.canonicalParamId = sb.canonicalParamId if (sb.condition && typeof sb.condition !== 'function') result.condition = sb.condition if (sb.dependsOn) result.dependsOn = sb.dependsOn diff --git a/apps/sim/lib/webhooks/providers/quickbooks.test.ts b/apps/sim/lib/webhooks/providers/quickbooks.test.ts index 76b22c34329..5cf95bcf85a 100644 --- a/apps/sim/lib/webhooks/providers/quickbooks.test.ts +++ b/apps/sim/lib/webhooks/providers/quickbooks.test.ts @@ -2,7 +2,7 @@ import crypto from 'node:crypto' import { NextRequest } from 'next/server' import { describe, expect, it } from 'vitest' import { quickBooksHandler, verifyQuickBooksSignature } from '@/lib/webhooks/providers/quickbooks' -import { isQuickBooksEventMatch } from '@/triggers/quickbooks/utils' +import { isQuickBooksEventMatch, quickBooksEventTypesSubBlockId } from '@/triggers/quickbooks/utils' const event = { specversion: '1.0', @@ -33,6 +33,28 @@ describe('QuickBooks webhook provider', () => { expect(isQuickBooksEventMatch('quickbooks_bill_events', event.type, ['updated'])).toBe(false) }) + it('normalizes Intuit void events to the configured voided action', async () => { + for (const entity of ['invoice', 'payment']) { + const voidEvent = { ...event, type: `qbo.${entity}.void.v1` } + expect( + isQuickBooksEventMatch(`quickbooks_${entity}_events`, voidEvent.type, ['voided']) + ).toBe(true) + + const result = await quickBooksHandler.formatInput!({ + body: voidEvent, + webhook: {}, + workflow: { id: 'workflow-1', userId: 'user-1' }, + headers: {}, + requestId: `request-${entity}`, + }) + expect(result.input).toMatchObject({ + eventType: `qbo.${entity}.void.v1`, + entityType: entity, + action: 'voided', + }) + } + }) + it('formats only the common verified event fields', async () => { const result = await quickBooksHandler.formatInput!({ body: event, @@ -66,7 +88,7 @@ describe('QuickBooks webhook provider', () => { requestId: 'request-5', providerConfig: { triggerId: 'quickbooks_invoice_events', - eventTypes: ['updated'], + [quickBooksEventTypesSubBlockId('quickbooks_invoice_events')]: ['updated'], }, webhook: {}, workflow: {}, diff --git a/apps/sim/lib/webhooks/providers/quickbooks.ts b/apps/sim/lib/webhooks/providers/quickbooks.ts index 9b57f5d0303..9980f652c71 100644 --- a/apps/sim/lib/webhooks/providers/quickbooks.ts +++ b/apps/sim/lib/webhooks/providers/quickbooks.ts @@ -53,8 +53,14 @@ export const quickBooksHandler: WebhookProviderHandler = { const event = asRecord(body) const triggerId = typeof providerConfig.triggerId === 'string' ? providerConfig.triggerId : '' const eventType = typeof event?.type === 'string' ? event.type : '' - const { isQuickBooksEventMatch } = await import('@/triggers/quickbooks/utils') - return isQuickBooksEventMatch(triggerId, eventType, providerConfig.eventTypes) + const { isQuickBooksEventMatch, quickBooksEventTypesSubBlockId } = await import( + '@/triggers/quickbooks/utils' + ) + return isQuickBooksEventMatch( + triggerId, + eventType, + providerConfig[quickBooksEventTypesSubBlockId(triggerId)] + ) }, async formatInput({ body }: FormatInputContext): Promise { diff --git a/apps/sim/triggers/quickbooks/quickbooks.test.ts b/apps/sim/triggers/quickbooks/quickbooks.test.ts index 1f93cfb3842..9ccf6087edc 100644 --- a/apps/sim/triggers/quickbooks/quickbooks.test.ts +++ b/apps/sim/triggers/quickbooks/quickbooks.test.ts @@ -4,6 +4,7 @@ import { getTrigger } from '@/triggers' import { isQuickBooksEventMatch, QUICKBOOKS_TRIGGER_DEFINITIONS, + quickBooksEventTypesSubBlockId, quickBooksTriggerOptions, } from '@/triggers/quickbooks/utils' @@ -32,11 +33,14 @@ describe('QuickBooks triggers', () => { ).toHaveLength(1) for (const definition of QUICKBOOKS_TRIGGER_DEFINITIONS) { const trigger = getTrigger(definition.id) - const eventTypes = trigger.subBlocks.find((subBlock) => subBlock.id === 'eventTypes') + const eventTypes = trigger.subBlocks.find( + (subBlock) => subBlock.id === quickBooksEventTypesSubBlockId(definition.id) + ) if (definition.id === 'quickbooks_preferences_updated') { expect(eventTypes).toBeUndefined() } else { expect(eventTypes?.options?.map((option) => option.id)).toEqual(definition.actions) + expect(eventTypes?.multiSelect).toBe(true) } const instructions = trigger.subBlocks.find((subBlock) => subBlock.id.startsWith('triggerInstructions') @@ -47,15 +51,22 @@ describe('QuickBooks triggers', () => { } expect(trigger.outputs).toEqual(getTrigger('quickbooks_invoice_events').outputs) } + + const eventTypeIds = QuickBooksBlock.subBlocks + .map((subBlock) => subBlock.id) + .filter((id) => id.startsWith('eventTypes_quickbooks_')) + expect(eventTypeIds).toHaveLength(28) + expect(new Set(eventTypeIds).size).toBe(eventTypeIds.length) }) it('matches all 101 supported combinations and rejects unsupported actions', () => { for (const definition of QUICKBOOKS_TRIGGER_DEFINITIONS) { for (const action of definition.actions) { + const providerAction = action === 'voided' ? 'void' : action expect( isQuickBooksEventMatch( definition.id, - `qbo.${definition.entity}.${action}.v1`, + `qbo.${definition.entity}.${providerAction}.v1`, definition.actions ) ).toBe(true) diff --git a/apps/sim/triggers/quickbooks/utils.ts b/apps/sim/triggers/quickbooks/utils.ts index 4dc854ef470..469106ec948 100644 --- a/apps/sim/triggers/quickbooks/utils.ts +++ b/apps/sim/triggers/quickbooks/utils.ts @@ -260,6 +260,10 @@ export const quickBooksTriggerOptions = QUICKBOOKS_TRIGGER_DEFINITIONS.map((defi group: definition.group, })) +export function quickBooksEventTypesSubBlockId(triggerId: string): string { + return `eventTypes_${triggerId}` +} + export const QUICKBOOKS_WEBHOOK_HEADERS = { 'Content-Type': 'application/json', 'intuit-signature': '', @@ -276,7 +280,8 @@ export function parseQuickBooksWebhookType(type: string): { entity: string } | null { const match = /^qbo\.([a-z]+)\.([a-z]+)\.v1$/.exec(type) - return match ? { entity: match[1], action: match[2] } : null + if (!match) return null + return { entity: match[1], action: match[2] === 'void' ? 'voided' : match[2] } } export function isQuickBooksEventMatch( @@ -326,7 +331,7 @@ export function buildQuickBooksTriggerSubBlocks(triggerId: string): SubBlockConf condition: { field: 'selectedTriggerId', value: triggerId }, }, { - id: 'eventTypes', + id: quickBooksEventTypesSubBlockId(triggerId), title: 'Event Types', type: 'dropdown', multiSelect: true, From 6e6494d3098c910161f45a5ff8e12cc62278f444 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Wed, 5 Aug 2026 15:37:22 -0700 Subject: [PATCH 9/9] fix(copilot): reject empty required multi-selects --- .../tools/server/workflow/edit-workflow/validation.test.ts | 4 ++++ .../copilot/tools/server/workflow/edit-workflow/validation.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts index 0c99d871827..c8e8a3d5c58 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts @@ -270,6 +270,7 @@ describe('validateInputsForBlock', () => { id: 'eventTypes', type: 'dropdown' as const, multiSelect: true, + required: true, options: [ { id: 'created', label: 'Created' }, { id: 'updated', label: 'Updated' }, @@ -293,6 +294,9 @@ describe('validateInputsForBlock', () => { 'quickbooks-1' ).valid ).toBe(false) + expect( + validateValueForSubBlockType(config, [], 'eventTypes', 'quickbooks', 'quickbooks-1').valid + ).toBe(false) }) it('accepts condition-input arrays with arbitrary item ids', () => { diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts index c5caa677c82..d64a988cbf6 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts @@ -298,7 +298,9 @@ export function validateValueForSubBlockType( if (options && Array.isArray(options)) { const validIds = options.map((opt) => opt.id) const values = subBlockConfig.multiSelect && Array.isArray(value) ? value : [value] - if (values.some((item) => !validIds.includes(item))) { + const isEmptyRequiredMultiSelect = + subBlockConfig.multiSelect && subBlockConfig.required && values.length === 0 + if (isEmptyRequiredMultiSelect || values.some((item) => !validIds.includes(item))) { return { valid: false, error: {