Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save digitaldrreamer/8cb98f2bb4396b7210597dc11e24f77f to your computer and use it in GitHub Desktop.
Save digitaldrreamer/8cb98f2bb4396b7210597dc11e24f77f to your computer and use it in GitHub Desktop.
flutterwave_endpoint
const express = require("express");
const Sentry = require("@sentry/node");
const prisma = require("./prisma"); // Adjust to the correct path for your Prisma instance
const { verifyFlutterwavePayment, fetchSubscriptions } = require("./functions/payment"); // Payment utility functions
const { FLUTTERWAVE_WEBHOOK_SECRET_DEV, FLUTTERWAVE_WEBHOOK_SECRET_PROD } = process.env; // Environment variables for webhook secrets
const { triggerUpdateNoBtn } = require("./functions/emails"); // Email notification utility
const logger = require("./utils/logger"); // Custom logger for structured logging
const { format } = require("timeago.js"); // Library for formatting dates relative to current time
const router = express.Router();
// Handle POST requests to the Flutterwave webhook
router.post("/flw-webhook", async (req, res) => {
logger.start("Processing Flutterwave webhook"); // Log the start of processing
// Retrieve the verification hash from the request headers
const hash = req.headers["verif-hash"];
logger.debug("Received verification hash", { hash });
// Retrieve saved hashes for comparison
const savedHashProd = FLUTTERWAVE_WEBHOOK_SECRET_PROD;
const savedHashDev = FLUTTERWAVE_WEBHOOK_SECRET_DEV;
// Verify the hash; if invalid, return a 401 Unauthorized response
if (hash !== savedHashProd && hash !== savedHashDev) {
logger.warn("Invalid verification hash");
return res.status(401).json({});
}
try {
// Parse the JSON body of the webhook request
const body = req.body;
logger.raw("Webhook payload received", body);
if (!body) {
logger.error("Missing webhook body");
return res.status(401).json({});
}
// Save the webhook payload in the database for auditing and debugging
await prisma.webhooks.create({
data: {
provider: "flutterwave",
data: body,
},
});
logger.info("Webhook payload saved to database");
// Process 'charge.completed' events (e.g., successful payments)
if (body.event === "charge.completed") {
logger.info("Processing 'charge.completed' event");
const verify = await verifyFlutterwavePayment(body.data.id); // Verify payment status with Flutterwave
if (verify?.success) {
logger.success("'charge.completed' event processed successfully");
return res.json({});
}
}
// Process 'subscription.cancelled' events
if (body.event === "subscription.cancelled") {
logger.info("Processing 'subscription.cancelled' event");
// Fetch subscription details using provided email and plan ID
const sub = await fetchSubscriptions({
email: body?.customer?.email,
plan_id: body?.plan?.id,
});
logger.debug("Fetched subscription details", { sub });
// Update payment record in the database to reflect the cancellation
const payment = await prisma.payment.update({
where: {
subscription_code: sub.data?.response?.data[0]?.id,
},
data: {
cancelledAt: new Date(),
},
select: {
job: {
select: {
title: true,
},
},
},
});
logger.info("Updated payment record", { payment });
// Helper function to calculate the next billing date for a subscription
const getNextBillingDate = (createdAt) => {
logger.debug("Calculating next billing date", { createdAt });
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
if (!isoRegex.test(createdAt)) {
logger.error("Invalid date format", { createdAt });
throw new Error("Invalid date format. Expected ISO 8601.");
}
const subscriptionStart = new Date(createdAt);
if (isNaN(subscriptionStart.getTime())) {
logger.error("Invalid date provided", { createdAt });
throw new Error("Invalid date provided");
}
const nextBilling = new Date(subscriptionStart);
nextBilling.setUTCMonth(nextBilling.getUTCMonth() + 1); // Add one month to the subscription start date
logger.success("Next billing date calculated", { nextBilling });
return nextBilling.toISOString();
};
// Calculate the next billing date based on the subscription's creation date
const nextBillingDate = getNextBillingDate(
sub.data?.response?.data[0]?.created_at
);
// Send a notification email to the user about the cancellation
await triggerUpdateNoBtn({
update_title: "Your Subscription has been cancelled",
update_text: `Your subscription for <code>${payment.job.title}</code> has been cancelled as per your request. The job will expire in ${format(nextBillingDate)}. You can re-activate it at any time on your dashboard.`,
email: sub.data?.response?.data[0]?.customer.customer_email,
});
logger.success("'subscription.cancelled' event processed successfully");
}
// Respond with a 200 OK status to indicate successful processing
return res.status(200).json({});
} catch (e) {
// Log and report any errors encountered during processing
logger.error("Error processing webhook", { error: e.message });
Sentry.captureException(e);
return res.status(500).json({
error: true,
message: "Something went wrong",
data: null,
});
} finally {
logger.end("Finished processing Flutterwave webhook"); // Log the end of processing
}
});
module.exports = router;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment