Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/platform/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@ type ResubscribeBillingEvent = {
operation: 'resubscribe'
source: ResubscribeClickMetadata['source']
payment_intent_source?: PaymentIntentSource
duration_ms?: number
} & (BillingStarted | BillingSucceeded | BillingFailed)

type TopupBillingEvent = {
Expand Down
103 changes: 103 additions & 0 deletions src/platform/workspace/composables/useDowngradeToPersonal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { WorkspaceApiError } from '@/platform/workspace/api/workspaceApi'
import type { WorkspaceMember } from '@/platform/workspace/stores/teamWorkspaceStore'

import {
ReactivationAmountChangedError,
ReactivationConfirmationRequiredError,
useDowngradeToPersonal
} from './useDowngradeToPersonal'
Expand Down Expand Up @@ -651,6 +652,32 @@ describe('useDowngradeToPersonal', () => {
expect(mockRemoveMember).toHaveBeenCalledWith('m1')
})

it('reports prior member removal when a reactivation retry returns no response', async () => {
mockMembers.value = teamWithOwnerAnd('m1')
mockPreviewSubscribe.mockResolvedValue({
allowed: true,
transition_type: 'downgrade',
cost_today_cents: 1500
})
mockSubscribe.mockRejectedValueOnce(
Object.assign(new Error('reactivation confirmation required'), {
code: 'REACTIVATION_CONFIRMATION_REQUIRED'
})
)
const { downgradeToPersonal } = useDowngradeToPersonal()

await expect(downgradeToPersonal('founder-monthly')).rejects.toThrow(
ReactivationConfirmationRequiredError
)

mockMembers.value = teamWithOwnerAnd()
mockSubscription.value = { isCancelled: true }
mockSubscribe.mockResolvedValueOnce(undefined)
await expect(
downgradeToPersonal('founder-monthly', true, 1500)
).rejects.toThrow('subscription.downgrade.failedAfterMemberRemoval')
})

it('surfaces which member failed and skips the plan change', async () => {
mockMembers.value = teamWithOwnerAnd('m1', 'm2')
mockRemoveMember.mockImplementation((id: string) =>
Expand All @@ -667,6 +694,82 @@ describe('useDowngradeToPersonal', () => {
})
Comment thread
christian-byrne marked this conversation as resolved.

Comment thread
christian-byrne marked this conversation as resolved.
describe('downgradeToPersonal telemetry', () => {
it('keeps one telemetry attempt across an authoritative reactivation retry', async () => {
mockMembers.value = teamWithOwnerAnd('m1')
mockPreviewSubscribe.mockResolvedValue({
allowed: true,
transition_type: 'downgrade',
cost_today_cents: 1500
})
mockSubscribe.mockRejectedValueOnce(
Object.assign(new Error('reactivation confirmation required'), {
code: 'REACTIVATION_CONFIRMATION_REQUIRED'
})
)
const { downgradeToPersonal } = useDowngradeToPersonal()

await expect(downgradeToPersonal('founder-monthly')).rejects.toThrow(
ReactivationConfirmationRequiredError
)
await downgradeToPersonal('founder-monthly', true, 1500)

const lifecycle = mockTrackBillingEvent.mock.calls.map(
([event]) => `${event.operation}.${event.stage}`
)
expect(lifecycle).toEqual([
'downgrade_to_personal.started',
'subscription_checkout.started',
'operation.started',
'downgrade_to_personal.succeeded',
'subscription_checkout.succeeded',
'operation.succeeded'
])
})

it('keeps one telemetry attempt when the reactivation amount changes', async () => {
mockMembers.value = teamWithOwnerAnd('m1')
mockPreviewSubscribe.mockResolvedValue({
allowed: true,
transition_type: 'downgrade',
cost_today_cents: 1500
})
mockSubscribe.mockRejectedValueOnce(
Object.assign(new Error('reactivation confirmation required'), {
code: 'REACTIVATION_CONFIRMATION_REQUIRED'
})
)
const { downgradeToPersonal } = useDowngradeToPersonal()

await expect(downgradeToPersonal('founder-monthly')).rejects.toThrow(
ReactivationConfirmationRequiredError
)

mockMembers.value = teamWithOwnerAnd()
mockSubscription.value = { isCancelled: true }
mockPreviewSubscribe.mockResolvedValue({
allowed: true,
transition_type: 'downgrade',
cost_today_cents: 2000
})
await expect(
downgradeToPersonal('founder-monthly', true, 1500)
).rejects.toThrow(ReactivationAmountChangedError)
await downgradeToPersonal('founder-monthly', true, 2000)

const lifecycle = mockTrackBillingEvent.mock.calls.map(
([event]) => `${event.operation}.${event.stage}`
)
expect(lifecycle).toEqual([
'downgrade_to_personal.started',
'subscription_checkout.started',
'operation.started',
'downgrade_to_personal.succeeded',
'subscription_checkout.succeeded',
'operation.succeeded'
])
expect(mockRemoveMember).toHaveBeenCalledTimes(1)
})

it('tracks the start of the downgrade with the pending removal count', async () => {
mockMembers.value = teamWithOwnerAnd('m1', 'm2')
const { downgradeToPersonal } = useDowngradeToPersonal()
Expand Down
Loading
Loading