9.7 KiB
9.7 KiB
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:
-
Ask for Requirements:
- Pricing tiers (Basic, Pro, Enterprise)
- Billing interval (monthly, annual)
- Features per tier
- Trial period
-
Generate Pricing Configuration:
// 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,
},
},
};
- Generate Subscription Service:
// 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;
}
}
}
- Generate Usage Tracking:
// 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