387 lines
9.7 KiB
Markdown
387 lines
9.7 KiB
Markdown
# Subscription Management
|
|
|
|
Generate complete subscription management system.
|
|
|
|
## Task
|
|
|
|
You are a subscription billing expert. Generate production-ready subscription management with billing, upgrades, and cancellations.
|
|
|
|
### Steps:
|
|
|
|
1. **Ask for Requirements**:
|
|
- Pricing tiers (Basic, Pro, Enterprise)
|
|
- Billing interval (monthly, annual)
|
|
- Features per tier
|
|
- Trial period
|
|
|
|
2. **Generate Pricing Configuration**:
|
|
|
|
```typescript
|
|
// config/pricing.ts
|
|
export const PRICING_PLANS = {
|
|
basic: {
|
|
id: 'basic',
|
|
name: 'Basic',
|
|
description: 'For individuals and small teams',
|
|
prices: {
|
|
monthly: {
|
|
amount: 9,
|
|
stripePriceId: 'price_basic_monthly',
|
|
},
|
|
annual: {
|
|
amount: 90,
|
|
stripePriceId: 'price_basic_annual',
|
|
savings: 18, // 2 months free
|
|
},
|
|
},
|
|
features: [
|
|
'10 projects',
|
|
'5 GB storage',
|
|
'Basic support',
|
|
],
|
|
limits: {
|
|
projects: 10,
|
|
storage: 5 * 1024 * 1024 * 1024, // 5 GB in bytes
|
|
apiCallsPerMonth: 10000,
|
|
},
|
|
},
|
|
pro: {
|
|
id: 'pro',
|
|
name: 'Pro',
|
|
description: 'For growing teams',
|
|
prices: {
|
|
monthly: {
|
|
amount: 29,
|
|
stripePriceId: 'price_pro_monthly',
|
|
},
|
|
annual: {
|
|
amount: 290,
|
|
stripePriceId: 'price_pro_annual',
|
|
savings: 58,
|
|
},
|
|
},
|
|
features: [
|
|
'Unlimited projects',
|
|
'50 GB storage',
|
|
'Priority support',
|
|
'Advanced analytics',
|
|
],
|
|
limits: {
|
|
projects: Infinity,
|
|
storage: 50 * 1024 * 1024 * 1024,
|
|
apiCallsPerMonth: 100000,
|
|
},
|
|
},
|
|
enterprise: {
|
|
id: 'enterprise',
|
|
name: 'Enterprise',
|
|
description: 'For large organizations',
|
|
prices: {
|
|
monthly: {
|
|
amount: 99,
|
|
stripePriceId: 'price_enterprise_monthly',
|
|
},
|
|
annual: {
|
|
amount: 990,
|
|
stripePriceId: 'price_enterprise_annual',
|
|
savings: 198,
|
|
},
|
|
},
|
|
features: [
|
|
'Unlimited everything',
|
|
'1 TB storage',
|
|
'24/7 dedicated support',
|
|
'Custom integrations',
|
|
'SLA guarantee',
|
|
],
|
|
limits: {
|
|
projects: Infinity,
|
|
storage: 1024 * 1024 * 1024 * 1024,
|
|
apiCallsPerMonth: Infinity,
|
|
},
|
|
},
|
|
};
|
|
```
|
|
|
|
3. **Generate Subscription Service**:
|
|
|
|
```typescript
|
|
// services/subscription.service.ts
|
|
import Stripe from 'stripe';
|
|
import { PRICING_PLANS } from '../config/pricing';
|
|
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
|
|
export class SubscriptionService {
|
|
// Create subscription
|
|
async create(userId: string, planId: string, interval: 'monthly' | 'annual') {
|
|
const user = await db.users.findUnique({ where: { id: userId } });
|
|
const plan = PRICING_PLANS[planId];
|
|
|
|
if (!plan) throw new Error('Invalid plan');
|
|
|
|
// Create Stripe customer if doesn't exist
|
|
let customerId = user.stripeCustomerId;
|
|
if (!customerId) {
|
|
const customer = await stripe.customers.create({
|
|
email: user.email,
|
|
metadata: { userId },
|
|
});
|
|
customerId = customer.id;
|
|
await db.users.update({
|
|
where: { id: userId },
|
|
data: { stripeCustomerId: customerId },
|
|
});
|
|
}
|
|
|
|
// Create subscription
|
|
const subscription = await stripe.subscriptions.create({
|
|
customer: customerId,
|
|
items: [{ price: plan.prices[interval].stripePriceId }],
|
|
trial_period_days: 14, // 14-day trial
|
|
payment_behavior: 'default_incomplete',
|
|
payment_settings: {
|
|
save_default_payment_method: 'on_subscription',
|
|
},
|
|
expand: ['latest_invoice.payment_intent'],
|
|
});
|
|
|
|
// Save to database
|
|
await db.subscriptions.create({
|
|
data: {
|
|
userId,
|
|
stripeSubscriptionId: subscription.id,
|
|
stripePriceId: plan.prices[interval].stripePriceId,
|
|
status: subscription.status,
|
|
planId,
|
|
interval,
|
|
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
trialEnd: subscription.trial_end
|
|
? new Date(subscription.trial_end * 1000)
|
|
: null,
|
|
},
|
|
});
|
|
|
|
return {
|
|
subscriptionId: subscription.id,
|
|
clientSecret: subscription.latest_invoice.payment_intent.client_secret,
|
|
};
|
|
}
|
|
|
|
// Upgrade/downgrade subscription
|
|
async changePlan(userId: string, newPlanId: string, newInterval: 'monthly' | 'annual') {
|
|
const subscription = await db.subscriptions.findFirst({
|
|
where: { userId, status: 'active' },
|
|
});
|
|
|
|
if (!subscription) throw new Error('No active subscription');
|
|
|
|
const newPlan = PRICING_PLANS[newPlanId];
|
|
const newPriceId = newPlan.prices[newInterval].stripePriceId;
|
|
|
|
// Update Stripe subscription
|
|
const stripeSubscription = await stripe.subscriptions.retrieve(
|
|
subscription.stripeSubscriptionId
|
|
);
|
|
|
|
const updatedSubscription = await stripe.subscriptions.update(
|
|
subscription.stripeSubscriptionId,
|
|
{
|
|
items: [
|
|
{
|
|
id: stripeSubscription.items.data[0].id,
|
|
price: newPriceId,
|
|
},
|
|
],
|
|
proration_behavior: 'always_invoice', // Prorate charges
|
|
}
|
|
);
|
|
|
|
// Update database
|
|
await db.subscriptions.update({
|
|
where: { id: subscription.id },
|
|
data: {
|
|
stripePriceId: newPriceId,
|
|
planId: newPlanId,
|
|
interval: newInterval,
|
|
},
|
|
});
|
|
|
|
return updatedSubscription;
|
|
}
|
|
|
|
// Cancel subscription
|
|
async cancel(userId: string, cancelAtPeriodEnd = true) {
|
|
const subscription = await db.subscriptions.findFirst({
|
|
where: { userId, status: 'active' },
|
|
});
|
|
|
|
if (!subscription) throw new Error('No active subscription');
|
|
|
|
if (cancelAtPeriodEnd) {
|
|
// Cancel at end of billing period
|
|
await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
|
|
cancel_at_period_end: true,
|
|
});
|
|
|
|
await db.subscriptions.update({
|
|
where: { id: subscription.id },
|
|
data: { cancelAtPeriodEnd: true },
|
|
});
|
|
} else {
|
|
// Cancel immediately
|
|
await stripe.subscriptions.cancel(subscription.stripeSubscriptionId);
|
|
|
|
await db.subscriptions.update({
|
|
where: { id: subscription.id },
|
|
data: { status: 'canceled', canceledAt: new Date() },
|
|
});
|
|
}
|
|
}
|
|
|
|
// Resume canceled subscription
|
|
async resume(userId: string) {
|
|
const subscription = await db.subscriptions.findFirst({
|
|
where: { userId, cancelAtPeriodEnd: true },
|
|
});
|
|
|
|
if (!subscription) throw new Error('No subscription to resume');
|
|
|
|
await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
|
|
cancel_at_period_end: false,
|
|
});
|
|
|
|
await db.subscriptions.update({
|
|
where: { id: subscription.id },
|
|
data: { cancelAtPeriodEnd: false },
|
|
});
|
|
}
|
|
|
|
// Get subscription status
|
|
async getStatus(userId: string) {
|
|
const subscription = await db.subscriptions.findFirst({
|
|
where: { userId },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
|
|
if (!subscription) return null;
|
|
|
|
const plan = PRICING_PLANS[subscription.planId];
|
|
return {
|
|
...subscription,
|
|
plan,
|
|
isActive: subscription.status === 'active',
|
|
isTrialing: subscription.status === 'trialing',
|
|
isCanceling: subscription.cancelAtPeriodEnd,
|
|
};
|
|
}
|
|
|
|
// Check feature access
|
|
async canAccess(userId: string, feature: string, value?: number) {
|
|
const status = await this.getStatus(userId);
|
|
|
|
if (!status || !status.isActive) return false;
|
|
|
|
const limits = status.plan.limits;
|
|
|
|
// Check specific limits
|
|
switch (feature) {
|
|
case 'projects':
|
|
const projectCount = await db.projects.count({ where: { userId } });
|
|
return projectCount < limits.projects;
|
|
|
|
case 'storage':
|
|
const storageUsed = await this.getStorageUsage(userId);
|
|
return storageUsed < limits.storage;
|
|
|
|
case 'api_calls':
|
|
const apiCalls = await this.getApiCallsThisMonth(userId);
|
|
return apiCalls < limits.apiCallsPerMonth;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
4. **Generate Usage Tracking**:
|
|
|
|
```typescript
|
|
// Track API usage for metered billing
|
|
export class UsageTracker {
|
|
async recordApiCall(userId: string) {
|
|
const subscription = await db.subscriptions.findFirst({
|
|
where: { userId, status: 'active' },
|
|
});
|
|
|
|
if (!subscription) return;
|
|
|
|
// Increment usage
|
|
await db.usageRecords.create({
|
|
data: {
|
|
subscriptionId: subscription.id,
|
|
type: 'api_call',
|
|
quantity: 1,
|
|
timestamp: new Date(),
|
|
},
|
|
});
|
|
|
|
// Optional: Report to Stripe for metered billing
|
|
if (subscription.meteringEnabled) {
|
|
await stripe.subscriptionItems.createUsageRecord(
|
|
subscription.stripeSubscriptionItemId,
|
|
{
|
|
quantity: 1,
|
|
timestamp: Math.floor(Date.now() / 1000),
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
async getUsage(userId: string, period: 'month' | 'all' = 'month') {
|
|
const subscription = await db.subscriptions.findFirst({
|
|
where: { userId },
|
|
});
|
|
|
|
if (!subscription) return null;
|
|
|
|
const startDate =
|
|
period === 'month'
|
|
? new Date(new Date().setDate(1)) // Start of month
|
|
: undefined;
|
|
|
|
const usage = await db.usageRecords.groupBy({
|
|
by: ['type'],
|
|
where: {
|
|
subscriptionId: subscription.id,
|
|
timestamp: startDate ? { gte: startDate } : undefined,
|
|
},
|
|
_sum: {
|
|
quantity: true,
|
|
},
|
|
});
|
|
|
|
return usage;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Best Practices Included:
|
|
|
|
- Trial periods
|
|
- Proration on plan changes
|
|
- Cancel at period end vs immediate
|
|
- Usage tracking for metered billing
|
|
- Feature gating based on plan
|
|
- Subscription resumption
|
|
- Clear pricing configuration
|
|
|
|
### Example Usage:
|
|
|
|
```
|
|
User: "Set up subscription with Basic, Pro, Enterprise tiers"
|
|
Result: Complete subscription system with billing, upgrades, trials
|
|
```
|