Preview: subscriptionController.js
Size: 12.78 KB
//home/byroehnu/easepay.easetack.com/controllers/subscriptionController.js
// controllers/subscriptionController.js - Enhanced Subscription Management with Flutterwave
const { prisma } = require('../config/prisma');
const flutterwaveService = require('../services/flutterwaveService');
/**
* Enhanced Subscription Controller
* Handles Flutterwave integration and subscription lifecycle
*/
/**
* Get all available plans
*/
const getPlans = async (req, res) => {
try {
const plans = await prisma.plan.findMany({
where: { isActive: true },
orderBy: { sortOrder: 'asc' }
});
res.json({
success: true,
plans: plans.map(plan => ({
id: plan.id,
name: plan.name,
description: plan.description,
priceMonthly: plan.priceMonthly,
priceYearly: plan.priceYearly,
currency: plan.currency,
features: {
maxInvoices: plan.maxInvoices === -1 ? 'Unlimited' : plan.maxInvoices,
maxTeamMembers: plan.maxTeamMembers === -1 ? 'Unlimited' : plan.maxTeamMembers,
maxReportExports: plan.maxReportExports === -1 ? 'Unlimited' : plan.maxReportExports,
hasAdvancedReports: plan.hasAdvancedReports,
hasAPIAccess: plan.hasAPIAccess,
hasPrioritySupport: plan.hasPrioritySupport
}
}))
});
} catch (error) {
console.error('❌ Get plans error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch plans',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Get current user's subscription details
*/
const getMySubscription = async (req, res) => {
try {
const userId = req.user.id;
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
subscription: true,
usage: true,
limits: true
}
});
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Get current plan details
let currentPlan = null;
if (user.subscription?.planName && user.subscription.planName !== 'Free') {
currentPlan = await prisma.plan.findUnique({
where: { name: user.subscription.planName }
});
}
res.json({
success: true,
subscription: {
status: user.subscription?.status || 'INACTIVE',
planName: user.subscription?.planName || 'Free',
nextBillingDate: user.subscription?.nextBillingDate,
gracePeriodEnd: user.subscription?.gracePeriodEnd,
amount: user.subscription?.amount,
currency: user.subscription?.currency || 'NGN',
interval: user.subscription?.interval
},
usage: {
invoiceCount: user.usage?.invoiceCount || 0,
teamMemberCount: user.usage?.teamMemberCount || 1,
reportExports: user.usage?.reportExports || 0
},
limits: {
maxInvoices: user.limits?.maxInvoices || 5,
maxTeamMembers: user.limits?.maxTeamMembers || 1,
maxReportExports: user.limits?.maxReportExports || 2,
hasAdvancedReports: user.limits?.hasAdvancedReports || false,
hasAPIAccess: user.limits?.hasAPIAccess || false
},
currentPlan
});
} catch (error) {
console.error('❌ Get subscription error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch subscription',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Initiate subscription process with Flutterwave
*/
const subscribe = async (req, res) => {
try {
const userId = req.user.id;
const { planId, interval = 'monthly', callbackUrl, redirectUrl } = req.body;
// Validate input
if (!planId || !['monthly', 'yearly'].includes(interval)) {
return res.status(400).json({
success: false,
message: 'Invalid plan or interval specified'
});
}
// Get plan details
const plan = await prisma.plan.findUnique({
where: { id: planId }
});
if (!plan || !plan.isActive) {
return res.status(404).json({
success: false,
message: 'Plan not found or inactive'
});
}
// Get user details
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Don't allow subscription to Free plan
if (plan.name === 'Free') {
return res.status(400).json({
success: false,
message: 'Cannot subscribe to free plan'
});
}
// Get the appropriate Flutterwave plan ID
const flwPlanId = interval === 'monthly' ? plan.flwPlanIdMonthly : plan.flwPlanIdYearly;
const amount = interval === 'monthly' ? plan.priceMonthly : plan.priceYearly;
if (!flwPlanId) {
return res.status(400).json({
success: false,
message: `${interval} subscription not available for this plan`
});
}
// Create or get Flutterwave customer
let flwCustomerId = user.subscription?.flwCustomerId;
if (!flwCustomerId) {
try {
const customer = await flutterwaveService.createCustomer({
email: user.email,
phone: user.phone,
name: `${user.firstName} ${user.lastName}`
});
flwCustomerId = customer.id;
} catch (error) {
console.log('⚠️ Customer creation failed, proceeding without customer ID');
}
}
// Generate unique transaction reference
const tx_ref = `easepay_sub_${userId}_${Date.now()}`;
// Generate payment link
const paymentData = await flutterwaveService.generatePaymentLink({
planId: flwPlanId,
tx_ref,
customer: {
email: user.email,
phone: user.phone,
name: `${user.firstName} ${user.lastName}`,
amount: amount,
planName: plan.name
},
redirect_url: redirectUrl || `${process.env.FRONTEND_URL}/subscription/success`,
callback_url: callbackUrl || `${process.env.BASE_URL}/api/webhook/flutterwave`
});
// Store subscription initiation data
await prisma.userSubscription.upsert({
where: { userId },
update: {
flwCustomerId,
status: 'INACTIVE' // Will be updated by webhook
},
create: {
userId,
planName: plan.name,
flwCustomerId,
status: 'INACTIVE',
amount,
currency: plan.currency,
interval
}
});
res.json({
success: true,
message: 'Payment link generated successfully',
paymentLink: paymentData.link,
tx_ref,
planName: plan.name,
amount,
interval
});
} catch (error) {
console.error('❌ Subscribe error:', error);
res.status(500).json({
success: false,
message: 'Failed to initiate subscription',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Cancel current subscription
*/
const cancelSubscription = async (req, res) => {
try {
const userId = req.user.id;
const user = await prisma.user.findUnique({
where: { id: userId },
include: { subscription: true }
});
if (!user?.subscription) {
return res.status(404).json({
success: false,
message: 'No active subscription found'
});
}
const subscription = user.subscription;
if (subscription.status === 'CANCELLED' || subscription.status === 'INACTIVE') {
return res.status(400).json({
success: false,
message: 'Subscription is already cancelled or inactive'
});
}
// Cancel in Flutterwave if subscription ID exists
if (subscription.flwSubscriptionId) {
try {
await flutterwaveService.cancelSubscription(subscription.flwSubscriptionId);
console.log('✅ Flutterwave subscription cancelled');
} catch (error) {
console.error('⚠️ Failed to cancel Flutterwave subscription:', error);
// Continue with local cancellation even if Flutterwave fails
}
}
// Update subscription status
await prisma.userSubscription.update({
where: { userId },
data: {
status: 'CANCELLED',
planId: null
}
});
// Set user limits to Free tier
await prisma.userLimits.upsert({
where: { userId },
update: {
maxInvoices: 5,
maxTeamMembers: 1,
maxReportExports: 2,
hasAdvancedReports: false,
hasAPIAccess: false
},
create: {
userId,
maxInvoices: 5,
maxTeamMembers: 1,
maxReportExports: 2,
hasAdvancedReports: false,
hasAPIAccess: false
}
});
res.json({
success: true,
message: `Subscription cancelled. You have access until ${subscription.nextBillingDate?.toDateString() || 'now'}`,
accessUntil: subscription.nextBillingDate
});
} catch (error) {
console.error('❌ Cancel subscription error:', error);
res.status(500).json({
success: false,
message: 'Failed to cancel subscription',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Get admin financial summary
*/
const getAdminStats = async (req, res) => {
try {
// Check if user is admin
if (req.user.role !== 'ADMIN') {
return res.status(403).json({
success: false,
message: 'Access denied. Admin role required.'
});
}
// Get subscription statistics
const stats = await prisma.userSubscription.groupBy({
by: ['planName', 'status'],
_count: {
id: true
},
_sum: {
amount: true
}
});
// Calculate MRR and other metrics
let totalMRR = 0;
let totalActiveUsers = 0;
let pastDueUsers = 0;
const planBreakdown = {};
for (const stat of stats) {
if (stat.status === 'ACTIVE') {
totalActiveUsers += stat._count.id;
const monthlyAmount = stat._sum.amount || 0;
totalMRR += parseFloat(monthlyAmount);
planBreakdown[stat.planName] = {
users: stat._count.id,
revenue: parseFloat(monthlyAmount)
};
} else if (stat.status === 'PAST_DUE') {
pastDueUsers += stat._count.id;
}
}
// Get total users
const totalUsers = await prisma.user.count();
// Get recent subscriptions (last 30 days)
const recentSubscriptions = await prisma.userSubscription.count({
where: {
createdAt: {
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
},
status: 'ACTIVE'
}
});
res.json({
success: true,
stats: {
totalUsers,
totalActiveUsers,
pastDueUsers,
freeUsers: totalUsers - totalActiveUsers - pastDueUsers,
totalMRR: totalMRR.toFixed(2),
currency: 'NGN',
recentSubscriptions,
planBreakdown,
conversionRate: totalUsers > 0 ? ((totalActiveUsers / totalUsers) * 100).toFixed(2) : 0,
churnRisk: pastDueUsers
}
});
} catch (error) {
console.error('❌ Admin stats error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch admin statistics',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Update user's plan limits (Internal use - called by webhook)
*/
const updateUserLimits = async (userId, planName) => {
try {
const plan = await prisma.plan.findUnique({
where: { name: planName }
});
if (!plan) {
console.error(`❌ Plan not found: ${planName}`);
return;
}
await prisma.userLimits.upsert({
where: { userId },
update: {
maxInvoices: plan.maxInvoices,
maxTeamMembers: plan.maxTeamMembers,
maxReportExports: plan.maxReportExports,
hasAdvancedReports: plan.hasAdvancedReports,
hasAPIAccess: plan.hasAPIAccess
},
create: {
userId,
maxInvoices: plan.maxInvoices,
maxTeamMembers: plan.maxTeamMembers,
maxReportExports: plan.maxReportExports,
hasAdvancedReports: plan.hasAdvancedReports,
hasAPIAccess: plan.hasAPIAccess
}
});
console.log(`✅ User limits updated for ${userId} - Plan: ${planName}`);
} catch (error) {
console.error('❌ Update user limits error:', error);
}
};
module.exports = {
getPlans,
getMySubscription,
subscribe,
cancelSubscription,
getAdminStats,
updateUserLimits
};
Directory Contents
Dirs: 1 × Files: 17