Initial commit
This commit is contained in:
386
commands/subscription-manage.md
Normal file
386
commands/subscription-manage.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user