feat(admin): grant/revoke comp Pro without faking a Stripe subscription - #225
Merged
Conversation
Needed to comp a user who hit the free-plan list cap. Writing a subscriptions row by hand would have been the obvious move and is a trap: stripeCustomerId is required, and both createCheckoutSession and createPortalSession pass that value straight to the Stripe API. An invented id means the user can never actually pay and their billing portal 500s — the opposite of a favour. referralProUntil is already the app's comp lever, honoured by getUserPlan (billing.ts) and by assertListQuota (lists.ts), and it leaves Stripe alone. These two internalMutations just make it operable from the CLI. Guards, because this writes real user billing state: email must match exactly one user (the schema does not make email unique, and granting to the wrong one of several is worse than refusing), and `until` must be in the future rather than silently granting nothing. Both return the previous value so a grant can be undone by hand if revoke is not enough. Verified on dev first — guards, happy path, and getUserPlan flipping to "pro" — then reverted the dev grant before touching prod. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Adds
convex/adminGrants.ts, the two internal mutations used to compraedugas@gmail.comafter they hit the free-plan list cap.Merge this to stop prod drifting
These functions are already live in production but are not in
main. I deployed them directly to run the grant; the commit missed #224's merge window. Convex deploys are declarative, so the next push tomaintouchingconvex/**will push main's function set and deleteadminGrantsfrom prod.The grant itself survives either way — it's a patch to the user's row, not a function — but
revokeProByEmailwould silently disappear. The file here is byte-identical to what's deployed (verified by checksum).Why not a subscriptions row
Writing one by hand is the obvious move and a trap.
stripeCustomerIdis required, and bothcreateCheckoutSessionandcreatePortalSessionpass that value straight to the Stripe API:An invented id means the user can never actually pay, and their billing portal 500s — the opposite of a favour.
referralProUntilis already the app's comp lever, honoured bygetUserPlan(billing.ts:57) and byassertListQuota(lists.ts:84), and it leaves Stripe untouched.Guards
This writes real user billing state, so:
emailunique; granting to the wrong one of several is worse than refusing and making the caller name an id.untilmust be in the future, rather than silently granting nothing.revokeisn't enough.internalMutation, so CLI-only and unreachable from any client.Verification
Guards, happy path, and
getUserPlanflipping to"pro"were all exercised on dev first, and the dev grant reverted before touching prod. 169 tests pass,tsc -bclean.Usage
🤖 Generated with Claude Code