feat(retention): a lapsed subscription loses the features, not the history - #879
Merged
Conversation
…story Losing Pro currently means losing everything older than 30 days on that night's retention run. The trigger is not only a deliberate cancellation: `statusGrantsPro` returns false for `past_due`, so ONE failed card payment is enough — the deletion lands before the customer has read the dunning email, and fixing the card the next morning brings nothing back. That is the worst possible moment to be destructive. A lapsed customer is someone we want back, winning them back is impossible once their history is gone, and holding the bytes for another month costs almost nothing. Features stop immediately; data gets PRO_DATA_GRACE_DAYS (default 30, env overridable). `billing.proEndedAt` is the clock, stamped by applySubscriptionState on the true -> false EDGE only — Stripe retries for three days and dunning fires repeatedly, so re-stamping on every past_due would walk the deadline forward forever and the history would never be reclaimed. Cleared on re-subscribe so a later lapse gets a fresh window. The grace window is deliberately NOT derived from PG_MESSAGE_RETENTION_DAYS: that window is the free tier's product, this one is a win-back runway, and squeezing free-tier storage must never shorten it. A malformed env value falls back to 30 rather than collapsing to zero, because the failure mode of that mistake is deleting a paying customer's history tonight. Nine tests; verified by reverting the change and watching six fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8
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.
Sam's call: "at time of expiry, we shouldn't immediately remove their message,
we should keep another 30 days."
Right, and the trigger is worse than deliberate cancellation.
statusGrantsProreturns false for
past_due, so one failed card payment flips theentitlement — and #878's protection disappears with it, meaning that night's
retention run permanently deletes everything older than 30 days. Before the
customer has read the dunning email. With nothing to recover if they fix the
card the next morning.
That is the worst possible moment to be destructive. A lapsed customer is
exactly who you want back; winning them back is impossible once their history
is gone; holding the bytes another month costs almost nothing.
Shape
Features stop immediately. Data gets a grace window. The entitlement still
flips to false at once, so Community listing and the rest lock the same
instant — the grace covers bytes, not access. There's a test pinning that
split, because it would be easy to "fix" this by leaving
protrue andaccidentally give away the paid features for a month.
billing.proEndedAtis the clock, stamped byapplySubscriptionStateon thetrue → false edge only. Stripe retries for three days and dunning fires
repeatedly; re-stamping on every
past_duewould walk the deadline forwardforever and the history would never be reclaimed. Cleared on re-subscribe, so
a later lapse gets a fresh window rather than an expired one.
Two deliberate choices
The window is not derived from
PG_MESSAGE_RETENTION_DAYS. That window isthe free tier's product; this one is a win-back runway. Tying them would mean
that squeezing free-tier storage silently shortens a paying customer's grace.
A malformed
PRO_DATA_GRACE_DAYSfalls back to 30, not 0. The failure modeof that particular typo is deleting a paying customer's history tonight, so it
fails toward keeping data. Tested.
Verification
Nine tests across both services. Reverted the change and confirmed six fail.
71 billing + retention tests pass, typecheck clean.
Related: #875, #877, #878.