Skip to content

Commit 2bb951d

Browse files
committed
progress
1 parent c1e39ac commit 2bb951d

27 files changed

Lines changed: 154 additions & 407 deletions

File tree

apps/sim/app/api/billing/switch-plan/route.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,6 @@ export async function POST(request: NextRequest) {
9494
)
9595
}
9696

97-
// Any subscription whose referenceId is an organization (team,
98-
// enterprise, or `pro_*` attached to an org) requires org admin/owner
99-
// to change the plan.
10097
if (isOrgScopedSubscription(sub, userId)) {
10198
const hasPermission = await isOrganizationOwnerOrAdmin(userId, sub.referenceId)
10299
if (!hasPermission) {

apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,6 @@ export async function PUT(
356356
.limit(1)
357357

358358
const orgSub = orgSubs[0]
359-
// Any paid subscription attached to the org triggers the
360-
// "snapshot my personal Pro usage and cancel it" flow — includes
361-
// `pro_*` plans transferred to the org, not just team/enterprise.
362359
const orgIsPaid = orgSub && isPaid(orgSub.plan)
363360

364361
if (orgIsPaid) {
@@ -414,23 +411,14 @@ export async function PUT(
414411
}
415412
}
416413

417-
// Transfer the joining user's accumulated personal storage
418-
// bytes into the organization's pool. After this point
419-
// `isOrgScopedSubscription` returns true for the user, so
420-
// `getUserStorageUsage`/`incrementStorageUsage`/`decrementStorageUsage`
421-
// all route through `organization.storageUsedBytes`. Without
422-
// this transfer, pre-join bytes would be orphaned on the
423-
// user's row and subsequent decrements (deleting a pre-join
424-
// file after joining) would wrongly reduce the org pool.
425-
//
426-
// `.for('update')` acquires a row-level write lock on the
427-
// user's `user_stats` row so a concurrent
428-
// `incrementStorageUsage`/`decrementStorageUsage` (from
429-
// another tab, a scheduled run, an API-key writer, etc.)
430-
// blocks until this transaction commits — otherwise Postgres
431-
// READ COMMITTED would let a write land between the snapshot
432-
// SELECT and the zero UPDATE, silently dropping those bytes.
433-
// Mirrors the bulk version in `syncSubscriptionUsageLimits`.
414+
// Transfer the joining user's pre-join storage bytes into
415+
// the org pool — after this point storage reads/writes route
416+
// through `organization.storageUsedBytes`, so bytes left on
417+
// `user_stats` would be orphaned (and a later decrement from
418+
// deleting a pre-join file would wrongly reduce the org
419+
// pool). `.for('update')` row-locks `user_stats` so a
420+
// concurrent increment/decrement can't land between the
421+
// SELECT and the zero UPDATE and get silently dropped.
434422
const storageRows = await tx
435423
.select({ storageUsedBytes: userStats.storageUsedBytes })
436424
.from(userStats)

apps/sim/app/api/v1/admin/credits/route.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,7 @@ export const POST = withAdminAuth(async (request) => {
111111
const plan = userSubscription.plan
112112
let seats: number | null = null
113113

114-
// Route admin credits to the subscription's actual entity. Any sub whose
115-
// `referenceId` is an org gets credited to the org pool — including
116-
// `pro_*` plans transferred to an org.
114+
// Route admin credits to the subscription's entity (org if org-scoped).
117115
if (isOrgScopedSubscription(userSubscription, resolvedUserId)) {
118116
entityType = 'organization'
119117
entityId = userSubscription.referenceId

apps/sim/app/api/v1/admin/users/[id]/billing/route.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,6 @@ export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) =
155155
.limit(1)
156156

157157
const userSubscription = await getHighestPrioritySubscription(userId)
158-
// True for any user whose effective subscription is attached to an org
159-
// (team, enterprise, or `pro_*` transferred to an org). They have no
160-
// individual usage limit — the org cap governs.
161158
const isOrgScopedMember = isOrgScopedSubscription(userSubscription, userId)
162159

163160
const [orgMembership] = await db

apps/sim/app/workspace/[workspaceId]/settings/components/subscription/subscription-permissions.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ export function getSubscriptionPermissions(
3939
const { isFree, isPro, isTeam, isEnterprise, isPaid, isOrgScoped } = subscription
4040
const { isTeamAdmin } = userRole
4141

42-
// "Org-scoped non-admin" collapses all the "team member" behaviors
43-
// (hidden edit, hidden cancel, no upgrade plans, pooled view, etc.).
44-
// This includes members of `pro_*` orgs that aren't admins/owners.
42+
// Non-admin org members see the "team member" view: no edit / no cancel
43+
// / no upgrade, pooled usage display.
4544
const orgMemberOnly = isOrgScoped && !isTeamAdmin
4645
const orgAdminOrSolo = !isOrgScoped || isTeamAdmin
4746

@@ -53,17 +52,9 @@ export function getSubscriptionPermissions(
5352
canUpgradeToTeam: isFree || (isPro && !isOrgScoped),
5453
canViewEnterprise: !isEnterprise && !orgMemberOnly,
5554
canManageTeam: isOrgScoped && isTeamAdmin && !isEnterprise,
56-
// Edit the limit when: paid plan (not free, not enterprise) AND either
57-
// personally-scoped or acting as an org admin/owner.
5855
canEditUsageLimit: (isFree || (isPaid && !isEnterprise)) && orgAdminOrSolo,
5956
canCancelSubscription: isPaid && !isEnterprise && orgAdminOrSolo,
6057
showTeamMemberView: orgMemberOnly,
61-
// Personal Pro can upgrade to team/enterprise. Any org admin/owner on
62-
// a non-enterprise plan can upgrade to enterprise — covers team admins
63-
// AND admins of `pro_*` plans attached to an org (previously missed by
64-
// the narrower `isTeam && isTeamAdmin` check, which left pro-on-org
65-
// admins with no upgrade path even though `getVisiblePlans` listed
66-
// enterprise for them).
6758
showUpgradePlans:
6859
(isFree || (isPro && !isOrgScoped) || (isOrgScoped && isTeamAdmin)) && !isEnterprise,
6960
isEnterpriseMember,
@@ -79,22 +70,13 @@ export function getVisiblePlans(
7970
const { isFree, isPro, isEnterprise, isOrgScoped } = subscription
8071
const { isTeamAdmin } = userRole
8172

82-
// Free users see all plans
8373
if (isFree) {
8474
plans.push('pro', 'team', 'enterprise')
85-
}
86-
// Personally-scoped Pro: can upgrade to team or enterprise
87-
else if (isPro && !isOrgScoped) {
75+
} else if (isPro && !isOrgScoped) {
8876
plans.push('team', 'enterprise')
89-
}
90-
// Org admin/owner on a non-enterprise plan: enterprise is the only
91-
// remaining upgrade. Covers team admins and `pro_*`-on-org admins.
92-
// Explicitly excludes enterprise admins (already on the top tier) so
93-
// this stays consistent with `showUpgradePlans`.
94-
else if (isOrgScoped && isTeamAdmin && !isEnterprise) {
77+
} else if (isOrgScoped && isTeamAdmin && !isEnterprise) {
9578
plans.push('enterprise')
9679
}
97-
// Org members, Enterprise users see no plans
9880

9981
return plans
10082
}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -335,20 +335,11 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
335335

336336
const contextMenuItems = useMemo(
337337
() => ({
338-
// Set limit: anyone who can manage billing on a paid non-enterprise
339-
// plan. `userCanManageBilling` already enforces owner/admin for
340-
// org-scoped subs (including `pro_*` attached to an org), so members
341-
// of an org don't see this.
342338
showSetLimit: userCanManageBilling && !isFree && !isEnterprise,
343-
// Upgrade to Pro: Only for free users
344339
showUpgradeToPro: isFree,
345-
// Upgrade to Team: Free users and Pro users with billing permission
346340
showUpgradeToTeam: isFree || (isPro && userCanManageBilling),
347-
// Manage seats: Only for Team admins
348341
showManageSeats: isTeam && userCanManageBilling,
349-
// Upgrade to Enterprise: Only for Team admins (not free, not pro, not enterprise)
350342
showUpgradeToEnterprise: isTeam && userCanManageBilling,
351-
// Contact support: Only for Enterprise admins
352343
showContactSupport: isEnterprise && userCanManageBilling,
353344
onSetLimit: handleSetLimit,
354345
onUpgradeToPro: handleUpgradeToPro,

apps/sim/lib/auth/auth.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2905,8 +2905,6 @@ export const auth = betterAuth({
29052905
)
29062906
}
29072907

2908-
// Persist the Stripe-resolved plan name to our DB row before
2909-
// any downstream work so subsequent reads see the fresh plan.
29102908
await syncSubscriptionPlan(subscription.id, subscription.plan, planFromStripe)
29112909

29122910
const subscriptionForOrg = {
@@ -2987,10 +2985,6 @@ export const auth = betterAuth({
29872985
)
29882986
}
29892987

2990-
// Sync the DB's `plan` column to whatever Stripe currently
2991-
// says. better-auth's upgrade flow updates Stripe price,
2992-
// seats, and referenceId, but historically left `plan`
2993-
// stale (see `pro_6000` attached to an org in prod).
29942988
await syncSubscriptionPlan(subscription.id, subscription.plan, planFromStripe)
29952989

29962990
const subscriptionForOrg = {

apps/sim/lib/billing/calculations/usage-monitor.ts

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,9 @@ interface UsageData {
2727
currentUsage: number
2828
limit: number
2929
/**
30-
* Whether the returned `currentUsage`/`limit` represent the user's
31-
* individual slice (`'user'`) or the organization's pooled total and cap
32-
* (`'organization'`). When `isExceeded` is driven by an org pool check,
33-
* the pooled values are surfaced here so downstream error messages are
34-
* accurate.
30+
* Whether the returned values are this user's individual slice or the
31+
* organization's pooled total/cap. When an org pool is the blocker,
32+
* the pooled values are surfaced here so error messages reflect it.
3533
*/
3634
scope: 'user' | 'organization'
3735
/** Present only when `scope === 'organization'`. */
@@ -47,9 +45,7 @@ export async function checkUsageStatus(
4745
preloadedSubscription?: HighestPrioritySubscription
4846
): Promise<UsageData> {
4947
try {
50-
// If billing is disabled, always return permissive limits
5148
if (!isBillingEnabled) {
52-
// Get actual usage from the database for display purposes
5349
const statsRecords = await db.select().from(userStats).where(eq(userStats.userId, userId))
5450
const currentUsage =
5551
statsRecords.length > 0
@@ -67,20 +63,14 @@ export async function checkUsageStatus(
6763
}
6864
}
6965

70-
// Resolve the highest-priority subscription once so every branch below
71-
// agrees on scope. The caller may have already loaded it.
7266
const sub =
7367
preloadedSubscription !== undefined
7468
? preloadedSubscription
7569
: await getHighestPrioritySubscription(userId)
7670

77-
// Primary limit from user_stats or org (routed by scope).
7871
const limit = await getUserUsageLimit(userId, sub)
7972
logger.info('Using stored usage limit', { userId, limit })
8073

81-
// Usage baseline.
82-
// Org-scoped: pooled sum across all org members (with pooled refresh).
83-
// Personal: user's own currentPeriodCost (with personal refresh).
8474
const subIsOrgScoped = isOrgScopedSubscription(sub, userId)
8575

8676
let currentUsage = 0
@@ -114,15 +104,14 @@ export async function checkUsageStatus(
114104
periodStart: sub.periodStart,
115105
periodEnd: sub.periodEnd ?? null,
116106
planDollars,
117-
seats: sub.seats ?? 1,
107+
seats: sub.seats || 1,
118108
})
119109
pooled = Math.max(0, pooled - refresh)
120110
}
121111
}
122112
}
123113
currentUsage = pooled
124114
} else {
125-
// Personally-scoped: use this user's own row (defensive default 0).
126115
const statsRecords = await db
127116
.select()
128117
.from(userStats)
@@ -161,24 +150,22 @@ export async function checkUsageStatus(
161150
currentUsage = Math.max(0, rawUsage - refresh)
162151
}
163152

164-
// Defense-in-depth: even when the user's priority sub is personal, they
165-
// may still be a member of an org whose pool has blown its cap (e.g.
166-
// billed-account scenarios). Enforce every entitled-org cap they belong
167-
// to and override the returned values when one is actually blocking —
168-
// that way the error message surfaces the org number, not personal.
153+
// Defense-in-depth: enforce every entitled org cap the user belongs
154+
// to, even when their priority sub is personal. If a secondary org
155+
// pool is blocking, surface its numbers so the error message does
156+
// not quote personal usage while enforcing an org cap.
169157
try {
170158
const memberships = await db
171159
.select({ organizationId: member.organizationId })
172160
.from(member)
173161
.where(eq(member.userId, userId))
174162

175163
for (const m of memberships) {
176-
// Skip the org the primary sub is already keyed to; we've already
177-
// computed pooled usage against its cap above.
164+
// Already handled above as the primary org.
178165
if (subIsOrgScoped && sub && sub.referenceId === m.organizationId) continue
179166

180-
// Pull the full org subscription row — refresh math below needs
181-
// THAT org's plan/period/seats, not the caller's primary sub.
167+
// Refresh math below needs THIS org's plan/period/seats, not
168+
// the caller's primary sub (which may be a personal Pro).
182169
const [orgSub] = await db
183170
.select({
184171
plan: subscription.plan,
@@ -221,9 +208,6 @@ export async function checkUsageStatus(
221208
}
222209
}
223210

224-
// Refresh is driven by the org's OWN subscription period, plan
225-
// dollars, and seats — not the caller's primary sub (which may be
226-
// a personal Pro in this branch).
227211
if (isPaid(orgSub.plan) && orgSub.periodStart) {
228212
const planDollars = getPlanTierDollars(orgSub.plan)
229213
if (planDollars > 0) {
@@ -233,7 +217,7 @@ export async function checkUsageStatus(
233217
periodStart: orgSub.periodStart,
234218
periodEnd: orgSub.periodEnd ?? null,
235219
planDollars,
236-
seats: orgSub.seats ?? 1,
220+
seats: orgSub.seats || 1,
237221
})
238222
pooledUsage = Math.max(0, pooledUsage - orgRefreshDeduction)
239223
}
@@ -295,9 +279,9 @@ export async function checkUsageStatus(
295279
return {
296280
percentUsed: 100,
297281
isWarning: false,
298-
isExceeded: true, // Block execution when we can't determine status
282+
isExceeded: true,
299283
currentUsage: 0,
300-
limit: 0, // Zero limit forces blocking
284+
limit: 0,
301285
scope: 'user',
302286
organizationId: null,
303287
}
@@ -310,22 +294,19 @@ export async function checkUsageStatus(
310294
*/
311295
export async function checkAndNotifyUsage(userId: string): Promise<void> {
312296
try {
313-
// Skip usage notifications if billing is disabled
314297
if (!isBillingEnabled) {
315298
return
316299
}
317300

318301
const usageData = await checkUsageStatus(userId)
319302

320303
if (usageData.isExceeded) {
321-
// User has exceeded their limit
322304
logger.warn('User has exceeded usage limits', {
323305
userId,
324306
usage: usageData.currentUsage,
325307
limit: usageData.limit,
326308
})
327309

328-
// Dispatch event to show a UI notification
329310
if (typeof window !== 'undefined') {
330311
window.dispatchEvent(
331312
new CustomEvent('usage-exceeded', {
@@ -334,15 +315,13 @@ export async function checkAndNotifyUsage(userId: string): Promise<void> {
334315
)
335316
}
336317
} else if (usageData.isWarning) {
337-
// User is approaching their limit
338318
logger.info('User approaching usage limits', {
339319
userId,
340320
usage: usageData.currentUsage,
341321
limit: usageData.limit,
342322
percent: usageData.percentUsed,
343323
})
344324

345-
// Dispatch event to show a UI notification
346325
if (typeof window !== 'undefined') {
347326
window.dispatchEvent(
348327
new CustomEvent('usage-warning', {
@@ -383,7 +362,6 @@ export async function checkServerSideUsageLimits(
383362

384363
logger.info('Server-side checking usage limits for user', { userId })
385364

386-
// Check user's own blocked status
387365
const stats = await db
388366
.select({
389367
blocked: userStats.billingBlocked,
@@ -413,14 +391,12 @@ export async function checkServerSideUsageLimits(
413391
}
414392
}
415393

416-
// Check if user is in an org where the owner is blocked
417394
const memberships = await db
418395
.select({ organizationId: member.organizationId })
419396
.from(member)
420397
.where(eq(member.userId, userId))
421398

422399
for (const m of memberships) {
423-
// Find the owner of this org
424400
const owners = await db
425401
.select({ userId: member.userId })
426402
.from(member)
@@ -479,9 +455,9 @@ export async function checkServerSideUsageLimits(
479455
})
480456

481457
return {
482-
isExceeded: true, // Block execution when we can't determine limits
458+
isExceeded: true,
483459
currentUsage: 0,
484-
limit: 0, // Zero limit forces blocking
460+
limit: 0,
485461
message:
486462
error instanceof Error && error.message.includes('No user stats record found')
487463
? 'User account not properly initialized. Please contact support.'

apps/sim/lib/billing/client/upgrade.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,6 @@ export function useSubscriptionUpgrade() {
6565
)
6666

6767
if (existingOrg) {
68-
// Check if this org already has ANY paid subscription — team,
69-
// enterprise, or a `pro_*` that's been transferred to the org.
70-
// Upgrading again would create a duplicate.
7168
const existingOrgSub = allSubscriptions.find(
7269
(sub: any) =>
7370
hasPaidSubscriptionStatus(sub.status) &&

0 commit comments

Comments
 (0)