Skip to content

Commit 00af124

Browse files
committed
Review fixes
1 parent 75a228f commit 00af124

File tree

3 files changed

+36
-21
lines changed

3 files changed

+36
-21
lines changed

packages/billing/src/subscription-webhooks.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ import type { Logger } from '@codebuff/common/types/contracts/logger'
1313
import type { PlanConfig } from '@codebuff/common/constants/subscription-plans'
1414
import type Stripe from 'stripe'
1515

16+
type SubscriptionStatus = (typeof schema.subscriptionStatusEnum.enumValues)[number]
17+
18+
/**
19+
* Maps a Stripe subscription status to our local enum.
20+
*/
21+
function mapStripeStatus(status: Stripe.Subscription.Status): SubscriptionStatus {
22+
if (status === 'past_due') return 'past_due'
23+
if (status === 'canceled') return 'canceled'
24+
return 'active'
25+
}
26+
1627
/**
1728
* Looks up a user ID by Stripe customer ID.
1829
*/
@@ -137,7 +148,7 @@ export async function handleSubscriptionInvoicePaid(params: {
137148
target: schema.subscription.stripe_subscription_id,
138149
set: {
139150
status: 'active',
140-
user_id: userId,
151+
...(userId ? { user_id: userId } : {}),
141152
stripe_price_id: priceId,
142153
plan_name: plan.name,
143154
billing_period_start: new Date(
@@ -250,6 +261,8 @@ export async function handleSubscriptionUpdated(params: {
250261
: stripeSubscription.customer.id
251262
const userId = await getUserIdByCustomerId(customerId)
252263

264+
const status = mapStripeStatus(stripeSubscription.status)
265+
253266
// Upsert — webhook ordering is not guaranteed by Stripe, so this event
254267
// may arrive before invoice.paid creates the row.
255268
await db
@@ -260,6 +273,7 @@ export async function handleSubscriptionUpdated(params: {
260273
user_id: userId,
261274
stripe_price_id: priceId,
262275
plan_name: planName,
276+
status,
263277
cancel_at_period_end: stripeSubscription.cancel_at_period_end,
264278
billing_period_start: new Date(
265279
stripeSubscription.current_period_start * 1000,
@@ -271,9 +285,10 @@ export async function handleSubscriptionUpdated(params: {
271285
.onConflictDoUpdate({
272286
target: schema.subscription.stripe_subscription_id,
273287
set: {
274-
user_id: userId,
288+
...(userId ? { user_id: userId } : {}),
275289
stripe_price_id: priceId,
276290
plan_name: planName,
291+
status,
277292
cancel_at_period_end: stripeSubscription.cancel_at_period_end,
278293
billing_period_start: new Date(
279294
stripeSubscription.current_period_start * 1000,

packages/billing/src/subscription.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -537,26 +537,26 @@ export async function handleSubscribe(params: {
537537
logger: Logger
538538
}): Promise<void> {
539539
const { userId, stripeSubscription, logger } = params
540-
541-
// Idempotency: skip if this subscription was already processed
542-
const existing = await db
543-
.select({ stripe_subscription_id: schema.subscription.stripe_subscription_id })
544-
.from(schema.subscription)
545-
.where(eq(schema.subscription.stripe_subscription_id, stripeSubscription.id))
546-
.limit(1)
547-
548-
if (existing.length > 0) {
549-
logger.info(
550-
{ userId, subscriptionId: stripeSubscription.id },
551-
'Subscription already processed — skipping handleSubscribe',
552-
)
553-
return
554-
}
555-
556540
const newResetDate = new Date(stripeSubscription.current_period_end * 1000)
557541

558542
await withAdvisoryLockTransaction({
559543
callback: async (tx) => {
544+
// Idempotency: skip if this subscription was already processed
545+
// Must be inside the lock to prevent TOCTOU races on concurrent webhooks
546+
const existing = await tx
547+
.select({ stripe_subscription_id: schema.subscription.stripe_subscription_id })
548+
.from(schema.subscription)
549+
.where(eq(schema.subscription.stripe_subscription_id, stripeSubscription.id))
550+
.limit(1)
551+
552+
if (existing.length > 0) {
553+
logger.info(
554+
{ userId, subscriptionId: stripeSubscription.id },
555+
'Subscription already processed — skipping handleSubscribe',
556+
)
557+
return
558+
}
559+
560560
// Move next_quota_reset and bump subscription_count
561561
await tx
562562
.update(schema.user)
@@ -652,7 +652,7 @@ async function migrateUnusedCredits(params: {
652652
}
653653

654654
// Create a single migration grant preserving the total
655-
const operationId = `migration-${userId}-${Date.now()}`
655+
const operationId = `migration-${userId}-${crypto.randomUUID()}`
656656
await tx
657657
.insert(schema.creditLedger)
658658
.values({

web/src/app/api/stripe/webhook/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,9 @@ async function handleSubscriptionEvent(subscription: Stripe.Subscription) {
251251
)
252252

253253
if (!organizationId) {
254-
logger.warn(
254+
logger.debug(
255255
{ subscriptionId: subscription.id },
256-
'Subscription event received without organization_id in metadata',
256+
'Subscription event received without organization_id in metadata (user subscription)',
257257
)
258258
return
259259
}

0 commit comments

Comments
 (0)