PHP 8.2.30
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

Name Size Perms Modified Actions
- drwxr-xr-x 2026-03-20 23:25:48
Edit Download
127 B lr--r--r-- 2026-03-14 01:49:22
Edit Download
15.34 KB lrw-r--r-- 2026-02-19 22:06:47
Edit Download
30.77 KB lrw-r--r-- 2026-03-04 06:33:18
Edit Download
62.98 KB lrw-r--r-- 2026-03-01 23:46:52
Edit Download
10.79 KB lrw-r--r-- 2026-02-07 16:28:16
Edit Download
14.17 KB lrw-r--r-- 2026-02-27 04:02:38
Edit Download
6.32 KB lrw-r--r-- 2026-02-06 17:10:19
Edit Download
15.13 KB lrw-r--r-- 2026-03-01 05:05:51
Edit Download
8.50 KB lrw-r--r-- 2026-03-01 05:05:52
Edit Download
12.15 KB lrw-r--r-- 2026-02-27 04:02:38
Edit Download
25.39 KB lrw-r--r-- 2026-03-01 05:05:51
Edit Download
17.61 KB lrw-r--r-- 2026-03-01 05:05:51
Edit Download
6.45 KB lrw-r--r-- 2026-03-01 05:05:51
Edit Download
2.33 KB lrw-r--r-- 2026-03-01 05:05:51
Edit Download
12.78 KB lrw-r--r-- 2026-02-21 08:13:30
Edit Download
9.96 KB lrw-r--r-- 2026-03-01 05:05:51
Edit Download
8.51 KB lrw-r--r-- 2026-02-20 19:31:28
Edit Download

If ZipArchive is unavailable, a .tar will be created (no compression).