Skip to content

Commit 95d3d99

Browse files
committed
improvement(billng): team upgrade + session management
1 parent 4da43d9 commit 95d3d99

File tree

2 files changed

+98
-47
lines changed

2 files changed

+98
-47
lines changed

apps/sim/lib/auth/auth.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2184,8 +2184,26 @@ export const auth = betterAuth({
21842184
status: subscription.status,
21852185
})
21862186

2187-
const resolvedSubscription =
2188-
await ensureOrganizationForTeamSubscription(subscription)
2187+
let resolvedSubscription = subscription
2188+
try {
2189+
resolvedSubscription = await ensureOrganizationForTeamSubscription(subscription)
2190+
} catch (orgError) {
2191+
// Critical: Log detailed error for manual investigation
2192+
// This can happen if user joined another org between checkout start and completion
2193+
logger.error(
2194+
'[onSubscriptionComplete] Failed to ensure organization for team subscription',
2195+
{
2196+
subscriptionId: subscription.id,
2197+
referenceId: subscription.referenceId,
2198+
plan: subscription.plan,
2199+
error: orgError instanceof Error ? orgError.message : String(orgError),
2200+
stack: orgError instanceof Error ? orgError.stack : undefined,
2201+
}
2202+
)
2203+
// Re-throw to signal webhook failure - Stripe will retry
2204+
// This ensures we don't leave subscriptions in broken state silently
2205+
throw orgError
2206+
}
21892207

21902208
await handleSubscriptionCreated(resolvedSubscription)
21912209

@@ -2206,8 +2224,23 @@ export const auth = betterAuth({
22062224
plan: subscription.plan,
22072225
})
22082226

2209-
const resolvedSubscription =
2210-
await ensureOrganizationForTeamSubscription(subscription)
2227+
let resolvedSubscription = subscription
2228+
try {
2229+
resolvedSubscription = await ensureOrganizationForTeamSubscription(subscription)
2230+
} catch (orgError) {
2231+
// Log but don't throw - subscription updates should still process other logic
2232+
// The subscription may have been created with user ID if org creation failed initially
2233+
logger.error(
2234+
'[onSubscriptionUpdate] Failed to ensure organization for team subscription',
2235+
{
2236+
subscriptionId: subscription.id,
2237+
referenceId: subscription.referenceId,
2238+
plan: subscription.plan,
2239+
error: orgError instanceof Error ? orgError.message : String(orgError),
2240+
}
2241+
)
2242+
// Continue with original subscription - don't block other updates
2243+
}
22112244

