Skip to content

Commit 79a3486

Browse files
committed
chore(billing): tidy payer-pool concealment cleanup
1 parent 8ff822e commit 79a3486

3 files changed

Lines changed: 25 additions & 37 deletions

File tree

apps/sim/lib/billing/application/billing-use-cases.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ const mocks = vi.hoisted(() => ({
2424
getUsageLogs: vi.fn(),
2525
getWorkspaceUsageLogs: vi.fn(),
2626
recordAudit: vi.fn(),
27-
canManageWorkspaceBilling: vi.fn(),
27+
canUserManageWorkspaceBilling: vi.fn(),
2828
}))
2929

3030
vi.mock('@/lib/billing/core/workspace-billing-authority', () => ({
31-
canUserManageWorkspaceBilling: mocks.canManageWorkspaceBilling,
31+
canUserManageWorkspaceBilling: mocks.canUserManageWorkspaceBilling,
3232
}))
3333

3434
vi.mock('@/lib/workspaces/application/workspace-context', () => ({
@@ -99,7 +99,7 @@ describe('billing application use cases', () => {
9999
vi.clearAllMocks()
100100
mocks.loadWorkspace.mockResolvedValue(workspaceContext)
101101
mocks.resolvePermission.mockResolvedValue('read')
102-
mocks.canManageWorkspaceBilling.mockResolvedValue(false)
102+
mocks.canUserManageWorkspaceBilling.mockResolvedValue(false)
103103
mocks.checkUsageStatus.mockResolvedValue({ currentUsage: 1, limit: 10, isExceeded: false })
104104
mocks.checkAttributedBlocks.mockResolvedValue({ blocked: false })
105105
mocks.toUsageLimitSubscription.mockReturnValue(null)
@@ -188,7 +188,7 @@ describe('billing application use cases', () => {
188188

189189
expect(result.credits).toBeNull()
190190
expect(result.storage).toBeNull()
191-
expect(mocks.canManageWorkspaceBilling).not.toHaveBeenCalled()
191+
expect(mocks.canUserManageWorkspaceBilling).not.toHaveBeenCalled()
192192
})
193193

194194
it('never reads the payer storage pool it may not disclose', async () => {
@@ -216,7 +216,7 @@ describe('billing application use cases', () => {
216216

217217
it('withholds the payer pool from a workspace member who cannot manage billing', async () => {
218218
mocks.resolvePermission.mockResolvedValue('read')
219-
mocks.canManageWorkspaceBilling.mockResolvedValue(false)
219+
mocks.canUserManageWorkspaceBilling.mockResolvedValue(false)
220220

221221
const result = await getBillingStatus.execute({
222222
principal: personalPrincipal,
@@ -226,12 +226,12 @@ describe('billing application use cases', () => {
226226
expect(result.credits).toBeNull()
227227
expect(result.storage).toBeNull()
228228
expect(result).toMatchObject({ workspaceId: 'workspace-1', plan: 'free', status: 'active' })
229-
expect(mocks.canManageWorkspaceBilling).toHaveBeenCalledWith(workspaceContext, 'user-1')
229+
expect(mocks.canUserManageWorkspaceBilling).toHaveBeenCalledWith(workspaceContext, 'user-1')
230230
})
231231

232232
it('withholds the payer pool from a workspace admin who cannot manage billing', async () => {
233233
mocks.resolvePermission.mockResolvedValue('admin')
234-
mocks.canManageWorkspaceBilling.mockResolvedValue(false)
234+
mocks.canUserManageWorkspaceBilling.mockResolvedValue(false)
235235

236236
const result = await getBillingStatus.execute({
237237
principal: personalPrincipal,
@@ -243,7 +243,7 @@ describe('billing application use cases', () => {
243243
})
244244

245245
it('still reports an exceeded payer limit without disclosing the pool', async () => {
246-
mocks.canManageWorkspaceBilling.mockResolvedValue(false)
246+
mocks.canUserManageWorkspaceBilling.mockResolvedValue(false)
247247
mocks.checkUsageStatus.mockResolvedValue({ currentUsage: 40, limit: 10, isExceeded: true })
248248

249249
const result = await getBillingStatus.execute({
@@ -256,7 +256,7 @@ describe('billing application use cases', () => {
256256
})
257257

258258
it('projects the payer pool to a member who can manage billing', async () => {
259-
mocks.canManageWorkspaceBilling.mockResolvedValue(true)
259+
mocks.canUserManageWorkspaceBilling.mockResolvedValue(true)
260260

261261
const result = await getBillingStatus.execute({
262262
principal: personalPrincipal,
@@ -272,7 +272,7 @@ describe('billing application use cases', () => {
272272
})
273273

274274
it('always reports the account-scoped pool the caller owns', async () => {
275-
mocks.canManageWorkspaceBilling.mockResolvedValue(false)
275+
mocks.canUserManageWorkspaceBilling.mockResolvedValue(false)
276276
mocks.getSubscription.mockResolvedValue({ plan: 'pro' })
277277
mocks.deriveBillingContext.mockReturnValue({
278278
billingEntity: { type: 'user', id: 'user-1' },

apps/sim/lib/billing/application/get-billing-status.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ function storageStatus(usedBytes: number, limitBytes: number): BillingStorageSta
6666
}
6767
}
6868

69+
function creditsStatus(usage: { currentUsage: number; limit: number }): BillingCreditsStatus {
70+
return {
71+
used: dollarsToCredits(usage.currentUsage),
72+
limit: dollarsToCredits(usage.limit),
73+
remaining: dollarsToCredits(usage.limit - usage.currentUsage),
74+
}
75+
}
76+
6977
/**
7078
* Resolves whether a caller may read the resolved payer's pooled allowances.
7179
*
@@ -89,15 +97,15 @@ function storageStatus(usedBytes: number, limitBytes: number): BillingStorageSta
8997
* A workspace key still reads the plan, period, and standing it needs to
9098
* monitor the workspace, including `limit_exceeded` and `billing_blocked`.
9199
*/
92-
function canReadPayerPool(
100+
async function canReadPayerPool(
93101
principal: BillingReadPrincipal,
94102
workspace: WorkspaceBillingAuthorityContext
95103
): Promise<boolean> {
96-
if (principal.kind !== 'personal_api_key') return Promise.resolve(false)
104+
if (principal.kind !== 'personal_api_key') return false
97105
return canUserManageWorkspaceBilling(workspace, principal.userId)
98106
}
99107

100-
/** Reads the payer's storage pool. Only called once disclosure is authorized. */
108+
/** Only invoked once payer-pool disclosure is authorized. */
101109
async function resolvePayerStorage(workspaceId: string): Promise<BillingStorageStatus> {
102110
const storageContext = await resolveStorageBillingContext(workspaceId)
103111
const usedBytes = await getStorageUsageForBillingContext(storageContext)
@@ -128,13 +136,7 @@ export const getBillingStatus = defineAuthorizedBillingReadUseCase({
128136
period: attribution.billingPeriod,
129137
plan: attribution.payerSubscription?.plan ?? 'free',
130138
status: block.blocked ? 'billing_blocked' : usage.isExceeded ? 'limit_exceeded' : 'active',
131-
credits: canViewPayerPool
132-
? {
133-
used: dollarsToCredits(usage.currentUsage),
134-
limit: dollarsToCredits(usage.limit),
135-
remaining: dollarsToCredits(usage.limit - usage.currentUsage),
136-
}
137-
: null,
139+
credits: canViewPayerPool ? creditsStatus(usage) : null,
138140
storage,
139141
}
140142
}
@@ -163,11 +165,7 @@ export const getBillingStatus = defineAuthorizedBillingReadUseCase({
163165
: usage.isExceeded
164166
? 'limit_exceeded'
165167
: 'active',
166-
credits: {
167-
used: dollarsToCredits(usage.currentUsage),
168-
limit: dollarsToCredits(usage.limit),
169-
remaining: dollarsToCredits(usage.limit - usage.currentUsage),
170-
},
168+
credits: creditsStatus(usage),
171169
storage: storageStatus(storageUsedBytes, storageLimitBytes),
172170
}
173171
},

apps/sim/lib/billing/core/workspace-billing-authority.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import { db } from '@sim/db'
2-
import { member } from '@sim/db/schema'
3-
import { isOrgAdminRole } from '@sim/platform-authz/workspace'
4-
import { and, eq } from 'drizzle-orm'
1+
import { isOrganizationAdminOrOwner } from '@/lib/workspaces/permissions/utils'
52

63
/**
74
* Canonical workspace state needed to decide payer-billing authority.
@@ -26,14 +23,7 @@ export async function canUserManageWorkspaceBilling(
2623
userId: string
2724
): Promise<boolean> {
2825
if (context.workspaceOrganizationId) {
29-
const [membership] = await db
30-
.select({ role: member.role })
31-
.from(member)
32-
.where(
33-
and(eq(member.organizationId, context.workspaceOrganizationId), eq(member.userId, userId))
34-
)
35-
.limit(1)
36-
return isOrgAdminRole(membership?.role)
26+
return isOrganizationAdminOrOwner(userId, context.workspaceOrganizationId)
3727
}
3828

3929
return context.billedAccountUserId === userId

0 commit comments

Comments
 (0)