fix(billing): restore default entitlement when a Stripe cancellation … - #175
Merged
Conversation
…fails or is missed (#174) * fix(billing): restore default subscription for users without entitlement and handle settlement failures * fix(billing): remove unused billing settings mock and streamline subscription handling logic
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
| Filename | Overview |
|---|---|
| apps/tradinggoose/lib/billing/core/subscription.ts | Adds non-throwing default-entitlement repair and integrates it into personal subscription reads. |
| apps/tradinggoose/lib/billing/webhooks/subscription.ts | Separates tier synchronization, settlement, and entitlement restoration while preventing settlement against stale pricing. |
| apps/tradinggoose/lib/billing/core/subscription.test.ts | Covers successful read-path restoration, billing-disabled behavior, and preservation of the original failure. |
| apps/tradinggoose/lib/billing/webhooks/subscription.test.ts | Covers entitlement restoration on failure, stale-tier settlement suppression, and retry identifier preservation. |
Sequence Diagram
sequenceDiagram
participant Stripe
participant Webhook
participant BillingTier
participant Settlement
participant Entitlement
Stripe->>Webhook: customer.subscription.deleted
Webhook->>BillingTier: synchronize tier
alt tier synchronization succeeds
BillingTier-->>Webhook: verified tier
Webhook->>Settlement: settle final overage
else tier synchronization fails
BillingTier-->>Webhook: error
Webhook->>Webhook: skip stale-tier settlement
end
Webhook->>Entitlement: restore default personal tier
alt settlement remains owed
Entitlement->>Entitlement: retain Stripe subscription ID
Webhook-->>Stripe: rethrow for retry
else settlement succeeds
Webhook-->>Stripe: acknowledge deletion
end
Reviews (2): Last reviewed commit: "fix(billing): skip final overage settlem..." | Re-trigger Greptile
BruzWJ
marked this pull request as draft
July 26, 2026 23:58
…not be verified (#176) * fix(billing): handle settlement failures gracefully while restoring entitlement * fix(billing): retain Stripe ID during restoration when settlement fails
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stops a failed or missed Stripe cancellation from leaving a user with no entitled subscription at all.
handleStripeSubscriptionDeleted()no longer aborts before the entitlement restore. Both settlement steps —syncSubscriptionBillingTierFromStripeSubscription()andhandleSubscriptionDeleted()— are wrapped so a failure is logged, the default-tier restore still runs, and the first error is rethrown afterwards. The settled log line now carriessettlementFailed.restorePersonalEntitlement()inlib/billing/core/subscription.ts: when a personal read finds no entitled row, it callsensureDefaultUserSubscription()to put the user back on the default tier. It is a no-op when billing is disabled, and never throws — a repair failure falls through so the original billing error surfaces instead of a new one.getActiveSubscriptionForReference()(user references only),getEffectiveSubscription(), andgetPersonalBillingSnapshot().Why
Personal Stripe subscriptions reuse the user's default subscription row rather than creating a separate one. So if the
customer.subscription.deletedwebhook is dropped, or fails part-way through settlement, that single row is left cancelled and non-entitled — the user has no subscription at all, and every billing read throws for them.The old handler made this worse: settlement talks to Stripe (overage calculation, final invoicing) and can fail, and an exception there aborted the handler before the code that restores the default tier. A transient Stripe error during cancellation therefore cost the user their default entitlement.
The fix separates the two concerns. Restoring the default tier depends only on local state — the row is not entitling anyone, and the default tier is the floor rather than a revocation — so it runs unconditionally, and settlement failures are surfaced afterwards instead of silently taking entitlement down with them. The read-path repair covers users already stranded by cancellations that failed before this change.
Affected Areas
apps/tradinggooseapps/docspackages/*Issue Links( if any )
Validation
Note:
bunx biome check apps/tradinggoose/lib/billingreports 15 pre-existing formatting errors in files this PR does not touch; the four changed files are clean.Risk / Rollout Notes
Low risk, no schema or config changes, deploy normally. Two behavior changes worth knowing about:
customer.subscription.deletednow rethrows settlement failures, so Stripe sees a failed webhook and retries where it previously saw a success. This is intentional — the failure is now visible instead of silent — but expect webhook error alerts for cases that used to pass quietly.stripeSubscriptionIdon the reused row, so the retry'sgetSubscriptionByStripeSubscriptionId()lookup finds nothing and the handler returns early. That is safe (no double settlement) but it means a failed personal overage settlement will not retry itself and needs manual follow-up from the loggedFailed to settle a cancelled subscription/settlementFailed: trueentries. Workspace subscriptions keep theirstripeSubscriptionIdand do retry normally.ensureDefaultUserSubscription()uses a deterministicdefault-<userId>id withonConflictDoUpdate— and only fires when the user has no entitled subscription, so it is a one-shot per stranded user, not per request.Config / Data Changes
customer.subscription.deletedhandling only — no API surface change, but the handler's success/failure signal to Stripe is now accurate (see Risk notes)Screenshots / Video
Checklist