Skip to content

Commit ecee977

Browse files
committed
improvement(admin): cleanup included usage settings for enterprise
1 parent 2798467 commit ecee977

14 files changed

Lines changed: 228 additions & 160 deletions

apps/sim/lib/admin/dashboard-organizations.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,37 @@ vi.mock('@/lib/core/idempotency/transaction', () => ({
6767
}))
6868
vi.mock('@/lib/core/outbox/service', () => ({ enqueueOutboxEvent: vi.fn() }))
6969

70-
import { listDashboardOrganizations } from '@/lib/admin/dashboard'
70+
import { listDashboardOrganizations, toDashboardConfigurationUpdate } from '@/lib/admin/dashboard'
71+
72+
describe('toDashboardConfigurationUpdate', () => {
73+
it('converts the pending Stripe metadata intent without replacing applied values', () => {
74+
expect(
75+
toDashboardConfigurationUpdate({
76+
latestRevision: 2,
77+
desiredMetadata: {},
78+
hasUnappliedIntent: true,
79+
effectiveSeatCapacity: 20,
80+
configurationUpdate: {
81+
id: 'config-2',
82+
status: 'pending',
83+
requestedMetadata: {
84+
usageLimitCredits: 10_000_000,
85+
seats: 20,
86+
concurrencyLimit: 50,
87+
},
88+
error: null,
89+
},
90+
})
91+
).toEqual({
92+
id: 'config-2',
93+
status: 'pending',
94+
requestedUsageLimitDollars: 50_000,
95+
requestedSeats: 20,
96+
requestedConcurrencyLimit: 50,
97+
error: null,
98+
})
99+
})
100+
})
71101

