@@ -104,7 +104,21 @@ export async function handleSubscriptionInvoicePaid(params: {
104104
105105 const status = mapStripeStatus ( stripeSub . status )
106106
107- // Upsert subscription row
107+ // Check for a pending scheduled tier change (downgrade)
108+ const existingSub = await db
109+ . select ( {
110+ tier : schema . subscription . tier ,
111+ scheduled_tier : schema . subscription . scheduled_tier ,
112+ } )
113+ . from ( schema . subscription )
114+ . where ( eq ( schema . subscription . stripe_subscription_id , subscriptionId ) )
115+ . limit ( 1 )
116+
117+ const previousTier = existingSub [ 0 ] ?. tier
118+ const hadScheduledTier = existingSub [ 0 ] ?. scheduled_tier != null
119+
120+ // Upsert subscription row — always apply the Stripe tier and clear
121+ // scheduled_tier so pending downgrades take effect on renewal.
108122 await db
109123 . insert ( schema . subscription )
110124 . values ( {
@@ -113,6 +127,7 @@ export async function handleSubscriptionInvoicePaid(params: {
113127 user_id : userId ,
114128 stripe_price_id : priceId ,
115129 tier,
130+ scheduled_tier : null ,
116131 status,
117132 billing_period_start : new Date ( stripeSub . current_period_start * 1000 ) ,
118133 billing_period_end : new Date ( stripeSub . current_period_end * 1000 ) ,
@@ -125,6 +140,7 @@ export async function handleSubscriptionInvoicePaid(params: {
125140 user_id : userId ,
126141 stripe_price_id : priceId ,
127142 tier,
143+ scheduled_tier : null ,
128144 billing_period_start : new Date (
129145 stripeSub . current_period_start * 1000 ,
130146 ) ,
@@ -134,6 +150,16 @@ export async function handleSubscriptionInvoicePaid(params: {
134150 } ,
135151 } )
136152
153+ // If a scheduled downgrade was applied, expire block grants so the user
154+ // gets new grants at the lower tier's limits.
155+ if ( hadScheduledTier ) {
156+ await expireActiveBlockGrants ( { userId, subscriptionId, logger } )
157+ logger . info (
158+ { userId, subscriptionId, previousTier, tier } ,
159+ 'Applied scheduled tier change and expired block grants' ,
160+ )
161+ }
162+
137163 logger . info (
138164 {
139165 subscriptionId,
@@ -233,10 +259,13 @@ export async function handleSubscriptionUpdated(params: {
233259
234260 const status = mapStripeStatus ( stripeSubscription . status )
235261
236- // Check existing tier to detect downgrades — downgrades preserve the
237- // current tier until the next billing period (invoice.paid updates it) .
262+ // Check existing tier to detect downgrades. During a downgrade the old
263+ // higher tier is kept in `scheduled_tier` so limits remain until renewal .
238264 const existingSub = await db
239- . select ( { tier : schema . subscription . tier } )
265+ . select ( {
266+ tier : schema . subscription . tier ,
267+ scheduled_tier : schema . subscription . scheduled_tier ,
268+ } )
240269 . from ( schema . subscription )
241270 . where ( eq ( schema . subscription . stripe_subscription_id , subscriptionId ) )
242271 . limit ( 1 )
@@ -267,8 +296,11 @@ export async function handleSubscriptionUpdated(params: {
267296 target : schema . subscription . stripe_subscription_id ,
268297 set : {
269298 user_id : userId ,
270- stripe_price_id : priceId ,
271- ...( isDowngrade ? { } : { tier } ) ,
299+ // Downgrade: preserve current tier & stripe_price_id, schedule the
300+ // new tier for the next billing period.
301+ ...( isDowngrade
302+ ? { scheduled_tier : tier }
303+ : { tier, stripe_price_id : priceId , scheduled_tier : null } ) ,
272304 status,
273305 cancel_at_period_end : stripeSubscription . cancel_at_period_end ,
274306 billing_period_start : new Date (
@@ -281,14 +313,23 @@ export async function handleSubscriptionUpdated(params: {
281313 } ,
282314 } )
283315
316+ // If this is an upgrade, expire old block grants so the user gets new
317+ // grants at the higher tier's limits. Also serves as a fallback if the
318+ // route handler's DB update failed.
319+ const isUpgrade = existingTier != null && tier > existingTier
320+ if ( isUpgrade ) {
321+ await expireActiveBlockGrants ( { userId, subscriptionId, logger } )
322+ }
323+
284324 logger . info (
285325 {
286326 subscriptionId,
287327 cancelAtPeriodEnd : stripeSubscription . cancel_at_period_end ,
288328 isDowngrade,
329+ isUpgrade,
289330 } ,
290331 isDowngrade
291- ? 'Processed subscription update — downgrade deferred to next billing period'
332+ ? 'Processed subscription update — downgrade scheduled for next billing period'
292333 : 'Processed subscription update' ,
293334 )
294335}
@@ -315,6 +356,7 @@ export async function handleSubscriptionDeleted(params: {
315356 . update ( schema . subscription )
316357 . set ( {
317358 status : 'canceled' ,
359+ scheduled_tier : null ,
318360 canceled_at : new Date ( ) ,
319361 updated_at : new Date ( ) ,
320362 } )
0 commit comments