Skip to content
Open
Changes from all commits
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
34 changes: 28 additions & 6 deletions packages/sync-engine/src/stripeSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,13 @@ export class StripeSync {
case 'invoice.payment_action_required':
case 'invoice.payment_failed':
case 'invoice.payment_succeeded':
case 'invoice.upcoming':
case 'invoice.upcoming': {
// invoice.upcoming is a preview invoice with no id — it cannot be persisted.
this.config.logger?.info(
`Received webhook ${event.id}: ${event.type} — skipping (preview invoice has no id)`
)
break
}
Comment on lines 233 to +242
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

Because this case 'invoice.upcoming' now contains executable statements and a break, every preceding invoice.* case label (invoice.created/deleted/finalized/etc.) will fall through into this block and be skipped. This changes behavior for all invoice webhook types, not just invoice.upcoming. Move the invoice.upcoming handler to its own case position before the shared invoice cases (or otherwise restructure so only invoice.upcoming hits this break).

Copilot uses AI. Check for mistakes.
case 'invoice.sent':
case 'invoice.voided':
case 'invoice.marked_uncollectible':
Expand Down Expand Up @@ -529,12 +535,28 @@ export class StripeSync {
.object as Stripe.Entitlements.ActiveEntitlementSummary
let entitlements = activeEntitlementSummary.entitlements
let refetched = false
if (this.config.revalidateObjectsViaStripeApi?.includes('entitlements')) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { lastResponse, ...rest } = await this.stripe.entitlements.activeEntitlements.list({
if (
this.config.revalidateObjectsViaStripeApi?.includes('entitlements') ||
entitlements.has_more
) {
// Fetch all pages from the API — the webhook payload is capped at 10 items
// and has_more signals that there are more entitlements than were included.
const allData: Stripe.Entitlements.ActiveEntitlement[] = []
let page = await this.stripe.entitlements.activeEntitlements.list({
customer: activeEntitlementSummary.customer,
})
entitlements = rest
limit: 100,
} as Stripe.Entitlements.ActiveEntitlementListParams)
allData.push(...page.data)
while (page.has_more) {
const lastId = page.data[page.data.length - 1].id
page = await this.stripe.entitlements.activeEntitlements.list({
customer: activeEntitlementSummary.customer,
limit: 100,
starting_after: lastId,
} as Stripe.Entitlements.ActiveEntitlementListParams)
allData.push(...page.data)
}
Comment on lines +542 to +558
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

This manual pagination duplicates existing auto-pagination patterns in this file (e.g., for await (...) in expandEntity / fillCheckoutSessionsLineItems) and is more error-prone to maintain. Consider switching to Stripe's async-iterable auto-pagination (for await (const entitlement of this.stripe.entitlements.activeEntitlements.list({ ... }))) to avoid managing starting_after/lastId yourself and keep pagination logic consistent across the codebase.

Copilot uses AI. Check for mistakes.
entitlements = { ...entitlements, data: allData, has_more: false }
refetched = true
}

Expand Down
Loading