Integrating eSewa Payment in a MERN Stack Web Application (part 2)

Samir PokhrelSamir Pokhrel
7 min read

In the final part of our eSewa payment integration tutorial, we’ll cover three crucial steps: serving configuration files from .env , setting up a MongoDB database to store transactions and verifying responses from the payment gateway. But first, let’s recap what we’ve accomplished so far:

In the first part of the tutorial, we:

Set Up a MERN Stack Application: We initialized a new MERN stack project, set up a Node.js and Express backend, and created a React frontend. This foundational setup allowed us to build a robust application for integrating payment features.

Initiated Payment Integration: We implemented basic payment integration by configuring routes in our backend to handle payment requests and redirect users to the eSewa payment gateway.

You can check out Part 1 following this link.

Let’s begin…

Serving Configuration Files from .env

To manage sensitive information such as API keys and secrets, it’s essential to use environment variables stored in a .env file. This approach keeps your configuration secure and prevents sensitive data from being hard-coded into your application.

Step-by-Step:

1.Create a .env File:
In the root of your project directory, create a file named .env. This file will store your environment variables.

echo > .env

2.Add Configuration Variables:
Open the .env file and add your configuration variables. For eSewa, you will require:

PORT=3000
MONGO_URI=mongodb://127.0.0.1:27017/mydatabase
MERCHANT_ID=EPAYTEST
SUCCESS_URL=http://localhost:5173/payment-success
FAILURE_URL=http://localhost:5173/payment-failure
ESEWAPAYMENT_URL=https://rc-epay.esewa.com.np/api/epay/main/v2/form
SECRET=8gBm/:&EnhH.1/q
ESEWAPAYMENT_STATUS_CHECK_URL=https://uat.esewa.com.np/api/epay/transaction/status/

3.Load Environment Variables in Your Application:
Use the dotenv package to load these variables. Ensure dotenv is installed:

npm install dotenv

4.At the beginning of your app.js file, load the environment variables:

require('dotenv').config();

5.Access the environment variables in your code using process.env. For example:

process.env.PORT

Setting up a MongoDB database to store transactions

To track and manage transactions, you need to set up MongoDB to store transaction records. This setup allows you to maintain a history of payments and manage order states effectively.

Step-by-Step:

This blog post assumes that the MongoDB Community Server and MongoDB Compass is already installed and running on your machine. If not, please install it from the MongoDB official website before proceeding.
MongoDB Community Server

1.Install Mongoose package: Install Mongoose for MongoDB object modeling:

npm install mongoose

2.Configure the MongoDB Database:
Create a config folder in the root of the project(server) directory and navigate into the folder and create a file db.config.js

mkdir config
cd config
echo > db.config.js

3.Set Up the MongoDB Connection:
Use Mongoose to connect to your MongoDB instance. This instance could be a local MongoDB server or a cloud-based MongoDB Atlas server.
Here, we will use the local MongoDB server for the sake of simplicity.

db.config.js

//later we will serve 'mongodb_connection_url' from .env  
const { connect } = require("mongoose");
const connectDB = () => {
  try {
    connect('mongodb://127.0.0.1:27017/mydatabase').then((res) => {
      console.log("MongoDB connected successfully");
    });
  } catch (error) {
    console.error("MongoDB connection error:", error);
  }
};
module.exports = connectDB;

4.Import the db.config.js on the app.js file

const connectDB = require("./config/db.config");
connectDB()

5.Create a Mongoose Model:
Define a Mongoose model for transactions. Create a file named Transaction.model.js in your models directory:

//navigate to the root of the directory
mkdir models
cd models
echo > Transaction.model.js
  1. Define the Schema
const mongoose = require("mongoose");
// Define the Transaction schema
const transactionSchema = new mongoose.Schema(
  {product_id: {
      type: String, 
      required: true,
    },
    amount: {
      type: Number,
      required: true,
      min: 0, // Amount should not be negative
    },
    status: {
      type: String,
      required: true,
      enum: ["PENDING", "COMPLETE", "FAILED", "REFUNDED"], // Example statuses
      default: "PENDING",
    },
  },
  {
    timestamps: true, // Adds createdAt and updatedAt fields automatically
  }
);
// Create the Transaction model from the schema
const Transaction = mongoose.model("Transaction", transactionSchema);
// Export the model
module.exports = Transaction;

