Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions packages/sync-engine/src/stripeSyncWebhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +195 to +204
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pagination loop assumes page.data is non-empty when page.has_more is true. If Stripe ever returns an empty page with has_more: true (or a filtering edge case yields data.length === 0), page.data[page.data.length - 1].id will throw and webhook processing will fail. Add a guard before computing lastId (e.g., break/throw with a clear error when page.data.length === 0) and only set starting_after when a last item exists (similar to the existing manual pagination pattern in tests).

Copilot uses AI. Check for mistakes.
entitlementItems.push(...(page.data as EntitlementItem[]))
}
fetched = true
}
Comment on lines +187 to +208
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces a new code path that calls the Stripe API and manually paginates entitlements when summary.entitlements.has_more is true, but there are no unit tests covering it. Add tests to verify: (1) activeEntitlements.list is called when has_more: true, (2) multiple pages are fetched and all entitlements are upserted, and (3) has_more: false does not trigger API calls (uses webhook body).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback


const activeEntitlements = entitlementItems.map((entitlement) => ({
id: entitlement.id,
object: entitlement.object,
feature:
Expand All @@ -197,7 +223,7 @@ export class StripeSyncWebhook {
activeEntitlements,
accountId,
false,
this.getSyncTimestamp(event, false)
this.getSyncTimestamp(event, fetched)
)
}
}
Expand Down
Loading