feat(gifts): FEAT-14-C — gift purchase, redeem, and status endpoints#2
feat(gifts): FEAT-14-C — gift purchase, redeem, and status endpoints#2apsisvictor-sys wants to merge 1 commit intomainfrom
Conversation
- Add GiftCode model + GiftCodeStatus enum to Prisma schema
- Add premiumGiftExpiresAt to User model
- Migration 0005_gift_codes ready for Railway deploy
- Add STRIPE_GIFT_PREMIUM_{1M,3M,6M,12M}_PRICE_ID config
- PrismaGiftRepository: create, getByCode, redeem, markPaid, markExpired
- POST /api/gifts/purchase — auth optional, generates XXXX-XXXX-XXXX-XXXX code, Stripe Checkout session (mode: payment), returns checkoutUrl
- POST /api/gifts/redeem — auth required, validates code state, atomic transaction upgrades user.subscriptionTier + records gift expiry, sends GiftRedemptionEmail
- GET /api/gifts/status/[code] — public, in-process rate limit (20 req/min/IP)
- Gift email stubs (GiftRedemptionEmail, GiftPurchaseConfirmationEmail) — full React Email templates deferred to FEAT-14-E (PIX-213)
Co-Authored-By: Paperclip <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🚅 Deployed to the neuroforge-pr-2 environment in neuroforge
1 service not affected by this PR
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e0d529e5c7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }), | ||
| prisma.user.update({ | ||
| where: { id: auth.userId }, | ||
| data: { subscriptionTier: "premium", premiumGiftExpiresAt: premiumExpiresAt }, |
There was a problem hiding this comment.
Keep professional tier when redeeming a gift
The user update inside redemption unconditionally sets subscriptionTier to premium, so a currently professional subscriber who redeems a gift is silently downgraded and can lose access to pro-only features (the tier ranking in src/infrastructure/auth/access-control.ts treats professional as higher than premium). This only occurs for users already on professional, but in that case it is a user-visible regression and likely revenue-impacting.
Useful? React with 👍 / 👎.
| // Atomically: mark code redeemed + upgrade user tier + set gift expiry | ||
| await prisma.$transaction([ | ||
| prisma.giftCode.update({ | ||
| where: { code }, |
There was a problem hiding this comment.
Enforce single-use redemption in the transactional update
Redemption first checks status, but the transactional write updates by code only, so two concurrent requests can both pass the pre-check and both commit, granting premium time to multiple accounts from one gift code. This is reproducible under concurrent redeems of the same PAID code; the write should include a status predicate (or equivalent optimistic locking) so only one request can transition PAID -> REDEEMED.
Useful? React with 👍 / 👎.
Summary
Implements PIX-211 — the three backend gift endpoints for FEAT-14 (Gift subscriptions).
Schema (covers FEAT-14-A dependency)
GiftCodeStatusenum (PENDING_PAYMENT | PAID | REDEEMED | EXPIRED)GiftCodemodel with code, durationMonths, purchaser/recipient fields, stripeSessionId, expirypremiumGiftExpiresAttoUsermodel for gift expiry tracking0005_gift_codesready to apply on RailwayStripe config (FEAT-14-B env vars wired in)
STRIPE_GIFT_PREMIUM_{1M,3M,6M,12M}_PRICE_IDmapped instripe-client.tsEndpoints
POST/api/gifts/purchasePOST/api/gifts/redeemGET/api/gifts/status/[code]Purchase flow: validates gift priceId → generates
XXXX-XXXX-XXXX-XXXXcode viacrypto.randomBytes→ persistsGiftCode(PENDING_PAYMENT) → creates Stripe Checkout session (mode: payment) → returnscheckoutUrl.Redeem flow: validates code state (expired/redeemed/pending-payment checks) → atomic Prisma transaction: marks code
REDEEMED+ setsuser.subscriptionTier = premium+ setspremiumGiftExpiresAt(stacking on existing gift if any) → sendsGiftRedemptionEmail.Status endpoint: public, in-process rate limit 20 req/min/IP, returns only non-PII fields.
Notes
checkout.session.completed) handled in FEAT-14-D (PIX-212)tsc --noEmitpasses cleanly afterprisma generateTest plan
npx prisma migrate deploy/api/gifts/purchasewith valid gift priceId → getscheckoutUrl/api/gifts/purchasewith invalid priceId → 400/api/gifts/redeemunauthenticated → 401/api/gifts/redeemwith unknown code → 404/api/gifts/redeemwith PENDING_PAYMENT code → 402/api/gifts/redeemwith REDEEMED code → 409/api/gifts/status/XXXX-XXXX-XXXX-XXXX→ returns status🤖 Generated with Claude Code