A grant over a paid tier uses up time the member paid for, or gives part of a period away, depending on where the subscription's renewal date falls #280
Open
opened 2026-10-27 16:26:31 +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#280
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 a person cannot do today
Some terms first. A plan ladder is a ranked sequence of tiers, and an organization holds one tier per ladder at a time. A grant is an operator's record that gives an organization a tier without payment. When an operator grants a tier on a ladder where the organization's own subscription holds a tier, the grant takes the ladder and the subscription delivers nothing there until the grant ends. As decided on #269, the subscription's payment collection is paused at Stripe for that time (
pause_collectionwithbehavior=void): each renewal invoice Stripe creates while the grant holds is voided, and collection resumes when the grant ends.The pause moves no money at the grant's two edges. A member billed on the 1st who is granted a tier on the 10th has already paid for the whole period, and the paid tier delivers nothing from the 10th. When a grant ends on the 20th of a later period, that period's renewal was voided, so the paid tier delivers from the 20th to the period's end unpaid. The two partial periods do not offset: with a 30-day period, a grant from day 10 to day 20 of one period costs the member 10 paid days; a grant from day 25 of one period to day 5 of a period two later gives the member 20 days free; on an annual price, a 7-day grant that starts 4 days before renewal gives the member almost a year free. Which way it falls is decided by where the renewal date happens to sit, which neither the member nor the operator chose, and a pause or resume call that reaches Stripe late, after a renewal was charged or voided, shifts a whole period one way or the other with nothing on any page saying so. Only a grant of whole periods comes out exact.
So a member cannot rely on a grant leaving the time they paid for intact, and an operator cannot give a grant of any length without possibly giving away up to a whole period, a year on an annual price.
What they should be able to do
A subscription pays only for the time in which it delivers its own tier. A grant does not use up the time the member paid for: when the grant ends, that time is delivered after it, and the next charge is a full period at the subscription's price once that time runs out. The same rule holds on every deployment, with no setting.
The proposed form gives the time back as time, not money. In the update that clears the pause when the grant ends, the console also moves the subscription's next billing date to
N = t_e + (P - t_s), wheret_sis when the grant took the ladder,t_eis when the subscription delivers again, andPis the end of the member's paid coverage att_s; Stripe documents moving an existing subscription's billing date withtrial_endandproration_behavior=none, which makes no invoice until that date. WhenNfalls within a few days of the current period's end, the console only clears the pause.Nis computed from the periods the member actually paid for (their invoices' line periods, net of any refund or credit note) and from the grant's interval in the transitions ledger, not from what Stripe voided, so a late pause or resume call still comes out exact. A grant of whole periods leaves the renewal date where it was; any other grant moves it by the difference.Money instead of time (a credit note and refund at the grant's start, an invoice item at its end) is the fallback, only if the time form fails the checks below: it needs three more permissions on the console's restricted Stripe key (credit notes, invoice items and refunds), so a deployment without them would quietly keep today's behaviour, and it issues refunds and credit notes the operator must reconcile.
Costs of the time form, accepted unless the checks show otherwise:
trialing, and with customer emails on, Stripe sends its trial-ending reminder before the next charge. The console already treatstrialingas delivering (internal/fulfillment/reconcile.go), and the operator Overview counts such subscriptions in its "trialing" caption (internal/server/operator_overview.go), whose wording then needs a decision.Why it matters
The console is meant for any deployment and any grant: short comps, sponsorships of a year, open-ended grants. Today a grant's cost to the member, or to the operator, depends on an accident of dates, and on annual prices the accident is large.
Where
The act that ends a grant's hold and the update that clears the pause (#269, #270); the switch (
internal/fulfillment/plan_change.go); the subscription reconcile (internal/fulfillment/reconcile.go); the grant's interval (core.pool_provision_transitions);docs/stripe.md;openspec/specs/stripe-subscription-management/spec.md. A service period is read from invoice line periods, never fromcore.invoices.period_start, which on a subscription invoice looks back one period.Done when
Before the change is specified, the time form has had one adversarial review, and these have been checked in a Stripe sandbox at the console's pinned API version:
trial_endat two years from it);customer.subscription.trial_will_endwith customer emails on, with the email's wording recorded;Then: every grant that ends after the change ships leaves the member the paid time it covered, delivered after the grant, with the next charge at the moved date; a whole-period grant leaves the renewal date unchanged; the console records, for each ended grant, its interval, the paid coverage used, the moved date and the date Stripe confirmed;
docs/stripe.mdstates the rule; and tests drive a short grant, a grant across a renewal, a whole-period grant, an annual price, a late pause call, and a switch during the grant.