PHP 8.2.30
Preview: webhookController.js Size: 8.51 KB
//home/byroehnu/easepay.easetack.com/controllers/webhookController.js

// controllers/webhookController.js - Flutterwave Webhook Handler
const crypto = require('crypto');
const { prisma } = require('../config/prisma');
const { updateUserLimits } = require('./subscriptionController');

/**
 * Flutterwave Webhook Handler
 * Processes subscription events from Flutterwave
 */
const handleFlutterwaveWebhook = async (req, res) => {
  try {
    // 1. Verify the signature (Security First!)
    const secretHash = process.env.FLW_WEBHOOK_HASH;
    const signature = req.headers['verif-hash'];

    if (!signature || signature !== secretHash) {
      console.error('❌ Invalid webhook signature');
      return res.status(401).json({
        success: false,
        message: "Invalid webhook signature"
      });
    }

    const payload = req.body;
    console.log('📥 Webhook received:', JSON.stringify(payload, null, 2));

    const { status, customer, event, amount, plan, tx_ref } = payload.data || payload;

    // 2. Process successful payments
    if (status === "successful") {
      await handleSuccessfulPayment({
        status,
        customer,
        event,
        amount,
        plan,
        tx_ref
      });
    }

    // 3. Handle failed payments
    if (status === "failed") {
      await handleFailedPayment({
        customer,
        event,
        amount,
        plan,
        tx_ref
      });
    }

    // 4. Handle subscription events
    if (event === "subscription.activated" || event === "subscription.cancelled") {
      await handleSubscriptionEvent({
        event,
        customer,
        plan,
        status
      });
    }

    // Always return a 200 to acknowledge receipt to Flutterwave
    res.status(200).json({
      success: true,
      message: "Webhook processed successfully"
    });

  } catch (error) {
    console.error('❌ Webhook processing error:', error);
    res.status(500).json({
      success: false,
      message: "Webhook processing failed",
      error: process.env.NODE_ENV === 'development' ? error.message : undefined
    });
  }
};

/**
 * Handle successful payment
 */
const handleSuccessfulPayment = async (paymentData) => {
  try {
    const { customer, amount, plan, tx_ref } = paymentData;
    
    console.log(`💰 Processing successful payment for ${customer.email}`);

    // Find user by email
    const user = await prisma.user.findUnique({
      where: { email: customer.email },
      include: { subscription: true }
    });

    if (!user) {
      console.error(`❌ User not found: ${customer.email}`);
      return;
    }

    // Extract plan name from tx_ref or plan data
    let planName = 'Basic'; // Default fallback
    if (tx_ref && tx_ref.includes('_')) {
      // tx_ref format: easepay_sub_userId_timestamp
      // We need to determine plan from the amount or other data
      if (amount >= 10000) planName = 'Enterprise';
      else if (amount >= 5000) planName = 'Pro';
      else if (amount >= 2000) planName = 'Basic';
    }

    // Calculate new billing date (Add 30 days for monthly, 365 for yearly)
    const newDate = new Date();
    const isYearly = amount >= 20000; // Assuming yearly plans are higher amounts
    
    if (isYearly) {
      newDate.setFullYear(newDate.getFullYear() + 1);
    } else {
      newDate.setMonth(newDate.getMonth() + 1);
    }

    // Update user subscription
    await prisma.userSubscription.upsert({
      where: { userId: user.id },
      update: {
        status: 'ACTIVE',
        nextBillingDate: newDate,
        planName,
        amount: parseFloat(amount),
        flwSubscriptionId: plan?.id || null,
        gracePeriodEnd: null // Clear grace period
      },
      create: {
        userId: user.id,
        status: 'ACTIVE',
        nextBillingDate: newDate,
        planName,
        amount: parseFloat(amount),
        currency: 'NGN',
        interval: isYearly ? 'yearly' : 'monthly',
        flwSubscriptionId: plan?.id || null
      }
    });

    // Reset monthly usage counters for new billing cycle
    await prisma.userUsage.upsert({
      where: { userId: user.id },
      update: {
        invoiceCount: 0,
        reportExports: 0,
        lastResetDate: new Date()
        // Don't reset teamMemberCount as it's cumulative
      },
      create: {
        userId: user.id,
        invoiceCount: 0,
        teamMemberCount: 1,
        reportExports: 0,
        lastResetDate: new Date()
      }
    });

    // Update user plan limits
    await updateUserLimits(user.id, planName);

    console.log(`✅ Successfully renewed subscription for ${customer.email} - Plan: ${planName}`);

  } catch (error) {
    console.error('❌ Handle successful payment error:', error);
  }
};

