Skip to content

Commit 07ca96a

Browse files
committed
fix(email): filter subscription-mode checkout, skip already-subscribed users, fix preview text
1 parent ffcc373 commit 07ca96a

2 files changed

Lines changed: 13 additions & 12 deletions

File tree

apps/sim/components/emails/billing/abandoned-checkout-email.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function AbandonedCheckoutEmail({ userName }: AbandonedCheckoutEmailProps
99
return (
1010
<Html>
1111
<Head />
12-
<Preview>Quick question</Preview>
12+
<Preview>Did you run into an issue with your upgrade?</Preview>
1313
<Body style={styles.body}>
1414
<div style={styles.container}>
1515
<Text style={styles.p}>{userName ? `Hi ${userName},` : 'Hi,'}</Text>

apps/sim/lib/billing/webhooks/checkout.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { eq } from 'drizzle-orm'
55
import type Stripe from 'stripe'
66
import { getEmailSubject, renderAbandonedCheckoutEmail } from '@/components/emails'
7+
import { hasPaidSubscription } from '@/lib/billing/core/subscription'
78
import { sendEmail } from '@/lib/messaging/email/mailer'
89
import { 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
*/
1619
export 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

Comments
 (0)