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