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_expiredand 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
- Surface
incompleteexplicitly 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. - Consider suppressing or relabelling the "manage billing" portal link while a subscription is
incomplete, since the portal cannot resolve that state. - Emit an event when a subscription lands in or leaves
incomplete.subscription_startedcurrently fires with whatever status came back, so anincompleteoutcome is recorded as an ordinary start and is not distinguishable from a successful one without inspecting the payload. - Decide what should happen at
incomplete_expired— at minimum it should not leave a dead row that reads as an in-flight subscription.
Open
No activity in this phase yet.
Sign in to post a ripple.