Beejartha

Categories
Node & React Js

Build an USSD Savings Account Application with NodeJS & Express

Introduction

Many of you or probably some, I guess, have been met in a situation where your mobile phone application is not operational or it’s experiencing some down time. Most probably when you want to do a certain transaction operation. And for some reason you get SMS notification from the company e.g. “Banks/Insurance company etc.” that goes like this;

Dear Customer, we wish to inform you that we shall be undertaking a system maintenance on our App platform. Services will be unavailable from a certain time to a certain. Mean-while continue to access our services through our USSD platform. Dial *000*0#. We regret the inconvenience caused.

Well, in this tutorial I am going to take you through how to build a simple Cash Saving USSD Application with NodeJS. Although, it’s a bit challenging to come up with a USSD Application, because mainly you have to have a great logic behind it, especially when navigating through the menu. So basically USSD is a technology that allows users to access services without the need of an internet.

So here are the steps;

Getting Started

First, we have to create our project folder using our terminal:

mkdir ussd-cashSaving
cd ussd-cashSaving

Assuming you are using VScode, enter the following command to open the project folder.

code .

Next, we create the package.json file.

npm init -y

Adding .gitignore file to hide dependencies in node_modules folder

# dependencies
node_modules
node_modules/# misc
.env

Then we install the libraries;

npm install –save express ussd-builder bcryptjs dotenv nodemon lodash mongoose colors morgan

Note: nodemon package allows you to reload the server each time our code is modified.

In the root directory of the app, create the following files

  • USSD-CASHSAVING
  • └─── node_modules
  • └─── _data
    • └─── users.json
  • └─── config
    • └─── db.js
  • └─── controllers
    • └─── users.js
  • └─── menu
    • └─── dashboard.js
    • └─── index.js
    • └─── register.js
    • └─── savings.js
    • └─── sendMoney.js
    • └─── settings.js
    • └─── statements.js
  • └─── middleware
    • └─── async.js
    • └─── auth.js
    • └─── errors.js
  • └─── models
    • └─── Users.js
  • └─── routes
    • └─── users.js
  • └─── utils
    • └─── errorResponse.js
    • └─── sessionHandler.js
    • └─── url.js
  • └─── package.json
  • └─── package-lock.json
  • └─── server.js
  • └─── .env

At package.json file rename “main”: “index.js” to “main”: “server.js”

{
“name”: “ussd-cashsaving”,
“version”: “1.0.0”,
“description”: “”,
“main”: “server.js”,
“scripts”: {
“test”: “echo \”Error: no test specified\” && exit 1″,
“dev”: “nodemon server”,
},
“keywords”: [],
“author”: “”,
“license”: “ISC”,
“dependencies”: {
“bcryptjs”: “^2.4.3”,
“colors”: “^1.4.0”,
“dotenv”: “^16.0.3”,
“express”: “^4.18.2”,
“lodash”: “^4.17.21”,
“mongoose”: “^6.8.4”,
“morgan”: “^1.10.0”,
“nodemon”: “^2.0.20”,
“ussd-builder”: “^1.2.5”
}
}

Add a .env file;

NODE_ENV=development
PORT=5000JWT_SECRET=
JWT_EXPIRE=
JWT_COOKIE_EXPIRE=X_SERVER_KEY=
MONGO_URI=

Creating our Database

At config/db.js add the following code

const mongoose = require(“mongoose”);const connectDB = async () => {
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline);
};
mongoose.set(“strictQuery”, false);module.exports = connectDB;

We’ll be creating our database on MongoDB. The code above is for connecting to our database through Mongoose.

Creating the Data Model

At models/User.js add the following code:

const crypto = require(“crypto”);
const mongoose = require(“mongoose”);
const bcrypt = require(“bcryptjs”);
const jwt = require(“jsonwebtoken”);const UserSchema = new mongoose.Schema(
{
Name: {
type: String,
required: true,
},
phone: {
type: String,
required: true,
},
pin: {
type: String,
required: true,
minlength: 4,
select: false,
},amount: {
required: true,
type: Number,
},
},
{
timestamps: true, // mongoose creates time fields (createdAt & updatedAt) automatically
}
);

// Encrypt pin using bcrypt
UserSchema.pre(“save”, async function (next) {
if (!this.isModified(“pin”)) {
next();
}

const salt = await bcrypt.genSalt(10);
this.pin = await bcrypt.hash(this.pin, salt);
});

// Match user entered pin to hashed pin in database
UserSchema.methods.matchPassword = async function (enteredPin) {
return await bcrypt.compare(enteredPin, this.pin);
};

module.exports = mongoose.model(“User”, UserSchema);

