Skip to content

Commit 593d9db

Browse files
Bill Leoutsakoswaleedlatif1
authored andcommitted
fix(quickbooks): align webhook event selection
1 parent de43b7f commit 593d9db

8 files changed

Lines changed: 100 additions & 9 deletions

File tree

‎apps/sim/lib/copilot/vfs/serializers.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,20 @@ describe('entitlement-projected block schemas', () => {
245245
})
246246

247247
describe('hosted-key VFS metadata', () => {
248+
it('preserves multi-select dropdown behavior in block schemas', () => {
249+
const block = {
250+
type: 'events',
251+
name: 'Events',
252+
subBlocks: [{ id: 'eventTypes', type: 'dropdown', multiSelect: true }],
253+
tools: { access: [] },
254+
inputs: {},
255+
outputs: {},
256+
} as unknown as BlockConfig
257+
258+
const schema = JSON.parse(serializeBlockSchema(block))
259+
expect(schema.subBlocks[0].multiSelect).toBe(true)
260+
})
261+
248262
it('indexes hosted and conditional-hosted operations for every configured service', () => {
249263
const metadata = JSON.parse(
250264
serializeApiKeyIntegrations(

‎apps/sim/lib/copilot/vfs/serializers.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,7 @@ function serializeSubBlock(sb: SubBlockConfig): Record<string, unknown> {
598598
if (sb.required === true) result.required = true
599599
if (sb.defaultValue !== undefined) result.defaultValue = sb.defaultValue
600600
if (sb.mode) result.mode = sb.mode
601+
if (sb.multiSelect) result.multiSelect = true
601602
if (sb.canonicalParamId) result.canonicalParamId = sb.canonicalParamId
602603
if (sb.condition && typeof sb.condition !== 'function') result.condition = sb.condition
603604
// Copied, not aliased: these are the registry's own arrays, shared by every

‎apps/sim/lib/webhooks/providers/quickbooks.test.ts‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import crypto from 'node:crypto'
22
import { NextRequest } from 'next/server'
33
import { describe, expect, it } from 'vitest'
44
import { quickBooksHandler, verifyQuickBooksSignature } from '@/lib/webhooks/providers/quickbooks'
5-
import { isQuickBooksEventMatch } from '@/triggers/quickbooks/utils'
5+
import { isQuickBooksEventMatch, quickBooksEventTypesSubBlockId } from '@/triggers/quickbooks/utils'
66

77
const event = {
88
specversion: '1.0',
@@ -33,6 +33,28 @@ describe('QuickBooks webhook provider', () => {
3333
expect(isQuickBooksEventMatch('quickbooks_bill_events', event.type, ['updated'])).toBe(false)
3434
})
3535

36+
it('normalizes Intuit void events to the configured voided action', async () => {
37+
for (const entity of ['invoice', 'payment']) {
38+
const voidEvent = { ...event, type: `qbo.${entity}.void.v1` }
39+
expect(
40+
isQuickBooksEventMatch(`quickbooks_${entity}_events`, voidEvent.type, ['voided'])
41+
).toBe(true)
42+
43+
const result = await quickBooksHandler.formatInput!({
44+
body: voidEvent,
45+
webhook: {},
46+
workflow: { id: 'workflow-1', userId: 'user-1' },
47+
headers: {},
48+
requestId: `request-${entity}`,
49+
})
50+
expect(result.input).toMatchObject({
51+
eventType: `qbo.${entity}.void.v1`,
52+
entityType: entity,
53+
action: 'voided',
54+
})
55+
}
56+
})
57+
3658
it('formats only the common verified event fields', async () => {
3759
const result = await quickBooksHandler.formatInput!({
3860
body: event,
@@ -66,7 +88,7 @@ describe('QuickBooks webhook provider', () => {
6688
requestId: 'request-5',
6789
providerConfig: {
6890
triggerId: 'quickbooks_invoice_events',
69-
eventTypes: ['updated'],
91+
[quickBooksEventTypesSubBlockId('quickbooks_invoice_events')]: ['updated'],
7092
},
7193
webhook: {},
7294
workflow: {},

‎apps/sim/lib/webhooks/providers/quickbooks.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,14 @@ export const quickBooksHandler: WebhookProviderHandler = {
5353
const event = asRecord(body)
5454
const triggerId = typeof providerConfig.triggerId === 'string' ? providerConfig.triggerId : ''
5555
const eventType = typeof event?.type === 'string' ? event.type : ''
56-
const { isQuickBooksEventMatch } = await import('@/triggers/quickbooks/utils')
57-
return isQuickBooksEventMatch(triggerId, eventType, providerConfig.eventTypes)
56+
const { isQuickBooksEventMatch, quickBooksEventTypesSubBlockId } = await import(
57+
'@/triggers/quickbooks/utils'
58+
)
59+
return isQuickBooksEventMatch(
60+
triggerId,
61+
eventType,
62+
providerConfig[quickBooksEventTypesSubBlockId(triggerId)]
63+
)
5864
},
5965

6066
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {

‎apps/sim/lib/workflows/editing/validation.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ import {
286286
collectUnresolvedReferences,
287287
preValidateCredentialInputs,
288288
validateInputsForBlock,
289+
validateValueForSubBlockType,
289290
validateWorkflowSelectorIds,
290291
} from './validation'
291292

@@ -303,6 +304,36 @@ describe('validateInputsForBlock', () => {
303304
mockValidateSelectorIds.mockResolvedValue({ valid: [], invalid: [] })
304305
})
305306

307+
it('validates every value in a multi-select dropdown array', () => {
308+
const config = {
309+
id: 'eventTypes',
310+
type: 'dropdown' as const,
311+
multiSelect: true,
312+
options: [
313+
{ id: 'created', label: 'Created' },
314+
{ id: 'updated', label: 'Updated' },
315+
],
316+
}
317+
expect(
318+
validateValueForSubBlockType(
319+
config,
320+
['created', 'updated'],
321+
'eventTypes',
322+
'quickbooks',
323+
'quickbooks-1'
324+
).valid
325+
).toBe(true)
326+
expect(
327+
validateValueForSubBlockType(
328+
config,
329+
['created', 'deleted'],
330+
'eventTypes',
331+
'quickbooks',
332+
'quickbooks-1'
333+
).valid
334+
).toBe(false)
335+
})
336+
306337
it('accepts condition-input arrays with arbitrary item ids', () => {
307338
const result = validateInputsForBlock(
308339
'condition',

‎apps/sim/lib/workflows/editing/validation.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,8 @@ export function validateValueForSubBlockType(
390390
: subBlockConfig.options
391391
if (options && Array.isArray(options)) {
392392
const validIds = options.map((opt) => opt.id)
393-
if (!validIds.includes(value)) {
393+
const values = subBlockConfig.multiSelect && Array.isArray(value) ? value : [value]
394+
if (values.some((item) => !validIds.includes(item))) {
394395
return {
395396
valid: false,
396397
error: {

‎apps/sim/triggers/quickbooks/quickbooks.test.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getTrigger } from '@/triggers'
44
import {
55
isQuickBooksEventMatch,
66
QUICKBOOKS_TRIGGER_DEFINITIONS,
7+
quickBooksEventTypesSubBlockId,
78
quickBooksTriggerOptions,
89
} from '@/triggers/quickbooks/utils'
910

@@ -32,11 +33,14 @@ describe('QuickBooks triggers', () => {
3233
).toHaveLength(1)
3334
for (const definition of QUICKBOOKS_TRIGGER_DEFINITIONS) {
3435
const trigger = getTrigger(definition.id)
35-
const eventTypes = trigger.subBlocks.find((subBlock) => subBlock.id === 'eventTypes')
36+
const eventTypes = trigger.subBlocks.find(
37+
(subBlock) => subBlock.id === quickBooksEventTypesSubBlockId(definition.id)
38+
)
3639
if (definition.id === 'quickbooks_preferences_updated') {
3740
expect(eventTypes).toBeUndefined()
3841
} else {
3942
expect(eventTypes?.options?.map((option) => option.id)).toEqual(definition.actions)
43+
expect(eventTypes?.multiSelect).toBe(true)
4044
}
4145
const instructions = trigger.subBlocks.find((subBlock) =>
4246
subBlock.id.startsWith('triggerInstructions')
@@ -47,15 +51,22 @@ describe('QuickBooks triggers', () => {
4751
}
4852
expect(trigger.outputs).toEqual(getTrigger('quickbooks_invoice_events').outputs)
4953
}
54+
55+
const eventTypeIds = QuickBooksBlock.subBlocks
56+
.map((subBlock) => subBlock.id)
57+
.filter((id) => id.startsWith('eventTypes_quickbooks_'))
58+
expect(eventTypeIds).toHaveLength(28)
59+
expect(new Set(eventTypeIds).size).toBe(eventTypeIds.length)
5060
})
5161

5262
it('matches all 101 supported combinations and rejects unsupported actions', () => {
5363
for (const definition of QUICKBOOKS_TRIGGER_DEFINITIONS) {
5464
for (const action of definition.actions) {
65+
const providerAction = action === 'voided' ? 'void' : action
5566
expect(
5667
isQuickBooksEventMatch(
5768
definition.id,
58-
`qbo.${definition.entity}.${action}.v1`,
69+
`qbo.${definition.entity}.${providerAction}.v1`,
5970
definition.actions
6071
)
6172
).toBe(true)

‎apps/sim/triggers/quickbooks/utils.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ export const quickBooksTriggerOptions = QUICKBOOKS_TRIGGER_DEFINITIONS.map((defi
260260
group: definition.group,
261261
}))
262262

263+
export function quickBooksEventTypesSubBlockId(triggerId: string): string {
264+
return `eventTypes_${triggerId}`
265+
}
266+
263267
export const QUICKBOOKS_WEBHOOK_HEADERS = {
264268
'Content-Type': 'application/json',
265269
'intuit-signature': '<base64-hmac-sha256>',
@@ -276,7 +280,8 @@ export function parseQuickBooksWebhookType(type: string): {
276280
entity: string
277281
} | null {
278282
const match = /^qbo\.([a-z]+)\.([a-z]+)\.v1$/.exec(type)
279-
return match ? { entity: match[1], action: match[2] } : null
283+
if (!match) return null
284+
return { entity: match[1], action: match[2] === 'void' ? 'voided' : match[2] }
280285
}
281286

282287
export function isQuickBooksEventMatch(
@@ -326,7 +331,7 @@ export function buildQuickBooksTriggerSubBlocks(triggerId: string): SubBlockConf
326331
condition: { field: 'selectedTriggerId', value: triggerId },
327332
},
328333
{
329-
id: 'eventTypes',
334+
id: quickBooksEventTypesSubBlockId(triggerId),
330335
title: 'Event Types',
331336
type: 'dropdown',
332337
multiSelect: true,

0 commit comments

Comments
 (0)