Files
biblical-guide.com/lib/subscription-utils.ts
Andrei c3cd353f2f 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>
2025-10-12 22:14:22 +00:00

149 lines
3.8 KiB
TypeScript

import { prisma } from '@/lib/db'
export const SUBSCRIPTION_LIMITS = {
free: 10,
premium: 999999 // Effectively unlimited
}
export const STRIPE_PRICES = {
premium_monthly: process.env.STRIPE_PREMIUM_MONTHLY_PRICE_ID || '',
premium_yearly: process.env.STRIPE_PREMIUM_YEARLY_PRICE_ID || ''
}
export interface ConversationLimitCheck {
allowed: boolean
remaining: number
limit: number
tier: string
resetDate: Date | null
}
/**
* Check if user can create a new conversation
* Handles limit checking and automatic monthly reset
*/
export async function checkConversationLimit(userId: string): Promise<ConversationLimitCheck> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
subscriptionTier: true,
conversationCount: true,
conversationLimit: true,
limitResetDate: true,
subscriptionStatus: true
}
})
if (!user) {
throw new Error('User not found')
}
// Reset counter if period expired
const now = new Date()
if (user.limitResetDate && now > user.limitResetDate) {
const nextReset = new Date(user.limitResetDate)
nextReset.setMonth(nextReset.getMonth() + 1)
await prisma.user.update({
where: { id: userId },
data: {
conversationCount: 0,
limitResetDate: nextReset
}
})
user.conversationCount = 0
}
// Calculate remaining conversations
const remaining = user.conversationLimit - user.conversationCount
// Premium users always have access (unless subscription is past_due or cancelled)
const isPremiumActive = user.subscriptionTier === 'premium' &&
(user.subscriptionStatus === 'active' || user.subscriptionStatus === 'trialing')
const allowed = isPremiumActive || remaining > 0
return {
allowed,
remaining: isPremiumActive ? Infinity : Math.max(0, remaining),
limit: user.conversationLimit,
tier: user.subscriptionTier,
resetDate: user.limitResetDate
}
}
/**
* Increment user's conversation count
*/
export async function incrementConversationCount(userId: string): Promise<void> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { limitResetDate: true }
})
if (!user) {
throw new Error('User not found')
}
// Set initial reset date if not set (1 month from now)
const now = new Date()
const limitResetDate = user.limitResetDate || new Date(now.setMonth(now.getMonth() + 1))
await prisma.user.update({
where: { id: userId },
data: {
conversationCount: { increment: 1 },
limitResetDate
}
})
}
/**
* Get subscription tier from Stripe price ID
*/
export function getTierFromPriceId(priceId: string): string {
if (priceId === STRIPE_PRICES.premium_monthly || priceId === STRIPE_PRICES.premium_yearly) {
return 'premium'
}
return 'free'
}
/**
* Get billing interval from Stripe price ID
*/
export function getIntervalFromPriceId(priceId: string): string {
if (priceId === STRIPE_PRICES.premium_yearly) return 'year'
return 'month'
}
/**
* Get conversation limit for a tier
*/
export function getLimitForTier(tier: string): number {
return SUBSCRIPTION_LIMITS[tier as keyof typeof SUBSCRIPTION_LIMITS] || SUBSCRIPTION_LIMITS.free
}
/**
* Format subscription status for display
*/
export function formatSubscriptionStatus(status: string): string {
const statusMap: Record<string, string> = {
'active': 'Active',
'cancelled': 'Cancelled',
'past_due': 'Past Due',
'trialing': 'Trial',
'incomplete': 'Incomplete',
'incomplete_expired': 'Expired',
'unpaid': 'Unpaid',
'expired': 'Expired'
}
return statusMap[status.toLowerCase()] || status
}
/**
* Check if subscription status is considered "active" for access purposes
*/
export function isSubscriptionActive(status: string): boolean {
return ['active', 'trialing'].includes(status.toLowerCase())
}