6.Save Transactions: In your payment route, save transaction details to MongoDB. Update the /initiate-payment route:

app.post("/initiate-payment", async (req, res) => {
  const { amount, productId } = req.body;
let paymentData = {
    amount,
    failure_url: process.env.FAILURE_URL,
    product_delivery_charge: "0",
    product_service_charge: "0",
    product_code: process.env.MERCHANT_ID,
    signed_field_names: "total_amount,transaction_uuid,product_code",
    success_url: process.env.SUCCESS_URL,
    tax_amount: "0",
    total_amount: amount,
    transaction_uuid: productId,
  };
  const data = `total_amount=${paymentData.total_amount},transaction_uuid=${paymentData.transaction_uuid},product_code=${paymentData.product_code}`;
  const signature = generateHmacSha256Hash(data, process.env.SECRET);
  paymentData = { ...paymentData, signature };
  try {
    const payment = await axios.post(process.env.ESEWAPAYMENT_URL, null, {
      params: paymentData,
    });
    const reqPayment = JSON.parse(safeStringify(payment));
    if (reqPayment.status === 200) {
      const transaction = new Transaction({
        product_id: productId,
        amount: amount,
      });
      await transaction.save();
      return res.send({
        url: reqPayment.request.res.responseUrl,
      });
    }
  } catch (error) {
    res.send(error);
  }
});

Verifying Responses from the Payment Gateway

It’s essential to verify the responses from the payment gateway to ensure that transactions are legitimate and to update your transaction records accordingly.

Step-by-Step:
1.Verify payment transaction (/payment-status): :
Implement routes to handle payment responses from eSewa.

// Route to handle payment status update
app.post("/payment-status", async (req, res) => {
  const { product_id } = req.body; // Extract data from request body
  try {
    // Find the transaction by its id
    const transaction = await Transaction.findOne({ product_id });
if (!transaction) {
      return res.status(400).json({ message: "Transaction not found" });
    }
    const paymentData = {
      product_code: process.env.MERCHANT_ID,
      total_amount: transaction.amount,
      transaction_uuid: transaction.product_id,
    };
    const response = await axios.get(
      process.env.ESEWAPAYMENT_STATUS_CHECK_URL,
      {
        params: paymentData,
      }
    );
    const paymentStatusCheck = JSON.parse(safeStringify(response));
    if (paymentStatusCheck.status === 200) {
      // Update the transaction status
      transaction.status = paymentStatusCheck.data.status;
      await transaction.save();
      res
        .status(200)
        .json({ message: "Transaction status updated successfully" });
    }
  } catch (error) {
    console.error("Error updating transaction status:", error);
    res.status(500).json({ message: "Server error", error: error.message });
  }
});

app.js file after update look like this:

const dotenv = require("dotenv");
const express = require("express");
const bodyParser = require("body-parser");
const axios = require("axios");
const cors = require("cors");
const { generateHmacSha256Hash, safeStringify } = require("./utils");
const Transaction = require("./model/TransactionModel");
const connectDB = require("./config/db.config");
// Load environment variables from .env file
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
connectDB();
const corsOptions = {
  origin: ["http://localhost:5173"],
};
app.use(cors(corsOptions));
// Middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Route to initiate payment
app.post("/initiate-payment", async (req, res) => {
  const { amount, productId } = req.body;
  let paymentData = {
    amount,
    failure_url: process.env.FAILURE_URL,
    product_delivery_charge: "0",
    product_service_charge: "0",
    product_code: process.env.MERCHANT_ID,
    signed_field_names: "total_amount,transaction_uuid,product_code",
    success_url: process.env.SUCCESS_URL,
    tax_amount: "0",
    total_amount: amount,
    transaction_uuid: productId,
  };
  const data = `total_amount=${paymentData.total_amount},transaction_uuid=${paymentData.transaction_uuid},product_code=${paymentData.product_code}`;
  const signature = generateHmacSha256Hash(data, process.env.SECRET);
  paymentData = { ...paymentData, signature };
  try {
    const payment = await axios.post(process.env.ESEWAPAYMENT_URL, null, {
      params: paymentData,
    });
    const reqPayment = JSON.parse(safeStringify(payment));
    if (reqPayment.status === 200) {
      const transaction = new Transaction({
        product_id: productId,
        amount: amount,
      });
      await transaction.save();
      return res.send({
        url: reqPayment.request.res.responseUrl,
      });
    }
  } catch (error) {
    res.send(error);
  }
});
// Route to handle payment status update
app.post("/payment-status", async (req, res) => {
  const { product_id } = req.body; // Extract data from request body
  try {
    // Find the transaction by its signature
    const transaction = await Transaction.findOne({ product_id });
    if (!transaction) {
      return res.status(400).json({ message: "Transaction not found" });
    }
    const paymentData = {
      product_code: "EPAYTEST",
      total_amount: transaction.amount,
      transaction_uuid: transaction.product_id,
    };
    const response = await axios.get(
      process.env.ESEWAPAYMENT_STATUS_CHECK_URL,
      {
        params: paymentData,
      }
    );
    const paymentStatusCheck = JSON.parse(safeStringify(response));
    if (paymentStatusCheck.status === 200) {
      // Update the transaction status
      transaction.status = paymentStatusCheck.data.status;
      await transaction.save();
      res
        .status(200)
        .json({ message: "Transaction status updated successfully" });
    }
  } catch (error) {
    console.error("Error updating transaction status:", error);
    res.status(500).json({ message: "Server error", error: error.message });
  }
});
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

