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:
773
SUBSCRIPTION_IMPLEMENTATION_PLAN.md
Normal file
773
SUBSCRIPTION_IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,773 @@
|
||||
# User Subscription System - Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Implement a subscription-based model for Biblical Guide that limits AI chat conversations for free users and offers paid tiers with increased or unlimited access.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### What EXISTS ✅
|
||||
- Authentication system (JWT-based, required for chat)
|
||||
- Chat conversation tracking in database
|
||||
- User model with basic fields
|
||||
- Stripe integration for one-time donations
|
||||
- Stripe webhook handling (donations only)
|
||||
|
||||
### What DOES NOT EXIST ❌
|
||||
- User subscription system
|
||||
- Subscription tiers/plans
|
||||
- Conversation limits (free vs paid)
|
||||
- Usage tracking/quota enforcement
|
||||
- Upgrade prompts when limits reached
|
||||
- Subscription management UI
|
||||
- Stripe subscription integration (only donations exist)
|
||||
|
||||
## Subscription Tiers
|
||||
|
||||
### Free Tier
|
||||
- **Price:** $0/month
|
||||
- **Conversations:** 10 per month
|
||||
- **Features:**
|
||||
- Full Bible access
|
||||
- Prayer wall access
|
||||
- Bookmarks & highlights
|
||||
- 10 AI conversations/month
|
||||
- **Reset:** Monthly on signup anniversary
|
||||
|
||||
### Premium Tier
|
||||
- **Price:** $10/month (or $100/year with 17% discount)
|
||||
- **Conversations:** Unlimited
|
||||
- **Features:**
|
||||
- Everything in Free
|
||||
- Unlimited AI conversations
|
||||
- Priority support
|
||||
- Early access to new features
|
||||
|
||||
### Donation System (Existing)
|
||||
- One-time donations (separate from subscriptions)
|
||||
- Recurring donations (separate from subscriptions)
|
||||
- No perks attached to donations
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Database Schema & Migrations
|
||||
|
||||
### 1.1 Update User Model
|
||||
|
||||
**File:** `prisma/schema.prisma`
|
||||
|
||||
Add to User model:
|
||||
```prisma
|
||||
model User {
|
||||
// ... existing fields ...
|
||||
|
||||
// Subscription fields
|
||||
subscriptionTier String @default("free") // "free", "premium"
|
||||
subscriptionStatus String @default("active") // "active", "cancelled", "expired", "past_due"
|
||||
conversationLimit Int @default(10)
|
||||
conversationCount Int @default(0) // Reset monthly
|
||||
limitResetDate DateTime? // When to reset conversation count
|
||||
stripeCustomerId String? @unique // For subscriptions (not donations)
|
||||
stripeSubscriptionId String? @unique
|
||||
|
||||
// Relations
|
||||
subscriptions Subscription[]
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Create Subscription Model
|
||||
|
||||
```prisma
|
||||
model Subscription {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
stripeSubscriptionId String @unique
|
||||
stripePriceId String // Stripe price ID for the plan
|
||||
stripeCustomerId String
|
||||
status SubscriptionStatus
|
||||
currentPeriodStart DateTime
|
||||
currentPeriodEnd DateTime
|
||||
cancelAtPeriodEnd Boolean @default(false)
|
||||
tier String // "premium"
|
||||
interval String // "month" or "year"
|
||||
metadata Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@index([stripeSubscriptionId])
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
ACTIVE
|
||||
CANCELLED
|
||||
PAST_DUE
|
||||
TRIALING
|
||||
INCOMPLETE
|
||||
INCOMPLETE_EXPIRED
|
||||
UNPAID
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Run Migration
|
||||
|
||||
```bash
|
||||
npx prisma migrate dev --name add_subscription_system
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Conversation Limit Enforcement
|
||||
|
||||
### 2.1 Update Chat API Route
|
||||
|
||||
**File:** `app/api/chat/route.ts`
|
||||
|
||||
Add conversation limit check before processing:
|
||||
|
||||
```typescript
|
||||
// Add after authentication check (line 58)
|
||||
|
||||
// Check conversation limits for authenticated users
|
||||
if (userId && !conversationId) {
|
||||
// Only check limits when creating NEW conversation
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
subscriptionTier: true,
|
||||
conversationCount: true,
|
||||
conversationLimit: true,
|
||||
limitResetDate: true,
|
||||
subscriptionStatus: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
code: 'USER_NOT_FOUND'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
// Reset counter if period expired
|
||||
const now = new Date()
|
||||
if (user.limitResetDate && now > user.limitResetDate) {
|
||||
// Reset monthly counter
|
||||
const nextResetDate = new Date(user.limitResetDate)
|
||||
nextResetDate.setMonth(nextResetDate.getMonth() + 1)
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
conversationCount: 0,
|
||||
limitResetDate: nextResetDate
|
||||
}
|
||||
})
|
||||
user.conversationCount = 0
|
||||
}
|
||||
|
||||
// Check if user has exceeded limit (only for free tier with active status)
|
||||
if (user.subscriptionTier === 'free' && user.subscriptionStatus === 'active') {
|
||||
if (user.conversationCount >= user.conversationLimit) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Conversation limit reached. Upgrade to Premium for unlimited conversations.',
|
||||
code: 'LIMIT_REACHED',
|
||||
data: {
|
||||
limit: user.conversationLimit,
|
||||
used: user.conversationCount,
|
||||
tier: user.subscriptionTier,
|
||||
upgradeUrl: `/${locale}/subscription`
|
||||
}
|
||||
}, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
// User is within limits - increment counter for new conversations
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
conversationCount: { increment: 1 },
|
||||
// Set initial reset date if not set
|
||||
limitResetDate: user.limitResetDate || new Date(now.setMonth(now.getMonth() + 1))
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Create Utility Functions
|
||||
|
||||
**File:** `lib/subscription-utils.ts` (NEW)
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export const SUBSCRIPTION_LIMITS = {
|
||||
free: 10,
|
||||
premium: Infinity
|
||||
}
|
||||
|
||||
export const STRIPE_PRICES = {
|
||||
premium_monthly: process.env.STRIPE_PREMIUM_MONTHLY_PRICE_ID!,
|
||||
premium_yearly: process.env.STRIPE_PREMIUM_YEARLY_PRICE_ID!
|
||||
}
|
||||
|
||||
export async function checkConversationLimit(userId: string): Promise<{
|
||||
allowed: boolean
|
||||
remaining: number
|
||||
limit: number
|
||||
tier: string
|
||||
}> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
subscriptionTier: true,
|
||||
conversationCount: true,
|
||||
conversationLimit: true,
|
||||
limitResetDate: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
|
||||
// Reset if needed
|
||||
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
|
||||
}
|
||||
|
||||
const remaining = user.conversationLimit - user.conversationCount
|
||||
const allowed = user.subscriptionTier === 'premium' || remaining > 0
|
||||
|
||||
return {
|
||||
allowed,
|
||||
remaining: user.subscriptionTier === 'premium' ? Infinity : remaining,
|
||||
limit: user.conversationLimit,
|
||||
tier: user.subscriptionTier
|
||||
}
|
||||
}
|
||||
|
||||
export function getTierFromPriceId(priceId: string): string {
|
||||
if (priceId === STRIPE_PRICES.premium_monthly || priceId === STRIPE_PRICES.premium_yearly) {
|
||||
return 'premium'
|
||||
}
|
||||
return 'free'
|
||||
}
|
||||
|
||||
export function getIntervalFromPriceId(priceId: string): string {
|
||||
if (priceId === STRIPE_PRICES.premium_yearly) return 'year'
|
||||
return 'month'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Stripe Subscription Integration
|
||||
|
||||
### 3.1 Create Stripe Products & Prices
|
||||
|
||||
**Manual Step - Stripe Dashboard:**
|
||||
|
||||
1. Go to Stripe Dashboard → Products
|
||||
2. Create product: "Biblical Guide Premium"
|
||||
3. Add prices:
|
||||
- Monthly: $10/month (ID: save to env as `STRIPE_PREMIUM_MONTHLY_PRICE_ID`)
|
||||
- Yearly: $100/year (ID: save to env as `STRIPE_PREMIUM_YEARLY_PRICE_ID`)
|
||||
|
||||
### 3.2 Update Environment Variables
|
||||
|
||||
**File:** `.env.local` and `.env.example`
|
||||
|
||||
Add:
|
||||
```env
|
||||
# Stripe Subscription Price IDs
|
||||
STRIPE_PREMIUM_MONTHLY_PRICE_ID=price_xxxxxxxxxxxxx
|
||||
STRIPE_PREMIUM_YEARLY_PRICE_ID=price_xxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### 3.3 Create Subscription Checkout API
|
||||
|
||||
**File:** `app/api/subscriptions/checkout/route.ts` (NEW)
|
||||
|
||||
```typescript
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { stripe } from '@/lib/stripe'
|
||||
import prisma from '@/lib/db'
|
||||
import { verifyToken } from '@/lib/auth'
|
||||
|
||||
const checkoutSchema = z.object({
|
||||
priceId: z.string(),
|
||||
interval: z.enum(['month', 'year']),
|
||||
locale: z.string().default('en')
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
// Verify authentication
|
||||
const authHeader = request.headers.get('authorization')
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7)
|
||||
const payload = await verifyToken(token)
|
||||
const userId = payload.userId
|
||||
|
||||
// Get user
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { email: true, stripeCustomerId: true, subscriptionTier: true }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'User not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if already premium
|
||||
if (user.subscriptionTier === 'premium') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Already subscribed to Premium' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { priceId, interval, locale } = checkoutSchema.parse(body)
|
||||
|
||||
// Create or retrieve Stripe customer
|
||||
let customerId = user.stripeCustomerId
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: { userId }
|
||||
})
|
||||
customerId = customer.id
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { stripeCustomerId: customerId }
|
||||
})
|
||||
}
|
||||
|
||||
// Create checkout session
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1
|
||||
}
|
||||
],
|
||||
success_url: `${process.env.NEXTAUTH_URL}/${locale}/subscription/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${process.env.NEXTAUTH_URL}/${locale}/subscription`,
|
||||
metadata: {
|
||||
userId,
|
||||
interval
|
||||
},
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
userId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
sessionId: session.id,
|
||||
url: session.url
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Subscription checkout error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to create checkout session' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Create Customer Portal API
|
||||
|
||||
**File:** `app/api/subscriptions/portal/route.ts` (NEW)
|
||||
|
||||
```typescript
|
||||
import { NextResponse } from 'next/server'
|
||||
import { stripe } from '@/lib/stripe'
|
||||
import prisma from '@/lib/db'
|
||||
import { verifyToken } from '@/lib/auth'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization')
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7)
|
||||
const payload = await verifyToken(token)
|
||||
const userId = payload.userId
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { stripeCustomerId: true }
|
||||
})
|
||||
|
||||
if (!user?.stripeCustomerId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'No subscription found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: user.stripeCustomerId,
|
||||
return_url: `${process.env.NEXTAUTH_URL}/settings`
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url: session.url
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Customer portal error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to create portal session' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 Update Stripe Webhook Handler
|
||||
|
||||
**File:** `app/api/stripe/webhook/route.ts`
|
||||
|
||||
Add new event handlers after existing donation handlers:
|
||||
|
||||
```typescript
|
||||
// After existing event handlers, add:
|
||||
|
||||
case 'customer.subscription.created':
|
||||
case 'customer.subscription.updated': {
|
||||
const subscription = event.data.object
|
||||
const userId = subscription.metadata.userId
|
||||
|
||||
if (!userId) {
|
||||
console.warn('No userId in subscription metadata')
|
||||
break
|
||||
}
|
||||
|
||||
const priceId = subscription.items.data[0]?.price.id
|
||||
const tier = getTierFromPriceId(priceId)
|
||||
const interval = getIntervalFromPriceId(priceId)
|
||||
|
||||
await prisma.subscription.upsert({
|
||||
where: { stripeSubscriptionId: subscription.id },
|
||||
create: {
|
||||
userId,
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: priceId,
|
||||
stripeCustomerId: subscription.customer as string,
|
||||
status: subscription.status.toUpperCase(),
|
||||
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
||||
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
||||
tier,
|
||||
interval
|
||||
},
|
||||
update: {
|
||||
status: subscription.status.toUpperCase(),
|
||||
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
||||
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
||||
cancelAtPeriodEnd: subscription.cancel_at_period_end
|
||||
}
|
||||
})
|
||||
|
||||
// Update user subscription tier
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
subscriptionTier: tier,
|
||||
conversationLimit: tier === 'premium' ? 999999 : 10,
|
||||
subscriptionStatus: subscription.status,
|
||||
stripeSubscriptionId: subscription.id
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`✅ Subscription ${subscription.status} for user ${userId}`)
|
||||
break
|
||||
}
|
||||
|
||||
case 'customer.subscription.deleted': {
|
||||
const subscription = event.data.object
|
||||
|
||||
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
|
||||
if (invoice.subscription) {
|
||||
console.log(`✅ Payment succeeded for subscription ${invoice.subscription}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'invoice.payment_failed': {
|
||||
const invoice = event.data.object
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Frontend Implementation
|
||||
|
||||
### 4.1 Subscription Page
|
||||
|
||||
**File:** `app/[locale]/subscription/page.tsx` (NEW)
|
||||
|
||||
Full subscription management page with:
|
||||
- Current plan display
|
||||
- Usage stats (conversations used/remaining)
|
||||
- Upgrade options
|
||||
- Monthly/yearly toggle
|
||||
- Stripe checkout integration
|
||||
- Manage subscription button (portal link)
|
||||
|
||||
### 4.2 Upgrade Modal Component
|
||||
|
||||
**File:** `components/subscription/upgrade-modal.tsx` (NEW)
|
||||
|
||||
Modal shown when limit is reached:
|
||||
- Clear messaging about limit
|
||||
- Show current usage
|
||||
- Upgrade CTA
|
||||
- Pricing display
|
||||
|
||||
### 4.3 Usage Display Component
|
||||
|
||||
**File:** `components/subscription/usage-display.tsx` (NEW)
|
||||
|
||||
Shows in settings/profile:
|
||||
- Conversations used this month
|
||||
- Progress bar
|
||||
- Reset date
|
||||
- Current tier badge
|
||||
|
||||
### 4.4 Success Page
|
||||
|
||||
**File:** `app/[locale]/subscription/success/page.tsx` (NEW)
|
||||
|
||||
Thank you page after successful subscription
|
||||
|
||||
### 4.5 Update Settings Page
|
||||
|
||||
**File:** `app/[locale]/settings/page.tsx`
|
||||
|
||||
Add subscription section showing:
|
||||
- Current plan
|
||||
- Usage stats
|
||||
- Manage/upgrade buttons
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Translation Keys
|
||||
|
||||
### 5.1 Add to Translation Files
|
||||
|
||||
**Files:** `messages/en.json`, `messages/ro.json`, `messages/es.json`, `messages/it.json`
|
||||
|
||||
Add complete subscription translation keys:
|
||||
- Plan names and descriptions
|
||||
- Upgrade prompts
|
||||
- Usage messages
|
||||
- Error messages
|
||||
- Success messages
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Testing Checklist
|
||||
|
||||
### Subscription Flow
|
||||
- [ ] Free user creates 10 conversations successfully
|
||||
- [ ] 11th conversation blocked with upgrade prompt
|
||||
- [ ] Upgrade to Premium via Stripe Checkout
|
||||
- [ ] Webhook updates user to Premium tier
|
||||
- [ ] Premium user has unlimited conversations
|
||||
- [ ] Monthly counter resets correctly
|
||||
- [ ] Cancel subscription (remains premium until period end)
|
||||
- [ ] Subscription expires → downgrade to free
|
||||
- [ ] Payment failure handling
|
||||
|
||||
### Edge Cases
|
||||
- [ ] User with existing Stripe customer ID
|
||||
- [ ] Multiple subscriptions (should prevent)
|
||||
- [ ] Webhook arrives before user returns from checkout
|
||||
- [ ] Invalid webhook signatures
|
||||
- [ ] Database transaction failures
|
||||
- [ ] Subscription status edge cases (past_due, unpaid, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Deployment
|
||||
|
||||
### 7.1 Environment Variables
|
||||
|
||||
Add to production:
|
||||
```env
|
||||
STRIPE_PREMIUM_MONTHLY_PRICE_ID=price_live_xxxxx
|
||||
STRIPE_PREMIUM_YEARLY_PRICE_ID=price_live_xxxxx
|
||||
```
|
||||
|
||||
### 7.2 Stripe Webhook Configuration
|
||||
|
||||
Production webhook endpoint:
|
||||
- URL: `https://biblical-guide.com/api/stripe/webhook`
|
||||
- Events:
|
||||
- `customer.subscription.created`
|
||||
- `customer.subscription.updated`
|
||||
- `customer.subscription.deleted`
|
||||
- `invoice.payment_succeeded`
|
||||
- `invoice.payment_failed`
|
||||
- `checkout.session.completed` (existing)
|
||||
- `checkout.session.expired` (existing)
|
||||
|
||||
### 7.3 Database Migration
|
||||
|
||||
Run in production:
|
||||
```bash
|
||||
npx prisma migrate deploy
|
||||
```
|
||||
|
||||
### 7.4 Monitoring
|
||||
|
||||
Set up monitoring for:
|
||||
- Subscription webhook failures
|
||||
- Payment failures
|
||||
- Limit enforcement errors
|
||||
- Subscription status inconsistencies
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Phase 1** - Database schema (30 min)
|
||||
2. **Phase 2** - Conversation limits (1 hour)
|
||||
3. **Phase 3** - Stripe subscription APIs (2 hours)
|
||||
4. **Phase 4** - Frontend pages (3 hours)
|
||||
5. **Phase 5** - Translations (1 hour)
|
||||
6. **Phase 6** - Testing (2 hours)
|
||||
7. **Phase 7** - Deployment (1 hour)
|
||||
|
||||
**Total Estimated Time:** 10-12 hours
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Technical
|
||||
- ✅ All webhook events handled correctly
|
||||
- ✅ No conversation limit bypasses
|
||||
- ✅ Proper subscription status sync
|
||||
- ✅ Clean upgrade/downgrade flows
|
||||
|
||||
### Business
|
||||
- Track conversion rate: free → premium
|
||||
- Monitor churn rate
|
||||
- Track average subscription lifetime
|
||||
- Monitor support tickets related to limits
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Team/family plans
|
||||
- Annual discount improvements
|
||||
- Gift subscriptions
|
||||
- Free trial period for Premium
|
||||
- Referral program
|
||||
- Custom limits for special users
|
||||
- API access tier
|
||||
- Lifetime access option
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Keep donations separate from subscriptions
|
||||
- Donations do NOT grant subscription perks
|
||||
- Clear communication about free tier limits
|
||||
- Grace period for payment failures (3 days)
|
||||
- Prorated charges when upgrading mid-cycle
|
||||
- Email notifications for limit approaching
|
||||
- Email notifications for payment issues
|
||||
Reference in New Issue
Block a user