Skip to content

Commit 9c58830

Browse files
dante01yoonConnor Byrne
andauthored
refactor(billing): retire consolidated billing rollout flag (#14615)
Part 4/4 of the workspace and billing rollout retirement stack. Predecessor: [#14614](#14614) Successor: none (final stack PR) ## Summary Retires `consolidated_billing_enabled` after canonical billing discovery and explicit rail preservation landed in [#14614](#14614). Loaded Cloud workspaces now use unified pricing, while personal workspaces on `legacy_stripe` retain legacy account operations. ## Root cause The final billing route still depended on a compatibility feature key even after the workspace billing contract and canonical rail status were available. Removing the backend key first would therefore change personal-workspace routing during bootstrap or strand legacy Stripe balance/top-up/management behavior. The frontend needs a deterministic route based on distribution, workspace readiness/type, and the canonical billing rail before backend compatibility keys disappear. ## Changes - Removes the consolidated-billing key from remote-config types, enum/getter, authenticated cache/refresh, local override path, Storybook mocks, and browser payloads. - Routes OSS and unloaded Cloud workspaces through legacy bootstrap. - Routes loaded Cloud team workspaces and personal `unknown`/`stripe` rails through workspace billing. - Preserves legacy account operations for personal `legacy_stripe` while keeping unified pricing for every loaded Cloud workspace. - Keeps the legacy adapter, balance, top-up, management behavior, and meaningful regression coverage. - Re-runs the canonical-status rail regression with both retired feature keys absent from `/api/features`. ## AS IS A loaded Cloud personal workspace still consults `consolidated_billing_enabled` to decide its billing adapter and pricing route. No intended visual change. This is rollout cleanup, so no screenshot is required. ## TO BE Loaded Cloud workspaces use unified pricing without a rollout key. Canonical `billing_rail` determines whether personal account operations remain on `legacy_stripe`; team and migrated/unknown personal workspaces use workspace billing. No intended visual change. The existing legacy rail UI remains available where the canonical status selects it. ## Regression coverage - OSS and unloaded-workspace legacy bootstrap. - Cloud team workspace billing. - Cloud personal unknown/Stripe workspace billing. - Cloud personal `legacy_stripe` legacy account operations plus unified pricing. - Billing-context adapter switching and preserved legacy adapter behavior. - Playwright avatar-popover rail selection with `/api/features` omitting both retired keys, canonical status discovery, legacy balance/credits, and no stale workspace zero balance. - Repository search confirms no consolidated-flag symbol/string remains in `src` or `browser_tests`. ## Validation - `pnpm test:unit src/composables/useFeatureFlags.test.ts src/composables/billing/useBillingRouting.test.ts src/composables/billing/useBillingContext.test.ts src/platform/remoteConfig/refreshRemoteConfig.test.ts` — 83 passed. - `pnpm typecheck` — passed. - `pnpm typecheck:browser` — passed. - Targeted oxfmt/oxlint/ESLint commit checks — passed. - `pnpm knip --cache` pre-push check — passed. - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5174 pnpm test:browser browser_tests/tests/billingFacadeConsumers.spec.ts -g 'avatar popover'` — 1 passed. - `git diff --check dante/canonical-billing-status...HEAD` — passed. ## Review focus Please review only this PR's removal of the consolidated flag and the final routing matrix. Canonical endpoint migration and pending-checkout stabilization belong to [#14614](#14614); workspace-flag retirement belongs to [#14613](#14613). ## Stack/deployment order 1. [#14612](#14612) — workspace initialization recovery 2. [#14613](#14613) — retire team workspace rollout flag 3. [#14614](#14614) — canonical billing status and legacy rail preservation 4. [#14615](#14615) — this PR; retire consolidated billing rollout flag Deploy the frontend stack in this order before the backend removes the `/api/features` compatibility keys. Each branch remains independently deployable; do not deploy a later PR without its predecessor. This is also a drain requirement: age out deployed frontend versions and open tabs that still read either retired key before removing backend compatibility keys. Confirm `consolidated_billing_enabled` is at 100% and update `Comfy-Org/cloud#6040` to depend on this selected stack rather than competing PR #14530 before merge. After backend key removal, reverting this PR is not a behavioral restore because a reinstated read of the missing key resolves to the off branch. Co-authored-by: Connor Byrne <c.byrne@comfy.org>
1 parent 147988b commit 9c58830

14 files changed

Lines changed: 20 additions & 130 deletions

browser_tests/fixtures/data/cloudWorkspace.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ import type {
1111
} from '@/platform/workspace/api/workspaceApi'
1212
import type { RemoteConfig } from '@/platform/remoteConfig/types'
1313

14-
export const CLOUD_REMOTE_CONFIG: RemoteConfig = {
15-
consolidated_billing_enabled: true
16-
}
14+
export const CLOUD_REMOTE_CONFIG: RemoteConfig = {}
1715

1816
export const LEGACY_PERSONAL_BILLING_STATUS = {
1917
billing_rail: 'legacy_stripe',

browser_tests/fixtures/utils/cloudAppSetup.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,6 @@ import { mockWorkspace } from '@e2e/fixtures/utils/workspaceMocks'
1414
export const APP_URL =
1515
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
1616

17-
// consolidated_billing_enabled routes personal workspaces to the unified
18-
// billing surfaces the cloud specs assert; without it they fall back to the
19-
// legacy variants.
20-
const DEFAULT_FEATURES = {
21-
consolidated_billing_enabled: true
22-
} satisfies RemoteConfig
23-
2417
// Disable the experimental Asset API: with it on (cloud default) the unmocked
2518
// asset endpoints 403 and workflow restore throws uncaught, aborting the
2619
// GraphCanvas onMounted chain before the URL action loaders.
@@ -63,7 +56,7 @@ export async function setupCloudApp(
6356
{ workspace, members = [], features }: CloudAppSetupOptions
6457
) {
6558
await mockCloudBoot(page, {
66-
features: { ...DEFAULT_FEATURES, ...features },
59+
features: features ?? {},
6760
settings: DEFAULT_SETTINGS
6861
})
6962
await mockGraphBootExtras(page)

browser_tests/tests/billingFacadeConsumers.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ test.describe('Billing facade consumers (FE-933)', { tag: '@cloud' }, () => {
163163
renewal_date: '2099-02-20T10:00:00Z',
164164
has_funds: true
165165
},
166-
{ consolidated_billing_enabled: true },
166+
{},
167167
'legacy_stripe'
168168
)
169169
await bootApp(page)
@@ -222,8 +222,7 @@ test.describe('Billing facade consumers (FE-933)', { tag: '@cloud' }, () => {
222222
has_funds: false
223223
},
224224
{
225-
subscription_required: true,
226-
consolidated_billing_enabled: true
225+
subscription_required: true
227226
},
228227
'stripe'
229228
)

browser_tests/tests/dialogs/creditsTile.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ async function mockCloudBoot(
9595
await page.route('**/api/features', (r) =>
9696
r.fulfill(
9797
jsonRoute({
98-
consolidated_billing_enabled: true,
9998
billing_control_enabled: billingControlEnabled
10099
} satisfies RemoteConfig)
101100
)

browser_tests/tests/dialogs/pricingTableDeepLink.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ const APP_URL = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
4242
const SELF_EMAIL = 'e2e@test.comfy.org'
4343

4444
const BOOT_FEATURES = {
45-
billing_control_enabled: true,
46-
consolidated_billing_enabled: true
45+
billing_control_enabled: true
4746
} satisfies RemoteConfig
4847
// Disable the experimental Asset API: with it on (cloud default) the unmocked
4948
// asset endpoints 403 and workflow restore throws uncaught, aborting the

src/composables/billing/useBillingContext.test.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const DEFAULT_BILLING_STATUS: BillingStatusResponse = {
2121
}
2222

2323
const {
24-
mockConsolidatedBillingEnabled,
2524
mockIsPersonal,
2625
mockBillingRail,
2726
mockPlans,
@@ -35,7 +34,6 @@ const {
3534
mockLegacyStatus,
3635
mockBillingStatus
3736
} = vi.hoisted(() => ({
38-
mockConsolidatedBillingEnabled: { value: true },
3937
mockIsPersonal: { value: true },
4038
mockBillingRail: { value: undefined as BillingRail | undefined },
4139
mockPlans: { value: [] as Plan[] },
@@ -63,16 +61,6 @@ const {
6361
}
6462
}))
6563

66-
vi.mock('@/composables/useFeatureFlags', () => ({
67-
useFeatureFlags: () => ({
68-
flags: {
69-
get consolidatedBillingEnabled() {
70-
return mockConsolidatedBillingEnabled.value
71-
}
72-
}
73-
})
74-
}))
75-
7664
vi.mock('@vueuse/core', async (importOriginal) => {
7765
const original = await importOriginal()
7866
return {
@@ -187,7 +175,6 @@ describe('useBillingContext', () => {
187175
beforeEach(() => {
188176
setActivePinia(createPinia())
189177
vi.clearAllMocks()
190-
mockConsolidatedBillingEnabled.value = true
191178
mockIsPersonal.value = true
192179
mockBillingRail.value = undefined
193180
mockSetWorkspaceBillingRail.mockImplementation(
@@ -211,14 +198,6 @@ describe('useBillingContext', () => {
211198
expect(type.value).toBe('workspace')
212199
})
213200

214-
it('keeps a Cloud personal workspace on legacy while consolidation is off', () => {
215-
mockConsolidatedBillingEnabled.value = false
216-
217-
const { type } = useBillingContext()
218-
219-
expect(type.value).toBe('legacy')
220-
})
221-
222201
it('selects workspace type for a Cloud team workspace', () => {
223202
mockIsPersonal.value = false
224203

src/composables/billing/useBillingRouting.test.ts

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,16 @@ import type { BillingRail } from '@/platform/workspace/api/workspaceApi'
44

55
import { useBillingRouting } from './useBillingRouting'
66

7-
const {
8-
mockIsCloud,
9-
mockConsolidatedBillingEnabled,
10-
mockActiveWorkspace,
11-
mockActiveWorkspaceBillingRail
12-
} = vi.hoisted(() => ({
13-
mockIsCloud: { value: true },
14-
mockConsolidatedBillingEnabled: { value: false },
15-
mockActiveWorkspace: {
16-
value: null as { id: string; type: 'personal' | 'team' } | null
17-
},
18-
mockActiveWorkspaceBillingRail: {
19-
value: null as BillingRail | null
20-
}
21-
}))
22-
23-
vi.mock('@/composables/useFeatureFlags', () => ({
24-
useFeatureFlags: () => ({
25-
flags: {
26-
get consolidatedBillingEnabled() {
27-
return mockConsolidatedBillingEnabled.value
28-
}
7+
const { mockIsCloud, mockActiveWorkspace, mockActiveWorkspaceBillingRail } =
8+
vi.hoisted(() => ({
9+
mockIsCloud: { value: true },
10+
mockActiveWorkspace: {
11+
value: null as { id: string; type: 'personal' | 'team' } | null
12+
},
13+
mockActiveWorkspaceBillingRail: {
14+
value: null as BillingRail | null
2915
}
30-
})
31-
}))
16+
}))
3217

3318
vi.mock('@/platform/distribution/types', () => ({
3419
get isCloud() {
@@ -53,7 +38,6 @@ const team = { id: 'w-team', type: 'team' as const }
5338
describe('useBillingRouting', () => {
5439
beforeEach(() => {
5540
mockIsCloud.value = true
56-
mockConsolidatedBillingEnabled.value = false
5741
mockActiveWorkspace.value = personal
5842
mockActiveWorkspaceBillingRail.value = null
5943
})
@@ -68,26 +52,14 @@ describe('useBillingRouting', () => {
6852
expect(shouldUseWorkspaceBilling.value).toBe(false)
6953
})
7054

71-
it('keeps a Cloud personal workspace on legacy while consolidation is off', () => {
72-
mockActiveWorkspace.value = personal
73-
74-
const { type, shouldUseWorkspaceBilling } = useBillingRouting()
75-
76-
expect(type.value).toBe('legacy')
77-
expect(shouldUseWorkspaceBilling.value).toBe(false)
78-
})
79-
80-
it('uses workspace billing for a Cloud personal workspace when consolidation is on', () => {
81-
mockConsolidatedBillingEnabled.value = true
82-
55+
it('uses workspace billing for a Cloud personal workspace', () => {
8356
const { type, shouldUseWorkspaceBilling } = useBillingRouting()
8457

8558
expect(type.value).toBe('workspace')
8659
expect(shouldUseWorkspaceBilling.value).toBe(true)
8760
})
8861

8962
it('uses unified pricing while keeping legacy Stripe top-ups on Checkout', () => {
90-
mockConsolidatedBillingEnabled.value = true
9163
mockActiveWorkspace.value = personal
9264
mockActiveWorkspaceBillingRail.value = 'legacy_stripe'
9365

@@ -100,7 +72,6 @@ describe('useBillingRouting', () => {
10072
})
10173

10274
it('uses workspace billing for migrated Stripe personal workspaces', () => {
103-
mockConsolidatedBillingEnabled.value = true
10475
mockActiveWorkspace.value = personal
10576
mockActiveWorkspaceBillingRail.value = 'stripe'
10677

src/composables/billing/useBillingRouting.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { computed } from 'vue'
22

3-
import { useFeatureFlags } from '@/composables/useFeatureFlags'
43
import { isCloud } from '@/platform/distribution/types'
54
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
65

@@ -9,21 +8,15 @@ import type { BillingType } from './types'
98
/**
109
* Selects the billing backend for the active workspace: legacy user-scoped
1110
* (`/customers/*`) or workspace-scoped (`/api/billing/*`). Personal workspaces
12-
* stay legacy until consolidated billing is enabled; an explicit legacy Stripe
13-
* rail continues to use legacy account operations after enablement. An unloaded
14-
* workspace remains legacy during bootstrap, and OSS always uses legacy billing.
11+
* use workspace billing unless an explicit legacy Stripe rail selects legacy
12+
* account operations. An unloaded workspace remains legacy during bootstrap,
13+
* and OSS always uses legacy billing.
1514
*/
1615
export function useBillingRouting() {
17-
const { flags } = useFeatureFlags()
1816
const workspaceStore = useTeamWorkspaceStore()
1917

2018
const shouldUseUnifiedPricing = computed(() => {
21-
if (!isCloud) return false
22-
23-
const workspaceType = workspaceStore.activeWorkspace?.type
24-
if (!workspaceType) return false
25-
26-
return workspaceType === 'team' || flags.consolidatedBillingEnabled
19+
return isCloud && workspaceStore.activeWorkspace?.type !== undefined
2720
})
2821

2922
const type = computed<BillingType>(() => {
@@ -36,8 +29,7 @@ export function useBillingRouting() {
3629

3730
if (
3831
workspaceType === 'personal' &&
39-
(!flags.consolidatedBillingEnabled ||
40-
workspaceStore.activeWorkspaceBillingRail === 'legacy_stripe')
32+
workspaceStore.activeWorkspaceBillingRail === 'legacy_stripe'
4133
) {
4234
return 'legacy'
4335
}

src/composables/useFeatureFlags.test.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
import * as distributionTypes from '@/platform/distribution/types'
99
import {
1010
cachedBillingControlEnabled,
11-
cachedConsolidatedBillingEnabled,
1211
cachedV1PaymentRecovery,
1312
remoteConfig,
1413
remoteConfigState
@@ -286,14 +285,6 @@ describe('useFeatureFlags', () => {
286285
expect(flags.v1PaymentRecovery).toBe(true)
287286
})
288287

289-
it('consolidatedBillingEnabled override bypasses isCloud and isAuthenticatedConfigLoaded guards', () => {
290-
vi.mocked(distributionTypes).isCloud = false
291-
localStorage.setItem('ff:consolidated_billing_enabled', 'true')
292-
293-
const { flags } = useFeatureFlags()
294-
expect(flags.consolidatedBillingEnabled).toBe(true)
295-
})
296-
297288
it('billingControlEnabled is false off-cloud even without an override', () => {
298289
vi.mocked(distributionTypes).isCloud = false
299290

@@ -307,7 +298,6 @@ describe('useFeatureFlags', () => {
307298
vi.mocked(distributionTypes).isCloud = true
308299
remoteConfigState.value = 'unloaded'
309300
remoteConfig.value = {}
310-
cachedConsolidatedBillingEnabled.value = undefined
311301
cachedBillingControlEnabled.value = undefined
312302
cachedV1PaymentRecovery.value = undefined
313303
localStorage.clear()
@@ -317,41 +307,35 @@ describe('useFeatureFlags', () => {
317307
vi.mocked(distributionTypes).isCloud = false
318308
remoteConfigState.value = 'unloaded'
319309
remoteConfig.value = {}
320-
cachedConsolidatedBillingEnabled.value = undefined
321310
cachedBillingControlEnabled.value = undefined
322311
cachedV1PaymentRecovery.value = undefined
323312
localStorage.clear()
324313
})
325314

326315
it('returns the cached session value during the auth window', () => {
327-
cachedConsolidatedBillingEnabled.value = true
328316
cachedBillingControlEnabled.value = true
329317
cachedV1PaymentRecovery.value = true
330318

331319
const { flags } = useFeatureFlags()
332-
expect(flags.consolidatedBillingEnabled).toBe(true)
333320
expect(flags.billingControlEnabled).toBe(true)
334321
expect(flags.v1PaymentRecovery).toBe(true)
335322
})
336323

337324
it('defaults to false during the auth window when nothing is cached', () => {
338325
const { flags } = useFeatureFlags()
339-
expect(flags.consolidatedBillingEnabled).toBe(false)
340326
expect(flags.billingControlEnabled).toBe(false)
341327
expect(flags.v1PaymentRecovery).toBe(false)
342328
})
343329

344330
it('prefers authenticated remoteConfig over the server feature fallback', () => {
345331
remoteConfigState.value = 'authenticated'
346332
remoteConfig.value = {
347-
consolidated_billing_enabled: true,
348333
billing_control_enabled: false,
349334
v1_payment_recovery: true
350335
}
351336
vi.mocked(api.getServerFeature).mockReturnValue(false)
352337

353338
const { flags } = useFeatureFlags()
354-
expect(flags.consolidatedBillingEnabled).toBe(true)
355339
expect(flags.billingControlEnabled).toBe(false)
356340
expect(flags.v1PaymentRecovery).toBe(true)
357341
})
@@ -361,16 +345,13 @@ describe('useFeatureFlags', () => {
361345
remoteConfig.value = {}
362346
vi.mocked(api.getServerFeature).mockImplementation(
363347
(path, defaultValue) => {
364-
if (path === ServerFeatureFlag.CONSOLIDATED_BILLING_ENABLED)
365-
return true
366348
if (path === ServerFeatureFlag.BILLING_CONTROL_ENABLED) return true
367349
if (path === ServerFeatureFlag.V1_PAYMENT_RECOVERY) return true
368350
return defaultValue
369351
}
370352
)
371353

372354
const { flags } = useFeatureFlags()
373-
expect(flags.consolidatedBillingEnabled).toBe(true)
374355
expect(flags.billingControlEnabled).toBe(true)
375356
expect(flags.v1PaymentRecovery).toBe(true)
376357
})

src/composables/useFeatureFlags.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { Ref } from 'vue'
44
import { isCloud, isNightly } from '@/platform/distribution/types'
55
import {
66
cachedBillingControlEnabled,
7-
cachedConsolidatedBillingEnabled,
87
cachedV1PaymentRecovery,
98
isAuthenticatedConfigLoaded,
109
remoteConfig
@@ -33,7 +32,6 @@ export enum ServerFeatureFlag {
3332
COMFYHUB_PROFILE_GATE_ENABLED = 'comfyhub_profile_gate_enabled',
3433
SHOW_SIGNIN_BUTTON = 'show_signin_button',
3534
UNIFIED_CLOUD_AUTH = 'unified_cloud_auth',
36-
CONSOLIDATED_BILLING_ENABLED = 'consolidated_billing_enabled',
3735
BILLING_CONTROL_ENABLED = 'billing_control_enabled',
3836
V1_PAYMENT_RECOVERY = 'v1_payment_recovery',
3937
FREE_TIER_JOB_ALLOWANCE_ENABLED = 'free_tier_job_allowance_enabled',
@@ -191,13 +189,6 @@ export function useFeatureFlags() {
191189
false
192190
)
193191
},
194-
get consolidatedBillingEnabled() {
195-
return resolveAuthGatedFlag(
196-
ServerFeatureFlag.CONSOLIDATED_BILLING_ENABLED,
197-
remoteConfig.value.consolidated_billing_enabled,
198-
cachedConsolidatedBillingEnabled
199-
)
200-
},
201192
get billingControlEnabled() {
202193
return resolveAuthGatedFlag(
203194
ServerFeatureFlag.BILLING_CONTROL_ENABLED,

0 commit comments

Comments
 (0)