feat: implement subscription system with conversation limits
Implement complete backend subscription system that limits free users to 10 AI conversations per month and offers Premium tier ($10/month or $100/year) with unlimited conversations. Changes: - Add User subscription fields (tier, status, limits, counters) - Create Subscription model to track Stripe subscriptions - Implement conversation limit enforcement in chat API - Add subscription checkout and customer portal APIs - Update Stripe webhook to handle subscription events - Add subscription utility functions (limit checks, tier management) - Add comprehensive subscription translations (en, ro, es, it) - Update environment variables for Stripe price IDs - Update footer "Sponsor Us" link to point to /donate - Add "Sponsor Us" button to home page hero section Database: - User model: subscriptionTier, subscriptionStatus, conversationLimit, conversationCount, limitResetDate, stripeCustomerId, stripeSubscriptionId - Subscription model: tracks Stripe subscription details, periods, status - SubscriptionStatus enum: ACTIVE, CANCELLED, PAST_DUE, TRIALING, etc. API Routes: - POST /api/subscriptions/checkout - Create Stripe checkout session - POST /api/subscriptions/portal - Get customer portal link - Webhook handlers for: customer.subscription.created/updated/deleted, invoice.payment_succeeded/failed Features: - Free tier: 10 conversations/month with automatic monthly reset - Premium tier: Unlimited conversations - Automatic limit enforcement before conversation creation - Returns LIMIT_REACHED error with upgrade URL when limit hit - Stripe Customer Portal integration for subscription management - Automatic tier upgrade/downgrade via webhooks Documentation: - SUBSCRIPTION_IMPLEMENTATION_PLAN.md - Complete implementation plan - SUBSCRIPTION_IMPLEMENTATION_STATUS.md - Current status and next steps Frontend UI still needed: subscription page, upgrade modal, usage display 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { stripe } from '@/lib/stripe-server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import Stripe from 'stripe'
|
||||
import { getTierFromPriceId, getIntervalFromPriceId, getLimitForTier } from '@/lib/subscription-utils'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.text()
|
||||
@@ -115,6 +116,134 @@ export async function POST(req: NextRequest) {
|
||||
break
|
||||
}
|
||||
|
||||
// Subscription events
|
||||
case 'customer.subscription.created':
|
||||
case 'customer.subscription.updated': {
|
||||
const stripeSubscription = event.data.object as Stripe.Subscription
|
||||
const userId = stripeSubscription.metadata.userId
|
||||
|
||||
if (!userId) {
|
||||
console.warn('⚠️ No userId in subscription metadata:', stripeSubscription.id)
|
||||
break
|
||||
}
|
||||
|
||||
const priceId = stripeSubscription.items.data[0]?.price.id
|
||||
if (!priceId) {
|
||||
console.warn('⚠️ No price ID in subscription:', stripeSubscription.id)
|
||||
break
|
||||
}
|
||||
|
||||
const tier = getTierFromPriceId(priceId)
|
||||
const interval = getIntervalFromPriceId(priceId)
|
||||
const limit = getLimitForTier(tier)
|
||||
|
||||
// Upsert subscription record
|
||||
await prisma.subscription.upsert({
|
||||
where: { stripeSubscriptionId: stripeSubscription.id },
|
||||
create: {
|
||||
userId,
|
||||
stripeSubscriptionId: stripeSubscription.id,
|
||||
stripePriceId: priceId,
|
||||
stripeCustomerId: stripeSubscription.customer as string,
|
||||
status: stripeSubscription.status.toUpperCase() as any,
|
||||
currentPeriodStart: new Date((stripeSubscription as any).current_period_start * 1000),
|
||||
currentPeriodEnd: new Date((stripeSubscription as any).current_period_end * 1000),
|
||||
cancelAtPeriodEnd: (stripeSubscription as any).cancel_at_period_end,
|
||||
tier,
|
||||
interval
|
||||
},
|
||||
update: {
|
||||
status: stripeSubscription.status.toUpperCase() as any,
|
||||
currentPeriodStart: new Date((stripeSubscription as any).current_period_start * 1000),
|
||||
currentPeriodEnd: new Date((stripeSubscription as any).current_period_end * 1000),
|
||||
cancelAtPeriodEnd: (stripeSubscription as any).cancel_at_period_end,
|
||||
stripePriceId: priceId
|
||||
}
|
||||
})
|
||||
|
||||
// Update user subscription tier and limit
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
subscriptionTier: tier,
|
||||
conversationLimit: limit,
|
||||
subscriptionStatus: stripeSubscription.status,
|
||||
stripeSubscriptionId: stripeSubscription.id,
|
||||
stripeCustomerId: stripeSubscription.customer as string
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`✅ Subscription ${stripeSubscription.status} for user ${userId} (tier: ${tier})`)
|
||||
break
|
||||
}
|
||||
|
||||
case 'customer.subscription.deleted': {
|
||||
const subscription = event.data.object as Stripe.Subscription
|
||||
|
||||
const sub = await prisma.subscription.findUnique({
|
||||
where: { stripeSubscriptionId: subscription.id },
|
||||
select: { userId: true }
|
||||
})
|
||||
|
||||
if (sub) {
|
||||
// Downgrade to free tier
|
||||
await prisma.user.update({
|
||||
where: { id: sub.userId },
|
||||
data: {
|
||||
subscriptionTier: 'free',
|
||||
conversationLimit: 10,
|
||||
subscriptionStatus: 'cancelled'
|
||||
}
|
||||
})
|
||||
|
||||
await prisma.subscription.update({
|
||||
where: { stripeSubscriptionId: subscription.id },
|
||||
data: { status: 'CANCELLED' }
|
||||
})
|
||||
|
||||
console.log(`✅ Subscription cancelled for user ${sub.userId}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'invoice.payment_succeeded': {
|
||||
const invoice = event.data.object as any
|
||||
if (invoice.subscription) {
|
||||
console.log(`✅ Payment succeeded for subscription ${invoice.subscription}`)
|
||||
|
||||
// Ensure subscription is still active
|
||||
const subscription = await prisma.subscription.findUnique({
|
||||
where: { stripeSubscriptionId: invoice.subscription as string }
|
||||
})
|
||||
|
||||
if (subscription) {
|
||||
await prisma.user.update({
|
||||
where: { id: subscription.userId },
|
||||
data: { subscriptionStatus: 'active' }
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'invoice.payment_failed': {
|
||||
const invoice = event.data.object as any
|
||||
if (invoice.subscription) {
|
||||
const subscription = await prisma.subscription.findUnique({
|
||||
where: { stripeSubscriptionId: invoice.subscription as string }
|
||||
})
|
||||
|
||||
if (subscription) {
|
||||
await prisma.user.update({
|
||||
where: { id: subscription.userId },
|
||||
data: { subscriptionStatus: 'past_due' }
|
||||
})
|
||||
console.warn(`⚠️ Payment failed for subscription ${invoice.subscription}`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user