Skip to content

Commit 6e50719

Browse files
huntcsgampagent
andcommitted
fix: preserve team checkout recovery
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a02c80-27c8-70bc-b916-186bbe86557a
1 parent 5fd3e35 commit 6e50719

4 files changed

Lines changed: 133 additions & 11 deletions

File tree

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

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
22

3+
import type { SubscriptionInfo } from '@/composables/billing/types'
4+
import type {
5+
BillingSubscriptionStatus,
6+
TeamCreditStops,
7+
TeamCreditStopSummary
8+
} from '@/platform/workspace/api/workspaceApi'
39
import { savePendingSubscriptionCheckout } from '@/platform/workspace/utils/pendingSubscriptionCheckout'
410

511
import { useSubscriptionDialog } from './useSubscriptionDialog'
@@ -24,7 +30,18 @@ const mockEmbeddedCheckoutEnabled = vi.hoisted(() => ({ value: false }))
2430
const mockActiveWorkspaceId = vi.hoisted(() => ({ value: 'workspace-1' }))
2531
const mockStartOperation = vi.hoisted(() => vi.fn())
2632
const mockFetchPlans = vi.hoisted(() => vi.fn())
27-
const mockTeamCreditStops = vi.hoisted(() => ({ value: null }))
33+
const mockTeamCreditStops = vi.hoisted(() => ({
34+
value: null as TeamCreditStops | null
35+
}))
36+
const mockCurrentTeamCreditStop = vi.hoisted(() => ({
37+
value: null as TeamCreditStopSummary | null
38+
}))
39+
const mockSubscription = vi.hoisted(() => ({
40+
value: null as Pick<SubscriptionInfo, 'duration'> | null
41+
}))
42+
const mockSubscriptionStatus = vi.hoisted(() => ({
43+
value: null as BillingSubscriptionStatus | null
44+
}))
2845

2946
vi.mock('vue', async (importOriginal) => {
3047
const actual = await importOriginal()
@@ -101,7 +118,10 @@ vi.mock('@/composables/billing/useBillingContext', () => ({
101118
currentPlanSlug: mockCurrentPlanSlug,
102119
tier: mockTier,
103120
fetchPlans: mockFetchPlans,
104-
teamCreditStops: mockTeamCreditStops
121+
teamCreditStops: mockTeamCreditStops,
122+
currentTeamCreditStop: mockCurrentTeamCreditStop,
123+
subscription: mockSubscription,
124+
subscriptionStatus: mockSubscriptionStatus
105125
})
106126
}))
107127

@@ -147,6 +167,10 @@ describe('useSubscriptionDialog', () => {
147167
mockActiveWorkspaceId.value = 'workspace-1'
148168
mockStartOperation.mockResolvedValue({ status: 'succeeded' })
149169
mockFetchPlans.mockResolvedValue(undefined)
170+
mockTeamCreditStops.value = null
171+
mockCurrentTeamCreditStop.value = null
172+
mockSubscription.value = null
173+
mockSubscriptionStatus.value = null
150174
sessionStorage.clear()
151175
})
152176

@@ -697,5 +721,91 @@ describe('useSubscriptionDialog', () => {
697721
sessionStorage.getItem('comfy:pending-subscription-checkout')
698722
).toBeNull()
699723
})
724+
725+
it('restores a Team plan change from fresh catalog and subscription state', async () => {
726+
mockShouldUseWorkspaceBilling.value = true
727+
mockStartOperation.mockResolvedValueOnce({ status: 'failed' })
728+
mockTeamCreditStops.value = {
729+
default_stop_index: 0,
730+
stops: [
731+
{
732+
id: 'team_700',
733+
credits: 147_700,
734+
monthly: {
735+
list_price_cents: 70_000,
736+
price_cents: 66_500
737+
},
738+
yearly: {
739+
list_price_cents: 70_000,
740+
price_cents: 63_000
741+
}
742+
}
743+
]
744+
}
745+
mockCurrentTeamCreditStop.value = {
746+
id: 'team_400',
747+
stop_usd: 400,
748+
credits_monthly: 84_400
749+
}
750+
mockSubscription.value = { duration: 'MONTHLY' }
751+
mockSubscriptionStatus.value = 'active'
752+
savePendingSubscriptionCheckout({
753+
operationId: 'op-team-change',
754+
workspaceId: 'workspace-1',
755+
selection: {
756+
planMode: 'team',
757+
teamCreditStopId: 'team_700',
758+
billingCycle: 'yearly'
759+
},
760+
attemptedAt: Date.now()
761+
})
762+
763+
const { resumePendingPricingFlow } = useSubscriptionDialog()
764+
await resumePendingPricingFlow()
765+
766+
expect(mockShowLayoutDialog).toHaveBeenCalledWith(
767+
expect.objectContaining({
768+
props: expect.objectContaining({
769+
initialCheckout: {
770+
planMode: 'team',
771+
stop: {
772+
id: 'team_700',
773+
credits: 147_700,
774+
usd: 700,
775+
discountedUsd: 630
776+
},
777+
billingCycle: 'yearly',
778+
isChange: true
779+
}
780+
})
781+
})
782+
)
783+
})
784+
785+
it('falls back to Team pricing when the stop catalog is unavailable', async () => {
786+
mockShouldUseWorkspaceBilling.value = true
787+
mockStartOperation.mockResolvedValueOnce({ status: 'failed' })
788+
savePendingSubscriptionCheckout({
789+
operationId: 'op-team-catalog-failure',
790+
workspaceId: 'workspace-1',
791+
selection: {
792+
planMode: 'team',
793+
teamCreditStopId: 'team_700',
794+
billingCycle: 'yearly'
795+
},
796+
attemptedAt: Date.now()
797+
})
798+
799+
const { resumePendingPricingFlow } = useSubscriptionDialog()
800+
await resumePendingPricingFlow()
801+
802+
expect(mockShowLayoutDialog).toHaveBeenCalledWith(
803+
expect.objectContaining({
804+
props: expect.objectContaining({ initialPlanMode: 'team' })
805+
})
806+
)
807+
const props = mockShowLayoutDialog.mock.calls[0][0].props
808+
expect(props.initialCheckout).toBeUndefined()
809+
})
700810
})
701811
})

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,13 @@ export const useSubscriptionDialog = () => {
243243
const selection = pending.selection
244244
if (selection.planMode === 'personal') return selection
245245

246-
const { fetchPlans, teamCreditStops } = useBillingContext()
246+
const {
247+
fetchPlans,
248+
teamCreditStops,
249+
currentTeamCreditStop,
250+
subscription,
251+
subscriptionStatus
252+
} = useBillingContext()
247253
await fetchPlans()
248254
const stop = mapApiTeamCreditStops(teamCreditStops.value?.stops ?? []).find(
249255
({ id }) => id === selection.teamCreditStopId
@@ -258,7 +264,14 @@ export const useSubscriptionDialog = () => {
258264
credits: stop.credits,
259265
discountedUsd: getStopDiscountedMonthlyUsd(stop, selection.billingCycle)
260266
},
261-
billingCycle: selection.billingCycle
267+
billingCycle: selection.billingCycle,
268+
isChange:
269+
currentTeamCreditStop.value !== null &&
270+
subscriptionStatus.value !== 'ended' &&
271+
(currentTeamCreditStop.value.id !== stop.id ||
272+
(subscription.value?.duration === 'MONTHLY'
273+
? 'monthly'
274+
: 'yearly') !== selection.billingCycle)
262275
}
263276
}
264277

@@ -287,12 +300,10 @@ export const useSubscriptionDialog = () => {
287300
if (operation.status === 'succeeded') return
288301

289302
const initialCheckout = await restoreCheckoutSelection(pending)
290-
if (initialCheckout) {
291-
showPricingTable({
292-
planMode: initialCheckout.planMode,
293-
initialCheckout
294-
})
295-
}
303+
showPricingTable({
304+
planMode: pending.selection.planMode,
305+
...(initialCheckout && { initialCheckout })
306+
})
296307
} finally {
297308
clearPendingSubscriptionCheckout(pending.operationId)
298309
}

src/platform/workspace/composables/useSubscriptionCheckout.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2964,7 +2964,7 @@ describe('useSubscriptionCheckout', () => {
29642964
})
29652965

29662966
it('persists the pending attempt until its operation becomes terminal', async () => {
2967-
const checkout = await setup()
2967+
const checkout = await setupWithApprovedPreview()
29682968
checkout.selectedTierKey.value = 'creator'
29692969
checkout.selectedBillingCycle.value = 'monthly'
29702970
mockSubscribe.mockResolvedValueOnce({

src/platform/workspace/composables/useSubscriptionCheckout.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type SubscriptionCheckoutSelection =
4848
planMode: 'team'
4949
stop: TeamPlanSelection
5050
billingCycle: BillingCycle
51+
isChange?: boolean
5152
}
5253

5354
interface SelectedTeamCheckout {

0 commit comments

Comments
 (0)