When a grant over a paid tier ends, the subscription that pays for that tier gets it back only at its next Stripe event, not when the grant ends #270

Closed
opened 2026-10-26 20:58:22 +00:00 by cgalo5758 · 2 comments
Owner

What a person cannot do today

Some terms first. A plan ladder is a ranked sequence of tiers, for example Plus, Pro and Max. An organization holds one tier per ladder at a time: from a subscription it pays for in Stripe, from a grant an operator issues, or from its organization type's default, a free tier the console confers when the organization holds nothing. A reconcile is the console reading a subscription back from Stripe and bringing the organization's tiers into line with it.

An operator can issue a grant over a tier that a subscription pays for. The grant takes the ladder; the subscription keeps billing and delivers nothing there, and its reconcile leaves the ladder alone while the grant holds it (docs/models/plan-ladders-transitions.md:80).

When the grant ends, by revocation or at its end date, that act does not give the subscription its tier back. Revocation and expiry end the grant's delivery, resume any grant marked to resume that the ended grant had replaced, and then, if the organization holds nothing on any ladder, confer the organization type's default (internal/entitlements/grant_acts.go:294-315 and :380-394; internal/entitlements/reapply_defaults.go:84-114). Nothing confers the subscription. It delivers again at its next reconcile; for a subscription nobody edits, that is the customer.subscription.updated event Stripe sends at renewal (internal/integrations/stripe/workflows/webhook_subscription.go:96). Until then the member pays for the tier, and the Products page shows the default as their plan, or nothing on that ladder when the organization holds another ladder. The same happens when the plan that held the ladder was a second subscription and that subscription ends (reconcileEnded, internal/fulfillment/reconcile.go:213-278).

What they should be able to do

A member whose organization still pays for a tier gets it back in the act that ends the plan holding the ladder against it: a revocation, an expiry, or the end of the subscription that held it. That act would confer the paying subscription from its recorded items, the way it already confers a grant marked to resume (ResumeReplacedGrantsTx, internal/entitlements/grant_resumption.go:85-140), for example after those grants and before the default floor. A subscription whose payment is due would get its tier back suspended, as a past-due subscription's tier is shown today.

What the step has to respect:

  • The ending acts lock the organization's pool row and never a subscription; the reconcile takes the subscription's lock and then the pool row (docs/database-locks.md:138-172). The step has to work from the pool row alone, or an ending act and a reconcile can wait on each other.
  • Which ladders a subscription is held off is stated once, in core.subscription_displacements (internal/db/migrations/00034_subscription_displacements.sql:30). The step reads that statement instead of writing a second one.
  • The grant-resumption spec says only the end of a replacing plan resumes a grant (openspec/specs/grant-resumption/spec.md:314-326), and describes what happens to a grant this subscription had replaced while an operator's grant holds the ladder. The step changes what that requirement covers.
  • docs/models/plan-ladders-transitions.md:80, docs/stripe.md:214 and docs/plan-management.md:164 state today's timing, "at its next reconcile".

Why it matters

For up to one billing period after a grant ends, a member pays for a tier and receives the free default or nothing on that ladder, and no page says why.

Where

internal/entitlements/grant_acts.go (RevokeGrantTx, ExpireGrantTx); internal/fulfillment/reconcile.go (reconcileEnded); internal/entitlements/grant_resumption.go; internal/db/migrations/00034_subscription_displacements.sql.

Done when

After a grant over a paid tier is revoked or expires, the organization holds the subscription's tier again in the same transaction, with no Stripe event; the same holds when a second subscription that held the ladder ends; a test drives each case; and the model page, docs/stripe.md and the grant-resumption spec state the new timing.

