Skip to content

Commit bb47e34

Browse files
fix: use generated workspace API response types (#15281)
## Summary Use the generated Comfy Cloud ingest contract as the source of truth for workspace and billing API response types, including the `TEAM` subscription tier. ## Root cause `workspaceApi.ts` locally overrode generated `@comfyorg/ingest-types` responses and sourced `SubscriptionTier` from the Registry contract. Those frontend-owned copies had drifted from the ingest API: Registry omitted `TEAM`, billing fields that ingest requires were made optional or normalized, and undocumented `scheduled_plan_slug` / `change_at` fields were added only on the frontend. The preview request also added a `billing_cycle` field that the ingest contract does not define and the server does not read. That let frontend behavior compile against a contract the server did not define. ## Changes - Import workspace, billing plan, billing status, plan, and subscription tier response types directly from `@comfyorg/ingest-types`. - Remove frontend-only billing status overrides, seat normalization, and scheduled plan field mapping. - Map generated `TEAM` tiers into existing team billing policy and subscription display behavior. - Remove `billing_cycle` only from preview-subscribe requests; the actual subscribe request still sends it as required by the server contract. - Update billing fixtures and behavioral coverage to match the generated response/request contracts. ## AS IS - Workspace billing responses were reshaped by frontend-owned response types. - `SubscriptionTier` came from the Registry schema, so the server-defined `TEAM` value was absent. - Undocumented scheduled plan fields and a preview-only `billing_cycle` were treated as ingest API fields. ## TO BE - Workspace API consumers use the generated ingest contract directly. - `TEAM` is represented and handled according to the server contract. - Frontend code no longer invents or normalizes server state or request fields outside that contract. ## Behavior and regression assessment The network endpoints and subscription flow are unchanged. `TEAM` handling is the only intentional policy correction: inconsistent payloads where a non-team workspace reports a `TEAM` tier now resolve to the existing team policy state instead of falling through an impossible TypeScript branch. Removing preview `billing_cycle` is behavior-neutral because the server derives cadence from `plan_slug` and does not read that field. Removing scheduled plan fields is behavior-neutral for the current backend, which does not define or emit them. ## Verification - `pnpm test:unit src/platform/workspace/api/workspaceApi.test.ts src/platform/workspace/composables/useSubscriptionCheckout.test.ts src/platform/workspace/composables/useWorkspaceBilling.test.ts` — 219 passed - `pnpm typecheck` — passed - ESLint — passed for changed files - oxfmt check — passed for changed files - `pnpm knip --cache` — passed via pre-push hook ## Screenshots N/A — API contract alignment with no intended visible UI change. --------- Co-authored-by: Amp <amp@ampcode.com>
1 parent 848cd39 commit bb47e34

16 files changed

Lines changed: 97 additions & 76 deletions

browser_tests/fixtures/data/subscriptionFixtures.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export function createSubscriptionStatus(
1313
subscription_tier: 'FREE',
1414
has_funds: false,
1515
billing_rail: 'legacy_stripe',
16+
max_seats: 0,
17+
occupied_seats: 0,
18+
team_credit_stop: null,
1619
...overrides
1720
}
1821
}

