Skip to content

Commit f114adf

Browse files
committed
Tweaks and don't downgrade tier immediately if switch plans
1 parent f95faaa commit f114adf

File tree

2 files changed

+51
-25
lines changed

2 files changed

+51
-25
lines changed

packages/billing/src/subscription-webhooks.ts

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const { getTierFromPriceId, getPriceIdFromTier } = createSubscriptionPric
4141
* Handles a paid invoice for a subscription.
4242
*
4343
* - On first payment (`subscription_create`): calls `handleSubscribe` to
44-
* migrate the user's renewal date and unused credits (Option B).
44+
* migrate the user's renewal date and unused credits.
4545
* - On every payment: upserts the `subscription` row with fresh billing
4646
* period dates from Stripe.
4747
*/
@@ -83,22 +83,23 @@ export async function handleSubscriptionInvoicePaid(params: {
8383
}
8484

8585
// Look up the user for this customer
86-
const userId = (await getUserByStripeCustomerId(customerId))?.id ?? null
86+
const user = await getUserByStripeCustomerId(customerId)
87+
if (!user) {
88+
logger.warn(
89+
{ customerId, subscriptionId },
90+
'No user found for customer — skipping handleSubscribe',
91+
)
92+
return
93+
}
94+
const userId = user.id
8795

88-
// On first invoice, migrate renewal date & credits (Option B)
96+
// On first invoice, migrate renewal date & credits
8997
if (invoice.billing_reason === 'subscription_create') {
90-
if (userId) {
91-
await handleSubscribe({
92-
userId,
93-
stripeSubscription: stripeSub,
94-
logger,
95-
})
96-
} else {
97-
logger.warn(
98-
{ customerId, subscriptionId },
99-
'No user found for customer — skipping handleSubscribe',
100-
)
101-
}
98+
await handleSubscribe({
99+
userId,
100+
stripeSubscription: stripeSub,
101+
logger,
102+
})
102103
}
103104

104105
const status = mapStripeStatus(stripeSub.status)
@@ -121,7 +122,7 @@ export async function handleSubscriptionInvoicePaid(params: {
121122
target: schema.subscription.stripe_subscription_id,
122123
set: {
123124
status,
124-
...(userId ? { user_id: userId } : {}),
125+
user_id: userId,
125126
stripe_price_id: priceId,
126127
tier,
127128
billing_period_start: new Date(
@@ -160,9 +161,11 @@ export async function handleSubscriptionInvoicePaymentFailed(params: {
160161
if (!invoice.subscription) return
161162
const subscriptionId = getStripeId(invoice.subscription)
162163
const customerId = getStripeId(invoice.customer)
163-
const userId = customerId
164-
? (await getUserByStripeCustomerId(customerId))?.id ?? null
165-
: null
164+
let userId = null
165+
if (customerId) {
166+
const user = await getUserByStripeCustomerId(customerId)
167+
userId = user?.id
168+
}
166169

167170
await db
168171
.update(schema.subscription)
@@ -218,10 +221,29 @@ export async function handleSubscriptionUpdated(params: {
218221
}
219222

220223
const customerId = getStripeId(stripeSubscription.customer)
221-
const userId = (await getUserByStripeCustomerId(customerId))?.id ?? null
224+
const user = await getUserByStripeCustomerId(customerId)
225+
if (!user) {
226+
logger.warn(
227+
{ customerId, subscriptionId },
228+
'No user found for customer — skipping',
229+
)
230+
return
231+
}
232+
const userId = user.id
222233

223234
const status = mapStripeStatus(stripeSubscription.status)
224235

236+
// Check existing tier to detect downgrades — downgrades preserve the
237+
// current tier until the next billing period (invoice.paid updates it).
238+
const existingSub = await db
239+
.select({ tier: schema.subscription.tier })
240+
.from(schema.subscription)
241+
.where(eq(schema.subscription.stripe_subscription_id, subscriptionId))
242+
.limit(1)
243+
244+
const existingTier = existingSub[0]?.tier
245+
const isDowngrade = existingTier != null && existingTier > tier
246+
225247
// Upsert — webhook ordering is not guaranteed by Stripe, so this event
226248
// may arrive before invoice.paid creates the row.
227249
await db
@@ -244,9 +266,9 @@ export async function handleSubscriptionUpdated(params: {
244266
.onConflictDoUpdate({
245267
target: schema.subscription.stripe_subscription_id,
246268
set: {
247-
...(userId ? { user_id: userId } : {}),
269+
user_id: userId,
248270
stripe_price_id: priceId,
249-
tier,
271+
...(isDowngrade ? {} : { tier }),
250272
status,
251273
cancel_at_period_end: stripeSubscription.cancel_at_period_end,
252274
billing_period_start: new Date(
@@ -263,8 +285,11 @@ export async function handleSubscriptionUpdated(params: {
263285
{
264286
subscriptionId,
265287
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
288+
isDowngrade,
266289
},
267-
'Processed subscription update',
290+
isDowngrade
291+
? 'Processed subscription update — downgrade deferred to next billing period'
292+
: 'Processed subscription update',
268293
)
269294
}
270295

@@ -283,7 +308,8 @@ export async function handleSubscriptionDeleted(params: {
283308
const subscriptionId = stripeSubscription.id
284309

285310
const customerId = getStripeId(stripeSubscription.customer)
286-
const userId = (await getUserByStripeCustomerId(customerId))?.id ?? null
311+
const user = await getUserByStripeCustomerId(customerId)
312+
const userId = user?.id ?? null
287313

288314
await db
289315
.update(schema.subscription)

packages/billing/src/subscription.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ export async function isSubscriber(params: {
554554
}
555555

556556
// ---------------------------------------------------------------------------
557-
// Subscribe flow (Option B — unify renewal dates)
557+
// Subscribe flow
558558
// ---------------------------------------------------------------------------
559559

560560
/**

0 commit comments

Comments
 (0)