@@ -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 */
142142export 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