Accept Online Payments In Node.js Using Pesepay

Accept Online Payments In Node.js Using Pesepay

Given Ncube

So earlier this year I published a post on how to integrate Paynow in to your application using a simple API that I came up with. If you're a developer in Zimbabwe you know how much of a pain Paynow is.

The good news is, there is an alternative, Pesepay

What is Pesepay

Pesepay is another payment gateway in Zimbabwe just like Paynow and they brand themselves as a platform that provides "Robust & Secure online payments solution for Africa". What sets Pesepay apart from other solutions is that they provide a JSON REST API that you can actually use!

Can you imagine? An API that you and I and anyone can use?

Anyways, enough excitement, let's get start

A few things to note though

In this post we will focus on accepting ZWL and USD payments with VISA and Ecocash

Use case

I was recently consulted to add payments on an ecommerce website written in Node so I thought I could share how I implemented it

So let's say you just built your ecommerce backend, store or whatever you have everything else working except the payments.

Our example project is an express.js app with mongo db

Models

So first we need to structure our data to be able to accept payments for orders and such. I'm going to assume you know to structure an express application and mongoose

So first create a model called models/Order.js

in that file paste the following snippet

const mongoose = require('mongoose');

exports.Order = mongoose.model('Order', new mongoose.Schema({
    user: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
    },
    number: {
        type: String,
        required: true
    },
    items: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: 'OrderItem',
        required: true
    }],
    name: {
        type: String,
        required: true
    },
    email: {
        type: String
    },
    address: {
        type: String,
        required: true
    },
    address1: {
        type: String,
        default: ''
    },
    city: {
        type: String,
        required: true
    },
    country: {
        type: String,
        required: true
    },
    phone: {
        type: String,
        default: ''
    },
    status: {
        type: String,
        required: true,
        default: 'pending'
    },
    total: {
        type: Number,
        default: 0
    },
   payment: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Payment'
    }
}, {
    timestamps: true
}));

So this project was using CommonJS, I couldn't migrate the whole project to ES modules, but you can easily convert this model to es6 if you prefer.

So this model has things the name, country, phone, address, etc for shipping information

There's a relationship to

Let's add the OrderItem model, I'm sure you know how to create a model, no?

const mongoose = require('mongoose')

exports.OrderItem = mongoose.model('OrderItem', new mongoose.Schema({
    product: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Product',
        required: true
    },
    order: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Order',
        required: true
    },
    attributes: {
        type: Object,
        default: {}
    },
    unitPrice: {
        type: Number,
        required: true
    },
    amount: {
        type: Number,
        required: true
    },
    quantity: {
        type: Number,
        default: 1
    }
}, {
    timestamps: true
}));

So this one has the relationship to product which we have in our ecommerce app, the attributes like color, size, etc from the shopping cart

Finally let's add the payment model

const mongoose = require('mongoose');

exports.Payment = mongoose.model('Payment', new mongoose.Schema({
    user: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User'
    },
    order: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Order'
    },
    paid_amount: {
        type: Number,
    },
    amount: {
        type: Number,
        required: true
    },
    currency: {
        type: String,
        required: true
    },
    status: {
        type: String,
        default: 'pending'
    },
    method: {
        type: String,
        required: true
    },
    reference: {
        type: String,
        required: true
    }
},{
    timestapes: true
}));

Here's an explanation of what each field is for

So far so good? OK now let's allow the customer to make an order

Controllers and routes

So this is an express application, in your controllers folder create a file called OrderController.js and paste the following code

const { Order } = require('../models/Order')
const { OrderItem } = require('../models/OrderItem')
const Product = require('../models/Product')

