Created
January 23, 2025 18:59
-
-
Save digitaldrreamer/8cb98f2bb4396b7210597dc11e24f77f to your computer and use it in GitHub Desktop.
flutterwave_endpoint
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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