@@ -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 )
0 commit comments