@@ -12,8 +12,6 @@ export const QUICKBOOKS_WEBHOOK_INGRESS_CONCURRENCY_LIMIT = 50
1212export const QUICKBOOKS_WEBHOOK_INGRESS_MAX_ATTEMPTS = 3
1313
1414export interface QuickBooksWebhookIngressPayload {
15- afterWebhookId ?: string
16- eventIndex ?: number
1715 events : QuickBooksWebhookEvent [ ]
1816 headers : { 'content-type' : string }
1917 requestId : string
@@ -23,102 +21,108 @@ export interface QuickBooksWebhookIngressPayload {
2321export interface QuickBooksWebhookIngressResult {
2422 failed : number
2523 ignored : number
26- nextCursor ?: string
2724 processed : number
2825 targetCount : number
2926}
3027
31- /** Process one event against one bounded target page. */
28+ /** Process the bounded delivery sequentially, retaining at most one target page at a time . */
3229export async function executeQuickBooksWebhookIngress (
3330 payload : QuickBooksWebhookIngressPayload
3431) : Promise < QuickBooksWebhookIngressResult > {
35- const eventIndex = payload . eventIndex ?? 0
36- const event = payload . events [ eventIndex ]
37- if ( ! event ) return { failed : 0 , ignored : 0 , processed : 0 , targetCount : 0 }
38-
39- const request = new NextRequest ( 'http://internal/api/webhooks/quickbooks' , {
40- method : 'POST' ,
41- headers : payload . headers ,
42- body : JSON . stringify ( event ) ,
43- } )
44- const page = await findQuickBooksWebhookTargetPage (
45- event . intuitaccountid ,
46- payload . requestId ,
47- payload . afterWebhookId
48- )
49- const nextCursor = page . hasMore ? page . nextCursor : null
50- if ( page . hasMore && ( ! nextCursor || nextCursor === payload . afterWebhookId ) ) {
51- throw new Error ( 'QuickBooks webhook target pagination did not advance' )
52- }
53-
5432 let ignored = 0
5533 let processed = 0
5634 let failed = 0
57- for ( const { webhook, workflow } of page . targets ) {
58- const result = await dispatchResolvedWebhookTarget ( webhook , workflow , event , request , {
59- requestId : payload . requestId ,
60- path : webhook . path ?? undefined ,
61- receivedAt : payload . receivedAt ,
62- triggerTimestampMs : Date . parse ( event . time ) ,
35+ let targetCount = 0
36+
37+ for ( const [ eventIndex , event ] of payload . events . entries ( ) ) {
38+ const request = new NextRequest ( 'http://internal/api/webhooks/quickbooks' , {
39+ method : 'POST' ,
40+ headers : payload . headers ,
41+ body : JSON . stringify ( event ) ,
6342 } )
64- if ( result . outcome === 'queued' ) processed += 1
65- else if ( result . outcome === 'ignored' ) ignored += 1
66- else failed += 1
43+ let afterWebhookId : string | undefined
44+
45+ while ( true ) {
46+ try {
47+ const page = await findQuickBooksWebhookTargetPage (
48+ event . intuitaccountid ,
49+ payload . requestId ,
50+ afterWebhookId
51+ )
52+ const nextCursor = page . hasMore ? page . nextCursor : null
53+ if ( page . hasMore && ( ! nextCursor || nextCursor === afterWebhookId ) ) {
54+ throw new Error ( 'QuickBooks webhook target pagination did not advance' )
55+ }
56+
57+ targetCount += page . targets . length
58+ for ( const { webhook, workflow } of page . targets ) {
59+ try {
60+ const result = await dispatchResolvedWebhookTarget ( webhook , workflow , event , request , {
61+ requestId : payload . requestId ,
62+ path : webhook . path ?? undefined ,
63+ receivedAt : payload . receivedAt ,
64+ triggerTimestampMs : Date . parse ( event . time ) ,
65+ } )
66+ if ( result . outcome === 'queued' ) processed += 1
67+ else if ( result . outcome === 'ignored' ) ignored += 1
68+ else failed += 1
69+ } catch ( error ) {
70+ failed += 1
71+ logger . error ( `[${ payload . requestId } ] QuickBooks webhook target dispatch failed` , {
72+ error,
73+ eventId : event . id ,
74+ eventIndex,
75+ webhookId : webhook . id ,
76+ } )
77+ }
78+ }
79+
80+ logger . info ( `[${ payload . requestId } ] QuickBooks webhook page completed` , {
81+ eventId : event . id ,
82+ eventIndex,
83+ ignored,
84+ processed,
85+ targetCount : page . targets . length ,
86+ } )
87+ if ( ! nextCursor ) break
88+ afterWebhookId = nextCursor
89+ } catch ( error ) {
90+ failed += 1
91+ logger . error ( `[${ payload . requestId } ] QuickBooks webhook event page failed` , {
92+ error,
93+ eventId : event . id ,
94+ eventIndex,
95+ } )
96+ break
97+ }
98+ }
6799 }
68100
69- logger . info ( `[${ payload . requestId } ] QuickBooks webhook page completed` , {
70- eventId : event . id ,
71- eventIndex,
72- ignored,
73- processed,
74- targetCount : page . targets . length ,
75- } )
76- return {
101+ logger . info ( `[${ payload . requestId } ] QuickBooks webhook delivery completed` , {
102+ eventCount : payload . events . length ,
77103 failed,
78104 ignored,
79105 processed,
80- targetCount : page . targets . length ,
81- ...( nextCursor ? { nextCursor } : { } ) ,
82- }
83- }
84-
85- async function enqueueQuickBooksWebhookContinuation (
86- payload : QuickBooksWebhookIngressPayload ,
87- result : QuickBooksWebhookIngressResult
88- ) : Promise < void > {
89- const eventIndex = payload . eventIndex ?? 0
90- if ( result . nextCursor ) {
91- await enqueueQuickBooksWebhookIngress ( { ...payload , afterWebhookId : result . nextCursor } )
92- } else if ( eventIndex + 1 < payload . events . length ) {
93- await enqueueQuickBooksWebhookIngress ( {
94- ...payload ,
95- eventIndex : eventIndex + 1 ,
96- afterWebhookId : undefined ,
97- } )
98- }
106+ targetCount,
107+ } )
108+ return { failed, ignored, processed, targetCount }
99109}
100110
101111async function runQuickBooksWebhookIngressJob (
102112 payload : QuickBooksWebhookIngressPayload
103113) : Promise < void > {
104114 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 )
108115 if ( result . failed > 0 ) {
109- throw new Error (
110- `Failed to dispatch ${ result . failed } of ${ result . targetCount } QuickBooks targets`
111- )
116+ throw new Error ( `QuickBooks webhook delivery completed with ${ result . failed } failures` )
112117 }
113118}
114119
115120export async function enqueueQuickBooksWebhookIngress (
116121 payload : QuickBooksWebhookIngressPayload
117122) : Promise < string > {
118123 const jobQueue = await getJobQueue ( )
119- const eventIndex = payload . eventIndex ?? 0
120124 return jobQueue . enqueue ( 'quickbooks-webhook-ingress' , payload , {
121- jobId : `quickbooks-webhook-ingress:${ payload . requestId } : ${ eventIndex } : ${ payload . afterWebhookId ?? 'root' } ` ,
125+ jobId : `quickbooks-webhook-ingress:${ payload . requestId } ` ,
122126 maxAttempts : QUICKBOOKS_WEBHOOK_INGRESS_MAX_ATTEMPTS ,
123127 concurrencyKey : 'quickbooks-webhook-ingress' ,
124128 concurrencyLimit : QUICKBOOKS_WEBHOOK_INGRESS_CONCURRENCY_LIMIT ,
0 commit comments