mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(billing): increase free tier credits (#862)
* Update free tier to 10 * Lint
This commit is contained in:
committed by
GitHub
parent
58e764c1dd
commit
e71a736400
@@ -12,6 +12,7 @@ import {
|
||||
Skeleton,
|
||||
} from '@/components/ui'
|
||||
import { useSession, useSubscription } from '@/lib/auth-client'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
BillingSummary,
|
||||
@@ -227,7 +228,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
|
||||
subscription.isEnterprise ||
|
||||
(subscription.isTeam && isTeamAdmin)
|
||||
}
|
||||
minimumLimit={usageLimitData?.minimumLimit ?? 5}
|
||||
minimumLimit={usageLimitData?.minimumLimit ?? DEFAULT_FREE_CREDITS}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
uuid,
|
||||
vector,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import { TAG_SLOTS } from '@/lib/constants/knowledge'
|
||||
|
||||
// Custom tsvector type for full-text search
|
||||
@@ -451,7 +452,9 @@ export const userStats = pgTable('user_stats', {
|
||||
totalChatExecutions: integer('total_chat_executions').notNull().default(0),
|
||||
totalTokensUsed: integer('total_tokens_used').notNull().default(0),
|
||||
totalCost: decimal('total_cost').notNull().default('0'),
|
||||
currentUsageLimit: decimal('current_usage_limit').notNull().default('5'), // Default $5 for free plan
|
||||
currentUsageLimit: decimal('current_usage_limit')
|
||||
.notNull()
|
||||
.default(DEFAULT_FREE_CREDITS.toString()), // Default $10 for free plan
|
||||
usageLimitSetBy: text('usage_limit_set_by'), // User ID who set the limit (for team admin tracking)
|
||||
usageLimitUpdatedAt: timestamp('usage_limit_updated_at').defaultNow(),
|
||||
// Billing period tracking
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import type { SubscriptionFeatures } from '@/lib/billing/types'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
@@ -89,7 +90,7 @@ export function useSubscriptionState() {
|
||||
|
||||
usage: {
|
||||
current: data?.usage?.current ?? 0,
|
||||
limit: data?.usage?.limit ?? 5,
|
||||
limit: data?.usage?.limit ?? DEFAULT_FREE_CREDITS,
|
||||
percentUsed: data?.usage?.percentUsed ?? 0,
|
||||
isWarning: data?.usage?.isWarning ?? false,
|
||||
isExceeded: data?.usage?.isExceeded ?? false,
|
||||
@@ -214,9 +215,9 @@ export function useUsageLimit() {
|
||||
}
|
||||
|
||||
return {
|
||||
currentLimit: data?.currentLimit ?? 5,
|
||||
currentLimit: data?.currentLimit ?? DEFAULT_FREE_CREDITS,
|
||||
canEdit: data?.canEdit ?? false,
|
||||
minimumLimit: data?.minimumLimit ?? 5,
|
||||
minimumLimit: data?.minimumLimit ?? DEFAULT_FREE_CREDITS,
|
||||
plan: data?.plan ?? 'free',
|
||||
setBy: data?.setBy,
|
||||
updatedAt: data?.updatedAt ? new Date(data.updatedAt) : null,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
renderPasswordResetEmail,
|
||||
} from '@/components/emails/render-email'
|
||||
import { getBaseURL } from '@/lib/auth-client'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import { env, isTruthy } from '@/lib/env'
|
||||
import { isProd } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -1080,7 +1081,7 @@ export const auth = betterAuth({
|
||||
name: 'free',
|
||||
priceId: env.STRIPE_FREE_PRICE_ID || '',
|
||||
limits: {
|
||||
cost: env.FREE_TIER_COST_LIMIT ?? 5,
|
||||
cost: env.FREE_TIER_COST_LIMIT ?? DEFAULT_FREE_CREDITS,
|
||||
sharingEnabled: 0,
|
||||
multiplayerEnabled: 0,
|
||||
workspaceCollaborationEnabled: 0,
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
* Billing and cost constants shared between client and server code
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default free credits (in dollars) for new users
|
||||
*/
|
||||
export const DEFAULT_FREE_CREDITS = 10
|
||||
|
||||
/**
|
||||
* Base charge applied to every workflow execution
|
||||
* This charge is applied regardless of whether the workflow uses AI models
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import {
|
||||
resetOrganizationBillingPeriod,
|
||||
resetUserBillingPeriod,
|
||||
@@ -917,7 +918,7 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') {
|
||||
currentUsage: 0,
|
||||
overageAmount: 0,
|
||||
totalProjected: 0,
|
||||
usageLimit: 5,
|
||||
usageLimit: DEFAULT_FREE_CREDITS,
|
||||
percentUsed: 0,
|
||||
isWarning: false,
|
||||
isExceeded: false,
|
||||
@@ -935,7 +936,7 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') {
|
||||
// Usage details
|
||||
usage: {
|
||||
current: 0,
|
||||
limit: 5,
|
||||
limit: DEFAULT_FREE_CREDITS,
|
||||
percentUsed: 0,
|
||||
isWarning: false,
|
||||
isExceeded: false,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import { getPlanPricing } from '@/lib/billing/core/billing'
|
||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -87,7 +88,7 @@ export async function getOrganizationBillingData(
|
||||
// Process member data
|
||||
const members: MemberUsageData[] = membersWithUsage.map((memberRecord) => {
|
||||
const currentUsage = Number(memberRecord.currentPeriodCost || 0)
|
||||
const usageLimit = Number(memberRecord.currentUsageLimit || 5)
|
||||
const usageLimit = Number(memberRecord.currentUsageLimit || DEFAULT_FREE_CREDITS)
|
||||
const percentUsed = usageLimit > 0 ? (currentUsage / usageLimit) * 100 : 0
|
||||
|
||||
return {
|
||||
@@ -197,13 +198,14 @@ export async function updateMemberUsageLimit(
|
||||
|
||||
// Validate minimum limit based on plan
|
||||
const planLimits = {
|
||||
free: 5,
|
||||
free: DEFAULT_FREE_CREDITS,
|
||||
pro: 20,
|
||||
team: 40,
|
||||
enterprise: 100, // Default, can be overridden by metadata
|
||||
}
|
||||
|
||||
let minimumLimit = planLimits[subscription.plan as keyof typeof planLimits] || 5
|
||||
let minimumLimit =
|
||||
planLimits[subscription.plan as keyof typeof planLimits] || DEFAULT_FREE_CREDITS
|
||||
|
||||
// For enterprise, check metadata for custom limits
|
||||
if (subscription.plan === 'enterprise' && subscription.metadata) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { and, eq, inArray } from 'drizzle-orm'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import {
|
||||
calculateDefaultUsageLimit,
|
||||
checkEnterprisePlan,
|
||||
@@ -156,7 +157,7 @@ export async function hasExceededCostLimit(userId: string): Promise<boolean> {
|
||||
const subscription = await getHighestPrioritySubscription(userId)
|
||||
|
||||
// Calculate usage limit
|
||||
let limit = 5 // Default free tier limit
|
||||
let limit = DEFAULT_FREE_CREDITS // Default free tier limit
|
||||
if (subscription) {
|
||||
limit = calculateDefaultUsageLimit(subscription)
|
||||
logger.info('Using subscription-based limit', {
|
||||
@@ -338,7 +339,7 @@ export async function getUserSubscriptionState(userId: string): Promise<UserSubs
|
||||
// Check cost limit using already-fetched user stats
|
||||
let hasExceededLimit = false
|
||||
if (isProd && statsRecords.length > 0) {
|
||||
let limit = 5 // Default free tier limit
|
||||
let limit = DEFAULT_FREE_CREDITS // Default free tier limit
|
||||
if (subscription) {
|
||||
limit = calculateDefaultUsageLimit(subscription)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
||||
import { calculateDefaultUsageLimit, canEditUsageLimit } from '@/lib/billing/subscriptions/utils'
|
||||
import type { BillingData, UsageData, UsageLimitInfo } from '@/lib/billing/types'
|
||||
@@ -29,7 +30,7 @@ export async function getUserUsageData(userId: string): Promise<UsageData> {
|
||||
await initializeUserUsageLimit(userId)
|
||||
return {
|
||||
currentUsage: 0,
|
||||
limit: 5,
|
||||
limit: DEFAULT_FREE_CREDITS,
|
||||
percentUsed: 0,
|
||||
isWarning: false,
|
||||
isExceeded: false,
|
||||
@@ -98,7 +99,7 @@ export async function getUserUsageLimitInfo(userId: string): Promise<UsageLimitI
|
||||
let minimumLimit: number
|
||||
if (!subscription || subscription.status !== 'active') {
|
||||
// Free plan users
|
||||
minimumLimit = 5
|
||||
minimumLimit = DEFAULT_FREE_CREDITS
|
||||
} else if (subscription.plan === 'pro') {
|
||||
// Pro plan users: $20 minimum
|
||||
minimumLimit = 20
|
||||
@@ -131,9 +132,9 @@ export async function getUserUsageLimitInfo(userId: string): Promise<UsageLimitI
|
||||
if (userStatsRecord.length === 0) {
|
||||
await initializeUserUsageLimit(userId)
|
||||
return {
|
||||
currentLimit: 5,
|
||||
currentLimit: DEFAULT_FREE_CREDITS,
|
||||
canEdit: false,
|
||||
minimumLimit: 5,
|
||||
minimumLimit: DEFAULT_FREE_CREDITS,
|
||||
plan: 'free',
|
||||
setBy: null,
|
||||
updatedAt: null,
|
||||
@@ -171,16 +172,16 @@ export async function initializeUserUsageLimit(userId: string): Promise<void> {
|
||||
return // User already has usage stats, don't override
|
||||
}
|
||||
|
||||
// Create initial usage stats with default $5 limit
|
||||
// Create initial usage stats with default free credits limit
|
||||
await db.insert(userStats).values({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
currentUsageLimit: '5', // Default $5 for new users
|
||||
currentUsageLimit: DEFAULT_FREE_CREDITS.toString(), // Default free credits for new users
|
||||
usageLimitUpdatedAt: new Date(),
|
||||
billingPeriodStart: new Date(), // Start billing period immediately
|
||||
})
|
||||
|
||||
logger.info('Initialized usage limit for new user', { userId, limit: 5 })
|
||||
logger.info('Initialized usage limit for new user', { userId, limit: DEFAULT_FREE_CREDITS })
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize usage limit', { userId, error })
|
||||
throw error
|
||||
@@ -233,7 +234,7 @@ export async function updateUserUsageLimit(
|
||||
|
||||
if (!subscription || subscription.status !== 'active') {
|
||||
// Free plan users (shouldn't reach here due to canEditUsageLimit check above)
|
||||
minimumLimit = 5
|
||||
minimumLimit = DEFAULT_FREE_CREDITS
|
||||
} else if (subscription.plan === 'pro') {
|
||||
// Pro plan users: $20 minimum
|
||||
minimumLimit = 20
|
||||
@@ -311,7 +312,7 @@ export async function getUserUsageLimit(userId: string): Promise<number> {
|
||||
if (userStatsQuery.length === 0) {
|
||||
// User doesn't have stats yet, initialize and return default
|
||||
await initializeUserUsageLimit(userId)
|
||||
return 5 // Default free plan limit
|
||||
return DEFAULT_FREE_CREDITS // Default free plan limit
|
||||
}
|
||||
|
||||
return Number.parseFloat(userStatsQuery[0].currentUsageLimit)
|
||||
@@ -380,16 +381,16 @@ export async function syncUsageLimitsFromSubscription(userId: string): Promise<v
|
||||
|
||||
// Only update if subscription is free plan or if current limit is below new minimum
|
||||
if (!subscription || subscription.status !== 'active') {
|
||||
// User downgraded to free plan - cap at $5
|
||||
// User downgraded to free plan - cap at default free credits
|
||||
await db
|
||||
.update(userStats)
|
||||
.set({
|
||||
currentUsageLimit: '5',
|
||||
currentUsageLimit: DEFAULT_FREE_CREDITS.toString(),
|
||||
usageLimitUpdatedAt: new Date(),
|
||||
})
|
||||
.where(eq(userStats.userId, userId))
|
||||
|
||||
logger.info('Synced usage limit to free plan', { userId, limit: 5 })
|
||||
logger.info('Synced usage limit to free plan', { userId, limit: DEFAULT_FREE_CREDITS })
|
||||
} else if (currentLimit < defaultLimit) {
|
||||
// User upgraded and current limit is below new minimum - raise to minimum
|
||||
await db
|
||||
@@ -451,7 +452,7 @@ export async function getTeamUsageLimits(organizationId: string): Promise<
|
||||
userId: memberData.userId,
|
||||
userName: memberData.userName,
|
||||
userEmail: memberData.userEmail,
|
||||
currentLimit: Number.parseFloat(memberData.currentLimit || '5'),
|
||||
currentLimit: Number.parseFloat(memberData.currentLimit || DEFAULT_FREE_CREDITS.toString()),
|
||||
currentUsage: Number.parseFloat(memberData.currentPeriodCost || '0'),
|
||||
totalCost: Number.parseFloat(memberData.totalCost || '0'),
|
||||
lastActive: memberData.lastActive,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { calculateDefaultUsageLimit, checkEnterprisePlan } from '@/lib/billing/s
|
||||
|
||||
vi.mock('@/lib/env', () => ({
|
||||
env: {
|
||||
FREE_TIER_COST_LIMIT: 5,
|
||||
FREE_TIER_COST_LIMIT: 10,
|
||||
PRO_TIER_COST_LIMIT: 20,
|
||||
TEAM_TIER_COST_LIMIT: 40,
|
||||
ENTERPRISE_TIER_COST_LIMIT: 200,
|
||||
@@ -27,15 +27,15 @@ describe('Subscription Utilities', () => {
|
||||
|
||||
describe('calculateDefaultUsageLimit', () => {
|
||||
it.concurrent('returns free-tier limit when subscription is null', () => {
|
||||
expect(calculateDefaultUsageLimit(null)).toBe(5)
|
||||
expect(calculateDefaultUsageLimit(null)).toBe(10)
|
||||
})
|
||||
|
||||
it.concurrent('returns free-tier limit when subscription is undefined', () => {
|
||||
expect(calculateDefaultUsageLimit(undefined)).toBe(5)
|
||||
expect(calculateDefaultUsageLimit(undefined)).toBe(10)
|
||||
})
|
||||
|
||||
it.concurrent('returns free-tier limit when subscription is not active', () => {
|
||||
expect(calculateDefaultUsageLimit({ plan: 'pro', status: 'canceled', seats: 1 })).toBe(5)
|
||||
expect(calculateDefaultUsageLimit({ plan: 'pro', status: 'canceled', seats: 1 })).toBe(10)
|
||||
})
|
||||
|
||||
it.concurrent('returns pro limit for active pro plan', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import { env } from '@/lib/env'
|
||||
|
||||
export function checkEnterprisePlan(subscription: any): boolean {
|
||||
@@ -20,7 +21,7 @@ export function checkTeamPlan(subscription: any): boolean {
|
||||
*/
|
||||
export function calculateDefaultUsageLimit(subscription: any): number {
|
||||
if (!subscription || subscription.status !== 'active') {
|
||||
return env.FREE_TIER_COST_LIMIT || 0
|
||||
return env.FREE_TIER_COST_LIMIT || DEFAULT_FREE_CREDITS
|
||||
}
|
||||
|
||||
const seats = subscription.seats || 1
|
||||
@@ -45,7 +46,7 @@ export function calculateDefaultUsageLimit(subscription: any): number {
|
||||
return seats * (env.ENTERPRISE_TIER_COST_LIMIT || 0)
|
||||
}
|
||||
|
||||
return env.FREE_TIER_COST_LIMIT || 0
|
||||
return env.FREE_TIER_COST_LIMIT || DEFAULT_FREE_CREDITS
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,7 +56,7 @@ async function runBillingTestSuite(): Promise<TestResults> {
|
||||
logger.info('\n📋 Creating test users...')
|
||||
|
||||
// Free user (no overage billing)
|
||||
const freeUser = await createTestUser('free', 5) // $5 usage on free plan
|
||||
const freeUser = await createTestUser('free', 10) // $10 usage on free plan
|
||||
results.users.push(freeUser)
|
||||
|
||||
// Pro user with no overage
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
BillingStatus,
|
||||
@@ -22,7 +23,7 @@ const defaultFeatures: SubscriptionFeatures = {
|
||||
|
||||
const defaultUsage: UsageData = {
|
||||
current: 0,
|
||||
limit: 5,
|
||||
limit: DEFAULT_FREE_CREDITS,
|
||||
percentUsed: 0,
|
||||
isWarning: false,
|
||||
isExceeded: false,
|
||||
|
||||
Reference in New Issue
Block a user