Skip to content

Commit 1809b38

Browse files
improvement(billing): immediately charge for billing upgrades (#3664)
* improvement(billing): immediately charge for billing upgrades * block on payment failures even for upgrades * address bugbot comments
1 parent bc111a6 commit 1809b38

File tree

3 files changed

+40
-24
lines changed

3 files changed

+40
-24
lines changed

apps/sim/app/api/billing/switch-plan/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export async function POST(request: NextRequest) {
142142
quantity: currentQuantity,
143143
},
144144
],
145-
proration_behavior: 'create_prorations',
145+
proration_behavior: 'always_invoice',
146146
})
147147
}
148148

apps/sim/app/api/organizations/[id]/seats/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
161161
quantity: newSeatCount,
162162
},
163163
],
164-
proration_behavior: 'create_prorations', // Stripe's default - charge/credit immediately
164+
proration_behavior: 'always_invoice',
165165
}
166166
)
167167

@@ -213,7 +213,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
213213
oldSeats: currentSeats,
214214
newSeats: newSeatCount,
215215
updatedBy: session.user.id,
216-
prorationBehavior: 'create_prorations',
216+
prorationBehavior: 'always_invoice',
217217
})
218218

219219
return NextResponse.json({

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

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -503,22 +503,37 @@ export async function handleInvoicePaymentSucceeded(event: Stripe.Event) {
503503
wasBlocked = row.length > 0 ? !!row[0].blocked : false
504504
}
505505

506-
if (isOrgPlan(sub.plan)) {
507-
await unblockOrgMembers(sub.referenceId, 'payment_failed')
508-
} else {
509-
// Only unblock users blocked for payment_failed, not disputes
510-
await db
511-
.update(userStats)
512-
.set({ billingBlocked: false, billingBlockedReason: null })
513-
.where(
514-
and(
515-
eq(userStats.userId, sub.referenceId),
516-
eq(userStats.billingBlockedReason, 'payment_failed')
506+
// For proration invoices (mid-cycle upgrades/seat changes), only unblock if real money
507+
// was collected. A $0 credit invoice from a downgrade should not unblock a user who
508+
// was blocked for a different failed payment.
509+
const isProrationInvoice = invoice.billing_reason === 'subscription_update'
510+
const shouldUnblock = !isProrationInvoice || (invoice.amount_paid ?? 0) > 0
511+
512+
if (shouldUnblock) {
513+
if (isOrgPlan(sub.plan)) {
514+
await unblockOrgMembers(sub.referenceId, 'payment_failed')
515+
} else {
516+
await db
517+
.update(userStats)
518+
.set({ billingBlocked: false, billingBlockedReason: null })
519+
.where(
520+
and(
521+
eq(userStats.userId, sub.referenceId),
522+
eq(userStats.billingBlockedReason, 'payment_failed')
523+
)
517524
)
518-
)
525+
}
526+
} else {
527+
logger.info('Skipping unblock for zero-amount proration invoice', {
528+
invoiceId: invoice.id,
529+
billingReason: invoice.billing_reason,
530+
amountPaid: invoice.amount_paid,
531+
})
519532
}
520533

521-
if (wasBlocked) {
534+
// Only reset usage for cycle renewals — proration invoices should not wipe
535+
// accumulated usage mid-cycle.
536+
if (wasBlocked && !isProrationInvoice) {
522537
await resetUsageForSubscription({ plan: sub.plan, referenceId: sub.referenceId })
523538
}
524539
} catch (error) {
@@ -584,14 +599,6 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
584599

585600
// Block users after first payment failure
586601
if (attemptCount >= 1) {
587-
logger.error('Payment failure - blocking users', {
588-
invoiceId: invoice.id,
589-
customerId,
590-
attemptCount,
591-
isOverageInvoice,
592-
stripeSubscriptionId,
593-
})
594-
595602
const records = await db
596603
.select()
597604
.from(subscriptionTable)
@@ -600,6 +607,15 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
600607

601608
if (records.length > 0) {
602609
const sub = records[0]
610+
611+
logger.error('Payment failure - blocking users', {
612+
invoiceId: invoice.id,
613+
customerId,
614+
attemptCount,
615+
isOverageInvoice,
616+
stripeSubscriptionId,
617+
})
618+
603619
if (isOrgPlan(sub.plan)) {
604620
const memberCount = await blockOrgMembers(sub.referenceId, 'payment_failed')
605621
logger.info('Blocked team/enterprise members due to payment failure', {

0 commit comments

Comments
 (0)