Skip to content

Commit 937ec78

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(quickbooks): continue webhook batches after target failures
1 parent b1be51d commit 937ec78

2 files changed

Lines changed: 41 additions & 11 deletions

File tree

apps/sim/background/quickbooks-webhook-ingress.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe('QuickBooks webhook ingress job', () => {
5959
return { outcome: 'queued' }
6060
})
6161
await expect(executeQuickBooksWebhookIngress(payload)).resolves.toEqual({
62+
failed: 0,
6263
ignored: 0,
6364
processed: 2,
6465
targetCount: 2,
@@ -98,4 +99,23 @@ describe('QuickBooks webhook ingress job', () => {
9899
expect.objectContaining({ jobId: 'quickbooks-webhook-ingress:request-1:1:root' })
99100
)
100101
})
102+
103+
it('durably continues the batch before retrying a failed target page', async () => {
104+
mockFindPage.mockResolvedValue({
105+
hasMore: false,
106+
nextCursor: null,
107+
targets: [{ webhook: { id: 'w1' }, workflow: { id: 'wf1' } }],
108+
})
109+
mockDispatch.mockResolvedValue({ outcome: 'failed' })
110+
111+
await enqueueQuickBooksWebhookIngress(payload)
112+
const options = mockEnqueue.mock.calls[0][2] as { runner: () => Promise<void> }
113+
await expect(options.runner()).rejects.toThrow('Failed to dispatch 1 of 1 QuickBooks targets')
114+
expect(mockEnqueue).toHaveBeenNthCalledWith(
115+
2,
116+
'quickbooks-webhook-ingress',
117+
expect.objectContaining({ eventIndex: 1, afterWebhookId: undefined }),
118+
expect.objectContaining({ jobId: 'quickbooks-webhook-ingress:request-1:1:root' })
119+
)
120+
})
101121
})

apps/sim/background/quickbooks-webhook-ingress.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface QuickBooksWebhookIngressPayload {
2121
}
2222

2323
export interface QuickBooksWebhookIngressResult {
24+
failed: number
2425
ignored: number
2526
nextCursor?: string
2627
processed: number
@@ -33,7 +34,7 @@ export async function executeQuickBooksWebhookIngress(
3334
): Promise<QuickBooksWebhookIngressResult> {
3435
const eventIndex = payload.eventIndex ?? 0
3536
const event = payload.events[eventIndex]
36-
if (!event) return { ignored: 0, processed: 0, targetCount: 0 }
37+
if (!event) return { failed: 0, ignored: 0, processed: 0, targetCount: 0 }
3738

3839
const request = new NextRequest('http://internal/api/webhooks/quickbooks', {
3940
method: 'POST',
@@ -65,10 +66,6 @@ export async function executeQuickBooksWebhookIngress(
6566
else failed += 1
6667
}
6768

68-
if (failed > 0) {
69-
throw new Error(`Failed to dispatch ${failed} of ${page.targets.length} QuickBooks targets`)
70-
}
71-
7269
logger.info(`[${payload.requestId}] QuickBooks webhook page completed`, {
7370
eventId: event.id,
7471
eventIndex,
@@ -77,23 +74,22 @@ export async function executeQuickBooksWebhookIngress(
7774
targetCount: page.targets.length,
7875
})
7976
return {
77+
failed,
8078
ignored,
8179
processed,
8280
targetCount: page.targets.length,
8381
...(nextCursor ? { nextCursor } : {}),
8482
}
8583
}
8684

87-
async function runQuickBooksWebhookIngressJob(
88-
payload: QuickBooksWebhookIngressPayload
85+
async function enqueueQuickBooksWebhookContinuation(
86+
payload: QuickBooksWebhookIngressPayload,
87+
result: QuickBooksWebhookIngressResult
8988
): Promise<void> {
9089
const eventIndex = payload.eventIndex ?? 0
91-
const result = await executeQuickBooksWebhookIngress(payload)
9290
if (result.nextCursor) {
9391
await enqueueQuickBooksWebhookIngress({ ...payload, afterWebhookId: result.nextCursor })
94-
return
95-
}
96-
if (eventIndex + 1 < payload.events.length) {
92+
} else if (eventIndex + 1 < payload.events.length) {
9793
await enqueueQuickBooksWebhookIngress({
9894
...payload,
9995
eventIndex: eventIndex + 1,
@@ -102,6 +98,20 @@ async function runQuickBooksWebhookIngressJob(
10298
}
10399
}
104100

101+
async function runQuickBooksWebhookIngressJob(
102+
payload: QuickBooksWebhookIngressPayload
103+
): Promise<void> {
104+
const result = await executeQuickBooksWebhookIngress(payload)
105+
// The continuation has a deterministic job id, so retries cannot duplicate it. Enqueue it
106+
// before retrying this page to avoid stranding later events in an already-acknowledged batch.
107+
await enqueueQuickBooksWebhookContinuation(payload, result)
108+
if (result.failed > 0) {
109+
throw new Error(
110+
`Failed to dispatch ${result.failed} of ${result.targetCount} QuickBooks targets`
111+
)
112+
}
113+
}
114+
105115
export async function enqueueQuickBooksWebhookIngress(
106116
payload: QuickBooksWebhookIngressPayload
107117
): Promise<string> {

0 commit comments

Comments
 (0)