Skip to content

Commit 077e8f3

Browse files
committed
feat(billing): route personal workspaces on billing control flag
1 parent e313a3f commit 077e8f3

12 files changed

Lines changed: 77 additions & 80 deletions

File tree

browser_tests/tests/dialogs/pricingTableDeepLink.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ const APP_URL = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
2828
// matches it against the members self-row.
2929
const SELF_EMAIL = 'e2e@test.comfy.org'
3030

31-
// consolidated_billing_enabled routes personal workspaces to the unified
31+
// billing_control_enabled routes personal workspaces to the unified
3232
// pricing table asserted here; without it they fall back to the legacy table.
3333
const BOOT_FEATURES = {
3434
team_workspaces_enabled: true,
35-
consolidated_billing_enabled: true
35+
billing_control_enabled: true
3636
} satisfies RemoteConfig
3737
// Disable the experimental Asset API: with it on (cloud default) the unmocked
3838
// asset endpoints 403 and workflow restore throws uncaught, aborting the

src/composables/billing/useBillingContext.test.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ const DEFAULT_BILLING_STATUS: BillingStatusResponse = {
1919

2020
const {
2121
mockTeamWorkspacesEnabled,
22-
mockConsolidatedBillingEnabled,
22+
mockBillingControlEnabled,
2323
mockIsPersonal,
2424
mockPlans,
2525
mockPurchaseCredits,
2626
mockUpdateActiveWorkspace,
2727
mockBillingStatus
2828
} = vi.hoisted(() => ({
2929
mockTeamWorkspacesEnabled: { value: false },
30-
mockConsolidatedBillingEnabled: { value: false },
30+
mockBillingControlEnabled: { value: false },
3131
mockIsPersonal: { value: true },
3232
mockPlans: { value: [] as Plan[] },
3333
mockPurchaseCredits: vi.fn(),
@@ -59,13 +59,11 @@ vi.mock('@/composables/useFeatureFlags', async () => {
5959
teamWorkspacesEnabledRef.value = value
6060
}
6161
})
62-
const consolidatedBillingEnabledRef = ref(
63-
mockConsolidatedBillingEnabled.value
64-
)
65-
Object.defineProperty(mockConsolidatedBillingEnabled, 'value', {
66-
get: () => consolidatedBillingEnabledRef.value,
62+
const billingControlEnabledRef = ref(mockBillingControlEnabled.value)
63+
Object.defineProperty(mockBillingControlEnabled, 'value', {
64+
get: () => billingControlEnabledRef.value,
6765
set: (value: boolean) => {
68-
consolidatedBillingEnabledRef.value = value
66+
billingControlEnabledRef.value = value
6967
}
7068
})
7169
return {
@@ -74,8 +72,8 @@ vi.mock('@/composables/useFeatureFlags', async () => {
7472
get teamWorkspacesEnabled() {
7573
return mockTeamWorkspacesEnabled.value
7674
},
77-
get consolidatedBillingEnabled() {
78-
return mockConsolidatedBillingEnabled.value
75+
get billingControlEnabled() {
76+
return mockBillingControlEnabled.value
7977
}
8078
}
8179
})
@@ -165,7 +163,7 @@ describe('useBillingContext', () => {
165163
setActivePinia(createPinia())
166164
vi.clearAllMocks()
167165
mockTeamWorkspacesEnabled.value = false
168-
mockConsolidatedBillingEnabled.value = false
166+
mockBillingControlEnabled.value = false
169167
mockIsPersonal.value = true
170168
mockPlans.value = []
171169
mockBillingStatus.value = { ...DEFAULT_BILLING_STATUS }
@@ -177,27 +175,27 @@ describe('useBillingContext', () => {
177175
expect(type.value).toBe('legacy')
178176
})
179177

180-
it('keeps personal on legacy when consolidated billing is disabled', () => {
178+
it('keeps personal on legacy when billing control is disabled', () => {
181179
mockTeamWorkspacesEnabled.value = true
182-
mockConsolidatedBillingEnabled.value = false
180+
mockBillingControlEnabled.value = false
183181
mockIsPersonal.value = true
184182

185183
const { type } = useBillingContext()
186184
expect(type.value).toBe('legacy')
187185
})
188186

189-
it('selects workspace type for personal when consolidated billing is enabled', () => {
187+
it('selects workspace type for personal when billing control is enabled', () => {
190188
mockTeamWorkspacesEnabled.value = true
191-
mockConsolidatedBillingEnabled.value = true
189+
mockBillingControlEnabled.value = true
192190
mockIsPersonal.value = true
193191

194192
const { type } = useBillingContext()
195193
expect(type.value).toBe('workspace')
196194
})
197195

198-
it('selects workspace type for team regardless of consolidated billing', () => {
196+
it('selects workspace type for team regardless of billing control', () => {
199197
mockTeamWorkspacesEnabled.value = true
200-
mockConsolidatedBillingEnabled.value = false
198+
mockBillingControlEnabled.value = false
201199
mockIsPersonal.value = false
202200

203201
const { type } = useBillingContext()
@@ -298,7 +296,7 @@ describe('useBillingContext', () => {
298296
expect(workspaceApi.getBillingStatus).not.toHaveBeenCalled()
299297

300298
// Authenticated remote config resolves the flag on for the same workspace
301-
mockConsolidatedBillingEnabled.value = true
299+
mockBillingControlEnabled.value = true
302300
mockTeamWorkspacesEnabled.value = true
303301

304302
await vi.waitFor(() => {
@@ -307,16 +305,16 @@ describe('useBillingContext', () => {
307305
})
308306
})
309307

310-
it('moves a personal workspace to workspace billing when consolidated billing flips on', async () => {
308+
it('moves a personal workspace to workspace billing when billing control flips on', async () => {
311309
mockTeamWorkspacesEnabled.value = true
312-
mockConsolidatedBillingEnabled.value = false
310+
mockBillingControlEnabled.value = false
313311
mockIsPersonal.value = true
314312

315313
const { type } = useBillingContext()
316314
await nextTick()
317315
expect(type.value).toBe('legacy')
318316

319-
mockConsolidatedBillingEnabled.value = true
317+
mockBillingControlEnabled.value = true
320318

321319
await vi.waitFor(() => {
322320
expect(type.value).toBe('workspace')
@@ -325,9 +323,9 @@ describe('useBillingContext', () => {
325323
})
326324

327325
describe('subscription mirror to workspace store', () => {
328-
it('mirrors subscription for personal workspaces on the consolidated billing flow', async () => {
326+
it('mirrors subscription for personal workspaces on the billing control flow', async () => {
329327
mockTeamWorkspacesEnabled.value = true
330-
mockConsolidatedBillingEnabled.value = true
328+
mockBillingControlEnabled.value = true
331329
mockIsPersonal.value = true
332330

333331
const { initialize } = useBillingContext()
@@ -584,7 +582,7 @@ describe('useBillingContext', () => {
584582

585583
it('is true for a per-credit Team plan in a personal workspace before its credit stop is populated', async () => {
586584
mockTeamWorkspacesEnabled.value = true
587-
mockConsolidatedBillingEnabled.value = true
585+
mockBillingControlEnabled.value = true
588586
mockIsPersonal.value = true
589587
mockBillingStatus.value = {
590588
is_active: true,

src/composables/billing/useBillingContext.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ function isTeamPlanSlug(planSlug: string | null | undefined): boolean {
4545
*
4646
* - Team workspaces disabled (OSS/Desktop): legacy billing via /customers/*
4747
* - Team workspaces enabled: workspace billing via /api/billing/* for team
48-
* workspaces, and for personal workspaces once consolidated billing is
49-
* enabled; personal workspaces otherwise stay on legacy billing
48+
* workspaces, and for personal workspaces once billing control is enabled;
49+
* personal workspaces otherwise stay on legacy billing
5050
*
5151
* The context automatically initializes when the workspace changes and provides
5252
* a unified interface for subscription status, balance, and billing actions.
@@ -218,9 +218,9 @@ function useBillingContextInternal(): BillingContext {
218218
error.value = null
219219
}
220220

221-
// type flips when the team-workspaces or consolidated-billing flag resolves
222-
// from authenticated config, swapping the active backend. Reset then reinit
223-
// on every workspace-id or type change.
221+
// type flips when the team-workspaces or billing-control flag resolves from
222+
// authenticated config, swapping the active backend. Reset then reinit on
223+
// every workspace-id or type change.
224224
watch(
225225
[() => store.activeWorkspace?.id, () => type.value],
226226
async ([newWorkspaceId]) => {

src/composables/billing/useBillingRouting.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useBillingRouting } from './useBillingRouting'
55
const { mockFlags, mockActiveWorkspace } = vi.hoisted(() => ({
66
mockFlags: {
77
teamWorkspacesEnabled: false,
8-
consolidatedBillingEnabled: false
8+
billingControlEnabled: false
99
},
1010
mockActiveWorkspace: {
1111
value: null as { id: string; type: 'personal' | 'team' } | null
@@ -30,7 +30,7 @@ const team = { id: 'w-team', type: 'team' as const }
3030
describe('useBillingRouting', () => {
3131
beforeEach(() => {
3232
mockFlags.teamWorkspacesEnabled = false
33-
mockFlags.consolidatedBillingEnabled = false
33+
mockFlags.billingControlEnabled = false
3434
mockActiveWorkspace.value = personal
3535
})
3636

@@ -44,19 +44,19 @@ describe('useBillingRouting', () => {
4444
expect(shouldUseWorkspaceBilling.value).toBe(false)
4545
})
4646

47-
it('keeps personal on legacy when consolidated billing is disabled', () => {
47+
it('keeps personal on legacy when billing control is disabled', () => {
4848
mockFlags.teamWorkspacesEnabled = true
49-
mockFlags.consolidatedBillingEnabled = false
49+
mockFlags.billingControlEnabled = false
5050
mockActiveWorkspace.value = personal
5151

5252
const { type } = useBillingRouting()
5353

5454
expect(type.value).toBe('legacy')
5555
})
5656

57-
it('moves personal to workspace billing when consolidated billing is enabled', () => {
57+
it('moves personal to workspace billing when billing control is enabled', () => {
5858
mockFlags.teamWorkspacesEnabled = true
59-
mockFlags.consolidatedBillingEnabled = true
59+
mockFlags.billingControlEnabled = true
6060
mockActiveWorkspace.value = personal
6161

6262
const { type, shouldUseWorkspaceBilling } = useBillingRouting()
@@ -65,9 +65,9 @@ describe('useBillingRouting', () => {
6565
expect(shouldUseWorkspaceBilling.value).toBe(true)
6666
})
6767

68-
it('uses workspace billing for team workspaces regardless of consolidated billing', () => {
68+
it('uses workspace billing for team workspaces regardless of billing control', () => {
6969
mockFlags.teamWorkspacesEnabled = true
70-
mockFlags.consolidatedBillingEnabled = false
70+
mockFlags.billingControlEnabled = false
7171
mockActiveWorkspace.value = team
7272

7373
const { type, shouldUseWorkspaceBilling } = useBillingRouting()
@@ -76,9 +76,9 @@ describe('useBillingRouting', () => {
7676
expect(shouldUseWorkspaceBilling.value).toBe(true)
7777
})
7878

79-
it('uses workspace billing for team workspaces with consolidated billing enabled', () => {
79+
it('uses workspace billing for team workspaces with billing control enabled', () => {
8080
mockFlags.teamWorkspacesEnabled = true
81-
mockFlags.consolidatedBillingEnabled = true
81+
mockFlags.billingControlEnabled = true
8282
mockActiveWorkspace.value = team
8383

8484
const { type, shouldUseWorkspaceBilling } = useBillingRouting()
@@ -89,7 +89,7 @@ describe('useBillingRouting', () => {
8989

9090
it('defaults to legacy while the workspace has not loaded', () => {
9191
mockFlags.teamWorkspacesEnabled = true
92-
mockFlags.consolidatedBillingEnabled = true
92+
mockFlags.billingControlEnabled = true
9393
mockActiveWorkspace.value = null
9494

9595
const { type } = useBillingRouting()

src/composables/billing/useBillingRouting.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { BillingType } from './types'
88
/**
99
* Selects the billing backend for the active workspace: legacy user-scoped
1010
* (`/customers/*`) or workspace-scoped (`/api/billing/*`). Personal workspaces
11-
* stay legacy until `consolidatedBillingEnabled`; team workspaces are always
11+
* stay legacy until `billingControlEnabled`; team workspaces are always
1212
* workspace-scoped. The routing matrix is covered in useBillingRouting.test.ts.
1313
*/
1414
export function useBillingRouting() {
@@ -23,7 +23,7 @@ export function useBillingRouting() {
2323
const workspaceType = workspaceStore.activeWorkspace?.type
2424
if (!workspaceType) return 'legacy'
2525

26-
if (workspaceType === 'personal' && !flags.consolidatedBillingEnabled) {
26+
if (workspaceType === 'personal' && !flags.billingControlEnabled) {
2727
return 'legacy'
2828
}
2929

src/composables/useFeatureFlags.test.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from '@/composables/useFeatureFlags'
88
import * as distributionTypes from '@/platform/distribution/types'
99
import {
10-
cachedConsolidatedBillingEnabled,
10+
cachedBillingControlEnabled,
1111
cachedTeamWorkspacesEnabled,
1212
remoteConfig,
1313
remoteConfigState
@@ -226,19 +226,19 @@ describe('useFeatureFlags', () => {
226226
expect(flags.teamWorkspacesEnabled).toBe(true)
227227
})
228228

229-
it('consolidatedBillingEnabled override bypasses isCloud and isAuthenticatedConfigLoaded guards', () => {
229+
it('billingControlEnabled override bypasses isCloud and isAuthenticatedConfigLoaded guards', () => {
230230
vi.mocked(distributionTypes).isCloud = false
231-
localStorage.setItem('ff:consolidated_billing_enabled', 'true')
231+
localStorage.setItem('ff:billing_control_enabled', 'true')
232232

233233
const { flags } = useFeatureFlags()
234-
expect(flags.consolidatedBillingEnabled).toBe(true)
234+
expect(flags.billingControlEnabled).toBe(true)
235235
})
236236

237-
it('consolidatedBillingEnabled is false off-cloud even without an override', () => {
237+
it('billingControlEnabled is false off-cloud even without an override', () => {
238238
vi.mocked(distributionTypes).isCloud = false
239239

240240
const { flags } = useFeatureFlags()
241-
expect(flags.consolidatedBillingEnabled).toBe(false)
241+
expect(flags.billingControlEnabled).toBe(false)
242242
})
243243
})
244244

@@ -248,7 +248,7 @@ describe('useFeatureFlags', () => {
248248
remoteConfigState.value = 'unloaded'
249249
remoteConfig.value = {}
250250
cachedTeamWorkspacesEnabled.value = undefined
251-
cachedConsolidatedBillingEnabled.value = undefined
251+
cachedBillingControlEnabled.value = undefined
252252
localStorage.clear()
253253
})
254254

@@ -257,36 +257,36 @@ describe('useFeatureFlags', () => {
257257
remoteConfigState.value = 'unloaded'
258258
remoteConfig.value = {}
259259
cachedTeamWorkspacesEnabled.value = undefined
260-
cachedConsolidatedBillingEnabled.value = undefined
260+
cachedBillingControlEnabled.value = undefined
261261
localStorage.clear()
262262
})
263263

264264
it('returns the cached session value during the auth window', () => {
265265
cachedTeamWorkspacesEnabled.value = false
266-
cachedConsolidatedBillingEnabled.value = true
266+
cachedBillingControlEnabled.value = true
267267

268268
const { flags } = useFeatureFlags()
269269
expect(flags.teamWorkspacesEnabled).toBe(false)
270-
expect(flags.consolidatedBillingEnabled).toBe(true)
270+
expect(flags.billingControlEnabled).toBe(true)
271271
})
272272

273273
it('defaults to false during the auth window when nothing is cached', () => {
274274
const { flags } = useFeatureFlags()
275275
expect(flags.teamWorkspacesEnabled).toBe(false)
276-
expect(flags.consolidatedBillingEnabled).toBe(false)
276+
expect(flags.billingControlEnabled).toBe(false)
277277
})
278278

279279
it('prefers authenticated remoteConfig over the server feature fallback', () => {
280280
remoteConfigState.value = 'authenticated'
281281
remoteConfig.value = {
282282
team_workspaces_enabled: true,
283-
consolidated_billing_enabled: true
283+
billing_control_enabled: true
284284
}
285285
vi.mocked(api.getServerFeature).mockReturnValue(false)
286286

287287
const { flags } = useFeatureFlags()
288288
expect(flags.teamWorkspacesEnabled).toBe(true)
289-
expect(flags.consolidatedBillingEnabled).toBe(true)
289+
expect(flags.billingControlEnabled).toBe(true)
290290
})
291291

292292
it('falls back to api.getServerFeature when authenticated config omits the flag', () => {
@@ -295,15 +295,14 @@ describe('useFeatureFlags', () => {
295295
vi.mocked(api.getServerFeature).mockImplementation(
296296
(path, defaultValue) => {
297297
if (path === ServerFeatureFlag.TEAM_WORKSPACES_ENABLED) return true
298-
if (path === ServerFeatureFlag.CONSOLIDATED_BILLING_ENABLED)
299-
return true
298+
if (path === ServerFeatureFlag.BILLING_CONTROL_ENABLED) return true
300299
return defaultValue
301300
}
302301
)
303302

304303
const { flags } = useFeatureFlags()
305304
expect(flags.teamWorkspacesEnabled).toBe(true)
306-
expect(flags.consolidatedBillingEnabled).toBe(true)
305+
expect(flags.billingControlEnabled).toBe(true)
307306
})
308307
})
309308

0 commit comments

Comments
 (0)