Keep plan pressed just after a downgrade takes effect answers nothing and records the downgrade as withdrawn #275
Open
opened 2026-10-26 20:58:24 +00:00 by cgalo5758
·
0 comments
Labels
Clear labels
accessibility
area/billing
area/catalog
area/discourse
area/domains
area/entitlements
area/fedwiki
area/identity
area/integrations
area/licensing
area/member-ui
area/meta
area/operator-ui
area/ops
area/testing
duplicate
good-first-issue
invalid
privacy
security
upstream
wontfix
A barrier for people using assistive technology or a keyboard alone.
The Stripe mirror, checkout, subscriptions, invoices, fulfillment.
Products, prices, plan ladders, purchasability.
The Discourse integration.
The domains registry, claims, placements, the certificate ask.
Entitlement sets, rules, grants, pools, provisioning.
The Federated Wiki integration and farm sync.
Sign-in, sessions, persons, organizations, workspaces, roles.
The provider registry, outbox and webhooks in general.
Licenses, the contributor agreement, SPDX headers.
Member pages.
The repository itself, its contributing guide, CI, the tracker and the workflow.
Operator pages, forms, lists, the design system.
Deployment, configuration, migrations, workflows, instance settings.
The test stack, screens, lint, walkthroughs.
Closed because another issue already covers it.
Small, self-contained, and explained enough to be a first contribution.
Closed because it is not a ticket for this repository.
Touches what a person's data reveals.
Touches authentication, authorization, secrets or data exposure.
Waits on another repository or project before it can move.
Closed because it will not be done, with the reason in the last comment.
kind
bug
The software does something other than what it promises; closed when it again does what it promises.
kind
debt
Code, tests or tooling to clean up with nothing visible changing; closed when they are cleaner.
kind
design
A question to settle before work can be defined; closed when the decision is written down.
kind
docs
Documentation that is wrong or missing; closed when it says the right thing.
kind
enhancement
Something the software does not do yet; closed when it does.
priority
critical
Blocks the active milestone or harms members now.
priority
high
Next in line inside the active milestone.
priority
low
Inside the active milestone, when nothing else is left.
priority
medium
Inside the active milestone, after the high ones.
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: wiki-cafe/member-console#275
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What happens
A member on Pro schedules a downgrade to Basic. Just after the renewal, they press Keep plan on the Products page. The page comes back with Basic as the current plan and no message. The console's records then say the member withdrew the downgrade, although Stripe carried it out.
A downgrade waits for the renewal.
downgradeAtRenewal(internal/fulfillment/plan_change.go:378) attaches a Stripe subscription schedule whose second phase starts the lower price at the period end (lines 417-432). It records the move as ascheduledrow incore.subscription_scheduled_changes, the console's record of each plan move that waits for a date. Keep plan (KeepCurrentPlan,plan_change.go:469) withdraws a scheduled downgrade or cancellation. It releases the schedule, marks the rowsupersededand reconciles. The reconcile (ReconcileSubscription,internal/fulfillment/reconcile.go) reads the subscription from Stripe and brings the console's records in line with it. It runs after each Stripe subscription event and from the hourly billing sweep. On an active subscription it marks the row of a downgrade that has landedapplied(applyLandedDowngrades,reconcile.go:1127).At the renewal, Stripe enters the schedule's second phase. The subscription now carries Basic's price, and the renewal invoice bills Basic. Until the reconcile runs on that event, the console still reads Pro with the downgrade scheduled. The plan's card still shows "moves to Basic on {date}" and Keep plan.
scheduledChanges(internal/server/plan_offer.go:175) drops both once the date has passed, but only for a suspended plan, meaning one whose subscription Stripe holds aspast_due,unpaidorpaused.When Keep plan is pressed in that interval, it reads the live subscription from Stripe (
plan_change.go:489). It checks whether the downgrade has already landed only when the plan is suspended (lines 493-498). On an active plan it goes on as if the downgrade were still waiting:releaseSpentSchedule,reconcile.go:1213).superseded(line 509), so the record shows a downgrade the member withdrew, not one Stripe applied.member:keep(reconcile.go:107), instead of the Stripe event's reason.internal/server/member_plan_moves.go:363-365), so the card just turns into Basic's.A suspended plan in the same state is refused with "That plan change has already taken effect.", and nothing changes. Tests cover that case at
internal/fulfillment/suspended_subscription_test.go:211and:241. No test covers an active plan. The check was written for suspended plans because their reconcile leaves scheduled rows alone (reconcile.go:123-126). On an active plan, a reconcile closes the row first (lines 114-122), and after that Keep plan finds nothing scheduled and changes nothing (plan_change.go:485-487). So the gap lasts from the renewal until its first reconcile. That is minutes when Stripe's event arrives, and up to the next hourly sweep when it does not.What should happen
Keep plan on any plan, active or suspended, refuses a scheduled downgrade that Stripe has already applied. It answers "That plan change has already taken effect." and releases and supersedes nothing. The reconcile then marks the row
applied, asopenspec/specs/stripe-subscription-management/spec.md:404requires. Once the downgrade's date has passed, the card stops showing the "moves to" line and Keep plan for an active plan as it does for a suspended one. The check atplan_change.go:493-498already reads the live subscription, so running it for every plan may be the whole fix on the act's side.Where
internal/fulfillment/plan_change.go:493(KeepCurrentPlan),internal/server/plan_offer.go:175(scheduledChanges)Steps
superseded.A test can build this state with the mock Stripe backend in
internal/stripetest, the wayTestKeepRefusesADowngradeThatLandeddoes, without the step that holds the subscription as past due and reconciles it.Why it matters
A member who pressed Keep plan to stay on Pro is moved to Basic with no word. The console's records say the member withdrew a downgrade that Stripe carried out, and they credit the move to the member's press.
Related: #177 asks who owns each kind of scheduled-change row and what its states mean.