22122245
try {
22132246
await syncSubscriptionUsageLimits(resolvedSubscription)

apps/sim/lib/billing/organization.ts

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { db } from '@sim/db'
2-
import * as schema from '@sim/db/schema'
2+
import {
3+
member,
4+
organization,
5+
session,
6+
subscription as subscriptionTable,
7+
user,
8+
} from '@sim/db/schema'
39
import { createLogger } from '@sim/logger'
410
import { and, eq } from 'drizzle-orm'
511
import { getPlanPricing } from '@/lib/billing/core/billing'
@@ -20,16 +26,16 @@ type SubscriptionData = {
2026
*/
2127
async function getUserOwnedOrganization(userId: string): Promise<string | null> {
2228
const existingMemberships = await db
23-
.select({ organizationId: schema.member.organizationId })
24-
.from(schema.member)
25-
.where(and(eq(schema.member.userId, userId), eq(schema.member.role, 'owner')))
29+
.select({ organizationId: member.organizationId })
30+
.from(member)
31+
.where(and(eq(member.userId, userId), eq(member.role, 'owner')))
2632
.limit(1)
2733

2834
if (existingMemberships.length > 0) {
2935
const [existingOrg] = await db
30-
.select({ id: schema.organization.id })
31-
.from(schema.organization)
32-
.where(eq(schema.organization.id, existingMemberships[0].organizationId))
36+
.select({ id: organization.id })
37+
.from(organization)
38+
.where(eq(organization.id, existingMemberships[0].organizationId))
3339
.limit(1)
3440

3541
return existingOrg?.id || null
@@ -40,6 +46,8 @@ async function getUserOwnedOrganization(userId: string): Promise<string | null>
4046

4147
/**
4248
* Create a new organization and add user as owner
49+
* Uses transaction to ensure org + member are created atomically
50+
* Also updates user's active sessions to set the new org as active
4351
*/
4452
async function createOrganizationWithOwner(
4553
userId: string,
@@ -49,31 +57,36 @@ async function createOrganizationWithOwner(
4957
): Promise<string> {
5058
const orgId = `org_${crypto.randomUUID()}`
5159

52-
const [newOrg] = await db
53-
.insert(schema.organization)
54-
.values({
60+
await db.transaction(async (tx) => {
61+
await tx.insert(organization).values({
5562
id: orgId,
5663
name: organizationName,
5764
slug: organizationSlug,
5865
metadata,
5966
})
60-
.returning({ id: schema.organization.id })
61-
62-
// Add user as owner/admin of the organization
63-
await db.insert(schema.member).values({
64-
id: crypto.randomUUID(),
65-
userId: userId,
66-
organizationId: newOrg.id,
67-
role: 'owner',
67+
68+
await tx.insert(member).values({
69+
id: crypto.randomUUID(),
70+
userId: userId,
71+
organizationId: orgId,
72+
role: 'owner',
73+
})
6874
})
6975

76+
const updatedSessions = await db
77+
.update(session)
78+
.set({ activeOrganizationId: orgId })
79+
.where(eq(session.userId, userId))
80+
.returning({ id: session.id })
81+
7082
logger.info('Created organization with owner', {
7183
userId,
72-
organizationId: newOrg.id,
84+
organizationId: orgId,
7385
organizationName,
86+
sessionsUpdated: updatedSessions.length,
7487
})
7588

76-
return newOrg.id
89+
return orgId
7790
}
7891

7992
export async function createOrganizationForTeamPlan(
@@ -132,12 +145,12 @@ export async function ensureOrganizationForTeamSubscription(
132145

133146
const existingMembership = await db
134147
.select({
135-
id: schema.member.id,
136-
organizationId: schema.member.organizationId,
137-
role: schema.member.role,
148+
id: member.id,
149+
organizationId: member.organizationId,
150+
role: member.role,
138151
})
139-
.from(schema.member)
140-
.where(eq(schema.member.userId, userId))
152+
.from(member)
153+
.where(eq(member.userId, userId))
141154
.limit(1)
142155

143156
if (existingMembership.length > 0) {
@@ -149,9 +162,14 @@ export async function ensureOrganizationForTeamSubscription(
149162
})
150163

151164
await db
152-
.update(schema.subscription)
165+
.update(subscriptionTable)
153166
.set({ referenceId: membership.organizationId })
154-
.where(eq(schema.subscription.id, subscription.id))
167+
.where(eq(subscriptionTable.id, subscription.id))
168+
169+
await db
170+
.update(session)
171+
.set({ activeOrganizationId: membership.organizationId })
172+
.where(eq(session.userId, userId))
155173

156174
return { ...subscription, referenceId: membership.organizationId }
157175
}
@@ -165,9 +183,9 @@ export async function ensureOrganizationForTeamSubscription(
165183
}
166184

167185
const [userData] = await db
168-
.select({ name: schema.user.name, email: schema.user.email })
169-
.from(schema.user)
170-
.where(eq(schema.user.id, userId))
186+
.select({ name: user.name, email: user.email })
187+
.from(user)
188+
.where(eq(user.id, userId))
171189
.limit(1)
172190

173191
const orgId = await createOrganizationForTeamPlan(
@@ -177,9 +195,9 @@ export async function ensureOrganizationForTeamSubscription(
177195
)
178196

179197
await db
180-
.update(schema.subscription)
198+
.update(subscriptionTable)
181199
.set({ referenceId: orgId })
182-
.where(eq(schema.subscription.id, subscription.id))
200+
.where(eq(subscriptionTable.id, subscription.id))
183201

184202
logger.info('Created organization and updated subscription referenceId', {
185203
subscriptionId: subscription.id,
@@ -204,9 +222,9 @@ export async function syncSubscriptionUsageLimits(subscription: SubscriptionData
204222

205223
// Check if this is a user or organization subscription
206224
const users = await db
207-
.select({ id: schema.user.id })
208-
.from(schema.user)
209-
.where(eq(schema.user.id, subscription.referenceId))
225+
.select({ id: user.id })
226+
.from(user)
227+
.where(eq(user.id, subscription.referenceId))
210228
.limit(1)
211229

212230
if (users.length > 0) {
@@ -230,9 +248,9 @@ export async function syncSubscriptionUsageLimits(subscription: SubscriptionData
230248

231249
// Only set if not already set or if updating to a higher value based on seats
232250
const orgData = await db
233-
.select({ orgUsageLimit: schema.organization.orgUsageLimit })
234-
.from(schema.organization)
235-
.where(eq(schema.organization.id, organizationId))
251+
.select({ orgUsageLimit: organization.orgUsageLimit })
252+
.from(organization)
253+
.where(eq(organization.id, organizationId))
236254
.limit(1)
237255

238256
const currentLimit =
@@ -243,12 +261,12 @@ export async function syncSubscriptionUsageLimits(subscription: SubscriptionData
243261
// Update if no limit set, or if new seat-based minimum is higher
244262
if (currentLimit < orgLimit) {
245263
await db
246-
.update(schema.organization)
264+
.update(organization)
247265
.set({
248266
orgUsageLimit: orgLimit.toFixed(2),
249267
updatedAt: new Date(),
250268
})
251-
.where(eq(schema.organization.id, organizationId))
269+
.where(eq(organization.id, organizationId))
252270

253271
logger.info('Set organization usage limit for team plan', {
254272
organizationId,
@@ -262,9 +280,9 @@ export async function syncSubscriptionUsageLimits(subscription: SubscriptionData
262280

263281
// Sync usage limits for all members
264282
const members = await db
265-
.select({ userId: schema.member.userId })
266-
.from(schema.member)
267-
.where(eq(schema.member.organizationId, organizationId))
283+
.select({ userId: member.userId })
284+
.from(member)
285+
.where(eq(member.organizationId, organizationId))
268286

269287
if (members.length > 0) {
270288
for (const member of members) {

0 commit comments

Comments
 (0)