Subscription Management
Complete subscription lifecycle management with billing oversight, plan changes, and revenue tracking.
Preview
Overview
The subscription management interface provides administrators with comprehensive control over user subscriptions, billing operations, and revenue optimization.
Subscription List Interface
Data Fetching
typescript
const { data: subscriptionsData, refresh: refreshSubscriptions } = await useFetch('/api/admin/subscriptions', {
query: {
page: currentPage.value,
limit: itemsPerPage.value,
status: selectedStatus.value,
plan: selectedPlan.value,
dateRange: dateRange.value
}
})Subscription Metrics
Real-time subscription statistics:
typescript
const subscriptionStats = computed(() => ({
total: subscriptionsData.value?.total || 0,
active: subscriptionsData.value?.subscriptions?.filter(s => s.status === 'active').length || 0,
trialing: subscriptionsData.value?.subscriptions?.filter(s => s.status === 'trialing').length || 0,
pastDue: subscriptionsData.value?.subscriptions?.filter(s => s.status === 'past_due').length || 0,
canceled: subscriptionsData.value?.subscriptions?.filter(s => s.status === 'canceled').length || 0,
totalRevenue: calculateTotalRevenue(subscriptionsData.value?.subscriptions || []),
mrr: calculateMRR(subscriptionsData.value?.subscriptions || [])
}))
const calculateTotalRevenue = (subscriptions) => {
return subscriptions.reduce((sum, sub) => {
return sum + (sub.status === 'active' ? (sub.planPrice / 100) : 0)
}, 0)
}
const calculateMRR = (subscriptions) => {
return subscriptions.reduce((sum, sub) => {
if (sub.status === 'active' && sub.interval === 'month') {
return sum + (sub.planPrice / 100)
} else if (sub.status === 'active' && sub.interval === 'year') {
return sum + (sub.planPrice / 100 / 12)
}
return sum
}, 0)
}Revenue Dashboard
Revenue Overview Cards
typescript
const revenueMetrics = computed(() => ({
totalRevenue: subscriptionStats.value.totalRevenue,
mrr: subscriptionStats.value.mrr,
arr: subscriptionStats.value.mrr * 12,
averageRevenuePerUser: subscriptionStats.value.totalRevenue / subscriptionStats.value.active,
churnRate: calculateChurnRate(),
growthRate: calculateGrowthRate()
}))Revenue Chart
Interactive revenue visualization:
typescript
const createRevenueChart = () => {
const ctx = revenueChart.value?.getContext('2d')
if (!ctx) return
revenueChartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: revenueData.value.map(d =>
new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
),
datasets: [{
label: 'Daily Revenue ($)',
data: revenueData.value.map(d => d.revenue),
borderColor: '#000000',
backgroundColor: 'rgba(0, 0, 0, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (value) => `$${value}`
}
}
}
}
})
}Subscription Actions
Individual Subscription Management
typescript
const subscriptionActions = [
{
label: 'View Details',
icon: 'eye',
action: (subscription) => showSubscriptionDetails(subscription),
color: 'blue'
},
{
label: 'Change Plan',
icon: 'refresh',
action: (subscription) => changePlan(subscription),
color: 'green',
condition: (sub) => sub.status === 'active'
},
{
label: 'Cancel Subscription',
icon: 'x',
action: (subscription) => cancelSubscription(subscription),
color: 'red',
condition: (sub) => sub.status === 'active'
},
{
label: 'Resume Subscription',
icon: 'play',
action: (subscription) => resumeSubscription(subscription),
color: 'green',
condition: (sub) => sub.status === 'canceled' && !sub.cancel_at_period_end
}
]Cancel Subscription
typescript
const cancelSubscription = async (subscription) => {
const options = await showCancelModal(subscription)
if (options) {
try {
await $fetch('/api/subscription/cancel', {
method: 'POST',
body: {
subscriptionId: subscription.stripe_subscription_id,
immediately: options.immediately,
reason: options.reason
}
})
refreshSubscriptions()
toast.success('Subscription canceled successfully')
} catch (error) {
toast.error('Failed to cancel subscription')
}
}
}
const showCancelModal = (subscription) => {
return new Promise((resolve) => {
const modal = {
title: 'Cancel Subscription',
subscription,
options: {
immediately: false,
reason: ''
},
onConfirm: (options) => resolve(options),
onCancel: () => resolve(null)
}
cancelModal.value = modal
showCancelModal.value = true
})
}Resume Subscription
typescript
const resumeSubscription = async (subscription) => {
const confirmed = await showConfirmModal(
'Resume Subscription',
`Resume ${subscription.user.full_name}'s ${subscription.plan.name} subscription?`
)
if (confirmed) {
try {
await $fetch('/api/subscription/resume', {
method: 'POST',
body: { subscriptionId: subscription.stripe_subscription_id }
})
refreshSubscriptions()
toast.success('Subscription resumed successfully')
} catch (error) {
toast.error('Failed to resume subscription')
}
}
}Plan Changes
typescript
const changePlan = async (subscription) => {
const newPlan = await showPlanChangeModal(subscription)
if (newPlan) {
try {
await $fetch('/api/subscription/change-plan', {
method: 'POST',
body: {
subscriptionId: subscription.stripe_subscription_id,
newPriceId: newPlan.stripe_price_id,
prorate: true
}
})
refreshSubscriptions()
toast.success('Plan changed successfully')
} catch (error) {
toast.error('Failed to change plan')
}
}
}Billing Management
Payment Issues
Handle failed payments and past due subscriptions:
typescript
const pastDueSubscriptions = computed(() =>
subscriptionsData.value?.subscriptions?.filter(s => s.status === 'past_due') || []
)
const retryPayment = async (subscription) => {
try {
await $fetch('/api/billing/retry-payment', {
method: 'POST',
body: { subscriptionId: subscription.stripe_subscription_id }
})
refreshSubscriptions()
toast.success('Payment retry initiated')
} catch (error) {
toast.error('Failed to retry payment')
}
}Refund Processing
typescript
const processRefund = async (subscription, amount = null) => {
const refundData = await showRefundModal(subscription, amount)
if (refundData) {
try {
await $fetch('/api/billing/refund', {
method: 'POST',
body: {
subscriptionId: subscription.stripe_subscription_id,
amount: refundData.amount,
reason: refundData.reason
}
})
refreshSubscriptions()
toast.success('Refund processed successfully')
} catch (error) {
toast.error('Failed to process refund')
}
}
}Subscription Analytics
Churn Analysis
typescript
const churnAnalysis = computed(() => {
const now = new Date()
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)
const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const canceledThisMonth = subscriptionsData.value?.subscriptions?.filter(s =>
s.status === 'canceled' &&
new Date(s.canceled_at) >= thisMonth
).length || 0
const activeLastMonth = subscriptionsData.value?.subscriptions?.filter(s =>
s.status === 'active' ||
(s.status === 'canceled' && new Date(s.canceled_at) >= thisMonth)
).length || 0
return {
churnRate: activeLastMonth > 0 ? (canceledThisMonth / activeLastMonth) * 100 : 0,
churned: canceledThisMonth,
retained: activeLastMonth - canceledThisMonth
}
})Revenue Forecasting
typescript
const revenueForecast = computed(() => {
const currentMRR = subscriptionStats.value.mrr
const growthRate = calculateMonthlyGrowthRate()
const forecast = []
for (let i = 1; i <= 12; i++) {
const projectedMRR = currentMRR * Math.pow(1 + growthRate, i)
forecast.push({
month: new Date(Date.now() + i * 30 * 24 * 60 * 60 * 1000).toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
mrr: projectedMRR,
arr: projectedMRR * 12
})
}
return forecast
})Subscription Filtering
Advanced Filters
typescript
const filters = reactive({
status: 'all',
plan: 'all',
dateRange: {
start: null,
end: null
},
paymentStatus: 'all',
trialStatus: 'all',
revenueRange: {
min: null,
max: null
}
})
const filterOptions = {
status: [
{ value: 'all', label: 'All Statuses' },
{ value: 'active', label: 'Active' },
{ value: 'trialing', label: 'Trial' },
{ value: 'past_due', label: 'Past Due' },
{ value: 'canceled', label: 'Canceled' }
],
plan: [
{ value: 'all', label: 'All Plans' },
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro' },
{ value: 'enterprise', label: 'Enterprise' }
]
}Bulk Operations
Bulk Subscription Actions
typescript
const bulkActions = [
{
label: 'Export Selected',
action: 'export',
icon: 'download'
},
{
label: 'Send Billing Reminder',
action: 'billing_reminder',
icon: 'mail'
},
{
label: 'Apply Discount',
action: 'discount',
icon: 'percent'
},
{
label: 'Cancel Subscriptions',
action: 'cancel',
icon: 'x',
requiresConfirmation: true
}
]
const executeBulkAction = async (action) => {
if (selectedSubscriptions.value.length === 0) {
toast.warning('No subscriptions selected')
return
}
const result = await $fetch('/api/admin/subscriptions/bulk', {
method: 'POST',
body: {
subscriptionIds: selectedSubscriptions.value,
action,
adminId: user.value.id
}
})
selectedSubscriptions.value = []
refreshSubscriptions()
toast.success(`Bulk ${action} completed successfully`)
}Real-time Updates
Subscription Events
typescript
onMounted(() => {
const { $socket } = useNuxtApp()
$socket.on('subscription:created', (data) => {
if (subscriptionsData.value?.subscriptions) {
subscriptionsData.value.subscriptions.unshift(data)
subscriptionsData.value.total++
}
toast.success(`New subscription: ${data.user.full_name}`)
})
$socket.on('subscription:canceled', (data) => {
const subIndex = subscriptionsData.value?.subscriptions?.findIndex(s => s.id === data.id)
if (subIndex !== -1) {
subscriptionsData.value.subscriptions[subIndex].status = 'canceled'
subscriptionsData.value.subscriptions[subIndex].canceled_at = data.canceled_at
}
toast.warning(`Subscription canceled: ${data.user.full_name}`)
})
$socket.on('payment:failed', (data) => {
const subIndex = subscriptionsData.value?.subscriptions?.findIndex(s => s.id === data.subscriptionId)
if (subIndex !== -1) {
subscriptionsData.value.subscriptions[subIndex].status = 'past_due'
}
toast.error(`Payment failed: ${data.user.full_name}`)
})
})API Endpoints
Subscription Management APIs
GET /api/admin/subscriptions # List subscriptions with filtering
GET /api/admin/subscriptions/:id # Get subscription details
POST /api/subscription/cancel # Cancel subscription
POST /api/subscription/resume # Resume subscription
POST /api/subscription/change-plan # Change subscription plan
POST /api/billing/retry-payment # Retry failed payment
POST /api/billing/refund # Process refund
POST /api/admin/subscriptions/bulk # Bulk operations
GET /api/admin/revenue/analytics # Revenue analyticsExport & Reporting
Subscription Export
typescript
const exportSubscriptions = async (format = 'csv') => {
const exportData = {
subscriptions: selectedSubscriptions.value.length > 0 ? selectedSubscriptions.value : 'all',
format,
fields: [
'id', 'user_email', 'user_name', 'plan_name', 'status',
'current_period_start', 'current_period_end', 'amount'
],
filters: filters
}
const response = await $fetch('/api/admin/subscriptions/export', {
method: 'POST',
body: exportData
})
downloadFile(response.data, `subscriptions-export-${new Date().toISOString().split('T')[0]}.${format}`)
toast.success('Subscription data exported successfully')
}The subscription management interface provides comprehensive tools for managing billing, revenue optimization, and subscription lifecycle operations while maintaining full integration with Stripe's billing system.
