fix: never pitch the current plan when a lowered limit trips the cloud connect gate - #97
Conversation
…d connect gate Seen live on UAT: a Starter workspace whose cloud-account limit an admin had lowered to 0 was offered "Switch to Starter now? (prorated)". The picker compared each catalog plan's limit against the workspace's own limit and never excluded the plan the workspace is already on, so the current plan qualified as "raising" the lowered number. A yes would have re-priced the subscription to the same plan. cheapestPlanRaising now takes the current plan id and skips it along with every plan priced at or below it, the same rule `subscription upgrade` already applies to its candidates. The Starter-at-0 case pitches Team. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
LGTM: Correct fix: the capacity gate's plan picker now excludes the current plan and anything priced at or below it, mirroring upgradeCandidates; regression pinned by unit and gate tests.
Approve — the fix is correct and well-tested; no findings.
What I checked
- The fix itself (
src/billing/plans.ts:83-99):cheapestPlanRaisingnow takescurrentPlanId, resolves its catalog price (defaulting to $0 for admin-granted plans the catalog doesn't list — the safe direction, since that keeps all paid plans offerable), and filters out the current plan plus anything priced at or below it. This exactly mirrors the pre-existingupgradeCandidatesrule forsubscription upgrade, so both surfaces now agree on what counts as an upgrade. - The one caller (
src/billing/cloud-capacity.ts:83): passesplan.plan.id; grep confirms there are no other call sites left on the old signature. - The regression scenario: with Starter's limit lowered to 0, the old code compared catalog limits against the workspace limit only, so Starter (limit 5 > 0) qualified and would be pitched to itself. New tests pin the live case (Starter→Team, Team→Scale, Scale→null) at both the unit level (
test/billing-plans.test.ts:105-117) and the gate level with exact stderr lines and the prorated-switch prompt (test/billing-cloud-capacity.test.ts:209-220). - Edge cases: unlimited-current-limit and non-selectable/free-plan behavior unchanged; existing tests updated only by adding the new argument — no assertions weakened or removed.
No spec directory in this repo, so no spec-contradiction check applied. Clean change.
|
Release receipt — shipped in v0.2.34 (cut-release 34915958044, release 34915999889, both green). The tag's commit |
|
UAT receipt, live gate yes path on 0.2.34 — Coreplane UAT workspace on Starter with cloud-account limit 0: The pitch is Team now (0.2.33 offered Starter to itself), the in-place change-plan went through Stripe test mode, the CLI waited for the webhook to flip the plan, then continued straight into the provider picker. |
On UAT the new
cloud connectgate offered a Starter workspace an upgrade to Starter. This PR makes the picker skip the plan the workspace is already on, and anything priced at or below it.What & why
Found during the UAT validation of #94: with the Coreplane UAT workspace on Starter and its cloud-account limit lowered to 0 in Polaris,
cloud connectprintedStarter ($80/mo) allows 5 cloud accountsand askedSwitch to Starter now? (prorated).cheapestPlanRaisingcompared each catalog plan's limit against the workspace's own (lowered) limit and never excluded the current plan, so it qualified. A yes would have sent a change-plan request to the same plan. Only reachable when an admin lowers a paid plan's limit, but wrong when it happens.Tour
1. The picker excludes the current plan and cheaper ones
It now takes the current plan id, looks up that plan's price in the catalog (0 when the catalog does not list it), and drops the current plan plus every plan priced at or below it before the limit comparison. This is the rule
upgradeCandidatesalready applies forsubscription upgrade, so both surfaces agree on what counts as up.cli/src/billing/plans.ts
Lines 78 to 99 in 232dbda
2. The regression, pinned
The live case as a unit test: Starter with a limit of 0 pitches Team; Team at 0 pitches Scale; Scale at 0 pitches nothing. Plus the gate-level test with the exact stderr lines and the
Switch to Team now? (prorated)question.cli/test/billing-plans.test.ts
Lines 105 to 117 in 232dbda
3. Remaining changes
src/billing/cloud-capacity.ts— passesplan.plan.idto the picker.test/billing-cloud-capacity.test.ts— the paying-workspace-with-lowered-limit case.Validation
npm test503 pass (the two new plans tests and the new gate test failed before the fix), lint clean, typecheck clean against the live spec.cloud connecton the same UAT workspace (Starter, limit 0) and seeTeam ($200/mo) allows unlimited cloud accountsandSwitch to Team now? (prorated); answer y and watch the connect continue into the picker.🤖 Generated with Claude Code