Skip to content

Commit 25b2c45

Browse files
fix(billing): change reset user stats func to invoice payment succeeded (#1116)
* fix(billing): change reset user stats func to invoice payment succeeded * remove nonexistent billing reason
1 parent 780870c commit 25b2c45

File tree

1 file changed

+66
-65
lines changed

1 file changed

+66
-65
lines changed

apps/sim/lib/billing/webhooks/stripe-invoice-webhooks.ts

Lines changed: 66 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {
105153
export 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

Comments
 (0)