mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
fix(cloud): EP-11 the plan blocker says 'ended' only for a plan that existed
The creating sheet's (and waking card's) plan block always said 'Your team's plan has ended.', even to a brand-new team that never paid. The body now follows billing's reason: subscription_inactive -> ended; trial_used_up -> trial used up; no_subscription / trial_unavailable -> no plan yet; unknown -> needs a plan. Fixtures for each; tests.
This commit is contained in:
@@ -42,7 +42,7 @@ export function cloudWakeBillingCopy(
|
||||
// Running limits and anything unknown: the shared workspace copy (it says what to do).
|
||||
return { body: describeError('entitlement_denied', reason, t), fix: null }
|
||||
}
|
||||
const copy = blockerCopy(kind, t)
|
||||
const copy = blockerCopy(kind, t, reason)
|
||||
// The sheet's payment line is about new sandboxes; this one already exists.
|
||||
const body =
|
||||
kind === 'payment'
|
||||
|
||||
@@ -38,7 +38,17 @@ afterEach(() => {
|
||||
|
||||
function renderBlocker(
|
||||
kind: BillingBlockKind,
|
||||
{ canManageBilling = true, capCents = 2500, offers = true } = {}
|
||||
{
|
||||
canManageBilling = true,
|
||||
capCents = 2500,
|
||||
offers = true,
|
||||
reason
|
||||
}: {
|
||||
canManageBilling?: boolean
|
||||
capCents?: number
|
||||
offers?: boolean
|
||||
reason?: string | null
|
||||
} = {}
|
||||
): {
|
||||
onContinue: ReturnType<typeof vi.fn>
|
||||
onBackToDraft: ReturnType<typeof vi.fn>
|
||||
@@ -53,7 +63,8 @@ function renderBlocker(
|
||||
entitlement={{
|
||||
...sampleTeam,
|
||||
offers: offers ? sampleTeam.offers : null,
|
||||
overage: { ...sampleTeam.overage, spendCapCents: capCents }
|
||||
overage: { ...sampleTeam.overage, spendCapCents: capCents },
|
||||
...(reason !== undefined ? { reason: reason as never } : {})
|
||||
}}
|
||||
onContinue={onContinue}
|
||||
onBackToDraft={onBackToDraft}
|
||||
@@ -147,6 +158,20 @@ describe('billing blocker on the creating sheet (JA-21)', () => {
|
||||
expect(openPlansPage).toHaveBeenCalledWith('sandbox')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['subscription_inactive', 'Your team’s plan has ended.'],
|
||||
['trial_used_up', 'Your team’s trial is used up.'],
|
||||
['no_subscription', 'Your team doesn’t have a plan yet.'],
|
||||
['trial_unavailable', 'Your team doesn’t have a plan yet.'],
|
||||
[null, 'Your team needs a plan to use Orca Cloud.']
|
||||
] as const)('no plan (%s): only a plan that existed has "ended" (EP-11)', (reason, body) => {
|
||||
renderBlocker('plan', { reason })
|
||||
expect(screen.getByText(body)).toBeTruthy()
|
||||
if (reason !== 'subscription_inactive') {
|
||||
expect(screen.queryByText('Your team’s plan has ended.')).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
['hoursUsed', 'Ask an owner or admin to add hours.'],
|
||||
['spendCap', 'Ask an owner or admin to raise the spend cap.'],
|
||||
|
||||
@@ -22,20 +22,42 @@ export type BillingBlockerPhase =
|
||||
/** A fix in the browser (portal): re-check on return, or with Check again. */
|
||||
| { kind: 'browser'; checking: boolean; stillBlocked: boolean }
|
||||
|
||||
/** Why there is no plan, in billing's words: only a plan that existed has "ended". */
|
||||
function planBlockBody(reason: string | null | undefined, t: Translate): string {
|
||||
switch (reason) {
|
||||
case 'subscription_inactive':
|
||||
return t('worktreeCreation.billingBlock.plan.body2', 'Your team’s plan has ended.')
|
||||
case 'trial_used_up':
|
||||
return t(
|
||||
'worktreeCreation.billingBlock.plan.bodyTrialUsedUp',
|
||||
'Your team’s trial is used up.'
|
||||
)
|
||||
case 'no_subscription':
|
||||
case 'trial_unavailable':
|
||||
return t(
|
||||
'worktreeCreation.billingBlock.plan.bodyNoPlan',
|
||||
'Your team doesn’t have a plan yet.'
|
||||
)
|
||||
default:
|
||||
return t(
|
||||
'worktreeCreation.billingBlock.plan.bodyNeedsPlan',
|
||||
'Your team needs a plan to use Orca Cloud.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The blocker's words per refusal; also the waking card's, so a refused wake says the same. */
|
||||
export function blockerCopy(
|
||||
kind: BillingBlockKind,
|
||||
t: Translate
|
||||
t: Translate,
|
||||
/** Billing's refusal reason: says whether a plan ended, a trial ran out, or there never was one (EP-11). */
|
||||
reason?: string | null
|
||||
): { title: string; body: string; ask: string } {
|
||||
switch (kind) {
|
||||
case 'plan':
|
||||
return {
|
||||
title: t('worktreeCreation.billingBlock.plan.title', 'Choose a plan'),
|
||||
body: t(
|
||||
// Only teams that had Orca Cloud reach the sheet; a never-planned team gets setup.
|
||||
'worktreeCreation.billingBlock.plan.body2',
|
||||
'Your team’s plan has ended.'
|
||||
),
|
||||
body: planBlockBody(reason, t),
|
||||
ask: t('worktreeCreation.billingBlock.plan.ask', 'Ask an owner or admin to choose a plan.')
|
||||
}
|
||||
case 'hoursUsed':
|
||||
@@ -73,6 +95,8 @@ export function blockerCopy(
|
||||
|
||||
export type VmBuildBillingBlockerViewProps = {
|
||||
kind: BillingBlockKind
|
||||
/** Billing's refusal reason (plan blocks word "ended" vs "no plan yet" from it). */
|
||||
reason?: string | null
|
||||
canManageBilling: boolean
|
||||
phase: BillingBlockerPhase
|
||||
/** Spend cap reached: the field's starting value, in whole dollars. */
|
||||
@@ -93,6 +117,7 @@ export type VmBuildBillingBlockerViewProps = {
|
||||
*/
|
||||
export function VmBuildBillingBlockerView({
|
||||
kind,
|
||||
reason,
|
||||
canManageBilling,
|
||||
phase,
|
||||
suggestedCapDollars,
|
||||
@@ -106,7 +131,7 @@ export function VmBuildBillingBlockerView({
|
||||
}: VmBuildBillingBlockerViewProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const locale = useIntlLocale()
|
||||
const copy = blockerCopy(kind, t)
|
||||
const copy = blockerCopy(kind, t, reason)
|
||||
const capId = React.useId()
|
||||
const [cap, setCap] = React.useState(String(suggestedCapDollars))
|
||||
const headingRef = React.useRef<HTMLHeadingElement>(null)
|
||||
@@ -340,6 +365,7 @@ export function VmBuildBillingBlocker({
|
||||
return (
|
||||
<VmBuildBillingBlockerView
|
||||
kind={kind}
|
||||
reason={entitlement?.reason ?? null}
|
||||
canManageBilling={canManageBilling}
|
||||
phase={phase}
|
||||
suggestedCapDollars={suggestedCapDollars}
|
||||
|
||||
@@ -41,6 +41,28 @@ export const cloudPlanRequired = cloudFailure(
|
||||
''
|
||||
)
|
||||
|
||||
// EP-11: the plan blocker's body follows billing's reason; only a plan that existed has "ended".
|
||||
export const cloudPlanRequiredNeverPaid = cloudFailure(
|
||||
'cloudPlanRequired',
|
||||
'Orca Cloud workspaces need a plan with cloud hours. Open Plan & billing to choose one.',
|
||||
'',
|
||||
{ plan: null, status: 'none', reason: 'no_subscription' }
|
||||
)
|
||||
|
||||
export const cloudPlanRequiredEnded = cloudFailure(
|
||||
'cloudPlanRequired',
|
||||
'Orca Cloud workspaces need a plan with cloud hours. Open Plan & billing to choose one.',
|
||||
'',
|
||||
{ status: 'canceled', reason: 'subscription_inactive' }
|
||||
)
|
||||
|
||||
export const cloudPlanRequiredTrialUsedUp = cloudFailure(
|
||||
'cloudPlanRequired',
|
||||
'Orca Cloud workspaces need a plan with cloud hours. Open Plan & billing to choose one.',
|
||||
'',
|
||||
{ plan: null, status: 'trial', reason: 'trial_used_up' }
|
||||
)
|
||||
|
||||
export const cloudConcurrencyLimit = cloudFailure(
|
||||
'cloudConcurrencyLimit',
|
||||
'You already have as many Orca Cloud workspaces running as your plan allows.',
|
||||
|
||||
@@ -29682,7 +29682,10 @@
|
||||
"plan": {
|
||||
"title": "Choose a plan",
|
||||
"ask": "Ask an owner or admin to choose a plan.",
|
||||
"body2": "Your team’s plan has ended."
|
||||
"body2": "Your team’s plan has ended.",
|
||||
"bodyTrialUsedUp": "Your team’s trial is used up.",
|
||||
"bodyNoPlan": "Your team doesn’t have a plan yet.",
|
||||
"bodyNeedsPlan": "Your team needs a plan to use Orca Cloud."
|
||||
},
|
||||
"hours": {
|
||||
"title": "Out of sandbox hours",
|
||||
|
||||
Reference in New Issue
Block a user