Skip to content

Commit 76f71c4

Browse files
committed
Add subscription tiers. Extract util getStripeId
1 parent 40a0b2e commit 76f71c4

File tree

12 files changed

+3142
-51
lines changed

12 files changed

+3142
-51
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ STRIPE_SECRET_KEY=sk_test_dummy_stripe_secret
1818
STRIPE_WEBHOOK_SECRET_KEY=whsec_dummy_webhook_secret
1919
STRIPE_USAGE_PRICE_ID=price_dummy_usage_id
2020
STRIPE_TEAM_FEE_PRICE_ID=price_dummy_team_fee_id
21+
STRIPE_SUBSCRIPTION_100_PRICE_ID=price_dummy_subscription_100_id
2122
STRIPE_SUBSCRIPTION_200_PRICE_ID=price_dummy_subscription_200_id
23+
STRIPE_SUBSCRIPTION_500_PRICE_ID=price_dummy_subscription_500_id
2224

2325
# External Services
2426
LINKUP_API_KEY=dummy_linkup_key

common/src/constants/subscription-plans.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,26 @@ export interface TierConfig {
88
}
99

1010
export const SUBSCRIPTION_TIERS = {
11+
100: {
12+
monthlyPrice: 100,
13+
creditsPerBlock: 400,
14+
blockDurationHours: 5,
15+
weeklyCreditsLimit: 4000,
16+
},
1117
200: {
1218
monthlyPrice: 200,
1319
creditsPerBlock: 1250,
1420
blockDurationHours: 5,
1521
weeklyCreditsLimit: 12500,
1622
},
23+
500: {
24+
monthlyPrice: 500,
25+
creditsPerBlock: 3125,
26+
blockDurationHours: 5,
27+
weeklyCreditsLimit: 31250,
28+
},
1729
} as const satisfies Record<number, TierConfig>
1830

31+
export type SubscriptionTierPrice = keyof typeof SUBSCRIPTION_TIERS
32+
1933
export const DEFAULT_TIER = SUBSCRIPTION_TIERS[200]

packages/billing/src/subscription-webhooks.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import { trackEvent } from '@codebuff/common/analytics'
22
import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
33
import db from '@codebuff/internal/db'
44
import * as schema from '@codebuff/internal/db/schema'
5+
import { env } from '@codebuff/internal/env'
56
import {
7+
getStripeId,
68
getUserByStripeCustomerId,
79
stripeServer,
810
} from '@codebuff/internal/util/stripe'
911
import { eq } from 'drizzle-orm'
1012

1113
import { handleSubscribe } from './subscription'
1214

15+
import type { SubscriptionTierPrice } from '@codebuff/common/constants/subscription-plans'
1316
import type { Logger } from '@codebuff/common/types/contracts/logger'
1417
import type Stripe from 'stripe'
1518

@@ -24,6 +27,16 @@ function mapStripeStatus(status: Stripe.Subscription.Status): SubscriptionStatus
2427
return 'incomplete'
2528
}
2629