/**
 * Handle failed payment
 */
const handleFailedPayment = async (paymentData) => {
  try {
    const { customer } = paymentData;
    
    console.log(`💸 Processing failed payment for ${customer.email}`);

    // Find user by email
    const user = await prisma.user.findUnique({
      where: { email: customer.email }
    });

    if (!user) {
      console.error(`❌ User not found: ${customer.email}`);
      return;
    }

    // Set grace period (7 days from now)
    const graceDate = new Date();
    graceDate.setDate(graceDate.getDate() + 7);

    // Update subscription status to PAST_DUE
    await prisma.userSubscription.upsert({
      where: { userId: user.id },
      update: {
        status: 'PAST_DUE',
        gracePeriodEnd: graceDate
      },
      create: {
        userId: user.id,
        status: 'PAST_DUE',
        planName: 'Free',
        gracePeriodEnd: graceDate
      }
    });

    console.log(`⚠️ Set grace period for ${customer.email} until ${graceDate.toDateString()}`);

    // TODO: Send payment failed notification email

  } catch (error) {
    console.error('❌ Handle failed payment error:', error);
  }
};

/**
 * Handle subscription lifecycle events
 */
const handleSubscriptionEvent = async (eventData) => {
  try {
    const { event, customer, plan, status } = eventData;
    
    console.log(`📅 Processing subscription event: ${event} for ${customer.email}`);

    // Find user by email
    const user = await prisma.user.findUnique({
      where: { email: customer.email }
    });

    if (!user) {
      console.error(`❌ User not found: ${customer.email}`);
      return;
    }

    switch (event) {
      case 'subscription.activated':
        await prisma.userSubscription.upsert({
          where: { userId: user.id },
          update: {
            status: 'ACTIVE',
            flwSubscriptionId: plan?.id
          },
          create: {
            userId: user.id,
            status: 'ACTIVE',
            planName: 'Basic',
            flwSubscriptionId: plan?.id
          }
        });
        console.log(`✅ Subscription activated for ${customer.email}`);
        break;

      case 'subscription.cancelled':
        await prisma.userSubscription.update({
          where: { userId: user.id },
          data: {
            status: 'CANCELLED'
          }
        });
        console.log(`❌ Subscription cancelled for ${customer.email}`);
        break;

      default:
        console.log(`ℹ️ Unhandled subscription event: ${event}`);
    }

  } catch (error) {
    console.error('❌ Handle subscription event error:', error);
  }
};

/**
 * Test webhook endpoint (for development)
 */
const testWebhook = async (req, res) => {
  try {
    // Simulate successful payment webhook
    const testPayload = {
      event: "charge.completed",
      data: {
        status: "successful",
        amount: 5000,
        currency: "NGN",
        customer: {
          email: req.user.email,
          name: `${req.user.firstName} ${req.user.lastName}`
        },
        tx_ref: `easepay_sub_${req.user.id}_${Date.now()}`,
        plan: {
          id: "test_plan_id",
          name: "Pro Plan"
        }
      }
    };

    // Process the test webhook
    await handleSuccessfulPayment(testPayload.data);

    res.json({
      success: true,
      message: 'Test webhook processed successfully',
      data: testPayload
    });

  } catch (error) {
    console.error('❌ Test webhook error:', error);
    res.status(500).json({
      success: false,
      message: 'Test webhook failed',
      error: process.env.NODE_ENV === 'development' ? error.message : undefined
    });
  }
};

module.exports = {
  handleFlutterwaveWebhook,
  testWebhook
};

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).