Skip to content

Commit 4da355d

Browse files
fix(billing-blocked): block platform usage if payment fails for regular subs as well (#1541)
1 parent 10692b5 commit 4da355d

File tree

1 file changed

+59
-33
lines changed

1 file changed

+59
-33
lines changed

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

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,29 @@ export async function handleInvoicePaymentSucceeded(event: Stripe.Event) {
137137

138138
/**
139139
* Handle invoice payment failed webhook
140-
* This is triggered when a user's payment fails for a usage billing invoice
140+
* This is triggered when a user's payment fails for any invoice (subscription or overage)
141141
*/
142142
export async function handleInvoicePaymentFailed(event: Stripe.Event) {
143143
try {
144144
const invoice = event.data.object as Stripe.Invoice
145145

146-
// Check if this is an overage billing invoice
147-
if (invoice.metadata?.type !== 'overage_billing') {
148-
logger.info('Ignoring non-overage billing invoice payment failure', { invoiceId: invoice.id })
146+
const isOverageInvoice = invoice.metadata?.type === 'overage_billing'
147+
let stripeSubscriptionId: string | undefined
148+
149+
if (isOverageInvoice) {
150+
// Overage invoices store subscription ID in metadata
151+
stripeSubscriptionId = invoice.metadata?.subscriptionId as string | undefined
152+
} else {
153+
// Regular subscription invoices have it in parent.subscription_details
154+
const subscription = invoice.parent?.subscription_details?.subscription
155+
stripeSubscriptionId = typeof subscription === 'string' ? subscription : subscription?.id
156+
}
157+
158+
if (!stripeSubscriptionId) {
159+
logger.info('No subscription found on invoice; skipping payment failed handler', {
160+
invoiceId: invoice.id,
161+
isOverageInvoice,
162+
})
149163
return
150164
}
151165

@@ -154,55 +168,67 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
154168
const billingPeriod = invoice.metadata?.billingPeriod || 'unknown'
155169
const attemptCount = invoice.attempt_count || 1
156170

157-
logger.warn('Overage billing invoice payment failed', {
171+
logger.warn('Invoice payment failed', {
158172
invoiceId: invoice.id,
159173
customerId,
160174
failedAmount,
161175
billingPeriod,
162176
attemptCount,
163177
customerEmail: invoice.customer_email,
164178
hostedInvoiceUrl: invoice.hosted_invoice_url,
179+
isOverageInvoice,
180+
invoiceType: isOverageInvoice ? 'overage' : 'subscription',
165181
})
166182

167-
// Implement dunning management logic here
168-
// For example: suspend service after multiple failures, notify admins, etc.
183+
// Block users after first payment failure
169184
if (attemptCount >= 1) {
170-
logger.error('Multiple payment failures for overage billing', {
185+
logger.error('Payment failure - blocking users', {
171186
invoiceId: invoice.id,
172187
customerId,
173188
attemptCount,
189+
isOverageInvoice,
190+
stripeSubscriptionId,
174191
})
175-
// Block all users under this customer (org members or individual)
176-
// Overage invoices are manual invoices without parent.subscription_details
177-
// We store the subscription ID in metadata when creating them
178-
const stripeSubscriptionId = invoice.metadata?.subscriptionId as string | undefined
179-
if (stripeSubscriptionId) {
180-
const records = await db
181-
.select()
182-
.from(subscriptionTable)
183-
.where(eq(subscriptionTable.stripeSubscriptionId, stripeSubscriptionId))
184-
.limit(1)
185192

186-
if (records.length > 0) {
187-
const sub = records[0]
188-
if (sub.plan === 'team' || sub.plan === 'enterprise') {
189-
const members = await db
190-
.select({ userId: member.userId })
191-
.from(member)
192-
.where(eq(member.organizationId, sub.referenceId))
193-
for (const m of members) {
194-
await db
195-
.update(userStats)
196-
.set({ billingBlocked: true })
197-
.where(eq(userStats.userId, m.userId))
198-
}
199-
} else {
193+
const records = await db
194+
.select()
195+
.from(subscriptionTable)
196+
.where(eq(subscriptionTable.stripeSubscriptionId, stripeSubscriptionId))
197+
.limit(1)
198+
199+
if (records.length > 0) {
200+
const sub = records[0]
201+
if (sub.plan === 'team' || sub.plan === 'enterprise') {
202+
const members = await db
203+
.select({ userId: member.userId })
204+
.from(member)
205+
.where(eq(member.organizationId, sub.referenceId))
206+
for (const m of members) {
200207
await db
201208
.update(userStats)
202209
.set({ billingBlocked: true })
203-
.where(eq(userStats.userId, sub.referenceId))
210+
.where(eq(userStats.userId, m.userId))
204211
}
212+
logger.info('Blocked team/enterprise members due to payment failure', {
213+
organizationId: sub.referenceId,
214+
memberCount: members.length,
215+
isOverageInvoice,
216+
})
217+
} else {
218+
await db
219+
.update(userStats)
220+
.set({ billingBlocked: true })
221+
.where(eq(userStats.userId, sub.referenceId))
222+
logger.info('Blocked user due to payment failure', {
223+
userId: sub.referenceId,
224+
isOverageInvoice,
225+
})
205226
}
227+
} else {
228+
logger.warn('Subscription not found in database for failed payment', {
229+
stripeSubscriptionId,
230+
invoiceId: invoice.id,
231+
})
206232
}
207233
}
208234
} catch (error) {

0 commit comments

Comments
 (0)