Skip to main content
← Back to list
01Issue
BugOpenSwamp ClubPublic
AssigneesNone

Relationships

#1813 A subscription stuck in incomplete has no path forward: it grants nothing, and the billing page offers no way to finish paying

Opened by keeb · 8/24/2026

Found by code scan of the billing domain and the checkout entry point.

The defect

incomplete is a modelled subscription status (lib/domain/billing/subscription.ts:17) but it is not entitling — ENTITLING_STATUSES is ["active", "trialing", "past_due"] at :48, and statusGrantsAccess at :59 derives from it. That is the right call: an incomplete subscription is one whose first payment never succeeded, so it should not grant a plan.

The problem is what the product does with the row afterwards: nothing.

Stripe writes incomplete when the initial payment intent needs action or fails — a declined card, or an SCA/3DS challenge that was not completed. This is not an edge case; it is the normal outcome of a mistyped card or an interrupted bank confirmation, and it is more common on mobile where the 3DS hand-off leaves the browser.

Once a collective is in that state:

  • The subscription row exists with status: "incomplete" and the plan/seat counts the operative chose.
  • It grants no access, so the collective still presents as unpaid.
  • Nothing surfaces the state. There is no "your payment did not complete, finish it here" affordance on the collective billing page.
  • The row persists until Stripe expires it (roughly 23h), at which point it becomes incomplete_expired and the intent is lost entirely.

lib/app/billing/start-checkout.ts:39 does behave correctly here:

if (existing && existing.grantsAccess()) { ... open portal instead ... }

Because incomplete does not grant access, a fresh checkout is opened rather than the billing portal — which is the right route out. But nothing tells the operative that starting checkout again is the fix. The visible cue in the product is a billing page that looks the same as it did before they paid, so the intuitive next move is the billing portal — and the Stripe portal cannot repair an incomplete subscription. That is a loop with no exit signal.

Suggested fix

  1. Surface incomplete explicitly on the collective billing page: state that the payment did not complete, and put the primary action on restarting checkout rather than on the portal.
  2. Consider suppressing or relabelling the "manage billing" portal link while a subscription is incomplete, since the portal cannot resolve that state.
  3. Emit an event when a subscription lands in or leaves incomplete. subscription_started currently fires with whatever status came back, so an incomplete outcome is recorded as an ordinary start and is not distinguishable from a successful one without inspecting the payload.
  4. Decide what should happen at incomplete_expired — at minimum it should not leave a dead row that reads as an in-flight subscription.
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

8/24/2026, 11:58:15 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.