Skip to content

Commit 26e6286

Browse files
authored
fix(billing): fix team plan upgrade (#1053)
1 parent c795fc8 commit 26e6286

5 files changed

Lines changed: 312 additions & 148 deletions

File tree

apps/sim/lib/auth.ts

Lines changed: 35 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,133 +1270,30 @@ export const auth = betterAuth({
12701270
})
12711271

12721272
// Auto-create organization for team plan purchases
1273-
if (subscription.plan === 'team') {
1274-
try {
1275-
// Get the user who purchased the subscription
1276-
const user = await db
1277-
.select()
1278-
.from(schema.user)
1279-
.where(eq(schema.user.id, subscription.referenceId))
1280-
.limit(1)
1281-
1282-
if (user.length > 0) {
1283-
const currentUser = user[0]
1284-
1285-
// Store the original user ID before we change the referenceId
1286-
const originalUserId = subscription.referenceId
1287-
1288-
// First, sync usage limits for the purchasing user with their new plan
1289-
try {
1290-
const { syncUsageLimitsFromSubscription } = await import('@/lib/billing')
1291-
await syncUsageLimitsFromSubscription(originalUserId)
1292-
logger.info(
1293-
'Usage limits synced for purchasing user before organization creation',
1294-
{
1295-
userId: originalUserId,
1296-
}
1297-
)
1298-
} catch (error) {
1299-
logger.error('Failed to sync usage limits for purchasing user', {
1300-
userId: originalUserId,
1301-
error,
1302-
})
1303-
}
1304-
1305-
// Create organization for the team
1306-
const orgId = `org_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`
1307-
const orgSlug = `${currentUser.name?.toLowerCase().replace(/\s+/g, '-') || 'team'}-${Date.now()}`
1308-
1309-
// Create a separate Stripe customer for the organization
1310-
let orgStripeCustomerId: string | null = null
1311-
if (stripeClient) {
1312-
try {
1313-
const orgStripeCustomer = await stripeClient.customers.create({
1314-
name: `${currentUser.name || 'User'}'s Team`,
1315-
email: currentUser.email,
1316-
metadata: {
1317-
organizationId: orgId,
1318-
type: 'organization',
1319-
},
1320-
})
1321-
orgStripeCustomerId = orgStripeCustomer.id
1322-
} catch (error) {
1323-
logger.error('Failed to create Stripe customer for organization', {
1324-
organizationId: orgId,
1325-
error,
1326-
})
1327-
// Continue without Stripe customer - can be created later
1328-
}
1329-
}
1330-
1331-
const newOrg = await db
1332-
.insert(schema.organization)
1333-
.values({
1334-
id: orgId,
1335-
name: `${currentUser.name || 'User'}'s Team`,
1336-
slug: orgSlug,
1337-
metadata: orgStripeCustomerId
1338-
? { stripeCustomerId: orgStripeCustomerId }
1339-
: null,
1340-
createdAt: new Date(),
1341-
updatedAt: new Date(),
1342-
})
1343-
.returning()
1344-
1345-
// Add the user as owner of the organization
1346-
await db.insert(schema.member).values({
1347-
id: `member_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`,
1348-
userId: currentUser.id,
1349-
organizationId: orgId,
1350-
role: 'owner',
1351-
createdAt: new Date(),
1352-
})
1353-
1354-
// Update the subscription to reference the organization instead of the user
1355-
await db
1356-
.update(schema.subscription)
1357-
.set({ referenceId: orgId })
1358-
.where(eq(schema.subscription.id, subscription.id))
1359-
1360-
// Update the session to set the new organization as active
1361-
await db
1362-
.update(schema.session)
1363-
.set({ activeOrganizationId: orgId })
1364-
.where(eq(schema.session.userId, currentUser.id))
1365-
1366-
logger.info('Auto-created organization for team subscription', {
1367-
organizationId: orgId,
1368-
userId: currentUser.id,
1369-
subscriptionId: subscription.id,
1370-
orgName: `${currentUser.name || 'User'}'s Team`,
1371-
})
1372-
1373-
// Update referenceId for usage limit sync
1374-
subscription.referenceId = orgId
1375-
}
1376-
} catch (error) {
1377-
logger.error('Failed to auto-create organization for team subscription', {
1378-
subscriptionId: subscription.id,
1379-
referenceId: subscription.referenceId,
1380-
error,
1381-
})
1382-
}
1273+
try {
1274+
const { handleTeamPlanOrganization } = await import(
1275+
'@/lib/billing/team-management'
1276+
)
1277+
await handleTeamPlanOrganization(subscription)
1278+
} catch (error) {
1279+
logger.error('Failed to handle team plan organization creation', {
1280+
subscriptionId: subscription.id,
1281+
referenceId: subscription.referenceId,
1282+
error,
1283+
})
13831284
}
13841285

1385-
// Initialize billing period for the user/organization
1286+
// Initialize billing period and sync usage limits
13861287
try {
13871288
const { initializeBillingPeriod } = await import(
13881289
'@/lib/billing/core/billing-periods'
13891290
)
1291+
const { syncSubscriptionUsageLimits } = await import(
1292+
'@/lib/billing/team-management'
1293+
)
13901294

1391-
// Note: Usage limits are already synced above for team plan users
1392-
// For non-team plans, sync usage limits using the referenceId (which is the user ID)
1393-
if (subscription.plan !== 'team') {
1394-
const { syncUsageLimitsFromSubscription } = await import('@/lib/billing')
1395-
await syncUsageLimitsFromSubscription(subscription.referenceId)
1396-
logger.info('Usage limits synced after subscription creation', {
1397-
referenceId: subscription.referenceId,
1398-
})
1399-
}
1295+
// Sync usage limits for user or organization members
1296+
await syncSubscriptionUsageLimits(subscription)
14001297

14011298
// Initialize billing period for new subscription using Stripe dates
14021299
if (subscription.plan !== 'free') {
@@ -1433,15 +1330,29 @@ export const auth = betterAuth({
14331330
logger.info('Subscription updated', {
14341331
subscriptionId: subscription.id,
14351332
status: subscription.status,
1333+
plan: subscription.plan,
14361334
})
14371335

1438-
// Sync usage limits for the user/organization
1336+
// Auto-create organization for team plan upgrades (free -> team)
14391337
try {
1440-
const { syncUsageLimitsFromSubscription } = await import('@/lib/billing')
1441-
await syncUsageLimitsFromSubscription(subscription.referenceId)
1442-
logger.info('Usage limits synced after subscription update', {
1338+
const { handleTeamPlanOrganization } = await import(
1339+
'@/lib/billing/team-management'
1340+
)
1341+
await handleTeamPlanOrganization(subscription)
1342+
} catch (error) {
1343+
logger.error('Failed to handle team plan organization creation on update', {
1344+
subscriptionId: subscription.id,
14431345
referenceId: subscription.referenceId,
1346+
error,
14441347
})
1348+
}
1349+
1350+
// Sync usage limits for the user/organization
1351+
try {
1352+
const { syncSubscriptionUsageLimits } = await import(
1353+
'@/lib/billing/team-management'
1354+
)
1355+
await syncSubscriptionUsageLimits(subscription)
14451356
} catch (error) {
14461357
logger.error('Failed to sync usage limits after subscription update', {
14471358
referenceId: subscription.referenceId,

apps/sim/lib/billing/core/billing.ts

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ import { member, organization, subscription, user, userStats } from '@/db/schema
1313

1414
const logger = createLogger('Billing')
1515

16+
/**
17+
* Get organization subscription directly by organization ID
18+
*/
19+
export async function getOrganizationSubscription(organizationId: string) {
20+
try {
21+
const orgSubs = await db
22+
.select()
23+
.from(subscription)
24+
.where(and(eq(subscription.referenceId, organizationId), eq(subscription.status, 'active')))
25+
.limit(1)
26+
27+
return orgSubs.length > 0 ? orgSubs[0] : null
28+
} catch (error) {
29+
logger.error('Error getting organization subscription', { error, organizationId })
30+
return null
31+
}
32+
}
33+
1634
interface BillingResult {
1735
success: boolean
1836
chargedAmount?: number
@@ -89,15 +107,43 @@ async function getStripeCustomerId(referenceId: string): Promise<string | null>
89107
.where(eq(organization.id, referenceId))
90108
.limit(1)
91109

92-
if (orgRecord.length > 0 && orgRecord[0].metadata) {
93-
const metadata =
94-
typeof orgRecord[0].metadata === 'string'
95-
? JSON.parse(orgRecord[0].metadata)
96-
: orgRecord[0].metadata
110+
if (orgRecord.length > 0) {
111+
// First, check if organization has its own Stripe customer (legacy support)
112+
if (orgRecord[0].metadata) {
113+
const metadata =
114+
typeof orgRecord[0].metadata === 'string'
115+
? JSON.parse(orgRecord[0].metadata)
116+
: orgRecord[0].metadata
117+
118+
if (metadata?.stripeCustomerId) {
119+
return metadata.stripeCustomerId
120+
}
121+
}
97122

98-
if (metadata?.stripeCustomerId) {
99-
return metadata.stripeCustomerId
123+
// If organization has no Stripe customer, use the owner's customer
124+
// This is our new pattern: subscriptions stay with user, referenceId = orgId
125+
const ownerRecord = await db
126+
.select({
127+
stripeCustomerId: user.stripeCustomerId,
128+
userId: user.id,
129+
})
130+
.from(user)
131+
.innerJoin(member, eq(member.userId, user.id))
132+
.where(and(eq(member.organizationId, referenceId), eq(member.role, 'owner')))
133+
.limit(1)
134+
135+
if (ownerRecord.length > 0 && ownerRecord[0].stripeCustomerId) {
136+
logger.debug('Using organization owner Stripe customer for billing', {
137+
organizationId: referenceId,
138+
ownerId: ownerRecord[0].userId,
139+
stripeCustomerId: ownerRecord[0].stripeCustomerId,
140+
})
141+
return ownerRecord[0].stripeCustomerId
100142
}
143+
144+
logger.warn('No Stripe customer found for organization or its owner', {
145+
organizationId: referenceId,
146+
})
101147
}
102148

103149
return null
@@ -431,8 +477,8 @@ export async function processOrganizationOverageBilling(
431477
organizationId: string
432478
): Promise<BillingResult> {
433479
try {
434-
// Get organization subscription
435-
const subscription = await getHighestPrioritySubscription(organizationId)
480+
// Get organization subscription directly (referenceId = organizationId)
481+
const subscription = await getOrganizationSubscription(organizationId)
436482

437483
if (!subscription || !['team', 'enterprise'].includes(subscription.plan)) {
438484
logger.warn('No team/enterprise subscription found for organization', { organizationId })
@@ -759,7 +805,9 @@ export async function getSimplifiedBillingSummary(
759805
try {
760806
// Get subscription and usage data upfront
761807
const [subscription, usageData] = await Promise.all([
762-
getHighestPrioritySubscription(organizationId || userId),
808+
organizationId
809+
? getOrganizationSubscription(organizationId)
810+
: getHighestPrioritySubscription(userId),
763811
getUserUsageData(userId),
764812
])
765813

apps/sim/lib/billing/core/organization-billing.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
import { and, eq } from 'drizzle-orm'
22
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
33
import { getPlanPricing } from '@/lib/billing/core/billing'
4-
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
54
import { createLogger } from '@/lib/logs/console/logger'
65
import { db } from '@/db'
7-
import { member, organization, user, userStats } from '@/db/schema'
6+
import { member, organization, subscription, user, userStats } from '@/db/schema'
87

98
const logger = createLogger('OrganizationBilling')
109

10+
/**
11+
* Get organization subscription directly by organization ID
12+
* This is for our new pattern where referenceId = organizationId
13+
*/
14+
async function getOrganizationSubscription(organizationId: string) {
15+
try {
16+
const orgSubs = await db
17+
.select()
18+
.from(subscription)
19+
.where(and(eq(subscription.referenceId, organizationId), eq(subscription.status, 'active')))
20+
.limit(1)
21+
22+
return orgSubs.length > 0 ? orgSubs[0] : null
23+
} catch (error) {
24+
logger.error('Error getting organization subscription', { error, organizationId })
25+
return null
26+
}
27+
}
28+
1129
interface OrganizationUsageData {
1230
organizationId: string
1331
organizationName: string
@@ -57,8 +75,8 @@ export async function getOrganizationBillingData(
5775

5876
const organizationData = orgRecord[0]
5977

60-
// Get organization subscription
61-
const subscription = await getHighestPrioritySubscription(organizationId)
78+
// Get organization subscription directly (referenceId = organizationId)
79+
const subscription = await getOrganizationSubscription(organizationId)
6280

6381
if (!subscription) {
6482
logger.warn('No subscription found for organization', { organizationId })
@@ -191,7 +209,7 @@ export async function updateMemberUsageLimit(
191209
}
192210

193211
// Get organization subscription to validate limit
194-
const subscription = await getHighestPrioritySubscription(organizationId)
212+
const subscription = await getOrganizationSubscription(organizationId)
195213
if (!subscription) {
196214
throw new Error('No active subscription found')
197215
}

0 commit comments

Comments
 (0)