The code above is our mongoose Schema. Generally, what is done here is creating fields where it will be captured by our database i.e name, phone, pin & amount. Also note that there is a middleware which will be used to encrypt the user’s pin.

At server.js add the following code:

const path = require(“path”);
const express = require(“express”);
const colors = require(“colors”);
const dotenv = require(“dotenv”);
const morgan = require(“morgan”);
const _ = require(“lodash”);
const UssdMenu = require(“ussd-builder”);
const session = require(“./utils/sessionHandler”);
const { notFound, errorHandler } = require(“./middleware/error”);
const connectDB = require(“./config/db”);// Load env vars
dotenv.config();let menu = new UssdMenu();

// Connect to database
connectDB();

// Route files
const List = require(“./menu”);

// Initialize express
const app = express();

// Body-parser –responsible for parsing the incoming request bodies in a middleware before you handle it
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Dev logging middleware
if (process.env.NODE_ENV === “development”) {
app.use(morgan(“dev”));
}

// Set static folder
const publicDirPath = path.join(__dirname, “public”);
app.use(express.static(publicDirPath));

// File extensions
app.use(
express.static(publicDirPath, {
extensions: [“html”, “css”, “js”, “png”, “jpg”, “json”, “ico”],
})
);

// Registering USSD handler with Express
_.over([session, List])(menu);

