-
Notifications
You must be signed in to change notification settings - Fork 15
fix(webhook): paginate entitlements when has_more=true in summary event #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -165,20 +165,46 @@ export class StripeSyncWebhook { | |
| } | ||
|
|
||
| async handleEntitlementSummaryEvent(event: Stripe.Event, accountId: string): Promise<void> { | ||
| type EntitlementItem = { | ||
| id: string | ||
| object: string | ||
| feature: string | { id: string } | ||
| livemode: boolean | ||
| lookup_key: string | ||
| } | ||
| const summary = event.data.object as { | ||
| customer: string | ||
| entitlements: { | ||
| data: Array<{ | ||
| id: string | ||
| object: string | ||
| feature: string | { id: string } | ||
| livemode: boolean | ||
| lookup_key: string | ||
| }> | ||
| data: EntitlementItem[] | ||
| has_more: boolean | ||
| } | ||
| } | ||
| const customerId = summary.customer | ||
| const activeEntitlements = summary.entitlements.data.map((entitlement) => ({ | ||
|
|
||
| let entitlementItems: EntitlementItem[] = summary.entitlements.data | ||
| let fetched = false | ||
|
|
||
| if (summary.entitlements.has_more) { | ||
| // Webhook body is truncated — page through all active entitlements for this customer | ||
| entitlementItems = [] | ||
| let page = await this.deps.stripe.entitlements.activeEntitlements.list({ | ||
| customer: customerId, | ||
| limit: 100, | ||
| } as Stripe.Entitlements.ActiveEntitlementListParams) | ||
| entitlementItems.push(...(page.data as EntitlementItem[])) | ||
| while (page.has_more) { | ||
| const lastId = page.data[page.data.length - 1].id | ||
| page = await this.deps.stripe.entitlements.activeEntitlements.list({ | ||
| customer: customerId, | ||
| limit: 100, | ||
| starting_after: lastId, | ||
| } as Stripe.Entitlements.ActiveEntitlementListParams) | ||
| entitlementItems.push(...(page.data as EntitlementItem[])) | ||
| } | ||
| fetched = true | ||
| } | ||
|
Comment on lines
+187
to
+208
|
||
|
|
||
| const activeEntitlements = entitlementItems.map((entitlement) => ({ | ||
| id: entitlement.id, | ||
| object: entitlement.object, | ||
| feature: | ||
|
|
@@ -197,7 +223,7 @@ export class StripeSyncWebhook { | |
| activeEntitlements, | ||
| accountId, | ||
| false, | ||
| this.getSyncTimestamp(event, false) | ||
| this.getSyncTimestamp(event, fetched) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pagination loop assumes
page.datais non-empty whenpage.has_moreis true. If Stripe ever returns an empty page withhas_more: true(or a filtering edge case yieldsdata.length === 0),page.data[page.data.length - 1].idwill throw and webhook processing will fail. Add a guard before computinglastId(e.g., break/throw with a clear error whenpage.data.length === 0) and only setstarting_afterwhen a last item exists (similar to the existing manual pagination pattern in tests).