Finally, lets update the Success.jsx page in the front-end to verify the payment status.

import React, { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import axios from "axios";
const Success = () => {
  const [isSuccess, setIsSuccess] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const navigate = useNavigate();
  const location = useLocation();
  // Create a new URLSearchParams object using the search string from location
  const queryParams = new URLSearchParams(location.search);
  const token = queryParams.get("data");
  // Decode the JWT without verifying the signature
  const decoded = base64Decode(token);
  const verifyPaymentAndUpdateStatus = async () => {
    try {
      const response = await axios.post(
        "http://localhost:3000/payment-status",
        {
          product_id: decoded.transaction_uuid,
        }
      );
      if (response.status === 200) {
        setIsLoading(false);
        setIsSuccess(true);
      }
    } catch (error) {
      setIsLoading(false);
      console.error("Error initiating payment:", error);
    }
  };
  useEffect(() => {
    verifyPaymentAndUpdateStatus();
  }, []);
  if (isLoading && !isSuccess) return <>Loading...</>;
  if (!isLoading && !isSuccess)
    return (
      <>
        <h1>Oops!..Error occurred on confirming payment</h1>
        <h2>We will resolve it soon.</h2>
        <button onClick={() => navigate("/")} className="go-home-button">
          Go to Homepage
        </button>
      </>
    );
  return (
    <div>
      <h1>Payment Successful!</h1>
      <p>Thank you for your payment. Your transaction was successful.</p>
      <button onClick={() => navigate("/")} className="go-home-button">
        Go to Homepage
      </button>
    </div>
  );
};
export default Success;
//using the utility fucntion in the same page for simplicity
//you can create a separate directory and serve it 
function base64Decode(base64) {
  // Convert Base64Url to standard Base64
  const standardBase64 = base64.replace(/-/g, "+").replace(/_/g, "/");
  // Decode Base64 to UTF-8 string
  const decoded = atob(standardBase64);
  return JSON.parse(decoded);
}

Conclusion

In this final part of our tutorial, we have ensured that our eSewa payment integration is complete and secure. By serving configuration files from .env, setting up MongoDB to store transactions, and verifying payment responses, we have laid a strong foundation for a reliable payment system in your MERN stack application.

Feel free to explore further enhancements and integrations, and as always, stay tuned for more tutorials. Happy coding!

0
Subscribe to my newsletter

Read articles from Samir Pokhrel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Samir Pokhrel
Samir Pokhrel

With a background in computer science and more than 2 years of hands-on experience in the field, I thrive on turning innovative ideas into reality through code. My journey in web development began when I discovered the limitless potential of the MERN stack. In addition to technical skills, I am a strong advocate for collaboration and effective communication. Outside of coding, I enjoy staying updated with the latest web development trends and technologies. I'm a lifelong learner who loves tackling new challenges and embracing emerging tools to enhance my skill set.