Skip to content

feat(gifts): FEAT-14-C — gift purchase, redeem, and status endpoints#2

Open
apsisvictor-sys wants to merge 1 commit intomainfrom
feat/gift-routes-pix211
Open

feat(gifts): FEAT-14-C — gift purchase, redeem, and status endpoints#2
apsisvictor-sys wants to merge 1 commit intomainfrom
feat/gift-routes-pix211

Conversation

@apsisvictor-sys
Copy link
Copy Markdown
Owner

Summary

Implements PIX-211 — the three backend gift endpoints for FEAT-14 (Gift subscriptions).

Schema (covers FEAT-14-A dependency)

  • Added GiftCodeStatus enum (PENDING_PAYMENT | PAID | REDEEMED | EXPIRED)
  • Added GiftCode model with code, durationMonths, purchaser/recipient fields, stripeSessionId, expiry
  • Added premiumGiftExpiresAt to User model for gift expiry tracking
  • Migration 0005_gift_codes ready to apply on Railway

Stripe config (FEAT-14-B env vars wired in)

  • STRIPE_GIFT_PREMIUM_{1M,3M,6M,12M}_PRICE_ID mapped in stripe-client.ts

Endpoints

Method Path Auth
POST /api/gifts/purchase Optional
POST /api/gifts/redeem Required
GET /api/gifts/status/[code] Public (rate-limited)

Purchase flow: validates gift priceId → generates XXXX-XXXX-XXXX-XXXX code via crypto.randomBytes → persists GiftCode (PENDING_PAYMENT) → creates Stripe Checkout session (mode: payment) → returns checkoutUrl.

Redeem flow: validates code state (expired/redeemed/pending-payment checks) → atomic Prisma transaction: marks code REDEEMED + sets user.subscriptionTier = premium + sets premiumGiftExpiresAt (stacking on existing gift if any) → sends GiftRedemptionEmail.

Status endpoint: public, in-process rate limit 20 req/min/IP, returns only non-PII fields.

Notes

  • Gift email stubs in place; full React Email templates deferred to FEAT-14-E (PIX-213)
  • Webhook extension (mark code PAID on checkout.session.completed) handled in FEAT-14-D (PIX-212)
  • tsc --noEmit passes cleanly after prisma generate

Test plan

  • Run migration against Railway DB: npx prisma migrate deploy
  • POST /api/gifts/purchase with valid gift priceId → gets checkoutUrl
  • POST /api/gifts/purchase with invalid priceId → 400
  • POST /api/gifts/redeem unauthenticated → 401
  • POST /api/gifts/redeem with unknown code → 404
  • POST /api/gifts/redeem with PENDING_PAYMENT code → 402
  • POST /api/gifts/redeem with REDEEMED code → 409
  • GET /api/gifts/status/XXXX-XXXX-XXXX-XXXX → returns status
  • Rate limiting: 21st request in a minute → 429

🤖 Generated with Claude Code

- 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]>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontend Ready Ready Preview, Comment Mar 26, 2026 8:36am

@railway-app
Copy link
Copy Markdown

railway-app bot commented Mar 26, 2026

🚅 Deployed to the neuroforge-pr-2 environment in neuroforge

Service Status Web Updated (UTC)
neuroforge-backend ✅ Success (View Logs) Web Mar 26, 2026 at 8:39 am
neuroforge-worker ✅ Success (View Logs) Mar 26, 2026 at 8:39 am
1 service not affected by this PR
  • neuroforge-postgres

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant