diff --git a/apps/docs/content/docs/en/integrations/quickbooks.mdx b/apps/docs/content/docs/en/integrations/quickbooks.mdx index cf2222d54c4..3efa2c08bdd 100644 --- a/apps/docs/content/docs/en/integrations/quickbooks.mdx +++ b/apps/docs/content/docs/en/integrations/quickbooks.mdx @@ -2919,3 +2919,874 @@ 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 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 + +#### 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 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 + +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 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 + +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 Item Events + +Trigger when selected Item 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 Code Events + +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 + +| 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..2fddf4d77c3 --- /dev/null +++ b/apps/sim/background/quickbooks-webhook-ingress.test.ts @@ -0,0 +1,126 @@ +/** @vitest-environment node */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockDispatch, mockEnqueue, mockFindWebhooks } = vi.hoisted(() => ({ + mockDispatch: vi.fn(), + mockEnqueue: vi.fn(), + mockFindWebhooks: vi.fn(), +})) +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 })), +})) + +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 the batch by company and dispatches targets sequentially', async () => { + const order: string[] = [] + 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' } + }) + await expect(executeQuickBooksWebhookIngress(payload)).resolves.toEqual({ + failed: 0, + ignored: 0, + processed: 3, + targetCount: 3, + }) + 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 () => { + mockFindWebhooks.mockResolvedValue([]) + await enqueueQuickBooksWebhookIngress(payload) + const options = mockEnqueue.mock.calls[0][2] as { + runner: () => Promise + } + await options.runner() + expect(mockEnqueue).toHaveBeenCalledOnce() + expect(mockEnqueue).toHaveBeenCalledWith( + 'quickbooks-webhook-ingress', + payload, + expect.objectContaining({ + jobId: 'quickbooks-webhook-ingress:request-1', + }) + ) + }) + + it('continues later events before retrying a delivery with failed targets', async () => { + 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 + } + await expect(options.runner()).rejects.toThrow( + 'QuickBooks webhook delivery completed with 2 failures' + ) + expect(mockFindWebhooks).toHaveBeenCalledWith('789', 'request-1', 'quickbooks') + expect(mockDispatch).toHaveBeenCalledTimes(3) + expect(mockEnqueue).toHaveBeenCalledOnce() + }) + + it('continues later events when targets cannot be resolved', async () => { + mockFindWebhooks + .mockRejectedValueOnce(new Error('database unavailable')) + .mockResolvedValueOnce([]) + + await expect(executeQuickBooksWebhookIngress(payload)).resolves.toEqual({ + failed: 1, + ignored: 0, + processed: 0, + targetCount: 0, + }) + 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 new file mode 100644 index 00000000000..4f93f9fd359 --- /dev/null +++ b/apps/sim/background/quickbooks-webhook-ingress.ts @@ -0,0 +1,133 @@ +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, findWebhooksByRoutingKey } from '@/lib/webhooks/processor' + +const logger = createLogger('QuickBooksWebhookIngressTask') + +export const QUICKBOOKS_WEBHOOK_INGRESS_CONCURRENCY_LIMIT = 50 +export const QUICKBOOKS_WEBHOOK_INGRESS_MAX_ATTEMPTS = 3 + +export interface QuickBooksWebhookIngressPayload { + events: QuickBooksWebhookEvent[] + headers: { 'content-type': string } + requestId: string + receivedAt: number +} + +export interface QuickBooksWebhookIngressResult { + failed: number + ignored: number + processed: number + targetCount: number +} + +/** Process the bounded delivery sequentially through the shared routing-key dispatcher. */ +export async function executeQuickBooksWebhookIngress( + payload: QuickBooksWebhookIngressPayload +): Promise { + let ignored = 0 + let processed = 0 + let failed = 0 + 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), + }) + + try { + const targets = await findWebhooksByRoutingKey( + event.intuitaccountid, + payload.requestId, + 'quickbooks' + ) + targetCount += targets.length + + 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 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, + }) + } + } + + logger.info(`[${payload.requestId}] QuickBooks webhook delivery completed`, { + eventCount: payload.events.length, + failed, + ignored, + processed, + targetCount, + }) + return { failed, ignored, processed, targetCount } +} + +async function runQuickBooksWebhookIngressJob( + payload: QuickBooksWebhookIngressPayload +): Promise { + const result = await executeQuickBooksWebhookIngress(payload) + if (result.failed > 0) { + throw new Error(`QuickBooks webhook delivery completed with ${result.failed} failures`) + } +} + +export async function enqueueQuickBooksWebhookIngress( + payload: QuickBooksWebhookIngressPayload +): Promise { + const jobQueue = await getJobQueue() + return jobQueue.enqueue('quickbooks-webhook-ingress', payload, { + jobId: `quickbooks-webhook-ingress:${payload.requestId}`, + 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/blocks/blocks/quickbooks.ts b/apps/sim/blocks/blocks/quickbooks.ts index 9f60068370c..ab5883ca0a1 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,70 @@ export const QuickBooksBlock: BlockConfig = { }, value: () => 'no', }, + ...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: { 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/copilot/tools/server/workflow/edit-workflow/validation.test.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts index 8ae05a41465..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 @@ -251,6 +251,7 @@ import { collectUnresolvedReferences, preValidateCredentialInputs, validateInputsForBlock, + validateValueForSubBlockType, validateWorkflowSelectorIds, } from './validation' @@ -264,6 +265,40 @@ 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, + required: 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) + expect( + validateValueForSubBlockType(config, [], '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..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 @@ -297,7 +297,10 @@ 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] + const isEmptyRequiredMultiSelect = + subBlockConfig.multiSelect && subBlockConfig.required && values.length === 0 + if (isEmptyRequiredMultiSelect || 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/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/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/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index ea5064ee1b3..e5b28eae763 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,154 @@ } ], "operationCount": 47, - "triggers": [], - "triggerCount": 0, + "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_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", + "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" + }, + { + "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": 29, "authType": "oauth", "oauthServiceId": "quickbooks", "category": "tools", 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 { 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..5cf95bcf85a --- /dev/null +++ b/apps/sim/lib/webhooks/providers/quickbooks.test.ts @@ -0,0 +1,98 @@ +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, quickBooksEventTypesSubBlockId } 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('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, + 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', + [quickBooksEventTypesSubBlockId('quickbooks_invoice_events')]: ['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..9980f652c71 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/quickbooks.ts @@ -0,0 +1,93 @@ +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, quickBooksEventTypesSubBlockId } = await import( + '@/triggers/quickbooks/utils' + ) + return isQuickBooksEventMatch( + triggerId, + eventType, + providerConfig[quickBooksEventTypesSubBlockId(triggerId)] + ) + }, + + 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/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/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/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/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/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 new file mode 100644 index 00000000000..6bec911cdca --- /dev/null +++ b/apps/sim/triggers/quickbooks/index.ts @@ -0,0 +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/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/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_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/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/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..9ccf6087edc --- /dev/null +++ b/apps/sim/triggers/quickbooks/quickbooks.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest' +import { QuickBooksBlock } from '@/blocks/blocks/quickbooks' +import { getTrigger } from '@/triggers' +import { + isQuickBooksEventMatch, + QUICKBOOKS_TRIGGER_DEFINITIONS, + quickBooksEventTypesSubBlockId, + 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([ + '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) + }) + + 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 === 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') + )?.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) + } + + 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}.${providerAction}.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 new file mode 100644 index 00000000000..469106ec948 --- /dev/null +++ b/apps/sim/triggers/quickbooks/utils.ts @@ -0,0 +1,399 @@ +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_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', + 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_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', + 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'], + }, + { + 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) => ({ + label: definition.label, + id: definition.id, + group: definition.group, +})) + +export function quickBooksEventTypesSubBlockId(triggerId: string): string { + return `eventTypes_${triggerId}` +} + +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) + if (!match) return null + return { entity: match[1], action: match[2] === 'void' ? 'voided' : match[2] } +} + +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 + if (definition.actions.length === 1) return true + return Array.isArray(selectedActions) && selectedActions.includes(parsed.action) +} + +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, 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.', + ...(includeActionSelection + ? [`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}`) + + return [ + { + id: 'triggerCredentials', + title: 'QuickBooks Account', + type: 'oauth-input', + serviceId: 'quickbooks', + requiredScopes: getScopesForService('quickbooks'), + mode: 'trigger', + required: true, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: quickBooksEventTypesSubBlockId(triggerId), + 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 }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: quickBooksSetupInstructions(definition.label), + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +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' }, + 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_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/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..0cf598f3b2b 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -369,6 +369,37 @@ import { pagerdutyIncidentTriggeredTrigger, 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 { resendEmailBouncedTrigger, resendEmailClickedTrigger, @@ -738,6 +769,35 @@ 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, 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