Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7209fe4
feat(billing): render the Enterprise tier and hide its self-serve pri…
comfydesigner Aug 18, 2026
5ee9b28
fix(billing): stop the billing policy state crashing on runtime-only …
comfydesigner Aug 18, 2026
ca29a67
fix(billing): close enterprise gaps from review — slug-only credits f…
comfydesigner Aug 18, 2026
fe0a575
feat(billing): route enterprise plan cancellation through sales
comfydesigner Aug 18, 2026
1d9543b
fix(billing): keep ended enterprise plans off the self-serve subscrib…
comfydesigner Aug 18, 2026
b849cd0
test(billing): cover the enterprise-tier path for cancellation gating
comfydesigner Aug 19, 2026
af2f2a8
fix(billing): close the enterprise fail-open paths — lapsed classific…
comfydesigner Aug 19, 2026
2b8fe82
test(billing): local-distribution tripwire for unrecognised tiers
comfydesigner Aug 19, 2026
844e4c6
fix: cover enterprise billing states
dante01yoon Aug 19, 2026
5e9d79b
Merge branch 'main' into comfydesigner/enterprise-tier-rendering
dante01yoon Aug 19, 2026
d296689
test: give the dialog-renderer billing mock a subscription ref
comfydesigner Aug 20, 2026
d222055
test: cover enterprise billing journeys
dante01yoon Aug 19, 2026
bb0bc30
test: close enterprise e2e regression gaps
dante01yoon Aug 19, 2026
8743dc5
test: separate unrecognized billing tier regression
dante01yoon Aug 20, 2026
8bf9f76
feat: render unrecognized tiers as Current plan with no catalog content
comfydesigner Aug 20, 2026
b38acfa
fix(billing): align enterprise workspace actions with design
dante01yoon Aug 20, 2026
32c7f10
Merge origin/main into comfydesigner/enterprise-tier-rendering
dante01yoon Aug 21, 2026
2e89c37
Merge remote-tracking branch 'origin/main' into dante/resolve-pr-1540…
dante01yoon Aug 21, 2026
9fd27ea
test: provide subscription in billing context mock
dante01yoon Aug 21, 2026
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
3 changes: 3 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2864,6 +2864,9 @@
"free": {
"name": "Free"
},
"enterprise": {
"name": "Enterprise"
},
"founder": {
"name": "Founder's Edition"
},
Expand Down
7 changes: 6 additions & 1 deletion src/platform/cloud/subscription/components/CreditsTile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables
import { useBillingPolicyCapabilities } from '@/platform/cloud/subscription/composables/useBillingPolicyCapabilities'
import {
DEFAULT_TIER_KEY,
isEnterprisePlanSlug,
isEnterpriseTier,
toTierKey,
getTierCredits
} from '@/platform/cloud/subscription/constants/tierPricing'
Expand Down Expand Up @@ -283,7 +285,10 @@ const tierKey = computed(() => {
const creditPoolTotalCredits = computed<number | null>(() => {
const monthlyCredits =
currentTeamCreditStop.value?.credits_monthly ??
getTierCredits(tierKey.value)
(isEnterpriseTier(subscription.value?.tier) ||
isEnterprisePlanSlug(subscription.value?.planSlug)
? null
: getTierCredits(tierKey.value))
if (monthlyCredits === null) return null
return subscription.value?.duration === 'ANNUAL'
? monthlyCredits * 12
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { describe, expect, it } from 'vitest'

import type { IngestSubscriptionTier } from '@/platform/cloud/subscription/constants/tierPricing'

import { deriveBillingPolicyState } from './useBillingPolicyState'

const runtimeTier = (tier: string) => tier as unknown as IngestSubscriptionTier

describe('deriveBillingPolicyState', () => {
it.for<[string, boolean]>([
['LocalWithoutActiveSubscription', false],
Expand Down Expand Up @@ -64,6 +68,40 @@ describe('deriveBillingPolicyState', () => {
}
)

it.for<[string, boolean]>([
['LocalAndTeam', false],
['CloudAndTeam', true]
])(
'maps an active Enterprise plan to the team policy state %s (isCloud=%s)',
([kind, isCloud]) => {
expect(
deriveBillingPolicyState({
isCloud,
canAccessSubscriptionFeatures: true,
isTeamPlan: false,
tier: runtimeTier('ENTERPRISE')
})
).toEqual({ kind })
}
)

it.for<[string, boolean]>([
['LocalAndUnknown', false],
['CloudAndUnknown', true]
])(
'degrades a tier outside the generated enum to %s instead of crashing (isCloud=%s)',
([kind, isCloud]) => {
expect(
deriveBillingPolicyState({
isCloud,
canAccessSubscriptionFeatures: true,
isTeamPlan: false,
tier: runtimeTier('ULTRA')
})
).toEqual({ kind })
}
)

it.for<[string, boolean]>([
['LocalAndTeam', false],
['CloudAndTeam', true]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { computed } from 'vue'

import { useBillingContext } from '@/composables/billing/useBillingContext'
import type { IngestSubscriptionTier } from '@/platform/cloud/subscription/constants/tierPricing'
import { isEnterpriseTier } from '@/platform/cloud/subscription/constants/tierPricing'
import { isCloud } from '@/platform/distribution/types'

import type { BillingPolicyState } from '../billingPolicyState'
Expand Down Expand Up @@ -30,6 +31,12 @@ export function deriveBillingPolicyState(input: {
return { kind: `${distribution}WithoutActiveSubscription` }
}

// ENTERPRISE arrives as a runtime string before the generated enum carries
// it; it is a workspace-level plan, so it takes the team policy state.
if (isEnterpriseTier(input.tier)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — The Enterprise branch sits after the canAccessSubscriptionFeatures early return, so a lapsed or paused Enterprise plan with isTeamPlan false resolves to WithoutActiveSubscription instead of the team-flavored state; that policy sets showsSubscribeUpsellUI: true, rendering the "Upgrade to add credits" button in CreditsTile which calls showPricingTable — the exact self-serve entry point this PR blocks elsewhere. The check also ignores planSlug, so the slug-only Enterprise shape falls through entirely. Raised by 4 of 8 reviewers (gpt-5.6-sol-max adversarial, gpt-5.6-sol-max edge-case, claude-opus-5-thinking-max edge-case, gemini-3.1-pro edge-case).

return { kind: `${distribution}AndTeam` }
}

switch (input.tier) {
case 'FREE':
return { kind: `${distribution}AndFree` }
Expand All @@ -45,8 +52,10 @@ export function deriveBillingPolicyState(input: {
return { kind: `${distribution}AndTeam` }
case null:
return { kind: `${distribution}AndUnknown` }
default:
return input.tier satisfies never
default: {
input.tier satisfies never
return { kind: `${distribution}AndUnknown` }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useRoute, useRouter } from 'vue-router'

import { useBillingContext } from '@/composables/billing/useBillingContext'
import {
isEnterprisePlanSlug,
isEnterpriseTier
} from '@/platform/cloud/subscription/constants/tierPricing'
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
import {
clearPreservedQuery,
Expand Down Expand Up @@ -102,7 +106,7 @@
const route = useRoute()
const router = useRouter()
const subscriptionDialog = useSubscriptionDialog()
const { teamCreditStops, fetchPlans } = useBillingContext()
const { subscription, teamCreditStops, fetchPlans } = useBillingContext()
const { permissions } = useWorkspaceUI()

/** Reads `?pricing=`, strips it, and opens the table when the gate allows. */
Expand Down Expand Up @@ -139,6 +143,14 @@
if (typeof param !== 'string' || !param) return

if (!permissions.value.canManageSubscription) return
// Enterprise is sales-managed: the pricing table never opens for it, even
// from a deep link. The param was already stripped above.
if (
isEnterpriseTier(subscription.value?.tier) ||

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens the selected plan confirmation from a marketing deep link

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:137:11

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens on the personal tab for ?pricing=personal

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:126:11

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens on the personal tab for ?pricing=personal

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:126:11

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens on the personal tab for ?pricing=personal

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:126:11

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens on the team tab for ?pricing=team

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:115:11

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens on the team tab for ?pricing=team

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:115:11

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens on the team tab for ?pricing=team

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:115:11

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens the pricing table for any owner capability

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:103:11

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens the pricing table for any owner capability

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:103:11

Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts

View workflow job for this annotation

GitHub Actions / test

src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts > usePricingTableUrlLoader > opens the pricing table for any owner capability

TypeError: Cannot read properties of undefined (reading 'value') ❯ loadPricingTableFromUrl src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts:149:37 ❯ src/platform/cloud/subscription/composables/usePricingTableUrlLoader.test.ts:103:11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High — The Enterprise gate reads subscription.value synchronously, but this loader runs from GraphCanvas's onMounted via runUrlActionLoaders() with nothing awaiting billing initialization, and the ?pricing= param is replayed right after a login redirect via the preserved-query system — exactly when status is still in flight — so the gate fails open. Unlike canManageSubscription, it is also never re-evaluated after the later await fetchPlans(); either await/watch the subscription before deciding or re-check after the fetch. Raised by 5 of 8 reviewers (gpt-5.6-sol-max adversarial, gpt-5.6-sol-max edge-case, claude-opus-5-thinking-max adversarial, claude-opus-5-thinking-max edge-case, kimi-k3-max adversarial, kimi-k3-max edge-case).

isEnterprisePlanSlug(subscription.value?.planSlug)
) {
return
}

const teamCheckoutRequest = getTeamCheckoutRequest(
param,
Expand Down
16 changes: 15 additions & 1 deletion src/platform/cloud/subscription/constants/tierPricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,21 @@ export const DEFAULT_TIER_KEY: TierKey = 'standard'

// TEAM is workspace-level, so it maps to no key in this personal plan catalog.
export function toTierKey(tier: IngestSubscriptionTier): TierKey | null {
return tier === 'TEAM' ? null : TIER_TO_KEY[tier]
return tier === 'TEAM' ? null : (TIER_TO_KEY[tier] ?? null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 LowTIER_TO_KEY[tier] ?? null still resolves inherited object properties, so a runtime tier string of __proto__ or constructor returns an object/function instead of TierKey | null, violating the declared return type and throwing in downstream pricing lookups. Use a Map, Object.hasOwn guard, or a null-prototype record. Raised by 1 of 8 reviewers (gpt-5.6-sol-max adversarial).

}

// ENTERPRISE is a sales-managed workspace tier: absent from the personal
// catalog and (until the ingest enum ships it) from the generated types, so it
// is matched as a runtime string. It never self-serves plan changes.
const ENTERPRISE_TIER = 'ENTERPRISE'
const ENTERPRISE_PLAN_SLUG_PREFIX = 'enterprise'

export function isEnterpriseTier(tier: string | null | undefined): boolean {
return tier?.toUpperCase() === ENTERPRISE_TIER
}

export function isEnterprisePlanSlug(slug: string | null | undefined): boolean {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 LowisEnterprisePlanSlug is an unanchored prefix match on a server-supplied slug, so any future slug starting with "enterprise" (e.g. a self-serve enterprise_trial) is silently classified as sales-managed and loses its price display, benefits list, and every plan-change control with no diagnostic. An exact-match set or a delimiter-anchored enterprise_ check would avoid the over-capture. Raised by 2 of 8 reviewers (claude-opus-5-thinking-max adversarial, kimi-k3-max adversarial).

return slug?.toLowerCase().startsWith(ENTERPRISE_PLAN_SLUG_PREFIX) === true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This predicate encodes server-owned business policy in the frontend. tier === 'ENTERPRISE' || planSlug.startsWith('enterprise') means every consumer must independently combine potentially stale/conflicting fields, and a future slug can silently inherit sales-managed behavior. Please have the billing API return an explicit capability/policy contract (for example subscriptionManagementMode: 'sales_managed', canChangePlan, canCancel, canReactivate, canOpenPricing) and make the UI consume that instead. The backend should enforce those capabilities as well; hiding controls here is not sufficient.

}

// Includes the workspace-level TEAM, which toTierKey maps to null: a catalog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,29 @@ describe('CurrentUserPopoverWorkspace', () => {
expect(state.showPricingTable).toHaveBeenCalledOnce()
})

it('hides Resubscribe for a cancelled enterprise workspace', () => {
state.planSlug = 'enterprise_monthly'
state.isCancelled = true
state.canManageSubscription = true
state.canManageSubscriptionLifecycle = true
renderComponent('team')

expect(
screen.queryByRole('button', { name: 'Resubscribe' })
).not.toBeInTheDocument()
})

it('hides Plans & pricing for an enterprise workspace but keeps Manage plan', () => {
state.planSlug = 'enterprise_monthly'
state.canManageSubscription = true
renderComponent('team')

expect(
screen.queryByTestId('plans-pricing-menu-item')
).not.toBeInTheDocument()
expect(screen.getByTestId('manage-plan-menu-item')).toBeInTheDocument()
})

for (const workspaceType of ['personal', 'team'] as const) {
it(`opens workspace plan management for a ${workspaceType} owner`, async () => {
const user = userEvent.setup()
Expand Down
20 changes: 15 additions & 5 deletions src/platform/workspace/components/CurrentUserPopoverWorkspace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'

import { useExternalLink } from '@/composables/useExternalLink'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import {
isEnterprisePlanSlug,
isEnterpriseTier
} from '@/platform/cloud/subscription/constants/tierPricing'
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
import { isCloud } from '@/platform/distribution/types'
Expand Down Expand Up @@ -303,8 +307,13 @@ const displayedCredits = computed(() => {
})
})

const isEnterprisePlan = computed(
() =>
isEnterpriseTier(subscription.value?.tier) ||
isEnterprisePlanSlug(subscription.value?.planSlug)
)
const showPlansAndPricing = computed(
() => permissions.value.canManageSubscription
() => permissions.value.canManageSubscription && !isEnterprisePlan.value
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High — Hiding plans-pricing-menu-item does not close the path it guards: the subscribe/resubscribe button earlier in this same popover (showSubscribeAction && !isPersonalWorkspace) calls the identical handleOpenPlansAndPricing, and showSubscribeAction is true whenever the subscription is cancelled or inactive. An existing test already asserts that clicking Resubscribe calls showPricingTable, so a cancelled Enterprise workspace still reaches the self-serve table one click away. Raised by 4 of 8 reviewers (gpt-5.6-sol-max adversarial, gpt-5.6-sol-max edge-case, claude-opus-5-thinking-max adversarial, claude-opus-5-thinking-max edge-case).

)
const hasDelinquentSubscription = computed(
() =>
Expand All @@ -319,10 +328,11 @@ const showManagePlan = computed(
)
const showSubscribeAction = computed(
() =>
(isCancelled.value && permissions.value.canManageSubscriptionLifecycle) ||
(!canAccessSubscriptionFeatures.value &&
!hasDelinquentSubscription.value &&
permissions.value.canManageSubscription)
!isEnterprisePlan.value &&
((isCancelled.value && permissions.value.canManageSubscriptionLifecycle) ||
(!canAccessSubscriptionFeatures.value &&
!hasDelinquentSubscription.value &&
permissions.value.canManageSubscription))
)

const handleOpenUserSettings = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,82 @@ describe('SubscriptionPanelContentWorkspace', () => {
)
})

it('renders Enterprise without price, benefits, or a plan-change action', () => {
mockHasTeamPlan.value = false
mockPlanSlug.value = 'enterprise_monthly'
mockCurrentTeamCreditStop.value = null
renderComponent()

expect(screen.getByText('Enterprise')).toBeInTheDocument()
expect(screen.queryByText('$665')).not.toBeInTheDocument()
expect(screen.queryByText('USD / mo')).not.toBeInTheDocument()
expect(
screen.queryByRole('button', { name: /change plan|upgrade plan/i })
).not.toBeInTheDocument()
expect(
screen.getByText(`Renews on ${formatPanelDate(RENEWAL_DATE_ISO)}`)
).toBeInTheDocument()
})

it('hides Reactivate for a cancelled enterprise plan', () => {
mockHasTeamPlan.value = false
mockPlanSlug.value = 'enterprise_monthly'
mockCurrentTeamCreditStop.value = null
mockSubscriptionStatus.value = 'canceled'
renderComponent()

expect(
screen.queryByRole('button', { name: /reactivate/i })
).not.toBeInTheDocument()
})

it('offers no subscribe or reactivate path for an ended enterprise plan', () => {
mockHasTeamPlan.value = false
mockPlanSlug.value = 'enterprise_monthly'
mockCurrentTeamCreditStop.value = null
mockSubscriptionStatus.value = 'ended'
mockIsActiveSubscription.value = false
renderComponent()

expect(screen.getByText('Enterprise')).toBeInTheDocument()
expect(
screen.queryByRole('button', { name: /subscribe|reactivate/i })
).not.toBeInTheDocument()
})

it('labels a scheduled change to Enterprise', () => {
const basePlans = mockPlans.value
mockScheduledPlanSlug.value = 'enterprise_monthly'
mockChangeAt.value = END_DATE_ISO
mockPlans.value = [
...basePlans,
{
slug: 'enterprise_monthly',
tier: 'PRO',
duration: 'MONTHLY',
price_cents: 0,
credits_cents: 0,
max_seats: 1,
availability: { available: true },
seat_summary: {
seat_count: 1,
total_cost_cents: 0,
total_credits_cents: 0
}
}
]
try {
renderComponent()
expect(
screen.getByText(
`Changes to Enterprise on ${formatPanelDate(END_DATE_ISO)}`
)
).toBeInTheDocument()
} finally {
mockPlans.value = basePlans
}
})

it('shows a scheduled plan change instead of the renewal date', () => {
mockScheduledPlanSlug.value = 'pro-annual'
mockChangeAt.value = END_DATE_ISO
Expand Down
Loading
Loading