### What a person cannot do today Some terms first. A plan ladder is a ranked sequence of tiers, for example Plus, Pro and Max. An organization holds one tier per ladder at a time: from a subscription it pays for in Stripe, from a grant an operator issues, or from its organization type's default, a free tier the console confers when the organization holds nothing. A reconcile is the console reading a subscription back from Stripe and bringing the organization's tiers into line with it. An operator can issue a grant over a tier that a subscription pays for. The grant takes the ladder; the subscription keeps billing and delivers nothing there, and its reconcile leaves the ladder alone while the grant holds it (`docs/models/plan-ladders-transitions.md:80`). When the grant ends, by revocation or at its end date, that act does not give the subscription its tier back. Revocation and expiry end the grant's delivery, resume any grant marked to resume that the ended grant had replaced, and then, if the organization holds nothing on any ladder, confer the organization type's default (`internal/entitlements/grant_acts.go:294-315` and `:380-394`; `internal/entitlements/reapply_defaults.go:84-114`). Nothing confers the subscription. It delivers again at its next reconcile; for a subscription nobody edits, that is the `customer.subscription.updated` event Stripe sends at renewal (`internal/integrations/stripe/workflows/webhook_subscription.go:96`). Until then the member pays for the tier, and the Products page shows the default as their plan, or nothing on that ladder when the organization holds another ladder. The same happens when the plan that held the ladder was a second subscription and that subscription ends (`reconcileEnded`, `internal/fulfillment/reconcile.go:213-278`). ### What they should be able to do A member whose organization still pays for a tier gets it back in the act that ends the plan holding the ladder against it: a revocation, an expiry, or the end of the subscription that held it. That act would confer the paying subscription from its recorded items, the way it already confers a grant marked to resume (`ResumeReplacedGrantsTx`, `internal/entitlements/grant_resumption.go:85-140`), for example after those grants and before the default floor. A subscription whose payment is due would get its tier back suspended, as a past-due subscription's tier is shown today. What the step has to respect: - The ending acts lock the organization's pool row and never a subscription; the reconcile takes the subscription's lock and then the pool row (`docs/database-locks.md:138-172`). The step has to work from the pool row alone, or an ending act and a reconcile can wait on each other. - Which ladders a subscription is held off is stated once, in `core.subscription_displacements` (`internal/db/migrations/00034_subscription_displacements.sql:30`). The step reads that statement instead of writing a second one. - The grant-resumption spec says only the end of a replacing plan resumes a grant (`openspec/specs/grant-resumption/spec.md:314-326`), and describes what happens to a grant this subscription had replaced while an operator's grant holds the ladder. The step changes what that requirement covers. - `docs/models/plan-ladders-transitions.md:80`, `docs/stripe.md:214` and `docs/plan-management.md:164` state today's timing, "at its next reconcile". ### Why it matters For up to one billing period after a grant ends, a member pays for a tier and receives the free default or nothing on that ladder, and no page says why. ### Where `internal/entitlements/grant_acts.go` (`RevokeGrantTx`, `ExpireGrantTx`); `internal/fulfillment/reconcile.go` (`reconcileEnded`); `internal/entitlements/grant_resumption.go`; `internal/db/migrations/00034_subscription_displacements.sql`. ### Done when After a grant over a paid tier is revoked or expires, the organization holds the subscription's tier again in the same transaction, with no Stripe event; the same holds when a second subscription that held the ladder ends; a test drives each case; and the model page, `docs/stripe.md` and the grant-resumption spec state the new timing.
cgalo5758 added this to the Public launch milestone 2026-10-26 20:58:22 +00:00
cgalo5758 added the
kind
enhancement
area/entitlementsarea/billing
priority
medium
labels 2026-10-26 20:58:22 +00:00
Author
Owner

This becomes part of #269's fix. While a grant holds the ladder, the subscription's billing is paused at Stripe; when the grant ends, the same change resumes the billing and delivers the subscription's tier again, so neither waits for the next Stripe event.

This becomes part of #269's fix. While a grant holds the ladder, the subscription's billing is paused at Stripe; when the grant ends, the same change resumes the billing and delivers the subscription's tier again, so neither waits for the next Stripe event.
Author
Owner

When a grant that took a subscription's ladder ends, whether it is revoked, expires, or was itself a subscription that ended, the subscription's tier is now delivered again in the same step, before the organization type's default would be put back. Nothing waits for the subscription's next event at Stripe. A subscription whose payment is due gets its tier back suspended, as it would at its own renewal. A grant that was marked to come back after a later plan replaced it still comes back first, and the subscription then waits for that grant.

Commit: dacb8b57

When a grant that took a subscription's ladder ends, whether it is revoked, expires, or was itself a subscription that ended, the subscription's tier is now delivered again in the same step, before the organization type's default would be put back. Nothing waits for the subscription's next event at Stripe. A subscription whose payment is due gets its tier back suspended, as it would at its own renewal. A grant that was marked to come back after a later plan replaced it still comes back first, and the subscription then waits for that grant. Commit: dacb8b57
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wiki-cafe/member-console#270