@@ -18,27 +18,75 @@ export async function handleInvoicePaymentSucceeded(event: Stripe.Event) {
1818 try {
1919 const invoice = event . data . object as Stripe . Invoice
2020
21- // Check if this is an overage billing invoice
22- if ( invoice . metadata ?. type !== 'overage_billing' ) {
23- logger . info ( 'Ignoring non-overage billing invoice' , { invoiceId : invoice . id } )
21+ // Case 1: Overage invoices (metadata.type === 'overage_billing')
22+ if ( invoice . metadata ?. type === 'overage_billing' ) {
23+ const customerId = invoice . customer as string
24+ const chargedAmount = invoice . amount_paid / 100
25+ const billingPeriod = invoice . metadata ?. billingPeriod || 'unknown'
26+
27+ logger . info ( 'Overage billing invoice payment succeeded' , {
28+ invoiceId : invoice . id ,
29+ customerId,
30+ chargedAmount,
31+ billingPeriod,
32+ customerEmail : invoice . customer_email ,
33+ hostedInvoiceUrl : invoice . hosted_invoice_url ,
34+ } )
35+
2436 return
2537 }
2638
27- const customerId = invoice . customer as string
28- const chargedAmount = invoice . amount_paid / 100 // Convert from cents to dollars
29- const billingPeriod = invoice . metadata ?. billingPeriod || 'unknown'
39+ // Case 2: Subscription renewal invoice paid (primary period rollover)
40+ // Only reset on successful payment to avoid granting a new period while in dunning
41+ if ( invoice . subscription ) {
42+ // Filter to subscription-cycle renewals; ignore updates/off-cycle charges
43+ const reason = invoice . billing_reason
44+ const isCycle = reason === 'subscription_cycle'
45+ if ( ! isCycle ) {
46+ logger . info ( 'Ignoring non-cycle subscription invoice on payment_succeeded' , {
47+ invoiceId : invoice . id ,
48+ billingReason : reason ,
49+ } )
50+ return
51+ }
3052
31- logger . info ( 'Overage billing invoice payment succeeded' , {
32- invoiceId : invoice . id ,
33- customerId,
34- chargedAmount,
35- billingPeriod,
36- customerEmail : invoice . customer_email ,
37- hostedInvoiceUrl : invoice . hosted_invoice_url ,
38- } )
53+ const stripeSubscriptionId = String ( invoice . subscription )
54+ const records = await db
55+ . select ( )
56+ . from ( subscriptionTable )
57+ . where ( eq ( subscriptionTable . stripeSubscriptionId , stripeSubscriptionId ) )
58+ . limit ( 1 )
3959
40- // Additional payment success logic can be added here
41- // For example: update internal billing status, trigger analytics events, etc.
60+ if ( records . length === 0 ) {
61+ logger . warn ( 'No matching internal subscription for paid Stripe invoice' , {
62+ invoiceId : invoice . id ,
63+ stripeSubscriptionId,
64+ } )
65+ return
66+ }
67+
68+ const sub = records [ 0 ]
69+
70+ if ( sub . plan === 'team' || sub . plan === 'enterprise' ) {
71+ await resetOrganizationBillingPeriod ( sub . referenceId )
72+ logger . info ( 'Reset organization billing period on subscription invoice payment' , {
73+ invoiceId : invoice . id ,
74+ organizationId : sub . referenceId ,
75+ plan : sub . plan ,
76+ } )
77+ } else {
78+ await resetUserBillingPeriod ( sub . referenceId )
79+ logger . info ( 'Reset user billing period on subscription invoice payment' , {
80+ invoiceId : invoice . id ,
81+ userId : sub . referenceId ,
82+ plan : sub . plan ,
83+ } )
84+ }
85+
86+ return
87+ }
88+
89+ logger . info ( 'Ignoring non-subscription invoice payment' , { invoiceId : invoice . id } )
4290 } catch ( error ) {
4391 logger . error ( 'Failed to handle invoice payment succeeded' , {
4492 eventId : event . id ,
@@ -105,67 +153,20 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
105153export async function handleInvoiceFinalized ( event : Stripe . Event ) {
106154 try {
107155 const invoice = event . data . object as Stripe . Invoice
108-
109- // Case 1: Overage invoices (metadata.type === 'overage_billing')
156+ // Do not reset usage on finalized; wait for payment success to avoid granting new period during dunning
110157 if ( invoice . metadata ?. type === 'overage_billing' ) {
111158 const customerId = invoice . customer as string
112159 const invoiceAmount = invoice . amount_due / 100
113160 const billingPeriod = invoice . metadata ?. billingPeriod || 'unknown'
114-
115161 logger . info ( 'Overage billing invoice finalized' , {
116162 invoiceId : invoice . id ,
117163 customerId,
118164 invoiceAmount,
119165 billingPeriod,
120- customerEmail : invoice . customer_email ,
121- hostedInvoiceUrl : invoice . hosted_invoice_url ,
122166 } )
123-
124- return
125- }
126-
127- // Case 2: Subscription cycle invoices (primary period rollover)
128- // When an invoice is finalized for a subscription cycle, align our usage reset to this boundary
129- if ( invoice . subscription ) {
130- const stripeSubscriptionId = String ( invoice . subscription )
131-
132- const records = await db
133- . select ( )
134- . from ( subscriptionTable )
135- . where ( eq ( subscriptionTable . stripeSubscriptionId , stripeSubscriptionId ) )
136- . limit ( 1 )
137-
138- if ( records . length === 0 ) {
139- logger . warn ( 'No matching internal subscription for Stripe invoice subscription' , {
140- invoiceId : invoice . id ,
141- stripeSubscriptionId,
142- } )
143- return
144- }
145-
146- const sub = records [ 0 ]
147-
148- // Idempotent reset aligned to the subscription’s new cycle
149- if ( sub . plan === 'team' || sub . plan === 'enterprise' ) {
150- await resetOrganizationBillingPeriod ( sub . referenceId )
151- logger . info ( 'Reset organization billing period on subscription invoice finalization' , {
152- invoiceId : invoice . id ,
153- organizationId : sub . referenceId ,
154- plan : sub . plan ,
155- } )
156- } else {
157- await resetUserBillingPeriod ( sub . referenceId )
158- logger . info ( 'Reset user billing period on subscription invoice finalization' , {
159- invoiceId : invoice . id ,
160- userId : sub . referenceId ,
161- plan : sub . plan ,
162- } )
163- }
164-
165167 return
166168 }
167-
168- logger . info ( 'Ignoring non-subscription invoice finalization' , {
169+ logger . info ( 'Ignoring subscription invoice finalization; will act on payment_succeeded' , {
169170 invoiceId : invoice . id ,
170171 billingReason : invoice . billing_reason ,
171172 } )
0 commit comments