@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44import { eq } from 'drizzle-orm'
55import type Stripe from 'stripe'
66import { getEmailSubject , renderAbandonedCheckoutEmail } from '@/components/emails'
7+ import { hasPaidSubscription } from '@/lib/billing/core/subscription'
78import { sendEmail } from '@/lib/messaging/email/mailer'
89import { getPersonalEmailFrom } from '@/lib/messaging/email/utils'
910
@@ -12,15 +13,17 @@ const logger = createLogger('CheckoutWebhooks')
1213/**
1314 * Handles checkout.session.expired — fires when a user starts an upgrade but doesn't complete it.
1415 * Sends a plain personal email to check in and offer help.
16+ * Only fires for subscription-mode sessions to avoid misfires on credit purchase or setup sessions.
17+ * Skips users who have already completed a subscription (session may expire after a successful upgrade).
1518 */
1619export async function handleAbandonedCheckout ( event : Stripe . Event ) : Promise < void > {
1720 const session = event . data . object as Stripe . Checkout . Session
1821
22+ if ( session . mode !== 'subscription' ) return
23+
1924 const customerId = typeof session . customer === 'string' ? session . customer : session . customer ?. id
2025 if ( ! customerId ) {
21- logger . warn ( '[handleAbandonedCheckout] No customer ID on expired session' , {
22- sessionId : session . id ,
23- } )
26+ logger . warn ( 'No customer ID on expired session' , { sessionId : session . id } )
2427 return
2528 }
2629
@@ -31,13 +34,14 @@ export async function handleAbandonedCheckout(event: Stripe.Event): Promise<void
3134 . limit ( 1 )
3235
3336 if ( ! userData ?. email ) {
34- logger . warn ( '[handleAbandonedCheckout] No user found for Stripe customer' , {
35- customerId,
36- sessionId : session . id ,
37- } )
37+ logger . warn ( 'No user found for Stripe customer' , { customerId, sessionId : session . id } )
3838 return
3939 }
4040
41+ // Skip if the user has since completed a subscription (first session expired after successful second)
42+ const alreadySubscribed = await hasPaidSubscription ( userData . id )
43+ if ( alreadySubscribed ) return
44+
4145 const { from, replyTo } = getPersonalEmailFrom ( )
4246 const html = await renderAbandonedCheckoutEmail ( userData . name || undefined )
4347
@@ -50,8 +54,5 @@ export async function handleAbandonedCheckout(event: Stripe.Event): Promise<void
5054 emailType : 'transactional' ,
5155 } )
5256
53- logger . info ( '[handleAbandonedCheckout] Sent abandoned checkout email' , {
54- userId : userData . id ,
55- sessionId : session . id ,
56- } )
57+ logger . info ( 'Sent abandoned checkout email' , { userId : userData . id , sessionId : session . id } )
5758}
0 commit comments