exports.OrderController = new class {
    async store(req, res, next) {
        const { products, address, address2, city, country, phone, email, name } = req.body

        const order = await Order.create({
            user: req.user._id,
            address,
            address2,
            city,
            country,
            phone,
            email,
            name,
            number: 'ORDR-' + Math.floor(Math.random() * 100000)
        })

        await Promise.all(products.map(async item => {
            const product = await Product.findById(item.product_id)

            product.stock -= item.quantity
            product.orders.push(order._id)
            await product.save()

            const orderItem = new OrderItem({ 
                product: product._id,
                quantity: item.quantity,
                unitPrice: product.price,
                order: order._id,
                amount: item.quantity * product.price,
                attributes: item.attributes
            })

            await orderItem.save()
            order.items.push(orderItem)
            order.total += orderItem.amount
        }))

        await order.save()

        return res.status(201).json({
            status: 'success',
            message: 'Order created successfully',
            data: {
                order
            }
        })
    }

    async show(req, res, next) {
        const order = await Order.findById(req.params.id).populate('items')

        if (!order) {
            return res.status(404).json({
                status: 'error',
                message: 'Order not found'
            })
        }

        return res.status(200).json({
            status: 'success',
            data: {
                order
            }
        })
    }

    async index(req, res, next) {
        const orders = await Order.find({ user: req.user._id }).populate('items')

        return res.status(200).json({
            status: 'success',
            data: {
                orders
            }
        })
    }

    async update(req, res, next) {
        const order = await Order.findById(req.params.id)

        if (!order) {
            return res.status(404).json({
                status: 'error',
                message: 'Order not found'
            })
        }

        order.status = req.body.status

        await order.save()

        return res.status(200).json({
            status: 'success',
            message: 'Order updated successfully',
            data: {
                order
            }
        })
    }

    async destroy(req, res, next) {
        const order = await Order.findById(req.params.id)

        if (!order) {
            return res.status(404).json({
                status: 'error',
                message: 'Order not found'
            })
        }

        await order.remove()

        return res.status(200).json({
            status: 'success',
            message: 'Order deleted successfully'
        })
    }

    async destroyAll(req, res, next) {
        await Order.deleteMany({ user: req.user._id })

        return res.status(200).json({
            status: 'success',
            message: 'Orders deleted successfully'
        })
    }
}

Notice we export a class instance of the controller from this file, I'm not a fan of function based controllers, but hey, that's just me, if that's your thing you can simply rewrite this controller to your flavour

I'm also a fan of resourceful routing, (Look it up!) so this class has, store(),update(),destroy(),index(),show() methods which map to CRUD actions, so let's go over what each method deos

Index()

This returns a list of all orders for the logged in user

Show()

This returns a specific order whose id was passed as a parameter in the URL

Store()

Store persists the model in the database. In our case, we receive order info like name, email, etc and the products array which would come from the shopping cart

We store the Order in the database then loop through the products from the cart to create order items and associate them with the order

We save and return the recently created order with a 201 Created response

Destroy()

You may have guessed this one, it removes the model from the database

Next let's define the routes for orders, in your routes file, add the following snippet

router.get('/orders', auth, OrderController.index)
router.get('/orders/:order', auth, OrderController.show)
router.patch('/orders/:order', auth, OrderController.update)
router.delete('/orders/:order', auth, OrderController.destroy)
router.post('/orders', auth, OrderController.store)

Where auth is your authentication middleware

If you hit these routes you should be able to create an order, next let's add payments

Adding payments

The first is to head over to pesepay.com and create an account. After your account is create it will be in test mode with a sandbox account which you can use to test your payments

Next, in the dashboard, select My business from the sidebar and submit the required docs for verification, it takes less than an hour to verify it's really fast and easy

After you are done head over to 'Applications' and create a new application after it's done, copy the integration id and encryption key to your .env file

The next thing we want to do is to create a configuration file. If you handle your configuration differently you can go ahead and do you, but I found this method cleaner and easier to work with

create a file called config/index.js

in the file paste the following code

module.exports = {
    redis:{
        host: process.env.REDIS_HOST,
        port: process.env.REDIS_PORT ,
    },
    mongo: {
        url: process.env.MONGO_URL
    },
    pesepay: {
        integration_id: process.env.PESEPAY_INTEGRATION_ID,
        encryption_key: process.env.PESEPAY_ENCRYPTION_KEY,
        mode: process.env.PESEPAY_MODE || 'sandbox',
    },
}

Now when we want any configuration variable we simply

const { redis } = require('../config')


console.log(redis.host)

Next, we need a library to interact with API, from the documentation, there's an official library which you can install by

yarn add pesepay

However, when I tried it, it didn't give me the results that I wanted and it was last updated like a year or 2 ago. There's an unofficial package by Michael Nyamande called pesepay-js which I recommend you use

Install the package with

yarn add pesepay-js

After the package is installed let's initialize it. So in this app we might need to use pesepay a lot and doesn't make sense to keep initializing it everywhere. We will create a singleton that returns the same instance of the client throughout the app's lifetime

create a file called composables/index.js and paste the following code

const { pesepay } = require('../config')
const { PesePayClient } = require('pesepay-js');