30+
const priceToTier: Record<string, SubscriptionTierPrice> = {
31+
...(env.STRIPE_SUBSCRIPTION_100_PRICE_ID && { [env.STRIPE_SUBSCRIPTION_100_PRICE_ID]: 100 as const }),
32+
...(env.STRIPE_SUBSCRIPTION_200_PRICE_ID && { [env.STRIPE_SUBSCRIPTION_200_PRICE_ID]: 200 as const }),
33+
...(env.STRIPE_SUBSCRIPTION_500_PRICE_ID && { [env.STRIPE_SUBSCRIPTION_500_PRICE_ID]: 500 as const }),
34+
}
35+
36+
function getTierFromPriceId(priceId: string): SubscriptionTierPrice | null {
37+
return priceToTier[priceId] ?? null
38+
}
39+
2740
// ---------------------------------------------------------------------------
2841
// invoice.paid
2942
// ---------------------------------------------------------------------------
@@ -43,14 +56,8 @@ export async function handleSubscriptionInvoicePaid(params: {
4356
const { invoice, logger } = params
4457

4558
if (!invoice.subscription) return
46-
const subscriptionId =
47-
typeof invoice.subscription === 'string'
48-
? invoice.subscription
49-
: invoice.subscription.id
50-
const customerId =
51-
typeof invoice.customer === 'string'
52-
? invoice.customer
53-
: invoice.customer?.id
59+
const subscriptionId = getStripeId(invoice.subscription)
60+
const customerId = getStripeId(invoice.customer)
5461

5562
if (!customerId) {
5663
logger.warn(
@@ -97,6 +104,7 @@ export async function handleSubscriptionInvoicePaid(params: {
97104
stripe_customer_id: customerId,
98105
user_id: userId,
99106
stripe_price_id: priceId,
107+
tier: getTierFromPriceId(priceId),
100108
status: 'active',
101109
billing_period_start: new Date(stripeSub.current_period_start * 1000),
102110
billing_period_end: new Date(stripeSub.current_period_end * 1000),
@@ -108,6 +116,7 @@ export async function handleSubscriptionInvoicePaid(params: {
108116
status: 'active',
109117
...(userId ? { user_id: userId } : {}),
110118
stripe_price_id: priceId,
119+
tier: getTierFromPriceId(priceId),
111120
billing_period_start: new Date(
112121
stripeSub.current_period_start * 1000,
113122
),
@@ -142,15 +151,8 @@ export async function handleSubscriptionInvoicePaymentFailed(params: {
142151
const { invoice, logger } = params
143152

144153
if (!invoice.subscription) return
145-
const subscriptionId =
146-
typeof invoice.subscription === 'string'
147-
? invoice.subscription
148-
: invoice.subscription.id
149-
150-
const customerId =
151-
typeof invoice.customer === 'string'
152-
? invoice.customer
153-
: invoice.customer?.id
154+
const subscriptionId = getStripeId(invoice.subscription)
155+
const customerId = getStripeId(invoice.customer)
154156
const userId = customerId
155157
? (await getUserByStripeCustomerId(customerId))?.id ?? null
156158
: null
@@ -199,10 +201,7 @@ export async function handleSubscriptionUpdated(params: {
199201
return
200202
}
201203

202-
const customerId =
203-
typeof stripeSubscription.customer === 'string'
204-
? stripeSubscription.customer
205-
: stripeSubscription.customer.id
204+
const customerId = getStripeId(stripeSubscription.customer)
206205
const userId = (await getUserByStripeCustomerId(customerId))?.id ?? null
207206

208207
const status = mapStripeStatus(stripeSubscription.status)
@@ -216,6 +215,7 @@ export async function handleSubscriptionUpdated(params: {
216215
stripe_customer_id: customerId,
217216
user_id: userId,
218217
stripe_price_id: priceId,
218+
tier: getTierFromPriceId(priceId),
219219
status,
220220
cancel_at_period_end: stripeSubscription.cancel_at_period_end,
221221
billing_period_start: new Date(
@@ -230,6 +230,7 @@ export async function handleSubscriptionUpdated(params: {
230230
set: {
231231
...(userId ? { user_id: userId } : {}),
232232
stripe_price_id: priceId,
233+
tier: getTierFromPriceId(priceId),
233234
status,
234235
cancel_at_period_end: stripeSubscription.cancel_at_period_end,
235236
billing_period_start: new Date(
@@ -265,10 +266,7 @@ export async function handleSubscriptionDeleted(params: {
265266
const { stripeSubscription, logger } = params
266267
const subscriptionId = stripeSubscription.id
267268

268-
const customerId =
269-
typeof stripeSubscription.customer === 'string'
270-
? stripeSubscription.customer
271-
: stripeSubscription.customer.id
269+
const customerId = getStripeId(stripeSubscription.customer)
272270
const userId = (await getUserByStripeCustomerId(customerId))?.id ?? null
273271

274272
await db

packages/billing/src/subscription.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { GRANT_PRIORITIES } from '@codebuff/common/constants/grant-priorities'
44
import {
55
DEFAULT_TIER,
66
SUBSCRIPTION_DISPLAY_NAME,
7+
SUBSCRIPTION_TIERS,
78
} from '@codebuff/common/constants/subscription-plans'
9+
10+
import type { SubscriptionTierPrice } from '@codebuff/common/constants/subscription-plans'
811
import db from '@codebuff/internal/db'
912
import * as schema from '@codebuff/internal/db/schema'
1013
import { withAdvisoryLockTransaction } from '@codebuff/internal/db/transaction'
@@ -137,8 +140,9 @@ export async function getSubscriptionLimits(params: {
137140
userId: string
138141
logger: Logger
139142
conn?: DbConn
143+
tier?: number | null
140144
}): Promise<SubscriptionLimits> {
141-
const { userId, logger, conn = db } = params
145+
const { userId, logger, conn = db, tier } = params
142146

143147
const overrides = await conn
144148
.select()
@@ -159,10 +163,15 @@ export async function getSubscriptionLimits(params: {
159163
}
160164
}
161165

166+
const tierConfig =
167+
tier != null && tier in SUBSCRIPTION_TIERS
168+
? SUBSCRIPTION_TIERS[tier as SubscriptionTierPrice]
169+
: DEFAULT_TIER
170+
162171
return {
163-
creditsPerBlock: DEFAULT_TIER.creditsPerBlock,
164-
blockDurationHours: DEFAULT_TIER.blockDurationHours,
165-
weeklyCreditsLimit: DEFAULT_TIER.weeklyCreditsLimit,
172+
creditsPerBlock: tierConfig.creditsPerBlock,
173+
blockDurationHours: tierConfig.blockDurationHours,
174+
weeklyCreditsLimit: tierConfig.weeklyCreditsLimit,
166175
}
167176
}
168177

@@ -274,6 +283,7 @@ export async function ensureActiveBlockGrant(params: {
274283
userId,
275284
logger,
276285
conn: tx,
286+
tier: subscription.tier,
277287
})
278288

279289
// 3. Check weekly limit before creating a new block
@@ -394,6 +404,7 @@ export async function checkRateLimit(params: {
394404
const limits = await getSubscriptionLimits({
395405
userId,
396406
logger,
407+
tier: subscription.tier,
397408
})
398409

399410
const weekly = await getWeeklyUsage({
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
DROP INDEX "idx_credit_ledger_subscription";--> statement-breakpoint
2+
ALTER TABLE "subscription" ADD COLUMN "tier" integer;--> statement-breakpoint
3+
CREATE INDEX "idx_credit_ledger_subscription" ON "credit_ledger" USING btree ("user_id","type","created_at");

0 commit comments

Comments
 (0)