@@ -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