app.post(“/”, async (req, res, next) => {
try {
await menu.run(req.body, (ussdResult) => {
res.send(ussdResult);

This is where you call all of your library files and run the application server.

User data

Paste the following code in your_data/user.json file:

[
{
“_id”: “5d7a514b5d2c12c7449be042”,
“Name”: “John Doe”,
“phone”: “254700000001”,
“pin”: 2541,
“amount”: 1000000
},
{
“_id”: “5d7a514b5d2c12c7449be043”,
“Name”: “Kevin Smith”,
“phone”: “254700000002”,
“pin”: 2542,
“amount”: 650000
},
{
“_id”: “5d7a514b5d2c12c7449be044”,
“Name”: “Sasha Ryan”,
“phone”: “254700000003”,
“pin”: 2543,
“amount”: 470000
}
]

Export data to MongoDB

Here you can add the json file above to mongoDb and also delete all.

node seeder -i // Send data to mongo-db
node seeder -d // Destroy all the data in mongo-db

Paste the following code in your seeder.js file:

const fs = require(“fs”);
const mongoose = require(“mongoose”);
const colors = require(“colors”);
const dotenv = require(“dotenv”);
const connectDB = require(“./config/db”);// Load env vars
dotenv.config();

// Load models
const User = require(“./models/User”);

// Connect to DB
connectDB();

// Read JSON files
const users = JSON.parse(
fs.readFileSync(`${__dirname}/_data/users.json`, “utf-8”)
);

// Import into DB
const importData = async () => {
try {
await User.create(users);
console.log(“Data Imported…”.green.inverse);
process.exit();
} catch (err) {
console.error(err);
}
};

// Delete data
const deleteData = async () => {
try {
await User.deleteMany();
console.log(“Data Destroyed…”.red.inverse);
process.exit();
} catch (err) {
console.error(err);
}
};

if (process.argv[2] === “-i”) {
importData();
} else if (process.argv[2] === “-d”) {
deleteData();
}

The code above allows you to add or delete the json payload to your mongo-db.

Controllers

Paste the following code in your controllers/users.js file:

const ErrorResponse = require(“../utils/errorResponse”);
const asyncHandler = require(“../middleware/async”);
const User = require(“../models/User”);// @desc Get single user
exports.getUser = async (phone) => {
try {
const user = await User.findOne({ phone: phone }).exec();
if (!user) return null;

return user;
} catch (error) {
return undefined;
}
};

// @desc Create user
exports.createUser = async (data) => {
try {
const user = await User.create(data);
Promise.resolve(user);
return user;
} catch (error) {
return error;
}
};

// @desc update Balance
exports.updateBalance = async (amount, phone) => {
try {
const user = await User.where(“phone”, phone).update({
$set: { amount: amount },
});

return user;
} catch (error) {
return error;
}
};

// @desc login
exports.login = async (pin, phone) => {
try {
const user = await User.findOne({ phone }).select(“+pin”).exec();
if (!user) return null;

// Check if pin matches
const isMatch = await user.matchPassword(pin);
if (!isMatch) return null;

sendTokenResponse(user, 200, res);
Promise.resolve(isMatch);
} catch (error) {
return error;
}
};

Here is where CRUD, (Create, Read, Update & Delete ) methods are

Creating USSD Menu

Here the users will be able to register an account to the cash saving App, enter the amount to his/her wallet and then have the ability to check the wallet balance and send cash to an existing user in the database.

We are going to use the ussd-builder library to create our menus.
Paste the following code in your Menu/index.js file:

const dotenv = require(“dotenv”);
const _ = require(“lodash”);
const { getUser } = require(“../controllers/users”);const dashboard = require(“./dashboard”);
const register = require(“./register”);

// Load env vars
dotenv.config();

module.exports = (menu) => {
// Define menu states
menu.startState({
run: async () => {
// use menu.con() to send response without terminating session

const { phoneNumber } = menu.args;
const user = await getUser(phoneNumber);

if (user) {
menu.con(
`Welcome back ${user.Name} to Cash savings:` +
“\nEnter your 4-digit PIN to continue:”
);
} else {
menu.con(“Welcome to Cash savings:” + “\n1. Get started” + “\n0. Exit”);
}
},
// next object links to next state based on user input
next: {
“*\\d{4}”: “dashboard”,
1: “register”,
0: “Exit”,
},

defaultNext: “invalidOption”,
});

menu.state(“Exit”, {
run: () => {
menu.end(“Goodbye :)”);
},
});

menu.state(“invalidOption”, {
run: () => {
menu.end(`Invalid option`);
},
});

menu.state(“IncorrectInput”, {
run: () => {
menu.end(`Incorrect input`);

The code above will check if there’s an already existing user in the database, if not, the user will be prompted to register his/her self. If the pin is accepted the user will be redirected to the dashboard menu.

Paste the following code in your Menu/dashboard.js file

const _ = require(“lodash”);
const { login } = require(“../controllers/users”);
const { session } = require(“../utils/sessionHandler”);const savings = require(“./savings”);
const sendMoney = require(“./sendMoney”);
const settings = require(“./settings”);
const statement = require(“./statement”);

module.exports = (menu) => {
// Define menu states

menu.state(“dashboard”, {
run: async () => {
const {
val,
args: { phoneNumber },
} = menu;
session[“newPin”] = val;

if (session.newPin) {
// Save the data to Mongo
await login(session.newPin, phoneNumber).then((res) => {
if (res) {
menu.con(
`Choose a service to proceed:` +
“\n1. Send Money” +
“\n2. Check savings” +
“\n3. View Mini-statement” +
“\n4. Change PIN” +
“\n0. Exit”
);
} else {
menu.go(“dashboard.WrongPin”);
}
});
} else {
menu.end(`invalid`);
}
},
// next object links to next state based on user input
next: {
1: “dashboard.sendMoney”,
2: “dashboard.savings”,
3: “dashboard.statement”,
4: “dashboard.settings”,
0: “Exit”,
},

defaultNext: “invalidOption”,
});

menu.state(“dashboard.WrongPin”, {

In the dashboard menu, the user will see a list of items

Paste the following code in your Menu/register.js file

const _ = require(“lodash”);
const { createUser } = require(“../controllers/users”);
const { session } = require(“../utils/sessionHandler”);

module.exports = (menu) => {
// Define menu states
menu.state(“register”, {
run: () => {
menu.con(“Enter your Name”);
},
// using regex to match user input to next state
next: {
“*[a-zA-Z]+”: “register.amount”,
},
});

menu.state(“register.amount”, {
run: async () => {
const { val } = menu;
session[“name”] = val;
menu.con(“Enter amount”);
},

// using regex to match user input to next state
next: {
“*\\d+”: “register.pin”,
},
});

menu.state(“register.pin”, {
run: async () => {
const { val } = menu;
session[“amount”] = val;
menu.con(“Enter your pin”);
},

// using regex to match user input to next state
next: {
“*\\d+”: “end”,
},
});

menu.state(“end”, {
run: async () => {
const {
val,
args: { phoneNumber },
} = menu;
session[“pin”] = val;

// Save the data to Mongo
await createUser({
Name: session.name,

Paste the following code in your Menu/savings.js file

const { getUser } = require(“../controllers/users”);module.exports = (menu) => {
// Define menu states
menu.state(“dashboard.savings”, {
run: async () => {
// use menu.con() to send response without terminating session

const { phoneNumber } = menu.args;
const user = await getUser(phoneNumber);

menu.con(`Your savings are: ${user.amount} \n0. Dashboard`);
},
// next object links to next state based on user input
next: {
0: “dashboard”,
},
});

return menu;
};

Paste the following code in your Menu/sendMoney.js file

const { getUser, updateBalance } = require(“../controllers/users”);
const { session } = require(“../utils/sessionHandler”);module.exports = (menu) => {
// Define menu states
menu.state(“dashboard.sendMoney”, {
run: async () => {
const { val } = menu;
menu.con(`Enter amount to send`);
},
// next object links to next state based on user input
next: {
“*\\d”: “dashboard.sendMoney.receiver”,
},
defaultNext: “IncorrectInput”,
});

menu.state(“dashboard.sendMoney.receiver”, {
run: async () => {
const {
val,
args: { phoneNumber },
} = menu;
session[“amount”] = val;
const user = await getUser(phoneNumber);
const enteredAmount = JSON.parse(val);

console.log(enteredAmount, user.amount);

if (val > user.amount) {
menu.end(“Sorry, you don’t have sufficient amount to send!”);
} else {
menu.con(`Enter phone number to send to`);
}
},
// next object links to next state based on user input
next: {
“*\\d{10}”: “dashboard.sendMoney.send”,
},
defaultNext: “IncorrectInput”,
});

menu.state(“dashboard.sendMoney.send”, {
run: async () => {
const {
val,
args: { phoneNumber },
} = menu;

const sender = await getUser(phoneNumber);
const reciever = await getUser(val);

if (reciever) {

Paste the following code in your Menu/settings.js file

const { changePin } = require(“../controllers/users”);
const { session } = require(“../utils/sessionHandler”);module.exports = (menu) => {
// Define menu states
menu.state(“dashboard.settings”, {
run: async () => {
menu.con(`Enter your 4-digit new PIN`);
},
// next object links to next state based on user input
next: {
“*\\d{4}”: “dashboard.settings.newPin”,
},
});

menu.state(“dashboard.settings.newPin”, {
run: async () => {
const { val } = menu;
session[“newPin”] = val;
menu.con(`Confirm your 4-digit new pin`);
},
// next object links to next state based on user input
next: {
“*\\d{4}”: “dashboard.settings.updatePin”,
},
defaultNext: “invalidOption”,
});

menu.state(“dashboard.settings.updatePin”, {
run: async () => {
const {
val,
args: { phoneNumber },
} = menu;

if (session.newPin === val) {
await changePin(val, phoneNumber);
menu.end(`You have successfully changed your PIN! `);
} else {
menu.end(`Your new PIN don’t match`);
}
},
});

menu.state(“invalidOption”, {
run: () => {
menu.end(`Invalid option`);
},
});

return menu;
};

Paste the following code in your Menu/statement.js file

module.exports = (menu) => {
// Define menu states
menu.state(“dashboard.statement”, {
run: async () => {
menu.end(
`Sorry, this service is unavailable. Please try again later.`
);
},
});return menu;
};

Add session Handler

This is where you can store temporary user data that persists through an entire session.
Paste the following code in your utils/sessionHandler.js file

const UssdMenu = require(“ussd-builder”);

let sessions = {};
let menu = new UssdMenu();

menu.sessionConfig({
start: (sessionId, callback) => {
// initialize current session if it doesn’t exist
// this is called by menu.run()
if (!(sessionId in sessions)) sessions[sessionId] = {};
callback();
},
end: (sessionId, callback) => {
// clear current session
// this is called by menu.end()
delete sessions[sessionId];
callback();
},
set: (sessionId, key, value, callback) => {
// store key-value pair in current session
sessions[sessionId][key] = value;
callback();
},
get: (sessionId, key, callback) => {
// retrieve value by key in current session
let value = sessions[sessionId][key];
callback(null, value);
},
});

module.exports = menu;

The code above is illustrated by the flow chart diagram below;

Create a folder called Public and add an html file inside

mkdir public
cd public
touch index.html

This is where you put static assets that can be viewed by everybody. Paste the following code to your index.html file

Testing the App

Go to your terminal and run

npm run dev

Your browser should open the project at http://localhost:5000/
Go to your postman application, create a new collection and add a new POST Request. Run the project on your local machine, then copy the url on your browser, paste the url on postman,
Add the following request body below then send the request to test your application.

{
“phoneNumber”: “254700000002”,
“sessionId”: “abcdef123456”,
“serviceCode”: “”,
“operator”: “”,
“text”: “”
}

Note: A string containing the input at each hop, is separated by the asterisk symbol (*). E.g

“text”:”1*2*3″

for menu navigation. This is parsed by the UssdMenu to find the appropriate state to run at each hop.

Credits

  • This tutorial is independently created and is not official Oracle Corporation documentation.
  • The content of this tutorial has been enriched by leveraging the insights and documentation available from Oracle Corporation. We extend our thanks to Oracle for their dedication to knowledge sharing. For official Oracle resources and additional information, please refer to www.oracle.com.

By Eric K

Experienced Software Developer with a demonstrated history of working in the computer software industry. Skilled in React.js, Vue.js, JavaScript and Node.js.

Any Query?

Ask Beejartha