Skip to content

Commit 4ce6bc9

Browse files
fix(stripe): use latest version to fix event mismatch issues (#1336)
* fix(stripe): use latest version to fix event mismatch issues * fix enterprise handling * cleanup * update better auth version * fix overage order of ops * upgrade better auth version * fix image typing * change image type to string | undefined
1 parent ba21d27 commit 4ce6bc9

File tree

7 files changed

+143
-119
lines changed

7 files changed

+143
-119
lines changed

apps/sim/lib/auth.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const validStripeKey = env.STRIPE_SECRET_KEY
4848
let stripeClient = null
4949
if (validStripeKey) {
5050
stripeClient = new Stripe(env.STRIPE_SECRET_KEY || '', {
51-
apiVersion: '2025-02-24.acacia',
51+
apiVersion: '2025-08-27.basil',
5252
})
5353
}
5454

@@ -592,7 +592,6 @@ export const auth = betterAuth({
592592
id: uniqueId,
593593
name: 'Wealthbox User',
594594
email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@wealthbox.user`,
595-
image: null,
596595
emailVerified: false,
597596
createdAt: now,
598597
updatedAt: now,
@@ -650,7 +649,6 @@ export const auth = betterAuth({
650649
id: uniqueId,
651650
name: 'Supabase User',
652651
email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@supabase.user`,
653-
image: null,
654652
emailVerified: false,
655653
createdAt: now,
656654
updatedAt: now,
@@ -760,7 +758,7 @@ export const auth = betterAuth({
760758
id: profile.account_id,
761759
name: profile.name || profile.display_name || 'Confluence User',
762760
email: profile.email || `${profile.account_id}@atlassian.com`,
763-
image: profile.picture || null,
761+
image: profile.picture || undefined,
764762
emailVerified: true, // Assume verified since it's an Atlassian account
765763
createdAt: now,
766764
updatedAt: now,
@@ -811,7 +809,7 @@ export const auth = betterAuth({
811809
email: profile.email || `${profile.id}@discord.user`,
812810
image: profile.avatar
813811
? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`
814-
: null,
812+
: undefined,
815813
emailVerified: profile.verified || false,
816814
createdAt: now,
817815
updatedAt: now,
@@ -881,7 +879,7 @@ export const auth = betterAuth({
881879
id: profile.account_id,
882880
name: profile.name || profile.display_name || 'Jira User',
883881
email: profile.email || `${profile.account_id}@atlassian.com`,
884-
image: profile.picture || null,
882+
image: profile.picture || undefined,
885883
emailVerified: true, // Assume verified since it's an Atlassian account
886884
createdAt: now,
887885
updatedAt: now,
@@ -949,7 +947,6 @@ export const auth = betterAuth({
949947
id: profile.bot?.owner?.user?.id || profile.id,
950948
name: profile.name || profile.bot?.owner?.user?.name || 'Notion User',
951949
email: profile.person?.email || `${profile.id}@notion.user`,
952-
image: null, // Notion API doesn't provide profile images
953950
emailVerified: !!profile.person?.email,
954951
createdAt: now,
955952
updatedAt: now,
@@ -1000,7 +997,7 @@ export const auth = betterAuth({
1000997
id: data.id,
1001998
name: data.name || 'Reddit User',
1002999
email: `${data.name}@reddit.user`, // Reddit doesn't provide email in identity scope
1003-
image: data.icon_img || null,
1000+
image: data.icon_img || undefined,
10041001
emailVerified: false,
10051002
createdAt: now,
10061003
updatedAt: now,
@@ -1075,7 +1072,7 @@ export const auth = betterAuth({
10751072
emailVerified: true,
10761073
createdAt: new Date(),
10771074
updatedAt: new Date(),
1078-
image: viewer.avatarUrl || null,
1075+
image: viewer.avatarUrl || undefined,
10791076
}
10801077
} catch (error) {
10811078
logger.error('Error in getUserInfo:', error)
@@ -1138,7 +1135,6 @@ export const auth = betterAuth({
11381135
id: uniqueId,
11391136
name: 'Slack Bot',
11401137
email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@slack.bot`,
1141-
image: null,
11421138
emailVerified: false,
11431139
createdAt: now,
11441140
updatedAt: now,

apps/sim/lib/billing/core/usage.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,12 @@ export async function getUserUsageData(userId: string): Promise<UsageData> {
5050
])
5151

5252
if (userStatsData.length === 0) {
53+
logger.error('User stats not found for userId', { userId })
5354
throw new Error(`User stats not found for userId: ${userId}`)
5455
}
5556

5657
const stats = userStatsData[0]
57-
const currentUsage = Number.parseFloat(
58-
stats.currentPeriodCost?.toString() ?? stats.totalCost.toString()
59-
)
58+
const currentUsage = Number.parseFloat(stats.currentPeriodCost?.toString() ?? '0')
6059

6160
// Determine usage limit based on plan type
6261
let limit: number

apps/sim/lib/billing/stripe-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const createStripeClientSingleton = () => {
3838
isInitializing = true
3939

4040
stripeClient = new Stripe(env.STRIPE_SECRET_KEY || '', {
41-
apiVersion: '2025-02-24.acacia',
41+
apiVersion: '2025-08-27.basil',
4242
})
4343

4444
logger.info('Stripe client initialized successfully')

apps/sim/lib/billing/webhooks/enterprise.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,21 @@ export async function handleManualEnterpriseSubscription(event: Stripe.Event) {
9898
throw new Error('Enterprise subscription must include valid monthlyPrice in metadata')
9999
}
100100

101+
// Get the first subscription item which contains the period information
102+
const referenceItem = stripeSubscription.items?.data?.[0]
103+
101104
const subscriptionRow = {
102105
id: crypto.randomUUID(),
103106
plan: 'enterprise',
104107
referenceId,
105108
stripeCustomerId,
106109
stripeSubscriptionId: stripeSubscription.id,
107110
status: stripeSubscription.status || null,
108-
periodStart: stripeSubscription.current_period_start
109-
? new Date(stripeSubscription.current_period_start * 1000)
111+
periodStart: referenceItem?.current_period_start
112+
? new Date(referenceItem.current_period_start * 1000)
110113
: null,
111-
periodEnd: stripeSubscription.current_period_end
112-
? new Date(stripeSubscription.current_period_end * 1000)
114+
periodEnd: referenceItem?.current_period_end
115+
? new Date(referenceItem.current_period_end * 1000)
113116
: null,
114117
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end ?? null,
115118
seats,

apps/sim/lib/billing/webhooks/invoices.ts

Lines changed: 106 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,14 @@ export async function handleInvoicePaymentSucceeded(event: Stripe.Event) {
5353
try {
5454
const invoice = event.data.object as Stripe.Invoice
5555

56-
if (!invoice.subscription) return
57-
const stripeSubscriptionId = String(invoice.subscription)
56+
const subscription = invoice.parent?.subscription_details?.subscription
57+
const stripeSubscriptionId = typeof subscription === 'string' ? subscription : subscription?.id
58+
if (!stripeSubscriptionId) {
59+
logger.info('No subscription found on invoice; skipping payment succeeded handler', {
60+
invoiceId: invoice.id,
61+
})
62+
return
63+
}
5864
const records = await db
5965
.select()
6066
.from(subscriptionTable)
@@ -156,7 +162,9 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
156162
attemptCount,
157163
})
158164
// Block all users under this customer (org members or individual)
159-
const stripeSubscriptionId = String(invoice.subscription || '')
165+
// Overage invoices are manual invoices without parent.subscription_details
166+
// We store the subscription ID in metadata when creating them
167+
const stripeSubscriptionId = invoice.metadata?.subscriptionId as string | undefined
160168
if (stripeSubscriptionId) {
161169
const records = await db
162170
.select()
@@ -203,10 +211,16 @@ export async function handleInvoiceFinalized(event: Stripe.Event) {
203211
try {
204212
const invoice = event.data.object as Stripe.Invoice
205213
// Only run for subscription renewal invoices (cycle boundary)
206-
if (!invoice.subscription) return
214+
const subscription = invoice.parent?.subscription_details?.subscription
215+
const stripeSubscriptionId = typeof subscription === 'string' ? subscription : subscription?.id
216+
if (!stripeSubscriptionId) {
217+
logger.info('No subscription found on invoice; skipping finalized handler', {
218+
invoiceId: invoice.id,
219+
})
220+
return
221+
}
207222
if (invoice.billing_reason && invoice.billing_reason !== 'subscription_cycle') return
208223

209-
const stripeSubscriptionId = String(invoice.subscription)
210224
const records = await db
211225
.select()
212226
.from(subscriptionTable)
@@ -216,11 +230,9 @@ export async function handleInvoiceFinalized(event: Stripe.Event) {
216230
if (records.length === 0) return
217231
const sub = records[0]
218232

219-
// Always reset usage at cycle end for all plans
220-
await resetUsageForSubscription({ plan: sub.plan, referenceId: sub.referenceId })
221-
222-
// Enterprise plans have no overages - skip overage invoice creation
233+
// Enterprise plans have no overages - reset usage and exit
223234
if (sub.plan === 'enterprise') {
235+
await resetUsageForSubscription({ plan: sub.plan, referenceId: sub.referenceId })
224236
return
225237
}
226238

@@ -229,7 +241,7 @@ export async function handleInvoiceFinalized(event: Stripe.Event) {
229241
invoice.lines?.data?.[0]?.period?.end || invoice.period_end || Math.floor(Date.now() / 1000)
230242
const billingPeriod = new Date(periodEnd * 1000).toISOString().slice(0, 7)
231243

232-
// Compute overage (only for team and pro plans)
244+
// Compute overage (only for team and pro plans), before resetting usage
233245
let totalOverage = 0
234246
if (sub.plan === 'team') {
235247
const members = await db
@@ -254,88 +266,101 @@ export async function handleInvoiceFinalized(event: Stripe.Event) {
254266
totalOverage = Math.max(0, usage.currentUsage - basePrice)
255267
}
256268

257-
if (totalOverage <= 0) return
258-
259-
const customerId = String(invoice.customer)
260-
const cents = Math.round(totalOverage * 100)
261-
const itemIdemKey = `overage-item:${customerId}:${stripeSubscriptionId}:${billingPeriod}`
262-
const invoiceIdemKey = `overage-invoice:${customerId}:${stripeSubscriptionId}:${billingPeriod}`
269+
if (totalOverage > 0) {
270+
const customerId = String(invoice.customer)
271+
const cents = Math.round(totalOverage * 100)
272+
const itemIdemKey = `overage-item:${customerId}:${stripeSubscriptionId}:${billingPeriod}`
273+
const invoiceIdemKey = `overage-invoice:${customerId}:${stripeSubscriptionId}:${billingPeriod}`
263274

264-
// Inherit billing settings from the Stripe subscription/customer for autopay
265-
const getPaymentMethodId = (
266-
pm: string | Stripe.PaymentMethod | null | undefined
267-
): string | undefined => (typeof pm === 'string' ? pm : pm?.id)
275+
// Inherit billing settings from the Stripe subscription/customer for autopay
276+
const getPaymentMethodId = (
277+
pm: string | Stripe.PaymentMethod | null | undefined
278+
): string | undefined => (typeof pm === 'string' ? pm : pm?.id)
268279

269-
let collectionMethod: 'charge_automatically' | 'send_invoice' = 'charge_automatically'
270-
let defaultPaymentMethod: string | undefined
271-
try {
272-
const stripeSub = await stripe.subscriptions.retrieve(stripeSubscriptionId)
273-
if (stripeSub.collection_method === 'send_invoice') {
274-
collectionMethod = 'send_invoice'
275-
}
276-
const subDpm = getPaymentMethodId(stripeSub.default_payment_method)
277-
if (subDpm) {
278-
defaultPaymentMethod = subDpm
279-
} else if (collectionMethod === 'charge_automatically') {
280-
const custObj = await stripe.customers.retrieve(customerId)
281-
if (custObj && !('deleted' in custObj)) {
282-
const cust = custObj as Stripe.Customer
283-
const custDpm = getPaymentMethodId(cust.invoice_settings?.default_payment_method)
284-
if (custDpm) defaultPaymentMethod = custDpm
280+
let collectionMethod: 'charge_automatically' | 'send_invoice' = 'charge_automatically'
281+
let defaultPaymentMethod: string | undefined
282+
try {
283+
const stripeSub = await stripe.subscriptions.retrieve(stripeSubscriptionId)
284+
if (stripeSub.collection_method === 'send_invoice') {
285+
collectionMethod = 'send_invoice'
286+
}
287+
const subDpm = getPaymentMethodId(stripeSub.default_payment_method)
288+
if (subDpm) {
289+
defaultPaymentMethod = subDpm
290+
} else if (collectionMethod === 'charge_automatically') {
291+
const custObj = await stripe.customers.retrieve(customerId)
292+
if (custObj && !('deleted' in custObj)) {
293+
const cust = custObj as Stripe.Customer
294+
const custDpm = getPaymentMethodId(cust.invoice_settings?.default_payment_method)
295+
if (custDpm) defaultPaymentMethod = custDpm
296+
}
285297
}
298+
} catch (e) {
299+
logger.error('Failed to retrieve subscription or customer', { error: e })
286300
}
287-
} catch (e) {
288-
logger.error('Failed to retrieve subscription or customer', { error: e })
289-
}
290301

291-
// Create a draft invoice first so we can attach the item directly
292-
const overageInvoice = await stripe.invoices.create(
293-
{
294-
customer: customerId,
295-
collection_method: collectionMethod,
296-
auto_advance: false,
297-
...(defaultPaymentMethod ? { default_payment_method: defaultPaymentMethod } : {}),
298-
metadata: {
299-
type: 'overage_billing',
300-
billingPeriod,
301-
subscriptionId: stripeSubscriptionId,
302+
// Create a draft invoice first so we can attach the item directly
303+
const overageInvoice = await stripe.invoices.create(
304+
{
305+
customer: customerId,
306+
collection_method: collectionMethod,
307+
auto_advance: false,
308+
...(defaultPaymentMethod ? { default_payment_method: defaultPaymentMethod } : {}),
309+
metadata: {
310+
type: 'overage_billing',
311+
billingPeriod,
312+
subscriptionId: stripeSubscriptionId,
313+
},
302314
},
303-
},
304-
{ idempotencyKey: invoiceIdemKey }
305-
)
315+
{ idempotencyKey: invoiceIdemKey }
316+
)
306317

307-
// Attach the item to this invoice
308-
await stripe.invoiceItems.create(
309-
{
310-
customer: customerId,
311-
invoice: overageInvoice.id,
312-
amount: cents,
313-
currency: 'usd',
314-
description: `Usage Based Overage – ${billingPeriod}`,
315-
metadata: {
316-
type: 'overage_billing',
317-
billingPeriod,
318-
subscriptionId: stripeSubscriptionId,
318+
// Attach the item to this invoice
319+
await stripe.invoiceItems.create(
320+
{
321+
customer: customerId,
322+
invoice: overageInvoice.id,
323+
amount: cents,
324+
currency: 'usd',
325+
description: `Usage Based Overage – ${billingPeriod}`,
326+
metadata: {
327+
type: 'overage_billing',
328+
billingPeriod,
329+
subscriptionId: stripeSubscriptionId,
330+
},
319331
},
320-
},
321-
{ idempotencyKey: itemIdemKey }
322-
)
332+
{ idempotencyKey: itemIdemKey }
333+
)
323334

324-
// Finalize to trigger autopay (if charge_automatically and a PM is present)
325-
const finalized = await stripe.invoices.finalizeInvoice(overageInvoice.id)
326-
// Some manual invoices may remain open after finalize; ensure we pay immediately when possible
327-
if (collectionMethod === 'charge_automatically' && finalized.status === 'open') {
328-
try {
329-
await stripe.invoices.pay(finalized.id, {
330-
payment_method: defaultPaymentMethod,
331-
})
332-
} catch (payError) {
333-
logger.error('Failed to auto-pay overage invoice', {
334-
error: payError,
335-
invoiceId: finalized.id,
336-
})
335+
// Finalize to trigger autopay (if charge_automatically and a PM is present)
336+
const draftId = overageInvoice.id
337+
if (typeof draftId !== 'string' || draftId.length === 0) {
338+
logger.error('Stripe created overage invoice without id; aborting finalize')
339+
} else {
340+
const finalized = await stripe.invoices.finalizeInvoice(draftId)
341+
// Some manual invoices may remain open after finalize; ensure we pay immediately when possible
342+
if (collectionMethod === 'charge_automatically' && finalized.status === 'open') {
343+
try {
344+
const payId = finalized.id
345+
if (typeof payId !== 'string' || payId.length === 0) {
346+
logger.error('Finalized invoice missing id')
347+
throw new Error('Finalized invoice missing id')
348+
}
349+
await stripe.invoices.pay(payId, {
350+
payment_method: defaultPaymentMethod,
351+
})
352+
} catch (payError) {
353+
logger.error('Failed to auto-pay overage invoice', {
354+
error: payError,
355+
invoiceId: finalized.id,
356+
})
357+
}
358+
}
337359
}
338360
}
361+
362+
// Finally, reset usage for this subscription after overage handling
363+
await resetUsageForSubscription({ plan: sub.plan, referenceId: sub.referenceId })
339364
} catch (error) {
340365
logger.error('Failed to handle invoice finalized', { error })
341366
throw error

0 commit comments

Comments
 (0)