browser_tests/tests/billingFacadeConsumers.spec.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,19 @@ const mockWorkspaceBalance: BillingBalanceResponse = {
5050

5151
async function mockCloudBoot(
5252
page: Page,
53-
subscriptionStatus: BillingStatusResponse,
53+
subscriptionStatus: Partial<BillingStatusResponse>,
5454
remoteConfig: RemoteConfig = {},
5555
billingRail?: BillingStatusResponse['billing_rail']
5656
) {
57+
const resolvedSubscriptionStatus: BillingStatusResponse = {
58+
is_active: false,
59+
has_funds: false,
60+
max_seats: 0,
61+
occupied_seats: 0,
62+
team_credit_stop: null,
63+
...subscriptionStatus,
64+
...(billingRail === undefined ? {} : { billing_rail: billingRail })
65+
}
5766
const billingRequests = {
5867
legacyStatus: 0,
5968
legacyBalance: 0,
@@ -112,18 +121,13 @@ async function mockCloudBoot(
112121
})
113122
await page.route('**/customers/cloud-subscription-status', (r) => {
114123
billingRequests.legacyStatus++
115-
return r.fulfill(jsonRoute(subscriptionStatus))
124+
return r.fulfill(jsonRoute(resolvedSubscriptionStatus))
116125
})
117126

118127
// Cloud personal workspaces route through `/api/billing/*`.
119128
await page.route('**/api/billing/status', (r) => {
120129
billingRequests.workspaceStatus++
121-
return r.fulfill(
122-
jsonRoute({
123-
...subscriptionStatus,
124-
billing_rail: billingRail
125-
})
126-
)
130+
return r.fulfill(jsonRoute(resolvedSubscriptionStatus))
127131
})
128132
await page.route('**/api/billing/balance', (r) => {
129133
billingRequests.workspaceBalance++

browser_tests/tests/currentUserPopoverCredits.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const mockBillingStatus: BillingStatusResponse = {
5353
is_active: true,
5454
max_seats: 1,
5555
occupied_seats: 1,
56+
team_credit_stop: null,
5657
subscription_status: 'canceled',
5758
subscription_tier: 'PRO',
5859
subscription_duration: 'MONTHLY',

browser_tests/tests/dialogs/creditsTile.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const mockBillingStatus: BillingStatusResponse = {
6363
is_active: true,
6464
max_seats: 1,
6565
occupied_seats: 1,
66+
team_credit_stop: null,
6667
subscription_tier: 'PRO',
6768
subscription_duration: 'MONTHLY',
6869
renewal_date: '2099-02-20T12:00:00Z',
@@ -71,12 +72,18 @@ const mockBillingStatus: BillingStatusResponse = {
7172

7273
const freeBillingStatus: BillingStatusResponse = {
7374
is_active: false,
75+
max_seats: 1,
76+
occupied_seats: 1,
77+
team_credit_stop: null,
7478
subscription_tier: 'FREE',
7579
has_funds: true
7680
}
7781

7882
const endedPersonalBillingStatus: BillingStatusResponse = {
7983
is_active: false,
84+
max_seats: 1,
85+
occupied_seats: 1,
86+
team_credit_stop: null,
8087
subscription_status: 'ended',
8188
subscription_tier: 'PRO',
8289
subscription_duration: 'MONTHLY',

browser_tests/tests/gettingStartedTour.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const TOUR_FEATURE_FLAGS: RemoteConfig = {
4949

5050
const ACTIVE_SUBSCRIPTION: BillingStatusResponse = {
5151
is_active: true,
52+
max_seats: 1,
53+
occupied_seats: 1,
54+
team_credit_stop: null,
5255
subscription_tier: 'PRO',
5356
subscription_duration: 'MONTHLY',
5457
renewal_date: '2099-01-01',
@@ -57,6 +60,9 @@ const ACTIVE_SUBSCRIPTION: BillingStatusResponse = {
5760

5861
const INACTIVE_SUBSCRIPTION: BillingStatusResponse = {
5962
is_active: false,
63+
max_seats: 1,
64+
occupied_seats: 1,
65+
team_credit_stop: null,
6066
subscription_tier: 'FREE',
6167
subscription_duration: 'MONTHLY',
6268
has_funds: false

src/composables/billing/useBillingContext.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const DEFAULT_BILLING_STATUS: BillingStatusResponse = {
1919
max_seats: 73,
2020
occupied_seats: 72,
2121
has_funds: true,
22+
team_credit_stop: null,
2223
subscription_tier: 'PRO',
2324
subscription_duration: 'MONTHLY'
2425
}
@@ -190,6 +191,9 @@ describe('useBillingContext', () => {
190191
mockLegacyStatus.value = {
191192
is_active: true,
192193
has_funds: true,
194+
max_seats: 0,
195+
occupied_seats: 0,
196+
team_credit_stop: null,
193197
renewal_date: '2025-01-01T00:00:00Z'
194198
}
195199
mockBillingStatus.value = { ...DEFAULT_BILLING_STATUS }

src/platform/cloud/subscription/composables/useBillingPolicyState.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,21 @@ describe('deriveBillingPolicyState', () => {
111111
).toEqual({ kind })
112112
}
113113
)
114+
115+
it.for<[string, boolean]>([
116+
['LocalTeamWithoutActiveSubscription', false],
117+
['CloudTeamWithoutActiveSubscription', true]
118+
])(
119+
'preserves an inactive TEAM tier as %s without the isTeamPlan signal (isCloud=%s)',
120+
([kind, isCloud]) => {
121+
expect(
122+
deriveBillingPolicyState({
123+
isCloud,
124+
canAccessSubscriptionFeatures: false,
125+
isTeamPlan: false,
126+
tier: 'TEAM'
127+
})
128+
).toEqual({ kind })
129+
}
130+
)
114131
})

src/platform/cloud/subscription/composables/useBillingPolicyState.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function deriveBillingPolicyState(input: {
1818
}): BillingPolicyState {
1919
const distribution = input.isCloud ? 'Cloud' : 'Local'
2020

21-
if (input.isTeamPlan) {
21+
if (input.isTeamPlan || input.tier === 'TEAM') {
2222
return {
2323
kind: input.canAccessSubscriptionFeatures
2424
? `${distribution}AndTeam`
@@ -41,8 +41,6 @@ export function deriveBillingPolicyState(input: {
4141
return { kind: `${distribution}AndPro` }
4242
case 'FOUNDERS_EDITION':
4343
return { kind: `${distribution}AndFounders` }
44-
case 'TEAM':
45-
return { kind: `${distribution}AndTeam` }
4644
case null:
4745
return { kind: `${distribution}AndUnknown` }
4846
default:

src/platform/cloud/subscription/composables/useSubscriptionCancellationWatcher.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ describe('useSubscriptionCancellationWatcher', () => {
1818
const baseStatus: BillingStatusResponse = {
1919
is_active: true,
2020
has_funds: true,
21+
max_seats: 0,
22+
occupied_seats: 0,
23+
team_credit_stop: null,
2124
renewal_date: '2025-11-16'
2225
}
2326

@@ -62,9 +65,8 @@ describe('useSubscriptionCancellationWatcher', () => {
6265
if (fetchStatus.mock.calls.length === 2) {
6366
isActive.value = false
6467
subscriptionStatus.value = {
68+
...baseStatus,
6569
is_active: false,
66-
has_funds: true,
67-
renewal_date: '2025-11-16',
6870
cancel_at: '2025-12-01'
6971
}
7072
}

src/platform/workspace/api/workspaceApi.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,15 +439,23 @@ describe('workspaceApi', () => {
439439
})
440440

441441
describe('subscription', () => {
442-
it('previewSubscribe() sends POST with plan_slug', async () => {
442+
it('previewSubscribe() sends only fields defined by the ingest contract', async () => {
443443
const data = { allowed: true, transition_type: 'new_subscription' }
444444
mockAxiosInstance.post.mockResolvedValue({ data })
445445

446-
const result = await workspaceApi.previewSubscribe('pro-monthly')
446+
const result = await workspaceApi.previewSubscribe(
447+
'team_per_credit_annual',
448+
{
449+
teamCreditStopId: 'team_700'
450+
}
451+
)
447452

448453
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
449454
'/api/billing/preview-subscribe',
450-
{ plan_slug: 'pro-monthly' },
455+
{
456+
plan_slug: 'team_per_credit_annual',
457+
team_credit_stop_id: 'team_700'
458+
},
451459
{ headers: AUTH_HEADER }
452460
)
453461
expect(result).toEqual(data)

0 commit comments

Comments
 (0)