module.exports = (() => {
    let instance = null;
    return {
        usePesePay: () => {
            if (!instance) {
                instance = new PesePayClient(pesepay.integration_id, pesepay.encryption_key)
            }
            return instance;
        }
    }
})()

So this returns an instance of PesePay client which is initialized with keys

All the basics are set, let's create the payment controller that will be used to make a payment

const { Order } = require('../models/Order')
const { Payment } = require('../models/Payment')
const { usePesePay } = require('../composables')

const paymentMethods = {
    ECOCASH: 'PZW201',
    VISA: 'PZW204',
    ECOCASH_USD: 'PZW211'
}

exports.PaymentController = new class {
    /**
     * Persist a payment in storage
     */
    async store(req, res, next) {
        const client = usePesePay()
        const order =  await Order.findOne({ _id: req.params.order })

        if (!order) {
            return res.status(404).json({ error: 'Order not found' })
        }

        const { currency, payment_details } = req.body

        const paymentMethodRequiredFields = paymentMethods[payment_details.method.toUpperCase()] === paymentMethods.ECOCASH ? {
            customerPhoneNumber: payment_details.phone
        } : {
            creditCardHolder: payment_details.card_holder,
            creditCardExpiryDate: payment_details.card_expiry,
            creditCardNumber: payment_details.card_number,
            creditCardSecurityNumber: payment_details.cvv
        }
        
        const response = await client.makeSeamlessPayment({
            amountDetails: {
                 amount: order.total,
                currencyCode: currency
            },
            merchantReference: order.number + '-' + Date.now(),
            reasonForPayment: 'Payment for order ' + order.number ,
            resultUrl: "resultUrl",
            returnUrl: "returnUrl",
            paymentMethodCode: paymentMethods[currency == 'USD' && payment_details.method.toUpperCase() == 'ECOCASH' ? 'ECOCASH_USD' : payment_details.method.toUpperCase()],
            customer: {
                phoneNumber: order.phone,
            },
            paymentMethodRequiredFields
        })
            .then(async response => {
                const _payment = new Payment({
                    order: order._id,
                    user: req.user._id,
                    method: payment_details.method,
                    amount: response.amountDetails.merchantAmount,
                    paid_amount: response.amountDetails.amount,
                    reference: response.referenceNumber || response.dateOfTransaction,
                    status: response.transactionStatus,
                    currency: response.amountDetails.currencyCode,
                })

                await _payment.save()
                order.status = 'paid'
                order.payment = _payment._id
                await order.save()

                return res.status(200).json({ message: 'Payment submitted for processing', data: { payment: _payment } })
            })
            .catch(error => {
                console.log(error)
                return res.status(400).json({ error: 'Payment failed' })
            });

        return response
    }

    /**
     * Show the payment
     * @param {reqeust} req 
     * @param {response} res 
     * @returns response
     */
    async show(req, res) {
        const payment = await Payment.findOne({ _id: req.params.payment })

        return res.status(200).json({
            data: { payment }
        })
    }
}

So here the client sends a payload like this

var data = JSON.stringify({
  "currency": "USD",
  "payment_details": {
    "phone": "0786801704",
    "method": "ECOCASH"
  }
});

If the payment method was a VISA card it would look something like this

{
    "currency": "ZWL", 
    "payment_details": {
        "phone": "0786801704",
        "method": "VISA",
        "card_number": "405405405405430",
        "cvv": "708",
        "card_holder": "G Ncube",
        "card_expiry": "03/26"
    }
}

So we get the pese pay client from the factory first, get the order the use wants to pay for from the request, set the required fields depending on the payment method provided

The information in the paymentMethods vairable can be obtained by calling getPaymentMethodsByCurrency(currency) on the pese pay client

We then call the makeSeamlessPayment method on the client to initiate a seamless payment, meaning the user doesn't need to leave our website or app to make a payment

That method returns a promise but we await it to get the final response.

In the then method we create a payment with the information recieved from the gateway.

Finally we return the created payment in the response

So what happened now is that the payment was submitted for processing upstream, this when the user is given a popup to enter PIN or VISA does it's magic with your bank to process the payment

This means we will have to check again later to see if the payment was successful or not, we will get to that shortly

Now let's add the routes for the payment controller

router.post('/orders/:order/payments', auth, PaymentController.store)
router.get('/orders/:order/payments/:payment', PaymentController.show)

Right now if you submit a request to this route using the above payloads you should have pending payment in your database

Polling for transaction status

