The Products page offers a plan switch on a subscription the switch cannot change #199

Open
opened 2026-10-09 16:01:30 +00:00 by cgalo5758 · 0 comments
Owner

What happens

When the plan a member's organization holds names a subscription, the Products page gives the other tiers an enabled Upgrade or Downgrade button. Either button switches the subscription's price. In some states the console cannot find the Stripe ids that the switch would send. When the member clicks the button in one of these states, the switch preview refuses with "We couldn't complete that change. Try again in a moment.":

  • The console has no row for the subscription the plan names. The current card reads "Billing details are still being processed." and offers no Cancel plan. The Downgrade button on the tier below is still enabled.
  • The subscription's Stripe mapping is missing or has no Stripe id.
  • The subscription has no item for the current tier's product, or that item's Stripe mapping is missing or has no Stripe id.

In the last two states, the current card shows its price and renewal date as usual. Nothing on the page tells this plan apart from one that can be switched. Trying again gives the same answer, and every click logs "plan move failed" as an error.

A subscription or item mapping can also be recorded in the other Stripe environment, or marked stale. That case fails one step later. The preview shows the confirmation, and then the switch refuses with "That plan is no longer available. Refresh the page and choose a current plan." After a refresh, the page draws the same enabled button.

The page draws its controls from the offer decision (plans.Offer). The position it passes to that decision counts the plan as paid for by a subscription as soon as the provision names one (positionLoader.onLadder, internal/server/plan_offer.go). GetSwitchPreview asks the same decision and gets the same yes. It then calls fulfillment.HasActiveSubscriptionOnLadder, which looks up the subscription and its item through resolveLadderSubscription. Any failure there other than "no subscription" ends in the catch-all message of renderPlansAfterMove. Only SwitchPlan checks the Stripe environment; the preview does not.

What should happen

The member offer spec says the Products page, the switch and its preview all decide through one function, "so no surface offers or accepts a move another refuses." So a tier whose switch cannot be carried out gets no enabled switch. To make that decision, the position records whether the subscription and its current item can be reached, as it already records for the target price. The preview refuses anything the switch would refuse, with the same answer.

Where

internal/server/plan_offer.go (positionLoader.onLadder, switchOffer); internal/server/member_plan_moves.go (GetSwitchPreview, renderPlansAfterMove); internal/fulfillment/plan_change.go (HasActiveSubscriptionOnLadder, resolveLadderSubscription, PreviewSwitch, SwitchPlan). #195 covers how a refused switch answers, and #197 covers a failed read in the same lookup.

Steps

TestPlansDrawAnUnprojectedSubscription in internal/server/member_plans_offer_db_test.go builds the first state: a provision that names a subscription with no row. The test asserts that the lower tier's switch is enabled. On the Products page, clicking that Downgrade gets the catch-all message.

Why it matters

A member is offered a plan change that cannot succeed and is told to try again. Reconcile writes the subscription, its mapping, its items, their mappings and the plan they pay for in one transaction. So the console's own writes do not leave the first three states, but rows written any other way do, and the page already has its own line for the first.

## What happens When the plan a member's organization holds names a subscription, the Products page gives the other tiers an enabled Upgrade or Downgrade button. Either button switches the subscription's price. In some states the console cannot find the Stripe ids that the switch would send. When the member clicks the button in one of these states, the switch preview refuses with "We couldn't complete that change. Try again in a moment.": - The console has no row for the subscription the plan names. The current card reads "Billing details are still being processed." and offers no Cancel plan. The Downgrade button on the tier below is still enabled. - The subscription's Stripe mapping is missing or has no Stripe id. - The subscription has no item for the current tier's product, or that item's Stripe mapping is missing or has no Stripe id. In the last two states, the current card shows its price and renewal date as usual. Nothing on the page tells this plan apart from one that can be switched. Trying again gives the same answer, and every click logs "plan move failed" as an error. A subscription or item mapping can also be recorded in the other Stripe environment, or marked stale. That case fails one step later. The preview shows the confirmation, and then the switch refuses with "That plan is no longer available. Refresh the page and choose a current plan." After a refresh, the page draws the same enabled button. The page draws its controls from the offer decision (`plans.Offer`). The position it passes to that decision counts the plan as paid for by a subscription as soon as the provision names one (`positionLoader.onLadder`, `internal/server/plan_offer.go`). `GetSwitchPreview` asks the same decision and gets the same yes. It then calls `fulfillment.HasActiveSubscriptionOnLadder`, which looks up the subscription and its item through `resolveLadderSubscription`. Any failure there other than "no subscription" ends in the catch-all message of `renderPlansAfterMove`. Only `SwitchPlan` checks the Stripe environment; the preview does not. ## What should happen The member offer spec says the Products page, the switch and its preview all decide through one function, "so no surface offers or accepts a move another refuses." So a tier whose switch cannot be carried out gets no enabled switch. To make that decision, the position records whether the subscription and its current item can be reached, as it already records for the target price. The preview refuses anything the switch would refuse, with the same answer. ## Where `internal/server/plan_offer.go` (`positionLoader.onLadder`, `switchOffer`); `internal/server/member_plan_moves.go` (`GetSwitchPreview`, `renderPlansAfterMove`); `internal/fulfillment/plan_change.go` (`HasActiveSubscriptionOnLadder`, `resolveLadderSubscription`, `PreviewSwitch`, `SwitchPlan`). #195 covers how a refused switch answers, and #197 covers a failed read in the same lookup. ## Steps `TestPlansDrawAnUnprojectedSubscription` in `internal/server/member_plans_offer_db_test.go` builds the first state: a provision that names a subscription with no row. The test asserts that the lower tier's switch is enabled. On the Products page, clicking that Downgrade gets the catch-all message. ## Why it matters A member is offered a plan change that cannot succeed and is told to try again. Reconcile writes the subscription, its mapping, its items, their mappings and the plan they pay for in one transaction. So the console's own writes do not leave the first three states, but rows written any other way do, and the page already has its own line for the first.
cgalo5758 added the
kind
bug
area/billingarea/member-ui
labels 2026-10-09 16:01:30 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wiki-cafe/member-console#199