(event: Stripe.Event)
| 17 | * Skips users who have already completed a subscription (session may expire after a successful upgrade). |
| 18 | */ |
| 19 | export async function handleAbandonedCheckout(event: Stripe.Event): Promise<void> { |
| 20 | const session = event.data.object as Stripe.Checkout.Session |
| 21 | |
| 22 | if (session.mode !== 'subscription') return |
| 23 | |
| 24 | const customerId = typeof session.customer === 'string' ? session.customer : session.customer?.id |
| 25 | if (!customerId) { |
| 26 | logger.warn('No customer ID on expired session', { sessionId: session.id }) |
| 27 | return |
| 28 | } |
| 29 | |
| 30 | const [userData] = await db |
| 31 | .select({ id: user.id, email: user.email, name: user.name }) |
| 32 | .from(user) |
| 33 | .where(eq(user.stripeCustomerId, customerId)) |
| 34 | .limit(1) |
| 35 | |
| 36 | if (!userData?.email) { |
| 37 | logger.warn('No user found for Stripe customer', { customerId, sessionId: session.id }) |
| 38 | return |
| 39 | } |
| 40 | |
| 41 | // Skip if the user already has a paid plan (direct or via org) — covers session expiring after a successful upgrade |
| 42 | const alreadySubscribed = await isProPlan(userData.id) |
| 43 | if (alreadySubscribed) return |
| 44 | |
| 45 | const { from, replyTo } = getPersonalEmailFrom() |
| 46 | const html = await renderAbandonedCheckoutEmail(userData.name || undefined) |
| 47 | |
| 48 | await sendEmail({ |
| 49 | to: userData.email, |
| 50 | subject: getEmailSubject('abandoned-checkout'), |
| 51 | html, |
| 52 | from, |
| 53 | replyTo, |
| 54 | emailType: 'notifications', |
| 55 | }) |
| 56 | |
| 57 | logger.info('Sent abandoned checkout email', { userId: userData.id, sessionId: session.id }) |
| 58 | } |
no test coverage detected