72102
describe('listDashboardOrganizations', () => {
73103
beforeEach(() => {

apps/sim/lib/admin/dashboard.ts

Lines changed: 83 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,9 @@ interface DashboardOrganizationSummaryInput {
154154
}
155155

156156
export function toDashboardProvisioning(view: EnterpriseProvisioningView) {
157-
const { includedMonthlyCredits, usageLimitCredits, ...rest } = view
157+
const { usageLimitCredits, ...rest } = view
158158
return {
159159
...rest,
160-
includedMonthlyDollars: creditsToDollars(includedMonthlyCredits),
161160
usageLimitDollars: creditsToDollars(usageLimitCredits),
162161
}
163162
}
@@ -172,11 +171,7 @@ function buildDashboardOrganizationSummary({
172171
}: DashboardOrganizationSummaryInput) {
173172
const metadata = metadataRecord(latestSubscription?.metadata)
174173
const teamEconomics = getTeamOrganizationEconomics(latestSubscription?.plan, memberCount)
175-
const includedMonthlyDollars =
176-
teamEconomics?.includedMonthlyDollars ??
177-
creditsToDollars(
178-
Math.max(0, Math.round(metadataNumber(metadata, 'includedMonthlyCredits') ?? 0))
179-
)
174+
const planAllowanceDollars = teamEconomics?.planAllowanceDollars ?? null
180175
const invoiceAmountCents = metadataNumber(metadata, 'invoiceAmountCents')
181176
const monthlyPrice = metadataNumber(metadata, 'monthlyPrice')
182177
const effectiveUsageLimitDollars = Number(org.orgUsageLimit ?? 0)
@@ -212,7 +207,7 @@ function buildDashboardOrganizationSummary({
212207
externalCollaboratorCount,
213208
seats,
214209
concurrencyLimit,
215-
includedMonthlyDollars,
210+
planAllowanceDollars,
216211
usageLimitDollars,
217212
effectiveUsageLimitDollars,
218213
prepaidBalanceDollars: Number(org.creditBalance ?? 0),
@@ -227,6 +222,27 @@ function buildDashboardOrganizationSummary({
227222
}
228223
}
229224

225+
export function toDashboardConfigurationUpdate(
226+
intent: Awaited<ReturnType<typeof resolveEnterpriseMetadataIntent>> | null
227+
) {
228+
const update = intent?.configurationUpdate
229+
if (!update) return null
230+
const metadata = update.requestedMetadata
231+
const usageLimitCredits = metadataNumber(metadata, 'usageLimitCredits')
232+
const seats = metadataNumber(metadata, 'seats')
233+
const concurrencyLimit = metadataNumber(metadata, 'concurrencyLimit')
234+
235+
return {
236+
id: update.id,
237+
status: update.status,
238+
requestedUsageLimitDollars:
239+
usageLimitCredits === null ? null : creditsToDollars(usageLimitCredits),
240+
requestedSeats: seats === null ? null : Math.round(seats),
241+
requestedConcurrencyLimit: concurrencyLimit === null ? null : Math.round(concurrencyLimit),
242+
error: update.error,
243+
}
244+
}
245+
230246
export async function listDashboardUsers({ search, limit, offset }: PaginationInput) {
231247
const trimmed = search.trim()
232248
// Mirror Better Auth's active-ban semantics: permanent bans and temporary
@@ -444,60 +460,65 @@ export async function listDashboardOrganizations({ search, limit, offset }: Pagi
444460
export async function getDashboardOrganization(organizationId: string) {
445461
const summary = await getDashboardOrganizationSummary(organizationId)
446462
if (!summary) return null
447-
const [memberRows, externalRows, workspaceRows, limitRows] = await Promise.all([
448-
db
449-
.select({
450-
id: member.id,
451-
userId: user.id,
452-
name: user.name,
453-
email: user.email,
454-
role: member.role,
455-
})
456-
.from(member)
457-
.innerJoin(user, eq(user.id, member.userId))
458-
.where(eq(member.organizationId, organizationId))
459-
.orderBy(user.name),
460-
db
461-
.select({
462-
userId: user.id,
463-
name: user.name,
464-
email: user.email,
465-
workspaceCount: countDistinct(workspace.id),
466-
})
467-
.from(permissions)
468-
.innerJoin(user, eq(user.id, permissions.userId))
469-
.innerJoin(
470-
workspace,
471-
and(
472-
eq(permissions.entityType, 'workspace'),
473-
eq(permissions.entityId, workspace.id),
474-
eq(workspace.organizationId, organizationId)
463+
const { subscription: subscriptionRow, ...base } = summary
464+
const [memberRows, externalRows, workspaceRows, limitRows, configurationIntent] =
465+
await Promise.all([
466+
db
467+
.select({
468+
id: member.id,
469+
userId: user.id,
470+
name: user.name,
471+
email: user.email,
472+
role: member.role,
473+
})
474+
.from(member)
475+
.innerJoin(user, eq(user.id, member.userId))
476+
.where(eq(member.organizationId, organizationId))
477+
.orderBy(user.name),
478+
db
479+
.select({
480+
userId: user.id,
481+
name: user.name,
482+
email: user.email,
483+
workspaceCount: countDistinct(workspace.id),
484+
})
485+
.from(permissions)
486+
.innerJoin(user, eq(user.id, permissions.userId))
487+
.innerJoin(
488+
workspace,
489+
and(
490+
eq(permissions.entityType, 'workspace'),
491+
eq(permissions.entityId, workspace.id),
492+
eq(workspace.organizationId, organizationId)
493+
)
475494
)
476-
)
477-
.leftJoin(
478-
member,
479-
and(eq(member.userId, permissions.userId), eq(member.organizationId, organizationId))
480-
)
481-
.where(and(isNull(member.id), isNull(workspace.archivedAt)))
482-
.groupBy(user.id, user.name, user.email)
483-
.orderBy(user.name),
484-
db
485-
.select({ id: workspace.id, name: workspace.name })
486-
.from(workspace)
487-
.where(eq(workspace.organizationId, organizationId))
488-
.orderBy(workspace.name),
489-
db
490-
.select({
491-
userId: organizationMemberUsageLimit.userId,
492-
limit: organizationMemberUsageLimit.usageLimit,
493-
})
494-
.from(organizationMemberUsageLimit)
495-
.where(eq(organizationMemberUsageLimit.organizationId, organizationId)),
496-
])
495+
.leftJoin(
496+
member,
497+
and(eq(member.userId, permissions.userId), eq(member.organizationId, organizationId))
498+
)
499+
.where(and(isNull(member.id), isNull(workspace.archivedAt)))
500+
.groupBy(user.id, user.name, user.email)
501+
.orderBy(user.name),
502+
db
503+
.select({ id: workspace.id, name: workspace.name })
504+
.from(workspace)
505+
.where(eq(workspace.organizationId, organizationId))
506+
.orderBy(workspace.name),
507+
db
508+
.select({
509+
userId: organizationMemberUsageLimit.userId,
510+
limit: organizationMemberUsageLimit.usageLimit,
511+
})
512+
.from(organizationMemberUsageLimit)
513+
.where(eq(organizationMemberUsageLimit.organizationId, organizationId)),
514+
subscriptionRow?.plan === 'enterprise'
515+
? resolveEnterpriseMetadataIntent(db, subscriptionRow.id, subscriptionRow.metadata)
516+
: Promise.resolve(null),
517+
])
497518
const limits = new Map(limitRows.map((row) => [row.userId, Number(row.limit)]))
498-
const { subscription: subscriptionRow, ...base } = summary
499519
return {
500520
...base,
521+
configurationUpdate: toDashboardConfigurationUpdate(configurationIntent),
501522
members: memberRows.map((row) => ({
502523
...row,
503524
usageLimitDollars: limits.get(row.userId) ?? null,
@@ -570,7 +591,6 @@ export async function updateDashboardEnterpriseSeats(
570591
export async function updateDashboardOrganizationLimits(
571592
organizationId: string,
572593
values: {
573-
includedMonthlyDollars?: number
574594
usageLimitDollars?: number
575595
concurrencyLimit?: number | null
576596
},
@@ -597,9 +617,6 @@ export async function updateDashboardOrganizationLimits(
597617
.for('update')
598618
.limit(1)
599619
const metadata = metadataRecord(subscriptionRow?.metadata)
600-
if (values.includedMonthlyDollars !== undefined && subscriptionRow?.plan !== 'enterprise') {
601-
throw new Error('Included allowance is editable only for Enterprise organizations')
602-
}
603620
if (values.concurrencyLimit !== undefined && subscriptionRow?.plan !== 'enterprise') {
604621
throw new Error('Concurrency is editable only for Enterprise organizations')
605622
}
@@ -612,10 +629,6 @@ export async function updateDashboardOrganizationLimits(
612629
subscriptionId: subscriptionRow.id,
613630
appliedMetadata: subscriptionRow.metadata,
614631
buildDesiredMetadata: (current) => {
615-
const included =
616-
values.includedMonthlyDollars === undefined
617-
? Math.round(metadataNumber(current, 'includedMonthlyCredits') ?? 0)
618-
: dollarsToCredits(values.includedMonthlyDollars)
619632
const configuredUsageLimit =
620633
values.usageLimitDollars === undefined
621634
? Math.round(
@@ -625,7 +638,6 @@ export async function updateDashboardOrganizationLimits(
625638
: dollarsToCredits(values.usageLimitDollars)
626639
return {
627640
...current,
628-
includedMonthlyCredits: included,
629641
usageLimitCredits: configuredUsageLimit,
630642
...(values.concurrencyLimit !== undefined
631643
? { concurrencyLimit: values.concurrencyLimit }
@@ -644,18 +656,14 @@ export async function updateDashboardOrganizationLimits(
644656
subscriptionRow?.plan,
645657
memberCountRow?.value ?? 0
646658
)
647-
const included =
648-
values.includedMonthlyDollars ??
649-
(teamEconomics
650-
? teamEconomics.includedMonthlyDollars
651-
: creditsToDollars(Math.round(metadataNumber(metadata, 'includedMonthlyCredits') ?? 0)))
659+
const planAllowance = teamEconomics?.planAllowanceDollars ?? 0
652660
const prepaid = Number(org.creditBalance)
653661
const configuredUsageLimit =
654662
values.usageLimitDollars ??
655663
(metadataNumber(metadata, 'usageLimitCredits') === null
656664
? Number(org.orgUsageLimit ?? 0)
657665
: creditsToDollars(metadataNumber(metadata, 'usageLimitCredits') ?? 0))
658-
const effective = Math.max(configuredUsageLimit, included + prepaid)
666+
const effective = Math.max(configuredUsageLimit, planAllowance + prepaid)
659667
await tx
660668
.update(organization)
661669
.set({ orgUsageLimit: effective.toString(), updatedAt: new Date() })
@@ -666,9 +674,6 @@ export async function updateDashboardOrganizationLimits(
666674
.set({
667675
metadata: {
668676
...metadata,
669-
...(subscriptionRow.plan === 'enterprise'
670-
? { includedMonthlyCredits: dollarsToCredits(included) }
671-
: {}),
672677
usageLimitCredits: dollarsToCredits(configuredUsageLimit),
673678
},
674679
})
@@ -731,16 +736,7 @@ export async function grantDashboardOrganizationBalance(
731736
subscriptionRow?.plan,
732737
memberCountRow?.value ?? 0
733738
)
734-
const includedDollars = teamEconomics
735-
? teamEconomics.includedMonthlyDollars
736-
: creditsToDollars(
737-
Math.round(
738-
metadataNumber(
739-
metadataRecord(subscriptionRow?.metadata),
740-
'includedMonthlyCredits'
741-
) ?? 0
742-
)
743-
)
739+
const planAllowanceDollars = teamEconomics?.planAllowanceDollars ?? 0
744740
const subscriptionMetadata = metadataRecord(subscriptionRow?.metadata)
745741
const configuredUsageLimitCredits = metadataNumber(
746742
subscriptionMetadata,
@@ -753,7 +749,7 @@ export async function grantDashboardOrganizationBalance(
753749
const grantDollarDelta = toDecimal(amountDollars).toString()
754750
const usageLimitFallback = getOrganizationUsageLimitFallbackDollars({
755751
creditBalanceDollarsBeforeGrant: org.creditBalance,
756-
includedDollars,
752+
planAllowanceDollars,
757753
configuredUsageLimitDollars,
758754
})
759755
const [updated] = await tx

apps/sim/lib/admin/organization-economics.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ describe('getTeamOrganizationEconomics', () => {
88
it('derives the pooled Pro allowance and invoice from internal seats', () => {
99
expect(getTeamOrganizationEconomics('team_6000', 3)).toEqual({
1010
seats: 3,
11-
includedMonthlyDollars: 90,
11+
planAllowanceDollars: 90,
1212
monthlyInvoiceAmountUsd: 75,
1313
})
1414
})
1515

1616
it('derives the pooled Max allowance and invoice from internal seats', () => {
1717
expect(getTeamOrganizationEconomics('team_25000', 2)).toEqual({
1818
seats: 2,
19-
includedMonthlyDollars: 250,
19+
planAllowanceDollars: 250,
2020
monthlyInvoiceAmountUsd: 200,
2121
})
2222
})
@@ -31,7 +31,7 @@ describe('getOrganizationUsageLimitFallbackDollars', () => {
3131
expect(
3232
getOrganizationUsageLimitFallbackDollars({
3333
creditBalanceDollarsBeforeGrant: '0.001',
34-
includedDollars: 0,
34+
planAllowanceDollars: 0,
3535
configuredUsageLimitDollars: 0,
3636
})
3737
).toBe('0.001')
@@ -41,7 +41,7 @@ describe('getOrganizationUsageLimitFallbackDollars', () => {
4141
expect(
4242
getOrganizationUsageLimitFallbackDollars({
4343
creditBalanceDollarsBeforeGrant: '1.259567',
44-
includedDollars: 5,
44+
planAllowanceDollars: 5,
4545
configuredUsageLimitDollars: 100,
4646
})
4747
).toBe('101.259567')

apps/sim/lib/admin/organization-economics.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Decimal, toDecimal } from '@/lib/billing/utils/decimal'
44

55
export interface TeamOrganizationEconomics {
66
seats: number
7-
includedMonthlyDollars: number
7+
planAllowanceDollars: number
88
monthlyInvoiceAmountUsd: number
99
}
1010

@@ -17,7 +17,7 @@ export function getTeamOrganizationEconomics(
1717
const seats = Math.max(0, Math.trunc(internalMemberCount))
1818
return {
1919
seats,
20-
includedMonthlyDollars: (getPlanTierCredits(plan) * seats) / CREDIT_MULTIPLIER,
20+
planAllowanceDollars: (getPlanTierCredits(plan) * seats) / CREDIT_MULTIPLIER,
2121
monthlyInvoiceAmountUsd: getPlanTierDollars(plan) * seats,
2222
}
2323
}
@@ -29,15 +29,15 @@ export function getTeamOrganizationEconomics(
2929
*/
3030
export function getOrganizationUsageLimitFallbackDollars(params: {
3131
creditBalanceDollarsBeforeGrant: string | number
32-
includedDollars: number
32+
planAllowanceDollars: number
3333
configuredUsageLimitDollars: number | null
3434
}): string {
3535
const configuredUsageLimitDollars =
3636
params.configuredUsageLimitDollars === null
3737
? toDecimal(0)
3838
: toDecimal(params.configuredUsageLimitDollars)
39-
const includedUsageLimitDollars = toDecimal(params.includedDollars)
40-
return Decimal.max(configuredUsageLimitDollars, includedUsageLimitDollars)
39+
const planAllowanceDollars = toDecimal(params.planAllowanceDollars)
40+
return Decimal.max(configuredUsageLimitDollars, planAllowanceDollars)
4141
.plus(toDecimal(params.creditBalanceDollarsBeforeGrant))
4242
.toString()
4343
}

0 commit comments

Comments
 (0)