In your frontend you might keep checking the payments show route to see if the status has changed. To change the status the idea is to use scheduled tasks

In express you have to do this from scratch using packages like bull, agenda toad-schedular, etc, In AdonisJs, Laravel, Rails and other frameworks Queue processing is a first class citizen

But hey, we developers after all. Let's add some poor man's queue processing

So the idea is that we have a job/task that runs every 20 seconds and checks for pending payments to see if they're successful or not

For this we will use a package called agenda

Install it with

yarn add @hokify/agenda

After it's intalled create a file called jobs/index.js and in that file paste the following code

//jobs/index.js
const {Agenda} = require('@hokify/agenda');
const { mongo } = require(__dirname + "/../config");
const { allDefinitions } = require("./definitions");

const agenda = new Agenda({
    db: { 
        address: mongo.url, 
        collection: 'agenda_jobs', 
        options: { useUnifiedTopology: true }, 
    },
    processEvery: "30 seconds",
    maxConcurrency: 20,
    defaultLockLifetime: 2000,
});


allDefinitions(agenda);
agenda.start()


agenda
    .on('ready', () => console.log("Agenda started!"))
    .on('error', (err) => console.log("Agenda connection error!", err));

module.exports = {agenda};

This simply initializes agenda, calls the all definitions method below to create agenda definitions and exports it.

Create another file called jobs/definitions/index.js and paste the following code

//jobs/definitions/index.js
const { pendingPayments } = require('./payments');

const definitions = [pendingPayments];

 const allDefinitions = (agenda) => {
  definitions.forEach((definition) => definition(agenda));
};

module.exports = { allDefinitions }

This goes through all defined jobs and register them with agenda

Create another one called jobs/definitions/payments.js and add the following code

const { JobHandlers } =  require("../handlers");

module.exports ={
    pendingPayments: (agenda) => {
        agenda.define('update-pending-payments', JobHandlers.PendingPayments)
    }
}

The actual job definition

Let's create another file for job handlers called jobs/handlers/index.js and paste the following code

//jobs/handlers/index.js
const PendingPayments = require('./PendingPayments')


const JobHandlers = {
    PendingPayments,
}

module.exports = { JobHandlers }

Let's create the job handler to update pending payments in a file called jobs/handlers/PendingPayments.js

const { usePesePay } = require("../../composables");
const { Payment } = require("../../models/Payment");

module.exports = async (job, done) => {
    const client = usePesePay();

    const payments = await Payment.find({ $or: [{ status: 'PENDING' }, { status: 'PROCESSING' }] })
    
    payments.forEach(async (payment) => {
        client.checkPaymentStatus(payment.reference).then(async (response) => {
        
            await Payment.updateOne({ _id: payment._id }, { status: response.transactionStatus })
        }).catch(error => {
            console.log(error)
        })
    })

    job.repeatAt('in 20 seconds')
    await job.save()
    done()
}

This is the acutal job handler which loops through the payment models which have a pending or processing status,

polls upstream for payment status by calling the checkPaymentStatus() method of the pesepay client, after getting a response we update the status from the gateway repeat again after 20 seconds

Now we need a schedular to to run this job

Create a file called jobs/schedular.js and add the following snippet

const { agenda } = require('./index')

const schedule = {
    updatePendingPayments: async (data) => {
        await agenda.now('update-pending-payments', data)
    },
// .... more methods that shedule tasks at the different intervals.
}

module.exports = { schedule }

Finally, let's run the schedular, from your index.js file or what your entry point is, add the following snippet

const { schedule } = require('./jobs/schedular')

schedule.updatePendingPayments()

So this will keep checking for pending payments and update the status. Now restart your server, create an order, and make a payment you should be able to the results

A note on sandbox accounts

So this example only works with verified accounts, ie, not in sandbox anymore. If you're account is still in sandbox you can't use this library because the base url for sandbox accounts is different, you may have to use the bare rest API for that. It's a bit tricky, I might write a separate tutorial on that, or simply make a PR to the package to add sandbox support

Useful links

In the meantime, if you have any questions, reach out to me on twitter at https://twitter.com/ncubegiven_

As always, Happy coding!

Lasting

Digital

Impressions

Subscribe to our newsletter

Subscribe to our newsletter to get the latest updates on our projects and services.

About

Flixtechs is a Laravel web development agency in Harare, Zimbabwe. We are a team of young and passionate developers who are always ready to help you build your dream website.

© 2023 Flixtechs. All rights reserved.