Skip to content

Commit d22b578

Browse files
fix(enterprise-plan): seats should be taken from metadata (#2200)
* fix(enterprise): seats need to be picked up from metadata not column * fix env var access * fix user avatar
1 parent 8e7d8c9 commit d22b578

File tree

15 files changed

+101
-66
lines changed

15 files changed

+101
-66
lines changed

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/subscription.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
197197
subscriptionData?.data?.status === 'active',
198198
plan: subscriptionData?.data?.plan || 'free',
199199
status: subscriptionData?.data?.status || 'inactive',
200-
seats: subscriptionData?.data?.seats || 1,
200+
seats: organizationBillingData?.totalSeats ?? 0,
201201
}
202202

203203
const usage = {
@@ -373,7 +373,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
373373
onBadgeClick={handleBadgeClick}
374374
seatsText={
375375
permissions.canManageTeam || subscription.isEnterprise
376-
? `${organizationBillingData?.totalSeats || subscription.seats || 1} seats`
376+
? `${subscription.seats} seats`
377377
: undefined
378378
}
379379
current={

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,20 @@ import { Skeleton } from '@/components/ui/skeleton'
33
import { checkEnterprisePlan } from '@/lib/billing/subscriptions/utils'
44
import { cn } from '@/lib/core/utils/cn'
55

6-
const PILL_COUNT = 8
7-
86
type Subscription = {
97
id: string
108
plan: string
119
status: string
12-
seats?: number
1310
referenceId: string
1411
cancelAtPeriodEnd?: boolean
1512
periodEnd?: number | Date
1613
trialEnd?: number | Date
17-
metadata?: any
1814
}
1915

2016
interface TeamSeatsOverviewProps {
2117
subscriptionData: Subscription | null
2218
isLoadingSubscription: boolean
19+
totalSeats: number
2320
usedSeats: number
2421
isLoading: boolean
2522
onConfirmTeamUpgrade: (seats: number) => Promise<void>
@@ -55,6 +52,7 @@ function TeamSeatsSkeleton() {
5552
export function TeamSeatsOverview({
5653
subscriptionData,
5754
isLoadingSubscription,
55+
totalSeats,
5856
usedSeats,
5957
isLoading,
6058
onConfirmTeamUpgrade,
@@ -78,7 +76,7 @@ export function TeamSeatsOverview({
7876
<Button
7977
variant='primary'
8078
onClick={() => {
81-
onConfirmTeamUpgrade(2) // Start with 2 seats as default
79+
onConfirmTeamUpgrade(2)
8280
}}
8381
disabled={isLoading}
8482
>
@@ -89,7 +87,6 @@ export function TeamSeatsOverview({
8987
)
9088
}
9189

92-
const totalSeats = subscriptionData.seats || 0
9390
const isEnterprise = checkEnterprisePlan(subscriptionData)
9491

9592
return (

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/team-seats/team-seats.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
Tooltip,
1414
} from '@/components/emcn'
1515
import { DEFAULT_TEAM_TIER_COST_LIMIT } from '@/lib/billing/constants'
16-
import { env } from '@/lib/core/config/env'
1716

1817
interface TeamSeatsProps {
1918
open: boolean
@@ -52,7 +51,7 @@ export function TeamSeats({
5251
}
5352
}, [open, initialSeats])
5453

55-
const costPerSeat = env.TEAM_TIER_COST_LIMIT ?? DEFAULT_TEAM_TIER_COST_LIMIT
54+
const costPerSeat = DEFAULT_TEAM_TIER_COST_LIMIT
5655
const totalMonthlyCost = selectedSeats * costPerSeat
5756
const costChange = currentSeats ? (selectedSeats - currentSeats) * costPerSeat : 0
5857

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/team-usage/team-usage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ export function TeamUsage({ hasAdminAccess }: TeamUsageProps) {
6363
return null
6464
}
6565

66-
const currentUsage = billingData.totalCurrentUsage || 0
67-
const currentCap = billingData.totalUsageLimit || billingData.minimumBillingAmount || 0
68-
const minimumBilling = billingData.minimumBillingAmount || 0
69-
const seatsCount = billingData.seatsCount || 1
66+
const currentUsage = billingData.totalCurrentUsage ?? 0
67+
const currentCap = billingData.totalUsageLimit ?? billingData.minimumBillingAmount ?? 0
68+
const minimumBilling = billingData.minimumBillingAmount ?? 0
69+
const seatsCount = billingData.seatsCount ?? 0
7070
const percentUsed =
7171
currentCap > 0 ? Math.round(Math.min((currentUsage / currentCap) * 100, 100)) : 0
7272
const status: 'ok' | 'warning' | 'exceeded' =

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/team-management.tsx

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Skeleton } from '@/components/ui'
33
import { useSession } from '@/lib/auth/auth-client'
44
import { DEFAULT_TEAM_TIER_COST_LIMIT } from '@/lib/billing/constants'
55
import { checkEnterprisePlan } from '@/lib/billing/subscriptions/utils'
6-
import { env } from '@/lib/core/config/env'
76
import { createLogger } from '@/lib/logs/console/logger'
87
import {
98
generateSlug,
@@ -23,6 +22,7 @@ import {
2322
useCreateOrganization,
2423
useInviteMember,
2524
useOrganization,
25+
useOrganizationBilling,
2626
useOrganizationSubscription,
2727
useOrganizations,
2828
useRemoveMember,
@@ -56,6 +56,8 @@ export function TeamManagement() {
5656
error: subscriptionError,
5757
} = useOrganizationSubscription(activeOrganization?.id || '')
5858

59+
const { data: organizationBillingData } = useOrganizationBilling(activeOrganization?.id || '')
60+
5961
const inviteMutation = useInviteMember()
6062
const removeMemberMutation = useRemoveMember()
6163
const updateSeatsMutation = useUpdateSeats()
@@ -89,6 +91,7 @@ export function TeamManagement() {
8991
const userRole = getUserRole(organization, session?.user?.email)
9092
const adminOrOwner = isAdminOrOwner(organization, session?.user?.email)
9193
const usedSeats = getUsedSeats(organization)
94+
const totalSeats = organizationBillingData?.data?.totalSeats ?? 0
9295

9396
useEffect(() => {
9497
if ((hasTeamPlan || hasEnterprisePlan) && session?.user?.name && !orgName) {
@@ -238,11 +241,11 @@ export function TeamManagement() {
238241
}, [session?.user?.id, activeOrganization?.id, subscriptionData, usedSeats, updateSeatsMutation])
239242

240243
const handleAddSeatDialog = useCallback(() => {
241-
if (subscriptionData) {
242-
setNewSeatCount((subscriptionData.seats || 1) + 1)
244+
if (subscriptionData && !checkEnterprisePlan(subscriptionData)) {
245+
setNewSeatCount(totalSeats + 1)
243246
setIsAddSeatDialogOpen(true)
244247
}
245-
}, [subscriptionData?.seats])
248+
}, [subscriptionData, totalSeats])
246249

247250
const confirmAddSeats = useCallback(
248251
async (selectedSeats?: number) => {
@@ -370,6 +373,7 @@ export function TeamManagement() {
370373
<TeamSeatsOverview
371374
subscriptionData={subscriptionData || null}
372375
isLoadingSubscription={isLoadingSubscription}
376+
totalSeats={totalSeats}
373377
usedSeats={usedSeats.used}
374378
isLoading={isLoading}
375379
onConfirmTeamUpgrade={confirmTeamUpgrade}
@@ -394,8 +398,8 @@ export function TeamManagement() {
394398
onLoadUserWorkspaces={async () => {}} // No-op: data is auto-loaded by React Query
395399
onWorkspaceToggle={handleWorkspaceToggle}
396400
inviteSuccess={inviteSuccess}
397-
availableSeats={Math.max(0, (subscriptionData?.seats || 0) - usedSeats.used)}
398-
maxSeats={subscriptionData?.seats || 0}
401+
availableSeats={Math.max(0, totalSeats - usedSeats.used)}
402+
maxSeats={totalSeats}
399403
invitationError={inviteMutation.error}
400404
isLoadingWorkspaces={isLoadingWorkspaces}
401405
/>
@@ -481,9 +485,8 @@ export function TeamManagement() {
481485
<ul className='ml-4 list-disc space-y-[8px] text-[var(--text-muted)] text-xs'>
482486
<li>
483487
Your team is billed a minimum of $
484-
{(subscriptionData?.seats || 0) *
485-
(env.TEAM_TIER_COST_LIMIT ?? DEFAULT_TEAM_TIER_COST_LIMIT)}
486-
/month for {subscriptionData?.seats || 0} licensed seats
488+
{(subscriptionData?.seats ?? 0) * DEFAULT_TEAM_TIER_COST_LIMIT}
489+
/month for {subscriptionData?.seats ?? 0} licensed seats
487490
</li>
488491
<li>All team member usage is pooled together from a shared limit</li>
489492
<li>
@@ -528,23 +531,25 @@ export function TeamManagement() {
528531
}
529532
/>
530533

531-
<TeamSeats
532-
open={isAddSeatDialogOpen}
533-
onOpenChange={setIsAddSeatDialogOpen}
534-
title='Add Team Seats'
535-
description={`Each seat costs $${env.TEAM_TIER_COST_LIMIT ?? DEFAULT_TEAM_TIER_COST_LIMIT}/month and provides $${env.TEAM_TIER_COST_LIMIT ?? DEFAULT_TEAM_TIER_COST_LIMIT} in monthly inference credits. Adjust the number of licensed seats for your team.`}
536-
currentSeats={subscriptionData?.seats || 1}
537-
initialSeats={newSeatCount}
538-
isLoading={isUpdatingSeats}
539-
error={updateSeatsMutation.error}
540-
onConfirm={async (selectedSeats: number) => {
541-
setNewSeatCount(selectedSeats)
542-
await confirmAddSeats(selectedSeats)
543-
}}
544-
confirmButtonText='Update Seats'
545-
showCostBreakdown={true}
546-
isCancelledAtPeriodEnd={subscriptionData?.cancelAtPeriodEnd}
547-
/>
534+
{subscriptionData && !checkEnterprisePlan(subscriptionData) && (
535+
<TeamSeats
536+
open={isAddSeatDialogOpen}
537+
onOpenChange={setIsAddSeatDialogOpen}
538+
title='Add Team Seats'
539+
description={`Each seat costs $${DEFAULT_TEAM_TIER_COST_LIMIT}/month and provides $${DEFAULT_TEAM_TIER_COST_LIMIT} in monthly inference credits. Adjust the number of licensed seats for your team.`}
540+
currentSeats={totalSeats}
541+
initialSeats={newSeatCount}
542+
isLoading={isUpdatingSeats}
543+
error={updateSeatsMutation.error}
544+
onConfirm={async (selectedSeats: number) => {
545+
setNewSeatCount(selectedSeats)
546+
await confirmAddSeats(selectedSeats)
547+
}}
548+
confirmButtonText='Update Seats'
549+
showCostBreakdown={true}
550+
isCancelledAtPeriodEnd={subscriptionData?.cancelAtPeriodEnd}
551+
/>
552+
)}
548553
</div>
549554
)
550555
}

apps/sim/components/user-avatar/user-avatar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function UserAvatar({
5555
sizes={`${size}px`}
5656
className='object-cover'
5757
referrerPolicy='no-referrer'
58-
unoptimized={avatarUrl.startsWith('http')}
58+
unoptimized
5959
onError={() => setImageError(true)}
6060
/>
6161
) : (

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export async function checkUsageStatus(userId: string): Promise<UsageData> {
115115
const orgSub = await getOrganizationSubscription(org.id)
116116
if (orgSub?.seats) {
117117
const { basePrice } = getPlanPricing(orgSub.plan)
118-
orgCap = (orgSub.seats || 1) * basePrice
118+
orgCap = (orgSub.seats ?? 0) * basePrice
119119
} else {
120120
// If no subscription, use team default
121121
const { basePrice } = getPlanPricing('team')

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,6 @@ export function isAtLeastTeam(subscriptionData: SubscriptionData | null | undefi
9696
return status.isTeam || status.isEnterprise
9797
}
9898

99-
/**
100-
* Check if user can upgrade
101-
*/
10299
export function canUpgrade(subscriptionData: SubscriptionData | null | undefined): boolean {
103100
const status = getSubscriptionStatus(subscriptionData)
104101
return status.plan === 'free' || status.plan === 'pro'

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export async function calculateSubscriptionOverage(sub: {
144144

145145
const totalUsageWithDeparted = totalTeamUsage + departedUsage
146146
const { basePrice } = getPlanPricing(sub.plan)
147-
const baseSubscriptionAmount = (sub.seats || 1) * basePrice
147+
const baseSubscriptionAmount = (sub.seats ?? 0) * basePrice
148148
totalOverage = Math.max(0, totalUsageWithDeparted - baseSubscriptionAmount)
149149

150150
logger.info('Calculated team overage', {
@@ -286,7 +286,7 @@ export async function getSimplifiedBillingSummary(
286286

287287
const { basePrice: basePricePerSeat } = getPlanPricing(subscription.plan)
288288
// Use licensed seats from Stripe as source of truth
289-
const licensedSeats = subscription.seats || 1
289+
const licensedSeats = subscription.seats ?? 0
290290
const totalBasePrice = basePricePerSeat * licensedSeats // Based on Stripe subscription
291291

292292
let totalCurrentUsage = 0

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { db } from '@sim/db'
22
import { member, organization, subscription, user, userStats } from '@sim/db/schema'
33
import { and, eq } from 'drizzle-orm'
44
import { getPlanPricing } from '@/lib/billing/core/billing'
5-
import { getFreeTierLimit } from '@/lib/billing/subscriptions/utils'
5+
import { getEffectiveSeats, getFreeTierLimit } from '@/lib/billing/subscriptions/utils'
66
import { createLogger } from '@/lib/logs/console/logger'
77

88
const logger = createLogger('OrganizationBilling')
@@ -133,9 +133,12 @@ export async function getOrganizationBillingData(
133133
// Get per-seat pricing for the plan
134134
const { basePrice: pricePerSeat } = getPlanPricing(subscription.plan)
135135

136-
// Use Stripe subscription seats as source of truth
137-
// Ensure we always have at least 1 seat (protect against 0 or falsy values)
138-
const licensedSeats = Math.max(subscription.seats || 1, 1)
136+
const licensedSeats = subscription.seats ?? 0
137+
138+
// For seat count used in UI (invitations, team management):
139+
// Team: seats column (Stripe quantity)
140+
// Enterprise: metadata.seats (allocated seats, not Stripe quantity which is always 1)
141+
const effectiveSeats = getEffectiveSeats(subscription)
139142

140143
// Calculate minimum billing amount
141144
let minimumBillingAmount: number
@@ -174,9 +177,9 @@ export async function getOrganizationBillingData(
174177
organizationName: organizationData.name || '',
175178
subscriptionPlan: subscription.plan,
176179
subscriptionStatus: subscription.status || 'inactive',
177-
totalSeats: Math.max(subscription.seats || 1, 1),
180+
totalSeats: effectiveSeats, // Uses metadata.seats for enterprise, seats column for team
178181
usedSeats: members.length,
179-
seatsCount: licensedSeats,
182+
seatsCount: licensedSeats, // Used for billing calculations (Stripe quantity)
180183
totalCurrentUsage: roundCurrency(totalCurrentUsage),
181184
totalUsageLimit: roundCurrency(totalUsageLimit),
182185
minimumBillingAmount: roundCurrency(minimumBillingAmount),
@@ -232,9 +235,8 @@ export async function updateOrganizationUsageLimit(
232235
}
233236
}
234237

235-
// Team plans have minimum based on seats
236238
const { basePrice } = getPlanPricing(subscription.plan)
237-
const minimumLimit = Math.max(subscription.seats || 1, 1) * basePrice
239+
const minimumLimit = (subscription.seats ?? 0) * basePrice
238240

239241
// Validate new limit is not below minimum
240242
if (newLimit < minimumLimit) {

0 commit comments

Comments
 (0)